diff --git a/.azure-pipelines/matrix.yml b/.azure-pipelines/matrix.yml deleted file mode 100644 index cc3fa7c170..0000000000 --- a/.azure-pipelines/matrix.yml +++ /dev/null @@ -1,72 +0,0 @@ -parameters: - os : ['ubuntu-latest'] - py_vers: ['3.8'] - test: ['tests/em', - 'tests/base tests/flow tests/seis tests/utils tests/meta', - 'tests/docs -s -v', - 'tests/examples/test_examples_1.py', - 'tests/examples/test_examples_2.py', - 'tests/examples/test_examples_3.py', - 'tests/examples/test_tutorials_1.py tests/examples/test_tutorials_2.py', - 'tests/examples/test_tutorials_3.py', - 'tests/pf', - 'tests/dask', # This must be ran on it's own to avoid modifying the code from any other tests. - ] - -jobs: - - ${{ each os in parameters.os }}: - - ${{ each py_vers in parameters.py_vers }}: - - ${{ each test in parameters.test }}: - - job: - displayName: ${{ os }}_${{ py_vers }}_${{ test }} - pool: - vmImage: ${{ os }} - timeoutInMinutes: 120 - steps: - - script: | - wget -O Mambaforge.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-$(uname)-$(uname -m).sh" - bash Mambaforge.sh -b -p "${HOME}/conda" - displayName: Install mamba - - - script: | - source "${HOME}/conda/etc/profile.d/conda.sh" - source "${HOME}/conda/etc/profile.d/mamba.sh" - echo " - python="${{ py_vers }} >> environment_test.yml - mamba env create -f environment_test.yml - conda activate simpeg-test - pip install pytest-azurepipelines - displayName: Create Anaconda testing environment - - - script: | - source "${HOME}/conda/etc/profile.d/conda.sh" - conda activate simpeg-test - pip install -e . - displayName: Build package - - - script: | - source "${HOME}/conda/etc/profile.d/conda.sh" - conda activate simpeg-test - export KMP_WARNINGS=0 - pytest ${{ test }} -v --cov-config=.coveragerc --cov=simpeg --cov-report=xml --cov-report=html -W ignore::DeprecationWarning - displayName: 'Testing ${{ test }}' - - - task: PublishPipelineArtifact@1 - inputs: - targetPath: $(Build.SourcesDirectory)/docs/_build/html - artifactName: html_docs - displayName: 'Publish documentation artifact' - condition: eq('${{ test }}', 'tests/docs -s -v') - - - script: | - curl -Os https://uploader.codecov.io/latest/linux/codecov - chmod +x codecov - ./codecov --sha $(system.pullRequest.sourceCommitId) - displayName: 'Upload PR coverage to codecov.io' - condition: and(eq(${{ py_vers }}, '3.8'), startsWith(variables['build.sourceBranch'], 'refs/pull/')) - - - script: | - curl -Os https://uploader.codecov.io/latest/linux/codecov - chmod +x codecov - ./codecov - displayName: 'Upload coverage to codecov.io' - condition: and(eq(${{ py_vers }}, '3.8'), not(startsWith(variables['build.sourceBranch'], 'refs/pull/'))) diff --git a/.ci/azure/codecov.yml b/.ci/azure/codecov.yml new file mode 100644 index 0000000000..47ee05929b --- /dev/null +++ b/.ci/azure/codecov.yml @@ -0,0 +1,36 @@ +jobs: + - job: + pool: + vmImage: "ubuntu-latest" + displayName: Upload to Codecov + steps: + # Checkout simpeg repo. Codecov needs the repo in the file system for + # uploading coverage reports. + - checkout: self + displayName: "Checkout repository" + + - task: DownloadPipelineArtifact@2 + inputs: + patterns: "coverage-*/coverage-*.xml" + displayName: "Download coverage artifacts" + + - bash: ls -la $(Pipeline.Workspace)/coverage-*/coverage-*.xml + displayName: "List downloaded coverage artifacts" + + - bash: | + cp $(Pipeline.Workspace)/coverage-*/coverage-*.xml . + ls -la + displayName: "Copy coverage files" + + - bash: | + curl -Os https://uploader.codecov.io/latest/linux/codecov + chmod +x codecov + displayName: "Install codecov cli" + + - bash: | + cc_file_args=() + for report in coverage-*.xml; do + cc_file_args+=( " --file " "$report" ) + done + ./codecov --verbose upload-process "$cc_file_args" + displayName: "Upload coverage to codecov.io" diff --git a/.ci/azure/deploy-dev-docs.sh b/.ci/azure/deploy-dev-docs.sh new file mode 100755 index 0000000000..8aa097618c --- /dev/null +++ b/.ci/azure/deploy-dev-docs.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +# Push built docs to dev branch in simpeg-docs repository + +set -ex #echo on and exit if any line fails + +# --------------------------- +# Push new docs to dev branch +# --------------------------- +# Capture hash of last commit in simpeg +commit=$(git rev-parse --short HEAD) + +# Clone the repo where we store the documentation (dev branch) +git clone -q --branch dev --depth 1 "https://${GH_TOKEN}@github.com/simpeg/simpeg-docs.git" +cd simpeg-docs + +# Remove all files (but .git folder) +find . -not -path "./.git/*" -not -path "./.git" -delete + +# Copy the built docs to the root of the repo +cp -r "$BUILD_SOURCESDIRECTORY"/docs/_build/html/. -t . + +# Add new files +git add . + +# List files in working directory and show git status +ls -la +git status + +# Commit the new docs. Amend to avoid having a very large history. +message="Azure CI deploy dev from ${commit}" +echo -e "\nAmending last commit:" +git commit --amend --reset-author -m "$message" + +# Make the push quiet just in case there is anything that could +# leak sensitive information. +echo -e "\nPushing changes to simpeg/simpeg-docs (dev branch)." +git push -fq origin dev 2>&1 >/dev/null +echo -e "\nFinished uploading doc files." + +# ---------------- +# Update submodule +# ---------------- +# Need to fetch the gh-pages branch first (because we clone with shallow depth) +git fetch --depth 1 origin gh-pages:gh-pages + +# Switch to the gh-pages branch +git switch gh-pages + +# Update the dev submodule +git submodule update --init --recursive --remote dev + +# Add updated submodule +git add dev + +# List files in working directory and show git status +ls -la +git status + +# Commit changes +message="Azure CI update dev submodule from ${commit}" +echo -e "\nMaking a new commit:" +git commit -m "$message" + +# Make the push quiet just in case there is anything that could +# leak sensitive information. +echo -e "\nPushing changes to simpeg/simpeg-docs (gh-pages branch)." +git push -q origin gh-pages 2>&1 >/dev/null +echo -e "\nFinished updating submodule dev." diff --git a/.ci/azure/deploy-release-docs.sh b/.ci/azure/deploy-release-docs.sh new file mode 100755 index 0000000000..2f1650af1c --- /dev/null +++ b/.ci/azure/deploy-release-docs.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +# Push built docs to gh-pages branch in simpeg-docs repository + +set -ex #echo on and exit if any line fails + +# Capture simpeg version +version=$(git tag --points-at HEAD) +if [[ -z $version ]]; then +echo "Version could not be obtained from tag. Exiting." +exit 1 +fi + +# Capture hash of last commit in simpeg +commit=$(git rev-parse --short HEAD) + +# Clone the repo where we store the documentation +git clone -q --branch gh-pages --depth 1 "https://${GH_TOKEN}@github.com/simpeg/simpeg-docs.git" +cd simpeg-docs + +# Move the built docs to a new dev folder +cp -r "$BUILD_SOURCESDIRECTORY/docs/_build/html" "$version" +cp "$BUILD_SOURCESDIRECTORY/docs/README.md" . + +# Add .nojekyll if missing +touch .nojekyll + +# Update latest symlink +rm -f latest +ln -s "$version" latest + +# Add new docs and relevant files +git add "$version" README.md .nojekyll latest + +# List files in working directory and show git status +ls -la +git status + +# Commit the new docs. +message="Azure CI deploy ${version} from ${commit}" +echo -e "\nMaking a new commit:" +git commit -m "$message" + +# Make the push quiet just in case there is anything that could +# leak sensitive information. +echo -e "\nPushing changes to simpeg/simpeg-docs." +git push -fq origin gh-pages 2>&1 >/dev/null +echo -e "\nFinished uploading generated files." diff --git a/.ci/azure/docs.yml b/.ci/azure/docs.yml new file mode 100644 index 0000000000..1c5a982e7b --- /dev/null +++ b/.ci/azure/docs.yml @@ -0,0 +1,112 @@ +jobs: + # Build docs only on scheduled jobs or on a relase + - job: BuildDocs + condition: or(eq(variables['Build.Reason'], 'Schedule'), startsWith(variables['build.sourceBranch'], 'refs/tags/')) + pool: + vmImage: ubuntu-latest + variables: + python.version: "3.11" + timeoutInMinutes: 240 + steps: + # Checkout simpeg repo. + # Sync tags and disable shallow depth to get the SimPEG version. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: Checkout repository (including tags) + + - bash: echo "##vso[task.prependpath]$CONDA/bin" + displayName: Add conda to PATH + + - bash: .ci/azure/setup_env.sh + displayName: Setup SimPEG environment + + - bash: | + source activate simpeg-test + make -C docs html + displayName: Building documentation + + - task: PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.SourcesDirectory)/docs/_build/html + artifactName: built-docs + displayName: "Upload docs as artifact" + + - job: DeployRelease + dependsOn: BuildDocs + condition: startsWith(variables['build.sourceBranch'], 'refs/tags/') + pool: + vmImage: ubuntu-latest + timeoutInMinutes: 240 + steps: + # Checkout simpeg repo. + # Sync tags and disable shallow depth to get the SimPEG version. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: Checkout repository (including tags) + + - bash: | + git config --global user.name ${GH_NAME} + git config --global user.email ${GH_EMAIL} + git config --list | grep user. + displayName: "Configure git" + env: + GH_NAME: $(gh.name) + GH_EMAIL: $(gh.email) + + - bash: | + mkdir -p docs/_build/html + displayName: "Create directory for built docs" + + - task: DownloadPipelineArtifact@2 + inputs: + artifact: built-docs + targetPath: docs/_build/html + displayName: "Download docs artifact" + + # Upload release build of the docs to gh-pages branch in simpeg/simpeg-docs + - bash: .ci/azure/deploy-release-docs.sh + displayName: Push documentation to simpeg-docs + env: + GH_TOKEN: $(gh.token) + + - job: DeployDev + dependsOn: BuildDocs + condition: eq(variables['Build.Reason'], 'Schedule') # run only scheduled triggers + pool: + vmImage: ubuntu-latest + timeoutInMinutes: 240 + steps: + # Checkout simpeg repo. + # Sync tags and disable shallow depth to get the SimPEG version. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: Checkout repository (including tags) + + - bash: | + git config --global user.name ${GH_NAME} + git config --global user.email ${GH_EMAIL} + git config --list | grep user. + displayName: "Configure git" + env: + GH_NAME: $(gh.name) + GH_EMAIL: $(gh.email) + + - bash: | + mkdir -p docs/_build/html + displayName: "Create directory for built docs" + + - task: DownloadPipelineArtifact@2 + inputs: + artifact: built-docs + targetPath: docs/_build/html + displayName: "Download docs artifact" + + # Upload dev build of the docs to a dev branch in simpeg/simpeg-docs + # and update submodule in the gh-pages branch + - bash: .ci/azure/deploy-dev-docs.sh + displayName: Push documentation to simpeg-docs (dev branch) + env: + GH_TOKEN: $(gh.token) diff --git a/.ci/azure/pypi.yml b/.ci/azure/pypi.yml new file mode 100644 index 0000000000..237f5c4b1d --- /dev/null +++ b/.ci/azure/pypi.yml @@ -0,0 +1,83 @@ +jobs: + - job: Build + pool: + vmImage: ubuntu-latest + steps: + # Checkout simpeg repo. + # Sync tags and disable shallow depth to get the SimPEG version. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: "Checkout repository (including tags)" + + - task: UsePythonVersion@0 + inputs: + versionSpec: "3.11" + displayName: "Setup Python" + + - bash: | + pip install build twine + displayName: "Install build dependencies" + + - bash: | + # Change setuptools-scm local_scheme to "no-local-version" so the + # local part of the version isn't included, making the version string + # compatible with Test PyPI. Only do this when building for TestPyPI. + sed --in-place 's/node-and-date/no-local-version/g' pyproject.toml + condition: not(startsWith(variables['build.sourceBranch'], 'refs/tags/')) + displayName: "Configure local_scheme (except on release)" + + - bash: | + python -m build --sdist . + displayName: "Create source distribution for simpeg" + + - bash: | + twine check dist/* + displayName: "Check the source distribution" + + - task: PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.SourcesDirectory)/dist + artifactName: pypi-dist + displayName: "Upload dist as artifact" + + - job: Deploy + dependsOn: Build + condition: or(startsWith(variables['build.sourceBranch'], 'refs/tags/'), eq(variables['Build.Reason'], 'Schedule')) + pool: + vmImage: ubuntu-latest + steps: + - checkout: none + + - task: DownloadPipelineArtifact@2 + inputs: + artifact: pypi-dist + targetPath: dist + displayName: "Download dist artifact" + + - task: UsePythonVersion@0 + inputs: + versionSpec: "3.11" + displayName: "Setup Python" + + - bash: | + pip install twine + displayName: "Install twine" + + # Push to TestPyPI (only on push to main) + - bash: | + twine upload --repository testpypi dist/* + displayName: "Upload to TestPyPI" + condition: eq(variables['Build.Reason'], 'Schedule') + env: + TWINE_USERNAME: $(twine.username) + TWINE_PASSWORD: $(test.twine.password) + + # Push to PyPI (only on release) + - bash: | + twine upload --skip-existing dist/* + displayName: "Upload to PyPI" + condition: startsWith(variables['build.sourceBranch'], 'refs/tags/') + env: + TWINE_USERNAME: $(twine.username) + TWINE_PASSWORD: $(twine.password) diff --git a/.ci/azure/run_tests_with_coverage.sh b/.ci/azure/run_tests_with_coverage.sh new file mode 100755 index 0000000000..a975c1daef --- /dev/null +++ b/.ci/azure/run_tests_with_coverage.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -x #echo on + +source activate simpeg-test +pytest $TEST_TARGET --cov --cov-config=pyproject.toml -v -W ignore::DeprecationWarning +pytest_retval=$? +coverage xml +exit $pytest_retval \ No newline at end of file diff --git a/.ci/azure/setup_env.sh b/.ci/azure/setup_env.sh new file mode 100755 index 0000000000..a23942d3d5 --- /dev/null +++ b/.ci/azure/setup_env.sh @@ -0,0 +1,40 @@ +#!/bin/bash +set -ex #echo on and exit if any line fails + +# TF_BUILD is set to True on azure pipelines. +is_azure=$(echo "${TF_BUILD:-false}" | tr '[:upper:]' '[:lower:]') + +if ${is_azure} +then + # Add conda-forge as channel + conda config --add channels conda-forge + # Remove defaults channels + # (both from ~/.condarc and from /usr/share/miniconda/.condarc) + conda config --remove channels defaults + conda config --remove channels defaults --system + # Update conda + conda update --yes -n base conda +fi + +cp .ci/environment_test.yml environment_test_with_pyversion.yml +echo " - python="$PYTHON_VERSION >> environment_test_with_pyversion.yml + +conda env create --file environment_test_with_pyversion.yml +rm environment_test_with_pyversion.yml + + +if ${is_azure} +then + source activate simpeg-test + pip install pytest-azurepipelines +else + conda activate simpeg-test +fi + +pip install --no-deps --editable . + +echo "Conda Environment:" +conda list + +echo "Installed SimPEG version:" +python -c "import simpeg; print(simpeg.__version__)" diff --git a/.ci/azure/style.yml b/.ci/azure/style.yml new file mode 100644 index 0000000000..59e6334475 --- /dev/null +++ b/.ci/azure/style.yml @@ -0,0 +1,41 @@ +jobs: + - job: + displayName: Run style checks with Black + pool: + vmImage: ubuntu-latest + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: "3.11" + - bash: .ci/install_style.sh + displayName: "Install dependencies to run the checks" + - script: make black + displayName: "Run black" + + - job: + displayName: Run (permissive) style checks with flake8 + pool: + vmImage: ubuntu-latest + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: "3.11" + - bash: .ci/install_style.sh + displayName: "Install dependencies to run the checks" + - script: make flake + displayName: "Run flake8" + + - job: + displayName: Run style checks with flake8 (allowed to fail) + pool: + vmImage: ubuntu-latest + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: "3.11" + - bash: .ci/install_style.sh + displayName: "Install dependencies to run the checks" + - script: make flake-all + displayName: "Run flake8" + env: + FLAKE8_OPTS: "--exit-zero" diff --git a/.ci/azure/test.yml b/.ci/azure/test.yml new file mode 100644 index 0000000000..403a4d69f6 --- /dev/null +++ b/.ci/azure/test.yml @@ -0,0 +1,68 @@ +parameters: + os : ['ubuntu-latest'] + py_vers: ['3.11'] + test: ['tests/em', + 'tests/base tests/flow tests/seis tests/utils', + 'tests/meta', + 'tests/docs -s -v', + 'tests/examples/test_examples_1.py', + 'tests/examples/test_examples_2.py', + 'tests/examples/test_examples_3.py', + 'tests/examples/test_tutorials_1.py tests/examples/test_tutorials_2.py', + 'tests/examples/test_tutorials_3.py', + 'tests/pf', + 'tests/dask', # This code must be tested on its own to avoid modifying the implementation for any other tests. + ] + +jobs: + - ${{ each os in parameters.os }}: + - ${{ each py_vers in parameters.py_vers }}: + - ${{ each test in parameters.test }}: + - job: + displayName: ${{ os }}_${{ py_vers }}_${{ test }} + pool: + vmImage: ${{ os }} + timeoutInMinutes: 120 + variables: + python.version: ${{ py_vers }} + test.target: ${{ test }} + steps: + + # Checkout simpeg repo, including tags. + # We need to sync tags and disable shallow depth in order to get the + # SimPEG version while building the docs. + - checkout: self + fetchDepth: 0 + fetchTags: true + displayName: Checkout repository (including tags) + + - bash: echo "##vso[task.prependpath]$CONDA/bin" + displayName: Add conda to PATH + + - bash: .ci/azure/setup_env.sh + displayName: Setup SimPEG environment + + - bash: .ci/azure/run_tests_with_coverage.sh + displayName: 'Testing ${{ test }}' + + - task: PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.SourcesDirectory)/docs/_build/html + artifactName: html_docs + displayName: 'Publish documentation artifact' + condition: and(eq('${{ test }}', 'tests/docs -s -v'), succeededOrFailed()) + + - bash: | + job="${{ os }}_${{ py_vers }}_${{ test }}" + jobhash=$(echo $job | sha256sum | cut -f 1 -d " " | cut -c 1-7) + cp coverage.xml "coverage-$jobhash.xml" + echo "##vso[task.setvariable variable=jobhash]$jobhash" + condition: succeededOrFailed() + displayName: 'Rename coverage report' + + - task: PublishPipelineArtifact@1 + inputs: + targetPath: $(Build.SourcesDirectory)/coverage-$(jobhash).xml + artifactName: coverage-$(jobhash) + condition: succeededOrFailed() + displayName: 'Publish coverage artifact' diff --git a/.ci/environment_test.yml b/.ci/environment_test.yml new file mode 100644 index 0000000000..e2f488baa1 --- /dev/null +++ b/.ci/environment_test.yml @@ -0,0 +1,49 @@ +name: simpeg-test +channels: + - conda-forge +dependencies: + - numpy>=1.22 + - scipy>=1.12 + - pymatsolver>=0.3 + - matplotlib-base + - discretize>=0.12 + - geoana>=0.7 + - libdlf + - typing_extensions + +# optional dependencies + - dask + - zarr + - fsspec>=0.3.3 + - choclo>=0.3.0 + - scooby + - plotly + - scikit-learn>=1.2 + - pandas + +# documentation building + - sphinx + - sphinx-gallery>=0.1.13 + - sphinxcontrib-apidoc + - sphinx-reredirects + - sphinx-design + - pydata-sphinx-theme + - nbsphinx + - numpydoc + - pillow + - empymod>=2.0.0 + - sympy + - memory_profiler + - python-kaleido + - h5py + +# testing + - pytest + - pytest-cov + +# Testing environment doesn't need style checkers + +# PyPI uploading + - wheel + - twine + - python-build diff --git a/.ci/install_style.sh b/.ci/install_style.sh new file mode 100755 index 0000000000..b26211ad95 --- /dev/null +++ b/.ci/install_style.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -ex #echo on and exit if any line fails + +# get directory of this script +script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +style_script=$script_dir/parse_style_requirements.py + +# parse the style requirements +requirements=$(python $style_script) + +pip install $requirements + diff --git a/.ci/parse_style_requirements.py b/.ci/parse_style_requirements.py new file mode 100644 index 0000000000..8183280b78 --- /dev/null +++ b/.ci/parse_style_requirements.py @@ -0,0 +1,12 @@ +import tomllib +import pathlib + +root_dir = pathlib.Path(__file__).parent.parent.resolve() +pyproject_file = root_dir / "pyproject.toml" + +with open(pyproject_file, "rb") as f: + pyproject = tomllib.load(f) + +style_requirements = pyproject["project"]["optional-dependencies"]["style"] +for req in style_requirements: + print(req) diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index a79c8987f3..0000000000 --- a/.coveragerc +++ /dev/null @@ -1,4 +0,0 @@ -[run] -source = simpeg -omit = - */setup.py diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 14d1e5f416..0000000000 --- a/.flake8 +++ /dev/null @@ -1,125 +0,0 @@ -# ----------------------------- -# Configuration file for flake8 -# ----------------------------- - -# Configure flake8 -# ---------------- -[flake8] -extend-ignore = - # Default ignores by flake (added here for when ignore gets overwritten) - E121,E123,E126,E226,E24,E704,W503,W504, - # Too many leading '#' for block comment - E266, - # Line too long (82 > 79 characters) - E501, - # Do not use variables named 'I', 'O', or 'l' - E741, - # Line break before binary operator (conflicts with black) - W503, - # Ignore spaces before a colon (Black handles it) - E203, -exclude = - .git, - __pycache__, - .ipynb_checkpoints, - setup.py, - docs/conf.py, - docs/_build/, -per-file-ignores = - # disable unused-imports errors on __init__.py - __init__.py: F401 -exclude-from-doctest = - # Don't check style in docstring of test functions - tests -# Define flake rules that will be ignored for now. Every time a new warning is -# solved througout the entire project, it should be removed to this list. -ignore = - # assertRaises(Exception): should be considered evil - B017, - # Missing docstring in public module - D100, - # Missing docstring in public class - D101, - # Missing docstring in public method - D102, - # Missing docstring in public function - D103, - # Missing docstring in public package - D104, - # Missing docstring in magic method - D105, - # Missing docstring in __init__ - D107, - # One-line docstring should fit on one line with quotes - D200, - # No blank lines allowed before function docstring - D201, - # No blank lines allowed after function docstring - D202, - # 1 blank line required between summary line and description - D205, - # Docstring is over-indented - D208, - # Multi-line docstring closing quotes should be on a separate line - D209, - # No whitespaces allowed surrounding docstring text - D210, - # No blank lines allowed before class docstring - D211, - # Use """triple double quotes""" - D300, - # First line should end with a period - D400, - # First line should be in imperative mood; try rephrasing - D401, - # First line should not be the function's "signature" - D402, - # First word of the first line should be properly capitalized - D403, - # No blank lines allowed between a section header and its content - D412, - # Section has no content - D414, - # Docstring is empty - D419, - # module level import not at top of file - E402, - # undefined name %r - F821, - # Block quote ends without a blank line; unexpected unindent. - RST201, - # Definition list ends without a blank line; unexpected unindent. - RST203, - # Field list ends without a blank line; unexpected unindent. - RST206, - # Inline strong start-string without end-string. - RST210, - # Title underline too short. - RST212, - # Inline emphasis start-string without end-string. - RST213, - # Inline interpreted text or phrase reference start-string without end-string. - RST215, - # Inline substitution_reference start-string without end-string. - RST219, - # Unexpected indentation. - RST301, - # Unknown directive type "*". - RST303, - # Unknown interpreted text role "*". - RST304, - # Error in "*" directive: - RST307, - # Previously unseen severe error, not yet assigned a unique code. - RST499, - - -# Configure flake8-rst-docstrings -# ------------------------------- -# Add some roles used in our docstrings -rst-roles = - class, - func, - mod, - meth, - ref, diff --git a/.git_archival.txt b/.git_archival.txt new file mode 100644 index 0000000000..b1a286bbb6 --- /dev/null +++ b/.git_archival.txt @@ -0,0 +1,4 @@ +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ +ref-names: $Format:%D$ \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..82bf71c1c5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.git_archival.txt export-subst \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 130f534ba2..4887a6e685 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -1,7 +1,7 @@ name: Bug report description: Report a bug in SimPEG. title: "BUG: " -labels: [Bug] +labels: ["bug"] body: - type: markdown @@ -46,6 +46,7 @@ body: description: > Please include the output from `simpeg.Report()` to describe your system for us. Paste the output from `from simpeg import Report; print(Report())` below. + render: shell validations: required: true diff --git a/.github/ISSUE_TEMPLATE/maintenance.md b/.github/ISSUE_TEMPLATE/maintenance.md new file mode 100644 index 0000000000..ffbb7cd4a2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/maintenance.md @@ -0,0 +1,18 @@ +--- +name: Maintenance +about: "Maintainers only: Issues for maintenance tasks" +title: "MNT: " +labels: "maintenance" +assignees: "" +--- + +**Description of the maintenance task** + + diff --git a/.github/ISSUE_TEMPLATE/post-install.yml b/.github/ISSUE_TEMPLATE/post-install.yml index 10b970aebf..5a86f2f0b1 100644 --- a/.github/ISSUE_TEMPLATE/post-install.yml +++ b/.github/ISSUE_TEMPLATE/post-install.yml @@ -9,7 +9,8 @@ body: label: "Steps to reproduce:" description: > Please describe the installation method (e.g. building from source, - Anaconda, pip), your OS and SimPEG, NumPy, Scipy, and Python version information. + Miniforge, Anaconda, pip), your OS and SimPEG, NumPy, Scipy, and Python + version information. placeholder: | If running in a conda environment you could paste the output of `conda list` here. validations: diff --git a/.github/ISSUE_TEMPLATE/release-checklist.md b/.github/ISSUE_TEMPLATE/release-checklist.md index d173424945..33a3c64690 100644 --- a/.github/ISSUE_TEMPLATE/release-checklist.md +++ b/.github/ISSUE_TEMPLATE/release-checklist.md @@ -6,8 +6,6 @@ labels: "maintenance" assignees: "" --- - - **Target date:** YYYY/MM/DD ## Generate release notes @@ -28,9 +26,7 @@ assignees: "" grep -Eo "@[[:alnum:]-]+" notes.rst | sort -u | sed -E 's/^/* /' ``` Paste the list into the file under a new `Contributors` category. -- [ ] Check if every contributor that participated in the release is in the - list. Generate a list of authors and co-authors from the git log with (update - the `last_release`): +- [ ] Check if every contributor that participated in the release is in the list. Generate a list of authors and co-authors from the git log with (update the `last_release`): ```bash export last_release="v0.20.0" git shortlog HEAD...$last_release -sne > contributors @@ -41,15 +37,11 @@ assignees: "" ```bash sed -Ei 's/@([[:alnum:]-]+)/`@\1 `__/' notes.rst ``` -- [ ] Copy the content of `notes.rst` to a new file - `docs/content/release/-notes.rst`. -- [ ] Edit the release notes file, following the template below and the - previous release notes. +- [ ] Copy the content of `notes.rst` to a new file `docs/content/release/-notes.rst`. +- [ ] Edit the release notes file, following the template below and the previous release notes. - [ ] Add the new release notes to the list in `docs/content/release/index.rst`. -- [ ] Open a PR with the new release notes. -- [ ] Manually view the built documentation by downloading the Azure `html_doc` - artifact and check for formatting and errors. -- [ ] Merge that PR +- [ ] **Open a PR** with the new release notes. +- [ ] Manually view the built documentation by downloading the Azure `html_doc` artifact and check for formatting and errors.
@@ -111,41 +103,49 @@ Pull Requests
+### Add new version to version switcher + +Edit the `docs/_static/versions.json` file and: + +- [ ] Add an entry for the new version (below dev, above the current _latest_). +- [ ] Make sure to set the new the version number in every field in the new entry. +- [ ] Mark the new version as the `latest`, and remove `latest` from the previous one. +- [ ] Set the new version as the `preferred` one. +- [ ] Remove the `preferred` line from the older version. +- [ ] Run `cat docs/_static/versions.json | python -m json.tool > /dev/null` to check if the syntax of the JSON file is correct. If no errors are prompted, then your file is OK. +- [ ] Double-check the changes. +- [ ] Commit the changes to the same branch. + +### Merge the PR + +- [ ] **Merge that PR.** + ## Make the new release -- [ ] Draft a new GitHub Release +- [ ] Draft a new GitHub Release. - [ ] Create a new tag for it (the version number with a leading `v`). -- [ ] Target the release on `main` or on a particular commit from `main` +- [ ] Target the release on `main` or on a particular commit from `main`. - [ ] Generate release notes automatically. -- [ ] Publish the release +- [ ] Publish the release. ## Extra tasks -After publishing the release, Azure will automatically push the new version to -PyPI, and build and deploy the docs. You can check the progress of these tasks -in: https://dev.azure.com/simpeg/simpeg/_build +After publishing the release, Azure will automatically push the new version to PyPI, and build and deploy the docs. You can check the progress of these tasks in: https://dev.azure.com/simpeg/simpeg/_build . After they finish: -- [ ] Check the new version is available in PyPI: https://pypi.org/project/SimPEG/ +- [ ] Check the new version is available in PyPI: https://pypi.org/project/SimPEG/ . - [ ] Check the new documentation is online: https://docs.simpeg.xyz -For the new version to be available in conda-forge, we need to update the -[conda-forge/simpeg-feedstock](https://github.com/conda-forge/simpeg-feedstock) -repository. Within the same day of the release a new PR will be automatically -open in that repository. So: +For the new version to be available in conda-forge, we need to update the [conda-forge/simpeg-feedstock](https://github.com/conda-forge/simpeg-feedstock) repository. Within the same day of the release a new PR will be automatically open in that repository. So: - [ ] Follow the steps provided in the checklist in that PR and merge it. - [ ] Make sure the new version is available through conda-forge: https://anaconda.org/conda-forge/simpeg -Lastly, we would need to update the SimPEG version used in -[`simpeg/user-tutorials`](https://github.com/simpeg/user-tutorials) and rerun -its notebooks: +Lastly, we would need to update the SimPEG version used in [`simpeg/user-tutorials`](https://github.com/simpeg/user-tutorials) and rerun its notebooks: -- [ ] Open issue in - [`simpeg/user-tutorials`](https://github.com/simpeg/user-tutorials) for - rerunning the notebooks using the new released version of SimPEG +- [ ] Open issue in [`simpeg/user-tutorials`](https://github.com/simpeg/user-tutorials) for rerunning the notebooks using the new released version of SimPEG. Finally: -- [ ] Close this issue +- [ ] Close this issue. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c6b74e7c19..cd17e1eb9a 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -18,10 +18,10 @@ Feel free to remove lines from this template that do not apply to you pull reque #### PR Checklist * [ ] If this is a work in progress PR, set as a Draft PR -* [ ] Linted my code according to the [style guides](https://docs.simpeg.xyz/content/getting_started/contributing/code-style.html). -* [ ] Added [tests](https://docs.simpeg.xyz/content/getting_started/practices.html#testing) to verify changes to the code. +* [ ] Linted my code according to the [style guides](https://docs.simpeg.xyz/latest/content/getting_started/contributing/code-style.html). +* [ ] Added [tests](https://docs.simpeg.xyz/latest/content/getting_started/contributing/testing.html) to verify changes to the code. * [ ] Added necessary documentation to any new functions/classes following the - expect [style](https://docs.simpeg.xyz/content/getting_started/practices.html#documentation). + expect [style](https://docs.simpeg.xyz/latest/content/getting_started/contributing/documentation.html). * [ ] Marked as ready for review (if this is was a draft PR), and converted to a Pull Request * [ ] Tagged ``@simpeg/simpeg-developers`` when ready for review. diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 0151372c8d..3052116a35 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,13 +1,54 @@ name : Reviewdog PR Annotations -on: [pull_request_target] + +# ========= +# IMPORTANT +# ========= +# +# TL;DR: +# THIS ACTION SHOULD NOT RUN ANY CODE FROM THE PR BRANCH. +# +# This action is triggered after new events in Pull Requests (as the +# `pull_request` trigger does), but this ones provides the workflow writing +# permissions to the repo. +# The second checkout step in each job checks out code from the PR branch. +# Code from any PR branch should be treated as malicious! +# Therefore, these action **SHOULD NOT RUN ANY CODE** from the PR branch since +# the workflow has writting permisions. +# Doing so introduces a high severity vulnerability that could be exploited to +# gain access to secrets and/or introduce malicious code. +# +# For this particular workflow we need the writting permission in order for +# reviewdog to publish comments in the PR. +# zizmor will complain about the `pull_request_target` trigger, so we will +# ignore it. +# +# Worth noting that the runner will execute the steps specified in the version +# of this workflow file that lives in the **target branch** (usually `main`), +# not the one in the Pull Request branch. This means that even if a contributor +# opens a PR with a change to this file, the change won't be executed. This is +# intended to prevent third-party contributors from running custom code with high +# privileges on the repo. +# +# References: +# * https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ +# * PR that added this action: https://github.com/simpeg/simpeg/pull/1424 +# * PR that added this warning: https://github.com/simpeg/simpeg/pull/1592 +# +on: [pull_request_target] # zizmor: ignore[dangerous-triggers] jobs: flake8: runs-on: ubuntu-latest name: Flake8 check + permissions: + contents: read + pull-requests: write + steps: - name: Checkout target repository source uses: actions/checkout@v4 + with: + persist-credentials: false - name: Setup Python env uses: actions/setup-python@v5 @@ -15,13 +56,16 @@ jobs: python-version: '3.11' - name: Install dependencies to run the flake8 checks - run: pip install -r requirements_style.txt + run: .ci/install_style.sh + # Checkout PR branch. + # TREAT THIS CODE AS MALICIOUS, DON'T RUN CODE FROM THIS BRANCH. - name: checkout pull request source uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} path: pr_source + persist-credentials: false - name: flake8 review uses: reviewdog/action-flake8@v3 @@ -33,9 +77,15 @@ jobs: black: name: Black check runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: - name: Checkout target repository source uses: actions/checkout@v4 + with: + persist-credentials: false - name: Setup Python env uses: actions/setup-python@v5 @@ -43,16 +93,19 @@ jobs: python-version: '3.11' - name: Install dependencies to run the black checks - run: pip install -r requirements_style.txt + run: .ci/install_style.sh + # Checkout PR branch. + # TREAT THIS CODE AS MALICIOUS, DON'T RUN CODE FROM THIS BRANCH. - name: checkout pull request source uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} path: 'pr_source' + persist-credentials: false - uses: reviewdog/action-black@v3 with: workdir: 'pr_source' github_token: ${{ secrets.GITHUB_TOKEN }} - reporter: github-pr-review \ No newline at end of file + reporter: github-pr-review diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000000..ca489ee9e1 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,38 @@ +# Lint GitHub Actions for common security issues using zizmor. +# Docs: https://woodruffw.github.io/zizmor + +name: Lint GitHub Actions + +on: + pull_request: + push: + branches: + - main + +permissions: {} + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install zizmor + run: python -m pip install zizmor + + - name: List installed packages + run: python -m pip freeze + + - name: Lint GitHub Actions + run: make check-actions + env: + # Set GH_TOKEN to allow zizmor to check online vulnerabilities + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 0000000000..b506949a16 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,17 @@ +# Zizmor configuration +# -------------------- +# +# This file configures zizmor. This is not a workflow that gets run in GitHub +# Actions. +# +# References: https://woodruffw.github.io/zizmor/configuration + +rules: + unpinned-uses: + config: + policies: + # Mimic default behaviour: official actions can get pinned by tag. + actions/*: ref-pin + # Allow to use tags to pin reviewdog actions. + reviewdog/action-black: ref-pin + reviewdog/action-flake8: ref-pin diff --git a/.gitignore b/.gitignore index c274ffb66f..aaddeda18b 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ pip-log.txt # Unit test / coverage reports .coverage +coverage_html_report/* .tox nosetests.xml @@ -46,17 +47,19 @@ docs/_build/ docs/warnings.txt docs/content/api/generated/* .DS_Store -docs/content/examples/* -docs/content/tutorials/* +docs/content/user-guide/examples/* +docs/content/user-guide/tutorials/* docs/modules/* docs/sg_execution_times.rst .vscode/* # paths to where data are downloaded +examples/04-dcip/test_url/* examples/20-published/bookpurnong_inversion/* examples/20-published/._bookpurnong_inversion examples/20-published/*.tar.gz examples/20-published/*.png +examples/20-published/Chile_GRAV_4_Miller/* tutorials/03-gravity/gravity/* tutorials/03-gravity/outputs/* tutorials/04-magnetics/magnetics/* diff --git a/.mailmap b/.mailmap index 26757e1035..7bdb3acc4f 100644 --- a/.mailmap +++ b/.mailmap @@ -23,25 +23,81 @@ Seogi Kang Seogi Kang Dom Fournier +Dom Fournier domfournier Gudni Karl Rosenkjaer Gudni Karl Rosenkjaer Gudni Karl Rosenkjaer -Thibaut Astic -Thibaut Astic + +Thibaut Astic +Thibaut Astic +Thibaut Astic +Thibaut Astic +Thibaut Astic <97514898+thibaut-kobold@users.noreply.github.com> +Thibaut Astic Devin Cowan Dave Marchant Michael Mitchell +Michael Mitchell Lars Ruthotto Dieter Werthmüller Dieter Werthmüller +Dieter Werthmüller Eldad Haber Brendan Smithyman + +Xiaolong Wei <56940558+xiaolongw1223@users.noreply.github.com> + +Ying Hu <64567062+YingHuuu@users.noreply.github.com> + +Richard Scott <72196131+RichardScottOZ@users.noreply.github.com> + +Nick Williams <86257151+nwilliams-kobold@users.noreply.github.com> + +Fernando Perez + +Adam Kosík + +Andrea Balza Morales + +Craig Miller + +Colton Kohnke +Colton Kohnke + +Franklin Koch + +Jacob Edman + +Joseph Capriotti +Joseph Capriotti + +John Kuttai + +John Weiss +John Weiss <49649694+johnweis0480@users.noreply.github.com> + +Kalen Martens + +Luz Angelica Caudillo Mata + +Neil Godber +Neil Godber + +Santiago Soler + +Sarah Devriese + +Zheng-Kai Ye + +Zhuo Liu <20036557+Zhuoliulz@users.noreply.github.com> + +Mike Wathen \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ccb608b94b..52c3fc8c2d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,18 @@ repos: - - repo: https://github.com/psf/black + - repo: https://github.com/psf/black-pre-commit-mirror rev: 24.3.0 hooks: - id: black - language_version: python3 + language_version: python3.11 - repo: https://github.com/pycqa/flake8 rev: 7.0.0 hooks: - id: flake8 - language_version: python3 + language_version: python3.11 additional_dependencies: - flake8-bugbear==23.12.2 - flake8-builtins==2.2.0 - flake8-mutable==1.2.0 - flake8-rst-docstrings==0.3.0 - flake8-docstrings==1.7.0 + - flake8-pyproject==1.2.3 diff --git a/CITATION.rst b/CITATION.rst index b0b2a76ff7..dcd90a96f4 100644 --- a/CITATION.rst +++ b/CITATION.rst @@ -1,13 +1,17 @@ +============= Citing SimPEG -------------- +============= -There is a `paper about SimPEG `_, if you use this code, please help our scientific visibility by citing our work! +There is a paper about SimPEG! If you are using this library in your research, +we greatly appreciate that the software gets cited! - Cockett, R., Kang, S., Heagy, L. J., Pidlisecky, A., & Oldenburg, D. W. (2015). SimPEG: An open source framework for simulation and gradient based parameter estimation in geophysical applications. Computers & Geosciences. + Cockett, R., Kang, S., Heagy, L. J., Pidlisecky, A., & Oldenburg, D. W. + (2015). SimPEG: An open source framework for simulation and gradient based + parameter estimation in geophysical applications. Computers & Geosciences. -BibTex: +Here is a Bibtex entry to make things easier if you’re using Latex: .. code:: @@ -19,3 +23,24 @@ BibTex: publisher={Elsevier} } +Electromagnetics +---------------- + +If you are using the :mod:`simpeg.electromagnetics` module, please also cite: + + Lindsey J. Heagy, Rowan Cockett, Seogi Kang, Gudni K. Rosenkjaer, Douglas W. Oldenburg, A framework for simulation and inversion in electromagnetics, Computers & Geosciences, Volume 107, 2017, Pages 1-19, ISSN 0098-3004, http://dx.doi.org/10.1016/j.cageo.2017.06.018. + +Here is a Bibtex entry to make things easier if you’re using Latex: + +.. code:: + + @article{heagy2017, + title={A framework for simulation and inversion in electromagnetics}, + author={Lindsey J. Heagy and Rowan Cockett and Seogi Kang and Gudni K. Rosenkjaer and Douglas W. Oldenburg}, + journal={Computers & Geosciences}, + volume={107}, + pages={1 - 19}, + year={2017}, + issn={0098-3004}, + doi={http://dx.doi.org/10.1016/j.cageo.2017.06.018} + } diff --git a/MANIFEST.in b/MANIFEST.in index 7c04fa62b2..b61f50d054 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,9 @@ prune .github -prune .azure-pipelines +prune .ci prune docs prune examples prune tutorials prune tests exclude .coveragerc .flake8 .gitignore MANIFEST.in .pre-commit-config.yaml exclude azure-pipelines.yml .mailmap -exclude environment_test.yml -exclude requirements.txt requirements_dev.txt requirements_dask.txt requirements_style.txt \ No newline at end of file +exclude .git_archival.txt .gitattributes Makefile environment.yml \ No newline at end of file diff --git a/Makefile b/Makefile index 20c18b456c..2ede57730f 100644 --- a/Makefile +++ b/Makefile @@ -1,32 +1,18 @@ STYLE_CHECK_FILES = simpeg examples tutorials tests +GITHUB_ACTIONS=.github/workflows -.PHONY: help build coverage lint graphs tests docs check black flake +.PHONY: help docs clean check black flake flake-all check-actions help: @echo "Commands:" @echo "" - @echo " check run code style and quality checks (black and flake8)" - @echo " black checks code style with black" - @echo " flake checks code style with flake8" - @echo " flake-all checks code style with flake8 (full set of rules)" + @echo " check run code style and quality checks (black and flake8)" + @echo " black checks code style with black" + @echo " flake checks code style with flake8" + @echo " flake-all checks code style with flake8 (full set of rules)" + @echo " check-actions lint GitHub Actions workflows (with zizmor)" @echo "" -build: - python setup.py build_ext --inplace - -coverage: - nosetests --logging-level=INFO --with-coverage --cover-package=simpeg --cover-html - open cover/index.html - -lint: - pylint --output-format=html simpeg> pylint.html - -graphs: - pyreverse -my -A -o pdf -p simpeg simpeg/**.py simpeg/**/**.py - -tests: - nosetests --logging-level=INFO - docs: cd docs;make html @@ -47,3 +33,6 @@ flake: flake-all: flake8 --version flake8 ${FLAKE8_OPTS} --ignore "" ${STYLE_CHECK_FILES} + +check-actions: + zizmor ${GITHUB_ACTIONS} diff --git a/README.rst b/README.rst index 54041d24e3..8875b3a6c5 100644 --- a/README.rst +++ b/README.rst @@ -48,7 +48,7 @@ The vision is to create a package for finite volume simulation with applications You are welcome to join our forum and engage with people who use and develop SimPEG at: https://simpeg.discourse.group/. -Weekly meetings are open to all. They are generally held on Wednesdays at 10:30am PDT. Please see the calendar (`GCAL `_, `ICAL `_) for information on the next meeting. +Weekly meetings are open to all. They are generally held on Wednesdays at 3:00 PM Pacific Time. Please see the calendar (`GCAL `_, `ICAL `_) for information on the next meeting. Overview Video ============== @@ -120,9 +120,8 @@ Meetings SimPEG hosts weekly meetings for users to interact with each other, for developers to discuss upcoming changes to the code base, and for discussing topics related to geophysics in general. -Currently our meetings are held every Wednesday, alternating between -a mornings (10:30 am pacific time) and afternoons (3:00 pm pacific time) -on even numbered Wednesdays. Find more info on our +Currently our meetings are held every Wednesday at 3:00 PM Pacific Time. +Find more info on our `Mattermost `_. diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f7f7ed21a9..93a3ad717c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,144 +1,63 @@ +# ============================================= +# Configure Azure Pipelines for automated tasks +# ============================================= + +# Define triggers for runs in CI +# ------------------------------ trigger: branches: include: - - 'main' + - "main" exclude: - - '*no-ci*' + - "*no-ci*" tags: include: - - '*' + - "*" pr: branches: include: - - '*' + - "*" exclude: - - '*no-ci*' + - "*no-ci*" + +schedules: + - cron: "0 8 * * *" # trigger cron job every day at 08:00 AM GMT + displayName: "Scheduled nightly job" + branches: + include: ["main"] + always: false # don't run if no changes have been applied since last sucessful run + batch: false # dont' run if last pipeline is still in-progress + +# Run stages +# ---------- stages: - -- stage: StyleChecks - displayName: "Style Checks" - jobs: - - job: - displayName: Run style checks with Black - pool: - vmImage: ubuntu-latest - variables: - python.version: '3.8' - steps: - - script: | - pip install -r requirements_style.txt - displayName: "Install dependencies to run the checks" - - script: make black - displayName: "Run black" - - - job: - displayName: Run (permissive) style checks with flake8 - pool: - vmImage: ubuntu-latest - variables: - python.version: '3.8' - steps: - - script: | - pip install -r requirements_style.txt - displayName: "Install dependencies to run the checks" - - script: make flake - displayName: "Run flake8" - - - job: - displayName: Run style checks with flake8 (allowed to fail) - pool: - vmImage: ubuntu-latest - variables: - python.version: '3.8' - steps: - - script: | - pip install -r requirements_style.txt - displayName: "Install dependencies to run the checks" - - script: FLAKE8_OPTS="--exit-zero" make flake-all - displayName: "Run flake8" - -- stage: Testing - dependsOn: StyleChecks - jobs: - - template: ./.azure-pipelines/matrix.yml - -- stage: Deploy - dependsOn: Testing - condition: startsWith(variables['build.sourceBranch'], 'refs/tags/') - jobs: - - job: - displayName: Deploy Docs and source - pool: - vmImage: ubuntu-latest - variables: - python.version: '3.8' - timeoutInMinutes: 240 - steps: - - script: | - git config --global user.name ${GH_NAME} - git config --global user.email ${GH_EMAIL} - git config --list | grep user. - displayName: 'Configure git' - env: - GH_NAME: $(gh.name) - GH_EMAIL: $(gh.email) - - - script: | - wget -O Mambaforge.sh "https://github.com/conda-forge/miniforge/releases/latest/download/Mambaforge-$(uname)-$(uname -m).sh" - bash Mambaforge.sh -b -p "${HOME}/conda" - displayName: Install mamba - - - script: | - source "${HOME}/conda/etc/profile.d/conda.sh" - source "${HOME}/conda/etc/profile.d/mamba.sh" - cp environment_test.yml environment_test_with_pyversion.yml - echo " - python="$(python.version) >> environment_test_with_pyversion.yml - mamba env create -f environment_test_with_pyversion.yml - rm environment_test_with_pyversion.yml - conda activate simpeg-test - pip install pytest-azurepipelines - displayName: Create Anaconda testing environment - - - script: | - source "${HOME}/conda/etc/profile.d/conda.sh" - conda activate simpeg-test - pip install -e . - displayName: Build package - - - script: | - source "${HOME}/conda/etc/profile.d/conda.sh" - conda activate simpeg-test - python setup.py sdist - twine upload --skip-existing dist/* - displayName: Deploy source - env: - TWINE_USERNAME: $(twine.username) - TWINE_PASSWORD: $(twine.password) - - - script: | - source "${HOME}/conda/etc/profile.d/conda.sh" - conda activate simpeg-test - export KMP_WARNINGS=0 - cd docs - make html - cd .. - displayName: Building documentation - - # upload documentation to simpeg-docs gh-pages on tags - - script: | - git clone --depth 1 https://${GH_TOKEN}@github.com/simpeg/simpeg-docs.git - cd simpeg-docs - git gc --prune=now - git remote prune origin - rm -rf * - cp -r $BUILD_SOURCESDIRECTORY/docs/_build/html/* . - cp $BUILD_SOURCESDIRECTORY/docs/README.md . - touch .nojekyll - echo "docs.simpeg.xyz" >> CNAME - git add . - git commit -am "Azure CI commit ref $(Build.SourceVersion)" - git push - displayName: Push documentation to simpeg-docs - env: - GH_TOKEN: $(gh.token) \ No newline at end of file + - stage: StyleChecks + displayName: "Style Checks" + jobs: + - template: .ci/azure/style.yml + + - stage: Testing + dependsOn: StyleChecks + jobs: + - template: .ci/azure/test.yml + + - stage: Codecov + dependsOn: Testing + condition: succeededOrFailed() + jobs: + - template: .ci/azure/codecov.yml + + - stage: Docs + dependsOn: + - StyleChecks + - Testing + jobs: + - template: .ci/azure/docs.yml + + - stage: PyPI + dependsOn: + - StyleChecks + - Testing + jobs: + - template: .ci/azure/pypi.yml diff --git a/docs/Makefile b/docs/Makefile index 3751f949e1..cf1d326cfb 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -43,8 +43,8 @@ help: clean: rm -rf $(BUILDDIR)/* rm -rf content/api/generated/ - rm -rf content/examples/ - rm -rf content/tutorials/ + rm -rf content/user-guide/examples/ + rm -rf content/user-guide/tutorials/ html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html -j auto diff --git a/docs/_static/versions.json b/docs/_static/versions.json new file mode 100644 index 0000000000..c5a34bd9c5 --- /dev/null +++ b/docs/_static/versions.json @@ -0,0 +1,62 @@ +[ + { + "version": "dev", + "url": "https://docs.simpeg.xyz/dev/" + }, + { + "name": "v0.25.0 (latest)", + "version": "0.25.0", + "url": "https://docs.simpeg.xyz/v0.25.0/", + "preferred": true + }, + { + "name": "v0.24.0", + "version": "0.24.0", + "url": "https://docs.simpeg.xyz/v0.24.0/" + }, + { + "name": "v0.23.0", + "version": "0.23.0", + "url": "https://docs.simpeg.xyz/v0.23.0/" + }, + { + "name": "v0.22.2", + "version": "0.22.2", + "url": "https://docs.simpeg.xyz/v0.22.2/" + }, + { + "name": "v0.22.1", + "version": "0.22.1", + "url": "https://docs.simpeg.xyz/v0.22.1/" + }, + { + "name": "v0.22.0", + "version": "0.22.0", + "url": "https://docs.simpeg.xyz/v0.22.0/" + }, + { + "name": "v0.21.1", + "version": "0.21.1", + "url": "https://docs.simpeg.xyz/v0.21.1/" + }, + { + "version": "0.21.0", + "url": "https://docs.simpeg.xyz/v0.21.0/" + }, + { + "version": "0.20.0", + "url": "https://docs.simpeg.xyz/v0.20.0/" + }, + { + "version": "0.19.0", + "url": "https://docs.simpeg.xyz/v0.19.0/" + }, + { + "version": "0.18.1", + "url": "https://docs.simpeg.xyz/v0.18.1/" + }, + { + "version": "0.18.0", + "url": "https://docs.simpeg.xyz/v0.18.0/" + } +] diff --git a/docs/conf.py b/docs/conf.py index dde265d6d5..f5a72fcf30 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,12 +10,13 @@ # # All configuration values have a default; values that are commented out # serve to show the default. - +from pathlib import Path import sys import os from sphinx_gallery.sorting import FileNameSortKey import glob import simpeg +from packaging.version import parse import plotly.io as pio from importlib.metadata import version @@ -43,12 +44,13 @@ "sphinx.ext.coverage", "sphinx.ext.doctest", "sphinx.ext.extlinks", + "sphinx_design", "sphinx.ext.intersphinx", "sphinx.ext.mathjax", "sphinx_gallery.gen_gallery", "sphinx.ext.todo", - "sphinx.ext.linkcode", "matplotlib.sphinxext.plot_directive", + "sphinx_reredirects", ] # Autosummary pages will be generated by sphinx-autogen instead of sphinx-build @@ -98,8 +100,8 @@ linkcheck_ignore = [ r"https://github.com/simpeg/simpeg*", - "/content/examples/*", - "/content/tutorials/*", + "/content/user-guide/examples/*", + "/content/user-guide/tutorials/*", r"https://www.pardiso-project.org", r"https://docs.github.com/*", # GJI refuses the connexion during the check @@ -135,17 +137,24 @@ # edit_on_github_branch = "main/docs" # check_meta = False -# source code links + +# ----------------- +# Source code links +# ----------------- +# Function inspired in matplotlib's configuration + link_github = True -# You can build old with link_github = False if link_github: import inspect - from os.path import relpath, dirname + from packaging.version import parse extensions.append("sphinx.ext.linkcode") def linkcode_resolve(domain, info): + """ + Determine the URL corresponding to Python object + """ if domain != "py": return None @@ -160,44 +169,45 @@ def linkcode_resolve(domain, info): for part in fullname.split("."): try: obj = getattr(obj, part) - except Exception: + except AttributeError: return None - try: - unwrap = inspect.unwrap - except AttributeError: - pass - else: - obj = unwrap(obj) - + if inspect.isfunction(obj): + obj = inspect.unwrap(obj) try: fn = inspect.getsourcefile(obj) - except Exception: + except TypeError: fn = None + if not fn or fn.endswith("__init__.py"): + try: + fn = inspect.getsourcefile(sys.modules[obj.__module__]) + except (TypeError, AttributeError, KeyError): + fn = None if not fn: return None try: source, lineno = inspect.getsourcelines(obj) - except Exception: + except (OSError, TypeError): lineno = None if lineno: - linespec = "#L%d-L%d" % (lineno, lineno + len(source) - 1) + linespec = f"#L{lineno:d}-L{lineno + len(source) - 1:d}" else: linespec = "" try: - fn = relpath(fn, start=dirname(simpeg.__file__)) + fn = os.path.relpath(fn, start=os.path.dirname(simpeg.__file__)) except ValueError: return None - return f"https://github.com/simpeg/simpeg/blob/main/simpeg/{fn}{linespec}" + simpeg_version = parse(simpeg.__version__) + tag = "main" if simpeg_version.is_devrelease else f"v{simpeg_version.public}" + return f"https://github.com/simpeg/simpeg/blob/{tag}/simpeg/{fn}{linespec}" else: extensions.append("sphinx.ext.viewcode") - # Make numpydoc to generate plots for example sections numpydoc_use_plots = True plot_pre_code = """ @@ -237,62 +247,74 @@ def linkcode_resolve(domain, info): dict(name="Contact", url="https://mattermost.softwareunderground.org/simpeg"), ] -try: - import pydata_sphinx_theme - - html_theme = "pydata_sphinx_theme" - - # If false, no module index is generated. - html_use_modindex = True - - html_theme_options = { - "external_links": external_links, - "icon_links": [ - { - "name": "GitHub", - "url": "https://github.com/simpeg/simpeg", - "icon": "fab fa-github", - }, - { - "name": "Mattermost", - "url": "https://mattermost.softwareunderground.org/simpeg", - "icon": "fas fa-comment", - }, - { - "name": "Discourse", - "url": "https://simpeg.discourse.group/", - "icon": "fab fa-discourse", - }, - { - "name": "Youtube", - "url": "https://www.youtube.com/c/geoscixyz", - "icon": "fab fa-youtube", - }, - ], - "use_edit_page_button": False, - "collapse_navigation": True, - "analytics": { - "plausible_analytics_domain": "docs.simpeg.xyz", - "plausible_analytics_url": "https://plausible.io/js/script.js", +# Define SimPEG version for the version switcher +simpeg_version = parse(simpeg.__version__) +if simpeg_version.is_devrelease: + switcher_version = "dev" +else: + switcher_version = simpeg_version.public + +# Use Pydata Sphinx theme +html_theme = "pydata_sphinx_theme" + +# If false, no module index is generated. +html_use_modindex = True + +html_theme_options = { + "external_links": external_links, + "icon_links": [ + { + "name": "GitHub", + "url": "https://github.com/simpeg/simpeg", + "icon": "fab fa-github", }, - "navbar_align": "left", # make elements closer to logo on the left - } - html_logo = "images/simpeg-logo.png" + { + "name": "Mattermost", + "url": "https://mattermost.softwareunderground.org/simpeg", + "icon": "fas fa-comment", + }, + { + "name": "Discourse", + "url": "https://simpeg.discourse.group/", + "icon": "fab fa-discourse", + }, + { + "name": "Youtube", + "url": "https://www.youtube.com/c/geoscixyz", + "icon": "fab fa-youtube", + }, + ], + "use_edit_page_button": False, + "collapse_navigation": True, + "analytics": { + "plausible_analytics_domain": "docs.simpeg.xyz", + "plausible_analytics_url": "https://plausible.io/js/script.js", + }, + "navbar_align": "left", # make elements closer to logo on the left + "navbar_end": ["version-switcher", "theme-switcher", "navbar-icon-links"], + # Configure version switcher + "switcher": { + "version_match": switcher_version, + "json_url": "https://docs.simpeg.xyz/latest/_static/versions.json", + }, + "show_version_warning_banner": True, + "navigation_with_keys": True, +} - html_static_path = ["_static"] +html_logo = "images/simpeg-logo.png" - html_css_files = [ - "css/custom.css", - ] +html_static_path = ["_static"] - html_context = { - "github_user": "simpeg", - "github_repo": "simpeg", - "github_version": "main", - "doc_path": "docs", - } -except Exception: - html_theme = "default" +html_css_files = [ + "css/custom.css", +] + +html_context = { + "github_user": "simpeg", + "github_repo": "simpeg", + "github_version": "main", + "doc_path": "docs", +} # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -445,24 +467,20 @@ def linkcode_resolve(domain, info): ] tutorial_dirs = glob.glob("../tutorials/[!_]*") -tut_gallery_dirs = ["content/tutorials/" + os.path.basename(f) for f in tutorial_dirs] +tut_gallery_dirs = [ + "content/user-guide/tutorials/" + os.path.basename(f) for f in tutorial_dirs +] # Scaping images to generate on website from plotly.io._sg_scraper import plotly_sg_scraper -import pyvista - -# Make sure off screen is set to true when building locally -pyvista.OFF_SCREEN = True -# necessary when building the sphinx gallery -pyvista.BUILDING_GALLERY = True -image_scrapers = ("matplotlib", plotly_sg_scraper, pyvista.Scraper()) +image_scrapers = ("matplotlib", plotly_sg_scraper) # Sphinx Gallery sphinx_gallery_conf = { # path to your examples scripts "examples_dirs": ["../examples"] + tutorial_dirs, - "gallery_dirs": ["content/examples"] + tut_gallery_dirs, + "gallery_dirs": ["content/user-guide/examples"] + tut_gallery_dirs, "within_subsection_order": FileNameSortKey, "filename_pattern": "\.py", "backreferences_dir": "content/api/generated/backreferences", @@ -487,6 +505,8 @@ def linkcode_resolve(domain, info): autodoc_member_order = "bysource" +# Don't show type hints in signatures +autodoc_typehints = "none" # def supress_nonlocal_image_warn(): # import sphinx.environment @@ -516,3 +536,51 @@ def linkcode_resolve(domain, info): ("py:class", "builtins.complex"), ("py:meth", "__call__"), ] + + +# Configure redirects +# ------------------- +# Redirect some pages to support old links +OLD_FILES_FNAME = Path(__file__).parent.resolve() / "old-docs-files.txt" +MAPS = { + "content/tutorials": "content/user-guide/tutorials", + "content/examples": "content/user-guide/examples", + "content/getting_started": "content/user-guide/getting-started", +} +IGNORE = ["content/getting_started/index.html", "content/user_guide.html"] + + +def _get_source_target(old_fname: str) -> tuple[str, str]: + for old_dir, new_dir in MAPS.items(): + if old_fname.startswith(old_dir): + source = old_fname.removesuffix(".html") + n_parents = len([p for p in Path(old_fname).parents if p != Path(".")]) + target = "../" * n_parents + old_fname.replace(old_dir, new_dir, 1) + return source, target + raise ValueError() + + +def build_redirects(): + """ + Build redirects dictionary for sphinx-reredirects. + """ + redirects = {} + with OLD_FILES_FNAME.open(mode="r") as f: + for line in f: + if line.startswith("#"): + continue + old_fname = line.strip() + if old_fname in IGNORE: + continue + source, target = _get_source_target(old_fname) + redirects[source] = target + return redirects + + +redirects = build_redirects() +redirects.update( + { + "content/getting_started/index": "../../content/user-guide/index.html", + "content/user_guide": "../content/user-guide/index.html", + } +) diff --git a/docs/content/api/SimPEG.directives.rst b/docs/content/api/SimPEG.directives.rst deleted file mode 100644 index 35999d49d0..0000000000 --- a/docs/content/api/SimPEG.directives.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.directives diff --git a/docs/content/api/SimPEG.electromagnetics.base.rst b/docs/content/api/SimPEG.electromagnetics.base.rst deleted file mode 100644 index c32e9227a1..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.base.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.frequency_domain.rst b/docs/content/api/SimPEG.electromagnetics.frequency_domain.rst deleted file mode 100644 index dc5de2199b..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.frequency_domain.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.frequency_domain \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.natural_source.rst b/docs/content/api/SimPEG.electromagnetics.natural_source.rst deleted file mode 100644 index 5e276c525b..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.natural_source.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.natural_source \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.rst b/docs/content/api/SimPEG.electromagnetics.rst deleted file mode 100644 index b465abd441..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.rst +++ /dev/null @@ -1,27 +0,0 @@ -========================= -Electromagnetics -========================= - -Things about electromagnetics - -.. toctree:: - :maxdepth: 2 - - SimPEG.electromagnetics.static.induced_polarization - SimPEG.electromagnetics.static.resistivity - SimPEG.electromagnetics.static.spectral_induced_polarization - SimPEG.electromagnetics.static.self_potential - SimPEG.electromagnetics.frequency_domain - SimPEG.electromagnetics.natural_source - SimPEG.electromagnetics.time_domain - SimPEG.electromagnetics.viscous_remanent_magnetization - -Electromagnetics Utilities --------------------------- - -.. toctree:: - :maxdepth: 2 - - SimPEG.electromagnetics.static.utils - SimPEG.electromagnetics.utils - SimPEG.electromagnetics.base diff --git a/docs/content/api/SimPEG.electromagnetics.static.induced_polarization.rst b/docs/content/api/SimPEG.electromagnetics.static.induced_polarization.rst deleted file mode 100644 index 94b7fdedd8..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.static.induced_polarization.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.static.induced_polarization \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.static.resistivity.rst b/docs/content/api/SimPEG.electromagnetics.static.resistivity.rst deleted file mode 100644 index f93b976667..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.static.resistivity.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.static.resistivity diff --git a/docs/content/api/SimPEG.electromagnetics.static.self_potential.rst b/docs/content/api/SimPEG.electromagnetics.static.self_potential.rst deleted file mode 100644 index ae8d5f2859..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.static.self_potential.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.static.self_potential diff --git a/docs/content/api/SimPEG.electromagnetics.static.spectral_induced_polarization.rst b/docs/content/api/SimPEG.electromagnetics.static.spectral_induced_polarization.rst deleted file mode 100644 index c02a3ec010..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.static.spectral_induced_polarization.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.static.spectral_induced_polarization \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.static.spontaneous_potential.rst b/docs/content/api/SimPEG.electromagnetics.static.spontaneous_potential.rst deleted file mode 100644 index d5d02e8ff2..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.static.spontaneous_potential.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.static.spontaneous_potential diff --git a/docs/content/api/SimPEG.electromagnetics.static.utils.rst b/docs/content/api/SimPEG.electromagnetics.static.utils.rst deleted file mode 100644 index 0cb346c648..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.static.utils.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.static.utils \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.time_domain.rst b/docs/content/api/SimPEG.electromagnetics.time_domain.rst deleted file mode 100644 index d93f52fce3..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.time_domain.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.time_domain \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.utils.rst b/docs/content/api/SimPEG.electromagnetics.utils.rst deleted file mode 100644 index eef7ebc5c5..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.utils.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.utils \ No newline at end of file diff --git a/docs/content/api/SimPEG.electromagnetics.viscous_remanent_magnetization.rst b/docs/content/api/SimPEG.electromagnetics.viscous_remanent_magnetization.rst deleted file mode 100644 index d9cf10e232..0000000000 --- a/docs/content/api/SimPEG.electromagnetics.viscous_remanent_magnetization.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.electromagnetics.viscous_remanent_magnetization \ No newline at end of file diff --git a/docs/content/api/SimPEG.flow.richards.rst b/docs/content/api/SimPEG.flow.richards.rst deleted file mode 100644 index 9367f1d62a..0000000000 --- a/docs/content/api/SimPEG.flow.richards.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.flow.richards diff --git a/docs/content/api/SimPEG.meta.rst b/docs/content/api/SimPEG.meta.rst deleted file mode 100644 index 469456456c..0000000000 --- a/docs/content/api/SimPEG.meta.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.meta \ No newline at end of file diff --git a/docs/content/api/SimPEG.potential_fields.base.rst b/docs/content/api/SimPEG.potential_fields.base.rst deleted file mode 100644 index aba910d082..0000000000 --- a/docs/content/api/SimPEG.potential_fields.base.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.potential_fields diff --git a/docs/content/api/SimPEG.potential_fields.gravity.rst b/docs/content/api/SimPEG.potential_fields.gravity.rst deleted file mode 100644 index 4fd6dec3f3..0000000000 --- a/docs/content/api/SimPEG.potential_fields.gravity.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.potential_fields.gravity diff --git a/docs/content/api/SimPEG.potential_fields.magnetics.rst b/docs/content/api/SimPEG.potential_fields.magnetics.rst deleted file mode 100644 index c7dfb47af0..0000000000 --- a/docs/content/api/SimPEG.potential_fields.magnetics.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.potential_fields.magnetics diff --git a/docs/content/api/SimPEG.regularization.rst b/docs/content/api/SimPEG.regularization.rst deleted file mode 100644 index 35fb57ad5a..0000000000 --- a/docs/content/api/SimPEG.regularization.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.regularization diff --git a/docs/content/api/SimPEG.seismic.straight_ray_tomography.rst b/docs/content/api/SimPEG.seismic.straight_ray_tomography.rst deleted file mode 100644 index 9590370726..0000000000 --- a/docs/content/api/SimPEG.seismic.straight_ray_tomography.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.seismic.straight_ray_tomography diff --git a/docs/content/api/SimPEG.utils.rst b/docs/content/api/SimPEG.utils.rst deleted file mode 100644 index 7791aa3277..0000000000 --- a/docs/content/api/SimPEG.utils.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: SimPEG.utils diff --git a/docs/content/api/index.rst b/docs/content/api/index.rst index 8ffe60ffa9..a2de68f773 100644 --- a/docs/content/api/index.rst +++ b/docs/content/api/index.rst @@ -10,10 +10,10 @@ Geophysical Simulation Modules .. toctree:: :maxdepth: 2 - SimPEG.potential_fields - SimPEG.electromagnetics - SimPEG.flow - SimPEG.seismic + simpeg.potential_fields + simpeg.electromagnetics + simpeg.flow + simpeg.seismic SimPEG Building Blocks ====================== @@ -23,31 +23,39 @@ Base SimPEG .. toctree:: :maxdepth: 3 - SimPEG + simpeg Regularizations --------------- .. toctree:: :maxdepth: 2 - SimPEG.regularization + simpeg.regularization + +Optimizers +---------- +Optimizers used within SimPEG inversions. + +.. toctree:: + :maxdepth: 2 + + simpeg.optimization Directives ---------- .. toctree:: :maxdepth: 2 - SimPEG.directives + simpeg.directives Utilities --------- - Classes and functions for performing useful operations. .. toctree:: :maxdepth: 2 - SimPEG.utils + simpeg.utils Meta ---- @@ -56,4 +64,15 @@ Classes for encapsulating many simulations. .. toctree:: :maxdepth: 2 - SimPEG.meta + simpeg.meta + + +Typing +------ + +PEP 484 type aliases used in ``simpeg``. + +.. toctree:: + :maxdepth: 1 + + simpeg.typing \ No newline at end of file diff --git a/docs/content/api/simpeg.directives.rst b/docs/content/api/simpeg.directives.rst new file mode 100644 index 0000000000..b6c05c89d2 --- /dev/null +++ b/docs/content/api/simpeg.directives.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.directives diff --git a/docs/content/api/simpeg.electromagnetics.base.rst b/docs/content/api/simpeg.electromagnetics.base.rst new file mode 100644 index 0000000000..fb103a8f43 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.base.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.frequency_domain.rst b/docs/content/api/simpeg.electromagnetics.frequency_domain.rst new file mode 100644 index 0000000000..c3a9a071af --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.frequency_domain.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.frequency_domain \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.natural_source.rst b/docs/content/api/simpeg.electromagnetics.natural_source.rst new file mode 100644 index 0000000000..cf9c7c669a --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.natural_source.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.natural_source \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.rst b/docs/content/api/simpeg.electromagnetics.rst new file mode 100644 index 0000000000..eb04516321 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.rst @@ -0,0 +1,27 @@ +========================= +Electromagnetics +========================= + +Things about electromagnetics + +.. toctree:: + :maxdepth: 2 + + simpeg.electromagnetics.static.induced_polarization + simpeg.electromagnetics.static.resistivity + simpeg.electromagnetics.static.spectral_induced_polarization + simpeg.electromagnetics.static.self_potential + simpeg.electromagnetics.frequency_domain + simpeg.electromagnetics.natural_source + simpeg.electromagnetics.time_domain + simpeg.electromagnetics.viscous_remanent_magnetization + +Electromagnetics Utilities +-------------------------- + +.. toctree:: + :maxdepth: 2 + + simpeg.electromagnetics.static.utils + simpeg.electromagnetics.utils + simpeg.electromagnetics.base diff --git a/docs/content/api/simpeg.electromagnetics.static.induced_polarization.rst b/docs/content/api/simpeg.electromagnetics.static.induced_polarization.rst new file mode 100644 index 0000000000..8c29897f92 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.static.induced_polarization.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.static.induced_polarization \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.static.resistivity.rst b/docs/content/api/simpeg.electromagnetics.static.resistivity.rst new file mode 100644 index 0000000000..1ad60928fe --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.static.resistivity.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.static.resistivity diff --git a/docs/content/api/simpeg.electromagnetics.static.self_potential.rst b/docs/content/api/simpeg.electromagnetics.static.self_potential.rst new file mode 100644 index 0000000000..968ab4855b --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.static.self_potential.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.static.self_potential diff --git a/docs/content/api/simpeg.electromagnetics.static.spectral_induced_polarization.rst b/docs/content/api/simpeg.electromagnetics.static.spectral_induced_polarization.rst new file mode 100644 index 0000000000..ea0594a742 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.static.spectral_induced_polarization.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.static.spectral_induced_polarization \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.static.spontaneous_potential.rst b/docs/content/api/simpeg.electromagnetics.static.spontaneous_potential.rst new file mode 100644 index 0000000000..2e7ee86039 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.static.spontaneous_potential.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.static.spontaneous_potential diff --git a/docs/content/api/simpeg.electromagnetics.static.utils.rst b/docs/content/api/simpeg.electromagnetics.static.utils.rst new file mode 100644 index 0000000000..7d70b243c7 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.static.utils.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.static.utils \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.time_domain.rst b/docs/content/api/simpeg.electromagnetics.time_domain.rst new file mode 100644 index 0000000000..4160a46799 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.time_domain.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.time_domain \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.utils.rst b/docs/content/api/simpeg.electromagnetics.utils.rst new file mode 100644 index 0000000000..e040bf84e2 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.utils.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.utils \ No newline at end of file diff --git a/docs/content/api/simpeg.electromagnetics.viscous_remanent_magnetization.rst b/docs/content/api/simpeg.electromagnetics.viscous_remanent_magnetization.rst new file mode 100644 index 0000000000..9eb72e4e07 --- /dev/null +++ b/docs/content/api/simpeg.electromagnetics.viscous_remanent_magnetization.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.electromagnetics.viscous_remanent_magnetization \ No newline at end of file diff --git a/docs/content/api/simpeg.flow.richards.rst b/docs/content/api/simpeg.flow.richards.rst new file mode 100644 index 0000000000..f357129635 --- /dev/null +++ b/docs/content/api/simpeg.flow.richards.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.flow.richards diff --git a/docs/content/api/SimPEG.flow.rst b/docs/content/api/simpeg.flow.rst similarity index 84% rename from docs/content/api/SimPEG.flow.rst rename to docs/content/api/simpeg.flow.rst index 576e049f1b..a9c972db27 100644 --- a/docs/content/api/SimPEG.flow.rst +++ b/docs/content/api/simpeg.flow.rst @@ -7,4 +7,4 @@ Things about the fluid flow module .. toctree:: :maxdepth: 2 - SimPEG.flow.richards + simpeg.flow.richards diff --git a/docs/content/api/simpeg.meta.rst b/docs/content/api/simpeg.meta.rst new file mode 100644 index 0000000000..4fd168df84 --- /dev/null +++ b/docs/content/api/simpeg.meta.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.meta \ No newline at end of file diff --git a/docs/content/api/simpeg.optimization.rst b/docs/content/api/simpeg.optimization.rst new file mode 100644 index 0000000000..822bee4abe --- /dev/null +++ b/docs/content/api/simpeg.optimization.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.optimization \ No newline at end of file diff --git a/docs/content/api/simpeg.potential_fields.base.rst b/docs/content/api/simpeg.potential_fields.base.rst new file mode 100644 index 0000000000..e62e05fcf3 --- /dev/null +++ b/docs/content/api/simpeg.potential_fields.base.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.potential_fields diff --git a/docs/content/api/simpeg.potential_fields.gravity.rst b/docs/content/api/simpeg.potential_fields.gravity.rst new file mode 100644 index 0000000000..aa28a01ba9 --- /dev/null +++ b/docs/content/api/simpeg.potential_fields.gravity.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.potential_fields.gravity diff --git a/docs/content/api/simpeg.potential_fields.magnetics.rst b/docs/content/api/simpeg.potential_fields.magnetics.rst new file mode 100644 index 0000000000..88bb6bfb84 --- /dev/null +++ b/docs/content/api/simpeg.potential_fields.magnetics.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.potential_fields.magnetics diff --git a/docs/content/api/SimPEG.potential_fields.rst b/docs/content/api/simpeg.potential_fields.rst similarity index 63% rename from docs/content/api/SimPEG.potential_fields.rst rename to docs/content/api/simpeg.potential_fields.rst index 26d7c34c0b..f5097a9176 100644 --- a/docs/content/api/SimPEG.potential_fields.rst +++ b/docs/content/api/simpeg.potential_fields.rst @@ -5,12 +5,12 @@ Potential Fields .. toctree:: :maxdepth: 2 - SimPEG.potential_fields.gravity - SimPEG.potential_fields.magnetics + simpeg.potential_fields.gravity + simpeg.potential_fields.magnetics Base Potential Fields --------------------- .. toctree:: :maxdepth: 2 - SimPEG.potential_fields.base + simpeg.potential_fields.base diff --git a/docs/content/api/simpeg.regularization.rst b/docs/content/api/simpeg.regularization.rst new file mode 100644 index 0000000000..fd8099173b --- /dev/null +++ b/docs/content/api/simpeg.regularization.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.regularization diff --git a/docs/content/api/SimPEG.rst b/docs/content/api/simpeg.rst similarity index 100% rename from docs/content/api/SimPEG.rst rename to docs/content/api/simpeg.rst diff --git a/docs/content/api/SimPEG.seismic.rst b/docs/content/api/simpeg.seismic.rst similarity index 75% rename from docs/content/api/SimPEG.seismic.rst rename to docs/content/api/simpeg.seismic.rst index 57f467182e..a92572ef1b 100644 --- a/docs/content/api/SimPEG.seismic.rst +++ b/docs/content/api/simpeg.seismic.rst @@ -7,4 +7,4 @@ Things about the Seismic module .. toctree:: :maxdepth: 2 - SimPEG.seismic.straight_ray_tomography + simpeg.seismic.straight_ray_tomography diff --git a/docs/content/api/simpeg.seismic.straight_ray_tomography.rst b/docs/content/api/simpeg.seismic.straight_ray_tomography.rst new file mode 100644 index 0000000000..e8bfe945d6 --- /dev/null +++ b/docs/content/api/simpeg.seismic.straight_ray_tomography.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.seismic.straight_ray_tomography diff --git a/docs/content/api/simpeg.typing.rst b/docs/content/api/simpeg.typing.rst new file mode 100644 index 0000000000..44c57f983f --- /dev/null +++ b/docs/content/api/simpeg.typing.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.typing diff --git a/docs/content/api/simpeg.utils.rst b/docs/content/api/simpeg.utils.rst new file mode 100644 index 0000000000..2ff4babe74 --- /dev/null +++ b/docs/content/api/simpeg.utils.rst @@ -0,0 +1 @@ +.. automodule:: simpeg.utils diff --git a/docs/content/getting_started/index.rst b/docs/content/getting_started/index.rst deleted file mode 100644 index 6721be40a5..0000000000 --- a/docs/content/getting_started/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -.. _getting_started: - -=============== -Getting Started -=============== - -Here you'll find instructions on getting up and running with ``SimPEG``. - -.. toctree:: - :maxdepth: 2 - - big_picture - installing - contributing/index.rst diff --git a/docs/content/getting_started/installing.rst b/docs/content/getting_started/installing.rst deleted file mode 100644 index 14adff1570..0000000000 --- a/docs/content/getting_started/installing.rst +++ /dev/null @@ -1,148 +0,0 @@ -.. _api_installing: - -Getting Started with SimPEG -*************************** - - -.. _installing_python: - -Prerequisite: Installing Python -=============================== - -SimPEG is written in Python_! -We highly recommend installing it using Anaconda_ (or the alternative Mambaforge_). -It installs `Python `_, -`Jupyter `_ and other core -Python libraries for scientific computing. -If you and Python_ are not yet acquainted, we highly -recommend checking out `Software Carpentry `_. - -.. note:: - - As of version 0.11.0, we will no longer ensure compatibility with Python 2.7. Please use - the latest version of Python 3 with SimPEG. For more information on the transition of the - Python ecosystem to Python 3, please see the `Python 3 Statement `_. - -.. image:: https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Python-logo-notext.svg/220px-Python-logo-notext.svg.png - :align: right - :width: 100 - :target: https://www.python.org/ - -.. _Python: https://www.python.org/ -.. _Anaconda: https://www.anaconda.com/products/individual -.. _Mambaforge: https://www.anaconda.com/products/individual - - -.. _installing_simpeg: - -Installing SimPEG -================= - -Conda Forge ------------ - -SimPEG is available through `conda-forge` and you can install is using the -`conda package manager `_ that comes with the Anaconda -distribution: - -.. code:: - - conda install SimPEG --channel conda-forge - -Installing through `conda`/`mamba` is our recommended method of installation. - -.. note:: - If you find yourself wanting a faster package manager than ``conda`` - check out the ``mamba`` project at https://mamba.readthedocs.io/. It - usually is able to set up environments much quicker than ``conda`` and - can be used as a drop-in replacement (i.e. replace ``conda`` commands with - ``mamba``). - -PyPi ----- - -SimPEG is on `pypi `_! First, make sure -your version of pip is up-to-date - -.. code:: - - pip install --upgrade pip - -Then you can install SimPEG - -.. code:: - - pip install SimPEG - - -To update SimPEG, you can run - -.. code:: - - pip install --upgrade SimPEG - - -Installing from Source ----------------------- - -First (you need git):: - - git clone https://github.com/simpeg/simpeg - -Second (from the root of the SimPEG repository):: - - pip install . - -If you are interested in contributing to SimPEG, please check out the page on :ref:`Contributing ` - - -Success? -======== - -If you have been successful at downloading and installing SimPEG, you should -be able to download and run any of the :ref:`examples and tutorials `. - -If not, you can reach out to other people developing and using SimPEG on the -`google forum `_ or on -`Mattermost `_. - -Useful Links -============ - -An enormous amount of information (including tutorials and examples) can be found on the official websites of the packages - -* `Python `_ -* `Numpy `_ -* `SciPy `_ -* `Matplotlib `_ - -Python for scientific computing -------------------------------- - -* `Python for Scientists `_ Links to commonly used packages, Matlab to Python comparison -* `Python Wiki `_ Lists packages and resources for scientific computing in Python -* `Jupyter `_ - -Numpy and Matlab ----------------- - -* `NumPy for Matlab Users `_ -* `Python vs Matlab `_ - -Lessons in Python ------------------ - -* `Software Carpentry `_ -* `Introduction to NumPy and Matplotlib `_ - - -Editing Python --------------- - -There are numerous ways to edit and test Python (see -`PythonWiki `_ for an overview) and -in our group at least the following options are being used: - -* `Jupyter `_ -* `Sublime `_ -* `PyCharm `_ diff --git a/docs/content/release/0.22.0-notes.rst b/docs/content/release/0.22.0-notes.rst new file mode 100644 index 0000000000..ac843655ac --- /dev/null +++ b/docs/content/release/0.22.0-notes.rst @@ -0,0 +1,213 @@ +.. _0.22.0_notes: + +============================ +SimPEG 0.22.0 Release Notes +============================ + +June 26th, 2024 + +.. contents:: Highlights + :depth: 3 + +Updates +======= + +``SimPEG`` has been renamed to ``simpeg`` +----------------------------------------- + +SimPEG v0.22.0 comes with a major change that everyone will experience! We are +excited to announce that we renamed the package from ``SimPEG`` to ``simpeg``, +making it compliant to `PEP8 `__. + +Since this version and moving forward we'll import ``simpeg`` as: + +.. code:: python + + import simpeg + +or any of its submodules as: + +.. code:: python + + from simpeg.potential_fields import magnetics + +Although we encourage users to update their code after they update their +installed SimPEG version, we still support the old ``SimPEG``. +We'll just receive a warning about the deprecation of ``SimPEG`` with +upper-cases if we try to import it: + +.. code:: python + + import SimPEG + +.. code:: + + FutureWarning: Importing `SimPEG` is deprecated. please import from `simpeg`. + +New features +------------ + +We have a faster and more memory efficient implementation of the magnetic +simulation +:class:`~simpeg.potential_fields.magnetics.Simulation3DIntegral`, built using +`Choclo `__ and `Numba `__. +In order to use it, you will need to `install Choclo +`__ in addition to +``simpeg``. This new implementation is able to forward model TMI and all +magnetic field components with susceptibility (scalar) or effective +susceptibility (vector) models. + +Documentation +------------- + +Since v0.22.0, we serve the documentation pages for `latest +`__ and older versions of SimPEG, along with +the pages for the `development version `__ (the +latest version in the ``main`` branch of our GitHub repository). +The docs now include a version switcher in the top bar that will allow users to +navigate between the pages for each different version. + +Several improvements have been made to the documentation pages. + +In the `Getting Started +`__ guide we +stopped recommending ``mamba`` as the best package manager for installing +SimPEG after ``conda`` started using ``libmamba`` under the hood, achieving the +same performance as ``mamba``. We also included instructions so our +contributors can easily update their local environment, which is specially +useful after we update the minimum version of the development dependencies +(such as autoformatters and linters). + +The documentation for Frequency-Domain (FDEM) and Time-Domain (TDEM) EM fields +and 3D simulations got greatly extended, with more details on the parameters +for each method, along with more mathematical background for their +implementations. + +Lastly, we fixed typos, the contour colors for one of the gravity examples, and +the broken links to the source code for each class, function and method. +These links now point to their corresponding version in the GitHub repository. + +Bugfixes +-------- + +This release comes with a few bug fixes. We solved an issue with the distance +calculation in the +:func:`~simpeg.electromagnetics.static.utils.convert_survey_3d_to_2d_lines` +utility function that converts 3D DC-IP survey locations to 2D lines. We fixed +a bug on how the arguments passed to the directives that estimate the beta +parameter where being passed to the parent classes. We updated the code in +:class:`~simpeg.utils.pgi_utils.GaussianMixtureWithPrior` to make it compatible +with the latest versions of `scikit-learn `__. And +we now make sure that the queues are joined when the +:class:`~simpeg.meta.MetaSimulation` is joined. + +Breaking changes +---------------- + +The :mod:`~simpeg.electromagnetics.static.spontaneous_potential` has been +renamed to :mod:`~simpeg.electromagnetics.static.self_potential` to accurately +reflect the nature of the process, as the term being used in the literature. + +General improvements +-------------------- + +We improved the interface of the +:class:`~simpeg.potential_fields.magnetics.UniformBackgroundField` class, and +for the DC :class:`~simpeg.electromagnetics.static.resistivity.sources.Dipole` +source class. + +We moved away from the deprecated Numpy's global random seeds and replace them +for the new `random number generator object +`__ +in the entire SimPEG's code base and in most of its tests. This greatly helps +the experience of ensuring reproducible runs of our inversions and tests. + +Lastly, the inversion logs now also include the SimPEG version that is being +used. + +Maintenance +----------- + +We updated the configuration files to build and install SimPEG, moving away +from the old ``setup.py`` into the new ``pyproject.toml``. +We fixed another important flake8 warning across the code base: F821, which +highlights undefined varibles in the code. +And cleaned up the scripts for running automated tasks in Azure Pipelines (like +checking style, testing, deploying docs and code). + +Contributors +============ + +This is a combination of contributors and reviewers who've made contributions +towards this release (in no particular order). + +- `@dccowan `__ +- `@jcapriot `__ +- `@jedman `__ +- `@kehrl-kobold `__ +- `@lheagy `__ +- `@santisoler `__ +- `@williamjsdavis `__ + +We would like to highlight the contributions made by new contributors: + +- `@kehrl-kobold `__ made their first contribution in + https://github.com/simpeg/simpeg/pull/1390 +- `@williamjsdavis `__ made their first contribution in + https://github.com/simpeg/simpeg/pull/1486 + + +Pull Requests +============= + +- Remove the parameters argument from docstring by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1417 +- Use reviewdog to annotate PR’s with black and flake8 errors. by `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1424 +- Safely run reviewdog on ``pull_request_target`` events by `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1427 +- Add new Issue template for making a release by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1410 +- Replace use of ``refine_tree_xyz`` in DCIP tutorials by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1381 +- Fix rst syntax in release notes for v0.21.0 by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1434 +- Move to a PEP8 compliant package name. by `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1430 +- Update copyright year in **init**.py by `@lheagy `__ in https://github.com/simpeg/simpeg/pull/1436 +- Replace SimPEG for simpeg across docstrings by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1438 +- Lowercase simpeg for generating coverage on Azure by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1443 +- Rename spontaneous potential to self potential by `@lheagy `__ in https://github.com/simpeg/simpeg/pull/1422 +- Fix distance calculation in ``convert_survey_3d_to_2d_lines`` by `@kehrl-kobold `__ in https://github.com/simpeg/simpeg/pull/1390 +- Replace SimPEG for simpeg in API reference by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1446 +- Replace SimPEG for simpeg in getting started pages by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1447 +- Check inputs for converting 3d surveys to 2d lines by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1392 +- Always use Pydata Sphinx theme for building docs by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1445 +- Simplify interface of UniformBackgroundField by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1421 +- Ensure the queue’s are joined when the meta simulation is joined. by `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1464 +- Add maintenance issue template by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1468 +- Add instructions to update the environment by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1462 +- Stop recommending mamba for installing simpeg by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1463 +- Fix bug on arguments of beta estimator directives by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1460 +- Improve interface for DC Dipole source by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1393 +- Use Numpy random number generator in codebase by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1394 +- Extend docstrings for FDEM and TDEM fields and 3D simulations by `@dccowan `__ in https://github.com/simpeg/simpeg/pull/1414 +- Magnetic simulation with Choclo as engine by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1321 +- Fix typos in EM docstrings by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1473 +- Fix call to private method in GaussianMixtureWithPrior by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1476 +- Add version switcher to Sphinx docs by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1428 +- Use random seed on synthetic data in mag tests by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1457 +- Fix links to source code in documentation pages by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1444 +- Fix script for new deployment of docs by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1478 +- Print SimPEG version in the inversion log by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1477 +- Use Numpy rng in FDEM tests by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1449 +- Add ``random_seed`` argument to objective fun’s derivative tests by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1448 +- Add ``random_seed`` to the ``test`` method of maps by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1465 +- Use Numpy rng in TDEM tests by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1452 +- Use random seed in missed objective function tests by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1483 +- Fix contour colors in gravity plot in User Guide by `@williamjsdavis `__ in https://github.com/simpeg/simpeg/pull/1486 +- Hide type hints from signatures in documentation pages by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1471 +- Reorganize the maps submodule by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1480 +- Pyproject.toml by `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1482 +- Use Numpy rng in static EM tests by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1451 +- Split Azure Pipelines configuration into multiple files by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1481 +- Ignore ``survey_type`` argument in DC and SIP surveys by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1458 +- Update deployment of docs to simpeg-docs by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1490 +- Fix F821 flake error: undefined variable by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1487 +- Use Numpy rng in viscous remanent mag tests by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1453 +- Use Numpy rng in NSEM tests by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1450 +- Unwrap lines in release checklist by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1498 +- Improve instructions to update versions.json by `@santisoler ` in https://github.com/simpeg/simpeg/pull/1500 diff --git a/docs/content/release/0.22.1-notes.rst b/docs/content/release/0.22.1-notes.rst new file mode 100644 index 0000000000..c06d4ee577 --- /dev/null +++ b/docs/content/release/0.22.1-notes.rst @@ -0,0 +1,35 @@ +.. _0.22.1_notes: + +=========================== +SimPEG 0.22.1 Release Notes +=========================== + +July 5th, 2024 + +.. contents:: Highlights + :depth: 2 + +Updates +======= + +This patch release includes and improvement in one of the import lines, fixing +an bug that was making importing frequency domain modules in :mod:`simpeg` to +fail when working with :mod:`matplotlib` ``>=3.9.0``. + +It also enables the version warning banner in PyData Sphinx theme, allowing +users to quickly notice if the docs they are reading are not the latest ones. + +Contributors +============ + +- `@santisoler `__ + +Pull Requests +============= + +- Configure version warning banner in Sphinx by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1501 +- Improve imports in natural source utils by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1503 +- Deploy to TestPyPI only on nightly builds by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1509 diff --git a/docs/content/release/0.22.2-notes.rst b/docs/content/release/0.22.2-notes.rst new file mode 100644 index 0000000000..9f635ae8b4 --- /dev/null +++ b/docs/content/release/0.22.2-notes.rst @@ -0,0 +1,41 @@ +.. _0.22.2_notes: + +=========================== +SimPEG 0.22.2 Release Notes +=========================== + +September 11th, 2024 + +.. contents:: Highlights + :depth: 2 + +Updates +======= + +This patch release includes a few fixes: SciPy solvers can now be used +with newer versions of SciPy (``>=1.14``), we fixed a bug in one of the methods +of the Gaussian Mixture Models used in PGI, we included a fix on one of the +internal validation functions, and we also applied some minor improvements to +the documentation pages. + +Contributors +============ + +- `@prisae `__ +- `@santisoler `__ + +Pull Requests +============= + +- Minor fixes to disclaimer in ``pgi_utils.py`` by `@prisae `__ in + https://github.com/simpeg/simpeg/pull/1512 +- Fix misuse of the ``requires`` decorator in ``code_utils.py`` by + `@prisae `__ in https://github.com/simpeg/simpeg/pull/1513 +- Use lowercase simpeg in a few missing docstrings by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1519 +- Pass ``rtol`` to SciPy solvers for SciPy>=1.12 by `@prisae `__ in + https://github.com/simpeg/simpeg/pull/1517 +- Fix ``validate_ndarray_with_shape`` by `@prisae `__ in + https://github.com/simpeg/simpeg/pull/1523 +- Fix bug in GMM’s ``_check_weights`` by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1526 diff --git a/docs/content/release/0.23.0-notes.rst b/docs/content/release/0.23.0-notes.rst new file mode 100644 index 0000000000..a7141dc50f --- /dev/null +++ b/docs/content/release/0.23.0-notes.rst @@ -0,0 +1,210 @@ +.. _0.23.0_notes: + +=========================== +SimPEG 0.23.0 Release Notes +=========================== + +November 3rd, 2024 + +.. contents:: Highlights + :depth: 3 + +Updates +======= + +New features +------------ + +Full support for Numpy 2.0 +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +With this release SimPEG is fully compatible with Numpy 2.0. + +Augmented receivers for airborne NSEM +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We added new types of receivers intended to be used for airborne natural-source +EM simulations: + +- :class:`~simpeg.electromagnetics.natural_source.receivers.Impedance` +- :class:`~simpeg.electromagnetics.natural_source.receivers.Admittance` +- :class:`~simpeg.electromagnetics.natural_source.receivers.ApparentConductivity` +- :class:`~simpeg.electromagnetics.natural_source.receivers.Tipper` + +They extend the type of airborne NSEM surveys that can be simulated in SimPEG. + +See https://github.com/simpeg/simpeg/pull/1454 for more details. + +Automatic selection of Solver +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This release includes a new +:func:`~simpeg.utils.solver_utils.get_default_solver` function that +automatically selects a solver based on what you have installed on +your system. +For example, it'll select ``Pardiso`` as the solver if you are running an Intel +CPU and have `pydiso` installed in your system. Alternatively, it can choose +``Mumps`` if you are using Apple silicon and the `python-mumps` package is installed. +If no fast solver is available, it'll choose SciPy's ``SparseLU`` solver. + +.. note:: + For those installing through `conda-forge`, ``conda install simpeg`` will now also grab + better solvers for your system as well, consistent with `simpeg`'s solver priority. + +Moreover, simulations will also use this function to get the default solver if no +solver is being provided by the user, making it easier to run efficient +finite volume simulations out of the box. +See https://github.com/simpeg/simpeg/pull/1511 for more information. + +Support for magnetic gradiometry in Choclo-based magnetic simulations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Now the magnetic simulations with ``engine="choclo"`` support forward modelling +the magnetic gradiometry components and TMI derivatives. This fully extends the +support of the Numba-based simulations to every field that can also be +computed using ``engine="geoana"``. + +See https://github.com/simpeg/simpeg/pull/1543 and +https://github.com/simpeg/simpeg/pull/1553 for more information. + +Numba-based implementation of gravity and equivalent sources +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This release includes new implementations of the gravity and magnetic +equivalent sources using `Choclo `__'s kernels and +forward modelling functions and `Numba `__ to +just-in-time compilations and parallelization, making them faster and more +memory efficient. + +See https://github.com/simpeg/simpeg/pull/1552 and +https://github.com/simpeg/simpeg/pull/1527. + +New ``UpdateIRLS`` directive +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We have renamed the :class:`simpeg.directives.UpdateIRLS` directive for +applying the Iterative Re-weighted Least-Squares during sparse norm inversions, +that includes better argument names and an improved implementation. +See https://github.com/simpeg/simpeg/pull/1349 for more details. + +Standardize arguments for active cells and random seeds +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We have standardize the name of the arguments for active cells in meshes through +all functions and methods in SimPEG. We have deprecated the old +names like ``indAct``, ``ind_active`` and ``indActive`` in favor of +``active_cells``. + +The arguments for passing random seeds have also been standardized to +``random_seed``. These arguments generalize a way to specify random states, +allowing to take seeds as integers or instances of +:class:`numpy.random.Generator`. + + +Upgraded dependencies +--------------------- + +In SimPEG v0.23.0 we dropped support for Python versions <= 3.9. Python 3.8 met +its end-of-life this year (October 2024). Python 3.10 is the minimum required +version for Numpy 2.1.0. To keep up with the latest updates in the scientific +Python ecosystem, we decided to set Python 3.10 as the minimum required version +for SimPEG as well. + +Moreover, we have increased the minimum required versions of ``discretize``, +``geoana`` and ``pymatsolver`` in order to support Numpy 2.0. +Lastly, now ``pandas``, ``scikit-learn`` and ``empymod`` are optional +dependencies (instead of required ones). + + +Documentation +------------- + +This release includes a few fixes to the documentation pages, like improvements +to some magnetic examples, and fixes to docstrings and math of a few classes. + + +Bugfixes +-------- + +We have fixed some issues of Dask-based simulations that were running into +race-conditions after one of the latest Dask updates. See +https://github.com/simpeg/simpeg/pull/1469 for more information. + + +Contributors +============ + +* `@domfournier `__ +* `@jcapriot `__ +* `@prisae `__ +* `@santisoler `__ +* `@thibaut-kobold `__ + +Pull Requests +============= + +- Make ``pandas`` & ``scikit-learn`` optional dependencies by `@prisae `__ in + https://github.com/simpeg/simpeg/pull/1514 +- Dask races by `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1469 +- Irls refactor by `@domfournier `__ in + https://github.com/simpeg/simpeg/pull/1349 +- Minimum python version update by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1522 +- Replace ``ind_active`` for ``active_cells`` in pf simulations by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1520 +- Move push to codecov to its own stage by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1493 +- Minor fix in deprecation notice in docstrings by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1535 +- Replace ``indActive`` and ``actInd`` for ``active_cells`` in maps by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1534 +- Update tests and examples to use the new ``UpdateIRLS`` by + `@domfournier `__ in https://github.com/simpeg/simpeg/pull/1472 +- Replace ``indActive`` in VRM simulations for ``active_cells`` by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1536 +- Test assigned values when passing deprecated args by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1544 +- Use random seed when using ``make_synthetic_data`` in tests by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1545 +- Minor improvements to ``UpdateIRLS`` class by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1529 +- Replace ``seed`` for ``random_seed`` in directives by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1538 +- Minor fixes to magnetic examples by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1547 +- Support magnetic gradiometry using Choclo as engine by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1543 +- Update usage of ``random_seed`` in one example by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1549 +- Replace ``seed`` for ``random_seed`` in ``model_builder`` by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1548 +- Replace old args for ``active_cells`` in EM static functions by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1550 +- Implement gravity equivalent sources with Choclo as engine by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1527 +- Implement tmi derivatives with Choclo in magnetic simulation by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1553 +- Implement magnetic eq sources with Choclo by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1552 +- Update links in PR template by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1554 +- Default solver by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1511 +- Fixes for most recent geoana 0.7 by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1557 +- Numpy2.0 and discretize 0.11.0 updates by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1558 +- Add missing seeds by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1560 +- Make use of meshes’ ``cell_bounds`` property by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1559 +- Fix docstring of ``SmoothnessFullGradient`` by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1562 +- Fix math in docstring of eigenvalue_by_power_iteration by `@santisoler `__ + in https://github.com/simpeg/simpeg/pull/1564 +- Only warn about default solver when set in simulations by `@santisoler `__ + in https://github.com/simpeg/simpeg/pull/1565 +- Augmented receivers for airborne NSEM by `@dccowan `__ in https://github.com/simpeg/simpeg/pull/1454 +- Try uploading all the coverage files at once. by `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1569 +- Re-implement distance weighting and add a strategy comparison by `@thibaut-kobold `__ in https://github.com/simpeg/simpeg/pull/1310 +- Remove empymod dependency by `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1571 diff --git a/docs/content/release/0.24.0-notes.rst b/docs/content/release/0.24.0-notes.rst new file mode 100644 index 0000000000..3e24893612 --- /dev/null +++ b/docs/content/release/0.24.0-notes.rst @@ -0,0 +1,232 @@ +.. _0.24.0_notes: + +=========================== +SimPEG 0.24.0 Release Notes +=========================== + +April 24th, 2025 + +.. contents:: Highlights + :depth: 3 + +Updates +======= + +New features +------------ + +Speed up of dot products involved in PGI +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This release includes optimizations of some dot products carried out in the +:class:`~simpeg.regularization.pgi.PGIsmallness`. They significantly reduce the +computation time of Petrophysically and Geologically Guided Inversions (PGI). + +Specifically, these changes optimize the dot products involved when evaluating +the regularization function itself and its derivatives. The optimization takes +advantage of the :func:`numpy.einsum` function. + +See https://github.com/simpeg/simpeg/pull/1587 and +https://github.com/simpeg/simpeg/pull/1588 for more information. + + +Potential field sensitivity matrices as Linear Operators +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The gravity and magnetic field simulations are now capable of building the +sensitivity matrix ``G`` as a SciPy +:class:`~scipy.sparse.linalg.LinearOperator` object when the +``store_sensitivities`` argument is set to ``"forward_only"``. +The :class:`~scipy.sparse.linalg.LinearOperator` objects +can be used to compute the dot product with any vector (``G +@ v``), or the dot product of their transpose (``G.T @ v``) as if they were +arrays, although the dense matrix is never fully built nor allocated in memory. +Instead, the forward computation is carried out whenever a dot product is +requested. + +This change allows to compute the simulation derivatives without requiring +large amount of memory to store large sensitivity matrices, enabling users to +run inversions of large models where the sensitivity matrix is larger than the +available memory. + +Using methods like +:meth:`~simpeg.potential_fields.gravity.Simulation3DIntegral.Jvec`, +:meth:`~simpeg.potential_fields.gravity.Simulation3DIntegral.Jtvec`, +and +:meth:`~simpeg.potential_fields.gravity.Simulation3DIntegral.getJtJdiag`, make +use of +:attr:`~simpeg.potential_fields.gravity.Simulation3DIntegral.G` +a linear operator when ``store_sensitivities="forward_only"``. +Meanwhile, the +:meth:`~simpeg.potential_fields.gravity.Simulation3DIntegral.getJ` +method returns a composite +:class:`~scipy.sparse.linalg.LinearOperator` object that can also be used to +compute dot products with any vector. + +See https://github.com/simpeg/simpeg/pull/1622 and +https://github.com/simpeg/simpeg/pull/1634 for more information. + +Move indexing of arrays from :class:`simpeg.data.Data` to Surveys +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We moved the indexing capabilities of the :class:`~simpeg.data.Data` objects to +the different ``Survey`` objects. This is useful in case we have some data as +a flat array that is related to a particular survey (or combination of sources +and receivers), and we want to obtain the data values associated to +a particular pair of source and receiver. + +With this change, we don't need to define a new :class:`~simpeg.data.Data` +object to slice an array, we can use the ``Survey`` itself. +For example, let's say we have a survey with two sources, and three receivers +each: + +.. code:: python + + receivers_a = [Recevier([[-2, 0]]), Recevier([[0, 0]]), Recevier([[2, 0]])] + source_a = Source(receiver_list=receivers_a) + receivers_b = [Recevier([[3, 1]]), Recevier([[4, 1]]), Recevier([[5, 1]])] + source_b = Source(receiver_list=receivers_b) + survey = Survey(source_list=[source_a, source_b]) + +And we have a ``dobs`` array that corresponds to this survey. We can obtain the +values of the ``dobs`` array associated with the second receiver and the first +source by using the ``get_slice`` method to obtain a ``slice`` object, and then +use it to index the ``dobs`` array: + +.. code:: python + + slice_obj = survey.get_slice(source_a, receivers_a[1]) + dobs_slice = dobs[slice_obj] + +See https://github.com/simpeg/simpeg/pull/1616 and +https://github.com/simpeg/simpeg/pull/1632 for more information. + +Documentation +------------- + +The documentation pages have been reorganized, merging the _Getting Started_ +section into the :ref:`User Guide `. +This change makes it easier to navigate through the different documentation +pages, with the assistance of a table of contents on the side. + +We updated the :ref:`installation instructions `, with `Miniforge +`_ as the recommended Python +distribution. + +We have also improved the documentation of some classes and methods. + + +Bugfixes +-------- + +This release includes a list of bug fixes. We solved issues related to the +``getJ`` method of the DC, SIP, TDEM, and FDEM simulations. The EM1D +simulations can now work with receivers objects with multiple locations. +The :class:`~simpeg.data_misfit.BaseDataMisfit` class and its children raise errors in case the +simulation is retuning non-numeric values as output. + +We have also improved some of the error messages that users get when things +don't work as expected, aiming to catch those mistakes earlier than late. + +Contributors +============ + +Contributors + +- `@ghwilliams `__ +- `@jcapriot `__ +- `@johnweis0480 `__ +- `@lheagy `__ +- `@santisoler `__ + + +Pull Requests +============= + +- Bugfix for TDEM magnetic dipole sources by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1572 +- Fix ubcstyle printout by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1577 +- Add docstring to ``n_processes`` in potential field simulations by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1578 +- Move simulation solver from base simulation to PDE simulation by + `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1582 +- Update and fix instructions to build the docs by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1583 +- Change location of ``mesh`` attribute by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1585 +- Speed up most commonly used deriv/deriv2 in PGI by `@johnweis0480 `__ in + https://github.com/simpeg/simpeg/pull/1587 +- Improve dot products in ``PGIsmallness.__call__`` and update docstring + by `@johnweis0480 `__ in https://github.com/simpeg/simpeg/pull/1588 +- Rename delete on model update by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1589 +- update PGI Example plotting script for deprecated collections by + `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1595 +- Coverage upload on failed test by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1596 +- Use zizmor to lint GitHub Actions workflows by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1592 +- Update installation instructions in docs by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1597 +- Set ``permissions`` in Actions to avoid zizmor’s + ``excessive-permissions`` by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1602 +- Fix for removed quadrature function on new scipy versions by `@jcapriot `__ + in https://github.com/simpeg/simpeg/pull/1603 +- Install zizmor through conda-forge in ``environment.yml`` by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1600 +- Raise errors if dpred in ``BaseDataMisfit`` has nans by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1615 +- Update Black’s Python versions in ``pyproject.toml`` by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1620 +- Use shell rendering in Bug report template by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1612 +- Merge Getting Started and Examples into User Guide by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1619 +- Fix usage of “bug” label in bug report template by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1624 +- Fix redirects links in docs by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1623 +- Fix bug on ``getJ`` of gravity simulation by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1621 +- Fix redirect to user guide index page by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1627 +- Move indexing of flat arrays to Survey classes by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1616 +- Replace Data indexing for Survey slicing where needed by `@santisoler `__ + in https://github.com/simpeg/simpeg/pull/1632 +- Implement ``G`` matrix as ``LinearOperator`` in gravity simulation by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1622 +- Set maximum number of iterations in eq sources tests by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1636 +- Em1d multiple rx locs by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1637 +- Fix definition of model in gravity J-related tests by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1643 +- Improve docstring of dip_azimuth2cartesian by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1642 +- Improve variable names in gravity test by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1641 +- Test transpose of gravity getJ as linear operator by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1644 +- Configure zizmor to pin reviewdog actions with tags by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1650 +- Deprecate ``components`` in potential field surveys by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1633 +- Fix bug on magnetic simulation ``nD`` property by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1646 +- Make pytest error on random seeded test by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1598 +- Add support for potential fields survey indexing by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1635 +- Implement magnetic G as linear operator by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1634 +- Use Numpy’s RNG in tests for depth weighting by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1570 +- Raise NotImplementedError on getJ for NSEM 1D simulations by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1653 +- Set the model when calling ``getJ`` in DC and SIP simulations by + `@lheagy `__ in https://github.com/simpeg/simpeg/pull/1361 +- Fix ``getJ`` method in TDEM and FDEM 1D simulations by `@ghwilliams `__ in + https://github.com/simpeg/simpeg/pull/1638 diff --git a/docs/content/release/0.25.0-notes.rst b/docs/content/release/0.25.0-notes.rst new file mode 100644 index 0000000000..1c2ec6de42 --- /dev/null +++ b/docs/content/release/0.25.0-notes.rst @@ -0,0 +1,372 @@ +.. _0.25.0_notes: + +============================ +SimPEG v0.25.0 Release Notes +============================ + +October 22nd, 2025 + +.. contents:: Highlights + :depth: 3 + +Updates +======= + +New features +------------ + +New differential simulation for magnetic fields +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This release ships with a new and improved version of the magnetic differential +simulation +:class:`~simpeg.potential_fields.magnetics.Simulation3DDifferential` by +`@johnweis0480 `__. +This simulation computes the magnetic field on every cell of the mesh by +numerically solving the magnetostatic PDE. It accounts for +self-demagnetization effects, model both induced and remanent magnetizations, +and is faster and less memory intensive than the integral simulation for large +problems. + +See https://github.com/simpeg/simpeg/pull/1682 for more details. + +Add utility function to shift electrodes to discrete topography +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A new :func:`simpeg.utils.shift_to_discrete_topography` function shifts +locations relative to discrete surface topography. When performing MT surveys, +we measure electric fields at the Earth's surface. +Similar to DC/IP, the original measurement locations of the electric fields can +end up in air cells when we discretize surface topography. This function allows +the user to shift locations relative to discrete topography on Tensor and Tree +meshes. +For Airborne NSEM, they also allow the user to preserve the original flight +heights. + +See https://github.com/simpeg/simpeg/pull/1683 for more details. + +Calculate B/H fields with a step-off waveform closed-loop wire source in TDEM +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We can now calculate the B and H fields using a closed-loop wire as source in +the TDEM code with a step-off waveform. +The simulation will first compute the vector potential using the Biot-Savart +Law and then take the curl of it to get the initial :math:`\mathbf{B}` field. + +See https://github.com/simpeg/simpeg/pull/1651 for more details. + +Sensitivity matrix as a ``LinearOperator`` in gravity and magnetic equivalent layers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Gravity and magnetic equivalent sources can now define the sensitivity +matrix ``J`` as a :class:`scipy.sparse.linalg.LinearOperator` when +``store_sensitivities="forward_only"``. This extends the behaviour previously +implemented in the integral gravity and magnetic simulations to the +equivalent layer classes. + +See https://github.com/simpeg/simpeg/pull/1674 and +https://github.com/simpeg/simpeg/pull/1676 for more details. + +Choosing the default solver is easier now +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :func:`~simpeg.utils.get_default_solver` can now be imported directly from +:mod:`simpeg.utils` making it easier to use it. + +Check out the new How to Guide on :ref:`choosing-solvers` for more information +on how we can use this function. + +Improved inversion printout +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The information table displayed during an inversion has been improved. Now, the +zero-th iteration corresponds to the status of the inversion problem **before** +any optimization steps, while subsequent iterations show information **after** +each iteration but before directives are applied. +The final iteration of the inversion is shown in the last row of the table. +Additionally, some non-very-useful messages have been removed to produce +a cleaner output. + +See https://github.com/simpeg/simpeg/pull/1626 for more details. + +Standardized directives for saving outputs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Directives that store and save inversion outputs have been +standardized and made more reliable. They now respect the output directory +chosen by the user, and output filenames follow a standardized +``name-timestamp-iteration`` format to make it easier to sort and identify +files from different inversions. + +See https://github.com/simpeg/simpeg/pull/1657 for more details. + +Updates to the Conjugate Gradient minimizers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The conjugate gradient minimizers were updated to be consistent with the latest +versions of SciPy. They can now accept both relative and absolute tolerances +through the ``cg_rtol`` and ``cg_atol`` arguments, respectively. + +The ``tolCG`` argument will be removed in the future, making ``cg_rtol`` and +``cg_atol`` the preferred way to set tolerances in these minimizers. + +See https://github.com/simpeg/simpeg/pull/1656 for more details. + + + +Documentation +------------- + +This release introduces a fresh new landing page for SimPEG docs, and a new +**How to Guide** section in our :ref:`user_guide` with pages on +:ref:`choosing-solvers` and :ref:`how-to-move-mesh`. + +We also included a new page that clarifies Python and Numpy +:ref:`version-compatibility` with SimPEG, and explain the criteria for dropping +older versions of our dependencies. + +We started removing the gravity, magnetic and DC tutorials from SimPEG's docs, +as part of our plan of moving all tutorials to our `User Tutorials +`_. + +Now we can navigate our docs using our arrow keys in the keyboard (for those +power users that don't want to leave the keyboard) thanks to `@prisae +`__. + +Finally, we improved and fixed a few things in the docs: mathematical +expressions, added missing classes to the API reference, updated admonitions in +docstrings, and more. + +Bugfixes +-------- + +In this release we included a few bugfixes: + +- Fixes sign error in 1D field calculation. by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1662 +- Fix beta cooling in ``UpdateIRLS`` directive by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1659 +- Fix bug in phase for recursive 1d NSEM simulation by `@dccowan `__ in + https://github.com/simpeg/simpeg/pull/1679 +- Fix bug on ``Impedance.eval`` when orientation is “xx” or “yy” by + `@dccowan `__ in https://github.com/simpeg/simpeg/pull/1692 +- Fix magnetic dipole source for for HJ formulation by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1575 +- Fix bug with duplicated current in ``LineCurrent.Mejs`` by `@santisoler `__ + in https://github.com/simpeg/simpeg/pull/1718 + +Breaking changes +---------------- + +We introduced a few breaking changes in SimPEG v0.25.0. + +Dropped support for Python 3.10 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We dropped support for Python 3.10, inline with our +:ref:`version-compatibility` schedule. So, remember to use Python 3.11 or higher +when installing SimPEG v0.25.0. If you still need to use Python 3.10, please +pin your SimPEG version to v0.24.0. + +Modified how mappings are applied in regularizations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We updated how mappings are applied in most of our regularization classes +(:class:`~simpeg.regularization.WeightedLeastSquares`, +:class:`~simpeg.regularization.Smallness`, +:class:`~simpeg.regularization.SmoothnessFirstOrder`, +:class:`~simpeg.regularization.Sparse`, +etc.). The ``mapping`` was applied, for example in the +:class:`~simpeg.regularization.Smallness` regularization, to the difference +between the ``model`` and the ``reference_model``: + +.. math:: + + \phi (\mathbf{m}) = + \left\lVert + \mathbf{W} \left[ \mu(\mathbf{m} - \mathbf{m}^\text{ref}) \right] + \right\rVert^2. + +where :math:`\mu()` is the ``mapping``. + +Since SimPEG v0.25.0 the regularizations are applied to the difference between +the *mapped* model and the *mapped* regularization model: + +.. math:: + + \phi (\mathbf{m}) = + \left\lVert + \mathbf{W} \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \right\rVert^2. + +This impacts only non-linear mappings, since the two expressions are equivalent +for linear ones. + +Changed the output of :func:`~simpeg.utils.model_builder.get_indices_block` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :func:`~simpeg.utils.model_builder.get_indices_block` function previously +returned a tuple with just a single element: the array with cell indices that +correspond the given block. We standardized its output to be in agreement with +similar functions in the module. It now returns a single NumPy array with the +cell indices of the block. + +If you were using this function as follows, where you used to extract the first +element of the tuple: + +.. code:: python + + ind = get_indices_block(p0, p1, mesh.cell_centers)[0] + +You'll need to update it to: + +.. code:: python + + ind = get_indices_block(p0, p1, mesh.cell_centers) + +An informative warning will be printed out every time the function is used to +remind users of this new behaviour. + +Removals +~~~~~~~~ + +We also removed several deprecated items marked for removal in previous +releases, including: + +- The ``Data.index_dictionary`` property. Use the new ``get_slice`` method of + ``Survey`` (for example: + :meth:`simpeg.potential_fields.gravity.Survey.get_slice`). +- The ``gtg_diagonal`` property from gravity simulation. +- The ``components`` property from gravity and magnetic surveys. + + +Contributors +============ + +Contributors: + +* `@dccowan `__ +* `@jcapriot `__ +* `@johnweis0480 `__ +* `@lheagy `__ +* `@prisae `__ +* `@santisoler `__ +* `@williamjsdavis `__ +* `@YingHuuu `__ +* `@domfournier `__ + + +Pull Requests +============= + +- Update docstring descriptions for gravity gradient component guv by + `@williamjsdavis `__ in https://github.com/simpeg/simpeg/pull/1665 +- Clean up Numba functions for potential field simulations by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1663 +- Make directives submodules private by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1667 +- Ensure misfit is purely real valued by `@prisae `__ in + https://github.com/simpeg/simpeg/pull/1524 +- Add key navigation to docs by `@prisae `__ in + https://github.com/simpeg/simpeg/pull/1668 +- Add missing map classes to the API reference by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1672 +- Replace sklearn deprecated method for ``validate_data`` function by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1673 +- Remove ``BaseSurvey.counter`` property by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1640 +- Fixes sign error in 1D field calculation. by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1662 +- Allow use of ``J`` as ``LinearOperator`` in mag equivalent layers by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1676 +- Fix beta cooling in ``UpdateIRLS`` directive by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1659 +- Allow use of ``J`` as ``LinearOperator`` in gravity equivalent layers + by `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1674 +- Improve admonitions in gravity simulation by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1677 +- Have an option to take a step when the Linesearch breaks by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1581 +- Fix bug in phase for recursive 1d NSEM simulation by `@dccowan `__ in + https://github.com/simpeg/simpeg/pull/1679 +- Use conda-forge as only channel in Azure pipelines by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1688 +- Expose solver utility functions in ``simpeg.utils`` by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1678 +- Use logging while setting default solver in PDE simulations by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1670 +- Use ``Impedance`` and ``Tipper`` in examples and tests by `@santisoler `__ + in https://github.com/simpeg/simpeg/pull/1690 +- Fix bug on ``Impedance.eval`` when orientation is “xx” or “yy” by + `@dccowan `__ in https://github.com/simpeg/simpeg/pull/1692 +- Remove deprecated objects missed in v0.24.0 by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1658 +- Update magnetic simulation using differential formulation by + `@johnweis0480 `__ in https://github.com/simpeg/simpeg/pull/1682 +- Standardize output directives and make them more reliable by `@jcapriot `__ + in https://github.com/simpeg/simpeg/pull/1657 +- Make tests error on implicit complex to real by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1696 +- Avoids calculating unused values for boundary conditions on DC 2D + simulations by `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1698 +- Add How to Guide page on how to choose a solver by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1695 +- Make Logger a bit quieter when running pytest by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1697 +- CG Minimizer Updates by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1656 +- Add top level descriptions to missing to functions by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1702 +- Update meeting times in README.rst by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1700 +- Add ``_faceDiv`` attribute to FDEM H ``Fields`` by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1346 +- Improve landing page of docs by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1701 +- Add How to Guide page on moving mesh to survey area by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1699 +- Remove gravity and magnetic tutorials by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1704 +- Minor fixes to docs of ``UpdateSensitivityWeights`` by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1705 +- Update iteration print out by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1626 +- Fix magnetic dipole source for for HJ formulation by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1575 +- Drop support for Python 3.10 by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1708 +- Add documentation page for version compatibility by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1707 +- Remove DC resistivity tutorials by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1710 +- Improve dipole source tests by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1711 +- Update deprecated calls in examples, tutorials, and tests to inexact + CG minimizers by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1703 +- Make ``ComplexMap.deriv`` to return a sparse diagonal matrix by + `@lheagy `__ in https://github.com/simpeg/simpeg/pull/1686 +- Standardize signature of mappings’ ``deriv`` method by `@YingHuuu `__ in + https://github.com/simpeg/simpeg/pull/1407 +- Update how mappings are applied in regularizations by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1605 +- Simple fix for pymatsolver 0.4.0 by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1717 +- Fix bug with duplicated current in ``LineCurrent.Mejs`` by `@santisoler `__ + in https://github.com/simpeg/simpeg/pull/1718 +- Minor fixes to LaTeX equations in regularizations by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1720 +- Fix return of ``get_indices_block`` by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1713 +- Remove deprecated bits marked for removal in v0.25.0 by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1719 +- Add shift to discrete topography for NSEM by `@dccowan `__ in + https://github.com/simpeg/simpeg/pull/1683 +- Deprecate unused arguments in ``drape_electrodes_on_topography`` by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1723 +- Fix LaTeX equations in ``Simulation3DDifferential`` by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1726 +- Implement a closed loop as a TDEM source by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1651 +- Allow for specifying ramp start and ramp end in ``RampOffWaveform`` by + `@jcapriot `__ in https://github.com/simpeg/simpeg/pull/1714 diff --git a/docs/content/release/index.rst b/docs/content/release/index.rst index 49daf1cfc9..dfa6fc1f20 100644 --- a/docs/content/release/index.rst +++ b/docs/content/release/index.rst @@ -1,3 +1,5 @@ +.. _release_notes: + ************* Release Notes ************* @@ -5,6 +7,12 @@ Release Notes .. toctree:: :maxdepth: 2 + 0.25.0 <0.25.0-notes> + 0.24.0 <0.24.0-notes> + 0.23.0 <0.23.0-notes> + 0.22.2 <0.22.2-notes> + 0.22.1 <0.22.1-notes> + 0.22.0 <0.22.0-notes> 0.21.1 <0.21.1-notes> 0.21.0 <0.21.0-notes> 0.20.0 <0.20.0-notes> diff --git a/docs/content/user-guide/getting-started/about-simpeg.rst b/docs/content/user-guide/getting-started/about-simpeg.rst new file mode 100644 index 0000000000..225122ca43 --- /dev/null +++ b/docs/content/user-guide/getting-started/about-simpeg.rst @@ -0,0 +1,44 @@ +.. _about_simpeg: + +============== +What's SimPEG? +============== + +SimPEG (Simulation and Parameter Estimation in Geophysics) is a +**Python library** for simulations, inversions and parameter estimation for +geophysical applications. + +The vision is to create a package for finite volume simulation with +applications to geophysical imaging and subsurface flow. To enable the +understanding of the many different components, this package has the following +features: + +* Modular with respect to the spacial discretization, optimization routine, and + geophysical problem. +* Built with the inverse problem in mind. +* Provides a framework for geophysical and hydrogeologic problems. +* Supports 1D, 2D and 3D problems. +* Designed for large-scale inversions. + +Overview Talk at SciPy 2016 +--------------------------- + +.. raw:: html + + diff --git a/docs/content/getting_started/big_picture.rst b/docs/content/user-guide/getting-started/big_picture.rst similarity index 90% rename from docs/content/getting_started/big_picture.rst rename to docs/content/user-guide/getting-started/big_picture.rst index ee3be2f3a3..8a152f7b5a 100644 --- a/docs/content/getting_started/big_picture.rst +++ b/docs/content/user-guide/getting-started/big_picture.rst @@ -67,7 +67,7 @@ implementation is a model, which, prior to interpretation, must be evaluated. This requires considering, and often re-assessing, the choices and assumptions made in both the input and implementation stages. -.. image:: ../../images/InversionWorkflow-PreSimPEG.png +.. image:: ../../../images/InversionWorkflow-PreSimPEG.png :width: 400 px :alt: Components :align: center @@ -86,7 +86,7 @@ of inversions into various units. We present it in this specific modular style, as each unit contains a targeted subset of choices crucial to the inversion process. -.. image:: ../../images/InversionWorkflow.png +.. image:: ../../../images/InversionWorkflow.png :width: 400 px :alt: Framework :align: center @@ -99,29 +99,29 @@ empirical by nature and our software package is designed to facilitate this iterative process. To accomplish this, we have divided the inversion methodology into eight major components (See figure above). The :class:`discretize.base.BaseMesh` class handles the discretization of the -earth and also provides numerical operators. The :class:`SimPEG.survey.BaseSurvey` +earth and also provides numerical operators. The :class:`simpeg.survey.BaseSurvey` class handles the geometry of a geophysical problem as well as sources and -receivers. The :class:`SimPEG.simulation.BaseSimulation` class handles the +receivers. The :class:`simpeg.simulation.BaseSimulation` class handles the simulation of the physics for the geophysical problem of interest. The -:class:`SimPEG.simulation.BaseSimulation` creates geophysical fields given a -source from the :class:`SimPEG.survey.BaseSurvey`, interpolates these fields to +:class:`simpeg.simulation.BaseSimulation` creates geophysical fields given a +source from the :class:`simpeg.survey.BaseSurvey`, interpolates these fields to the receiver locations, and converts them to the appropriate data type, for example, by selecting only the measured components of the field. Each of these operations may have associated derivatives with respect to the model and the computed field; these are included in the calculation of the sensitivity. For -the inversion, a :class:`SimPEG.data_misfit.BaseDataMisfit` is chosen to capture +the inversion, a :class:`simpeg.data_misfit.BaseDataMisfit` is chosen to capture the goodness of fit of the predicted data and a -:class:`SimPEG.regularization.BaseRegularization` is chosen to handle the non- +:class:`simpeg.regularization.BaseRegularization` is chosen to handle the non- uniqueness. These inversion elements and an Optimization routine are combined -into an inverse problem class :class:`SimPEG.inverse_problem.BaseInvProblem`. -:class:`SimPEG.inverse_problem.BaseInvProblem` is the mathematical statement that +into an inverse problem class :class:`simpeg.inverse_problem.BaseInvProblem`. +:class:`simpeg.inverse_problem.BaseInvProblem` is the mathematical statement that will be numerically solved by running an Inversion. The -:class:`SimPEG.inversion.BaseInversion` class handles organization and +:class:`simpeg.inversion.BaseInversion` class handles organization and dispatch of directives between all of the various pieces of the framework. The arrows in the figure above indicate what each class takes as a primary -argument. For example, both the :class:`SimPEG.simulation.BaseSimulation` and -:class:`SimPEG.regularization.BaseRegularization` classes take a +argument. For example, both the :class:`simpeg.simulation.BaseSimulation` and +:class:`simpeg.regularization.BaseRegularization` classes take a :class:`discretize.base.BaseMesh` class as an argument. The diagram does not show class inheritance, as each of the base classes outlined have many subtypes that can be interchanged. The :class:`discretize.base.BaseMesh` @@ -133,15 +133,7 @@ be exploited through inheritance of base classes, and differences can be expressed through subtype polymorphism. Please look at the documentation here for more in-depth information. - -.. include:: ../../../CITATION.rst - -Authors -------- - -.. include:: ../../../AUTHORS.rst - License ------- -.. include:: ../../../LICENSE +.. include:: ../../../../LICENSE diff --git a/docs/content/user-guide/getting-started/citing.rst b/docs/content/user-guide/getting-started/citing.rst new file mode 100644 index 0000000000..1ae6828364 --- /dev/null +++ b/docs/content/user-guide/getting-started/citing.rst @@ -0,0 +1,3 @@ +.. _citing: + +.. include:: ../../../../CITATION.rst diff --git a/docs/content/getting_started/contributing/advanced.rst b/docs/content/user-guide/getting-started/contributing/advanced.rst similarity index 100% rename from docs/content/getting_started/contributing/advanced.rst rename to docs/content/user-guide/getting-started/contributing/advanced.rst diff --git a/docs/content/getting_started/contributing/code-style.rst b/docs/content/user-guide/getting-started/contributing/code-style.rst similarity index 97% rename from docs/content/getting_started/contributing/code-style.rst rename to docs/content/user-guide/getting-started/contributing/code-style.rst index 8a021f333a..3202d48ac2 100644 --- a/docs/content/getting_started/contributing/code-style.rst +++ b/docs/content/user-guide/getting-started/contributing/code-style.rst @@ -20,7 +20,7 @@ Run ``black`` on SimPEG directories that contain Python source files: .. code:: - black SimPEG examples tutorials tests + black . Run ``flake8`` on the whole project with: diff --git a/docs/content/getting_started/contributing/documentation.rst b/docs/content/user-guide/getting-started/contributing/documentation.rst similarity index 74% rename from docs/content/getting_started/contributing/documentation.rst rename to docs/content/user-guide/getting-started/contributing/documentation.rst index d4bda313e0..0d5f8e3962 100644 --- a/docs/content/getting_started/contributing/documentation.rst +++ b/docs/content/user-guide/getting-started/contributing/documentation.rst @@ -39,7 +39,7 @@ For example: Second order smoothness weights for the respective dimensions. length_scale_x, length_scale_y, length_scale_z : float, optional First order smoothness length scales for the respective dimensions. - mapping : SimPEG.maps.IdentityMap, optional + mapping : simpeg.maps.IdentityMap, optional A mapping to apply to the model before regularization. reference_model : array_like, optional reference_model_in_smooth : bool, optional @@ -70,18 +70,44 @@ For example: Building the documentation ~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you would like to see the documentation changes. -In the repo's root directory, enter the following in your terminal. +You can build the documentation pages locally to see how the new changes will +look. First, make sure that you have :ref:`created and activated an environment +` with simpeg installed in it. Then, navigate to the +``docs`` folder: -.. code:: +.. code:: bash - make all + cd docs -Serving the documentation locally +And run the following to build the docs: + +.. code:: bash + + make html + +.. note:: + + This command will build all documentation pages, including all the examples + and tutorials. Running the examples might take considerable amount of time. + + If you want to build the docs, but avoid running the examples, you can + alternatively run: + + .. code:: bash + + make html-noplot + +Serving the documentation locally ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Once the documentation is built. You can view it directly using the following command. This will automatically serve the docs and you can see them in your browser. +Once the documentation is built, you can view it using the following +command (make sure you are in the ``docs`` directory): -.. code:: +.. code:: bash make serve + +It will automatically serve the docs and you can see them in your browser. + +Alternatively, you can open your file manager and open the ``index.html`` file +in the ``docs/_build/html`` folder. diff --git a/docs/content/getting_started/contributing/index.rst b/docs/content/user-guide/getting-started/contributing/index.rst similarity index 100% rename from docs/content/getting_started/contributing/index.rst rename to docs/content/user-guide/getting-started/contributing/index.rst diff --git a/docs/content/getting_started/contributing/pull-requests.rst b/docs/content/user-guide/getting-started/contributing/pull-requests.rst similarity index 100% rename from docs/content/getting_started/contributing/pull-requests.rst rename to docs/content/user-guide/getting-started/contributing/pull-requests.rst diff --git a/docs/content/getting_started/contributing/setting-up-environment.rst b/docs/content/user-guide/getting-started/contributing/setting-up-environment.rst similarity index 64% rename from docs/content/getting_started/contributing/setting-up-environment.rst rename to docs/content/user-guide/getting-started/contributing/setting-up-environment.rst index 0ec601f540..c5dd3a95ce 100644 --- a/docs/content/getting_started/contributing/setting-up-environment.rst +++ b/docs/content/user-guide/getting-started/contributing/setting-up-environment.rst @@ -7,18 +7,17 @@ Install Python -------------- First you will need to install Python. You can find instructions in -:ref:`installing_python`. We highly encourage to install Anaconda_ or -Mambaforge_. +:ref:`installing_python`. We highly encourage to install Miniforge_ (or +Anaconda_). Create environment ------------------ To get started developing SimPEG we recommend setting up an environment using -the ``conda`` (or ``mamba``) package manager that mimics the testing -environment used for continuous integration testing. Most of the packages that -we use are available through the ``conda-forge`` project. This will ensure you -have all of the necessary packages to both develop SimPEG and run tests -locally. We provide an ``environment_test.yml`` in the base level directory. +the ``conda`` package manager that includes all odthe packages necessary to +both develop SimPEG and run tests locally. Most of the packages that +we use are available through the ``conda-forge`` project. +We provide an ``environment.yml`` in the base level directory. To create the environment and install all packages needed to run and write code for SimPEG, navigate to the directory where you :ref:`cloned SimPEG's @@ -26,21 +25,29 @@ repository ` and run: .. code:: - conda env create -f environment_test.yml + conda env create -f environment.yml .. note:: - If you find yourself wanting a faster package manager than ``conda`` - check out the ``mamba`` project at https://mamba.readthedocs.io/. It - usually is able to set up environments much quicker than ``conda`` and - can be used as a drop-in replacement (i.e. replace ``conda`` commands with - ``mamba``). + Since `version 23.10.0 + `_, + ``conda`` makes use of the ``libmamba`` solver to resolve dependencies. It + makes creation of environments and installation of new packages much faster + than when using older versions of ``conda``. + + Since this version, ``conda`` can achieve the same performance as + ``mamba``, so there's no need to install ``mamba`` if you have an updated + version of ``conda``. + If not, either `update conda + `_, or + keep using ``mamba`` instead. + Once the environment is successfully created, you can *activate* it with .. code:: - conda activate simpeg-test + conda activate simpeg-dev Install SimPEG in developer mode @@ -48,7 +55,7 @@ Install SimPEG in developer mode There are many options to install SimPEG into this local environment, we recommend using `pip`. After ensuring that all necessary packages from -`environment_test.yml` are installed, the most robust command you can use, +`environment.yml` are installed, the most robust command you can use, executed from the base level directory would be: .. code:: @@ -64,7 +71,7 @@ This practice also allows you to uninstall SimPEG if so desired: .. code:: - pip uninstall SimPEG + pip uninstall simpeg .. note:: @@ -72,12 +79,12 @@ This practice also allows you to uninstall SimPEG if so desired: a way to install SimPEG for developers. .. _Anaconda: https://www.anaconda.com/products/individual -.. _Mambaforge: https://www.anaconda.com/products/individual +.. _Miniforge: https://github.com/conda-forge/miniforge Check your installation ----------------------- -You should be able to open a terminal within SimPEG/tutorials and run an +You should be able to open a terminal within simpeg/tutorials and run an example, i.e. .. code:: @@ -138,3 +145,19 @@ you want to commit them nonetheless. .. _pre-commit: https://pre-commit.com/ .. _Black: https://black.readthedocs.io .. _flake8: https://flake8.pycqa.org + + +Update your environment +----------------------- + +Every once in a while, the minimum versions of the packages in the +``environment.yml`` file get updated. After this happens, it's better to update +the ``simpeg-dev`` environment we have created. This way we ensure that we are +checking the style and testing our code using those updated versions. + +To update our environment we need to navigate to the directory where you +:ref:`cloned SimPEG's repository ` and run: + +.. code:: + + conda env update -f environment.yml diff --git a/docs/content/getting_started/contributing/testing.rst b/docs/content/user-guide/getting-started/contributing/testing.rst similarity index 98% rename from docs/content/getting_started/contributing/testing.rst rename to docs/content/user-guide/getting-started/contributing/testing.rst index ba4e207e86..282bc90997 100644 --- a/docs/content/getting_started/contributing/testing.rst +++ b/docs/content/user-guide/getting-started/contributing/testing.rst @@ -58,7 +58,7 @@ the ``numpy.testing`` module to check for approximate equals. For instance, import numpy as np import discretize - from SimPEG import maps + from simpeg import maps def test_map_multiplication(self): mesh = discretize.TensorMesh([2,3]) @@ -131,7 +131,7 @@ have first order convergence (e.g. the improvement in the approximation is directly related to how small :math:`\Delta x` is, while if we include the first derivative in our approximation, we expect that :math:`\|f(x) + J(x)\Delta x - f(x + \Delta x)\|` to converge at a second-order rate. For -example, all `maps have an associated derivative test `_ . An example from `test_FDEM_derivs.py `_ . An example from `test_FDEM_derivs.py `_ diff --git a/docs/content/getting_started/contributing/working-with-github.rst b/docs/content/user-guide/getting-started/contributing/working-with-github.rst similarity index 86% rename from docs/content/getting_started/contributing/working-with-github.rst rename to docs/content/user-guide/getting-started/contributing/working-with-github.rst index 2876c1e0c2..979cab63ef 100644 --- a/docs/content/getting_started/contributing/working-with-github.rst +++ b/docs/content/user-guide/getting-started/contributing/working-with-github.rst @@ -3,7 +3,7 @@ Working with Git and GitHub --------------------------- -.. image:: https://github.githubassets.com/images/modules/logos_page/Octocat.png +.. image:: https://octodex.github.com/images/original.png :align: right :width: 100 :target: https://github.com @@ -21,11 +21,11 @@ There are two ways you can clone a repository: 1. From a terminal (checkout: https://docs.github.com/en/get-started/quickstart/set-up-git for an tutorial) :: - git clone https://github.com/YOUR-USERNAME/SimPEG + git clone https://github.com/YOUR-USERNAME/simpeg 2. Using a desktop client such as SourceTree_ or GitKraken_. - .. image:: ../../../images/sourceTreeSimPEG.png + .. image:: ../../../../images/sourceTreeSimPEG.png :align: center :width: 400 :target: https://www.sourcetreeapp.com/ @@ -34,7 +34,7 @@ There are two ways you can clone a repository: it is also handy to set up the remote account so it remembers your github_ user name and password - .. image:: ../../../images/sourceTreeRemote.png + .. image:: ../../../../images/sourceTreeRemote.png :align: center :width: 400 diff --git a/docs/content/user-guide/getting-started/installing.rst b/docs/content/user-guide/getting-started/installing.rst new file mode 100644 index 0000000000..f3233d4a76 --- /dev/null +++ b/docs/content/user-guide/getting-started/installing.rst @@ -0,0 +1,173 @@ +.. _installing: + +========== +Installing +========== + + +.. _installing_python: + +Installing Python +================= + +SimPEG is written in Python_! +This means we need Python_ in order to run SimPEG. +We highly recommend installing a Python distribution like Miniforge_ that will +install the Python interpreter along with the conda_ package manager. + +.. note:: + + Miniforge_ is a community-driven alternative to Anaconda_, a well-known + Python distribution. + + We recommend Miniforge_ over Anaconda_ because it's more lightweight and + because it makes use of the conda-forge_ community-led channel to download + packages. Downloading packages from Anaconda_ (usually refered as the + ``default`` channel) requires us to adhere to their `Terms of Service + `_. + Make sure to read them and their `FAQs + `_ if you decide to + still use Anaconda_. + +.. seealso:: + + If you are starting with Python_ and want to learn more and feel more + comfortable with the language, we recommend checking out + `Software Carpentry `_'s lessons. + +.. _Python: https://www.python.org/ +.. _Anaconda: https://www.anaconda.com/products/individual +.. _Miniforge: https://github.com/conda-forge/miniforge +.. _conda: https://docs.conda.io/en/latest +.. _conda-forge: https://conda-forge.org/ + + +.. _installing_simpeg: + +Installing SimPEG +================= + +Conda Forge +----------- + +SimPEG is available through conda-forge_ and you can install is using the +`conda package manager `_ that comes with Miniforge_ (or +Anaconda_): + +.. code:: bash + + conda install --channel conda-forge simpeg + +.. note:: + + Installing through ``conda`` is our recommended method of installation. + +.. note:: + + Since `version 23.10.0 + `_, + ``conda`` makes use of the ``libmamba`` solver to resolve dependencies. It + makes creation of environments and installation of new packages much faster + than when using older versions of ``conda``. + + Since this version, ``conda`` can achieve the same performance as + ``mamba``, so there's no need to install ``mamba`` if you have an updated + version of ``conda``. + If not, either `update conda + `_, or + keep using ``mamba`` instead. + +PyPi +---- + +SimPEG is on `pypi `_! First, make sure +your version of pip is up-to-date + +.. code:: bash + + pip install --upgrade pip + +Then you can install SimPEG + +.. code:: bash + + pip install simpeg + + +To update SimPEG, you can run + +.. code:: bash + + pip install --upgrade simpeg + + +Installing from Source +---------------------- + +First (you need git): + +.. code:: bash + + git clone https://github.com/simpeg/simpeg + +Second (from the root of the SimPEG repository): + +.. code:: bash + + pip install . + +If you are interested in contributing to SimPEG, please check out the page on :ref:`Contributing ` + + +Success? +======== + +If you have been successful at downloading and installing SimPEG, you should +be able to download and run any of the :ref:`examples and tutorials `. + +If not, you can reach out to other people developing and using SimPEG on our +Mattermost_ channel or in our `Discourse forum`_. + +.. _Discourse forum: https://simpeg.discourse.group/ +.. _Mattermost: https://mattermost.softwareunderground.org/simpeg + +Useful Links +============ + +An enormous amount of information (including tutorials and examples) can be found on the official websites of the packages + +* `Python `_ +* `Numpy `_ +* `SciPy `_ +* `Matplotlib `_ + +Python for scientific computing +------------------------------- + +* `Python for Scientists `_ Links to commonly used packages, Matlab to Python comparison +* `Python Wiki `_ Lists packages and resources for scientific computing in Python +* `Jupyter `_ + +Numpy and Matlab +---------------- + +* `NumPy for Matlab Users `_ +* `Python vs Matlab `_ + +Lessons in Python +----------------- + +* `Software Carpentry `_ +* `Introduction to NumPy and Matplotlib `_ + + +Editing Python +-------------- + +There are numerous ways to edit and test Python (see +`PythonWiki `_ for an overview) and +in our group at least the following options are being used: + +* `Jupyter `_ +* `Sublime `_ +* `PyCharm `_ diff --git a/docs/content/user-guide/getting-started/version-compatibility.rst b/docs/content/user-guide/getting-started/version-compatibility.rst new file mode 100644 index 0000000000..b4d5117c6d --- /dev/null +++ b/docs/content/user-guide/getting-started/version-compatibility.rst @@ -0,0 +1,38 @@ +.. _version-compatibility: + +Version compatibility +===================== + +SimPEG follows the time-window based policy for support of Python and Numpy +versions introduced in `NEP29 +`_. In summary, SimPEG supports: + +- all minor versions of Python released in the **prior 42 months** before + a SimPEG release, and +- all minor versions of Numpy released in the **prior 24 months** before + a SimPEG release. + +We follow these guidelines conservatively, meaning that we might not drop +support for older versions of our dependencies if they are not causing any +issue. We include notes in the :ref:`release_notes` every time we drop support +for a Python or Numpy version. + + +Supported Python versions +------------------------- + +If you require support for older Python versions, please pin SimPEG to the +following releases to ensure compatibility: + + +.. list-table:: + :widths: 40 60 + + * - **Python version** + - **Last compatible release** + * - 3.8 + - 0.22.2 + * - 3.9 + - 0.22.2 + * - 3.10 + - 0.24.0 diff --git a/docs/content/user-guide/how-to-guide/choosing-solvers.rst b/docs/content/user-guide/how-to-guide/choosing-solvers.rst new file mode 100644 index 0000000000..b46c12da9e --- /dev/null +++ b/docs/content/user-guide/how-to-guide/choosing-solvers.rst @@ -0,0 +1,110 @@ +.. _choosing-solvers: + +================ +Choosing solvers +================ + +Several simulations available in SimPEG need to numerically solve a partial +differential equations system (PDE), such as +:class:`~simpeg.electromagnetics.static.resistivity.Simulation3DNodal` (DC) +:class:`~simpeg.electromagnetics.time_domain.Simulation3DMagneticFluxDensity` +(TDEM) +and +:class:`~simpeg.electromagnetics.frequency_domain.Simulation3DMagneticFluxDensity` +(FDEM). +A numerical solver is needed to solve the PDEs. +SimPEG can make use of the solvers available in :mod:`pymatsolver`, like +:class:`pymatsolver.Pardiso`, :class:`pymatsolver.Mumps` or +:class:`pymatsolver.SolverLU`. +The choice of an appropriate solver can affect the computation time required to +solve the PDE. Generally we recommend using direct solvers over iterative solvers +for SimPEG, but be aware that direct solvers have much larger memory requirements. + +The ``Pardiso`` solver wraps the `oneMKL PARDISO +`_ +solver available for x86_64 CPUs. + +The ``Mumps`` solver wraps `MUMPS +`_, a fast solver available for +all CPU brands, including Apple silicon architecture. + +The ``SolverLU`` wraps SciPy's :func:`scipy.sparse.linalg.splu`. The +performance of this solver is not up to the level of ``Mumps`` and ``Pardiso``. +Usage of the ``SolveLU`` is recommended only when it's not possible to use +other faster solvers. + + +The default solver +------------------ + +We can use :func:`simpeg.utils.get_default_solver` to obtain a reasonable default +solver available for our system: + +.. code:: python + + import simpeg + import simpeg.electromagnetics.static.resistivity as dc + + # Get default solver + solver = simpeg.utils.get_default_solver() + print(f"Solver: {solver}") + +which would print out on an x86_64 cpu: + +.. code:: + + Solver: + +We can then use this solver in a simulation: + +.. code:: python + + # Define a simple mesh + h = [(1.0, 10)] + mesh = discretize.TensorMesh([h, h, h], origin="CCC") + + # And a DC survey + receiver = dc.receivers.Dipole(locations_m=(-1, 0, 0), locations_n=(1, 0, 0)) + source = dc.sources.Dipole( + receiver_list=[receiver], location_a=(-2, 0, 0), location_b=(2, 0, 0) + ) + survey = dc.Survey([source]) + + # Use the default solver in the simulation + simulation = dc.Simulation3DNodal(mesh=mesh, survey=survey, solver=solver) + +.. note:: + + The priority list used to choose a default solver is: + + 1) ``Pardiso`` + 2) ``Mumps`` + 3) ``SolverLU`` + + +Setting solvers manually +------------------------ + +Alternatively, we can manually set a solver. For example, if we want to use +``Mumps`` in our DC resistivity simulation, we can import +:class:`pymatsolver.Mumps` and pass it to our simulation: + +.. code:: python + + import simpeg.electromagnetics.static.resistivity as dc + from pymatsolver import Mumps + + # Manually set Mumps as our solver + simulation = dc.Simulation3DNodal(mesh=mesh, survey=survey, solver=Mumps) + +.. note:: + + When sharing your notebook or script with a colleague, keep in mind that + your code might not work if ``Pardiso`` is not available in their system. + + For such scenarios, we recommend using the + :func:`simpeg.utils.get_default_solver` function, that will always return + a suitable solver for the current system. + +Ultimately, choosing the best solver is a mixture of the problem you are solving and your current system. Experiment with different solvers yourself to choose the best. + diff --git a/docs/content/user-guide/how-to-guide/move-mesh-to-survey.rst b/docs/content/user-guide/how-to-guide/move-mesh-to-survey.rst new file mode 100644 index 0000000000..0d0c0a84a3 --- /dev/null +++ b/docs/content/user-guide/how-to-guide/move-mesh-to-survey.rst @@ -0,0 +1,204 @@ +.. _how-to-move-mesh: + +============================ +Locating mesh on survey area +============================ + +The :mod:`discretize` package allows us to define 3D meshes that can be used +for running SimPEG's forward and inverse problems. +Mesh dimensions for :class:`discretize.TensorMesh` and +:class:`discretize.TreeMesh` are assumed to be in meters, and by default their +origin (the westmost-southmost-lowest point) is located in the origin of the +coordinate system (the ``(0, 0, 0)``). + +When working with real-world data, we want our mesh to be located around the +survey area. We can move our mesh location by by shifting its ``origin``. + +For example, suppose we want to invert some magnetic data from Osborne Mine in +Australia that spans in a region between 448353.0 m and 482422.0 m along the +easting, and between 7578158.0 m and 7594834.0 m along the northing +(UTM zone 54). +Let's also assume that the maximum topographic height of the area is 417 +m. + +We can build a mesh that spans 34 km on the easting, 17 km on the northing, and +5500 m vertically: + +.. code:: python + + import discretize + + dx, dy, dz = 200.0, 200.0, 100.0 + nx, ny, nz = 170, 85, 55 + hx, hy, hz = [(dx, nx)], [(dy, ny)], [(dz, nz)] + + mesh = discretize.TensorMesh([hx, hy, hz]) + print(mesh) + + +.. code:: + + TensorMesh: 794,750 cells + + MESH EXTENT CELL WIDTH FACTOR + dir nC min max min max max + --- --- --------------------------- ------------------ ------ + x 170 0.00 34,000.00 200.00 200.00 1.00 + y 85 0.00 17,000.00 200.00 200.00 1.00 + z 55 0.00 5,500.00 100.00 100.00 1.00 + + +The ``origin`` of this mesh is located on ``(0, 0, 0)``. We can move it to the +survey area by shifting it to (448353.0 m, 7578158.0 m, -5000 m): + +.. code:: python + + mesh.origin = (448353.0, 7578158.0, -5000) + print(mesh) + +.. code:: + + TensorMesh: 794,750 cells + + MESH EXTENT CELL WIDTH FACTOR + dir nC min max min max max + --- --- --------------------------- ------------------ ------ + x 170 448,353.00 482,353.00 200.00 200.00 1.00 + y 85 7,578,158.00 7,595,158.00 200.00 200.00 1.00 + z 55 -5,000.00 500.00 100.00 100.00 1.00 + + +By shifting the ``origin`` we are not changing the number of cells in the mesh +nor their dimensions. We are just moving the location of the mesh in the three +directions. + +.. note:: + + We shift the z coordinate of the origin to -5000 m so we leave 500 m above + the zeroth height to possibly account for topography. + + +.. tip:: + + Alternatively, we can set the ``origin`` when defining the mesh, by passing + it as an argument. For example: + + .. code:: python + + origin = (448353.0, 7578158.0, -5000) + mesh = discretize.TensorMesh([hx, hy, hz], origin=origin) + print(mesh) + + +Considering padding: simple case +-------------------------------- + +It's best practice to add some padding to the mesh when using it in an +inversion. The padding cells will allocate any potential body outside the +survey area, which effect might be present in the data. + +Let's take the previous example and build a mesh that has 3 km of padding +on each horizontal direction: + +.. code:: python + + hx = [(200.0, 15), (dx, nx), (200.0, 15)] + hy = [(200.0, 15), (dy, ny), (200.0, 15)] + hz = [(dz, nz)] + + mesh = discretize.TensorMesh([hx, hy, hz]) + print(mesh) + +.. code:: + + TensorMesh: 1,265,000 cells + + MESH EXTENT CELL WIDTH FACTOR + dir nC min max min max max + --- --- --------------------------- ------------------ ------ + x 200 0.00 40,000.00 200.00 200.00 1.00 + y 115 0.00 23,000.00 200.00 200.00 1.00 + z 55 0.00 5,500.00 100.00 100.00 1.00 + +Now we can shift the ``origin``, but we also need to take into account the +padding cells. We will set the origin to the westmost-southmost corner of the +survey minus the padding distance we added to the mesh (3km): + +.. code:: python + + mesh.origin = (448353.0 - 3000, 7578158.0 - 3000, -5000) + print(mesh) + +.. code:: + + TensorMesh: 1,265,000 cells + + MESH EXTENT CELL WIDTH FACTOR + dir nC min max min max max + --- --- --------------------------- ------------------ ------ + x 200 445,353.00 485,353.00 200.00 200.00 1.00 + y 115 7,575,158.00 7,598,158.00 200.00 200.00 1.00 + z 55 -5,000.00 500.00 100.00 100.00 1.00 + + +Considering padding: padding factor +----------------------------------- + +Alternatively, we can introduce padding through a *padding factor*. Instead of +creating padding cells of the same size, we can use the padding factor to +create padding cells that increase in volume as they move away from the survey +area. +This is the usual approach to add padding cells to +:class:`discretize.TensorMesh` since it reduces the amount of cells in the +mesh, making inversions less expensive. + +Following the previous example, let's add 7 cells to each side of the +horizontal directions. Let's make the first cells the same size of the ones in +the mesh, and then start increasing their size with a factor of 1.5: + +.. code:: python + + n_pad_cells = 7 + factor = 1.5 + + hx = [(dx, n_pad_cells, -factor), (dx, nx), (dx, n_pad_cells, factor)] + hy = [(dy, n_pad_cells, -factor), (dy, ny), (dy, n_pad_cells, factor)] + hz = [(dz, nz)] + + mesh = discretize.TensorMesh([hx, hy, hz]) + print(mesh) + +.. code:: + + TensorMesh: 1,001,880 cells + + MESH EXTENT CELL WIDTH FACTOR + dir nC min max min max max + --- --- --------------------------- ------------------ ------ + x 184 0.00 53,303.12 200.00 3,417.19 1.50 + y 99 0.00 36,303.12 200.00 3,417.19 1.50 + z 55 0.00 5,500.00 100.00 100.00 1.00 + + +As before, we need to consider the padding cells when shifting the ``origin`` +of the mesh. Since we know that we added 7 cells to each side, we can leverage +that by shifting the 7th node of the x and y axes to the westmost-southmost +corner of the survey: + +.. code:: python + + x_node_7th = mesh.nodes_x[n_pad_cells] + y_node_7th = mesh.nodes_y[n_pad_cells] + mesh.origin = (448353.0 - x_node_7th, 7578158.0 - y_node_7th, -5000) + print(mesh) + +.. code:: + + TensorMesh: 1,001,880 cells + + MESH EXTENT CELL WIDTH FACTOR + dir nC min max min max max + --- --- --------------------------- ------------------ ------ + x 184 438,701.44 492,004.56 200.00 3,417.19 1.50 + y 99 7,568,506.44 7,604,809.56 200.00 3,417.19 1.50 + z 55 -5,000.00 500.00 100.00 100.00 1.00 diff --git a/docs/content/user-guide/index.rst b/docs/content/user-guide/index.rst new file mode 100644 index 0000000000..8ca423fb58 --- /dev/null +++ b/docs/content/user-guide/index.rst @@ -0,0 +1,43 @@ +.. _user_guide: + +SimPEG User Guide +================= + +This guide is aimed to help users to get started with SimPEG and to learn how +to simulate physics and run inversions for different types of geophysical data. + +For details on the available classes and functions in SimPEG, please visit the +:ref:`api`. + +.. toctree:: + :glob: + :maxdepth: 1 + :caption: Getting Started + + getting-started/about-simpeg.rst + getting-started/big_picture + getting-started/installing + getting-started/contributing/index.rst + getting-started/citing.rst + getting-started/version-compatibility.rst + +.. toctree:: + :glob: + :maxdepth: 1 + :caption: How to Guide + + how-to-guide/choosing-solvers + how-to-guide/move-mesh-to-survey.rst + +.. toctree:: + :glob: + :maxdepth: 1 + :caption: Tutorials + + tutorials/**/index + +.. toctree:: + :maxdepth: 2 + :caption: Examples + + examples/index diff --git a/docs/content/user_guide.rst b/docs/content/user_guide.rst deleted file mode 100644 index aeebe15070..0000000000 --- a/docs/content/user_guide.rst +++ /dev/null @@ -1,24 +0,0 @@ -.. _user_guide: - -========== -User Guide -========== - -We've included some tutorials and gallery examples that will walk you through using -discretize to solve your PDE. For more details on any of the functions, check out the -API documentation. - -Tutorials ---------- -.. toctree:: - :glob: - :maxdepth: 2 - - tutorials/**/index - -Examples --------- -.. toctree:: - :maxdepth: 1 - - examples/index diff --git a/docs/index.rst b/docs/index.rst index 7f742f6860..8769985ee9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,18 +1,90 @@ -.. include:: ../README.rst +:html_theme.sidebar_secondary.remove: true + +.. image:: ./images/simpeg-logo.png + :alt: SimPEG logo + +==================== +SimPEG Documentation +==================== + +Simulation and Parameter Estimation in Geophysics. +An open-source Python library for simulation, inversion and parameter +estimation for geophysical applications. + +**Useful links:** +:ref:`Install ` | +`GitHub Repository `_ | +`Bugs and Issues `_ | +`SimPEG website `_ | +`License `_ + +.. grid:: 1 2 1 2 + :margin: 5 5 0 0 + :padding: 0 0 0 0 + :gutter: 4 + + .. grid-item-card:: :fas:`book-open` User Guide + :text-align: center + :class-title: sd-fs-5 + :class-card: sd-p-3 + + Learn how to use SimPEG. + + .. button-ref:: user_guide + :ref-type: ref + :click-parent: + :color: primary + :shadow: + :expand: + + .. grid-item-card:: :fas:`book` User Tutorials + :text-align: center + :class-title: sd-fs-5 + :class-card: sd-p-3 + + Apply SimPEG to geophysical problems. + + .. button-link:: https://simpeg.xyz/user-tutorials + :click-parent: + :color: primary + :shadow: + :expand: + + Checkout the User Tutorials :octicon:`link-external` + + .. grid-item-card:: :fas:`code` API Reference + :text-align: center + :class-title: sd-fs-5 + :class-card: sd-p-3 + + A list of modules, classes and functions. + + .. button-ref:: api + :ref-type: ref + :color: primary + :shadow: + :expand: + + .. grid-item-card:: :fas:`comments` Contact + :text-align: center + :class-title: sd-fs-5 + :class-card: sd-p-3 + + Chat with the rest of the community. + + .. button-link:: https://mattermost.softwareunderground.org/simpeg + :click-parent: + :color: primary + :shadow: + :expand: + + Join our Mattermost channel :octicon:`link-external` .. toctree:: :maxdepth: 2 :hidden: :titlesonly: - content/getting_started/index - content/user_guide - content/api/index - content/release/index - -.. Project Index & Search -.. ====================== - -.. * :ref:`genindex` -.. * :ref:`modindex` -.. * :ref:`search` + User Guide + API Reference + Release Notes diff --git a/docs/old-docs-files.txt b/docs/old-docs-files.txt new file mode 100644 index 0000000000..9107882ac3 --- /dev/null +++ b/docs/old-docs-files.txt @@ -0,0 +1,154 @@ +# This file contains a list of old html files that got generated when building +# the docs (simpeg v0.23.0). The list is used to create sphinx-reredirects, so +# the old links to these files can be redirected to the new locations. +# See docs/conf.py for more details. +content/examples/01-maps/index.html +content/examples/01-maps/plot_block_in_layer.html +content/examples/01-maps/plot_combo.html +content/examples/01-maps/plot_layer.html +content/examples/01-maps/plot_mesh2mesh.html +content/examples/01-maps/plot_sumMap.html +content/examples/01-maps/sg_execution_times.html +content/examples/02-gravity/index.html +content/examples/02-gravity/plot_inv_grav_tiled.html +content/examples/02-gravity/sg_execution_times.html +content/examples/03-magnetics/index.html +content/examples/03-magnetics/plot_0_analytic.html +content/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.html +content/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.html +content/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.html +content/examples/03-magnetics/sg_execution_times.html +content/examples/04-dcip/index.html +content/examples/04-dcip/plot_dc_analytic.html +content/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.html +content/examples/04-dcip/plot_inv_dcip_dipoledipole_parametric_inversion.html +content/examples/04-dcip/plot_read_DC_data_with_IO_class.html +content/examples/04-dcip/sg_execution_times.html +content/examples/05-fdem/index.html +content/examples/05-fdem/plot_0_fdem_analytic.html +content/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.html +content/examples/05-fdem/sg_execution_times.html +content/examples/06-tdem/index.html +content/examples/06-tdem/plot_0_tdem_analytic.html +content/examples/06-tdem/plot_fwd_tdem_3d_model.html +content/examples/06-tdem/plot_fwd_tdem_inductive_src_permeable_target.html +content/examples/06-tdem/plot_fwd_tdem_waveforms.html +content/examples/06-tdem/plot_inv_tdem_1D.html +content/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.html +content/examples/06-tdem/sg_execution_times.html +content/examples/07-nsem/index.html +content/examples/07-nsem/plot_fwd_nsem_MTTipper3D.html +content/examples/07-nsem/sg_execution_times.html +content/examples/08-vrm/index.html +content/examples/08-vrm/plot_fwd_vrm.html +content/examples/08-vrm/plot_inv_vrm_eq.html +content/examples/08-vrm/sg_execution_times.html +content/examples/09-flow/index.html +content/examples/09-flow/plot_fwd_flow_richards_1D.html +content/examples/09-flow/plot_inv_flow_richards_1D.html +content/examples/09-flow/sg_execution_times.html +content/examples/10-pgi/index.html +content/examples/10-pgi/plot_inv_0_PGI_Linear_1D.html +content/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.html +content/examples/10-pgi/sg_execution_times.html +content/examples/20-published/index.html +content/examples/20-published/plot_booky_1Dstitched_resolve_inv.html +content/examples/20-published/plot_booky_1D_time_freq_inv.html +content/examples/20-published/plot_effective_medium_theory.html +content/examples/20-published/plot_heagyetal2017_casing.html +content/examples/20-published/plot_heagyetal2017_cyl_inversions.html +content/examples/20-published/plot_laguna_del_maule_inversion.html +content/examples/20-published/plot_load_booky.html +content/examples/20-published/plot_richards_celia1990.html +content/examples/20-published/plot_schenkel_morrison_casing.html +content/examples/20-published/plot_tomo_joint_with_volume.html +content/examples/20-published/plot_vadose_vangenuchten.html +content/examples/20-published/sg_execution_times.html +content/examples/index.html +content/examples/sg_execution_times.html +content/getting_started/big_picture.html +content/getting_started/contributing/advanced.html +content/getting_started/contributing/code-style.html +content/getting_started/contributing/documentation.html +content/getting_started/contributing/index.html +content/getting_started/contributing/pull-requests.html +content/getting_started/contributing/setting-up-environment.html +content/getting_started/contributing/testing.html +content/getting_started/contributing/working-with-github.html +content/getting_started/index.html +content/getting_started/installing.html +content/tutorials/01-models_mapping/index.html +content/tutorials/01-models_mapping/plot_1_tensor_models.html +content/tutorials/01-models_mapping/plot_2_cyl_models.html +content/tutorials/01-models_mapping/plot_3_tree_models.html +content/tutorials/01-models_mapping/sg_execution_times.html +content/tutorials/02-linear_inversion/index.html +content/tutorials/02-linear_inversion/plot_inv_1_inversion_lsq.html +content/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.html +content/tutorials/02-linear_inversion/sg_execution_times.html +content/tutorials/03-gravity/index.html +content/tutorials/03-gravity/plot_1a_gravity_anomaly.html +content/tutorials/03-gravity/plot_1b_gravity_gradiometry.html +content/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.html +content/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.html +content/tutorials/03-gravity/plot_inv_1c_gravity_anomaly_irls_compare_weighting.html +content/tutorials/03-gravity/sg_execution_times.html +content/tutorials/04-magnetics/index.html +content/tutorials/04-magnetics/plot_2a_magnetics_induced.html +content/tutorials/04-magnetics/plot_2b_magnetics_mvi.html +content/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.html +content/tutorials/04-magnetics/sg_execution_times.html +content/tutorials/05-dcr/index.html +content/tutorials/05-dcr/plot_fwd_1_dcr_sounding.html +content/tutorials/05-dcr/plot_fwd_2_dcr2d.html +content/tutorials/05-dcr/plot_fwd_3_dcr3d.html +content/tutorials/05-dcr/plot_gen_3_3d_to_2d.html +content/tutorials/05-dcr/plot_inv_1_dcr_sounding.html +content/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.html +content/tutorials/05-dcr/plot_inv_1_dcr_sounding_parametric.html +content/tutorials/05-dcr/plot_inv_2_dcr2d.html +content/tutorials/05-dcr/plot_inv_2_dcr2d_irls.html +content/tutorials/05-dcr/plot_inv_3_dcr3d.html +content/tutorials/05-dcr/sg_execution_times.html +content/tutorials/06-ip/index.html +content/tutorials/06-ip/plot_fwd_2_dcip2d.html +content/tutorials/06-ip/plot_fwd_3_dcip3d.html +content/tutorials/06-ip/plot_inv_2_dcip2d.html +content/tutorials/06-ip/plot_inv_3_dcip3d.html +content/tutorials/06-ip/sg_execution_times.html +content/tutorials/07-fdem/index.html +content/tutorials/07-fdem/plot_fwd_1_em1dfm_dispersive.html +content/tutorials/07-fdem/plot_fwd_1_em1dfm.html +content/tutorials/07-fdem/plot_fwd_2_fem_cyl.html +content/tutorials/07-fdem/plot_fwd_3_fem_3d.html +content/tutorials/07-fdem/plot_inv_1_em1dfm.html +content/tutorials/07-fdem/sg_execution_times.html +content/tutorials/08-tdem/index.html +content/tutorials/08-tdem/plot_fwd_1_em1dtm_dispersive.html +content/tutorials/08-tdem/plot_fwd_1_em1dtm.html +content/tutorials/08-tdem/plot_fwd_1_em1dtm_waveforms.html +content/tutorials/08-tdem/plot_fwd_2_tem_cyl.html +content/tutorials/08-tdem/plot_fwd_3_tem_3d.html +content/tutorials/08-tdem/plot_inv_1_em1dtm.html +content/tutorials/08-tdem/sg_execution_times.html +content/tutorials/09-nsem/index.html +content/tutorials/09-nsem/sg_execution_times.html +content/tutorials/10-vrm/index.html +content/tutorials/10-vrm/plot_fwd_1_vrm_layer.html +content/tutorials/10-vrm/plot_fwd_2_vrm_topsoil.html +content/tutorials/10-vrm/plot_fwd_3_vrm_tem.html +content/tutorials/10-vrm/sg_execution_times.html +content/tutorials/11-flow/index.html +content/tutorials/11-flow/sg_execution_times.html +content/tutorials/12-seismic/index.html +content/tutorials/12-seismic/plot_fwd_1_tomography_2D.html +content/tutorials/12-seismic/plot_inv_1_tomography_2D.html +content/tutorials/12-seismic/sg_execution_times.html +content/tutorials/13-joint_inversion/index.html +content/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.html +content/tutorials/13-joint_inversion/sg_execution_times.html +content/tutorials/14-pgi/index.html +content/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.html +content/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.html +content/tutorials/14-pgi/sg_execution_times.html +content/user_guide.html diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000000..66139319ae --- /dev/null +++ b/environment.yml @@ -0,0 +1,66 @@ +name: simpeg-dev +channels: + - conda-forge +dependencies: +# dependencies + - python=3.11 + - numpy>=1.22 + - scipy>=1.12 + - pymatsolver>=0.3 + - matplotlib-base + - discretize>=0.12 + - geoana>=0.7 + - libdlf + - typing_extensions + +# solver +# uncomment the next line if you are on an intel platform +# - pydiso # if on intel pc +# uncomment this line if you want to install mumps solvers +# - python-mumps + +# optional dependencies + - dask + - zarr + - fsspec>=0.3.3 + - choclo>=0.3.0 + - scooby + - plotly + - scikit-learn>=1.2 + - pandas + +# documentation building + - sphinx + - sphinx-gallery>=0.1.13 + - sphinxcontrib-apidoc + - sphinx-reredirects + - sphinx-design + - pydata-sphinx-theme + - empymod>=2.0.0 + - nbsphinx + - numpydoc + - pillow + - sympy + - memory_profiler + - python-kaleido + - h5py + +# testing + - pytest + - pytest-cov + +# style checking + - pre-commit + - black==24.3.0 + - flake8==7.0.0 + - flake8-pyproject==1.2.3 + - flake8-bugbear==23.12.2 + - flake8-builtins==2.2.0 + - flake8-mutable==1.2.0 + - flake8-rst-docstrings==0.3.0 + - flake8-docstrings==1.7.0 + - zizmor # lint GitHub Actions workflows + +# recommended + - jupyter + - pyvista diff --git a/environment_test.yml b/environment_test.yml deleted file mode 100644 index 84940f92d6..0000000000 --- a/environment_test.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: simpeg-test -channels: - - conda-forge -dependencies: - - numpy>=1.20 - - scipy>=1.8 - - scikit-learn>=1.2 - - pymatsolver>=0.2 - - matplotlib - - discretize>=0.10 - - geoana>=0.5.0 - - empymod>=2.0.0 - - setuptools_scm - - pandas - - dask - - zarr - - fsspec - - jupyter - - h5py - - sphinx - - sphinx-gallery>=0.1.13 - - sphinxcontrib-apidoc - - pydata-sphinx-theme - - nbsphinx - - numpydoc - - pillow - - pylint - - sympy - - wheel - - pytest - - pytest-cov - - toolz - - twine - - memory_profiler - - plotly - - pyvista - - pip - - python-kaleido - # Optional dependencies - - choclo - # Linters and code style - - pre-commit - - black==24.3.0 - - flake8==7.0.0 - - flake8-bugbear==23.12.2 - - flake8-builtins==2.2.0 - - flake8-mutable==1.2.0 - - flake8-rst-docstrings==0.3.0 - - flake8-docstrings==1.7.0 diff --git a/examples/01-maps/plot_mesh2mesh.py b/examples/01-maps/plot_mesh2mesh.py index b5621e5ae2..1e76d511ac 100644 --- a/examples/01-maps/plot_mesh2mesh.py +++ b/examples/01-maps/plot_mesh2mesh.py @@ -15,7 +15,7 @@ def run(plotIt=True): h1 = utils.unpack_widths([(6, 7, -1.5), (6, 10), (6, 7, 1.5)]) h1 = h1 / h1.sum() M2 = discretize.TensorMesh([h1, h1]) - V = utils.model_builder.create_random_model(M.vnC, seed=79, its=50) + V = utils.model_builder.create_random_model(M.vnC, random_seed=79, its=50) v = utils.mkvc(V) modh = maps.Mesh2Mesh([M, M2]) modH = maps.Mesh2Mesh([M2, M]) diff --git a/examples/01-maps/plot_sumMap.py b/examples/01-maps/plot_sumMap.py index 7cbc4f89c5..369d2ab868 100644 --- a/examples/01-maps/plot_sumMap.py +++ b/examples/01-maps/plot_sumMap.py @@ -96,7 +96,7 @@ def run(plotIt=True): mesh, survey=survey, chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="forward_only", ) @@ -117,7 +117,7 @@ def run(plotIt=True): # Create the forward model operator prob = magnetics.Simulation3DIntegral( - mesh, survey=survey, chiMap=sumMap, ind_active=actv, store_sensitivities="ram" + mesh, survey=survey, chiMap=sumMap, active_cells=actv, store_sensitivities="ram" ) # Make sensitivity weighting @@ -164,8 +164,8 @@ def run(plotIt=True): lower=0.0, upper=1.0, maxIterLS=20, - maxIterCG=10, - tolCG=1e-3, + cg_maxiter=10, + cg_rtol=1e-3, tolG=1e-3, eps=1e-6, ) @@ -175,7 +175,8 @@ def run(plotIt=True): # Here is where the norms are applied # Use pick a threshold parameter empirically based on the distribution of # model parameters - IRLS = directives.Update_IRLS(f_min_change=1e-3, minGNiter=1) + IRLS = directives.UpdateIRLS(f_min_change=1e-3) + update_Jacobi = directives.UpdatePreconditioner() inv = inversion.BaseInversion(invProb, directiveList=[IRLS, betaest, update_Jacobi]) diff --git a/examples/02-gravity/plot_inv_grav_tiled.py b/examples/02-gravity/plot_inv_grav_tiled.py index f5676a5938..2178532cc3 100644 --- a/examples/02-gravity/plot_inv_grav_tiled.py +++ b/examples/02-gravity/plot_inv_grav_tiled.py @@ -120,7 +120,7 @@ np.r_[-10, -10, -30], np.r_[10, 10, -10], mesh.gridCC, -)[0] +) # Assign magnetization values model[ind] = 0.3 @@ -138,7 +138,7 @@ # Create the forward simulation for the global dataset simulation = gravity.simulation.Simulation3DIntegral( - survey=survey, mesh=mesh, rhoMap=idenMap, ind_active=activeCells + survey=survey, mesh=mesh, rhoMap=idenMap, active_cells=activeCells ) # Compute linear forward operator and compute some data @@ -166,7 +166,7 @@ survey=local_survey, mesh=local_meshes[ii], rhoMap=tile_map, - ind_active=local_actives, + active_cells=local_actives, sensitivity_path=os.path.join("Inversion", f"Tile{ii}.zarr"), ) @@ -228,7 +228,7 @@ # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 + maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, cg_maxiter=10, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(global_misfit, reg, opt) betaest = directives.BetaEstimate_ByEig(beta0_ratio=1e-1) @@ -236,11 +236,11 @@ # Here is where the norms are applied # Use a threshold parameter empirically based on the distribution of # model parameters -update_IRLS = directives.Update_IRLS( +update_IRLS = directives.UpdateIRLS( f_min_change=1e-4, max_irls_iterations=0, - coolEpsFact=1.5, - beta_tol=1e-2, + irls_cooling_factor=1.5, + misfit_tolerance=1e-2, ) saveDict = directives.SaveOutputEveryIteration(save_txt=False) update_Jacobi = directives.UpdatePreconditioner() diff --git a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py index c9e1ab7b4d..02088a0950 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py @@ -1,8 +1,8 @@ """ -Magnetic inversion on a TreeMesh -================================ +Magnetic inversion on a TreeMesh with remanence +=============================================== -In this example, we demonstrate the use of a Magnetic Vector Inverison +In this example, we demonstrate the use of a Magnetic Vector Inversion on 3D TreeMesh for the inversion of magnetics affected by remanence. The mesh is auto-generated based on the position of the observation locations and topography. @@ -11,8 +11,7 @@ Cartesian coordinate system, and second for a compact model using the Spherical formulation. -The inverse problem uses the :class:'simpeg.regularization.Sparse' -that +The inverse problem uses the :class:`simpeg.regularization.Sparse`. """ @@ -30,7 +29,7 @@ from simpeg import utils from simpeg.utils import mkvc -from discretize.utils import active_from_xyz, mesh_builder_xyz, refine_tree_xyz +from discretize.utils import active_from_xyz, mesh_builder_xyz from simpeg.potential_fields import magnetics import scipy as sp import numpy as np @@ -112,9 +111,7 @@ mesh = mesh_builder_xyz( xyzLoc, h, padding_distance=padDist, depth_core=100, mesh_type="tree" ) -mesh = refine_tree_xyz( - mesh, topo, method="surface", octree_levels=[4, 4], finalize=True -) +mesh.refine_surface(topo, padding_cells_by_level=[4, 4], finalize=True) # Define an active cells from topo @@ -135,15 +132,15 @@ # Convert the inclination declination to vector in Cartesian M_xyz = utils.mat_utils.dip_azimuth2cartesian(M[0], M[1]) -# Get the indicies of the magnetized block +# Get the indices of the magnetized block ind = utils.model_builder.get_indices_block( np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, -)[0] +) # Assign magnetization values -model[ind, :] = np.kron(np.ones((ind.shape[0], 1)), M_xyz * 0.05) +model[ind, :] = np.kron(np.ones((ind.size, 1)), M_xyz * 0.05) # Remove air cells model = model[actv, :] @@ -156,7 +153,7 @@ # Create the simulation simulation = magnetics.simulation.Simulation3DIntegral( - survey=survey, mesh=mesh, chiMap=idenMap, ind_active=actv, model_type="vector" + survey=survey, mesh=mesh, chiMap=idenMap, active_cells=actv, model_type="vector" ) # Compute some data and add some random noise @@ -246,7 +243,7 @@ # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=10, lower=-10, upper=10.0, maxIterLS=20, maxIterCG=20, tolCG=1e-4 + maxIter=10, lower=-10, upper=10.0, maxIterLS=20, cg_maxiter=20, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) @@ -260,7 +257,9 @@ # Here is where the norms are applied # Use a threshold parameter empirically based on the distribution of # model parameters -IRLS = directives.Update_IRLS(f_min_change=1e-3, max_irls_iterations=2, beta_tol=5e-1) +IRLS = directives.UpdateIRLS( + f_min_change=1e-3, max_irls_iterations=2, misfit_tolerance=5e-1 +) # Pre-conditioner update_Jacobi = directives.UpdatePreconditioner() @@ -335,25 +334,23 @@ lower=lower_bound, upper=upper_bound, maxIterLS=20, - maxIterCG=30, - tolCG=1e-3, - stepOffBoundsFact=1e-3, + cg_maxiter=30, + cg_rtol=1e-3, + active_set_grad_scale=1e-3, ) opt.approxHinv = None invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=beta) # Here is where the norms are applied -irls = directives.Update_IRLS( +irls = directives.UpdateIRLS( f_min_change=1e-4, max_irls_iterations=20, - minGNiter=1, - beta_tol=0.5, - coolingRate=1, - coolEps_q=True, - sphericalDomain=True, + misfit_tolerance=0.5, +) +scale_spherical = directives.SphericalUnitsWeights( + amplitude=wires.amp, angles=[reg_t, reg_p] ) - # Special directive specific to the mag amplitude problem. The sensitivity # weights are updated between each iteration. spherical_projection = directives.ProjectSphericalBounds() @@ -362,7 +359,13 @@ inv = inversion.BaseInversion( invProb, - directiveList=[spherical_projection, irls, sensitivity_weights, update_Jacobi], + directiveList=[ + scale_spherical, + spherical_projection, + irls, + sensitivity_weights, + update_Jacobi, + ], ) mrec_MVI_S = inv.run(m_start) diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py index edd69836d2..5cf55b5eeb 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -2,11 +2,11 @@ Magnetic inversion on a TreeMesh ================================ -In this example, we demonstrate the use of a Magnetic Vector Inverison +In this example, we demonstrate the use of a Magnetic Vector Inversion on 3D TreeMesh for the inversion of magnetic data. -The inverse problem uses the :class:'simpeg.regularization.VectorAmplitude' -regularization borrowed from ... +The inverse problem uses the :class:`simpeg.regularization.VectorAmplitude` +regularization. """ @@ -106,7 +106,7 @@ np.r_[-30, -20, -10], np.r_[30, 20, 25], mesh.gridCC, -)[0] +) model_amp[ind] = 0.05 model_azm_dip[ind, 0] = 45.0 model_azm_dip[ind, 1] = 90.0 @@ -123,7 +123,7 @@ # Create the simulation simulation = magnetics.simulation.Simulation3DIntegral( - survey=survey, mesh=mesh, chiMap=idenMap, ind_active=actv, model_type="vector" + survey=survey, mesh=mesh, chiMap=idenMap, active_cells=actv, model_type="vector" ) # Compute some data and add some random noise @@ -170,7 +170,7 @@ # The optimization scheme opt = optimization.ProjectedGNCG( - maxIter=20, lower=-10, upper=10.0, maxIterLS=20, maxIterCG=20, tolCG=1e-4 + maxIter=20, lower=-10, upper=10.0, maxIterLS=20, cg_maxiter=20, cg_rtol=1e-3 ) # The inverse problem @@ -183,7 +183,9 @@ sensitivity_weights = directives.UpdateSensitivityWeights() # Here is where the norms are applied -IRLS = directives.Update_IRLS(f_min_change=1e-3, max_irls_iterations=10, beta_tol=5e-1) +IRLS = directives.UpdateIRLS( + f_min_change=1e-3, max_irls_iterations=10, misfit_tolerance=5e-1 +) # Pre-conditioner update_Jacobi = directives.UpdatePreconditioner() diff --git a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py index d114a30609..643df614ca 100644 --- a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py +++ b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py @@ -134,7 +134,7 @@ np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, -)[0] +) # Assign magnetization value, inducing field strength will # be applied in by the :class:`simpeg.PF.Magnetics` problem @@ -152,7 +152,7 @@ survey=survey, mesh=mesh, chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="forward_only", ) simulation.M = M_xyz @@ -225,7 +225,11 @@ # Create static map simulation = magnetics.simulation.Simulation3DIntegral( - mesh=mesh, survey=survey, chiMap=idenMap, ind_active=surf, store_sensitivities="ram" + mesh=mesh, + survey=survey, + chiMap=idenMap, + active_cells=surf, + store_sensitivities="ram", ) wr = simulation.getJtJdiag(mstart) ** 0.5 @@ -239,7 +243,7 @@ # Specify how the optimization will proceed, set susceptibility bounds to inf opt = optimization.ProjectedGNCG( - maxIter=20, lower=-np.inf, upper=np.inf, maxIterLS=20, maxIterCG=20, tolCG=1e-3 + maxIter=20, lower=-np.inf, upper=np.inf, maxIterLS=20, cg_maxiter=20, cg_rtol=1e-3 ) # Define misfit function (obs-calc) @@ -253,9 +257,10 @@ # Target misfit to stop the inversion, # try to fit as much as possible of the signal, we don't want to lose anything -IRLS = directives.Update_IRLS( - f_min_change=1e-3, minGNiter=1, beta_tol=1e-1, max_irls_iterations=5 +IRLS = directives.UpdateIRLS( + f_min_change=1e-3, misfit_tolerance=1e-1, max_irls_iterations=5 ) + update_Jacobi = directives.UpdatePreconditioner() # Put all the parts together inv = inversion.BaseInversion(invProb, directiveList=[betaest, IRLS, update_Jacobi]) @@ -281,7 +286,11 @@ surveyAmp = magnetics.survey.Survey(srcField) simulation = magnetics.simulation.Simulation3DIntegral( - mesh=mesh, survey=surveyAmp, chiMap=idenMap, ind_active=surf, is_amplitude_data=True + mesh=mesh, + survey=surveyAmp, + chiMap=idenMap, + active_cells=surf, + is_amplitude_data=True, ) bAmp = simulation.fields(mrec) @@ -339,7 +348,11 @@ # Create the forward model operator simulation = magnetics.simulation.Simulation3DIntegral( - survey=surveyAmp, mesh=mesh, chiMap=idenMap, ind_active=actv, is_amplitude_data=True + survey=surveyAmp, + mesh=mesh, + chiMap=idenMap, + active_cells=actv, + is_amplitude_data=True, ) data_obj = data.Data(survey, dobs=bAmp, noise_floor=wd) @@ -354,7 +367,7 @@ # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=30, lower=0.0, upper=1.0, maxIterLS=20, maxIterCG=20, tolCG=1e-3 + maxIter=30, lower=0.0, upper=1.0, maxIterLS=20, cg_maxiter=20, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) @@ -363,13 +376,7 @@ betaest = directives.BetaEstimate_ByEig(beta0_ratio=1) # Specify the sparse norms -IRLS = directives.Update_IRLS( - max_irls_iterations=10, - f_min_change=1e-3, - minGNiter=1, - coolingRate=1, - beta_search=False, -) +IRLS = directives.UpdateIRLS(max_irls_iterations=10, f_min_change=1e-3) # Special directive specific to the mag amplitude problem. The sensitivity # weights are updated between each iteration. @@ -378,7 +385,8 @@ # Put all together inv = inversion.BaseInversion( - invProb, directiveList=[update_SensWeight, betaest, IRLS, update_Jacobi] + invProb, + directiveList=[update_SensWeight, betaest, IRLS, update_Jacobi], ) # Invert diff --git a/examples/04-dcip/dcr3d/topo_xyz.txt b/examples/04-dcip/dcr3d/topo_xyz.txt deleted file mode 100644 index 7c149205d2..0000000000 --- a/examples/04-dcip/dcr3d/topo_xyz.txt +++ /dev/null @@ -1,19881 +0,0 @@ --2.1000e+03 -2.0000e+03 6.9049e+00 --2.1000e+03 -1.9714e+03 6.8784e+00 --2.1000e+03 -1.9429e+03 6.8516e+00 --2.1000e+03 -1.9143e+03 6.8245e+00 --2.1000e+03 -1.8857e+03 6.7972e+00 --2.1000e+03 -1.8571e+03 6.7697e+00 --2.1000e+03 -1.8286e+03 6.7419e+00 --2.1000e+03 -1.8000e+03 6.7139e+00 --2.1000e+03 -1.7714e+03 6.6857e+00 --2.1000e+03 -1.7429e+03 6.6573e+00 --2.1000e+03 -1.7143e+03 6.6286e+00 --2.1000e+03 -1.6857e+03 6.5998e+00 --2.1000e+03 -1.6571e+03 6.5707e+00 --2.1000e+03 -1.6286e+03 6.5415e+00 --2.1000e+03 -1.6000e+03 6.5121e+00 --2.1000e+03 -1.5714e+03 6.4825e+00 --2.1000e+03 -1.5429e+03 6.4528e+00 --2.1000e+03 -1.5143e+03 6.4230e+00 --2.1000e+03 -1.4857e+03 6.3930e+00 --2.1000e+03 -1.4571e+03 6.3629e+00 --2.1000e+03 -1.4286e+03 6.3327e+00 --2.1000e+03 -1.4000e+03 6.3024e+00 --2.1000e+03 -1.3714e+03 6.2720e+00 --2.1000e+03 -1.3429e+03 6.2416e+00 --2.1000e+03 -1.3143e+03 6.2112e+00 --2.1000e+03 -1.2857e+03 6.1808e+00 --2.1000e+03 -1.2571e+03 6.1503e+00 --2.1000e+03 -1.2286e+03 6.1199e+00 --2.1000e+03 -1.2000e+03 6.0896e+00 --2.1000e+03 -1.1714e+03 6.0593e+00 --2.1000e+03 -1.1429e+03 6.0291e+00 --2.1000e+03 -1.1143e+03 5.9990e+00 --2.1000e+03 -1.0857e+03 5.9691e+00 --2.1000e+03 -1.0571e+03 5.9394e+00 --2.1000e+03 -1.0286e+03 5.9099e+00 --2.1000e+03 -1.0000e+03 5.8806e+00 --2.1000e+03 -9.7143e+02 5.8516e+00 --2.1000e+03 -9.4286e+02 5.8229e+00 --2.1000e+03 -9.1429e+02 5.7945e+00 --2.1000e+03 -8.8571e+02 5.7665e+00 --2.1000e+03 -8.5714e+02 5.7389e+00 --2.1000e+03 -8.2857e+02 5.7117e+00 --2.1000e+03 -8.0000e+02 5.6849e+00 --2.1000e+03 -7.7143e+02 5.6587e+00 --2.1000e+03 -7.4286e+02 5.6330e+00 --2.1000e+03 -7.1429e+02 5.6079e+00 --2.1000e+03 -6.8571e+02 5.5834e+00 --2.1000e+03 -6.5714e+02 5.5596e+00 --2.1000e+03 -6.2857e+02 5.5364e+00 --2.1000e+03 -6.0000e+02 5.5140e+00 --2.1000e+03 -5.7143e+02 5.4923e+00 --2.1000e+03 -5.4286e+02 5.4714e+00 --2.1000e+03 -5.1429e+02 5.4513e+00 --2.1000e+03 -4.8571e+02 5.4321e+00 --2.1000e+03 -4.5714e+02 5.4137e+00 --2.1000e+03 -4.2857e+02 5.3963e+00 --2.1000e+03 -4.0000e+02 5.3799e+00 --2.1000e+03 -3.7143e+02 5.3645e+00 --2.1000e+03 -3.4286e+02 5.3500e+00 --2.1000e+03 -3.1429e+02 5.3366e+00 --2.1000e+03 -2.8571e+02 5.3243e+00 --2.1000e+03 -2.5714e+02 5.3131e+00 --2.1000e+03 -2.2857e+02 5.3030e+00 --2.1000e+03 -2.0000e+02 5.2941e+00 --2.1000e+03 -1.7143e+02 5.2863e+00 --2.1000e+03 -1.4286e+02 5.2796e+00 --2.1000e+03 -1.1429e+02 5.2742e+00 --2.1000e+03 -8.5714e+01 5.2700e+00 --2.1000e+03 -5.7143e+01 5.2669e+00 --2.1000e+03 -2.8571e+01 5.2651e+00 --2.1000e+03 0.0000e+00 5.2645e+00 --2.1000e+03 2.8571e+01 5.2651e+00 --2.1000e+03 5.7143e+01 5.2669e+00 --2.1000e+03 8.5714e+01 5.2700e+00 --2.1000e+03 1.1429e+02 5.2742e+00 --2.1000e+03 1.4286e+02 5.2796e+00 --2.1000e+03 1.7143e+02 5.2863e+00 --2.1000e+03 2.0000e+02 5.2941e+00 --2.1000e+03 2.2857e+02 5.3030e+00 --2.1000e+03 2.5714e+02 5.3131e+00 --2.1000e+03 2.8571e+02 5.3243e+00 --2.1000e+03 3.1429e+02 5.3366e+00 --2.1000e+03 3.4286e+02 5.3500e+00 --2.1000e+03 3.7143e+02 5.3645e+00 --2.1000e+03 4.0000e+02 5.3799e+00 --2.1000e+03 4.2857e+02 5.3963e+00 --2.1000e+03 4.5714e+02 5.4137e+00 --2.1000e+03 4.8571e+02 5.4321e+00 --2.1000e+03 5.1429e+02 5.4513e+00 --2.1000e+03 5.4286e+02 5.4714e+00 --2.1000e+03 5.7143e+02 5.4923e+00 --2.1000e+03 6.0000e+02 5.5140e+00 --2.1000e+03 6.2857e+02 5.5364e+00 --2.1000e+03 6.5714e+02 5.5596e+00 --2.1000e+03 6.8571e+02 5.5834e+00 --2.1000e+03 7.1429e+02 5.6079e+00 --2.1000e+03 7.4286e+02 5.6330e+00 --2.1000e+03 7.7143e+02 5.6587e+00 --2.1000e+03 8.0000e+02 5.6849e+00 --2.1000e+03 8.2857e+02 5.7117e+00 --2.1000e+03 8.5714e+02 5.7389e+00 --2.1000e+03 8.8571e+02 5.7665e+00 --2.1000e+03 9.1429e+02 5.7945e+00 --2.1000e+03 9.4286e+02 5.8229e+00 --2.1000e+03 9.7143e+02 5.8516e+00 --2.1000e+03 1.0000e+03 5.8806e+00 --2.1000e+03 1.0286e+03 5.9099e+00 --2.1000e+03 1.0571e+03 5.9394e+00 --2.1000e+03 1.0857e+03 5.9691e+00 --2.1000e+03 1.1143e+03 5.9990e+00 --2.1000e+03 1.1429e+03 6.0291e+00 --2.1000e+03 1.1714e+03 6.0593e+00 --2.1000e+03 1.2000e+03 6.0896e+00 --2.1000e+03 1.2286e+03 6.1199e+00 --2.1000e+03 1.2571e+03 6.1503e+00 --2.1000e+03 1.2857e+03 6.1808e+00 --2.1000e+03 1.3143e+03 6.2112e+00 --2.1000e+03 1.3429e+03 6.2416e+00 --2.1000e+03 1.3714e+03 6.2720e+00 --2.1000e+03 1.4000e+03 6.3024e+00 --2.1000e+03 1.4286e+03 6.3327e+00 --2.1000e+03 1.4571e+03 6.3629e+00 --2.1000e+03 1.4857e+03 6.3930e+00 --2.1000e+03 1.5143e+03 6.4230e+00 --2.1000e+03 1.5429e+03 6.4528e+00 --2.1000e+03 1.5714e+03 6.4825e+00 --2.1000e+03 1.6000e+03 6.5121e+00 --2.1000e+03 1.6286e+03 6.5415e+00 --2.1000e+03 1.6571e+03 6.5707e+00 --2.1000e+03 1.6857e+03 6.5998e+00 --2.1000e+03 1.7143e+03 6.6286e+00 --2.1000e+03 1.7429e+03 6.6573e+00 --2.1000e+03 1.7714e+03 6.6857e+00 --2.1000e+03 1.8000e+03 6.7139e+00 --2.1000e+03 1.8286e+03 6.7419e+00 --2.1000e+03 1.8571e+03 6.7697e+00 --2.1000e+03 1.8857e+03 6.7972e+00 --2.1000e+03 1.9143e+03 6.8245e+00 --2.1000e+03 1.9429e+03 6.8516e+00 --2.1000e+03 1.9714e+03 6.8784e+00 --2.1000e+03 2.0000e+03 6.9049e+00 --2.0700e+03 -2.0000e+03 6.8756e+00 --2.0700e+03 -1.9714e+03 6.8483e+00 --2.0700e+03 -1.9429e+03 6.8208e+00 --2.0700e+03 -1.9143e+03 6.7930e+00 --2.0700e+03 -1.8857e+03 6.7649e+00 --2.0700e+03 -1.8571e+03 6.7366e+00 --2.0700e+03 -1.8286e+03 6.7080e+00 --2.0700e+03 -1.8000e+03 6.6792e+00 --2.0700e+03 -1.7714e+03 6.6502e+00 --2.0700e+03 -1.7429e+03 6.6209e+00 --2.0700e+03 -1.7143e+03 6.5913e+00 --2.0700e+03 -1.6857e+03 6.5616e+00 --2.0700e+03 -1.6571e+03 6.5316e+00 --2.0700e+03 -1.6286e+03 6.5015e+00 --2.0700e+03 -1.6000e+03 6.4711e+00 --2.0700e+03 -1.5714e+03 6.4406e+00 --2.0700e+03 -1.5429e+03 6.4098e+00 --2.0700e+03 -1.5143e+03 6.3790e+00 --2.0700e+03 -1.4857e+03 6.3479e+00 --2.0700e+03 -1.4571e+03 6.3168e+00 --2.0700e+03 -1.4286e+03 6.2855e+00 --2.0700e+03 -1.4000e+03 6.2541e+00 --2.0700e+03 -1.3714e+03 6.2227e+00 --2.0700e+03 -1.3429e+03 6.1911e+00 --2.0700e+03 -1.3143e+03 6.1595e+00 --2.0700e+03 -1.2857e+03 6.1279e+00 --2.0700e+03 -1.2571e+03 6.0963e+00 --2.0700e+03 -1.2286e+03 6.0647e+00 --2.0700e+03 -1.2000e+03 6.0331e+00 --2.0700e+03 -1.1714e+03 6.0016e+00 --2.0700e+03 -1.1429e+03 5.9702e+00 --2.0700e+03 -1.1143e+03 5.9389e+00 --2.0700e+03 -1.0857e+03 5.9077e+00 --2.0700e+03 -1.0571e+03 5.8768e+00 --2.0700e+03 -1.0286e+03 5.8460e+00 --2.0700e+03 -1.0000e+03 5.8154e+00 --2.0700e+03 -9.7143e+02 5.7851e+00 --2.0700e+03 -9.4286e+02 5.7551e+00 --2.0700e+03 -9.1429e+02 5.7254e+00 --2.0700e+03 -8.8571e+02 5.6961e+00 --2.0700e+03 -8.5714e+02 5.6672e+00 --2.0700e+03 -8.2857e+02 5.6388e+00 --2.0700e+03 -8.0000e+02 5.6108e+00 --2.0700e+03 -7.7143e+02 5.5833e+00 --2.0700e+03 -7.4286e+02 5.5564e+00 --2.0700e+03 -7.1429e+02 5.5300e+00 --2.0700e+03 -6.8571e+02 5.5043e+00 --2.0700e+03 -6.5714e+02 5.4793e+00 --2.0700e+03 -6.2857e+02 5.4550e+00 --2.0700e+03 -6.0000e+02 5.4314e+00 --2.0700e+03 -5.7143e+02 5.4086e+00 --2.0700e+03 -5.4286e+02 5.3866e+00 --2.0700e+03 -5.1429e+02 5.3655e+00 --2.0700e+03 -4.8571e+02 5.3452e+00 --2.0700e+03 -4.5714e+02 5.3260e+00 --2.0700e+03 -4.2857e+02 5.3076e+00 --2.0700e+03 -4.0000e+02 5.2903e+00 --2.0700e+03 -3.7143e+02 5.2740e+00 --2.0700e+03 -3.4286e+02 5.2588e+00 --2.0700e+03 -3.1429e+02 5.2447e+00 --2.0700e+03 -2.8571e+02 5.2317e+00 --2.0700e+03 -2.5714e+02 5.2199e+00 --2.0700e+03 -2.2857e+02 5.2093e+00 --2.0700e+03 -2.0000e+02 5.1998e+00 --2.0700e+03 -1.7143e+02 5.1916e+00 --2.0700e+03 -1.4286e+02 5.1846e+00 --2.0700e+03 -1.1429e+02 5.1788e+00 --2.0700e+03 -8.5714e+01 5.1744e+00 --2.0700e+03 -5.7143e+01 5.1712e+00 --2.0700e+03 -2.8571e+01 5.1692e+00 --2.0700e+03 0.0000e+00 5.1686e+00 --2.0700e+03 2.8571e+01 5.1692e+00 --2.0700e+03 5.7143e+01 5.1712e+00 --2.0700e+03 8.5714e+01 5.1744e+00 --2.0700e+03 1.1429e+02 5.1788e+00 --2.0700e+03 1.4286e+02 5.1846e+00 --2.0700e+03 1.7143e+02 5.1916e+00 --2.0700e+03 2.0000e+02 5.1998e+00 --2.0700e+03 2.2857e+02 5.2093e+00 --2.0700e+03 2.5714e+02 5.2199e+00 --2.0700e+03 2.8571e+02 5.2317e+00 --2.0700e+03 3.1429e+02 5.2447e+00 --2.0700e+03 3.4286e+02 5.2588e+00 --2.0700e+03 3.7143e+02 5.2740e+00 --2.0700e+03 4.0000e+02 5.2903e+00 --2.0700e+03 4.2857e+02 5.3076e+00 --2.0700e+03 4.5714e+02 5.3260e+00 --2.0700e+03 4.8571e+02 5.3452e+00 --2.0700e+03 5.1429e+02 5.3655e+00 --2.0700e+03 5.4286e+02 5.3866e+00 --2.0700e+03 5.7143e+02 5.4086e+00 --2.0700e+03 6.0000e+02 5.4314e+00 --2.0700e+03 6.2857e+02 5.4550e+00 --2.0700e+03 6.5714e+02 5.4793e+00 --2.0700e+03 6.8571e+02 5.5043e+00 --2.0700e+03 7.1429e+02 5.5300e+00 --2.0700e+03 7.4286e+02 5.5564e+00 --2.0700e+03 7.7143e+02 5.5833e+00 --2.0700e+03 8.0000e+02 5.6108e+00 --2.0700e+03 8.2857e+02 5.6388e+00 --2.0700e+03 8.5714e+02 5.6672e+00 --2.0700e+03 8.8571e+02 5.6961e+00 --2.0700e+03 9.1429e+02 5.7254e+00 --2.0700e+03 9.4286e+02 5.7551e+00 --2.0700e+03 9.7143e+02 5.7851e+00 --2.0700e+03 1.0000e+03 5.8154e+00 --2.0700e+03 1.0286e+03 5.8460e+00 --2.0700e+03 1.0571e+03 5.8768e+00 --2.0700e+03 1.0857e+03 5.9077e+00 --2.0700e+03 1.1143e+03 5.9389e+00 --2.0700e+03 1.1429e+03 5.9702e+00 --2.0700e+03 1.1714e+03 6.0016e+00 --2.0700e+03 1.2000e+03 6.0331e+00 --2.0700e+03 1.2286e+03 6.0647e+00 --2.0700e+03 1.2571e+03 6.0963e+00 --2.0700e+03 1.2857e+03 6.1279e+00 --2.0700e+03 1.3143e+03 6.1595e+00 --2.0700e+03 1.3429e+03 6.1911e+00 --2.0700e+03 1.3714e+03 6.2227e+00 --2.0700e+03 1.4000e+03 6.2541e+00 --2.0700e+03 1.4286e+03 6.2855e+00 --2.0700e+03 1.4571e+03 6.3168e+00 --2.0700e+03 1.4857e+03 6.3479e+00 --2.0700e+03 1.5143e+03 6.3790e+00 --2.0700e+03 1.5429e+03 6.4098e+00 --2.0700e+03 1.5714e+03 6.4406e+00 --2.0700e+03 1.6000e+03 6.4711e+00 --2.0700e+03 1.6286e+03 6.5015e+00 --2.0700e+03 1.6571e+03 6.5316e+00 --2.0700e+03 1.6857e+03 6.5616e+00 --2.0700e+03 1.7143e+03 6.5913e+00 --2.0700e+03 1.7429e+03 6.6209e+00 --2.0700e+03 1.7714e+03 6.6502e+00 --2.0700e+03 1.8000e+03 6.6792e+00 --2.0700e+03 1.8286e+03 6.7080e+00 --2.0700e+03 1.8571e+03 6.7366e+00 --2.0700e+03 1.8857e+03 6.7649e+00 --2.0700e+03 1.9143e+03 6.7930e+00 --2.0700e+03 1.9429e+03 6.8208e+00 --2.0700e+03 1.9714e+03 6.8483e+00 --2.0700e+03 2.0000e+03 6.8756e+00 --2.0400e+03 -2.0000e+03 6.8459e+00 --2.0400e+03 -1.9714e+03 6.8179e+00 --2.0400e+03 -1.9429e+03 6.7897e+00 --2.0400e+03 -1.9143e+03 6.7611e+00 --2.0400e+03 -1.8857e+03 6.7322e+00 --2.0400e+03 -1.8571e+03 6.7031e+00 --2.0400e+03 -1.8286e+03 6.6737e+00 --2.0400e+03 -1.8000e+03 6.6440e+00 --2.0400e+03 -1.7714e+03 6.6141e+00 --2.0400e+03 -1.7429e+03 6.5839e+00 --2.0400e+03 -1.7143e+03 6.5535e+00 --2.0400e+03 -1.6857e+03 6.5228e+00 --2.0400e+03 -1.6571e+03 6.4919e+00 --2.0400e+03 -1.6286e+03 6.4607e+00 --2.0400e+03 -1.6000e+03 6.4294e+00 --2.0400e+03 -1.5714e+03 6.3978e+00 --2.0400e+03 -1.5429e+03 6.3660e+00 --2.0400e+03 -1.5143e+03 6.3341e+00 --2.0400e+03 -1.4857e+03 6.3020e+00 --2.0400e+03 -1.4571e+03 6.2697e+00 --2.0400e+03 -1.4286e+03 6.2373e+00 --2.0400e+03 -1.4000e+03 6.2048e+00 --2.0400e+03 -1.3714e+03 6.1722e+00 --2.0400e+03 -1.3429e+03 6.1395e+00 --2.0400e+03 -1.3143e+03 6.1067e+00 --2.0400e+03 -1.2857e+03 6.0738e+00 --2.0400e+03 -1.2571e+03 6.0410e+00 --2.0400e+03 -1.2286e+03 6.0081e+00 --2.0400e+03 -1.2000e+03 5.9753e+00 --2.0400e+03 -1.1714e+03 5.9425e+00 --2.0400e+03 -1.1429e+03 5.9098e+00 --2.0400e+03 -1.1143e+03 5.8772e+00 --2.0400e+03 -1.0857e+03 5.8447e+00 --2.0400e+03 -1.0571e+03 5.8123e+00 --2.0400e+03 -1.0286e+03 5.7802e+00 --2.0400e+03 -1.0000e+03 5.7483e+00 --2.0400e+03 -9.7143e+02 5.7166e+00 --2.0400e+03 -9.4286e+02 5.6853e+00 --2.0400e+03 -9.1429e+02 5.6543e+00 --2.0400e+03 -8.8571e+02 5.6236e+00 --2.0400e+03 -8.5714e+02 5.5933e+00 --2.0400e+03 -8.2857e+02 5.5635e+00 --2.0400e+03 -8.0000e+02 5.5342e+00 --2.0400e+03 -7.7143e+02 5.5054e+00 --2.0400e+03 -7.4286e+02 5.4771e+00 --2.0400e+03 -7.1429e+02 5.4495e+00 --2.0400e+03 -6.8571e+02 5.4225e+00 --2.0400e+03 -6.5714e+02 5.3962e+00 --2.0400e+03 -6.2857e+02 5.3706e+00 --2.0400e+03 -6.0000e+02 5.3458e+00 --2.0400e+03 -5.7143e+02 5.3218e+00 --2.0400e+03 -5.4286e+02 5.2987e+00 --2.0400e+03 -5.1429e+02 5.2764e+00 --2.0400e+03 -4.8571e+02 5.2551e+00 --2.0400e+03 -4.5714e+02 5.2348e+00 --2.0400e+03 -4.2857e+02 5.2155e+00 --2.0400e+03 -4.0000e+02 5.1973e+00 --2.0400e+03 -3.7143e+02 5.1801e+00 --2.0400e+03 -3.4286e+02 5.1640e+00 --2.0400e+03 -3.1429e+02 5.1492e+00 --2.0400e+03 -2.8571e+02 5.1355e+00 --2.0400e+03 -2.5714e+02 5.1230e+00 --2.0400e+03 -2.2857e+02 5.1117e+00 --2.0400e+03 -2.0000e+02 5.1017e+00 --2.0400e+03 -1.7143e+02 5.0930e+00 --2.0400e+03 -1.4286e+02 5.0857e+00 --2.0400e+03 -1.1429e+02 5.0796e+00 --2.0400e+03 -8.5714e+01 5.0748e+00 --2.0400e+03 -5.7143e+01 5.0715e+00 --2.0400e+03 -2.8571e+01 5.0694e+00 --2.0400e+03 0.0000e+00 5.0687e+00 --2.0400e+03 2.8571e+01 5.0694e+00 --2.0400e+03 5.7143e+01 5.0715e+00 --2.0400e+03 8.5714e+01 5.0748e+00 --2.0400e+03 1.1429e+02 5.0796e+00 --2.0400e+03 1.4286e+02 5.0857e+00 --2.0400e+03 1.7143e+02 5.0930e+00 --2.0400e+03 2.0000e+02 5.1017e+00 --2.0400e+03 2.2857e+02 5.1117e+00 --2.0400e+03 2.5714e+02 5.1230e+00 --2.0400e+03 2.8571e+02 5.1355e+00 --2.0400e+03 3.1429e+02 5.1492e+00 --2.0400e+03 3.4286e+02 5.1640e+00 --2.0400e+03 3.7143e+02 5.1801e+00 --2.0400e+03 4.0000e+02 5.1973e+00 --2.0400e+03 4.2857e+02 5.2155e+00 --2.0400e+03 4.5714e+02 5.2348e+00 --2.0400e+03 4.8571e+02 5.2551e+00 --2.0400e+03 5.1429e+02 5.2764e+00 --2.0400e+03 5.4286e+02 5.2987e+00 --2.0400e+03 5.7143e+02 5.3218e+00 --2.0400e+03 6.0000e+02 5.3458e+00 --2.0400e+03 6.2857e+02 5.3706e+00 --2.0400e+03 6.5714e+02 5.3962e+00 --2.0400e+03 6.8571e+02 5.4225e+00 --2.0400e+03 7.1429e+02 5.4495e+00 --2.0400e+03 7.4286e+02 5.4771e+00 --2.0400e+03 7.7143e+02 5.5054e+00 --2.0400e+03 8.0000e+02 5.5342e+00 --2.0400e+03 8.2857e+02 5.5635e+00 --2.0400e+03 8.5714e+02 5.5933e+00 --2.0400e+03 8.8571e+02 5.6236e+00 --2.0400e+03 9.1429e+02 5.6543e+00 --2.0400e+03 9.4286e+02 5.6853e+00 --2.0400e+03 9.7143e+02 5.7166e+00 --2.0400e+03 1.0000e+03 5.7483e+00 --2.0400e+03 1.0286e+03 5.7802e+00 --2.0400e+03 1.0571e+03 5.8123e+00 --2.0400e+03 1.0857e+03 5.8447e+00 --2.0400e+03 1.1143e+03 5.8772e+00 --2.0400e+03 1.1429e+03 5.9098e+00 --2.0400e+03 1.1714e+03 5.9425e+00 --2.0400e+03 1.2000e+03 5.9753e+00 --2.0400e+03 1.2286e+03 6.0081e+00 --2.0400e+03 1.2571e+03 6.0410e+00 --2.0400e+03 1.2857e+03 6.0738e+00 --2.0400e+03 1.3143e+03 6.1067e+00 --2.0400e+03 1.3429e+03 6.1395e+00 --2.0400e+03 1.3714e+03 6.1722e+00 --2.0400e+03 1.4000e+03 6.2048e+00 --2.0400e+03 1.4286e+03 6.2373e+00 --2.0400e+03 1.4571e+03 6.2697e+00 --2.0400e+03 1.4857e+03 6.3020e+00 --2.0400e+03 1.5143e+03 6.3341e+00 --2.0400e+03 1.5429e+03 6.3660e+00 --2.0400e+03 1.5714e+03 6.3978e+00 --2.0400e+03 1.6000e+03 6.4294e+00 --2.0400e+03 1.6286e+03 6.4607e+00 --2.0400e+03 1.6571e+03 6.4919e+00 --2.0400e+03 1.6857e+03 6.5228e+00 --2.0400e+03 1.7143e+03 6.5535e+00 --2.0400e+03 1.7429e+03 6.5839e+00 --2.0400e+03 1.7714e+03 6.6141e+00 --2.0400e+03 1.8000e+03 6.6440e+00 --2.0400e+03 1.8286e+03 6.6737e+00 --2.0400e+03 1.8571e+03 6.7031e+00 --2.0400e+03 1.8857e+03 6.7322e+00 --2.0400e+03 1.9143e+03 6.7611e+00 --2.0400e+03 1.9429e+03 6.7897e+00 --2.0400e+03 1.9714e+03 6.8179e+00 --2.0400e+03 2.0000e+03 6.8459e+00 --2.0100e+03 -2.0000e+03 6.8159e+00 --2.0100e+03 -1.9714e+03 6.7872e+00 --2.0100e+03 -1.9429e+03 6.7581e+00 --2.0100e+03 -1.9143e+03 6.7287e+00 --2.0100e+03 -1.8857e+03 6.6991e+00 --2.0100e+03 -1.8571e+03 6.6691e+00 --2.0100e+03 -1.8286e+03 6.6388e+00 --2.0100e+03 -1.8000e+03 6.6083e+00 --2.0100e+03 -1.7714e+03 6.5775e+00 --2.0100e+03 -1.7429e+03 6.5463e+00 --2.0100e+03 -1.7143e+03 6.5150e+00 --2.0100e+03 -1.6857e+03 6.4833e+00 --2.0100e+03 -1.6571e+03 6.4514e+00 --2.0100e+03 -1.6286e+03 6.4193e+00 --2.0100e+03 -1.6000e+03 6.3869e+00 --2.0100e+03 -1.5714e+03 6.3542e+00 --2.0100e+03 -1.5429e+03 6.3214e+00 --2.0100e+03 -1.5143e+03 6.2884e+00 --2.0100e+03 -1.4857e+03 6.2551e+00 --2.0100e+03 -1.4571e+03 6.2217e+00 --2.0100e+03 -1.4286e+03 6.1881e+00 --2.0100e+03 -1.4000e+03 6.1544e+00 --2.0100e+03 -1.3714e+03 6.1206e+00 --2.0100e+03 -1.3429e+03 6.0866e+00 --2.0100e+03 -1.3143e+03 6.0526e+00 --2.0100e+03 -1.2857e+03 6.0185e+00 --2.0100e+03 -1.2571e+03 5.9843e+00 --2.0100e+03 -1.2286e+03 5.9501e+00 --2.0100e+03 -1.2000e+03 5.9160e+00 --2.0100e+03 -1.1714e+03 5.8818e+00 --2.0100e+03 -1.1429e+03 5.8477e+00 --2.0100e+03 -1.1143e+03 5.8137e+00 --2.0100e+03 -1.0857e+03 5.7798e+00 --2.0100e+03 -1.0571e+03 5.7461e+00 --2.0100e+03 -1.0286e+03 5.7125e+00 --2.0100e+03 -1.0000e+03 5.6792e+00 --2.0100e+03 -9.7143e+02 5.6461e+00 --2.0100e+03 -9.4286e+02 5.6133e+00 --2.0100e+03 -9.1429e+02 5.5808e+00 --2.0100e+03 -8.8571e+02 5.5487e+00 --2.0100e+03 -8.5714e+02 5.5170e+00 --2.0100e+03 -8.2857e+02 5.4858e+00 --2.0100e+03 -8.0000e+02 5.4550e+00 --2.0100e+03 -7.7143e+02 5.4248e+00 --2.0100e+03 -7.4286e+02 5.3951e+00 --2.0100e+03 -7.1429e+02 5.3661e+00 --2.0100e+03 -6.8571e+02 5.3377e+00 --2.0100e+03 -6.5714e+02 5.3101e+00 --2.0100e+03 -6.2857e+02 5.2832e+00 --2.0100e+03 -6.0000e+02 5.2571e+00 --2.0100e+03 -5.7143e+02 5.2318e+00 --2.0100e+03 -5.4286e+02 5.2075e+00 --2.0100e+03 -5.1429e+02 5.1840e+00 --2.0100e+03 -4.8571e+02 5.1616e+00 --2.0100e+03 -4.5714e+02 5.1401e+00 --2.0100e+03 -4.2857e+02 5.1198e+00 --2.0100e+03 -4.0000e+02 5.1005e+00 --2.0100e+03 -3.7143e+02 5.0824e+00 --2.0100e+03 -3.4286e+02 5.0654e+00 --2.0100e+03 -3.1429e+02 5.0497e+00 --2.0100e+03 -2.8571e+02 5.0352e+00 --2.0100e+03 -2.5714e+02 5.0220e+00 --2.0100e+03 -2.2857e+02 5.0102e+00 --2.0100e+03 -2.0000e+02 4.9996e+00 --2.0100e+03 -1.7143e+02 4.9904e+00 --2.0100e+03 -1.4286e+02 4.9826e+00 --2.0100e+03 -1.1429e+02 4.9762e+00 --2.0100e+03 -8.5714e+01 4.9712e+00 --2.0100e+03 -5.7143e+01 4.9676e+00 --2.0100e+03 -2.8571e+01 4.9654e+00 --2.0100e+03 0.0000e+00 4.9647e+00 --2.0100e+03 2.8571e+01 4.9654e+00 --2.0100e+03 5.7143e+01 4.9676e+00 --2.0100e+03 8.5714e+01 4.9712e+00 --2.0100e+03 1.1429e+02 4.9762e+00 --2.0100e+03 1.4286e+02 4.9826e+00 --2.0100e+03 1.7143e+02 4.9904e+00 --2.0100e+03 2.0000e+02 4.9996e+00 --2.0100e+03 2.2857e+02 5.0102e+00 --2.0100e+03 2.5714e+02 5.0220e+00 --2.0100e+03 2.8571e+02 5.0352e+00 --2.0100e+03 3.1429e+02 5.0497e+00 --2.0100e+03 3.4286e+02 5.0654e+00 --2.0100e+03 3.7143e+02 5.0824e+00 --2.0100e+03 4.0000e+02 5.1005e+00 --2.0100e+03 4.2857e+02 5.1198e+00 --2.0100e+03 4.5714e+02 5.1401e+00 --2.0100e+03 4.8571e+02 5.1616e+00 --2.0100e+03 5.1429e+02 5.1840e+00 --2.0100e+03 5.4286e+02 5.2075e+00 --2.0100e+03 5.7143e+02 5.2318e+00 --2.0100e+03 6.0000e+02 5.2571e+00 --2.0100e+03 6.2857e+02 5.2832e+00 --2.0100e+03 6.5714e+02 5.3101e+00 --2.0100e+03 6.8571e+02 5.3377e+00 --2.0100e+03 7.1429e+02 5.3661e+00 --2.0100e+03 7.4286e+02 5.3951e+00 --2.0100e+03 7.7143e+02 5.4248e+00 --2.0100e+03 8.0000e+02 5.4550e+00 --2.0100e+03 8.2857e+02 5.4858e+00 --2.0100e+03 8.5714e+02 5.5170e+00 --2.0100e+03 8.8571e+02 5.5487e+00 --2.0100e+03 9.1429e+02 5.5808e+00 --2.0100e+03 9.4286e+02 5.6133e+00 --2.0100e+03 9.7143e+02 5.6461e+00 --2.0100e+03 1.0000e+03 5.6792e+00 --2.0100e+03 1.0286e+03 5.7125e+00 --2.0100e+03 1.0571e+03 5.7461e+00 --2.0100e+03 1.0857e+03 5.7798e+00 --2.0100e+03 1.1143e+03 5.8137e+00 --2.0100e+03 1.1429e+03 5.8477e+00 --2.0100e+03 1.1714e+03 5.8818e+00 --2.0100e+03 1.2000e+03 5.9160e+00 --2.0100e+03 1.2286e+03 5.9501e+00 --2.0100e+03 1.2571e+03 5.9843e+00 --2.0100e+03 1.2857e+03 6.0185e+00 --2.0100e+03 1.3143e+03 6.0526e+00 --2.0100e+03 1.3429e+03 6.0866e+00 --2.0100e+03 1.3714e+03 6.1206e+00 --2.0100e+03 1.4000e+03 6.1544e+00 --2.0100e+03 1.4286e+03 6.1881e+00 --2.0100e+03 1.4571e+03 6.2217e+00 --2.0100e+03 1.4857e+03 6.2551e+00 --2.0100e+03 1.5143e+03 6.2884e+00 --2.0100e+03 1.5429e+03 6.3214e+00 --2.0100e+03 1.5714e+03 6.3542e+00 --2.0100e+03 1.6000e+03 6.3869e+00 --2.0100e+03 1.6286e+03 6.4193e+00 --2.0100e+03 1.6571e+03 6.4514e+00 --2.0100e+03 1.6857e+03 6.4833e+00 --2.0100e+03 1.7143e+03 6.5150e+00 --2.0100e+03 1.7429e+03 6.5463e+00 --2.0100e+03 1.7714e+03 6.5775e+00 --2.0100e+03 1.8000e+03 6.6083e+00 --2.0100e+03 1.8286e+03 6.6388e+00 --2.0100e+03 1.8571e+03 6.6691e+00 --2.0100e+03 1.8857e+03 6.6991e+00 --2.0100e+03 1.9143e+03 6.7287e+00 --2.0100e+03 1.9429e+03 6.7581e+00 --2.0100e+03 1.9714e+03 6.7872e+00 --2.0100e+03 2.0000e+03 6.8159e+00 --1.9800e+03 -2.0000e+03 6.7856e+00 --1.9800e+03 -1.9714e+03 6.7560e+00 --1.9800e+03 -1.9429e+03 6.7262e+00 --1.9800e+03 -1.9143e+03 6.6960e+00 --1.9800e+03 -1.8857e+03 6.6655e+00 --1.9800e+03 -1.8571e+03 6.6346e+00 --1.9800e+03 -1.8286e+03 6.6035e+00 --1.9800e+03 -1.8000e+03 6.5720e+00 --1.9800e+03 -1.7714e+03 6.5403e+00 --1.9800e+03 -1.7429e+03 6.5082e+00 --1.9800e+03 -1.7143e+03 6.4758e+00 --1.9800e+03 -1.6857e+03 6.4432e+00 --1.9800e+03 -1.6571e+03 6.4103e+00 --1.9800e+03 -1.6286e+03 6.3771e+00 --1.9800e+03 -1.6000e+03 6.3436e+00 --1.9800e+03 -1.5714e+03 6.3099e+00 --1.9800e+03 -1.5429e+03 6.2759e+00 --1.9800e+03 -1.5143e+03 6.2417e+00 --1.9800e+03 -1.4857e+03 6.2073e+00 --1.9800e+03 -1.4571e+03 6.1727e+00 --1.9800e+03 -1.4286e+03 6.1379e+00 --1.9800e+03 -1.4000e+03 6.1029e+00 --1.9800e+03 -1.3714e+03 6.0678e+00 --1.9800e+03 -1.3429e+03 6.0326e+00 --1.9800e+03 -1.3143e+03 5.9972e+00 --1.9800e+03 -1.2857e+03 5.9618e+00 --1.9800e+03 -1.2571e+03 5.9262e+00 --1.9800e+03 -1.2286e+03 5.8907e+00 --1.9800e+03 -1.2000e+03 5.8551e+00 --1.9800e+03 -1.1714e+03 5.8195e+00 --1.9800e+03 -1.1429e+03 5.7840e+00 --1.9800e+03 -1.1143e+03 5.7485e+00 --1.9800e+03 -1.0857e+03 5.7132e+00 --1.9800e+03 -1.0571e+03 5.6780e+00 --1.9800e+03 -1.0286e+03 5.6429e+00 --1.9800e+03 -1.0000e+03 5.6081e+00 --1.9800e+03 -9.7143e+02 5.5734e+00 --1.9800e+03 -9.4286e+02 5.5391e+00 --1.9800e+03 -9.1429e+02 5.5051e+00 --1.9800e+03 -8.8571e+02 5.4715e+00 --1.9800e+03 -8.5714e+02 5.4383e+00 --1.9800e+03 -8.2857e+02 5.4055e+00 --1.9800e+03 -8.0000e+02 5.3732e+00 --1.9800e+03 -7.7143e+02 5.3415e+00 --1.9800e+03 -7.4286e+02 5.3103e+00 --1.9800e+03 -7.1429e+02 5.2798e+00 --1.9800e+03 -6.8571e+02 5.2499e+00 --1.9800e+03 -6.5714e+02 5.2208e+00 --1.9800e+03 -6.2857e+02 5.1925e+00 --1.9800e+03 -6.0000e+02 5.1650e+00 --1.9800e+03 -5.7143e+02 5.1384e+00 --1.9800e+03 -5.4286e+02 5.1127e+00 --1.9800e+03 -5.1429e+02 5.0880e+00 --1.9800e+03 -4.8571e+02 5.0643e+00 --1.9800e+03 -4.5714e+02 5.0417e+00 --1.9800e+03 -4.2857e+02 5.0202e+00 --1.9800e+03 -4.0000e+02 4.9999e+00 --1.9800e+03 -3.7143e+02 4.9807e+00 --1.9800e+03 -3.4286e+02 4.9628e+00 --1.9800e+03 -3.1429e+02 4.9462e+00 --1.9800e+03 -2.8571e+02 4.9309e+00 --1.9800e+03 -2.5714e+02 4.9169e+00 --1.9800e+03 -2.2857e+02 4.9043e+00 --1.9800e+03 -2.0000e+02 4.8932e+00 --1.9800e+03 -1.7143e+02 4.8834e+00 --1.9800e+03 -1.4286e+02 4.8751e+00 --1.9800e+03 -1.1429e+02 4.8683e+00 --1.9800e+03 -8.5714e+01 4.8630e+00 --1.9800e+03 -5.7143e+01 4.8592e+00 --1.9800e+03 -2.8571e+01 4.8570e+00 --1.9800e+03 0.0000e+00 4.8562e+00 --1.9800e+03 2.8571e+01 4.8570e+00 --1.9800e+03 5.7143e+01 4.8592e+00 --1.9800e+03 8.5714e+01 4.8630e+00 --1.9800e+03 1.1429e+02 4.8683e+00 --1.9800e+03 1.4286e+02 4.8751e+00 --1.9800e+03 1.7143e+02 4.8834e+00 --1.9800e+03 2.0000e+02 4.8932e+00 --1.9800e+03 2.2857e+02 4.9043e+00 --1.9800e+03 2.5714e+02 4.9169e+00 --1.9800e+03 2.8571e+02 4.9309e+00 --1.9800e+03 3.1429e+02 4.9462e+00 --1.9800e+03 3.4286e+02 4.9628e+00 --1.9800e+03 3.7143e+02 4.9807e+00 --1.9800e+03 4.0000e+02 4.9999e+00 --1.9800e+03 4.2857e+02 5.0202e+00 --1.9800e+03 4.5714e+02 5.0417e+00 --1.9800e+03 4.8571e+02 5.0643e+00 --1.9800e+03 5.1429e+02 5.0880e+00 --1.9800e+03 5.4286e+02 5.1127e+00 --1.9800e+03 5.7143e+02 5.1384e+00 --1.9800e+03 6.0000e+02 5.1650e+00 --1.9800e+03 6.2857e+02 5.1925e+00 --1.9800e+03 6.5714e+02 5.2208e+00 --1.9800e+03 6.8571e+02 5.2499e+00 --1.9800e+03 7.1429e+02 5.2798e+00 --1.9800e+03 7.4286e+02 5.3103e+00 --1.9800e+03 7.7143e+02 5.3415e+00 --1.9800e+03 8.0000e+02 5.3732e+00 --1.9800e+03 8.2857e+02 5.4055e+00 --1.9800e+03 8.5714e+02 5.4383e+00 --1.9800e+03 8.8571e+02 5.4715e+00 --1.9800e+03 9.1429e+02 5.5051e+00 --1.9800e+03 9.4286e+02 5.5391e+00 --1.9800e+03 9.7143e+02 5.5734e+00 --1.9800e+03 1.0000e+03 5.6081e+00 --1.9800e+03 1.0286e+03 5.6429e+00 --1.9800e+03 1.0571e+03 5.6780e+00 --1.9800e+03 1.0857e+03 5.7132e+00 --1.9800e+03 1.1143e+03 5.7485e+00 --1.9800e+03 1.1429e+03 5.7840e+00 --1.9800e+03 1.1714e+03 5.8195e+00 --1.9800e+03 1.2000e+03 5.8551e+00 --1.9800e+03 1.2286e+03 5.8907e+00 --1.9800e+03 1.2571e+03 5.9262e+00 --1.9800e+03 1.2857e+03 5.9618e+00 --1.9800e+03 1.3143e+03 5.9972e+00 --1.9800e+03 1.3429e+03 6.0326e+00 --1.9800e+03 1.3714e+03 6.0678e+00 --1.9800e+03 1.4000e+03 6.1029e+00 --1.9800e+03 1.4286e+03 6.1379e+00 --1.9800e+03 1.4571e+03 6.1727e+00 --1.9800e+03 1.4857e+03 6.2073e+00 --1.9800e+03 1.5143e+03 6.2417e+00 --1.9800e+03 1.5429e+03 6.2759e+00 --1.9800e+03 1.5714e+03 6.3099e+00 --1.9800e+03 1.6000e+03 6.3436e+00 --1.9800e+03 1.6286e+03 6.3771e+00 --1.9800e+03 1.6571e+03 6.4103e+00 --1.9800e+03 1.6857e+03 6.4432e+00 --1.9800e+03 1.7143e+03 6.4758e+00 --1.9800e+03 1.7429e+03 6.5082e+00 --1.9800e+03 1.7714e+03 6.5403e+00 --1.9800e+03 1.8000e+03 6.5720e+00 --1.9800e+03 1.8286e+03 6.6035e+00 --1.9800e+03 1.8571e+03 6.6346e+00 --1.9800e+03 1.8857e+03 6.6655e+00 --1.9800e+03 1.9143e+03 6.6960e+00 --1.9800e+03 1.9429e+03 6.7262e+00 --1.9800e+03 1.9714e+03 6.7560e+00 --1.9800e+03 2.0000e+03 6.7856e+00 --1.9500e+03 -2.0000e+03 6.7549e+00 --1.9500e+03 -1.9714e+03 6.7245e+00 --1.9500e+03 -1.9429e+03 6.6938e+00 --1.9500e+03 -1.9143e+03 6.6628e+00 --1.9500e+03 -1.8857e+03 6.6314e+00 --1.9500e+03 -1.8571e+03 6.5997e+00 --1.9500e+03 -1.8286e+03 6.5677e+00 --1.9500e+03 -1.8000e+03 6.5353e+00 --1.9500e+03 -1.7714e+03 6.5025e+00 --1.9500e+03 -1.7429e+03 6.4695e+00 --1.9500e+03 -1.7143e+03 6.4361e+00 --1.9500e+03 -1.6857e+03 6.4024e+00 --1.9500e+03 -1.6571e+03 6.3684e+00 --1.9500e+03 -1.6286e+03 6.3341e+00 --1.9500e+03 -1.6000e+03 6.2995e+00 --1.9500e+03 -1.5714e+03 6.2647e+00 --1.9500e+03 -1.5429e+03 6.2296e+00 --1.9500e+03 -1.5143e+03 6.1942e+00 --1.9500e+03 -1.4857e+03 6.1585e+00 --1.9500e+03 -1.4571e+03 6.1227e+00 --1.9500e+03 -1.4286e+03 6.0866e+00 --1.9500e+03 -1.4000e+03 6.0503e+00 --1.9500e+03 -1.3714e+03 6.0139e+00 --1.9500e+03 -1.3429e+03 5.9773e+00 --1.9500e+03 -1.3143e+03 5.9405e+00 --1.9500e+03 -1.2857e+03 5.9037e+00 --1.9500e+03 -1.2571e+03 5.8667e+00 --1.9500e+03 -1.2286e+03 5.8297e+00 --1.9500e+03 -1.2000e+03 5.7927e+00 --1.9500e+03 -1.1714e+03 5.7556e+00 --1.9500e+03 -1.1429e+03 5.7185e+00 --1.9500e+03 -1.1143e+03 5.6815e+00 --1.9500e+03 -1.0857e+03 5.6446e+00 --1.9500e+03 -1.0571e+03 5.6078e+00 --1.9500e+03 -1.0286e+03 5.5712e+00 --1.9500e+03 -1.0000e+03 5.5348e+00 --1.9500e+03 -9.7143e+02 5.4985e+00 --1.9500e+03 -9.4286e+02 5.4626e+00 --1.9500e+03 -9.1429e+02 5.4270e+00 --1.9500e+03 -8.8571e+02 5.3917e+00 --1.9500e+03 -8.5714e+02 5.3569e+00 --1.9500e+03 -8.2857e+02 5.3225e+00 --1.9500e+03 -8.0000e+02 5.2886e+00 --1.9500e+03 -7.7143e+02 5.2552e+00 --1.9500e+03 -7.4286e+02 5.2225e+00 --1.9500e+03 -7.1429e+02 5.1903e+00 --1.9500e+03 -6.8571e+02 5.1590e+00 --1.9500e+03 -6.5714e+02 5.1283e+00 --1.9500e+03 -6.2857e+02 5.0985e+00 --1.9500e+03 -6.0000e+02 5.0695e+00 --1.9500e+03 -5.7143e+02 5.0414e+00 --1.9500e+03 -5.4286e+02 5.0143e+00 --1.9500e+03 -5.1429e+02 4.9882e+00 --1.9500e+03 -4.8571e+02 4.9632e+00 --1.9500e+03 -4.5714e+02 4.9393e+00 --1.9500e+03 -4.2857e+02 4.9166e+00 --1.9500e+03 -4.0000e+02 4.8951e+00 --1.9500e+03 -3.7143e+02 4.8748e+00 --1.9500e+03 -3.4286e+02 4.8559e+00 --1.9500e+03 -3.1429e+02 4.8383e+00 --1.9500e+03 -2.8571e+02 4.8221e+00 --1.9500e+03 -2.5714e+02 4.8073e+00 --1.9500e+03 -2.2857e+02 4.7939e+00 --1.9500e+03 -2.0000e+02 4.7821e+00 --1.9500e+03 -1.7143e+02 4.7718e+00 --1.9500e+03 -1.4286e+02 4.7630e+00 --1.9500e+03 -1.1429e+02 4.7558e+00 --1.9500e+03 -8.5714e+01 4.7502e+00 --1.9500e+03 -5.7143e+01 4.7462e+00 --1.9500e+03 -2.8571e+01 4.7437e+00 --1.9500e+03 0.0000e+00 4.7429e+00 --1.9500e+03 2.8571e+01 4.7437e+00 --1.9500e+03 5.7143e+01 4.7462e+00 --1.9500e+03 8.5714e+01 4.7502e+00 --1.9500e+03 1.1429e+02 4.7558e+00 --1.9500e+03 1.4286e+02 4.7630e+00 --1.9500e+03 1.7143e+02 4.7718e+00 --1.9500e+03 2.0000e+02 4.7821e+00 --1.9500e+03 2.2857e+02 4.7939e+00 --1.9500e+03 2.5714e+02 4.8073e+00 --1.9500e+03 2.8571e+02 4.8221e+00 --1.9500e+03 3.1429e+02 4.8383e+00 --1.9500e+03 3.4286e+02 4.8559e+00 --1.9500e+03 3.7143e+02 4.8748e+00 --1.9500e+03 4.0000e+02 4.8951e+00 --1.9500e+03 4.2857e+02 4.9166e+00 --1.9500e+03 4.5714e+02 4.9393e+00 --1.9500e+03 4.8571e+02 4.9632e+00 --1.9500e+03 5.1429e+02 4.9882e+00 --1.9500e+03 5.4286e+02 5.0143e+00 --1.9500e+03 5.7143e+02 5.0414e+00 --1.9500e+03 6.0000e+02 5.0695e+00 --1.9500e+03 6.2857e+02 5.0985e+00 --1.9500e+03 6.5714e+02 5.1283e+00 --1.9500e+03 6.8571e+02 5.1590e+00 --1.9500e+03 7.1429e+02 5.1903e+00 --1.9500e+03 7.4286e+02 5.2225e+00 --1.9500e+03 7.7143e+02 5.2552e+00 --1.9500e+03 8.0000e+02 5.2886e+00 --1.9500e+03 8.2857e+02 5.3225e+00 --1.9500e+03 8.5714e+02 5.3569e+00 --1.9500e+03 8.8571e+02 5.3917e+00 --1.9500e+03 9.1429e+02 5.4270e+00 --1.9500e+03 9.4286e+02 5.4626e+00 --1.9500e+03 9.7143e+02 5.4985e+00 --1.9500e+03 1.0000e+03 5.5348e+00 --1.9500e+03 1.0286e+03 5.5712e+00 --1.9500e+03 1.0571e+03 5.6078e+00 --1.9500e+03 1.0857e+03 5.6446e+00 --1.9500e+03 1.1143e+03 5.6815e+00 --1.9500e+03 1.1429e+03 5.7185e+00 --1.9500e+03 1.1714e+03 5.7556e+00 --1.9500e+03 1.2000e+03 5.7927e+00 --1.9500e+03 1.2286e+03 5.8297e+00 --1.9500e+03 1.2571e+03 5.8667e+00 --1.9500e+03 1.2857e+03 5.9037e+00 --1.9500e+03 1.3143e+03 5.9405e+00 --1.9500e+03 1.3429e+03 5.9773e+00 --1.9500e+03 1.3714e+03 6.0139e+00 --1.9500e+03 1.4000e+03 6.0503e+00 --1.9500e+03 1.4286e+03 6.0866e+00 --1.9500e+03 1.4571e+03 6.1227e+00 --1.9500e+03 1.4857e+03 6.1585e+00 --1.9500e+03 1.5143e+03 6.1942e+00 --1.9500e+03 1.5429e+03 6.2296e+00 --1.9500e+03 1.5714e+03 6.2647e+00 --1.9500e+03 1.6000e+03 6.2995e+00 --1.9500e+03 1.6286e+03 6.3341e+00 --1.9500e+03 1.6571e+03 6.3684e+00 --1.9500e+03 1.6857e+03 6.4024e+00 --1.9500e+03 1.7143e+03 6.4361e+00 --1.9500e+03 1.7429e+03 6.4695e+00 --1.9500e+03 1.7714e+03 6.5025e+00 --1.9500e+03 1.8000e+03 6.5353e+00 --1.9500e+03 1.8286e+03 6.5677e+00 --1.9500e+03 1.8571e+03 6.5997e+00 --1.9500e+03 1.8857e+03 6.6314e+00 --1.9500e+03 1.9143e+03 6.6628e+00 --1.9500e+03 1.9429e+03 6.6938e+00 --1.9500e+03 1.9714e+03 6.7245e+00 --1.9500e+03 2.0000e+03 6.7549e+00 --1.9200e+03 -2.0000e+03 6.7238e+00 --1.9200e+03 -1.9714e+03 6.6927e+00 --1.9200e+03 -1.9429e+03 6.6611e+00 --1.9200e+03 -1.9143e+03 6.6292e+00 --1.9200e+03 -1.8857e+03 6.5970e+00 --1.9200e+03 -1.8571e+03 6.5643e+00 --1.9200e+03 -1.8286e+03 6.5313e+00 --1.9200e+03 -1.8000e+03 6.4980e+00 --1.9200e+03 -1.7714e+03 6.4642e+00 --1.9200e+03 -1.7429e+03 6.4302e+00 --1.9200e+03 -1.7143e+03 6.3957e+00 --1.9200e+03 -1.6857e+03 6.3610e+00 --1.9200e+03 -1.6571e+03 6.3259e+00 --1.9200e+03 -1.6286e+03 6.2905e+00 --1.9200e+03 -1.6000e+03 6.2547e+00 --1.9200e+03 -1.5714e+03 6.2187e+00 --1.9200e+03 -1.5429e+03 6.1823e+00 --1.9200e+03 -1.5143e+03 6.1457e+00 --1.9200e+03 -1.4857e+03 6.1088e+00 --1.9200e+03 -1.4571e+03 6.0716e+00 --1.9200e+03 -1.4286e+03 6.0342e+00 --1.9200e+03 -1.4000e+03 5.9966e+00 --1.9200e+03 -1.3714e+03 5.9587e+00 --1.9200e+03 -1.3429e+03 5.9207e+00 --1.9200e+03 -1.3143e+03 5.8825e+00 --1.9200e+03 -1.2857e+03 5.8442e+00 --1.9200e+03 -1.2571e+03 5.8057e+00 --1.9200e+03 -1.2286e+03 5.7672e+00 --1.9200e+03 -1.2000e+03 5.7286e+00 --1.9200e+03 -1.1714e+03 5.6899e+00 --1.9200e+03 -1.1429e+03 5.6513e+00 --1.9200e+03 -1.1143e+03 5.6127e+00 --1.9200e+03 -1.0857e+03 5.5741e+00 --1.9200e+03 -1.0571e+03 5.5357e+00 --1.9200e+03 -1.0286e+03 5.4974e+00 --1.9200e+03 -1.0000e+03 5.4592e+00 --1.9200e+03 -9.7143e+02 5.4213e+00 --1.9200e+03 -9.4286e+02 5.3837e+00 --1.9200e+03 -9.1429e+02 5.3463e+00 --1.9200e+03 -8.8571e+02 5.3093e+00 --1.9200e+03 -8.5714e+02 5.2727e+00 --1.9200e+03 -8.2857e+02 5.2366e+00 --1.9200e+03 -8.0000e+02 5.2010e+00 --1.9200e+03 -7.7143e+02 5.1659e+00 --1.9200e+03 -7.4286e+02 5.1314e+00 --1.9200e+03 -7.1429e+02 5.0976e+00 --1.9200e+03 -6.8571e+02 5.0646e+00 --1.9200e+03 -6.5714e+02 5.0323e+00 --1.9200e+03 -6.2857e+02 5.0008e+00 --1.9200e+03 -6.0000e+02 4.9702e+00 --1.9200e+03 -5.7143e+02 4.9406e+00 --1.9200e+03 -5.4286e+02 4.9120e+00 --1.9200e+03 -5.1429e+02 4.8844e+00 --1.9200e+03 -4.8571e+02 4.8580e+00 --1.9200e+03 -4.5714e+02 4.8327e+00 --1.9200e+03 -4.2857e+02 4.8087e+00 --1.9200e+03 -4.0000e+02 4.7859e+00 --1.9200e+03 -3.7143e+02 4.7644e+00 --1.9200e+03 -3.4286e+02 4.7444e+00 --1.9200e+03 -3.1429e+02 4.7257e+00 --1.9200e+03 -2.8571e+02 4.7085e+00 --1.9200e+03 -2.5714e+02 4.6929e+00 --1.9200e+03 -2.2857e+02 4.6787e+00 --1.9200e+03 -2.0000e+02 4.6662e+00 --1.9200e+03 -1.7143e+02 4.6552e+00 --1.9200e+03 -1.4286e+02 4.6459e+00 --1.9200e+03 -1.1429e+02 4.6383e+00 --1.9200e+03 -8.5714e+01 4.6323e+00 --1.9200e+03 -5.7143e+01 4.6280e+00 --1.9200e+03 -2.8571e+01 4.6255e+00 --1.9200e+03 0.0000e+00 4.6246e+00 --1.9200e+03 2.8571e+01 4.6255e+00 --1.9200e+03 5.7143e+01 4.6280e+00 --1.9200e+03 8.5714e+01 4.6323e+00 --1.9200e+03 1.1429e+02 4.6383e+00 --1.9200e+03 1.4286e+02 4.6459e+00 --1.9200e+03 1.7143e+02 4.6552e+00 --1.9200e+03 2.0000e+02 4.6662e+00 --1.9200e+03 2.2857e+02 4.6787e+00 --1.9200e+03 2.5714e+02 4.6929e+00 --1.9200e+03 2.8571e+02 4.7085e+00 --1.9200e+03 3.1429e+02 4.7257e+00 --1.9200e+03 3.4286e+02 4.7444e+00 --1.9200e+03 3.7143e+02 4.7644e+00 --1.9200e+03 4.0000e+02 4.7859e+00 --1.9200e+03 4.2857e+02 4.8087e+00 --1.9200e+03 4.5714e+02 4.8327e+00 --1.9200e+03 4.8571e+02 4.8580e+00 --1.9200e+03 5.1429e+02 4.8844e+00 --1.9200e+03 5.4286e+02 4.9120e+00 --1.9200e+03 5.7143e+02 4.9406e+00 --1.9200e+03 6.0000e+02 4.9702e+00 --1.9200e+03 6.2857e+02 5.0008e+00 --1.9200e+03 6.5714e+02 5.0323e+00 --1.9200e+03 6.8571e+02 5.0646e+00 --1.9200e+03 7.1429e+02 5.0976e+00 --1.9200e+03 7.4286e+02 5.1314e+00 --1.9200e+03 7.7143e+02 5.1659e+00 --1.9200e+03 8.0000e+02 5.2010e+00 --1.9200e+03 8.2857e+02 5.2366e+00 --1.9200e+03 8.5714e+02 5.2727e+00 --1.9200e+03 8.8571e+02 5.3093e+00 --1.9200e+03 9.1429e+02 5.3463e+00 --1.9200e+03 9.4286e+02 5.3837e+00 --1.9200e+03 9.7143e+02 5.4213e+00 --1.9200e+03 1.0000e+03 5.4592e+00 --1.9200e+03 1.0286e+03 5.4974e+00 --1.9200e+03 1.0571e+03 5.5357e+00 --1.9200e+03 1.0857e+03 5.5741e+00 --1.9200e+03 1.1143e+03 5.6127e+00 --1.9200e+03 1.1429e+03 5.6513e+00 --1.9200e+03 1.1714e+03 5.6899e+00 --1.9200e+03 1.2000e+03 5.7286e+00 --1.9200e+03 1.2286e+03 5.7672e+00 --1.9200e+03 1.2571e+03 5.8057e+00 --1.9200e+03 1.2857e+03 5.8442e+00 --1.9200e+03 1.3143e+03 5.8825e+00 --1.9200e+03 1.3429e+03 5.9207e+00 --1.9200e+03 1.3714e+03 5.9587e+00 --1.9200e+03 1.4000e+03 5.9966e+00 --1.9200e+03 1.4286e+03 6.0342e+00 --1.9200e+03 1.4571e+03 6.0716e+00 --1.9200e+03 1.4857e+03 6.1088e+00 --1.9200e+03 1.5143e+03 6.1457e+00 --1.9200e+03 1.5429e+03 6.1823e+00 --1.9200e+03 1.5714e+03 6.2187e+00 --1.9200e+03 1.6000e+03 6.2547e+00 --1.9200e+03 1.6286e+03 6.2905e+00 --1.9200e+03 1.6571e+03 6.3259e+00 --1.9200e+03 1.6857e+03 6.3610e+00 --1.9200e+03 1.7143e+03 6.3957e+00 --1.9200e+03 1.7429e+03 6.4302e+00 --1.9200e+03 1.7714e+03 6.4642e+00 --1.9200e+03 1.8000e+03 6.4980e+00 --1.9200e+03 1.8286e+03 6.5313e+00 --1.9200e+03 1.8571e+03 6.5643e+00 --1.9200e+03 1.8857e+03 6.5970e+00 --1.9200e+03 1.9143e+03 6.6292e+00 --1.9200e+03 1.9429e+03 6.6611e+00 --1.9200e+03 1.9714e+03 6.6927e+00 --1.9200e+03 2.0000e+03 6.7238e+00 --1.8900e+03 -2.0000e+03 6.6924e+00 --1.8900e+03 -1.9714e+03 6.6604e+00 --1.8900e+03 -1.9429e+03 6.6280e+00 --1.8900e+03 -1.9143e+03 6.5952e+00 --1.8900e+03 -1.8857e+03 6.5620e+00 --1.8900e+03 -1.8571e+03 6.5285e+00 --1.8900e+03 -1.8286e+03 6.4945e+00 --1.8900e+03 -1.8000e+03 6.4601e+00 --1.8900e+03 -1.7714e+03 6.4254e+00 --1.8900e+03 -1.7429e+03 6.3903e+00 --1.8900e+03 -1.7143e+03 6.3547e+00 --1.8900e+03 -1.6857e+03 6.3189e+00 --1.8900e+03 -1.6571e+03 6.2826e+00 --1.8900e+03 -1.6286e+03 6.2460e+00 --1.8900e+03 -1.6000e+03 6.2091e+00 --1.8900e+03 -1.5714e+03 6.1718e+00 --1.8900e+03 -1.5429e+03 6.1342e+00 --1.8900e+03 -1.5143e+03 6.0962e+00 --1.8900e+03 -1.4857e+03 6.0580e+00 --1.8900e+03 -1.4571e+03 6.0195e+00 --1.8900e+03 -1.4286e+03 5.9807e+00 --1.8900e+03 -1.4000e+03 5.9416e+00 --1.8900e+03 -1.3714e+03 5.9023e+00 --1.8900e+03 -1.3429e+03 5.8628e+00 --1.8900e+03 -1.3143e+03 5.8231e+00 --1.8900e+03 -1.2857e+03 5.7833e+00 --1.8900e+03 -1.2571e+03 5.7432e+00 --1.8900e+03 -1.2286e+03 5.7031e+00 --1.8900e+03 -1.2000e+03 5.6628e+00 --1.8900e+03 -1.1714e+03 5.6225e+00 --1.8900e+03 -1.1429e+03 5.5822e+00 --1.8900e+03 -1.1143e+03 5.5419e+00 --1.8900e+03 -1.0857e+03 5.5016e+00 --1.8900e+03 -1.0571e+03 5.4614e+00 --1.8900e+03 -1.0286e+03 5.4213e+00 --1.8900e+03 -1.0000e+03 5.3814e+00 --1.8900e+03 -9.7143e+02 5.3416e+00 --1.8900e+03 -9.4286e+02 5.3022e+00 --1.8900e+03 -9.1429e+02 5.2630e+00 --1.8900e+03 -8.8571e+02 5.2242e+00 --1.8900e+03 -8.5714e+02 5.1857e+00 --1.8900e+03 -8.2857e+02 5.1478e+00 --1.8900e+03 -8.0000e+02 5.1103e+00 --1.8900e+03 -7.7143e+02 5.0734e+00 --1.8900e+03 -7.4286e+02 5.0371e+00 --1.8900e+03 -7.1429e+02 5.0015e+00 --1.8900e+03 -6.8571e+02 4.9666e+00 --1.8900e+03 -6.5714e+02 4.9326e+00 --1.8900e+03 -6.2857e+02 4.8994e+00 --1.8900e+03 -6.0000e+02 4.8671e+00 --1.8900e+03 -5.7143e+02 4.8358e+00 --1.8900e+03 -5.4286e+02 4.8055e+00 --1.8900e+03 -5.1429e+02 4.7764e+00 --1.8900e+03 -4.8571e+02 4.7484e+00 --1.8900e+03 -4.5714e+02 4.7216e+00 --1.8900e+03 -4.2857e+02 4.6962e+00 --1.8900e+03 -4.0000e+02 4.6720e+00 --1.8900e+03 -3.7143e+02 4.6493e+00 --1.8900e+03 -3.4286e+02 4.6280e+00 --1.8900e+03 -3.1429e+02 4.6082e+00 --1.8900e+03 -2.8571e+02 4.5900e+00 --1.8900e+03 -2.5714e+02 4.5734e+00 --1.8900e+03 -2.2857e+02 4.5583e+00 --1.8900e+03 -2.0000e+02 4.5450e+00 --1.8900e+03 -1.7143e+02 4.5334e+00 --1.8900e+03 -1.4286e+02 4.5235e+00 --1.8900e+03 -1.1429e+02 4.5154e+00 --1.8900e+03 -8.5714e+01 4.5090e+00 --1.8900e+03 -5.7143e+01 4.5045e+00 --1.8900e+03 -2.8571e+01 4.5018e+00 --1.8900e+03 0.0000e+00 4.5008e+00 --1.8900e+03 2.8571e+01 4.5018e+00 --1.8900e+03 5.7143e+01 4.5045e+00 --1.8900e+03 8.5714e+01 4.5090e+00 --1.8900e+03 1.1429e+02 4.5154e+00 --1.8900e+03 1.4286e+02 4.5235e+00 --1.8900e+03 1.7143e+02 4.5334e+00 --1.8900e+03 2.0000e+02 4.5450e+00 --1.8900e+03 2.2857e+02 4.5583e+00 --1.8900e+03 2.5714e+02 4.5734e+00 --1.8900e+03 2.8571e+02 4.5900e+00 --1.8900e+03 3.1429e+02 4.6082e+00 --1.8900e+03 3.4286e+02 4.6280e+00 --1.8900e+03 3.7143e+02 4.6493e+00 --1.8900e+03 4.0000e+02 4.6720e+00 --1.8900e+03 4.2857e+02 4.6962e+00 --1.8900e+03 4.5714e+02 4.7216e+00 --1.8900e+03 4.8571e+02 4.7484e+00 --1.8900e+03 5.1429e+02 4.7764e+00 --1.8900e+03 5.4286e+02 4.8055e+00 --1.8900e+03 5.7143e+02 4.8358e+00 --1.8900e+03 6.0000e+02 4.8671e+00 --1.8900e+03 6.2857e+02 4.8994e+00 --1.8900e+03 6.5714e+02 4.9326e+00 --1.8900e+03 6.8571e+02 4.9666e+00 --1.8900e+03 7.1429e+02 5.0015e+00 --1.8900e+03 7.4286e+02 5.0371e+00 --1.8900e+03 7.7143e+02 5.0734e+00 --1.8900e+03 8.0000e+02 5.1103e+00 --1.8900e+03 8.2857e+02 5.1478e+00 --1.8900e+03 8.5714e+02 5.1857e+00 --1.8900e+03 8.8571e+02 5.2242e+00 --1.8900e+03 9.1429e+02 5.2630e+00 --1.8900e+03 9.4286e+02 5.3022e+00 --1.8900e+03 9.7143e+02 5.3416e+00 --1.8900e+03 1.0000e+03 5.3814e+00 --1.8900e+03 1.0286e+03 5.4213e+00 --1.8900e+03 1.0571e+03 5.4614e+00 --1.8900e+03 1.0857e+03 5.5016e+00 --1.8900e+03 1.1143e+03 5.5419e+00 --1.8900e+03 1.1429e+03 5.5822e+00 --1.8900e+03 1.1714e+03 5.6225e+00 --1.8900e+03 1.2000e+03 5.6628e+00 --1.8900e+03 1.2286e+03 5.7031e+00 --1.8900e+03 1.2571e+03 5.7432e+00 --1.8900e+03 1.2857e+03 5.7833e+00 --1.8900e+03 1.3143e+03 5.8231e+00 --1.8900e+03 1.3429e+03 5.8628e+00 --1.8900e+03 1.3714e+03 5.9023e+00 --1.8900e+03 1.4000e+03 5.9416e+00 --1.8900e+03 1.4286e+03 5.9807e+00 --1.8900e+03 1.4571e+03 6.0195e+00 --1.8900e+03 1.4857e+03 6.0580e+00 --1.8900e+03 1.5143e+03 6.0962e+00 --1.8900e+03 1.5429e+03 6.1342e+00 --1.8900e+03 1.5714e+03 6.1718e+00 --1.8900e+03 1.6000e+03 6.2091e+00 --1.8900e+03 1.6286e+03 6.2460e+00 --1.8900e+03 1.6571e+03 6.2826e+00 --1.8900e+03 1.6857e+03 6.3189e+00 --1.8900e+03 1.7143e+03 6.3547e+00 --1.8900e+03 1.7429e+03 6.3903e+00 --1.8900e+03 1.7714e+03 6.4254e+00 --1.8900e+03 1.8000e+03 6.4601e+00 --1.8900e+03 1.8286e+03 6.4945e+00 --1.8900e+03 1.8571e+03 6.5285e+00 --1.8900e+03 1.8857e+03 6.5620e+00 --1.8900e+03 1.9143e+03 6.5952e+00 --1.8900e+03 1.9429e+03 6.6280e+00 --1.8900e+03 1.9714e+03 6.6604e+00 --1.8900e+03 2.0000e+03 6.6924e+00 --1.8600e+03 -2.0000e+03 6.6607e+00 --1.8600e+03 -1.9714e+03 6.6278e+00 --1.8600e+03 -1.9429e+03 6.5945e+00 --1.8600e+03 -1.9143e+03 6.5608e+00 --1.8600e+03 -1.8857e+03 6.5267e+00 --1.8600e+03 -1.8571e+03 6.4921e+00 --1.8600e+03 -1.8286e+03 6.4572e+00 --1.8600e+03 -1.8000e+03 6.4218e+00 --1.8600e+03 -1.7714e+03 6.3860e+00 --1.8600e+03 -1.7429e+03 6.3497e+00 --1.8600e+03 -1.7143e+03 6.3131e+00 --1.8600e+03 -1.6857e+03 6.2761e+00 --1.8600e+03 -1.6571e+03 6.2386e+00 --1.8600e+03 -1.6286e+03 6.2008e+00 --1.8600e+03 -1.6000e+03 6.1626e+00 --1.8600e+03 -1.5714e+03 6.1241e+00 --1.8600e+03 -1.5429e+03 6.0851e+00 --1.8600e+03 -1.5143e+03 6.0459e+00 --1.8600e+03 -1.4857e+03 6.0062e+00 --1.8600e+03 -1.4571e+03 5.9663e+00 --1.8600e+03 -1.4286e+03 5.9260e+00 --1.8600e+03 -1.4000e+03 5.8855e+00 --1.8600e+03 -1.3714e+03 5.8447e+00 --1.8600e+03 -1.3429e+03 5.8036e+00 --1.8600e+03 -1.3143e+03 5.7623e+00 --1.8600e+03 -1.2857e+03 5.7208e+00 --1.8600e+03 -1.2571e+03 5.6791e+00 --1.8600e+03 -1.2286e+03 5.6373e+00 --1.8600e+03 -1.2000e+03 5.5954e+00 --1.8600e+03 -1.1714e+03 5.5533e+00 --1.8600e+03 -1.1429e+03 5.5112e+00 --1.8600e+03 -1.1143e+03 5.4691e+00 --1.8600e+03 -1.0857e+03 5.4270e+00 --1.8600e+03 -1.0571e+03 5.3849e+00 --1.8600e+03 -1.0286e+03 5.3429e+00 --1.8600e+03 -1.0000e+03 5.3011e+00 --1.8600e+03 -9.7143e+02 5.2595e+00 --1.8600e+03 -9.4286e+02 5.2180e+00 --1.8600e+03 -9.1429e+02 5.1769e+00 --1.8600e+03 -8.8571e+02 5.1361e+00 --1.8600e+03 -8.5714e+02 5.0958e+00 --1.8600e+03 -8.2857e+02 5.0558e+00 --1.8600e+03 -8.0000e+02 5.0164e+00 --1.8600e+03 -7.7143e+02 4.9775e+00 --1.8600e+03 -7.4286e+02 4.9393e+00 --1.8600e+03 -7.1429e+02 4.9017e+00 --1.8600e+03 -6.8571e+02 4.8650e+00 --1.8600e+03 -6.5714e+02 4.8290e+00 --1.8600e+03 -6.2857e+02 4.7939e+00 --1.8600e+03 -6.0000e+02 4.7598e+00 --1.8600e+03 -5.7143e+02 4.7267e+00 --1.8600e+03 -5.4286e+02 4.6947e+00 --1.8600e+03 -5.1429e+02 4.6638e+00 --1.8600e+03 -4.8571e+02 4.6342e+00 --1.8600e+03 -4.5714e+02 4.6058e+00 --1.8600e+03 -4.2857e+02 4.5788e+00 --1.8600e+03 -4.0000e+02 4.5532e+00 --1.8600e+03 -3.7143e+02 4.5291e+00 --1.8600e+03 -3.4286e+02 4.5065e+00 --1.8600e+03 -3.1429e+02 4.4855e+00 --1.8600e+03 -2.8571e+02 4.4661e+00 --1.8600e+03 -2.5714e+02 4.4484e+00 --1.8600e+03 -2.2857e+02 4.4325e+00 --1.8600e+03 -2.0000e+02 4.4183e+00 --1.8600e+03 -1.7143e+02 4.4059e+00 --1.8600e+03 -1.4286e+02 4.3954e+00 --1.8600e+03 -1.1429e+02 4.3867e+00 --1.8600e+03 -8.5714e+01 4.3800e+00 --1.8600e+03 -5.7143e+01 4.3752e+00 --1.8600e+03 -2.8571e+01 4.3723e+00 --1.8600e+03 0.0000e+00 4.3713e+00 --1.8600e+03 2.8571e+01 4.3723e+00 --1.8600e+03 5.7143e+01 4.3752e+00 --1.8600e+03 8.5714e+01 4.3800e+00 --1.8600e+03 1.1429e+02 4.3867e+00 --1.8600e+03 1.4286e+02 4.3954e+00 --1.8600e+03 1.7143e+02 4.4059e+00 --1.8600e+03 2.0000e+02 4.4183e+00 --1.8600e+03 2.2857e+02 4.4325e+00 --1.8600e+03 2.5714e+02 4.4484e+00 --1.8600e+03 2.8571e+02 4.4661e+00 --1.8600e+03 3.1429e+02 4.4855e+00 --1.8600e+03 3.4286e+02 4.5065e+00 --1.8600e+03 3.7143e+02 4.5291e+00 --1.8600e+03 4.0000e+02 4.5532e+00 --1.8600e+03 4.2857e+02 4.5788e+00 --1.8600e+03 4.5714e+02 4.6058e+00 --1.8600e+03 4.8571e+02 4.6342e+00 --1.8600e+03 5.1429e+02 4.6638e+00 --1.8600e+03 5.4286e+02 4.6947e+00 --1.8600e+03 5.7143e+02 4.7267e+00 --1.8600e+03 6.0000e+02 4.7598e+00 --1.8600e+03 6.2857e+02 4.7939e+00 --1.8600e+03 6.5714e+02 4.8290e+00 --1.8600e+03 6.8571e+02 4.8650e+00 --1.8600e+03 7.1429e+02 4.9017e+00 --1.8600e+03 7.4286e+02 4.9393e+00 --1.8600e+03 7.7143e+02 4.9775e+00 --1.8600e+03 8.0000e+02 5.0164e+00 --1.8600e+03 8.2857e+02 5.0558e+00 --1.8600e+03 8.5714e+02 5.0958e+00 --1.8600e+03 8.8571e+02 5.1361e+00 --1.8600e+03 9.1429e+02 5.1769e+00 --1.8600e+03 9.4286e+02 5.2180e+00 --1.8600e+03 9.7143e+02 5.2595e+00 --1.8600e+03 1.0000e+03 5.3011e+00 --1.8600e+03 1.0286e+03 5.3429e+00 --1.8600e+03 1.0571e+03 5.3849e+00 --1.8600e+03 1.0857e+03 5.4270e+00 --1.8600e+03 1.1143e+03 5.4691e+00 --1.8600e+03 1.1429e+03 5.5112e+00 --1.8600e+03 1.1714e+03 5.5533e+00 --1.8600e+03 1.2000e+03 5.5954e+00 --1.8600e+03 1.2286e+03 5.6373e+00 --1.8600e+03 1.2571e+03 5.6791e+00 --1.8600e+03 1.2857e+03 5.7208e+00 --1.8600e+03 1.3143e+03 5.7623e+00 --1.8600e+03 1.3429e+03 5.8036e+00 --1.8600e+03 1.3714e+03 5.8447e+00 --1.8600e+03 1.4000e+03 5.8855e+00 --1.8600e+03 1.4286e+03 5.9260e+00 --1.8600e+03 1.4571e+03 5.9663e+00 --1.8600e+03 1.4857e+03 6.0062e+00 --1.8600e+03 1.5143e+03 6.0459e+00 --1.8600e+03 1.5429e+03 6.0851e+00 --1.8600e+03 1.5714e+03 6.1241e+00 --1.8600e+03 1.6000e+03 6.1626e+00 --1.8600e+03 1.6286e+03 6.2008e+00 --1.8600e+03 1.6571e+03 6.2386e+00 --1.8600e+03 1.6857e+03 6.2761e+00 --1.8600e+03 1.7143e+03 6.3131e+00 --1.8600e+03 1.7429e+03 6.3497e+00 --1.8600e+03 1.7714e+03 6.3860e+00 --1.8600e+03 1.8000e+03 6.4218e+00 --1.8600e+03 1.8286e+03 6.4572e+00 --1.8600e+03 1.8571e+03 6.4921e+00 --1.8600e+03 1.8857e+03 6.5267e+00 --1.8600e+03 1.9143e+03 6.5608e+00 --1.8600e+03 1.9429e+03 6.5945e+00 --1.8600e+03 1.9714e+03 6.6278e+00 --1.8600e+03 2.0000e+03 6.6607e+00 --1.8300e+03 -2.0000e+03 6.6287e+00 --1.8300e+03 -1.9714e+03 6.5949e+00 --1.8300e+03 -1.9429e+03 6.5607e+00 --1.8300e+03 -1.9143e+03 6.5260e+00 --1.8300e+03 -1.8857e+03 6.4909e+00 --1.8300e+03 -1.8571e+03 6.4553e+00 --1.8300e+03 -1.8286e+03 6.4193e+00 --1.8300e+03 -1.8000e+03 6.3829e+00 --1.8300e+03 -1.7714e+03 6.3460e+00 --1.8300e+03 -1.7429e+03 6.3086e+00 --1.8300e+03 -1.7143e+03 6.2708e+00 --1.8300e+03 -1.6857e+03 6.2326e+00 --1.8300e+03 -1.6571e+03 6.1939e+00 --1.8300e+03 -1.6286e+03 6.1549e+00 --1.8300e+03 -1.6000e+03 6.1154e+00 --1.8300e+03 -1.5714e+03 6.0755e+00 --1.8300e+03 -1.5429e+03 6.0352e+00 --1.8300e+03 -1.5143e+03 5.9945e+00 --1.8300e+03 -1.4857e+03 5.9534e+00 --1.8300e+03 -1.4571e+03 5.9120e+00 --1.8300e+03 -1.4286e+03 5.8702e+00 --1.8300e+03 -1.4000e+03 5.8281e+00 --1.8300e+03 -1.3714e+03 5.7857e+00 --1.8300e+03 -1.3429e+03 5.7430e+00 --1.8300e+03 -1.3143e+03 5.7001e+00 --1.8300e+03 -1.2857e+03 5.6569e+00 --1.8300e+03 -1.2571e+03 5.6135e+00 --1.8300e+03 -1.2286e+03 5.5698e+00 --1.8300e+03 -1.2000e+03 5.5261e+00 --1.8300e+03 -1.1714e+03 5.4822e+00 --1.8300e+03 -1.1429e+03 5.4382e+00 --1.8300e+03 -1.1143e+03 5.3942e+00 --1.8300e+03 -1.0857e+03 5.3501e+00 --1.8300e+03 -1.0571e+03 5.3061e+00 --1.8300e+03 -1.0286e+03 5.2621e+00 --1.8300e+03 -1.0000e+03 5.2183e+00 --1.8300e+03 -9.7143e+02 5.1746e+00 --1.8300e+03 -9.4286e+02 5.1312e+00 --1.8300e+03 -9.1429e+02 5.0880e+00 --1.8300e+03 -8.8571e+02 5.0451e+00 --1.8300e+03 -8.5714e+02 5.0026e+00 --1.8300e+03 -8.2857e+02 4.9606e+00 --1.8300e+03 -8.0000e+02 4.9191e+00 --1.8300e+03 -7.7143e+02 4.8781e+00 --1.8300e+03 -7.4286e+02 4.8378e+00 --1.8300e+03 -7.1429e+02 4.7981e+00 --1.8300e+03 -6.8571e+02 4.7593e+00 --1.8300e+03 -6.5714e+02 4.7213e+00 --1.8300e+03 -6.2857e+02 4.6842e+00 --1.8300e+03 -6.0000e+02 4.6481e+00 --1.8300e+03 -5.7143e+02 4.6130e+00 --1.8300e+03 -5.4286e+02 4.5791e+00 --1.8300e+03 -5.1429e+02 4.5464e+00 --1.8300e+03 -4.8571e+02 4.5150e+00 --1.8300e+03 -4.5714e+02 4.4849e+00 --1.8300e+03 -4.2857e+02 4.4562e+00 --1.8300e+03 -4.0000e+02 4.4291e+00 --1.8300e+03 -3.7143e+02 4.4034e+00 --1.8300e+03 -3.4286e+02 4.3794e+00 --1.8300e+03 -3.1429e+02 4.3571e+00 --1.8300e+03 -2.8571e+02 4.3365e+00 --1.8300e+03 -2.5714e+02 4.3176e+00 --1.8300e+03 -2.2857e+02 4.3007e+00 --1.8300e+03 -2.0000e+02 4.2856e+00 --1.8300e+03 -1.7143e+02 4.2724e+00 --1.8300e+03 -1.4286e+02 4.2612e+00 --1.8300e+03 -1.1429e+02 4.2520e+00 --1.8300e+03 -8.5714e+01 4.2448e+00 --1.8300e+03 -5.7143e+01 4.2396e+00 --1.8300e+03 -2.8571e+01 4.2365e+00 --1.8300e+03 0.0000e+00 4.2355e+00 --1.8300e+03 2.8571e+01 4.2365e+00 --1.8300e+03 5.7143e+01 4.2396e+00 --1.8300e+03 8.5714e+01 4.2448e+00 --1.8300e+03 1.1429e+02 4.2520e+00 --1.8300e+03 1.4286e+02 4.2612e+00 --1.8300e+03 1.7143e+02 4.2724e+00 --1.8300e+03 2.0000e+02 4.2856e+00 --1.8300e+03 2.2857e+02 4.3007e+00 --1.8300e+03 2.5714e+02 4.3176e+00 --1.8300e+03 2.8571e+02 4.3365e+00 --1.8300e+03 3.1429e+02 4.3571e+00 --1.8300e+03 3.4286e+02 4.3794e+00 --1.8300e+03 3.7143e+02 4.4034e+00 --1.8300e+03 4.0000e+02 4.4291e+00 --1.8300e+03 4.2857e+02 4.4562e+00 --1.8300e+03 4.5714e+02 4.4849e+00 --1.8300e+03 4.8571e+02 4.5150e+00 --1.8300e+03 5.1429e+02 4.5464e+00 --1.8300e+03 5.4286e+02 4.5791e+00 --1.8300e+03 5.7143e+02 4.6130e+00 --1.8300e+03 6.0000e+02 4.6481e+00 --1.8300e+03 6.2857e+02 4.6842e+00 --1.8300e+03 6.5714e+02 4.7213e+00 --1.8300e+03 6.8571e+02 4.7593e+00 --1.8300e+03 7.1429e+02 4.7981e+00 --1.8300e+03 7.4286e+02 4.8378e+00 --1.8300e+03 7.7143e+02 4.8781e+00 --1.8300e+03 8.0000e+02 4.9191e+00 --1.8300e+03 8.2857e+02 4.9606e+00 --1.8300e+03 8.5714e+02 5.0026e+00 --1.8300e+03 8.8571e+02 5.0451e+00 --1.8300e+03 9.1429e+02 5.0880e+00 --1.8300e+03 9.4286e+02 5.1312e+00 --1.8300e+03 9.7143e+02 5.1746e+00 --1.8300e+03 1.0000e+03 5.2183e+00 --1.8300e+03 1.0286e+03 5.2621e+00 --1.8300e+03 1.0571e+03 5.3061e+00 --1.8300e+03 1.0857e+03 5.3501e+00 --1.8300e+03 1.1143e+03 5.3942e+00 --1.8300e+03 1.1429e+03 5.4382e+00 --1.8300e+03 1.1714e+03 5.4822e+00 --1.8300e+03 1.2000e+03 5.5261e+00 --1.8300e+03 1.2286e+03 5.5698e+00 --1.8300e+03 1.2571e+03 5.6135e+00 --1.8300e+03 1.2857e+03 5.6569e+00 --1.8300e+03 1.3143e+03 5.7001e+00 --1.8300e+03 1.3429e+03 5.7430e+00 --1.8300e+03 1.3714e+03 5.7857e+00 --1.8300e+03 1.4000e+03 5.8281e+00 --1.8300e+03 1.4286e+03 5.8702e+00 --1.8300e+03 1.4571e+03 5.9120e+00 --1.8300e+03 1.4857e+03 5.9534e+00 --1.8300e+03 1.5143e+03 5.9945e+00 --1.8300e+03 1.5429e+03 6.0352e+00 --1.8300e+03 1.5714e+03 6.0755e+00 --1.8300e+03 1.6000e+03 6.1154e+00 --1.8300e+03 1.6286e+03 6.1549e+00 --1.8300e+03 1.6571e+03 6.1939e+00 --1.8300e+03 1.6857e+03 6.2326e+00 --1.8300e+03 1.7143e+03 6.2708e+00 --1.8300e+03 1.7429e+03 6.3086e+00 --1.8300e+03 1.7714e+03 6.3460e+00 --1.8300e+03 1.8000e+03 6.3829e+00 --1.8300e+03 1.8286e+03 6.4193e+00 --1.8300e+03 1.8571e+03 6.4553e+00 --1.8300e+03 1.8857e+03 6.4909e+00 --1.8300e+03 1.9143e+03 6.5260e+00 --1.8300e+03 1.9429e+03 6.5607e+00 --1.8300e+03 1.9714e+03 6.5949e+00 --1.8300e+03 2.0000e+03 6.6287e+00 --1.8000e+03 -2.0000e+03 6.5963e+00 --1.8000e+03 -1.9714e+03 6.5616e+00 --1.8000e+03 -1.9429e+03 6.5264e+00 --1.8000e+03 -1.9143e+03 6.4908e+00 --1.8000e+03 -1.8857e+03 6.4547e+00 --1.8000e+03 -1.8571e+03 6.4181e+00 --1.8000e+03 -1.8286e+03 6.3810e+00 --1.8000e+03 -1.8000e+03 6.3434e+00 --1.8000e+03 -1.7714e+03 6.3054e+00 --1.8000e+03 -1.7429e+03 6.2669e+00 --1.8000e+03 -1.7143e+03 6.2279e+00 --1.8000e+03 -1.6857e+03 6.1884e+00 --1.8000e+03 -1.6571e+03 6.1485e+00 --1.8000e+03 -1.6286e+03 6.1081e+00 --1.8000e+03 -1.6000e+03 6.0673e+00 --1.8000e+03 -1.5714e+03 6.0260e+00 --1.8000e+03 -1.5429e+03 5.9843e+00 --1.8000e+03 -1.5143e+03 5.9421e+00 --1.8000e+03 -1.4857e+03 5.8995e+00 --1.8000e+03 -1.4571e+03 5.8566e+00 --1.8000e+03 -1.4286e+03 5.8132e+00 --1.8000e+03 -1.4000e+03 5.7695e+00 --1.8000e+03 -1.3714e+03 5.7254e+00 --1.8000e+03 -1.3429e+03 5.6810e+00 --1.8000e+03 -1.3143e+03 5.6363e+00 --1.8000e+03 -1.2857e+03 5.5913e+00 --1.8000e+03 -1.2571e+03 5.5461e+00 --1.8000e+03 -1.2286e+03 5.5006e+00 --1.8000e+03 -1.2000e+03 5.4550e+00 --1.8000e+03 -1.1714e+03 5.4091e+00 --1.8000e+03 -1.1429e+03 5.3632e+00 --1.8000e+03 -1.1143e+03 5.3171e+00 --1.8000e+03 -1.0857e+03 5.2710e+00 --1.8000e+03 -1.0571e+03 5.2249e+00 --1.8000e+03 -1.0286e+03 5.1788e+00 --1.8000e+03 -1.0000e+03 5.1329e+00 --1.8000e+03 -9.7143e+02 5.0870e+00 --1.8000e+03 -9.4286e+02 5.0414e+00 --1.8000e+03 -9.1429e+02 4.9960e+00 --1.8000e+03 -8.8571e+02 4.9509e+00 --1.8000e+03 -8.5714e+02 4.9062e+00 --1.8000e+03 -8.2857e+02 4.8619e+00 --1.8000e+03 -8.0000e+02 4.8181e+00 --1.8000e+03 -7.7143e+02 4.7749e+00 --1.8000e+03 -7.4286e+02 4.7324e+00 --1.8000e+03 -7.1429e+02 4.6905e+00 --1.8000e+03 -6.8571e+02 4.6494e+00 --1.8000e+03 -6.5714e+02 4.6092e+00 --1.8000e+03 -6.2857e+02 4.5700e+00 --1.8000e+03 -6.0000e+02 4.5318e+00 --1.8000e+03 -5.7143e+02 4.4946e+00 --1.8000e+03 -5.4286e+02 4.4587e+00 --1.8000e+03 -5.1429e+02 4.4240e+00 --1.8000e+03 -4.8571e+02 4.3906e+00 --1.8000e+03 -4.5714e+02 4.3586e+00 --1.8000e+03 -4.2857e+02 4.3282e+00 --1.8000e+03 -4.0000e+02 4.2993e+00 --1.8000e+03 -3.7143e+02 4.2720e+00 --1.8000e+03 -3.4286e+02 4.2464e+00 --1.8000e+03 -3.1429e+02 4.2226e+00 --1.8000e+03 -2.8571e+02 4.2007e+00 --1.8000e+03 -2.5714e+02 4.1807e+00 --1.8000e+03 -2.2857e+02 4.1626e+00 --1.8000e+03 -2.0000e+02 4.1465e+00 --1.8000e+03 -1.7143e+02 4.1324e+00 --1.8000e+03 -1.4286e+02 4.1205e+00 --1.8000e+03 -1.1429e+02 4.1106e+00 --1.8000e+03 -8.5714e+01 4.1030e+00 --1.8000e+03 -5.7143e+01 4.0975e+00 --1.8000e+03 -2.8571e+01 4.0942e+00 --1.8000e+03 0.0000e+00 4.0931e+00 --1.8000e+03 2.8571e+01 4.0942e+00 --1.8000e+03 5.7143e+01 4.0975e+00 --1.8000e+03 8.5714e+01 4.1030e+00 --1.8000e+03 1.1429e+02 4.1106e+00 --1.8000e+03 1.4286e+02 4.1205e+00 --1.8000e+03 1.7143e+02 4.1324e+00 --1.8000e+03 2.0000e+02 4.1465e+00 --1.8000e+03 2.2857e+02 4.1626e+00 --1.8000e+03 2.5714e+02 4.1807e+00 --1.8000e+03 2.8571e+02 4.2007e+00 --1.8000e+03 3.1429e+02 4.2226e+00 --1.8000e+03 3.4286e+02 4.2464e+00 --1.8000e+03 3.7143e+02 4.2720e+00 --1.8000e+03 4.0000e+02 4.2993e+00 --1.8000e+03 4.2857e+02 4.3282e+00 --1.8000e+03 4.5714e+02 4.3586e+00 --1.8000e+03 4.8571e+02 4.3906e+00 --1.8000e+03 5.1429e+02 4.4240e+00 --1.8000e+03 5.4286e+02 4.4587e+00 --1.8000e+03 5.7143e+02 4.4946e+00 --1.8000e+03 6.0000e+02 4.5318e+00 --1.8000e+03 6.2857e+02 4.5700e+00 --1.8000e+03 6.5714e+02 4.6092e+00 --1.8000e+03 6.8571e+02 4.6494e+00 --1.8000e+03 7.1429e+02 4.6905e+00 --1.8000e+03 7.4286e+02 4.7324e+00 --1.8000e+03 7.7143e+02 4.7749e+00 --1.8000e+03 8.0000e+02 4.8181e+00 --1.8000e+03 8.2857e+02 4.8619e+00 --1.8000e+03 8.5714e+02 4.9062e+00 --1.8000e+03 8.8571e+02 4.9509e+00 --1.8000e+03 9.1429e+02 4.9960e+00 --1.8000e+03 9.4286e+02 5.0414e+00 --1.8000e+03 9.7143e+02 5.0870e+00 --1.8000e+03 1.0000e+03 5.1329e+00 --1.8000e+03 1.0286e+03 5.1788e+00 --1.8000e+03 1.0571e+03 5.2249e+00 --1.8000e+03 1.0857e+03 5.2710e+00 --1.8000e+03 1.1143e+03 5.3171e+00 --1.8000e+03 1.1429e+03 5.3632e+00 --1.8000e+03 1.1714e+03 5.4091e+00 --1.8000e+03 1.2000e+03 5.4550e+00 --1.8000e+03 1.2286e+03 5.5006e+00 --1.8000e+03 1.2571e+03 5.5461e+00 --1.8000e+03 1.2857e+03 5.5913e+00 --1.8000e+03 1.3143e+03 5.6363e+00 --1.8000e+03 1.3429e+03 5.6810e+00 --1.8000e+03 1.3714e+03 5.7254e+00 --1.8000e+03 1.4000e+03 5.7695e+00 --1.8000e+03 1.4286e+03 5.8132e+00 --1.8000e+03 1.4571e+03 5.8566e+00 --1.8000e+03 1.4857e+03 5.8995e+00 --1.8000e+03 1.5143e+03 5.9421e+00 --1.8000e+03 1.5429e+03 5.9843e+00 --1.8000e+03 1.5714e+03 6.0260e+00 --1.8000e+03 1.6000e+03 6.0673e+00 --1.8000e+03 1.6286e+03 6.1081e+00 --1.8000e+03 1.6571e+03 6.1485e+00 --1.8000e+03 1.6857e+03 6.1884e+00 --1.8000e+03 1.7143e+03 6.2279e+00 --1.8000e+03 1.7429e+03 6.2669e+00 --1.8000e+03 1.7714e+03 6.3054e+00 --1.8000e+03 1.8000e+03 6.3434e+00 --1.8000e+03 1.8286e+03 6.3810e+00 --1.8000e+03 1.8571e+03 6.4181e+00 --1.8000e+03 1.8857e+03 6.4547e+00 --1.8000e+03 1.9143e+03 6.4908e+00 --1.8000e+03 1.9429e+03 6.5264e+00 --1.8000e+03 1.9714e+03 6.5616e+00 --1.8000e+03 2.0000e+03 6.5963e+00 --1.7700e+03 -2.0000e+03 6.5636e+00 --1.7700e+03 -1.9714e+03 6.5279e+00 --1.7700e+03 -1.9429e+03 6.4918e+00 --1.7700e+03 -1.9143e+03 6.4552e+00 --1.7700e+03 -1.8857e+03 6.4180e+00 --1.7700e+03 -1.8571e+03 6.3804e+00 --1.7700e+03 -1.8286e+03 6.3422e+00 --1.7700e+03 -1.8000e+03 6.3035e+00 --1.7700e+03 -1.7714e+03 6.2643e+00 --1.7700e+03 -1.7429e+03 6.2246e+00 --1.7700e+03 -1.7143e+03 6.1843e+00 --1.7700e+03 -1.6857e+03 6.1436e+00 --1.7700e+03 -1.6571e+03 6.1023e+00 --1.7700e+03 -1.6286e+03 6.0606e+00 --1.7700e+03 -1.6000e+03 6.0183e+00 --1.7700e+03 -1.5714e+03 5.9756e+00 --1.7700e+03 -1.5429e+03 5.9324e+00 --1.7700e+03 -1.5143e+03 5.8887e+00 --1.7700e+03 -1.4857e+03 5.8446e+00 --1.7700e+03 -1.4571e+03 5.8000e+00 --1.7700e+03 -1.4286e+03 5.7550e+00 --1.7700e+03 -1.4000e+03 5.7096e+00 --1.7700e+03 -1.3714e+03 5.6638e+00 --1.7700e+03 -1.3429e+03 5.6176e+00 --1.7700e+03 -1.3143e+03 5.5711e+00 --1.7700e+03 -1.2857e+03 5.5242e+00 --1.7700e+03 -1.2571e+03 5.4770e+00 --1.7700e+03 -1.2286e+03 5.4296e+00 --1.7700e+03 -1.2000e+03 5.3819e+00 --1.7700e+03 -1.1714e+03 5.3340e+00 --1.7700e+03 -1.1429e+03 5.2860e+00 --1.7700e+03 -1.1143e+03 5.2378e+00 --1.7700e+03 -1.0857e+03 5.1895e+00 --1.7700e+03 -1.0571e+03 5.1412e+00 --1.7700e+03 -1.0286e+03 5.0929e+00 --1.7700e+03 -1.0000e+03 5.0447e+00 --1.7700e+03 -9.7143e+02 4.9966e+00 --1.7700e+03 -9.4286e+02 4.9486e+00 --1.7700e+03 -9.1429e+02 4.9008e+00 --1.7700e+03 -8.8571e+02 4.8534e+00 --1.7700e+03 -8.5714e+02 4.8063e+00 --1.7700e+03 -8.2857e+02 4.7596e+00 --1.7700e+03 -8.0000e+02 4.7134e+00 --1.7700e+03 -7.7143e+02 4.6678e+00 --1.7700e+03 -7.4286e+02 4.6229e+00 --1.7700e+03 -7.1429e+02 4.5786e+00 --1.7700e+03 -6.8571e+02 4.5352e+00 --1.7700e+03 -6.5714e+02 4.4926e+00 --1.7700e+03 -6.2857e+02 4.4510e+00 --1.7700e+03 -6.0000e+02 4.4105e+00 --1.7700e+03 -5.7143e+02 4.3711e+00 --1.7700e+03 -5.4286e+02 4.3329e+00 --1.7700e+03 -5.1429e+02 4.2960e+00 --1.7700e+03 -4.8571e+02 4.2606e+00 --1.7700e+03 -4.5714e+02 4.2266e+00 --1.7700e+03 -4.2857e+02 4.1942e+00 --1.7700e+03 -4.0000e+02 4.1634e+00 --1.7700e+03 -3.7143e+02 4.1344e+00 --1.7700e+03 -3.4286e+02 4.1071e+00 --1.7700e+03 -3.1429e+02 4.0818e+00 --1.7700e+03 -2.8571e+02 4.0584e+00 --1.7700e+03 -2.5714e+02 4.0370e+00 --1.7700e+03 -2.2857e+02 4.0177e+00 --1.7700e+03 -2.0000e+02 4.0005e+00 --1.7700e+03 -1.7143e+02 3.9855e+00 --1.7700e+03 -1.4286e+02 3.9727e+00 --1.7700e+03 -1.1429e+02 3.9622e+00 --1.7700e+03 -8.5714e+01 3.9540e+00 --1.7700e+03 -5.7143e+01 3.9481e+00 --1.7700e+03 -2.8571e+01 3.9446e+00 --1.7700e+03 0.0000e+00 3.9434e+00 --1.7700e+03 2.8571e+01 3.9446e+00 --1.7700e+03 5.7143e+01 3.9481e+00 --1.7700e+03 8.5714e+01 3.9540e+00 --1.7700e+03 1.1429e+02 3.9622e+00 --1.7700e+03 1.4286e+02 3.9727e+00 --1.7700e+03 1.7143e+02 3.9855e+00 --1.7700e+03 2.0000e+02 4.0005e+00 --1.7700e+03 2.2857e+02 4.0177e+00 --1.7700e+03 2.5714e+02 4.0370e+00 --1.7700e+03 2.8571e+02 4.0584e+00 --1.7700e+03 3.1429e+02 4.0818e+00 --1.7700e+03 3.4286e+02 4.1071e+00 --1.7700e+03 3.7143e+02 4.1344e+00 --1.7700e+03 4.0000e+02 4.1634e+00 --1.7700e+03 4.2857e+02 4.1942e+00 --1.7700e+03 4.5714e+02 4.2266e+00 --1.7700e+03 4.8571e+02 4.2606e+00 --1.7700e+03 5.1429e+02 4.2960e+00 --1.7700e+03 5.4286e+02 4.3329e+00 --1.7700e+03 5.7143e+02 4.3711e+00 --1.7700e+03 6.0000e+02 4.4105e+00 --1.7700e+03 6.2857e+02 4.4510e+00 --1.7700e+03 6.5714e+02 4.4926e+00 --1.7700e+03 6.8571e+02 4.5352e+00 --1.7700e+03 7.1429e+02 4.5786e+00 --1.7700e+03 7.4286e+02 4.6229e+00 --1.7700e+03 7.7143e+02 4.6678e+00 --1.7700e+03 8.0000e+02 4.7134e+00 --1.7700e+03 8.2857e+02 4.7596e+00 --1.7700e+03 8.5714e+02 4.8063e+00 --1.7700e+03 8.8571e+02 4.8534e+00 --1.7700e+03 9.1429e+02 4.9008e+00 --1.7700e+03 9.4286e+02 4.9486e+00 --1.7700e+03 9.7143e+02 4.9966e+00 --1.7700e+03 1.0000e+03 5.0447e+00 --1.7700e+03 1.0286e+03 5.0929e+00 --1.7700e+03 1.0571e+03 5.1412e+00 --1.7700e+03 1.0857e+03 5.1895e+00 --1.7700e+03 1.1143e+03 5.2378e+00 --1.7700e+03 1.1429e+03 5.2860e+00 --1.7700e+03 1.1714e+03 5.3340e+00 --1.7700e+03 1.2000e+03 5.3819e+00 --1.7700e+03 1.2286e+03 5.4296e+00 --1.7700e+03 1.2571e+03 5.4770e+00 --1.7700e+03 1.2857e+03 5.5242e+00 --1.7700e+03 1.3143e+03 5.5711e+00 --1.7700e+03 1.3429e+03 5.6176e+00 --1.7700e+03 1.3714e+03 5.6638e+00 --1.7700e+03 1.4000e+03 5.7096e+00 --1.7700e+03 1.4286e+03 5.7550e+00 --1.7700e+03 1.4571e+03 5.8000e+00 --1.7700e+03 1.4857e+03 5.8446e+00 --1.7700e+03 1.5143e+03 5.8887e+00 --1.7700e+03 1.5429e+03 5.9324e+00 --1.7700e+03 1.5714e+03 5.9756e+00 --1.7700e+03 1.6000e+03 6.0183e+00 --1.7700e+03 1.6286e+03 6.0606e+00 --1.7700e+03 1.6571e+03 6.1023e+00 --1.7700e+03 1.6857e+03 6.1436e+00 --1.7700e+03 1.7143e+03 6.1843e+00 --1.7700e+03 1.7429e+03 6.2246e+00 --1.7700e+03 1.7714e+03 6.2643e+00 --1.7700e+03 1.8000e+03 6.3035e+00 --1.7700e+03 1.8286e+03 6.3422e+00 --1.7700e+03 1.8571e+03 6.3804e+00 --1.7700e+03 1.8857e+03 6.4180e+00 --1.7700e+03 1.9143e+03 6.4552e+00 --1.7700e+03 1.9429e+03 6.4918e+00 --1.7700e+03 1.9714e+03 6.5279e+00 --1.7700e+03 2.0000e+03 6.5636e+00 --1.7400e+03 -2.0000e+03 6.5305e+00 --1.7400e+03 -1.9714e+03 6.4940e+00 --1.7400e+03 -1.9429e+03 6.4568e+00 --1.7400e+03 -1.9143e+03 6.4192e+00 --1.7400e+03 -1.8857e+03 6.3810e+00 --1.7400e+03 -1.8571e+03 6.3422e+00 --1.7400e+03 -1.8286e+03 6.3029e+00 --1.7400e+03 -1.8000e+03 6.2630e+00 --1.7400e+03 -1.7714e+03 6.2226e+00 --1.7400e+03 -1.7429e+03 6.1816e+00 --1.7400e+03 -1.7143e+03 6.1401e+00 --1.7400e+03 -1.6857e+03 6.0980e+00 --1.7400e+03 -1.6571e+03 6.0554e+00 --1.7400e+03 -1.6286e+03 6.0123e+00 --1.7400e+03 -1.6000e+03 5.9686e+00 --1.7400e+03 -1.5714e+03 5.9243e+00 --1.7400e+03 -1.5429e+03 5.8796e+00 --1.7400e+03 -1.5143e+03 5.8343e+00 --1.7400e+03 -1.4857e+03 5.7886e+00 --1.7400e+03 -1.4571e+03 5.7423e+00 --1.7400e+03 -1.4286e+03 5.6956e+00 --1.7400e+03 -1.4000e+03 5.6484e+00 --1.7400e+03 -1.3714e+03 5.6008e+00 --1.7400e+03 -1.3429e+03 5.5527e+00 --1.7400e+03 -1.3143e+03 5.5042e+00 --1.7400e+03 -1.2857e+03 5.4554e+00 --1.7400e+03 -1.2571e+03 5.4062e+00 --1.7400e+03 -1.2286e+03 5.3567e+00 --1.7400e+03 -1.2000e+03 5.3069e+00 --1.7400e+03 -1.1714e+03 5.2569e+00 --1.7400e+03 -1.1429e+03 5.2066e+00 --1.7400e+03 -1.1143e+03 5.1562e+00 --1.7400e+03 -1.0857e+03 5.1056e+00 --1.7400e+03 -1.0571e+03 5.0550e+00 --1.7400e+03 -1.0286e+03 5.0043e+00 --1.7400e+03 -1.0000e+03 4.9537e+00 --1.7400e+03 -9.7143e+02 4.9031e+00 --1.7400e+03 -9.4286e+02 4.8526e+00 --1.7400e+03 -9.1429e+02 4.8024e+00 --1.7400e+03 -8.8571e+02 4.7524e+00 --1.7400e+03 -8.5714e+02 4.7028e+00 --1.7400e+03 -8.2857e+02 4.6535e+00 --1.7400e+03 -8.0000e+02 4.6048e+00 --1.7400e+03 -7.7143e+02 4.5566e+00 --1.7400e+03 -7.4286e+02 4.5090e+00 --1.7400e+03 -7.1429e+02 4.4622e+00 --1.7400e+03 -6.8571e+02 4.4162e+00 --1.7400e+03 -6.5714e+02 4.3711e+00 --1.7400e+03 -6.2857e+02 4.3270e+00 --1.7400e+03 -6.0000e+02 4.2840e+00 --1.7400e+03 -5.7143e+02 4.2421e+00 --1.7400e+03 -5.4286e+02 4.2016e+00 --1.7400e+03 -5.1429e+02 4.1624e+00 --1.7400e+03 -4.8571e+02 4.1246e+00 --1.7400e+03 -4.5714e+02 4.0884e+00 --1.7400e+03 -4.2857e+02 4.0539e+00 --1.7400e+03 -4.0000e+02 4.0211e+00 --1.7400e+03 -3.7143e+02 3.9901e+00 --1.7400e+03 -3.4286e+02 3.9611e+00 --1.7400e+03 -3.1429e+02 3.9340e+00 --1.7400e+03 -2.8571e+02 3.9090e+00 --1.7400e+03 -2.5714e+02 3.8861e+00 --1.7400e+03 -2.2857e+02 3.8655e+00 --1.7400e+03 -2.0000e+02 3.8471e+00 --1.7400e+03 -1.7143e+02 3.8311e+00 --1.7400e+03 -1.4286e+02 3.8174e+00 --1.7400e+03 -1.1429e+02 3.8062e+00 --1.7400e+03 -8.5714e+01 3.7974e+00 --1.7400e+03 -5.7143e+01 3.7911e+00 --1.7400e+03 -2.8571e+01 3.7873e+00 --1.7400e+03 0.0000e+00 3.7861e+00 --1.7400e+03 2.8571e+01 3.7873e+00 --1.7400e+03 5.7143e+01 3.7911e+00 --1.7400e+03 8.5714e+01 3.7974e+00 --1.7400e+03 1.1429e+02 3.8062e+00 --1.7400e+03 1.4286e+02 3.8174e+00 --1.7400e+03 1.7143e+02 3.8311e+00 --1.7400e+03 2.0000e+02 3.8471e+00 --1.7400e+03 2.2857e+02 3.8655e+00 --1.7400e+03 2.5714e+02 3.8861e+00 --1.7400e+03 2.8571e+02 3.9090e+00 --1.7400e+03 3.1429e+02 3.9340e+00 --1.7400e+03 3.4286e+02 3.9611e+00 --1.7400e+03 3.7143e+02 3.9901e+00 --1.7400e+03 4.0000e+02 4.0211e+00 --1.7400e+03 4.2857e+02 4.0539e+00 --1.7400e+03 4.5714e+02 4.0884e+00 --1.7400e+03 4.8571e+02 4.1246e+00 --1.7400e+03 5.1429e+02 4.1624e+00 --1.7400e+03 5.4286e+02 4.2016e+00 --1.7400e+03 5.7143e+02 4.2421e+00 --1.7400e+03 6.0000e+02 4.2840e+00 --1.7400e+03 6.2857e+02 4.3270e+00 --1.7400e+03 6.5714e+02 4.3711e+00 --1.7400e+03 6.8571e+02 4.4162e+00 --1.7400e+03 7.1429e+02 4.4622e+00 --1.7400e+03 7.4286e+02 4.5090e+00 --1.7400e+03 7.7143e+02 4.5566e+00 --1.7400e+03 8.0000e+02 4.6048e+00 --1.7400e+03 8.2857e+02 4.6535e+00 --1.7400e+03 8.5714e+02 4.7028e+00 --1.7400e+03 8.8571e+02 4.7524e+00 --1.7400e+03 9.1429e+02 4.8024e+00 --1.7400e+03 9.4286e+02 4.8526e+00 --1.7400e+03 9.7143e+02 4.9031e+00 --1.7400e+03 1.0000e+03 4.9537e+00 --1.7400e+03 1.0286e+03 5.0043e+00 --1.7400e+03 1.0571e+03 5.0550e+00 --1.7400e+03 1.0857e+03 5.1056e+00 --1.7400e+03 1.1143e+03 5.1562e+00 --1.7400e+03 1.1429e+03 5.2066e+00 --1.7400e+03 1.1714e+03 5.2569e+00 --1.7400e+03 1.2000e+03 5.3069e+00 --1.7400e+03 1.2286e+03 5.3567e+00 --1.7400e+03 1.2571e+03 5.4062e+00 --1.7400e+03 1.2857e+03 5.4554e+00 --1.7400e+03 1.3143e+03 5.5042e+00 --1.7400e+03 1.3429e+03 5.5527e+00 --1.7400e+03 1.3714e+03 5.6008e+00 --1.7400e+03 1.4000e+03 5.6484e+00 --1.7400e+03 1.4286e+03 5.6956e+00 --1.7400e+03 1.4571e+03 5.7423e+00 --1.7400e+03 1.4857e+03 5.7886e+00 --1.7400e+03 1.5143e+03 5.8343e+00 --1.7400e+03 1.5429e+03 5.8796e+00 --1.7400e+03 1.5714e+03 5.9243e+00 --1.7400e+03 1.6000e+03 5.9686e+00 --1.7400e+03 1.6286e+03 6.0123e+00 --1.7400e+03 1.6571e+03 6.0554e+00 --1.7400e+03 1.6857e+03 6.0980e+00 --1.7400e+03 1.7143e+03 6.1401e+00 --1.7400e+03 1.7429e+03 6.1816e+00 --1.7400e+03 1.7714e+03 6.2226e+00 --1.7400e+03 1.8000e+03 6.2630e+00 --1.7400e+03 1.8286e+03 6.3029e+00 --1.7400e+03 1.8571e+03 6.3422e+00 --1.7400e+03 1.8857e+03 6.3810e+00 --1.7400e+03 1.9143e+03 6.4192e+00 --1.7400e+03 1.9429e+03 6.4568e+00 --1.7400e+03 1.9714e+03 6.4940e+00 --1.7400e+03 2.0000e+03 6.5305e+00 --1.7100e+03 -2.0000e+03 6.4972e+00 --1.7100e+03 -1.9714e+03 6.4596e+00 --1.7100e+03 -1.9429e+03 6.4215e+00 --1.7100e+03 -1.9143e+03 6.3828e+00 --1.7100e+03 -1.8857e+03 6.3435e+00 --1.7100e+03 -1.8571e+03 6.3036e+00 --1.7100e+03 -1.8286e+03 6.2631e+00 --1.7100e+03 -1.8000e+03 6.2220e+00 --1.7100e+03 -1.7714e+03 6.1803e+00 --1.7100e+03 -1.7429e+03 6.1381e+00 --1.7100e+03 -1.7143e+03 6.0952e+00 --1.7100e+03 -1.6857e+03 6.0518e+00 --1.7100e+03 -1.6571e+03 6.0078e+00 --1.7100e+03 -1.6286e+03 5.9631e+00 --1.7100e+03 -1.6000e+03 5.9179e+00 --1.7100e+03 -1.5714e+03 5.8722e+00 --1.7100e+03 -1.5429e+03 5.8258e+00 --1.7100e+03 -1.5143e+03 5.7789e+00 --1.7100e+03 -1.4857e+03 5.7314e+00 --1.7100e+03 -1.4571e+03 5.6834e+00 --1.7100e+03 -1.4286e+03 5.6349e+00 --1.7100e+03 -1.4000e+03 5.5858e+00 --1.7100e+03 -1.3714e+03 5.5363e+00 --1.7100e+03 -1.3429e+03 5.4863e+00 --1.7100e+03 -1.3143e+03 5.4358e+00 --1.7100e+03 -1.2857e+03 5.3849e+00 --1.7100e+03 -1.2571e+03 5.3336e+00 --1.7100e+03 -1.2286e+03 5.2819e+00 --1.7100e+03 -1.2000e+03 5.2299e+00 --1.7100e+03 -1.1714e+03 5.1776e+00 --1.7100e+03 -1.1429e+03 5.1250e+00 --1.7100e+03 -1.1143e+03 5.0722e+00 --1.7100e+03 -1.0857e+03 5.0192e+00 --1.7100e+03 -1.0571e+03 4.9661e+00 --1.7100e+03 -1.0286e+03 4.9129e+00 --1.7100e+03 -1.0000e+03 4.8596e+00 --1.7100e+03 -9.7143e+02 4.8065e+00 --1.7100e+03 -9.4286e+02 4.7534e+00 --1.7100e+03 -9.1429e+02 4.7005e+00 --1.7100e+03 -8.8571e+02 4.6478e+00 --1.7100e+03 -8.5714e+02 4.5954e+00 --1.7100e+03 -8.2857e+02 4.5434e+00 --1.7100e+03 -8.0000e+02 4.4919e+00 --1.7100e+03 -7.7143e+02 4.4409e+00 --1.7100e+03 -7.4286e+02 4.3906e+00 --1.7100e+03 -7.1429e+02 4.3410e+00 --1.7100e+03 -6.8571e+02 4.2922e+00 --1.7100e+03 -6.5714e+02 4.2444e+00 --1.7100e+03 -6.2857e+02 4.1976e+00 --1.7100e+03 -6.0000e+02 4.1519e+00 --1.7100e+03 -5.7143e+02 4.1074e+00 --1.7100e+03 -5.4286e+02 4.0642e+00 --1.7100e+03 -5.1429e+02 4.0225e+00 --1.7100e+03 -4.8571e+02 3.9823e+00 --1.7100e+03 -4.5714e+02 3.9437e+00 --1.7100e+03 -4.2857e+02 3.9068e+00 --1.7100e+03 -4.0000e+02 3.8718e+00 --1.7100e+03 -3.7143e+02 3.8388e+00 --1.7100e+03 -3.4286e+02 3.8077e+00 --1.7100e+03 -3.1429e+02 3.7788e+00 --1.7100e+03 -2.8571e+02 3.7520e+00 --1.7100e+03 -2.5714e+02 3.7276e+00 --1.7100e+03 -2.2857e+02 3.7055e+00 --1.7100e+03 -2.0000e+02 3.6858e+00 --1.7100e+03 -1.7143e+02 3.6686e+00 --1.7100e+03 -1.4286e+02 3.6540e+00 --1.7100e+03 -1.1429e+02 3.6419e+00 --1.7100e+03 -8.5714e+01 3.6325e+00 --1.7100e+03 -5.7143e+01 3.6258e+00 --1.7100e+03 -2.8571e+01 3.6217e+00 --1.7100e+03 0.0000e+00 3.6204e+00 --1.7100e+03 2.8571e+01 3.6217e+00 --1.7100e+03 5.7143e+01 3.6258e+00 --1.7100e+03 8.5714e+01 3.6325e+00 --1.7100e+03 1.1429e+02 3.6419e+00 --1.7100e+03 1.4286e+02 3.6540e+00 --1.7100e+03 1.7143e+02 3.6686e+00 --1.7100e+03 2.0000e+02 3.6858e+00 --1.7100e+03 2.2857e+02 3.7055e+00 --1.7100e+03 2.5714e+02 3.7276e+00 --1.7100e+03 2.8571e+02 3.7520e+00 --1.7100e+03 3.1429e+02 3.7788e+00 --1.7100e+03 3.4286e+02 3.8077e+00 --1.7100e+03 3.7143e+02 3.8388e+00 --1.7100e+03 4.0000e+02 3.8718e+00 --1.7100e+03 4.2857e+02 3.9068e+00 --1.7100e+03 4.5714e+02 3.9437e+00 --1.7100e+03 4.8571e+02 3.9823e+00 --1.7100e+03 5.1429e+02 4.0225e+00 --1.7100e+03 5.4286e+02 4.0642e+00 --1.7100e+03 5.7143e+02 4.1074e+00 --1.7100e+03 6.0000e+02 4.1519e+00 --1.7100e+03 6.2857e+02 4.1976e+00 --1.7100e+03 6.5714e+02 4.2444e+00 --1.7100e+03 6.8571e+02 4.2922e+00 --1.7100e+03 7.1429e+02 4.3410e+00 --1.7100e+03 7.4286e+02 4.3906e+00 --1.7100e+03 7.7143e+02 4.4409e+00 --1.7100e+03 8.0000e+02 4.4919e+00 --1.7100e+03 8.2857e+02 4.5434e+00 --1.7100e+03 8.5714e+02 4.5954e+00 --1.7100e+03 8.8571e+02 4.6478e+00 --1.7100e+03 9.1429e+02 4.7005e+00 --1.7100e+03 9.4286e+02 4.7534e+00 --1.7100e+03 9.7143e+02 4.8065e+00 --1.7100e+03 1.0000e+03 4.8596e+00 --1.7100e+03 1.0286e+03 4.9129e+00 --1.7100e+03 1.0571e+03 4.9661e+00 --1.7100e+03 1.0857e+03 5.0192e+00 --1.7100e+03 1.1143e+03 5.0722e+00 --1.7100e+03 1.1429e+03 5.1250e+00 --1.7100e+03 1.1714e+03 5.1776e+00 --1.7100e+03 1.2000e+03 5.2299e+00 --1.7100e+03 1.2286e+03 5.2819e+00 --1.7100e+03 1.2571e+03 5.3336e+00 --1.7100e+03 1.2857e+03 5.3849e+00 --1.7100e+03 1.3143e+03 5.4358e+00 --1.7100e+03 1.3429e+03 5.4863e+00 --1.7100e+03 1.3714e+03 5.5363e+00 --1.7100e+03 1.4000e+03 5.5858e+00 --1.7100e+03 1.4286e+03 5.6349e+00 --1.7100e+03 1.4571e+03 5.6834e+00 --1.7100e+03 1.4857e+03 5.7314e+00 --1.7100e+03 1.5143e+03 5.7789e+00 --1.7100e+03 1.5429e+03 5.8258e+00 --1.7100e+03 1.5714e+03 5.8722e+00 --1.7100e+03 1.6000e+03 5.9179e+00 --1.7100e+03 1.6286e+03 5.9631e+00 --1.7100e+03 1.6571e+03 6.0078e+00 --1.7100e+03 1.6857e+03 6.0518e+00 --1.7100e+03 1.7143e+03 6.0952e+00 --1.7100e+03 1.7429e+03 6.1381e+00 --1.7100e+03 1.7714e+03 6.1803e+00 --1.7100e+03 1.8000e+03 6.2220e+00 --1.7100e+03 1.8286e+03 6.2631e+00 --1.7100e+03 1.8571e+03 6.3036e+00 --1.7100e+03 1.8857e+03 6.3435e+00 --1.7100e+03 1.9143e+03 6.3828e+00 --1.7100e+03 1.9429e+03 6.4215e+00 --1.7100e+03 1.9714e+03 6.4596e+00 --1.7100e+03 2.0000e+03 6.4972e+00 --1.6800e+03 -2.0000e+03 6.4636e+00 --1.6800e+03 -1.9714e+03 6.4250e+00 --1.6800e+03 -1.9429e+03 6.3858e+00 --1.6800e+03 -1.9143e+03 6.3460e+00 --1.6800e+03 -1.8857e+03 6.3055e+00 --1.6800e+03 -1.8571e+03 6.2645e+00 --1.6800e+03 -1.8286e+03 6.2228e+00 --1.6800e+03 -1.8000e+03 6.1805e+00 --1.6800e+03 -1.7714e+03 6.1375e+00 --1.6800e+03 -1.7429e+03 6.0940e+00 --1.6800e+03 -1.7143e+03 6.0497e+00 --1.6800e+03 -1.6857e+03 6.0049e+00 --1.6800e+03 -1.6571e+03 5.9594e+00 --1.6800e+03 -1.6286e+03 5.9132e+00 --1.6800e+03 -1.6000e+03 5.8665e+00 --1.6800e+03 -1.5714e+03 5.8191e+00 --1.6800e+03 -1.5429e+03 5.7710e+00 --1.6800e+03 -1.5143e+03 5.7224e+00 --1.6800e+03 -1.4857e+03 5.6732e+00 --1.6800e+03 -1.4571e+03 5.6233e+00 --1.6800e+03 -1.4286e+03 5.5729e+00 --1.6800e+03 -1.4000e+03 5.5219e+00 --1.6800e+03 -1.3714e+03 5.4704e+00 --1.6800e+03 -1.3429e+03 5.4183e+00 --1.6800e+03 -1.3143e+03 5.3657e+00 --1.6800e+03 -1.2857e+03 5.3126e+00 --1.6800e+03 -1.2571e+03 5.2591e+00 --1.6800e+03 -1.2286e+03 5.2051e+00 --1.6800e+03 -1.2000e+03 5.1508e+00 --1.6800e+03 -1.1714e+03 5.0960e+00 --1.6800e+03 -1.1429e+03 5.0410e+00 --1.6800e+03 -1.1143e+03 4.9857e+00 --1.6800e+03 -1.0857e+03 4.9301e+00 --1.6800e+03 -1.0571e+03 4.8744e+00 --1.6800e+03 -1.0286e+03 4.8185e+00 --1.6800e+03 -1.0000e+03 4.7625e+00 --1.6800e+03 -9.7143e+02 4.7066e+00 --1.6800e+03 -9.4286e+02 4.6507e+00 --1.6800e+03 -9.1429e+02 4.5949e+00 --1.6800e+03 -8.8571e+02 4.5393e+00 --1.6800e+03 -8.5714e+02 4.4840e+00 --1.6800e+03 -8.2857e+02 4.4291e+00 --1.6800e+03 -8.0000e+02 4.3746e+00 --1.6800e+03 -7.7143e+02 4.3207e+00 --1.6800e+03 -7.4286e+02 4.2674e+00 --1.6800e+03 -7.1429e+02 4.2148e+00 --1.6800e+03 -6.8571e+02 4.1630e+00 --1.6800e+03 -6.5714e+02 4.1122e+00 --1.6800e+03 -6.2857e+02 4.0624e+00 --1.6800e+03 -6.0000e+02 4.0138e+00 --1.6800e+03 -5.7143e+02 3.9665e+00 --1.6800e+03 -5.4286e+02 3.9205e+00 --1.6800e+03 -5.1429e+02 3.8760e+00 --1.6800e+03 -4.8571e+02 3.8331e+00 --1.6800e+03 -4.5714e+02 3.7919e+00 --1.6800e+03 -4.2857e+02 3.7526e+00 --1.6800e+03 -4.0000e+02 3.7151e+00 --1.6800e+03 -3.7143e+02 3.6798e+00 --1.6800e+03 -3.4286e+02 3.6465e+00 --1.6800e+03 -3.1429e+02 3.6155e+00 --1.6800e+03 -2.8571e+02 3.5869e+00 --1.6800e+03 -2.5714e+02 3.5607e+00 --1.6800e+03 -2.2857e+02 3.5370e+00 --1.6800e+03 -2.0000e+02 3.5159e+00 --1.6800e+03 -1.7143e+02 3.4975e+00 --1.6800e+03 -1.4286e+02 3.4818e+00 --1.6800e+03 -1.1429e+02 3.4688e+00 --1.6800e+03 -8.5714e+01 3.4587e+00 --1.6800e+03 -5.7143e+01 3.4515e+00 --1.6800e+03 -2.8571e+01 3.4471e+00 --1.6800e+03 0.0000e+00 3.4457e+00 --1.6800e+03 2.8571e+01 3.4471e+00 --1.6800e+03 5.7143e+01 3.4515e+00 --1.6800e+03 8.5714e+01 3.4587e+00 --1.6800e+03 1.1429e+02 3.4688e+00 --1.6800e+03 1.4286e+02 3.4818e+00 --1.6800e+03 1.7143e+02 3.4975e+00 --1.6800e+03 2.0000e+02 3.5159e+00 --1.6800e+03 2.2857e+02 3.5370e+00 --1.6800e+03 2.5714e+02 3.5607e+00 --1.6800e+03 2.8571e+02 3.5869e+00 --1.6800e+03 3.1429e+02 3.6155e+00 --1.6800e+03 3.4286e+02 3.6465e+00 --1.6800e+03 3.7143e+02 3.6798e+00 --1.6800e+03 4.0000e+02 3.7151e+00 --1.6800e+03 4.2857e+02 3.7526e+00 --1.6800e+03 4.5714e+02 3.7919e+00 --1.6800e+03 4.8571e+02 3.8331e+00 --1.6800e+03 5.1429e+02 3.8760e+00 --1.6800e+03 5.4286e+02 3.9205e+00 --1.6800e+03 5.7143e+02 3.9665e+00 --1.6800e+03 6.0000e+02 4.0138e+00 --1.6800e+03 6.2857e+02 4.0624e+00 --1.6800e+03 6.5714e+02 4.1122e+00 --1.6800e+03 6.8571e+02 4.1630e+00 --1.6800e+03 7.1429e+02 4.2148e+00 --1.6800e+03 7.4286e+02 4.2674e+00 --1.6800e+03 7.7143e+02 4.3207e+00 --1.6800e+03 8.0000e+02 4.3746e+00 --1.6800e+03 8.2857e+02 4.4291e+00 --1.6800e+03 8.5714e+02 4.4840e+00 --1.6800e+03 8.8571e+02 4.5393e+00 --1.6800e+03 9.1429e+02 4.5949e+00 --1.6800e+03 9.4286e+02 4.6507e+00 --1.6800e+03 9.7143e+02 4.7066e+00 --1.6800e+03 1.0000e+03 4.7625e+00 --1.6800e+03 1.0286e+03 4.8185e+00 --1.6800e+03 1.0571e+03 4.8744e+00 --1.6800e+03 1.0857e+03 4.9301e+00 --1.6800e+03 1.1143e+03 4.9857e+00 --1.6800e+03 1.1429e+03 5.0410e+00 --1.6800e+03 1.1714e+03 5.0960e+00 --1.6800e+03 1.2000e+03 5.1508e+00 --1.6800e+03 1.2286e+03 5.2051e+00 --1.6800e+03 1.2571e+03 5.2591e+00 --1.6800e+03 1.2857e+03 5.3126e+00 --1.6800e+03 1.3143e+03 5.3657e+00 --1.6800e+03 1.3429e+03 5.4183e+00 --1.6800e+03 1.3714e+03 5.4704e+00 --1.6800e+03 1.4000e+03 5.5219e+00 --1.6800e+03 1.4286e+03 5.5729e+00 --1.6800e+03 1.4571e+03 5.6233e+00 --1.6800e+03 1.4857e+03 5.6732e+00 --1.6800e+03 1.5143e+03 5.7224e+00 --1.6800e+03 1.5429e+03 5.7710e+00 --1.6800e+03 1.5714e+03 5.8191e+00 --1.6800e+03 1.6000e+03 5.8665e+00 --1.6800e+03 1.6286e+03 5.9132e+00 --1.6800e+03 1.6571e+03 5.9594e+00 --1.6800e+03 1.6857e+03 6.0049e+00 --1.6800e+03 1.7143e+03 6.0497e+00 --1.6800e+03 1.7429e+03 6.0940e+00 --1.6800e+03 1.7714e+03 6.1375e+00 --1.6800e+03 1.8000e+03 6.1805e+00 --1.6800e+03 1.8286e+03 6.2228e+00 --1.6800e+03 1.8571e+03 6.2645e+00 --1.6800e+03 1.8857e+03 6.3055e+00 --1.6800e+03 1.9143e+03 6.3460e+00 --1.6800e+03 1.9429e+03 6.3858e+00 --1.6800e+03 1.9714e+03 6.4250e+00 --1.6800e+03 2.0000e+03 6.4636e+00 --1.6500e+03 -2.0000e+03 6.4297e+00 --1.6500e+03 -1.9714e+03 6.3900e+00 --1.6500e+03 -1.9429e+03 6.3498e+00 --1.6500e+03 -1.9143e+03 6.3088e+00 --1.6500e+03 -1.8857e+03 6.2672e+00 --1.6500e+03 -1.8571e+03 6.2250e+00 --1.6500e+03 -1.8286e+03 6.1821e+00 --1.6500e+03 -1.8000e+03 6.1385e+00 --1.6500e+03 -1.7714e+03 6.0942e+00 --1.6500e+03 -1.7429e+03 6.0492e+00 --1.6500e+03 -1.7143e+03 6.0036e+00 --1.6500e+03 -1.6857e+03 5.9572e+00 --1.6500e+03 -1.6571e+03 5.9102e+00 --1.6500e+03 -1.6286e+03 5.8625e+00 --1.6500e+03 -1.6000e+03 5.8141e+00 --1.6500e+03 -1.5714e+03 5.7651e+00 --1.6500e+03 -1.5429e+03 5.7153e+00 --1.6500e+03 -1.5143e+03 5.6649e+00 --1.6500e+03 -1.4857e+03 5.6138e+00 --1.6500e+03 -1.4571e+03 5.5620e+00 --1.6500e+03 -1.4286e+03 5.5096e+00 --1.6500e+03 -1.4000e+03 5.4566e+00 --1.6500e+03 -1.3714e+03 5.4030e+00 --1.6500e+03 -1.3429e+03 5.3488e+00 --1.6500e+03 -1.3143e+03 5.2940e+00 --1.6500e+03 -1.2857e+03 5.2386e+00 --1.6500e+03 -1.2571e+03 5.1827e+00 --1.6500e+03 -1.2286e+03 5.1263e+00 --1.6500e+03 -1.2000e+03 5.0695e+00 --1.6500e+03 -1.1714e+03 5.0122e+00 --1.6500e+03 -1.1429e+03 4.9546e+00 --1.6500e+03 -1.1143e+03 4.8966e+00 --1.6500e+03 -1.0857e+03 4.8383e+00 --1.6500e+03 -1.0571e+03 4.7797e+00 --1.6500e+03 -1.0286e+03 4.7210e+00 --1.6500e+03 -1.0000e+03 4.6622e+00 --1.6500e+03 -9.7143e+02 4.6032e+00 --1.6500e+03 -9.4286e+02 4.5443e+00 --1.6500e+03 -9.1429e+02 4.4855e+00 --1.6500e+03 -8.8571e+02 4.4268e+00 --1.6500e+03 -8.5714e+02 4.3684e+00 --1.6500e+03 -8.2857e+02 4.3103e+00 --1.6500e+03 -8.0000e+02 4.2527e+00 --1.6500e+03 -7.7143e+02 4.1955e+00 --1.6500e+03 -7.4286e+02 4.1390e+00 --1.6500e+03 -7.1429e+02 4.0832e+00 --1.6500e+03 -6.8571e+02 4.0282e+00 --1.6500e+03 -6.5714e+02 3.9742e+00 --1.6500e+03 -6.2857e+02 3.9212e+00 --1.6500e+03 -6.0000e+02 3.8695e+00 --1.6500e+03 -5.7143e+02 3.8190e+00 --1.6500e+03 -5.4286e+02 3.7699e+00 --1.6500e+03 -5.1429e+02 3.7224e+00 --1.6500e+03 -4.8571e+02 3.6766e+00 --1.6500e+03 -4.5714e+02 3.6326e+00 --1.6500e+03 -4.2857e+02 3.5905e+00 --1.6500e+03 -4.0000e+02 3.5504e+00 --1.6500e+03 -3.7143e+02 3.5125e+00 --1.6500e+03 -3.4286e+02 3.4769e+00 --1.6500e+03 -3.1429e+02 3.4437e+00 --1.6500e+03 -2.8571e+02 3.4130e+00 --1.6500e+03 -2.5714e+02 3.3848e+00 --1.6500e+03 -2.2857e+02 3.3594e+00 --1.6500e+03 -2.0000e+02 3.3367e+00 --1.6500e+03 -1.7143e+02 3.3169e+00 --1.6500e+03 -1.4286e+02 3.3000e+00 --1.6500e+03 -1.1429e+02 3.2861e+00 --1.6500e+03 -8.5714e+01 3.2753e+00 --1.6500e+03 -5.7143e+01 3.2675e+00 --1.6500e+03 -2.8571e+01 3.2628e+00 --1.6500e+03 0.0000e+00 3.2612e+00 --1.6500e+03 2.8571e+01 3.2628e+00 --1.6500e+03 5.7143e+01 3.2675e+00 --1.6500e+03 8.5714e+01 3.2753e+00 --1.6500e+03 1.1429e+02 3.2861e+00 --1.6500e+03 1.4286e+02 3.3000e+00 --1.6500e+03 1.7143e+02 3.3169e+00 --1.6500e+03 2.0000e+02 3.3367e+00 --1.6500e+03 2.2857e+02 3.3594e+00 --1.6500e+03 2.5714e+02 3.3848e+00 --1.6500e+03 2.8571e+02 3.4130e+00 --1.6500e+03 3.1429e+02 3.4437e+00 --1.6500e+03 3.4286e+02 3.4769e+00 --1.6500e+03 3.7143e+02 3.5125e+00 --1.6500e+03 4.0000e+02 3.5504e+00 --1.6500e+03 4.2857e+02 3.5905e+00 --1.6500e+03 4.5714e+02 3.6326e+00 --1.6500e+03 4.8571e+02 3.6766e+00 --1.6500e+03 5.1429e+02 3.7224e+00 --1.6500e+03 5.4286e+02 3.7699e+00 --1.6500e+03 5.7143e+02 3.8190e+00 --1.6500e+03 6.0000e+02 3.8695e+00 --1.6500e+03 6.2857e+02 3.9212e+00 --1.6500e+03 6.5714e+02 3.9742e+00 --1.6500e+03 6.8571e+02 4.0282e+00 --1.6500e+03 7.1429e+02 4.0832e+00 --1.6500e+03 7.4286e+02 4.1390e+00 --1.6500e+03 7.7143e+02 4.1955e+00 --1.6500e+03 8.0000e+02 4.2527e+00 --1.6500e+03 8.2857e+02 4.3103e+00 --1.6500e+03 8.5714e+02 4.3684e+00 --1.6500e+03 8.8571e+02 4.4268e+00 --1.6500e+03 9.1429e+02 4.4855e+00 --1.6500e+03 9.4286e+02 4.5443e+00 --1.6500e+03 9.7143e+02 4.6032e+00 --1.6500e+03 1.0000e+03 4.6622e+00 --1.6500e+03 1.0286e+03 4.7210e+00 --1.6500e+03 1.0571e+03 4.7797e+00 --1.6500e+03 1.0857e+03 4.8383e+00 --1.6500e+03 1.1143e+03 4.8966e+00 --1.6500e+03 1.1429e+03 4.9546e+00 --1.6500e+03 1.1714e+03 5.0122e+00 --1.6500e+03 1.2000e+03 5.0695e+00 --1.6500e+03 1.2286e+03 5.1263e+00 --1.6500e+03 1.2571e+03 5.1827e+00 --1.6500e+03 1.2857e+03 5.2386e+00 --1.6500e+03 1.3143e+03 5.2940e+00 --1.6500e+03 1.3429e+03 5.3488e+00 --1.6500e+03 1.3714e+03 5.4030e+00 --1.6500e+03 1.4000e+03 5.4566e+00 --1.6500e+03 1.4286e+03 5.5096e+00 --1.6500e+03 1.4571e+03 5.5620e+00 --1.6500e+03 1.4857e+03 5.6138e+00 --1.6500e+03 1.5143e+03 5.6649e+00 --1.6500e+03 1.5429e+03 5.7153e+00 --1.6500e+03 1.5714e+03 5.7651e+00 --1.6500e+03 1.6000e+03 5.8141e+00 --1.6500e+03 1.6286e+03 5.8625e+00 --1.6500e+03 1.6571e+03 5.9102e+00 --1.6500e+03 1.6857e+03 5.9572e+00 --1.6500e+03 1.7143e+03 6.0036e+00 --1.6500e+03 1.7429e+03 6.0492e+00 --1.6500e+03 1.7714e+03 6.0942e+00 --1.6500e+03 1.8000e+03 6.1385e+00 --1.6500e+03 1.8286e+03 6.1821e+00 --1.6500e+03 1.8571e+03 6.2250e+00 --1.6500e+03 1.8857e+03 6.2672e+00 --1.6500e+03 1.9143e+03 6.3088e+00 --1.6500e+03 1.9429e+03 6.3498e+00 --1.6500e+03 1.9714e+03 6.3900e+00 --1.6500e+03 2.0000e+03 6.4297e+00 --1.6200e+03 -2.0000e+03 6.3955e+00 --1.6200e+03 -1.9714e+03 6.3548e+00 --1.6200e+03 -1.9429e+03 6.3134e+00 --1.6200e+03 -1.9143e+03 6.2713e+00 --1.6200e+03 -1.8857e+03 6.2285e+00 --1.6200e+03 -1.8571e+03 6.1850e+00 --1.6200e+03 -1.8286e+03 6.1408e+00 --1.6200e+03 -1.8000e+03 6.0959e+00 --1.6200e+03 -1.7714e+03 6.0503e+00 --1.6200e+03 -1.7429e+03 6.0039e+00 --1.6200e+03 -1.7143e+03 5.9568e+00 --1.6200e+03 -1.6857e+03 5.9089e+00 --1.6200e+03 -1.6571e+03 5.8603e+00 --1.6200e+03 -1.6286e+03 5.8110e+00 --1.6200e+03 -1.6000e+03 5.7609e+00 --1.6200e+03 -1.5714e+03 5.7101e+00 --1.6200e+03 -1.5429e+03 5.6586e+00 --1.6200e+03 -1.5143e+03 5.6063e+00 --1.6200e+03 -1.4857e+03 5.5532e+00 --1.6200e+03 -1.4571e+03 5.4995e+00 --1.6200e+03 -1.4286e+03 5.4451e+00 --1.6200e+03 -1.4000e+03 5.3899e+00 --1.6200e+03 -1.3714e+03 5.3341e+00 --1.6200e+03 -1.3429e+03 5.2776e+00 --1.6200e+03 -1.3143e+03 5.2205e+00 --1.6200e+03 -1.2857e+03 5.1627e+00 --1.6200e+03 -1.2571e+03 5.1044e+00 --1.6200e+03 -1.2286e+03 5.0454e+00 --1.6200e+03 -1.2000e+03 4.9860e+00 --1.6200e+03 -1.1714e+03 4.9260e+00 --1.6200e+03 -1.1429e+03 4.8656e+00 --1.6200e+03 -1.1143e+03 4.8048e+00 --1.6200e+03 -1.0857e+03 4.7436e+00 --1.6200e+03 -1.0571e+03 4.6821e+00 --1.6200e+03 -1.0286e+03 4.6204e+00 --1.6200e+03 -1.0000e+03 4.5584e+00 --1.6200e+03 -9.7143e+02 4.4963e+00 --1.6200e+03 -9.4286e+02 4.4342e+00 --1.6200e+03 -9.1429e+02 4.3721e+00 --1.6200e+03 -8.8571e+02 4.3102e+00 --1.6200e+03 -8.5714e+02 4.2484e+00 --1.6200e+03 -8.2857e+02 4.1869e+00 --1.6200e+03 -8.0000e+02 4.1258e+00 --1.6200e+03 -7.7143e+02 4.0652e+00 --1.6200e+03 -7.4286e+02 4.0052e+00 --1.6200e+03 -7.1429e+02 3.9459e+00 --1.6200e+03 -6.8571e+02 3.8874e+00 --1.6200e+03 -6.5714e+02 3.8299e+00 --1.6200e+03 -6.2857e+02 3.7735e+00 --1.6200e+03 -6.0000e+02 3.7183e+00 --1.6200e+03 -5.7143e+02 3.6645e+00 --1.6200e+03 -5.4286e+02 3.6121e+00 --1.6200e+03 -5.1429e+02 3.5613e+00 --1.6200e+03 -4.8571e+02 3.5123e+00 --1.6200e+03 -4.5714e+02 3.4652e+00 --1.6200e+03 -4.2857e+02 3.4201e+00 --1.6200e+03 -4.0000e+02 3.3771e+00 --1.6200e+03 -3.7143e+02 3.3365e+00 --1.6200e+03 -3.4286e+02 3.2982e+00 --1.6200e+03 -3.1429e+02 3.2625e+00 --1.6200e+03 -2.8571e+02 3.2295e+00 --1.6200e+03 -2.5714e+02 3.1992e+00 --1.6200e+03 -2.2857e+02 3.1719e+00 --1.6200e+03 -2.0000e+02 3.1475e+00 --1.6200e+03 -1.7143e+02 3.1261e+00 --1.6200e+03 -1.4286e+02 3.1080e+00 --1.6200e+03 -1.1429e+02 3.0930e+00 --1.6200e+03 -8.5714e+01 3.0813e+00 --1.6200e+03 -5.7143e+01 3.0729e+00 --1.6200e+03 -2.8571e+01 3.0679e+00 --1.6200e+03 0.0000e+00 3.0662e+00 --1.6200e+03 2.8571e+01 3.0679e+00 --1.6200e+03 5.7143e+01 3.0729e+00 --1.6200e+03 8.5714e+01 3.0813e+00 --1.6200e+03 1.1429e+02 3.0930e+00 --1.6200e+03 1.4286e+02 3.1080e+00 --1.6200e+03 1.7143e+02 3.1261e+00 --1.6200e+03 2.0000e+02 3.1475e+00 --1.6200e+03 2.2857e+02 3.1719e+00 --1.6200e+03 2.5714e+02 3.1992e+00 --1.6200e+03 2.8571e+02 3.2295e+00 --1.6200e+03 3.1429e+02 3.2625e+00 --1.6200e+03 3.4286e+02 3.2982e+00 --1.6200e+03 3.7143e+02 3.3365e+00 --1.6200e+03 4.0000e+02 3.3771e+00 --1.6200e+03 4.2857e+02 3.4201e+00 --1.6200e+03 4.5714e+02 3.4652e+00 --1.6200e+03 4.8571e+02 3.5123e+00 --1.6200e+03 5.1429e+02 3.5613e+00 --1.6200e+03 5.4286e+02 3.6121e+00 --1.6200e+03 5.7143e+02 3.6645e+00 --1.6200e+03 6.0000e+02 3.7183e+00 --1.6200e+03 6.2857e+02 3.7735e+00 --1.6200e+03 6.5714e+02 3.8299e+00 --1.6200e+03 6.8571e+02 3.8874e+00 --1.6200e+03 7.1429e+02 3.9459e+00 --1.6200e+03 7.4286e+02 4.0052e+00 --1.6200e+03 7.7143e+02 4.0652e+00 --1.6200e+03 8.0000e+02 4.1258e+00 --1.6200e+03 8.2857e+02 4.1869e+00 --1.6200e+03 8.5714e+02 4.2484e+00 --1.6200e+03 8.8571e+02 4.3102e+00 --1.6200e+03 9.1429e+02 4.3721e+00 --1.6200e+03 9.4286e+02 4.4342e+00 --1.6200e+03 9.7143e+02 4.4963e+00 --1.6200e+03 1.0000e+03 4.5584e+00 --1.6200e+03 1.0286e+03 4.6204e+00 --1.6200e+03 1.0571e+03 4.6821e+00 --1.6200e+03 1.0857e+03 4.7436e+00 --1.6200e+03 1.1143e+03 4.8048e+00 --1.6200e+03 1.1429e+03 4.8656e+00 --1.6200e+03 1.1714e+03 4.9260e+00 --1.6200e+03 1.2000e+03 4.9860e+00 --1.6200e+03 1.2286e+03 5.0454e+00 --1.6200e+03 1.2571e+03 5.1044e+00 --1.6200e+03 1.2857e+03 5.1627e+00 --1.6200e+03 1.3143e+03 5.2205e+00 --1.6200e+03 1.3429e+03 5.2776e+00 --1.6200e+03 1.3714e+03 5.3341e+00 --1.6200e+03 1.4000e+03 5.3899e+00 --1.6200e+03 1.4286e+03 5.4451e+00 --1.6200e+03 1.4571e+03 5.4995e+00 --1.6200e+03 1.4857e+03 5.5532e+00 --1.6200e+03 1.5143e+03 5.6063e+00 --1.6200e+03 1.5429e+03 5.6586e+00 --1.6200e+03 1.5714e+03 5.7101e+00 --1.6200e+03 1.6000e+03 5.7609e+00 --1.6200e+03 1.6286e+03 5.8110e+00 --1.6200e+03 1.6571e+03 5.8603e+00 --1.6200e+03 1.6857e+03 5.9089e+00 --1.6200e+03 1.7143e+03 5.9568e+00 --1.6200e+03 1.7429e+03 6.0039e+00 --1.6200e+03 1.7714e+03 6.0503e+00 --1.6200e+03 1.8000e+03 6.0959e+00 --1.6200e+03 1.8286e+03 6.1408e+00 --1.6200e+03 1.8571e+03 6.1850e+00 --1.6200e+03 1.8857e+03 6.2285e+00 --1.6200e+03 1.9143e+03 6.2713e+00 --1.6200e+03 1.9429e+03 6.3134e+00 --1.6200e+03 1.9714e+03 6.3548e+00 --1.6200e+03 2.0000e+03 6.3955e+00 --1.5900e+03 -2.0000e+03 6.3610e+00 --1.5900e+03 -1.9714e+03 6.3192e+00 --1.5900e+03 -1.9429e+03 6.2767e+00 --1.5900e+03 -1.9143e+03 6.2334e+00 --1.5900e+03 -1.8857e+03 6.1894e+00 --1.5900e+03 -1.8571e+03 6.1447e+00 --1.5900e+03 -1.8286e+03 6.0992e+00 --1.5900e+03 -1.8000e+03 6.0529e+00 --1.5900e+03 -1.7714e+03 6.0058e+00 --1.5900e+03 -1.7429e+03 5.9580e+00 --1.5900e+03 -1.7143e+03 5.9093e+00 --1.5900e+03 -1.6857e+03 5.8599e+00 --1.5900e+03 -1.6571e+03 5.8097e+00 --1.5900e+03 -1.6286e+03 5.7587e+00 --1.5900e+03 -1.6000e+03 5.7069e+00 --1.5900e+03 -1.5714e+03 5.6542e+00 --1.5900e+03 -1.5429e+03 5.6008e+00 --1.5900e+03 -1.5143e+03 5.5466e+00 --1.5900e+03 -1.4857e+03 5.4916e+00 --1.5900e+03 -1.4571e+03 5.4357e+00 --1.5900e+03 -1.4286e+03 5.3791e+00 --1.5900e+03 -1.4000e+03 5.3218e+00 --1.5900e+03 -1.3714e+03 5.2637e+00 --1.5900e+03 -1.3429e+03 5.2048e+00 --1.5900e+03 -1.3143e+03 5.1452e+00 --1.5900e+03 -1.2857e+03 5.0850e+00 --1.5900e+03 -1.2571e+03 5.0240e+00 --1.5900e+03 -1.2286e+03 4.9624e+00 --1.5900e+03 -1.2000e+03 4.9002e+00 --1.5900e+03 -1.1714e+03 4.8374e+00 --1.5900e+03 -1.1429e+03 4.7741e+00 --1.5900e+03 -1.1143e+03 4.7103e+00 --1.5900e+03 -1.0857e+03 4.6460e+00 --1.5900e+03 -1.0571e+03 4.5813e+00 --1.5900e+03 -1.0286e+03 4.5164e+00 --1.5900e+03 -1.0000e+03 4.4511e+00 --1.5900e+03 -9.7143e+02 4.3857e+00 --1.5900e+03 -9.4286e+02 4.3201e+00 --1.5900e+03 -9.1429e+02 4.2546e+00 --1.5900e+03 -8.8571e+02 4.1890e+00 --1.5900e+03 -8.5714e+02 4.1236e+00 --1.5900e+03 -8.2857e+02 4.0585e+00 --1.5900e+03 -8.0000e+02 3.9937e+00 --1.5900e+03 -7.7143e+02 3.9294e+00 --1.5900e+03 -7.4286e+02 3.8656e+00 --1.5900e+03 -7.1429e+02 3.8026e+00 --1.5900e+03 -6.8571e+02 3.7403e+00 --1.5900e+03 -6.5714e+02 3.6791e+00 --1.5900e+03 -6.2857e+02 3.6189e+00 --1.5900e+03 -6.0000e+02 3.5600e+00 --1.5900e+03 -5.7143e+02 3.5024e+00 --1.5900e+03 -5.4286e+02 3.4464e+00 --1.5900e+03 -5.1429e+02 3.3920e+00 --1.5900e+03 -4.8571e+02 3.3395e+00 --1.5900e+03 -4.5714e+02 3.2890e+00 --1.5900e+03 -4.2857e+02 3.2406e+00 --1.5900e+03 -4.0000e+02 3.1945e+00 --1.5900e+03 -3.7143e+02 3.1508e+00 --1.5900e+03 -3.4286e+02 3.1097e+00 --1.5900e+03 -3.1429e+02 3.0713e+00 --1.5900e+03 -2.8571e+02 3.0357e+00 --1.5900e+03 -2.5714e+02 3.0031e+00 --1.5900e+03 -2.2857e+02 2.9736e+00 --1.5900e+03 -2.0000e+02 2.9473e+00 --1.5900e+03 -1.7143e+02 2.9243e+00 --1.5900e+03 -1.4286e+02 2.9047e+00 --1.5900e+03 -1.1429e+02 2.8885e+00 --1.5900e+03 -8.5714e+01 2.8759e+00 --1.5900e+03 -5.7143e+01 2.8668e+00 --1.5900e+03 -2.8571e+01 2.8614e+00 --1.5900e+03 0.0000e+00 2.8596e+00 --1.5900e+03 2.8571e+01 2.8614e+00 --1.5900e+03 5.7143e+01 2.8668e+00 --1.5900e+03 8.5714e+01 2.8759e+00 --1.5900e+03 1.1429e+02 2.8885e+00 --1.5900e+03 1.4286e+02 2.9047e+00 --1.5900e+03 1.7143e+02 2.9243e+00 --1.5900e+03 2.0000e+02 2.9473e+00 --1.5900e+03 2.2857e+02 2.9736e+00 --1.5900e+03 2.5714e+02 3.0031e+00 --1.5900e+03 2.8571e+02 3.0357e+00 --1.5900e+03 3.1429e+02 3.0713e+00 --1.5900e+03 3.4286e+02 3.1097e+00 --1.5900e+03 3.7143e+02 3.1508e+00 --1.5900e+03 4.0000e+02 3.1945e+00 --1.5900e+03 4.2857e+02 3.2406e+00 --1.5900e+03 4.5714e+02 3.2890e+00 --1.5900e+03 4.8571e+02 3.3395e+00 --1.5900e+03 5.1429e+02 3.3920e+00 --1.5900e+03 5.4286e+02 3.4464e+00 --1.5900e+03 5.7143e+02 3.5024e+00 --1.5900e+03 6.0000e+02 3.5600e+00 --1.5900e+03 6.2857e+02 3.6189e+00 --1.5900e+03 6.5714e+02 3.6791e+00 --1.5900e+03 6.8571e+02 3.7403e+00 --1.5900e+03 7.1429e+02 3.8026e+00 --1.5900e+03 7.4286e+02 3.8656e+00 --1.5900e+03 7.7143e+02 3.9294e+00 --1.5900e+03 8.0000e+02 3.9937e+00 --1.5900e+03 8.2857e+02 4.0585e+00 --1.5900e+03 8.5714e+02 4.1236e+00 --1.5900e+03 8.8571e+02 4.1890e+00 --1.5900e+03 9.1429e+02 4.2546e+00 --1.5900e+03 9.4286e+02 4.3201e+00 --1.5900e+03 9.7143e+02 4.3857e+00 --1.5900e+03 1.0000e+03 4.4511e+00 --1.5900e+03 1.0286e+03 4.5164e+00 --1.5900e+03 1.0571e+03 4.5813e+00 --1.5900e+03 1.0857e+03 4.6460e+00 --1.5900e+03 1.1143e+03 4.7103e+00 --1.5900e+03 1.1429e+03 4.7741e+00 --1.5900e+03 1.1714e+03 4.8374e+00 --1.5900e+03 1.2000e+03 4.9002e+00 --1.5900e+03 1.2286e+03 4.9624e+00 --1.5900e+03 1.2571e+03 5.0240e+00 --1.5900e+03 1.2857e+03 5.0850e+00 --1.5900e+03 1.3143e+03 5.1452e+00 --1.5900e+03 1.3429e+03 5.2048e+00 --1.5900e+03 1.3714e+03 5.2637e+00 --1.5900e+03 1.4000e+03 5.3218e+00 --1.5900e+03 1.4286e+03 5.3791e+00 --1.5900e+03 1.4571e+03 5.4357e+00 --1.5900e+03 1.4857e+03 5.4916e+00 --1.5900e+03 1.5143e+03 5.5466e+00 --1.5900e+03 1.5429e+03 5.6008e+00 --1.5900e+03 1.5714e+03 5.6542e+00 --1.5900e+03 1.6000e+03 5.7069e+00 --1.5900e+03 1.6286e+03 5.7587e+00 --1.5900e+03 1.6571e+03 5.8097e+00 --1.5900e+03 1.6857e+03 5.8599e+00 --1.5900e+03 1.7143e+03 5.9093e+00 --1.5900e+03 1.7429e+03 5.9580e+00 --1.5900e+03 1.7714e+03 6.0058e+00 --1.5900e+03 1.8000e+03 6.0529e+00 --1.5900e+03 1.8286e+03 6.0992e+00 --1.5900e+03 1.8571e+03 6.1447e+00 --1.5900e+03 1.8857e+03 6.1894e+00 --1.5900e+03 1.9143e+03 6.2334e+00 --1.5900e+03 1.9429e+03 6.2767e+00 --1.5900e+03 1.9714e+03 6.3192e+00 --1.5900e+03 2.0000e+03 6.3610e+00 --1.5600e+03 -2.0000e+03 6.3263e+00 --1.5600e+03 -1.9714e+03 6.2834e+00 --1.5600e+03 -1.9429e+03 6.2397e+00 --1.5600e+03 -1.9143e+03 6.1952e+00 --1.5600e+03 -1.8857e+03 6.1499e+00 --1.5600e+03 -1.8571e+03 6.1039e+00 --1.5600e+03 -1.8286e+03 6.0570e+00 --1.5600e+03 -1.8000e+03 6.0093e+00 --1.5600e+03 -1.7714e+03 5.9608e+00 --1.5600e+03 -1.7429e+03 5.9115e+00 --1.5600e+03 -1.7143e+03 5.8613e+00 --1.5600e+03 -1.6857e+03 5.8102e+00 --1.5600e+03 -1.6571e+03 5.7583e+00 --1.5600e+03 -1.6286e+03 5.7056e+00 --1.5600e+03 -1.6000e+03 5.6519e+00 --1.5600e+03 -1.5714e+03 5.5974e+00 --1.5600e+03 -1.5429e+03 5.5421e+00 --1.5600e+03 -1.5143e+03 5.4858e+00 --1.5600e+03 -1.4857e+03 5.4287e+00 --1.5600e+03 -1.4571e+03 5.3707e+00 --1.5600e+03 -1.4286e+03 5.3119e+00 --1.5600e+03 -1.4000e+03 5.2522e+00 --1.5600e+03 -1.3714e+03 5.1917e+00 --1.5600e+03 -1.3429e+03 5.1303e+00 --1.5600e+03 -1.3143e+03 5.0682e+00 --1.5600e+03 -1.2857e+03 5.0053e+00 --1.5600e+03 -1.2571e+03 4.9416e+00 --1.5600e+03 -1.2286e+03 4.8772e+00 --1.5600e+03 -1.2000e+03 4.8120e+00 --1.5600e+03 -1.1714e+03 4.7462e+00 --1.5600e+03 -1.1429e+03 4.6798e+00 --1.5600e+03 -1.1143e+03 4.6128e+00 --1.5600e+03 -1.0857e+03 4.5453e+00 --1.5600e+03 -1.0571e+03 4.4773e+00 --1.5600e+03 -1.0286e+03 4.4089e+00 --1.5600e+03 -1.0000e+03 4.3402e+00 --1.5600e+03 -9.7143e+02 4.2711e+00 --1.5600e+03 -9.4286e+02 4.2019e+00 --1.5600e+03 -9.1429e+02 4.1326e+00 --1.5600e+03 -8.8571e+02 4.0632e+00 --1.5600e+03 -8.5714e+02 3.9940e+00 --1.5600e+03 -8.2857e+02 3.9249e+00 --1.5600e+03 -8.0000e+02 3.8561e+00 --1.5600e+03 -7.7143e+02 3.7878e+00 --1.5600e+03 -7.4286e+02 3.7200e+00 --1.5600e+03 -7.1429e+02 3.6528e+00 --1.5600e+03 -6.8571e+02 3.5865e+00 --1.5600e+03 -6.5714e+02 3.5211e+00 --1.5600e+03 -6.2857e+02 3.4569e+00 --1.5600e+03 -6.0000e+02 3.3939e+00 --1.5600e+03 -5.7143e+02 3.3323e+00 --1.5600e+03 -5.4286e+02 3.2723e+00 --1.5600e+03 -5.1429e+02 3.2140e+00 --1.5600e+03 -4.8571e+02 3.1577e+00 --1.5600e+03 -4.5714e+02 3.1034e+00 --1.5600e+03 -4.2857e+02 3.0514e+00 --1.5600e+03 -4.0000e+02 3.0018e+00 --1.5600e+03 -3.7143e+02 2.9548e+00 --1.5600e+03 -3.4286e+02 2.9105e+00 --1.5600e+03 -3.1429e+02 2.8691e+00 --1.5600e+03 -2.8571e+02 2.8307e+00 --1.5600e+03 -2.5714e+02 2.7956e+00 --1.5600e+03 -2.2857e+02 2.7637e+00 --1.5600e+03 -2.0000e+02 2.7353e+00 --1.5600e+03 -1.7143e+02 2.7104e+00 --1.5600e+03 -1.4286e+02 2.6892e+00 --1.5600e+03 -1.1429e+02 2.6718e+00 --1.5600e+03 -8.5714e+01 2.6581e+00 --1.5600e+03 -5.7143e+01 2.6483e+00 --1.5600e+03 -2.8571e+01 2.6424e+00 --1.5600e+03 0.0000e+00 2.6404e+00 --1.5600e+03 2.8571e+01 2.6424e+00 --1.5600e+03 5.7143e+01 2.6483e+00 --1.5600e+03 8.5714e+01 2.6581e+00 --1.5600e+03 1.1429e+02 2.6718e+00 --1.5600e+03 1.4286e+02 2.6892e+00 --1.5600e+03 1.7143e+02 2.7104e+00 --1.5600e+03 2.0000e+02 2.7353e+00 --1.5600e+03 2.2857e+02 2.7637e+00 --1.5600e+03 2.5714e+02 2.7956e+00 --1.5600e+03 2.8571e+02 2.8307e+00 --1.5600e+03 3.1429e+02 2.8691e+00 --1.5600e+03 3.4286e+02 2.9105e+00 --1.5600e+03 3.7143e+02 2.9548e+00 --1.5600e+03 4.0000e+02 3.0018e+00 --1.5600e+03 4.2857e+02 3.0514e+00 --1.5600e+03 4.5714e+02 3.1034e+00 --1.5600e+03 4.8571e+02 3.1577e+00 --1.5600e+03 5.1429e+02 3.2140e+00 --1.5600e+03 5.4286e+02 3.2723e+00 --1.5600e+03 5.7143e+02 3.3323e+00 --1.5600e+03 6.0000e+02 3.3939e+00 --1.5600e+03 6.2857e+02 3.4569e+00 --1.5600e+03 6.5714e+02 3.5211e+00 --1.5600e+03 6.8571e+02 3.5865e+00 --1.5600e+03 7.1429e+02 3.6528e+00 --1.5600e+03 7.4286e+02 3.7200e+00 --1.5600e+03 7.7143e+02 3.7878e+00 --1.5600e+03 8.0000e+02 3.8561e+00 --1.5600e+03 8.2857e+02 3.9249e+00 --1.5600e+03 8.5714e+02 3.9940e+00 --1.5600e+03 8.8571e+02 4.0632e+00 --1.5600e+03 9.1429e+02 4.1326e+00 --1.5600e+03 9.4286e+02 4.2019e+00 --1.5600e+03 9.7143e+02 4.2711e+00 --1.5600e+03 1.0000e+03 4.3402e+00 --1.5600e+03 1.0286e+03 4.4089e+00 --1.5600e+03 1.0571e+03 4.4773e+00 --1.5600e+03 1.0857e+03 4.5453e+00 --1.5600e+03 1.1143e+03 4.6128e+00 --1.5600e+03 1.1429e+03 4.6798e+00 --1.5600e+03 1.1714e+03 4.7462e+00 --1.5600e+03 1.2000e+03 4.8120e+00 --1.5600e+03 1.2286e+03 4.8772e+00 --1.5600e+03 1.2571e+03 4.9416e+00 --1.5600e+03 1.2857e+03 5.0053e+00 --1.5600e+03 1.3143e+03 5.0682e+00 --1.5600e+03 1.3429e+03 5.1303e+00 --1.5600e+03 1.3714e+03 5.1917e+00 --1.5600e+03 1.4000e+03 5.2522e+00 --1.5600e+03 1.4286e+03 5.3119e+00 --1.5600e+03 1.4571e+03 5.3707e+00 --1.5600e+03 1.4857e+03 5.4287e+00 --1.5600e+03 1.5143e+03 5.4858e+00 --1.5600e+03 1.5429e+03 5.5421e+00 --1.5600e+03 1.5714e+03 5.5974e+00 --1.5600e+03 1.6000e+03 5.6519e+00 --1.5600e+03 1.6286e+03 5.7056e+00 --1.5600e+03 1.6571e+03 5.7583e+00 --1.5600e+03 1.6857e+03 5.8102e+00 --1.5600e+03 1.7143e+03 5.8613e+00 --1.5600e+03 1.7429e+03 5.9115e+00 --1.5600e+03 1.7714e+03 5.9608e+00 --1.5600e+03 1.8000e+03 6.0093e+00 --1.5600e+03 1.8286e+03 6.0570e+00 --1.5600e+03 1.8571e+03 6.1039e+00 --1.5600e+03 1.8857e+03 6.1499e+00 --1.5600e+03 1.9143e+03 6.1952e+00 --1.5600e+03 1.9429e+03 6.2397e+00 --1.5600e+03 1.9714e+03 6.2834e+00 --1.5600e+03 2.0000e+03 6.3263e+00 --1.5300e+03 -2.0000e+03 6.2913e+00 --1.5300e+03 -1.9714e+03 6.2472e+00 --1.5300e+03 -1.9429e+03 6.2024e+00 --1.5300e+03 -1.9143e+03 6.1567e+00 --1.5300e+03 -1.8857e+03 6.1101e+00 --1.5300e+03 -1.8571e+03 6.0627e+00 --1.5300e+03 -1.8286e+03 6.0145e+00 --1.5300e+03 -1.8000e+03 5.9653e+00 --1.5300e+03 -1.7714e+03 5.9153e+00 --1.5300e+03 -1.7429e+03 5.8644e+00 --1.5300e+03 -1.7143e+03 5.8126e+00 --1.5300e+03 -1.6857e+03 5.7599e+00 --1.5300e+03 -1.6571e+03 5.7062e+00 --1.5300e+03 -1.6286e+03 5.6517e+00 --1.5300e+03 -1.6000e+03 5.5962e+00 --1.5300e+03 -1.5714e+03 5.5397e+00 --1.5300e+03 -1.5429e+03 5.4823e+00 --1.5300e+03 -1.5143e+03 5.4240e+00 --1.5300e+03 -1.4857e+03 5.3647e+00 --1.5300e+03 -1.4571e+03 5.3044e+00 --1.5300e+03 -1.4286e+03 5.2432e+00 --1.5300e+03 -1.4000e+03 5.1811e+00 --1.5300e+03 -1.3714e+03 5.1181e+00 --1.5300e+03 -1.3429e+03 5.0542e+00 --1.5300e+03 -1.3143e+03 4.9893e+00 --1.5300e+03 -1.2857e+03 4.9236e+00 --1.5300e+03 -1.2571e+03 4.8570e+00 --1.5300e+03 -1.2286e+03 4.7896e+00 --1.5300e+03 -1.2000e+03 4.7215e+00 --1.5300e+03 -1.1714e+03 4.6525e+00 --1.5300e+03 -1.1429e+03 4.5828e+00 --1.5300e+03 -1.1143e+03 4.5125e+00 --1.5300e+03 -1.0857e+03 4.4415e+00 --1.5300e+03 -1.0571e+03 4.3699e+00 --1.5300e+03 -1.0286e+03 4.2979e+00 --1.5300e+03 -1.0000e+03 4.2254e+00 --1.5300e+03 -9.7143e+02 4.1525e+00 --1.5300e+03 -9.4286e+02 4.0793e+00 --1.5300e+03 -9.1429e+02 4.0060e+00 --1.5300e+03 -8.8571e+02 3.9326e+00 --1.5300e+03 -8.5714e+02 3.8591e+00 --1.5300e+03 -8.2857e+02 3.7858e+00 --1.5300e+03 -8.0000e+02 3.7127e+00 --1.5300e+03 -7.7143e+02 3.6400e+00 --1.5300e+03 -7.4286e+02 3.5678e+00 --1.5300e+03 -7.1429e+02 3.4963e+00 --1.5300e+03 -6.8571e+02 3.4255e+00 --1.5300e+03 -6.5714e+02 3.3557e+00 --1.5300e+03 -6.2857e+02 3.2870e+00 --1.5300e+03 -6.0000e+02 3.2195e+00 --1.5300e+03 -5.7143e+02 3.1535e+00 --1.5300e+03 -5.4286e+02 3.0892e+00 --1.5300e+03 -5.1429e+02 3.0266e+00 --1.5300e+03 -4.8571e+02 2.9661e+00 --1.5300e+03 -4.5714e+02 2.9077e+00 --1.5300e+03 -4.2857e+02 2.8517e+00 --1.5300e+03 -4.0000e+02 2.7983e+00 --1.5300e+03 -3.7143e+02 2.7475e+00 --1.5300e+03 -3.4286e+02 2.6997e+00 --1.5300e+03 -3.1429e+02 2.6550e+00 --1.5300e+03 -2.8571e+02 2.6136e+00 --1.5300e+03 -2.5714e+02 2.5756e+00 --1.5300e+03 -2.2857e+02 2.5411e+00 --1.5300e+03 -2.0000e+02 2.5103e+00 --1.5300e+03 -1.7143e+02 2.4834e+00 --1.5300e+03 -1.4286e+02 2.4604e+00 --1.5300e+03 -1.1429e+02 2.4415e+00 --1.5300e+03 -8.5714e+01 2.4267e+00 --1.5300e+03 -5.7143e+01 2.4160e+00 --1.5300e+03 -2.8571e+01 2.4096e+00 --1.5300e+03 0.0000e+00 2.4075e+00 --1.5300e+03 2.8571e+01 2.4096e+00 --1.5300e+03 5.7143e+01 2.4160e+00 --1.5300e+03 8.5714e+01 2.4267e+00 --1.5300e+03 1.1429e+02 2.4415e+00 --1.5300e+03 1.4286e+02 2.4604e+00 --1.5300e+03 1.7143e+02 2.4834e+00 --1.5300e+03 2.0000e+02 2.5103e+00 --1.5300e+03 2.2857e+02 2.5411e+00 --1.5300e+03 2.5714e+02 2.5756e+00 --1.5300e+03 2.8571e+02 2.6136e+00 --1.5300e+03 3.1429e+02 2.6550e+00 --1.5300e+03 3.4286e+02 2.6997e+00 --1.5300e+03 3.7143e+02 2.7475e+00 --1.5300e+03 4.0000e+02 2.7983e+00 --1.5300e+03 4.2857e+02 2.8517e+00 --1.5300e+03 4.5714e+02 2.9077e+00 --1.5300e+03 4.8571e+02 2.9661e+00 --1.5300e+03 5.1429e+02 3.0266e+00 --1.5300e+03 5.4286e+02 3.0892e+00 --1.5300e+03 5.7143e+02 3.1535e+00 --1.5300e+03 6.0000e+02 3.2195e+00 --1.5300e+03 6.2857e+02 3.2870e+00 --1.5300e+03 6.5714e+02 3.3557e+00 --1.5300e+03 6.8571e+02 3.4255e+00 --1.5300e+03 7.1429e+02 3.4963e+00 --1.5300e+03 7.4286e+02 3.5678e+00 --1.5300e+03 7.7143e+02 3.6400e+00 --1.5300e+03 8.0000e+02 3.7127e+00 --1.5300e+03 8.2857e+02 3.7858e+00 --1.5300e+03 8.5714e+02 3.8591e+00 --1.5300e+03 8.8571e+02 3.9326e+00 --1.5300e+03 9.1429e+02 4.0060e+00 --1.5300e+03 9.4286e+02 4.0793e+00 --1.5300e+03 9.7143e+02 4.1525e+00 --1.5300e+03 1.0000e+03 4.2254e+00 --1.5300e+03 1.0286e+03 4.2979e+00 --1.5300e+03 1.0571e+03 4.3699e+00 --1.5300e+03 1.0857e+03 4.4415e+00 --1.5300e+03 1.1143e+03 4.5125e+00 --1.5300e+03 1.1429e+03 4.5828e+00 --1.5300e+03 1.1714e+03 4.6525e+00 --1.5300e+03 1.2000e+03 4.7215e+00 --1.5300e+03 1.2286e+03 4.7896e+00 --1.5300e+03 1.2571e+03 4.8570e+00 --1.5300e+03 1.2857e+03 4.9236e+00 --1.5300e+03 1.3143e+03 4.9893e+00 --1.5300e+03 1.3429e+03 5.0542e+00 --1.5300e+03 1.3714e+03 5.1181e+00 --1.5300e+03 1.4000e+03 5.1811e+00 --1.5300e+03 1.4286e+03 5.2432e+00 --1.5300e+03 1.4571e+03 5.3044e+00 --1.5300e+03 1.4857e+03 5.3647e+00 --1.5300e+03 1.5143e+03 5.4240e+00 --1.5300e+03 1.5429e+03 5.4823e+00 --1.5300e+03 1.5714e+03 5.5397e+00 --1.5300e+03 1.6000e+03 5.5962e+00 --1.5300e+03 1.6286e+03 5.6517e+00 --1.5300e+03 1.6571e+03 5.7062e+00 --1.5300e+03 1.6857e+03 5.7599e+00 --1.5300e+03 1.7143e+03 5.8126e+00 --1.5300e+03 1.7429e+03 5.8644e+00 --1.5300e+03 1.7714e+03 5.9153e+00 --1.5300e+03 1.8000e+03 5.9653e+00 --1.5300e+03 1.8286e+03 6.0145e+00 --1.5300e+03 1.8571e+03 6.0627e+00 --1.5300e+03 1.8857e+03 6.1101e+00 --1.5300e+03 1.9143e+03 6.1567e+00 --1.5300e+03 1.9429e+03 6.2024e+00 --1.5300e+03 1.9714e+03 6.2472e+00 --1.5300e+03 2.0000e+03 6.2913e+00 --1.5000e+03 -2.0000e+03 6.2561e+00 --1.5000e+03 -1.9714e+03 6.2109e+00 --1.5000e+03 -1.9429e+03 6.1648e+00 --1.5000e+03 -1.9143e+03 6.1178e+00 --1.5000e+03 -1.8857e+03 6.0699e+00 --1.5000e+03 -1.8571e+03 6.0212e+00 --1.5000e+03 -1.8286e+03 5.9715e+00 --1.5000e+03 -1.8000e+03 5.9209e+00 --1.5000e+03 -1.7714e+03 5.8693e+00 --1.5000e+03 -1.7429e+03 5.8168e+00 --1.5000e+03 -1.7143e+03 5.7633e+00 --1.5000e+03 -1.6857e+03 5.7089e+00 --1.5000e+03 -1.6571e+03 5.6534e+00 --1.5000e+03 -1.6286e+03 5.5970e+00 --1.5000e+03 -1.6000e+03 5.5395e+00 --1.5000e+03 -1.5714e+03 5.4810e+00 --1.5000e+03 -1.5429e+03 5.4215e+00 --1.5000e+03 -1.5143e+03 5.3610e+00 --1.5000e+03 -1.4857e+03 5.2994e+00 --1.5000e+03 -1.4571e+03 5.2369e+00 --1.5000e+03 -1.4286e+03 5.1732e+00 --1.5000e+03 -1.4000e+03 5.1086e+00 --1.5000e+03 -1.3714e+03 5.0429e+00 --1.5000e+03 -1.3429e+03 4.9763e+00 --1.5000e+03 -1.3143e+03 4.9086e+00 --1.5000e+03 -1.2857e+03 4.8400e+00 --1.5000e+03 -1.2571e+03 4.7704e+00 --1.5000e+03 -1.2286e+03 4.6998e+00 --1.5000e+03 -1.2000e+03 4.6284e+00 --1.5000e+03 -1.1714e+03 4.5561e+00 --1.5000e+03 -1.1429e+03 4.4829e+00 --1.5000e+03 -1.1143e+03 4.4090e+00 --1.5000e+03 -1.0857e+03 4.3343e+00 --1.5000e+03 -1.0571e+03 4.2590e+00 --1.5000e+03 -1.0286e+03 4.1830e+00 --1.5000e+03 -1.0000e+03 4.1065e+00 --1.5000e+03 -9.7143e+02 4.0296e+00 --1.5000e+03 -9.4286e+02 3.9522e+00 --1.5000e+03 -9.1429e+02 3.8745e+00 --1.5000e+03 -8.8571e+02 3.7967e+00 --1.5000e+03 -8.5714e+02 3.7188e+00 --1.5000e+03 -8.2857e+02 3.6409e+00 --1.5000e+03 -8.0000e+02 3.5632e+00 --1.5000e+03 -7.7143e+02 3.4858e+00 --1.5000e+03 -7.4286e+02 3.4088e+00 --1.5000e+03 -7.1429e+02 3.3324e+00 --1.5000e+03 -6.8571e+02 3.2568e+00 --1.5000e+03 -6.5714e+02 3.1821e+00 --1.5000e+03 -6.2857e+02 3.1086e+00 --1.5000e+03 -6.0000e+02 3.0363e+00 --1.5000e+03 -5.7143e+02 2.9655e+00 --1.5000e+03 -5.4286e+02 2.8963e+00 --1.5000e+03 -5.1429e+02 2.8291e+00 --1.5000e+03 -4.8571e+02 2.7639e+00 --1.5000e+03 -4.5714e+02 2.7010e+00 --1.5000e+03 -4.2857e+02 2.6406e+00 --1.5000e+03 -4.0000e+02 2.5829e+00 --1.5000e+03 -3.7143e+02 2.5281e+00 --1.5000e+03 -3.4286e+02 2.4764e+00 --1.5000e+03 -3.1429e+02 2.4280e+00 --1.5000e+03 -2.8571e+02 2.3832e+00 --1.5000e+03 -2.5714e+02 2.3419e+00 --1.5000e+03 -2.2857e+02 2.3046e+00 --1.5000e+03 -2.0000e+02 2.2712e+00 --1.5000e+03 -1.7143e+02 2.2420e+00 --1.5000e+03 -1.4286e+02 2.2170e+00 --1.5000e+03 -1.1429e+02 2.1965e+00 --1.5000e+03 -8.5714e+01 2.1804e+00 --1.5000e+03 -5.7143e+01 2.1688e+00 --1.5000e+03 -2.8571e+01 2.1619e+00 --1.5000e+03 0.0000e+00 2.1595e+00 --1.5000e+03 2.8571e+01 2.1619e+00 --1.5000e+03 5.7143e+01 2.1688e+00 --1.5000e+03 8.5714e+01 2.1804e+00 --1.5000e+03 1.1429e+02 2.1965e+00 --1.5000e+03 1.4286e+02 2.2170e+00 --1.5000e+03 1.7143e+02 2.2420e+00 --1.5000e+03 2.0000e+02 2.2712e+00 --1.5000e+03 2.2857e+02 2.3046e+00 --1.5000e+03 2.5714e+02 2.3419e+00 --1.5000e+03 2.8571e+02 2.3832e+00 --1.5000e+03 3.1429e+02 2.4280e+00 --1.5000e+03 3.4286e+02 2.4764e+00 --1.5000e+03 3.7143e+02 2.5281e+00 --1.5000e+03 4.0000e+02 2.5829e+00 --1.5000e+03 4.2857e+02 2.6406e+00 --1.5000e+03 4.5714e+02 2.7010e+00 --1.5000e+03 4.8571e+02 2.7639e+00 --1.5000e+03 5.1429e+02 2.8291e+00 --1.5000e+03 5.4286e+02 2.8963e+00 --1.5000e+03 5.7143e+02 2.9655e+00 --1.5000e+03 6.0000e+02 3.0363e+00 --1.5000e+03 6.2857e+02 3.1086e+00 --1.5000e+03 6.5714e+02 3.1821e+00 --1.5000e+03 6.8571e+02 3.2568e+00 --1.5000e+03 7.1429e+02 3.3324e+00 --1.5000e+03 7.4286e+02 3.4088e+00 --1.5000e+03 7.7143e+02 3.4858e+00 --1.5000e+03 8.0000e+02 3.5632e+00 --1.5000e+03 8.2857e+02 3.6409e+00 --1.5000e+03 8.5714e+02 3.7188e+00 --1.5000e+03 8.8571e+02 3.7967e+00 --1.5000e+03 9.1429e+02 3.8745e+00 --1.5000e+03 9.4286e+02 3.9522e+00 --1.5000e+03 9.7143e+02 4.0296e+00 --1.5000e+03 1.0000e+03 4.1065e+00 --1.5000e+03 1.0286e+03 4.1830e+00 --1.5000e+03 1.0571e+03 4.2590e+00 --1.5000e+03 1.0857e+03 4.3343e+00 --1.5000e+03 1.1143e+03 4.4090e+00 --1.5000e+03 1.1429e+03 4.4829e+00 --1.5000e+03 1.1714e+03 4.5561e+00 --1.5000e+03 1.2000e+03 4.6284e+00 --1.5000e+03 1.2286e+03 4.6998e+00 --1.5000e+03 1.2571e+03 4.7704e+00 --1.5000e+03 1.2857e+03 4.8400e+00 --1.5000e+03 1.3143e+03 4.9086e+00 --1.5000e+03 1.3429e+03 4.9763e+00 --1.5000e+03 1.3714e+03 5.0429e+00 --1.5000e+03 1.4000e+03 5.1086e+00 --1.5000e+03 1.4286e+03 5.1732e+00 --1.5000e+03 1.4571e+03 5.2369e+00 --1.5000e+03 1.4857e+03 5.2994e+00 --1.5000e+03 1.5143e+03 5.3610e+00 --1.5000e+03 1.5429e+03 5.4215e+00 --1.5000e+03 1.5714e+03 5.4810e+00 --1.5000e+03 1.6000e+03 5.5395e+00 --1.5000e+03 1.6286e+03 5.5970e+00 --1.5000e+03 1.6571e+03 5.6534e+00 --1.5000e+03 1.6857e+03 5.7089e+00 --1.5000e+03 1.7143e+03 5.7633e+00 --1.5000e+03 1.7429e+03 5.8168e+00 --1.5000e+03 1.7714e+03 5.8693e+00 --1.5000e+03 1.8000e+03 5.9209e+00 --1.5000e+03 1.8286e+03 5.9715e+00 --1.5000e+03 1.8571e+03 6.0212e+00 --1.5000e+03 1.8857e+03 6.0699e+00 --1.5000e+03 1.9143e+03 6.1178e+00 --1.5000e+03 1.9429e+03 6.1648e+00 --1.5000e+03 1.9714e+03 6.2109e+00 --1.5000e+03 2.0000e+03 6.2561e+00 --1.4700e+03 -2.0000e+03 6.2207e+00 --1.4700e+03 -1.9714e+03 6.1743e+00 --1.4700e+03 -1.9429e+03 6.1269e+00 --1.4700e+03 -1.9143e+03 6.0786e+00 --1.4700e+03 -1.8857e+03 6.0294e+00 --1.4700e+03 -1.8571e+03 5.9792e+00 --1.4700e+03 -1.8286e+03 5.9281e+00 --1.4700e+03 -1.8000e+03 5.8760e+00 --1.4700e+03 -1.7714e+03 5.8228e+00 --1.4700e+03 -1.7429e+03 5.7687e+00 --1.4700e+03 -1.7143e+03 5.7135e+00 --1.4700e+03 -1.6857e+03 5.6572e+00 --1.4700e+03 -1.6571e+03 5.5999e+00 --1.4700e+03 -1.6286e+03 5.5415e+00 --1.4700e+03 -1.6000e+03 5.4820e+00 --1.4700e+03 -1.5714e+03 5.4215e+00 --1.4700e+03 -1.5429e+03 5.3598e+00 --1.4700e+03 -1.5143e+03 5.2970e+00 --1.4700e+03 -1.4857e+03 5.2330e+00 --1.4700e+03 -1.4571e+03 5.1680e+00 --1.4700e+03 -1.4286e+03 5.1018e+00 --1.4700e+03 -1.4000e+03 5.0345e+00 --1.4700e+03 -1.3714e+03 4.9661e+00 --1.4700e+03 -1.3429e+03 4.8966e+00 --1.4700e+03 -1.3143e+03 4.8260e+00 --1.4700e+03 -1.2857e+03 4.7542e+00 --1.4700e+03 -1.2571e+03 4.6814e+00 --1.4700e+03 -1.2286e+03 4.6076e+00 --1.4700e+03 -1.2000e+03 4.5327e+00 --1.4700e+03 -1.1714e+03 4.4569e+00 --1.4700e+03 -1.1429e+03 4.3801e+00 --1.4700e+03 -1.1143e+03 4.3024e+00 --1.4700e+03 -1.0857e+03 4.2238e+00 --1.4700e+03 -1.0571e+03 4.1444e+00 --1.4700e+03 -1.0286e+03 4.0643e+00 --1.4700e+03 -1.0000e+03 3.9835e+00 --1.4700e+03 -9.7143e+02 3.9021e+00 --1.4700e+03 -9.4286e+02 3.8203e+00 --1.4700e+03 -9.1429e+02 3.7380e+00 --1.4700e+03 -8.8571e+02 3.6554e+00 --1.4700e+03 -8.5714e+02 3.5727e+00 --1.4700e+03 -8.2857e+02 3.4899e+00 --1.4700e+03 -8.0000e+02 3.4071e+00 --1.4700e+03 -7.7143e+02 3.3246e+00 --1.4700e+03 -7.4286e+02 3.2425e+00 --1.4700e+03 -7.1429e+02 3.1609e+00 --1.4700e+03 -6.8571e+02 3.0800e+00 --1.4700e+03 -6.5714e+02 3.0000e+00 --1.4700e+03 -6.2857e+02 2.9211e+00 --1.4700e+03 -6.0000e+02 2.8435e+00 --1.4700e+03 -5.7143e+02 2.7674e+00 --1.4700e+03 -5.4286e+02 2.6930e+00 --1.4700e+03 -5.1429e+02 2.6205e+00 --1.4700e+03 -4.8571e+02 2.5503e+00 --1.4700e+03 -4.5714e+02 2.4824e+00 --1.4700e+03 -4.2857e+02 2.4171e+00 --1.4700e+03 -4.0000e+02 2.3547e+00 --1.4700e+03 -3.7143e+02 2.2954e+00 --1.4700e+03 -3.4286e+02 2.2394e+00 --1.4700e+03 -3.1429e+02 2.1869e+00 --1.4700e+03 -2.8571e+02 2.1382e+00 --1.4700e+03 -2.5714e+02 2.0935e+00 --1.4700e+03 -2.2857e+02 2.0529e+00 --1.4700e+03 -2.0000e+02 2.0166e+00 --1.4700e+03 -1.7143e+02 1.9848e+00 --1.4700e+03 -1.4286e+02 1.9576e+00 --1.4700e+03 -1.1429e+02 1.9352e+00 --1.4700e+03 -8.5714e+01 1.9177e+00 --1.4700e+03 -5.7143e+01 1.9051e+00 --1.4700e+03 -2.8571e+01 1.8975e+00 --1.4700e+03 0.0000e+00 1.8950e+00 --1.4700e+03 2.8571e+01 1.8975e+00 --1.4700e+03 5.7143e+01 1.9051e+00 --1.4700e+03 8.5714e+01 1.9177e+00 --1.4700e+03 1.1429e+02 1.9352e+00 --1.4700e+03 1.4286e+02 1.9576e+00 --1.4700e+03 1.7143e+02 1.9848e+00 --1.4700e+03 2.0000e+02 2.0166e+00 --1.4700e+03 2.2857e+02 2.0529e+00 --1.4700e+03 2.5714e+02 2.0935e+00 --1.4700e+03 2.8571e+02 2.1382e+00 --1.4700e+03 3.1429e+02 2.1869e+00 --1.4700e+03 3.4286e+02 2.2394e+00 --1.4700e+03 3.7143e+02 2.2954e+00 --1.4700e+03 4.0000e+02 2.3547e+00 --1.4700e+03 4.2857e+02 2.4171e+00 --1.4700e+03 4.5714e+02 2.4824e+00 --1.4700e+03 4.8571e+02 2.5503e+00 --1.4700e+03 5.1429e+02 2.6205e+00 --1.4700e+03 5.4286e+02 2.6930e+00 --1.4700e+03 5.7143e+02 2.7674e+00 --1.4700e+03 6.0000e+02 2.8435e+00 --1.4700e+03 6.2857e+02 2.9211e+00 --1.4700e+03 6.5714e+02 3.0000e+00 --1.4700e+03 6.8571e+02 3.0800e+00 --1.4700e+03 7.1429e+02 3.1609e+00 --1.4700e+03 7.4286e+02 3.2425e+00 --1.4700e+03 7.7143e+02 3.3246e+00 --1.4700e+03 8.0000e+02 3.4071e+00 --1.4700e+03 8.2857e+02 3.4899e+00 --1.4700e+03 8.5714e+02 3.5727e+00 --1.4700e+03 8.8571e+02 3.6554e+00 --1.4700e+03 9.1429e+02 3.7380e+00 --1.4700e+03 9.4286e+02 3.8203e+00 --1.4700e+03 9.7143e+02 3.9021e+00 --1.4700e+03 1.0000e+03 3.9835e+00 --1.4700e+03 1.0286e+03 4.0643e+00 --1.4700e+03 1.0571e+03 4.1444e+00 --1.4700e+03 1.0857e+03 4.2238e+00 --1.4700e+03 1.1143e+03 4.3024e+00 --1.4700e+03 1.1429e+03 4.3801e+00 --1.4700e+03 1.1714e+03 4.4569e+00 --1.4700e+03 1.2000e+03 4.5327e+00 --1.4700e+03 1.2286e+03 4.6076e+00 --1.4700e+03 1.2571e+03 4.6814e+00 --1.4700e+03 1.2857e+03 4.7542e+00 --1.4700e+03 1.3143e+03 4.8260e+00 --1.4700e+03 1.3429e+03 4.8966e+00 --1.4700e+03 1.3714e+03 4.9661e+00 --1.4700e+03 1.4000e+03 5.0345e+00 --1.4700e+03 1.4286e+03 5.1018e+00 --1.4700e+03 1.4571e+03 5.1680e+00 --1.4700e+03 1.4857e+03 5.2330e+00 --1.4700e+03 1.5143e+03 5.2970e+00 --1.4700e+03 1.5429e+03 5.3598e+00 --1.4700e+03 1.5714e+03 5.4215e+00 --1.4700e+03 1.6000e+03 5.4820e+00 --1.4700e+03 1.6286e+03 5.5415e+00 --1.4700e+03 1.6571e+03 5.5999e+00 --1.4700e+03 1.6857e+03 5.6572e+00 --1.4700e+03 1.7143e+03 5.7135e+00 --1.4700e+03 1.7429e+03 5.7687e+00 --1.4700e+03 1.7714e+03 5.8228e+00 --1.4700e+03 1.8000e+03 5.8760e+00 --1.4700e+03 1.8286e+03 5.9281e+00 --1.4700e+03 1.8571e+03 5.9792e+00 --1.4700e+03 1.8857e+03 6.0294e+00 --1.4700e+03 1.9143e+03 6.0786e+00 --1.4700e+03 1.9429e+03 6.1269e+00 --1.4700e+03 1.9714e+03 6.1743e+00 --1.4700e+03 2.0000e+03 6.2207e+00 --1.4400e+03 -2.0000e+03 6.1851e+00 --1.4400e+03 -1.9714e+03 6.1374e+00 --1.4400e+03 -1.9429e+03 6.0888e+00 --1.4400e+03 -1.9143e+03 6.0392e+00 --1.4400e+03 -1.8857e+03 5.9886e+00 --1.4400e+03 -1.8571e+03 5.9370e+00 --1.4400e+03 -1.8286e+03 5.8843e+00 --1.4400e+03 -1.8000e+03 5.8306e+00 --1.4400e+03 -1.7714e+03 5.7758e+00 --1.4400e+03 -1.7429e+03 5.7200e+00 --1.4400e+03 -1.7143e+03 5.6630e+00 --1.4400e+03 -1.6857e+03 5.6049e+00 --1.4400e+03 -1.6571e+03 5.5457e+00 --1.4400e+03 -1.6286e+03 5.4853e+00 --1.4400e+03 -1.6000e+03 5.4237e+00 --1.4400e+03 -1.5714e+03 5.3610e+00 --1.4400e+03 -1.5429e+03 5.2970e+00 --1.4400e+03 -1.5143e+03 5.2318e+00 --1.4400e+03 -1.4857e+03 5.1655e+00 --1.4400e+03 -1.4571e+03 5.0979e+00 --1.4400e+03 -1.4286e+03 5.0290e+00 --1.4400e+03 -1.4000e+03 4.9590e+00 --1.4400e+03 -1.3714e+03 4.8877e+00 --1.4400e+03 -1.3429e+03 4.8151e+00 --1.4400e+03 -1.3143e+03 4.7414e+00 --1.4400e+03 -1.2857e+03 4.6664e+00 --1.4400e+03 -1.2571e+03 4.5903e+00 --1.4400e+03 -1.2286e+03 4.5130e+00 --1.4400e+03 -1.2000e+03 4.4345e+00 --1.4400e+03 -1.1714e+03 4.3549e+00 --1.4400e+03 -1.1429e+03 4.2742e+00 --1.4400e+03 -1.1143e+03 4.1924e+00 --1.4400e+03 -1.0857e+03 4.1097e+00 --1.4400e+03 -1.0571e+03 4.0260e+00 --1.4400e+03 -1.0286e+03 3.9415e+00 --1.4400e+03 -1.0000e+03 3.8561e+00 --1.4400e+03 -9.7143e+02 3.7701e+00 --1.4400e+03 -9.4286e+02 3.6833e+00 --1.4400e+03 -9.1429e+02 3.5961e+00 --1.4400e+03 -8.8571e+02 3.5084e+00 --1.4400e+03 -8.5714e+02 3.4205e+00 --1.4400e+03 -8.2857e+02 3.3323e+00 --1.4400e+03 -8.0000e+02 3.2441e+00 --1.4400e+03 -7.7143e+02 3.1561e+00 --1.4400e+03 -7.4286e+02 3.0683e+00 --1.4400e+03 -7.1429e+02 2.9810e+00 --1.4400e+03 -6.8571e+02 2.8944e+00 --1.4400e+03 -6.5714e+02 2.8086e+00 --1.4400e+03 -6.2857e+02 2.7239e+00 --1.4400e+03 -6.0000e+02 2.6404e+00 --1.4400e+03 -5.7143e+02 2.5585e+00 --1.4400e+03 -5.4286e+02 2.4783e+00 --1.4400e+03 -5.1429e+02 2.4001e+00 --1.4400e+03 -4.8571e+02 2.3242e+00 --1.4400e+03 -4.5714e+02 2.2508e+00 --1.4400e+03 -4.2857e+02 2.1802e+00 --1.4400e+03 -4.0000e+02 2.1125e+00 --1.4400e+03 -3.7143e+02 2.0482e+00 --1.4400e+03 -3.4286e+02 1.9874e+00 --1.4400e+03 -3.1429e+02 1.9304e+00 --1.4400e+03 -2.8571e+02 1.8774e+00 --1.4400e+03 -2.5714e+02 1.8287e+00 --1.4400e+03 -2.2857e+02 1.7844e+00 --1.4400e+03 -2.0000e+02 1.7449e+00 --1.4400e+03 -1.7143e+02 1.7102e+00 --1.4400e+03 -1.4286e+02 1.6806e+00 --1.4400e+03 -1.1429e+02 1.6561e+00 --1.4400e+03 -8.5714e+01 1.6370e+00 --1.4400e+03 -5.7143e+01 1.6232e+00 --1.4400e+03 -2.8571e+01 1.6150e+00 --1.4400e+03 0.0000e+00 1.6122e+00 --1.4400e+03 2.8571e+01 1.6150e+00 --1.4400e+03 5.7143e+01 1.6232e+00 --1.4400e+03 8.5714e+01 1.6370e+00 --1.4400e+03 1.1429e+02 1.6561e+00 --1.4400e+03 1.4286e+02 1.6806e+00 --1.4400e+03 1.7143e+02 1.7102e+00 --1.4400e+03 2.0000e+02 1.7449e+00 --1.4400e+03 2.2857e+02 1.7844e+00 --1.4400e+03 2.5714e+02 1.8287e+00 --1.4400e+03 2.8571e+02 1.8774e+00 --1.4400e+03 3.1429e+02 1.9304e+00 --1.4400e+03 3.4286e+02 1.9874e+00 --1.4400e+03 3.7143e+02 2.0482e+00 --1.4400e+03 4.0000e+02 2.1125e+00 --1.4400e+03 4.2857e+02 2.1802e+00 --1.4400e+03 4.5714e+02 2.2508e+00 --1.4400e+03 4.8571e+02 2.3242e+00 --1.4400e+03 5.1429e+02 2.4001e+00 --1.4400e+03 5.4286e+02 2.4783e+00 --1.4400e+03 5.7143e+02 2.5585e+00 --1.4400e+03 6.0000e+02 2.6404e+00 --1.4400e+03 6.2857e+02 2.7239e+00 --1.4400e+03 6.5714e+02 2.8086e+00 --1.4400e+03 6.8571e+02 2.8944e+00 --1.4400e+03 7.1429e+02 2.9810e+00 --1.4400e+03 7.4286e+02 3.0683e+00 --1.4400e+03 7.7143e+02 3.1561e+00 --1.4400e+03 8.0000e+02 3.2441e+00 --1.4400e+03 8.2857e+02 3.3323e+00 --1.4400e+03 8.5714e+02 3.4205e+00 --1.4400e+03 8.8571e+02 3.5084e+00 --1.4400e+03 9.1429e+02 3.5961e+00 --1.4400e+03 9.4286e+02 3.6833e+00 --1.4400e+03 9.7143e+02 3.7701e+00 --1.4400e+03 1.0000e+03 3.8561e+00 --1.4400e+03 1.0286e+03 3.9415e+00 --1.4400e+03 1.0571e+03 4.0260e+00 --1.4400e+03 1.0857e+03 4.1097e+00 --1.4400e+03 1.1143e+03 4.1924e+00 --1.4400e+03 1.1429e+03 4.2742e+00 --1.4400e+03 1.1714e+03 4.3549e+00 --1.4400e+03 1.2000e+03 4.4345e+00 --1.4400e+03 1.2286e+03 4.5130e+00 --1.4400e+03 1.2571e+03 4.5903e+00 --1.4400e+03 1.2857e+03 4.6664e+00 --1.4400e+03 1.3143e+03 4.7414e+00 --1.4400e+03 1.3429e+03 4.8151e+00 --1.4400e+03 1.3714e+03 4.8877e+00 --1.4400e+03 1.4000e+03 4.9590e+00 --1.4400e+03 1.4286e+03 5.0290e+00 --1.4400e+03 1.4571e+03 5.0979e+00 --1.4400e+03 1.4857e+03 5.1655e+00 --1.4400e+03 1.5143e+03 5.2318e+00 --1.4400e+03 1.5429e+03 5.2970e+00 --1.4400e+03 1.5714e+03 5.3610e+00 --1.4400e+03 1.6000e+03 5.4237e+00 --1.4400e+03 1.6286e+03 5.4853e+00 --1.4400e+03 1.6571e+03 5.5457e+00 --1.4400e+03 1.6857e+03 5.6049e+00 --1.4400e+03 1.7143e+03 5.6630e+00 --1.4400e+03 1.7429e+03 5.7200e+00 --1.4400e+03 1.7714e+03 5.7758e+00 --1.4400e+03 1.8000e+03 5.8306e+00 --1.4400e+03 1.8286e+03 5.8843e+00 --1.4400e+03 1.8571e+03 5.9370e+00 --1.4400e+03 1.8857e+03 5.9886e+00 --1.4400e+03 1.9143e+03 6.0392e+00 --1.4400e+03 1.9429e+03 6.0888e+00 --1.4400e+03 1.9714e+03 6.1374e+00 --1.4400e+03 2.0000e+03 6.1851e+00 --1.4100e+03 -2.0000e+03 6.1493e+00 --1.4100e+03 -1.9714e+03 6.1004e+00 --1.4100e+03 -1.9429e+03 6.0505e+00 --1.4100e+03 -1.9143e+03 5.9995e+00 --1.4100e+03 -1.8857e+03 5.9475e+00 --1.4100e+03 -1.8571e+03 5.8944e+00 --1.4100e+03 -1.8286e+03 5.8402e+00 --1.4100e+03 -1.8000e+03 5.7849e+00 --1.4100e+03 -1.7714e+03 5.7284e+00 --1.4100e+03 -1.7429e+03 5.6708e+00 --1.4100e+03 -1.7143e+03 5.6120e+00 --1.4100e+03 -1.6857e+03 5.5520e+00 --1.4100e+03 -1.6571e+03 5.4908e+00 --1.4100e+03 -1.6286e+03 5.4283e+00 --1.4100e+03 -1.6000e+03 5.3646e+00 --1.4100e+03 -1.5714e+03 5.2995e+00 --1.4100e+03 -1.5429e+03 5.2332e+00 --1.4100e+03 -1.5143e+03 5.1656e+00 --1.4100e+03 -1.4857e+03 5.0967e+00 --1.4100e+03 -1.4571e+03 5.0264e+00 --1.4100e+03 -1.4286e+03 4.9548e+00 --1.4100e+03 -1.4000e+03 4.8819e+00 --1.4100e+03 -1.3714e+03 4.8076e+00 --1.4100e+03 -1.3429e+03 4.7319e+00 --1.4100e+03 -1.3143e+03 4.6549e+00 --1.4100e+03 -1.2857e+03 4.5765e+00 --1.4100e+03 -1.2571e+03 4.4968e+00 --1.4100e+03 -1.2286e+03 4.4158e+00 --1.4100e+03 -1.2000e+03 4.3335e+00 --1.4100e+03 -1.1714e+03 4.2499e+00 --1.4100e+03 -1.1429e+03 4.1651e+00 --1.4100e+03 -1.1143e+03 4.0791e+00 --1.4100e+03 -1.0857e+03 3.9920e+00 --1.4100e+03 -1.0571e+03 3.9037e+00 --1.4100e+03 -1.0286e+03 3.8144e+00 --1.4100e+03 -1.0000e+03 3.7242e+00 --1.4100e+03 -9.7143e+02 3.6331e+00 --1.4100e+03 -9.4286e+02 3.5412e+00 --1.4100e+03 -9.1429e+02 3.4486e+00 --1.4100e+03 -8.8571e+02 3.3554e+00 --1.4100e+03 -8.5714e+02 3.2618e+00 --1.4100e+03 -8.2857e+02 3.1679e+00 --1.4100e+03 -8.0000e+02 3.0738e+00 --1.4100e+03 -7.7143e+02 2.9797e+00 --1.4100e+03 -7.4286e+02 2.8858e+00 --1.4100e+03 -7.1429e+02 2.7923e+00 --1.4100e+03 -6.8571e+02 2.6994e+00 --1.4100e+03 -6.5714e+02 2.6073e+00 --1.4100e+03 -6.2857e+02 2.5161e+00 --1.4100e+03 -6.0000e+02 2.4263e+00 --1.4100e+03 -5.7143e+02 2.3380e+00 --1.4100e+03 -5.4286e+02 2.2514e+00 --1.4100e+03 -5.1429e+02 2.1669e+00 --1.4100e+03 -4.8571e+02 2.0847e+00 --1.4100e+03 -4.5714e+02 2.0052e+00 --1.4100e+03 -4.2857e+02 1.9285e+00 --1.4100e+03 -4.0000e+02 1.8551e+00 --1.4100e+03 -3.7143e+02 1.7851e+00 --1.4100e+03 -3.4286e+02 1.7190e+00 --1.4100e+03 -3.1429e+02 1.6569e+00 --1.4100e+03 -2.8571e+02 1.5991e+00 --1.4100e+03 -2.5714e+02 1.5459e+00 --1.4100e+03 -2.2857e+02 1.4976e+00 --1.4100e+03 -2.0000e+02 1.4544e+00 --1.4100e+03 -1.7143e+02 1.4165e+00 --1.4100e+03 -1.4286e+02 1.3841e+00 --1.4100e+03 -1.1429e+02 1.3573e+00 --1.4100e+03 -8.5714e+01 1.3364e+00 --1.4100e+03 -5.7143e+01 1.3213e+00 --1.4100e+03 -2.8571e+01 1.3123e+00 --1.4100e+03 0.0000e+00 1.3092e+00 --1.4100e+03 2.8571e+01 1.3123e+00 --1.4100e+03 5.7143e+01 1.3213e+00 --1.4100e+03 8.5714e+01 1.3364e+00 --1.4100e+03 1.1429e+02 1.3573e+00 --1.4100e+03 1.4286e+02 1.3841e+00 --1.4100e+03 1.7143e+02 1.4165e+00 --1.4100e+03 2.0000e+02 1.4544e+00 --1.4100e+03 2.2857e+02 1.4976e+00 --1.4100e+03 2.5714e+02 1.5459e+00 --1.4100e+03 2.8571e+02 1.5991e+00 --1.4100e+03 3.1429e+02 1.6569e+00 --1.4100e+03 3.4286e+02 1.7190e+00 --1.4100e+03 3.7143e+02 1.7851e+00 --1.4100e+03 4.0000e+02 1.8551e+00 --1.4100e+03 4.2857e+02 1.9285e+00 --1.4100e+03 4.5714e+02 2.0052e+00 --1.4100e+03 4.8571e+02 2.0847e+00 --1.4100e+03 5.1429e+02 2.1669e+00 --1.4100e+03 5.4286e+02 2.2514e+00 --1.4100e+03 5.7143e+02 2.3380e+00 --1.4100e+03 6.0000e+02 2.4263e+00 --1.4100e+03 6.2857e+02 2.5161e+00 --1.4100e+03 6.5714e+02 2.6073e+00 --1.4100e+03 6.8571e+02 2.6994e+00 --1.4100e+03 7.1429e+02 2.7923e+00 --1.4100e+03 7.4286e+02 2.8858e+00 --1.4100e+03 7.7143e+02 2.9797e+00 --1.4100e+03 8.0000e+02 3.0738e+00 --1.4100e+03 8.2857e+02 3.1679e+00 --1.4100e+03 8.5714e+02 3.2618e+00 --1.4100e+03 8.8571e+02 3.3554e+00 --1.4100e+03 9.1429e+02 3.4486e+00 --1.4100e+03 9.4286e+02 3.5412e+00 --1.4100e+03 9.7143e+02 3.6331e+00 --1.4100e+03 1.0000e+03 3.7242e+00 --1.4100e+03 1.0286e+03 3.8144e+00 --1.4100e+03 1.0571e+03 3.9037e+00 --1.4100e+03 1.0857e+03 3.9920e+00 --1.4100e+03 1.1143e+03 4.0791e+00 --1.4100e+03 1.1429e+03 4.1651e+00 --1.4100e+03 1.1714e+03 4.2499e+00 --1.4100e+03 1.2000e+03 4.3335e+00 --1.4100e+03 1.2286e+03 4.4158e+00 --1.4100e+03 1.2571e+03 4.4968e+00 --1.4100e+03 1.2857e+03 4.5765e+00 --1.4100e+03 1.3143e+03 4.6549e+00 --1.4100e+03 1.3429e+03 4.7319e+00 --1.4100e+03 1.3714e+03 4.8076e+00 --1.4100e+03 1.4000e+03 4.8819e+00 --1.4100e+03 1.4286e+03 4.9548e+00 --1.4100e+03 1.4571e+03 5.0264e+00 --1.4100e+03 1.4857e+03 5.0967e+00 --1.4100e+03 1.5143e+03 5.1656e+00 --1.4100e+03 1.5429e+03 5.2332e+00 --1.4100e+03 1.5714e+03 5.2995e+00 --1.4100e+03 1.6000e+03 5.3646e+00 --1.4100e+03 1.6286e+03 5.4283e+00 --1.4100e+03 1.6571e+03 5.4908e+00 --1.4100e+03 1.6857e+03 5.5520e+00 --1.4100e+03 1.7143e+03 5.6120e+00 --1.4100e+03 1.7429e+03 5.6708e+00 --1.4100e+03 1.7714e+03 5.7284e+00 --1.4100e+03 1.8000e+03 5.7849e+00 --1.4100e+03 1.8286e+03 5.8402e+00 --1.4100e+03 1.8571e+03 5.8944e+00 --1.4100e+03 1.8857e+03 5.9475e+00 --1.4100e+03 1.9143e+03 5.9995e+00 --1.4100e+03 1.9429e+03 6.0505e+00 --1.4100e+03 1.9714e+03 6.1004e+00 --1.4100e+03 2.0000e+03 6.1493e+00 --1.3800e+03 -2.0000e+03 6.1134e+00 --1.3800e+03 -1.9714e+03 6.0632e+00 --1.3800e+03 -1.9429e+03 6.0119e+00 --1.3800e+03 -1.9143e+03 5.9595e+00 --1.3800e+03 -1.8857e+03 5.9061e+00 --1.3800e+03 -1.8571e+03 5.8514e+00 --1.3800e+03 -1.8286e+03 5.7957e+00 --1.3800e+03 -1.8000e+03 5.7387e+00 --1.3800e+03 -1.7714e+03 5.6805e+00 --1.3800e+03 -1.7429e+03 5.6211e+00 --1.3800e+03 -1.7143e+03 5.5604e+00 --1.3800e+03 -1.6857e+03 5.4984e+00 --1.3800e+03 -1.6571e+03 5.4352e+00 --1.3800e+03 -1.6286e+03 5.3706e+00 --1.3800e+03 -1.6000e+03 5.3046e+00 --1.3800e+03 -1.5714e+03 5.2373e+00 --1.3800e+03 -1.5429e+03 5.1685e+00 --1.3800e+03 -1.5143e+03 5.0984e+00 --1.3800e+03 -1.4857e+03 5.0268e+00 --1.3800e+03 -1.4571e+03 4.9537e+00 --1.3800e+03 -1.4286e+03 4.8792e+00 --1.3800e+03 -1.4000e+03 4.8032e+00 --1.3800e+03 -1.3714e+03 4.7258e+00 --1.3800e+03 -1.3429e+03 4.6468e+00 --1.3800e+03 -1.3143e+03 4.5664e+00 --1.3800e+03 -1.2857e+03 4.4844e+00 --1.3800e+03 -1.2571e+03 4.4010e+00 --1.3800e+03 -1.2286e+03 4.3161e+00 --1.3800e+03 -1.2000e+03 4.2298e+00 --1.3800e+03 -1.1714e+03 4.1420e+00 --1.3800e+03 -1.1429e+03 4.0529e+00 --1.3800e+03 -1.1143e+03 3.9623e+00 --1.3800e+03 -1.0857e+03 3.8705e+00 --1.3800e+03 -1.0571e+03 3.7773e+00 --1.3800e+03 -1.0286e+03 3.6830e+00 --1.3800e+03 -1.0000e+03 3.5875e+00 --1.3800e+03 -9.7143e+02 3.4910e+00 --1.3800e+03 -9.4286e+02 3.3935e+00 --1.3800e+03 -9.1429e+02 3.2951e+00 --1.3800e+03 -8.8571e+02 3.1960e+00 --1.3800e+03 -8.5714e+02 3.0963e+00 --1.3800e+03 -8.2857e+02 2.9962e+00 --1.3800e+03 -8.0000e+02 2.8957e+00 --1.3800e+03 -7.7143e+02 2.7951e+00 --1.3800e+03 -7.4286e+02 2.6945e+00 --1.3800e+03 -7.1429e+02 2.5942e+00 --1.3800e+03 -6.8571e+02 2.4944e+00 --1.3800e+03 -6.5714e+02 2.3953e+00 --1.3800e+03 -6.2857e+02 2.2971e+00 --1.3800e+03 -6.0000e+02 2.2002e+00 --1.3800e+03 -5.7143e+02 2.1048e+00 --1.3800e+03 -5.4286e+02 2.0112e+00 --1.3800e+03 -5.1429e+02 1.9197e+00 --1.3800e+03 -4.8571e+02 1.8306e+00 --1.3800e+03 -4.5714e+02 1.7442e+00 --1.3800e+03 -4.2857e+02 1.6609e+00 --1.3800e+03 -4.0000e+02 1.5809e+00 --1.3800e+03 -3.7143e+02 1.5047e+00 --1.3800e+03 -3.4286e+02 1.4325e+00 --1.3800e+03 -3.1429e+02 1.3647e+00 --1.3800e+03 -2.8571e+02 1.3016e+00 --1.3800e+03 -2.5714e+02 1.2434e+00 --1.3800e+03 -2.2857e+02 1.1905e+00 --1.3800e+03 -2.0000e+02 1.1432e+00 --1.3800e+03 -1.7143e+02 1.1016e+00 --1.3800e+03 -1.4286e+02 1.0661e+00 --1.3800e+03 -1.1429e+02 1.0367e+00 --1.3800e+03 -8.5714e+01 1.0137e+00 --1.3800e+03 -5.7143e+01 9.9714e-01 --1.3800e+03 -2.8571e+01 9.8719e-01 --1.3800e+03 0.0000e+00 9.8386e-01 --1.3800e+03 2.8571e+01 9.8719e-01 --1.3800e+03 5.7143e+01 9.9714e-01 --1.3800e+03 8.5714e+01 1.0137e+00 --1.3800e+03 1.1429e+02 1.0367e+00 --1.3800e+03 1.4286e+02 1.0661e+00 --1.3800e+03 1.7143e+02 1.1016e+00 --1.3800e+03 2.0000e+02 1.1432e+00 --1.3800e+03 2.2857e+02 1.1905e+00 --1.3800e+03 2.5714e+02 1.2434e+00 --1.3800e+03 2.8571e+02 1.3016e+00 --1.3800e+03 3.1429e+02 1.3647e+00 --1.3800e+03 3.4286e+02 1.4325e+00 --1.3800e+03 3.7143e+02 1.5047e+00 --1.3800e+03 4.0000e+02 1.5809e+00 --1.3800e+03 4.2857e+02 1.6609e+00 --1.3800e+03 4.5714e+02 1.7442e+00 --1.3800e+03 4.8571e+02 1.8306e+00 --1.3800e+03 5.1429e+02 1.9197e+00 --1.3800e+03 5.4286e+02 2.0112e+00 --1.3800e+03 5.7143e+02 2.1048e+00 --1.3800e+03 6.0000e+02 2.2002e+00 --1.3800e+03 6.2857e+02 2.2971e+00 --1.3800e+03 6.5714e+02 2.3953e+00 --1.3800e+03 6.8571e+02 2.4944e+00 --1.3800e+03 7.1429e+02 2.5942e+00 --1.3800e+03 7.4286e+02 2.6945e+00 --1.3800e+03 7.7143e+02 2.7951e+00 --1.3800e+03 8.0000e+02 2.8957e+00 --1.3800e+03 8.2857e+02 2.9962e+00 --1.3800e+03 8.5714e+02 3.0963e+00 --1.3800e+03 8.8571e+02 3.1960e+00 --1.3800e+03 9.1429e+02 3.2951e+00 --1.3800e+03 9.4286e+02 3.3935e+00 --1.3800e+03 9.7143e+02 3.4910e+00 --1.3800e+03 1.0000e+03 3.5875e+00 --1.3800e+03 1.0286e+03 3.6830e+00 --1.3800e+03 1.0571e+03 3.7773e+00 --1.3800e+03 1.0857e+03 3.8705e+00 --1.3800e+03 1.1143e+03 3.9623e+00 --1.3800e+03 1.1429e+03 4.0529e+00 --1.3800e+03 1.1714e+03 4.1420e+00 --1.3800e+03 1.2000e+03 4.2298e+00 --1.3800e+03 1.2286e+03 4.3161e+00 --1.3800e+03 1.2571e+03 4.4010e+00 --1.3800e+03 1.2857e+03 4.4844e+00 --1.3800e+03 1.3143e+03 4.5664e+00 --1.3800e+03 1.3429e+03 4.6468e+00 --1.3800e+03 1.3714e+03 4.7258e+00 --1.3800e+03 1.4000e+03 4.8032e+00 --1.3800e+03 1.4286e+03 4.8792e+00 --1.3800e+03 1.4571e+03 4.9537e+00 --1.3800e+03 1.4857e+03 5.0268e+00 --1.3800e+03 1.5143e+03 5.0984e+00 --1.3800e+03 1.5429e+03 5.1685e+00 --1.3800e+03 1.5714e+03 5.2373e+00 --1.3800e+03 1.6000e+03 5.3046e+00 --1.3800e+03 1.6286e+03 5.3706e+00 --1.3800e+03 1.6571e+03 5.4352e+00 --1.3800e+03 1.6857e+03 5.4984e+00 --1.3800e+03 1.7143e+03 5.5604e+00 --1.3800e+03 1.7429e+03 5.6211e+00 --1.3800e+03 1.7714e+03 5.6805e+00 --1.3800e+03 1.8000e+03 5.7387e+00 --1.3800e+03 1.8286e+03 5.7957e+00 --1.3800e+03 1.8571e+03 5.8514e+00 --1.3800e+03 1.8857e+03 5.9061e+00 --1.3800e+03 1.9143e+03 5.9595e+00 --1.3800e+03 1.9429e+03 6.0119e+00 --1.3800e+03 1.9714e+03 6.0632e+00 --1.3800e+03 2.0000e+03 6.1134e+00 --1.3500e+03 -2.0000e+03 6.0773e+00 --1.3500e+03 -1.9714e+03 6.0258e+00 --1.3500e+03 -1.9429e+03 5.9732e+00 --1.3500e+03 -1.9143e+03 5.9194e+00 --1.3500e+03 -1.8857e+03 5.8644e+00 --1.3500e+03 -1.8571e+03 5.8082e+00 --1.3500e+03 -1.8286e+03 5.7508e+00 --1.3500e+03 -1.8000e+03 5.6922e+00 --1.3500e+03 -1.7714e+03 5.6322e+00 --1.3500e+03 -1.7429e+03 5.5709e+00 --1.3500e+03 -1.7143e+03 5.5083e+00 --1.3500e+03 -1.6857e+03 5.4443e+00 --1.3500e+03 -1.6571e+03 5.3789e+00 --1.3500e+03 -1.6286e+03 5.3121e+00 --1.3500e+03 -1.6000e+03 5.2438e+00 --1.3500e+03 -1.5714e+03 5.1741e+00 --1.3500e+03 -1.5429e+03 5.1028e+00 --1.3500e+03 -1.5143e+03 5.0300e+00 --1.3500e+03 -1.4857e+03 4.9556e+00 --1.3500e+03 -1.4571e+03 4.8797e+00 --1.3500e+03 -1.4286e+03 4.8022e+00 --1.3500e+03 -1.4000e+03 4.7231e+00 --1.3500e+03 -1.3714e+03 4.6423e+00 --1.3500e+03 -1.3429e+03 4.5599e+00 --1.3500e+03 -1.3143e+03 4.4758e+00 --1.3500e+03 -1.2857e+03 4.3902e+00 --1.3500e+03 -1.2571e+03 4.3028e+00 --1.3500e+03 -1.2286e+03 4.2139e+00 --1.3500e+03 -1.2000e+03 4.1233e+00 --1.3500e+03 -1.1714e+03 4.0310e+00 --1.3500e+03 -1.1429e+03 3.9372e+00 --1.3500e+03 -1.1143e+03 3.8419e+00 --1.3500e+03 -1.0857e+03 3.7450e+00 --1.3500e+03 -1.0571e+03 3.6467e+00 --1.3500e+03 -1.0286e+03 3.5469e+00 --1.3500e+03 -1.0000e+03 3.4459e+00 --1.3500e+03 -9.7143e+02 3.3435e+00 --1.3500e+03 -9.4286e+02 3.2400e+00 --1.3500e+03 -9.1429e+02 3.1355e+00 --1.3500e+03 -8.8571e+02 3.0300e+00 --1.3500e+03 -8.5714e+02 2.9237e+00 --1.3500e+03 -8.2857e+02 2.8167e+00 --1.3500e+03 -8.0000e+02 2.7093e+00 --1.3500e+03 -7.7143e+02 2.6016e+00 --1.3500e+03 -7.4286e+02 2.4937e+00 --1.3500e+03 -7.1429e+02 2.3860e+00 --1.3500e+03 -6.8571e+02 2.2786e+00 --1.3500e+03 -6.5714e+02 2.1718e+00 --1.3500e+03 -6.2857e+02 2.0660e+00 --1.3500e+03 -6.0000e+02 1.9612e+00 --1.3500e+03 -5.7143e+02 1.8580e+00 --1.3500e+03 -5.4286e+02 1.7566e+00 --1.3500e+03 -5.1429e+02 1.6572e+00 --1.3500e+03 -4.8571e+02 1.5604e+00 --1.3500e+03 -4.5714e+02 1.4664e+00 --1.3500e+03 -4.2857e+02 1.3756e+00 --1.3500e+03 -4.0000e+02 1.2884e+00 --1.3500e+03 -3.7143e+02 1.2051e+00 --1.3500e+03 -3.4286e+02 1.1262e+00 --1.3500e+03 -3.1429e+02 1.0520e+00 --1.3500e+03 -2.8571e+02 9.8277e-01 --1.3500e+03 -2.5714e+02 9.1898e-01 --1.3500e+03 -2.2857e+02 8.6092e-01 --1.3500e+03 -2.0000e+02 8.0890e-01 --1.3500e+03 -1.7143e+02 7.6319e-01 --1.3500e+03 -1.4286e+02 7.2407e-01 --1.3500e+03 -1.1429e+02 6.9174e-01 --1.3500e+03 -8.5714e+01 6.6640e-01 --1.3500e+03 -5.7143e+01 6.4819e-01 --1.3500e+03 -2.8571e+01 6.3722e-01 --1.3500e+03 0.0000e+00 6.3355e-01 --1.3500e+03 2.8571e+01 6.3722e-01 --1.3500e+03 5.7143e+01 6.4819e-01 --1.3500e+03 8.5714e+01 6.6640e-01 --1.3500e+03 1.1429e+02 6.9174e-01 --1.3500e+03 1.4286e+02 7.2407e-01 --1.3500e+03 1.7143e+02 7.6319e-01 --1.3500e+03 2.0000e+02 8.0890e-01 --1.3500e+03 2.2857e+02 8.6092e-01 --1.3500e+03 2.5714e+02 9.1898e-01 --1.3500e+03 2.8571e+02 9.8277e-01 --1.3500e+03 3.1429e+02 1.0520e+00 --1.3500e+03 3.4286e+02 1.1262e+00 --1.3500e+03 3.7143e+02 1.2051e+00 --1.3500e+03 4.0000e+02 1.2884e+00 --1.3500e+03 4.2857e+02 1.3756e+00 --1.3500e+03 4.5714e+02 1.4664e+00 --1.3500e+03 4.8571e+02 1.5604e+00 --1.3500e+03 5.1429e+02 1.6572e+00 --1.3500e+03 5.4286e+02 1.7566e+00 --1.3500e+03 5.7143e+02 1.8580e+00 --1.3500e+03 6.0000e+02 1.9612e+00 --1.3500e+03 6.2857e+02 2.0660e+00 --1.3500e+03 6.5714e+02 2.1718e+00 --1.3500e+03 6.8571e+02 2.2786e+00 --1.3500e+03 7.1429e+02 2.3860e+00 --1.3500e+03 7.4286e+02 2.4937e+00 --1.3500e+03 7.7143e+02 2.6016e+00 --1.3500e+03 8.0000e+02 2.7093e+00 --1.3500e+03 8.2857e+02 2.8167e+00 --1.3500e+03 8.5714e+02 2.9237e+00 --1.3500e+03 8.8571e+02 3.0300e+00 --1.3500e+03 9.1429e+02 3.1355e+00 --1.3500e+03 9.4286e+02 3.2400e+00 --1.3500e+03 9.7143e+02 3.3435e+00 --1.3500e+03 1.0000e+03 3.4459e+00 --1.3500e+03 1.0286e+03 3.5469e+00 --1.3500e+03 1.0571e+03 3.6467e+00 --1.3500e+03 1.0857e+03 3.7450e+00 --1.3500e+03 1.1143e+03 3.8419e+00 --1.3500e+03 1.1429e+03 3.9372e+00 --1.3500e+03 1.1714e+03 4.0310e+00 --1.3500e+03 1.2000e+03 4.1233e+00 --1.3500e+03 1.2286e+03 4.2139e+00 --1.3500e+03 1.2571e+03 4.3028e+00 --1.3500e+03 1.2857e+03 4.3902e+00 --1.3500e+03 1.3143e+03 4.4758e+00 --1.3500e+03 1.3429e+03 4.5599e+00 --1.3500e+03 1.3714e+03 4.6423e+00 --1.3500e+03 1.4000e+03 4.7231e+00 --1.3500e+03 1.4286e+03 4.8022e+00 --1.3500e+03 1.4571e+03 4.8797e+00 --1.3500e+03 1.4857e+03 4.9556e+00 --1.3500e+03 1.5143e+03 5.0300e+00 --1.3500e+03 1.5429e+03 5.1028e+00 --1.3500e+03 1.5714e+03 5.1741e+00 --1.3500e+03 1.6000e+03 5.2438e+00 --1.3500e+03 1.6286e+03 5.3121e+00 --1.3500e+03 1.6571e+03 5.3789e+00 --1.3500e+03 1.6857e+03 5.4443e+00 --1.3500e+03 1.7143e+03 5.5083e+00 --1.3500e+03 1.7429e+03 5.5709e+00 --1.3500e+03 1.7714e+03 5.6322e+00 --1.3500e+03 1.8000e+03 5.6922e+00 --1.3500e+03 1.8286e+03 5.7508e+00 --1.3500e+03 1.8571e+03 5.8082e+00 --1.3500e+03 1.8857e+03 5.8644e+00 --1.3500e+03 1.9143e+03 5.9194e+00 --1.3500e+03 1.9429e+03 5.9732e+00 --1.3500e+03 1.9714e+03 6.0258e+00 --1.3500e+03 2.0000e+03 6.0773e+00 --1.3200e+03 -2.0000e+03 6.0412e+00 --1.3200e+03 -1.9714e+03 5.9883e+00 --1.3200e+03 -1.9429e+03 5.9343e+00 --1.3200e+03 -1.9143e+03 5.8790e+00 --1.3200e+03 -1.8857e+03 5.8225e+00 --1.3200e+03 -1.8571e+03 5.7648e+00 --1.3200e+03 -1.8286e+03 5.7057e+00 --1.3200e+03 -1.8000e+03 5.6453e+00 --1.3200e+03 -1.7714e+03 5.5835e+00 --1.3200e+03 -1.7429e+03 5.5204e+00 --1.3200e+03 -1.7143e+03 5.4557e+00 --1.3200e+03 -1.6857e+03 5.3897e+00 --1.3200e+03 -1.6571e+03 5.3221e+00 --1.3200e+03 -1.6286e+03 5.2530e+00 --1.3200e+03 -1.6000e+03 5.1823e+00 --1.3200e+03 -1.5714e+03 5.1100e+00 --1.3200e+03 -1.5429e+03 5.0362e+00 --1.3200e+03 -1.5143e+03 4.9606e+00 --1.3200e+03 -1.4857e+03 4.8834e+00 --1.3200e+03 -1.4571e+03 4.8044e+00 --1.3200e+03 -1.4286e+03 4.7238e+00 --1.3200e+03 -1.4000e+03 4.6413e+00 --1.3200e+03 -1.3714e+03 4.5571e+00 --1.3200e+03 -1.3429e+03 4.4711e+00 --1.3200e+03 -1.3143e+03 4.3833e+00 --1.3200e+03 -1.2857e+03 4.2937e+00 --1.3200e+03 -1.2571e+03 4.2022e+00 --1.3200e+03 -1.2286e+03 4.1089e+00 --1.3200e+03 -1.2000e+03 4.0138e+00 --1.3200e+03 -1.1714e+03 3.9169e+00 --1.3200e+03 -1.1429e+03 3.8182e+00 --1.3200e+03 -1.1143e+03 3.7177e+00 --1.3200e+03 -1.0857e+03 3.6155e+00 --1.3200e+03 -1.0571e+03 3.5117e+00 --1.3200e+03 -1.0286e+03 3.4061e+00 --1.3200e+03 -1.0000e+03 3.2991e+00 --1.3200e+03 -9.7143e+02 3.1905e+00 --1.3200e+03 -9.4286e+02 3.0805e+00 --1.3200e+03 -9.1429e+02 2.9693e+00 --1.3200e+03 -8.8571e+02 2.8569e+00 --1.3200e+03 -8.5714e+02 2.7434e+00 --1.3200e+03 -8.2857e+02 2.6291e+00 --1.3200e+03 -8.0000e+02 2.5141e+00 --1.3200e+03 -7.7143e+02 2.3986e+00 --1.3200e+03 -7.4286e+02 2.2828e+00 --1.3200e+03 -7.1429e+02 2.1669e+00 --1.3200e+03 -6.8571e+02 2.0513e+00 --1.3200e+03 -6.5714e+02 1.9361e+00 --1.3200e+03 -6.2857e+02 1.8216e+00 --1.3200e+03 -6.0000e+02 1.7083e+00 --1.3200e+03 -5.7143e+02 1.5964e+00 --1.3200e+03 -5.4286e+02 1.4862e+00 --1.3200e+03 -5.1429e+02 1.3782e+00 --1.3200e+03 -4.8571e+02 1.2728e+00 --1.3200e+03 -4.5714e+02 1.1702e+00 --1.3200e+03 -4.2857e+02 1.0711e+00 --1.3200e+03 -4.0000e+02 9.7570e-01 --1.3200e+03 -3.7143e+02 8.8452e-01 --1.3200e+03 -3.4286e+02 7.9797e-01 --1.3200e+03 -3.1429e+02 7.1647e-01 --1.3200e+03 -2.8571e+02 6.4043e-01 --1.3200e+03 -2.5714e+02 5.7025e-01 --1.3200e+03 -2.2857e+02 5.0631e-01 --1.3200e+03 -2.0000e+02 4.4897e-01 --1.3200e+03 -1.7143e+02 3.9856e-01 --1.3200e+03 -1.4286e+02 3.5538e-01 --1.3200e+03 -1.1429e+02 3.1968e-01 --1.3200e+03 -8.5714e+01 2.9168e-01 --1.3200e+03 -5.7143e+01 2.7155e-01 --1.3200e+03 -2.8571e+01 2.5942e-01 --1.3200e+03 0.0000e+00 2.5537e-01 --1.3200e+03 2.8571e+01 2.5942e-01 --1.3200e+03 5.7143e+01 2.7155e-01 --1.3200e+03 8.5714e+01 2.9168e-01 --1.3200e+03 1.1429e+02 3.1968e-01 --1.3200e+03 1.4286e+02 3.5538e-01 --1.3200e+03 1.7143e+02 3.9856e-01 --1.3200e+03 2.0000e+02 4.4897e-01 --1.3200e+03 2.2857e+02 5.0631e-01 --1.3200e+03 2.5714e+02 5.7025e-01 --1.3200e+03 2.8571e+02 6.4043e-01 --1.3200e+03 3.1429e+02 7.1647e-01 --1.3200e+03 3.4286e+02 7.9797e-01 --1.3200e+03 3.7143e+02 8.8452e-01 --1.3200e+03 4.0000e+02 9.7570e-01 --1.3200e+03 4.2857e+02 1.0711e+00 --1.3200e+03 4.5714e+02 1.1702e+00 --1.3200e+03 4.8571e+02 1.2728e+00 --1.3200e+03 5.1429e+02 1.3782e+00 --1.3200e+03 5.4286e+02 1.4862e+00 --1.3200e+03 5.7143e+02 1.5964e+00 --1.3200e+03 6.0000e+02 1.7083e+00 --1.3200e+03 6.2857e+02 1.8216e+00 --1.3200e+03 6.5714e+02 1.9361e+00 --1.3200e+03 6.8571e+02 2.0513e+00 --1.3200e+03 7.1429e+02 2.1669e+00 --1.3200e+03 7.4286e+02 2.2828e+00 --1.3200e+03 7.7143e+02 2.3986e+00 --1.3200e+03 8.0000e+02 2.5141e+00 --1.3200e+03 8.2857e+02 2.6291e+00 --1.3200e+03 8.5714e+02 2.7434e+00 --1.3200e+03 8.8571e+02 2.8569e+00 --1.3200e+03 9.1429e+02 2.9693e+00 --1.3200e+03 9.4286e+02 3.0805e+00 --1.3200e+03 9.7143e+02 3.1905e+00 --1.3200e+03 1.0000e+03 3.2991e+00 --1.3200e+03 1.0286e+03 3.4061e+00 --1.3200e+03 1.0571e+03 3.5117e+00 --1.3200e+03 1.0857e+03 3.6155e+00 --1.3200e+03 1.1143e+03 3.7177e+00 --1.3200e+03 1.1429e+03 3.8182e+00 --1.3200e+03 1.1714e+03 3.9169e+00 --1.3200e+03 1.2000e+03 4.0138e+00 --1.3200e+03 1.2286e+03 4.1089e+00 --1.3200e+03 1.2571e+03 4.2022e+00 --1.3200e+03 1.2857e+03 4.2937e+00 --1.3200e+03 1.3143e+03 4.3833e+00 --1.3200e+03 1.3429e+03 4.4711e+00 --1.3200e+03 1.3714e+03 4.5571e+00 --1.3200e+03 1.4000e+03 4.6413e+00 --1.3200e+03 1.4286e+03 4.7238e+00 --1.3200e+03 1.4571e+03 4.8044e+00 --1.3200e+03 1.4857e+03 4.8834e+00 --1.3200e+03 1.5143e+03 4.9606e+00 --1.3200e+03 1.5429e+03 5.0362e+00 --1.3200e+03 1.5714e+03 5.1100e+00 --1.3200e+03 1.6000e+03 5.1823e+00 --1.3200e+03 1.6286e+03 5.2530e+00 --1.3200e+03 1.6571e+03 5.3221e+00 --1.3200e+03 1.6857e+03 5.3897e+00 --1.3200e+03 1.7143e+03 5.4557e+00 --1.3200e+03 1.7429e+03 5.5204e+00 --1.3200e+03 1.7714e+03 5.5835e+00 --1.3200e+03 1.8000e+03 5.6453e+00 --1.3200e+03 1.8286e+03 5.7057e+00 --1.3200e+03 1.8571e+03 5.7648e+00 --1.3200e+03 1.8857e+03 5.8225e+00 --1.3200e+03 1.9143e+03 5.8790e+00 --1.3200e+03 1.9429e+03 5.9343e+00 --1.3200e+03 1.9714e+03 5.9883e+00 --1.3200e+03 2.0000e+03 6.0412e+00 --1.2900e+03 -2.0000e+03 6.0049e+00 --1.2900e+03 -1.9714e+03 5.9507e+00 --1.2900e+03 -1.9429e+03 5.8952e+00 --1.2900e+03 -1.9143e+03 5.8385e+00 --1.2900e+03 -1.8857e+03 5.7805e+00 --1.2900e+03 -1.8571e+03 5.7211e+00 --1.2900e+03 -1.8286e+03 5.6603e+00 --1.2900e+03 -1.8000e+03 5.5981e+00 --1.2900e+03 -1.7714e+03 5.5345e+00 --1.2900e+03 -1.7429e+03 5.4693e+00 --1.2900e+03 -1.7143e+03 5.4027e+00 --1.2900e+03 -1.6857e+03 5.3345e+00 --1.2900e+03 -1.6571e+03 5.2646e+00 --1.2900e+03 -1.6286e+03 5.1932e+00 --1.2900e+03 -1.6000e+03 5.1200e+00 --1.2900e+03 -1.5714e+03 5.0452e+00 --1.2900e+03 -1.5429e+03 4.9686e+00 --1.2900e+03 -1.5143e+03 4.8902e+00 --1.2900e+03 -1.4857e+03 4.8100e+00 --1.2900e+03 -1.4571e+03 4.7279e+00 --1.2900e+03 -1.4286e+03 4.6440e+00 --1.2900e+03 -1.4000e+03 4.5581e+00 --1.2900e+03 -1.3714e+03 4.4703e+00 --1.2900e+03 -1.3429e+03 4.3805e+00 --1.2900e+03 -1.3143e+03 4.2887e+00 --1.2900e+03 -1.2857e+03 4.1949e+00 --1.2900e+03 -1.2571e+03 4.0991e+00 --1.2900e+03 -1.2286e+03 4.0013e+00 --1.2900e+03 -1.2000e+03 3.9014e+00 --1.2900e+03 -1.1714e+03 3.7996e+00 --1.2900e+03 -1.1429e+03 3.6957e+00 --1.2900e+03 -1.1143e+03 3.5898e+00 --1.2900e+03 -1.0857e+03 3.4819e+00 --1.2900e+03 -1.0571e+03 3.3721e+00 --1.2900e+03 -1.0286e+03 3.2604e+00 --1.2900e+03 -1.0000e+03 3.1469e+00 --1.2900e+03 -9.7143e+02 3.0316e+00 --1.2900e+03 -9.4286e+02 2.9147e+00 --1.2900e+03 -9.1429e+02 2.7962e+00 --1.2900e+03 -8.8571e+02 2.6764e+00 --1.2900e+03 -8.5714e+02 2.5552e+00 --1.2900e+03 -8.2857e+02 2.4329e+00 --1.2900e+03 -8.0000e+02 2.3096e+00 --1.2900e+03 -7.7143e+02 2.1856e+00 --1.2900e+03 -7.4286e+02 2.0611e+00 --1.2900e+03 -7.1429e+02 1.9363e+00 --1.2900e+03 -6.8571e+02 1.8115e+00 --1.2900e+03 -6.5714e+02 1.6870e+00 --1.2900e+03 -6.2857e+02 1.5631e+00 --1.2900e+03 -6.0000e+02 1.4402e+00 --1.2900e+03 -5.7143e+02 1.3186e+00 --1.2900e+03 -5.4286e+02 1.1988e+00 --1.2900e+03 -5.1429e+02 1.0811e+00 --1.2900e+03 -4.8571e+02 9.6596e-01 --1.2900e+03 -4.5714e+02 8.5389e-01 --1.2900e+03 -4.2857e+02 7.4532e-01 --1.2900e+03 -4.0000e+02 6.4073e-01 --1.2900e+03 -3.7143e+02 5.4061e-01 --1.2900e+03 -3.4286e+02 4.4544e-01 --1.2900e+03 -3.1429e+02 3.5570e-01 --1.2900e+03 -2.8571e+02 2.7188e-01 --1.2900e+03 -2.5714e+02 1.9443e-01 --1.2900e+03 -2.2857e+02 1.2380e-01 --1.2900e+03 -2.0000e+02 6.0403e-02 --1.2900e+03 -1.7143e+02 4.6172e-03 --1.2900e+03 -1.4286e+02 -4.3208e-02 --1.2900e+03 -1.1429e+02 -8.2769e-02 --1.2900e+03 -8.5714e+01 -1.1381e-01 --1.2900e+03 -5.7143e+01 -1.3613e-01 --1.2900e+03 -2.8571e+01 -1.4959e-01 --1.2900e+03 0.0000e+00 -1.5408e-01 --1.2900e+03 2.8571e+01 -1.4959e-01 --1.2900e+03 5.7143e+01 -1.3613e-01 --1.2900e+03 8.5714e+01 -1.1381e-01 --1.2900e+03 1.1429e+02 -8.2769e-02 --1.2900e+03 1.4286e+02 -4.3208e-02 --1.2900e+03 1.7143e+02 4.6172e-03 --1.2900e+03 2.0000e+02 6.0403e-02 --1.2900e+03 2.2857e+02 1.2380e-01 --1.2900e+03 2.5714e+02 1.9443e-01 --1.2900e+03 2.8571e+02 2.7188e-01 --1.2900e+03 3.1429e+02 3.5570e-01 --1.2900e+03 3.4286e+02 4.4544e-01 --1.2900e+03 3.7143e+02 5.4061e-01 --1.2900e+03 4.0000e+02 6.4073e-01 --1.2900e+03 4.2857e+02 7.4532e-01 --1.2900e+03 4.5714e+02 8.5389e-01 --1.2900e+03 4.8571e+02 9.6596e-01 --1.2900e+03 5.1429e+02 1.0811e+00 --1.2900e+03 5.4286e+02 1.1988e+00 --1.2900e+03 5.7143e+02 1.3186e+00 --1.2900e+03 6.0000e+02 1.4402e+00 --1.2900e+03 6.2857e+02 1.5631e+00 --1.2900e+03 6.5714e+02 1.6870e+00 --1.2900e+03 6.8571e+02 1.8115e+00 --1.2900e+03 7.1429e+02 1.9363e+00 --1.2900e+03 7.4286e+02 2.0611e+00 --1.2900e+03 7.7143e+02 2.1856e+00 --1.2900e+03 8.0000e+02 2.3096e+00 --1.2900e+03 8.2857e+02 2.4329e+00 --1.2900e+03 8.5714e+02 2.5552e+00 --1.2900e+03 8.8571e+02 2.6764e+00 --1.2900e+03 9.1429e+02 2.7962e+00 --1.2900e+03 9.4286e+02 2.9147e+00 --1.2900e+03 9.7143e+02 3.0316e+00 --1.2900e+03 1.0000e+03 3.1469e+00 --1.2900e+03 1.0286e+03 3.2604e+00 --1.2900e+03 1.0571e+03 3.3721e+00 --1.2900e+03 1.0857e+03 3.4819e+00 --1.2900e+03 1.1143e+03 3.5898e+00 --1.2900e+03 1.1429e+03 3.6957e+00 --1.2900e+03 1.1714e+03 3.7996e+00 --1.2900e+03 1.2000e+03 3.9014e+00 --1.2900e+03 1.2286e+03 4.0013e+00 --1.2900e+03 1.2571e+03 4.0991e+00 --1.2900e+03 1.2857e+03 4.1949e+00 --1.2900e+03 1.3143e+03 4.2887e+00 --1.2900e+03 1.3429e+03 4.3805e+00 --1.2900e+03 1.3714e+03 4.4703e+00 --1.2900e+03 1.4000e+03 4.5581e+00 --1.2900e+03 1.4286e+03 4.6440e+00 --1.2900e+03 1.4571e+03 4.7279e+00 --1.2900e+03 1.4857e+03 4.8100e+00 --1.2900e+03 1.5143e+03 4.8902e+00 --1.2900e+03 1.5429e+03 4.9686e+00 --1.2900e+03 1.5714e+03 5.0452e+00 --1.2900e+03 1.6000e+03 5.1200e+00 --1.2900e+03 1.6286e+03 5.1932e+00 --1.2900e+03 1.6571e+03 5.2646e+00 --1.2900e+03 1.6857e+03 5.3345e+00 --1.2900e+03 1.7143e+03 5.4027e+00 --1.2900e+03 1.7429e+03 5.4693e+00 --1.2900e+03 1.7714e+03 5.5345e+00 --1.2900e+03 1.8000e+03 5.5981e+00 --1.2900e+03 1.8286e+03 5.6603e+00 --1.2900e+03 1.8571e+03 5.7211e+00 --1.2900e+03 1.8857e+03 5.7805e+00 --1.2900e+03 1.9143e+03 5.8385e+00 --1.2900e+03 1.9429e+03 5.8952e+00 --1.2900e+03 1.9714e+03 5.9507e+00 --1.2900e+03 2.0000e+03 6.0049e+00 --1.2600e+03 -2.0000e+03 5.9686e+00 --1.2600e+03 -1.9714e+03 5.9130e+00 --1.2600e+03 -1.9429e+03 5.8561e+00 --1.2600e+03 -1.9143e+03 5.7978e+00 --1.2600e+03 -1.8857e+03 5.7382e+00 --1.2600e+03 -1.8571e+03 5.6772e+00 --1.2600e+03 -1.8286e+03 5.6146e+00 --1.2600e+03 -1.8000e+03 5.5506e+00 --1.2600e+03 -1.7714e+03 5.4851e+00 --1.2600e+03 -1.7429e+03 5.4180e+00 --1.2600e+03 -1.7143e+03 5.3492e+00 --1.2600e+03 -1.6857e+03 5.2788e+00 --1.2600e+03 -1.6571e+03 5.2066e+00 --1.2600e+03 -1.6286e+03 5.1327e+00 --1.2600e+03 -1.6000e+03 5.0570e+00 --1.2600e+03 -1.5714e+03 4.9795e+00 --1.2600e+03 -1.5429e+03 4.9001e+00 --1.2600e+03 -1.5143e+03 4.8188e+00 --1.2600e+03 -1.4857e+03 4.7355e+00 --1.2600e+03 -1.4571e+03 4.6501e+00 --1.2600e+03 -1.4286e+03 4.5628e+00 --1.2600e+03 -1.4000e+03 4.4733e+00 --1.2600e+03 -1.3714e+03 4.3817e+00 --1.2600e+03 -1.3429e+03 4.2880e+00 --1.2600e+03 -1.3143e+03 4.1921e+00 --1.2600e+03 -1.2857e+03 4.0939e+00 --1.2600e+03 -1.2571e+03 3.9936e+00 --1.2600e+03 -1.2286e+03 3.8910e+00 --1.2600e+03 -1.2000e+03 3.7861e+00 --1.2600e+03 -1.1714e+03 3.6789e+00 --1.2600e+03 -1.1429e+03 3.5695e+00 --1.2600e+03 -1.1143e+03 3.4578e+00 --1.2600e+03 -1.0857e+03 3.3439e+00 --1.2600e+03 -1.0571e+03 3.2278e+00 --1.2600e+03 -1.0286e+03 3.1095e+00 --1.2600e+03 -1.0000e+03 2.9891e+00 --1.2600e+03 -9.7143e+02 2.8666e+00 --1.2600e+03 -9.4286e+02 2.7422e+00 --1.2600e+03 -9.1429e+02 2.6160e+00 --1.2600e+03 -8.8571e+02 2.4880e+00 --1.2600e+03 -8.5714e+02 2.3584e+00 --1.2600e+03 -8.2857e+02 2.2274e+00 --1.2600e+03 -8.0000e+02 2.0951e+00 --1.2600e+03 -7.7143e+02 1.9619e+00 --1.2600e+03 -7.4286e+02 1.8278e+00 --1.2600e+03 -7.1429e+02 1.6931e+00 --1.2600e+03 -6.8571e+02 1.5583e+00 --1.2600e+03 -6.5714e+02 1.4235e+00 --1.2600e+03 -6.2857e+02 1.2892e+00 --1.2600e+03 -6.0000e+02 1.1556e+00 --1.2600e+03 -5.7143e+02 1.0233e+00 --1.2600e+03 -5.4286e+02 8.9262e-01 --1.2600e+03 -5.1429e+02 7.6407e-01 --1.2600e+03 -4.8571e+02 6.3812e-01 --1.2600e+03 -4.5714e+02 5.1529e-01 --1.2600e+03 -4.2857e+02 3.9610e-01 --1.2600e+03 -4.0000e+02 2.8110e-01 --1.2600e+03 -3.7143e+02 1.7084e-01 --1.2600e+03 -3.4286e+02 6.5885e-02 --1.2600e+03 -3.1429e+02 -3.3215e-02 --1.2600e+03 -2.8571e+02 -1.2591e-01 --1.2600e+03 -2.5714e+02 -2.1165e-01 --1.2600e+03 -2.2857e+02 -2.8994e-01 --1.2600e+03 -2.0000e+02 -3.6029e-01 --1.2600e+03 -1.7143e+02 -4.2224e-01 --1.2600e+03 -1.4286e+02 -4.7540e-01 --1.2600e+03 -1.1429e+02 -5.1940e-01 --1.2600e+03 -8.5714e+01 -5.5394e-01 --1.2600e+03 -5.7143e+01 -5.7879e-01 --1.2600e+03 -2.8571e+01 -5.9377e-01 --1.2600e+03 0.0000e+00 -5.9877e-01 --1.2600e+03 2.8571e+01 -5.9377e-01 --1.2600e+03 5.7143e+01 -5.7879e-01 --1.2600e+03 8.5714e+01 -5.5394e-01 --1.2600e+03 1.1429e+02 -5.1940e-01 --1.2600e+03 1.4286e+02 -4.7540e-01 --1.2600e+03 1.7143e+02 -4.2224e-01 --1.2600e+03 2.0000e+02 -3.6029e-01 --1.2600e+03 2.2857e+02 -2.8994e-01 --1.2600e+03 2.5714e+02 -2.1165e-01 --1.2600e+03 2.8571e+02 -1.2591e-01 --1.2600e+03 3.1429e+02 -3.3215e-02 --1.2600e+03 3.4286e+02 6.5885e-02 --1.2600e+03 3.7143e+02 1.7084e-01 --1.2600e+03 4.0000e+02 2.8110e-01 --1.2600e+03 4.2857e+02 3.9610e-01 --1.2600e+03 4.5714e+02 5.1529e-01 --1.2600e+03 4.8571e+02 6.3812e-01 --1.2600e+03 5.1429e+02 7.6407e-01 --1.2600e+03 5.4286e+02 8.9262e-01 --1.2600e+03 5.7143e+02 1.0233e+00 --1.2600e+03 6.0000e+02 1.1556e+00 --1.2600e+03 6.2857e+02 1.2892e+00 --1.2600e+03 6.5714e+02 1.4235e+00 --1.2600e+03 6.8571e+02 1.5583e+00 --1.2600e+03 7.1429e+02 1.6931e+00 --1.2600e+03 7.4286e+02 1.8278e+00 --1.2600e+03 7.7143e+02 1.9619e+00 --1.2600e+03 8.0000e+02 2.0951e+00 --1.2600e+03 8.2857e+02 2.2274e+00 --1.2600e+03 8.5714e+02 2.3584e+00 --1.2600e+03 8.8571e+02 2.4880e+00 --1.2600e+03 9.1429e+02 2.6160e+00 --1.2600e+03 9.4286e+02 2.7422e+00 --1.2600e+03 9.7143e+02 2.8666e+00 --1.2600e+03 1.0000e+03 2.9891e+00 --1.2600e+03 1.0286e+03 3.1095e+00 --1.2600e+03 1.0571e+03 3.2278e+00 --1.2600e+03 1.0857e+03 3.3439e+00 --1.2600e+03 1.1143e+03 3.4578e+00 --1.2600e+03 1.1429e+03 3.5695e+00 --1.2600e+03 1.1714e+03 3.6789e+00 --1.2600e+03 1.2000e+03 3.7861e+00 --1.2600e+03 1.2286e+03 3.8910e+00 --1.2600e+03 1.2571e+03 3.9936e+00 --1.2600e+03 1.2857e+03 4.0939e+00 --1.2600e+03 1.3143e+03 4.1921e+00 --1.2600e+03 1.3429e+03 4.2880e+00 --1.2600e+03 1.3714e+03 4.3817e+00 --1.2600e+03 1.4000e+03 4.4733e+00 --1.2600e+03 1.4286e+03 4.5628e+00 --1.2600e+03 1.4571e+03 4.6501e+00 --1.2600e+03 1.4857e+03 4.7355e+00 --1.2600e+03 1.5143e+03 4.8188e+00 --1.2600e+03 1.5429e+03 4.9001e+00 --1.2600e+03 1.5714e+03 4.9795e+00 --1.2600e+03 1.6000e+03 5.0570e+00 --1.2600e+03 1.6286e+03 5.1327e+00 --1.2600e+03 1.6571e+03 5.2066e+00 --1.2600e+03 1.6857e+03 5.2788e+00 --1.2600e+03 1.7143e+03 5.3492e+00 --1.2600e+03 1.7429e+03 5.4180e+00 --1.2600e+03 1.7714e+03 5.4851e+00 --1.2600e+03 1.8000e+03 5.5506e+00 --1.2600e+03 1.8286e+03 5.6146e+00 --1.2600e+03 1.8571e+03 5.6772e+00 --1.2600e+03 1.8857e+03 5.7382e+00 --1.2600e+03 1.9143e+03 5.7978e+00 --1.2600e+03 1.9429e+03 5.8561e+00 --1.2600e+03 1.9714e+03 5.9130e+00 --1.2600e+03 2.0000e+03 5.9686e+00 --1.2300e+03 -2.0000e+03 5.9322e+00 --1.2300e+03 -1.9714e+03 5.8752e+00 --1.2300e+03 -1.9429e+03 5.8168e+00 --1.2300e+03 -1.9143e+03 5.7571e+00 --1.2300e+03 -1.8857e+03 5.6958e+00 --1.2300e+03 -1.8571e+03 5.6331e+00 --1.2300e+03 -1.8286e+03 5.5688e+00 --1.2300e+03 -1.8000e+03 5.5029e+00 --1.2300e+03 -1.7714e+03 5.4354e+00 --1.2300e+03 -1.7429e+03 5.3662e+00 --1.2300e+03 -1.7143e+03 5.2953e+00 --1.2300e+03 -1.6857e+03 5.2226e+00 --1.2300e+03 -1.6571e+03 5.1481e+00 --1.2300e+03 -1.6286e+03 5.0717e+00 --1.2300e+03 -1.6000e+03 4.9934e+00 --1.2300e+03 -1.5714e+03 4.9131e+00 --1.2300e+03 -1.5429e+03 4.8308e+00 --1.2300e+03 -1.5143e+03 4.7464e+00 --1.2300e+03 -1.4857e+03 4.6598e+00 --1.2300e+03 -1.4571e+03 4.5711e+00 --1.2300e+03 -1.4286e+03 4.4802e+00 --1.2300e+03 -1.4000e+03 4.3870e+00 --1.2300e+03 -1.3714e+03 4.2915e+00 --1.2300e+03 -1.3429e+03 4.1937e+00 --1.2300e+03 -1.3143e+03 4.0934e+00 --1.2300e+03 -1.2857e+03 3.9907e+00 --1.2300e+03 -1.2571e+03 3.8855e+00 --1.2300e+03 -1.2286e+03 3.7779e+00 --1.2300e+03 -1.2000e+03 3.6677e+00 --1.2300e+03 -1.1714e+03 3.5549e+00 --1.2300e+03 -1.1429e+03 3.4397e+00 --1.2300e+03 -1.1143e+03 3.3219e+00 --1.2300e+03 -1.0857e+03 3.2015e+00 --1.2300e+03 -1.0571e+03 3.0786e+00 --1.2300e+03 -1.0286e+03 2.9533e+00 --1.2300e+03 -1.0000e+03 2.8255e+00 --1.2300e+03 -9.7143e+02 2.6953e+00 --1.2300e+03 -9.4286e+02 2.5628e+00 --1.2300e+03 -9.1429e+02 2.4282e+00 --1.2300e+03 -8.8571e+02 2.2914e+00 --1.2300e+03 -8.5714e+02 2.1527e+00 --1.2300e+03 -8.2857e+02 2.0122e+00 --1.2300e+03 -8.0000e+02 1.8701e+00 --1.2300e+03 -7.7143e+02 1.7267e+00 --1.2300e+03 -7.4286e+02 1.5821e+00 --1.2300e+03 -7.1429e+02 1.4366e+00 --1.2300e+03 -6.8571e+02 1.2907e+00 --1.2300e+03 -6.5714e+02 1.1445e+00 --1.2300e+03 -6.2857e+02 9.9851e-01 --1.2300e+03 -6.0000e+02 8.5313e-01 --1.2300e+03 -5.7143e+02 7.0880e-01 --1.2300e+03 -5.4286e+02 5.6600e-01 --1.2300e+03 -5.1429e+02 4.2526e-01 --1.2300e+03 -4.8571e+02 2.8711e-01 --1.2300e+03 -4.5714e+02 1.5213e-01 --1.2300e+03 -4.2857e+02 2.0919e-02 --1.2300e+03 -4.0000e+02 -1.0590e-01 --1.2300e+03 -3.7143e+02 -2.2768e-01 --1.2300e+03 -3.4286e+02 -3.4380e-01 --1.2300e+03 -3.1429e+02 -4.5361e-01 --1.2300e+03 -2.8571e+02 -5.5646e-01 --1.2300e+03 -2.5714e+02 -6.5173e-01 --1.2300e+03 -2.2857e+02 -7.3882e-01 --1.2300e+03 -2.0000e+02 -8.1716e-01 --1.2300e+03 -1.7143e+02 -8.8622e-01 --1.2300e+03 -1.4286e+02 -9.4552e-01 --1.2300e+03 -1.1429e+02 -9.9465e-01 --1.2300e+03 -8.5714e+01 -1.0332e+00 --1.2300e+03 -5.7143e+01 -1.0610e+00 --1.2300e+03 -2.8571e+01 -1.0778e+00 --1.2300e+03 0.0000e+00 -1.0834e+00 --1.2300e+03 2.8571e+01 -1.0778e+00 --1.2300e+03 5.7143e+01 -1.0610e+00 --1.2300e+03 8.5714e+01 -1.0332e+00 --1.2300e+03 1.1429e+02 -9.9465e-01 --1.2300e+03 1.4286e+02 -9.4552e-01 --1.2300e+03 1.7143e+02 -8.8622e-01 --1.2300e+03 2.0000e+02 -8.1716e-01 --1.2300e+03 2.2857e+02 -7.3882e-01 --1.2300e+03 2.5714e+02 -6.5173e-01 --1.2300e+03 2.8571e+02 -5.5646e-01 --1.2300e+03 3.1429e+02 -4.5361e-01 --1.2300e+03 3.4286e+02 -3.4380e-01 --1.2300e+03 3.7143e+02 -2.2768e-01 --1.2300e+03 4.0000e+02 -1.0590e-01 --1.2300e+03 4.2857e+02 2.0919e-02 --1.2300e+03 4.5714e+02 1.5213e-01 --1.2300e+03 4.8571e+02 2.8711e-01 --1.2300e+03 5.1429e+02 4.2526e-01 --1.2300e+03 5.4286e+02 5.6600e-01 --1.2300e+03 5.7143e+02 7.0880e-01 --1.2300e+03 6.0000e+02 8.5313e-01 --1.2300e+03 6.2857e+02 9.9851e-01 --1.2300e+03 6.5714e+02 1.1445e+00 --1.2300e+03 6.8571e+02 1.2907e+00 --1.2300e+03 7.1429e+02 1.4366e+00 --1.2300e+03 7.4286e+02 1.5821e+00 --1.2300e+03 7.7143e+02 1.7267e+00 --1.2300e+03 8.0000e+02 1.8701e+00 --1.2300e+03 8.2857e+02 2.0122e+00 --1.2300e+03 8.5714e+02 2.1527e+00 --1.2300e+03 8.8571e+02 2.2914e+00 --1.2300e+03 9.1429e+02 2.4282e+00 --1.2300e+03 9.4286e+02 2.5628e+00 --1.2300e+03 9.7143e+02 2.6953e+00 --1.2300e+03 1.0000e+03 2.8255e+00 --1.2300e+03 1.0286e+03 2.9533e+00 --1.2300e+03 1.0571e+03 3.0786e+00 --1.2300e+03 1.0857e+03 3.2015e+00 --1.2300e+03 1.1143e+03 3.3219e+00 --1.2300e+03 1.1429e+03 3.4397e+00 --1.2300e+03 1.1714e+03 3.5549e+00 --1.2300e+03 1.2000e+03 3.6677e+00 --1.2300e+03 1.2286e+03 3.7779e+00 --1.2300e+03 1.2571e+03 3.8855e+00 --1.2300e+03 1.2857e+03 3.9907e+00 --1.2300e+03 1.3143e+03 4.0934e+00 --1.2300e+03 1.3429e+03 4.1937e+00 --1.2300e+03 1.3714e+03 4.2915e+00 --1.2300e+03 1.4000e+03 4.3870e+00 --1.2300e+03 1.4286e+03 4.4802e+00 --1.2300e+03 1.4571e+03 4.5711e+00 --1.2300e+03 1.4857e+03 4.6598e+00 --1.2300e+03 1.5143e+03 4.7464e+00 --1.2300e+03 1.5429e+03 4.8308e+00 --1.2300e+03 1.5714e+03 4.9131e+00 --1.2300e+03 1.6000e+03 4.9934e+00 --1.2300e+03 1.6286e+03 5.0717e+00 --1.2300e+03 1.6571e+03 5.1481e+00 --1.2300e+03 1.6857e+03 5.2226e+00 --1.2300e+03 1.7143e+03 5.2953e+00 --1.2300e+03 1.7429e+03 5.3662e+00 --1.2300e+03 1.7714e+03 5.4354e+00 --1.2300e+03 1.8000e+03 5.5029e+00 --1.2300e+03 1.8286e+03 5.5688e+00 --1.2300e+03 1.8571e+03 5.6331e+00 --1.2300e+03 1.8857e+03 5.6958e+00 --1.2300e+03 1.9143e+03 5.7571e+00 --1.2300e+03 1.9429e+03 5.8168e+00 --1.2300e+03 1.9714e+03 5.8752e+00 --1.2300e+03 2.0000e+03 5.9322e+00 --1.2000e+03 -2.0000e+03 5.8958e+00 --1.2000e+03 -1.9714e+03 5.8374e+00 --1.2000e+03 -1.9429e+03 5.7776e+00 --1.2000e+03 -1.9143e+03 5.7162e+00 --1.2000e+03 -1.8857e+03 5.6533e+00 --1.2000e+03 -1.8571e+03 5.5888e+00 --1.2000e+03 -1.8286e+03 5.5227e+00 --1.2000e+03 -1.8000e+03 5.4550e+00 --1.2000e+03 -1.7714e+03 5.3854e+00 --1.2000e+03 -1.7429e+03 5.3142e+00 --1.2000e+03 -1.7143e+03 5.2410e+00 --1.2000e+03 -1.6857e+03 5.1660e+00 --1.2000e+03 -1.6571e+03 5.0890e+00 --1.2000e+03 -1.6286e+03 5.0101e+00 --1.2000e+03 -1.6000e+03 4.9290e+00 --1.2000e+03 -1.5714e+03 4.8459e+00 --1.2000e+03 -1.5429e+03 4.7606e+00 --1.2000e+03 -1.5143e+03 4.6730e+00 --1.2000e+03 -1.4857e+03 4.5832e+00 --1.2000e+03 -1.4571e+03 4.4910e+00 --1.2000e+03 -1.4286e+03 4.3963e+00 --1.2000e+03 -1.4000e+03 4.2993e+00 --1.2000e+03 -1.3714e+03 4.1996e+00 --1.2000e+03 -1.3429e+03 4.0975e+00 --1.2000e+03 -1.3143e+03 3.9926e+00 --1.2000e+03 -1.2857e+03 3.8852e+00 --1.2000e+03 -1.2571e+03 3.7749e+00 --1.2000e+03 -1.2286e+03 3.6619e+00 --1.2000e+03 -1.2000e+03 3.5462e+00 --1.2000e+03 -1.1714e+03 3.4275e+00 --1.2000e+03 -1.1429e+03 3.3061e+00 --1.2000e+03 -1.1143e+03 3.1817e+00 --1.2000e+03 -1.0857e+03 3.0545e+00 --1.2000e+03 -1.0571e+03 2.9245e+00 --1.2000e+03 -1.0286e+03 2.7915e+00 --1.2000e+03 -1.0000e+03 2.6558e+00 --1.2000e+03 -9.7143e+02 2.5173e+00 --1.2000e+03 -9.4286e+02 2.3761e+00 --1.2000e+03 -9.1429e+02 2.2324e+00 --1.2000e+03 -8.8571e+02 2.0861e+00 --1.2000e+03 -8.5714e+02 1.9375e+00 --1.2000e+03 -8.2857e+02 1.7866e+00 --1.2000e+03 -8.0000e+02 1.6338e+00 --1.2000e+03 -7.7143e+02 1.4792e+00 --1.2000e+03 -7.4286e+02 1.3231e+00 --1.2000e+03 -7.1429e+02 1.1657e+00 --1.2000e+03 -6.8571e+02 1.0074e+00 --1.2000e+03 -6.5714e+02 8.4863e-01 --1.2000e+03 -6.2857e+02 6.8971e-01 --1.2000e+03 -6.0000e+02 5.3111e-01 --1.2000e+03 -5.7143e+02 3.7334e-01 --1.2000e+03 -5.4286e+02 2.1692e-01 --1.2000e+03 -5.1429e+02 6.2437e-02 --1.2000e+03 -4.8571e+02 -8.9501e-02 --1.2000e+03 -4.5714e+02 -2.3824e-01 --1.2000e+03 -4.2857e+02 -3.8310e-01 --1.2000e+03 -4.0000e+02 -5.2338e-01 --1.2000e+03 -3.7143e+02 -6.5834e-01 --1.2000e+03 -3.4286e+02 -7.8723e-01 --1.2000e+03 -3.1429e+02 -9.0932e-01 --1.2000e+03 -2.8571e+02 -1.0239e+00 --1.2000e+03 -2.5714e+02 -1.1301e+00 --1.2000e+03 -2.2857e+02 -1.2274e+00 --1.2000e+03 -2.0000e+02 -1.3149e+00 --1.2000e+03 -1.7143e+02 -1.3922e+00 --1.2000e+03 -1.4286e+02 -1.4587e+00 --1.2000e+03 -1.1429e+02 -1.5138e+00 --1.2000e+03 -8.5714e+01 -1.5571e+00 --1.2000e+03 -5.7143e+01 -1.5882e+00 --1.2000e+03 -2.8571e+01 -1.6070e+00 --1.2000e+03 0.0000e+00 -1.6133e+00 --1.2000e+03 2.8571e+01 -1.6070e+00 --1.2000e+03 5.7143e+01 -1.5882e+00 --1.2000e+03 8.5714e+01 -1.5571e+00 --1.2000e+03 1.1429e+02 -1.5138e+00 --1.2000e+03 1.4286e+02 -1.4587e+00 --1.2000e+03 1.7143e+02 -1.3922e+00 --1.2000e+03 2.0000e+02 -1.3149e+00 --1.2000e+03 2.2857e+02 -1.2274e+00 --1.2000e+03 2.5714e+02 -1.1301e+00 --1.2000e+03 2.8571e+02 -1.0239e+00 --1.2000e+03 3.1429e+02 -9.0932e-01 --1.2000e+03 3.4286e+02 -7.8723e-01 --1.2000e+03 3.7143e+02 -6.5834e-01 --1.2000e+03 4.0000e+02 -5.2338e-01 --1.2000e+03 4.2857e+02 -3.8310e-01 --1.2000e+03 4.5714e+02 -2.3824e-01 --1.2000e+03 4.8571e+02 -8.9501e-02 --1.2000e+03 5.1429e+02 6.2437e-02 --1.2000e+03 5.4286e+02 2.1692e-01 --1.2000e+03 5.7143e+02 3.7334e-01 --1.2000e+03 6.0000e+02 5.3111e-01 --1.2000e+03 6.2857e+02 6.8971e-01 --1.2000e+03 6.5714e+02 8.4863e-01 --1.2000e+03 6.8571e+02 1.0074e+00 --1.2000e+03 7.1429e+02 1.1657e+00 --1.2000e+03 7.4286e+02 1.3231e+00 --1.2000e+03 7.7143e+02 1.4792e+00 --1.2000e+03 8.0000e+02 1.6338e+00 --1.2000e+03 8.2857e+02 1.7866e+00 --1.2000e+03 8.5714e+02 1.9375e+00 --1.2000e+03 8.8571e+02 2.0861e+00 --1.2000e+03 9.1429e+02 2.2324e+00 --1.2000e+03 9.4286e+02 2.3761e+00 --1.2000e+03 9.7143e+02 2.5173e+00 --1.2000e+03 1.0000e+03 2.6558e+00 --1.2000e+03 1.0286e+03 2.7915e+00 --1.2000e+03 1.0571e+03 2.9245e+00 --1.2000e+03 1.0857e+03 3.0545e+00 --1.2000e+03 1.1143e+03 3.1817e+00 --1.2000e+03 1.1429e+03 3.3061e+00 --1.2000e+03 1.1714e+03 3.4275e+00 --1.2000e+03 1.2000e+03 3.5462e+00 --1.2000e+03 1.2286e+03 3.6619e+00 --1.2000e+03 1.2571e+03 3.7749e+00 --1.2000e+03 1.2857e+03 3.8852e+00 --1.2000e+03 1.3143e+03 3.9926e+00 --1.2000e+03 1.3429e+03 4.0975e+00 --1.2000e+03 1.3714e+03 4.1996e+00 --1.2000e+03 1.4000e+03 4.2993e+00 --1.2000e+03 1.4286e+03 4.3963e+00 --1.2000e+03 1.4571e+03 4.4910e+00 --1.2000e+03 1.4857e+03 4.5832e+00 --1.2000e+03 1.5143e+03 4.6730e+00 --1.2000e+03 1.5429e+03 4.7606e+00 --1.2000e+03 1.5714e+03 4.8459e+00 --1.2000e+03 1.6000e+03 4.9290e+00 --1.2000e+03 1.6286e+03 5.0101e+00 --1.2000e+03 1.6571e+03 5.0890e+00 --1.2000e+03 1.6857e+03 5.1660e+00 --1.2000e+03 1.7143e+03 5.2410e+00 --1.2000e+03 1.7429e+03 5.3142e+00 --1.2000e+03 1.7714e+03 5.3854e+00 --1.2000e+03 1.8000e+03 5.4550e+00 --1.2000e+03 1.8286e+03 5.5227e+00 --1.2000e+03 1.8571e+03 5.5888e+00 --1.2000e+03 1.8857e+03 5.6533e+00 --1.2000e+03 1.9143e+03 5.7162e+00 --1.2000e+03 1.9429e+03 5.7776e+00 --1.2000e+03 1.9714e+03 5.8374e+00 --1.2000e+03 2.0000e+03 5.8958e+00 --1.1700e+03 -2.0000e+03 5.8595e+00 --1.1700e+03 -1.9714e+03 5.7996e+00 --1.1700e+03 -1.9429e+03 5.7382e+00 --1.1700e+03 -1.9143e+03 5.6753e+00 --1.1700e+03 -1.8857e+03 5.6107e+00 --1.1700e+03 -1.8571e+03 5.5445e+00 --1.1700e+03 -1.8286e+03 5.4766e+00 --1.1700e+03 -1.8000e+03 5.4068e+00 --1.1700e+03 -1.7714e+03 5.3353e+00 --1.1700e+03 -1.7429e+03 5.2618e+00 --1.1700e+03 -1.7143e+03 5.1864e+00 --1.1700e+03 -1.6857e+03 5.1090e+00 --1.1700e+03 -1.6571e+03 5.0295e+00 --1.1700e+03 -1.6286e+03 4.9479e+00 --1.1700e+03 -1.6000e+03 4.8641e+00 --1.1700e+03 -1.5714e+03 4.7780e+00 --1.1700e+03 -1.5429e+03 4.6896e+00 --1.1700e+03 -1.5143e+03 4.5988e+00 --1.1700e+03 -1.4857e+03 4.5055e+00 --1.1700e+03 -1.4571e+03 4.4096e+00 --1.1700e+03 -1.4286e+03 4.3112e+00 --1.1700e+03 -1.4000e+03 4.2100e+00 --1.1700e+03 -1.3714e+03 4.1061e+00 --1.1700e+03 -1.3429e+03 3.9994e+00 --1.1700e+03 -1.3143e+03 3.8899e+00 --1.1700e+03 -1.2857e+03 3.7773e+00 --1.1700e+03 -1.2571e+03 3.6618e+00 --1.1700e+03 -1.2286e+03 3.5432e+00 --1.1700e+03 -1.2000e+03 3.4215e+00 --1.1700e+03 -1.1714e+03 3.2967e+00 --1.1700e+03 -1.1429e+03 3.1686e+00 --1.1700e+03 -1.1143e+03 3.0374e+00 --1.1700e+03 -1.0857e+03 2.9029e+00 --1.1700e+03 -1.0571e+03 2.7651e+00 --1.1700e+03 -1.0286e+03 2.6241e+00 --1.1700e+03 -1.0000e+03 2.4799e+00 --1.1700e+03 -9.7143e+02 2.3324e+00 --1.1700e+03 -9.4286e+02 2.1819e+00 --1.1700e+03 -9.1429e+02 2.0282e+00 --1.1700e+03 -8.8571e+02 1.8716e+00 --1.1700e+03 -8.5714e+02 1.7122e+00 --1.1700e+03 -8.2857e+02 1.5501e+00 --1.1700e+03 -8.0000e+02 1.3855e+00 --1.1700e+03 -7.7143e+02 1.2187e+00 --1.1700e+03 -7.4286e+02 1.0498e+00 --1.1700e+03 -7.1429e+02 8.7926e-01 --1.1700e+03 -6.8571e+02 7.0736e-01 --1.1700e+03 -6.5714e+02 5.3451e-01 --1.1700e+03 -6.2857e+02 3.6115e-01 --1.1700e+03 -6.0000e+02 1.8776e-01 --1.1700e+03 -5.7143e+02 1.4889e-02 --1.1700e+03 -5.4286e+02 -1.5687e-01 --1.1700e+03 -5.1429e+02 -3.2688e-01 --1.1700e+03 -4.8571e+02 -4.9444e-01 --1.1700e+03 -4.5714e+02 -6.5883e-01 --1.1700e+03 -4.2857e+02 -8.1927e-01 --1.1700e+03 -4.0000e+02 -9.7493e-01 --1.1700e+03 -3.7143e+02 -1.1250e+00 --1.1700e+03 -3.4286e+02 -1.2686e+00 --1.1700e+03 -3.1429e+02 -1.4048e+00 --1.1700e+03 -2.8571e+02 -1.5329e+00 --1.1700e+03 -2.5714e+02 -1.6518e+00 --1.1700e+03 -2.2857e+02 -1.7609e+00 --1.1700e+03 -2.0000e+02 -1.8592e+00 --1.1700e+03 -1.7143e+02 -1.9461e+00 --1.1700e+03 -1.4286e+02 -2.0209e+00 --1.1700e+03 -1.1429e+02 -2.0829e+00 --1.1700e+03 -8.5714e+01 -2.1317e+00 --1.1700e+03 -5.7143e+01 -2.1669e+00 --1.1700e+03 -2.8571e+01 -2.1881e+00 --1.1700e+03 0.0000e+00 -2.1952e+00 --1.1700e+03 2.8571e+01 -2.1881e+00 --1.1700e+03 5.7143e+01 -2.1669e+00 --1.1700e+03 8.5714e+01 -2.1317e+00 --1.1700e+03 1.1429e+02 -2.0829e+00 --1.1700e+03 1.4286e+02 -2.0209e+00 --1.1700e+03 1.7143e+02 -1.9461e+00 --1.1700e+03 2.0000e+02 -1.8592e+00 --1.1700e+03 2.2857e+02 -1.7609e+00 --1.1700e+03 2.5714e+02 -1.6518e+00 --1.1700e+03 2.8571e+02 -1.5329e+00 --1.1700e+03 3.1429e+02 -1.4048e+00 --1.1700e+03 3.4286e+02 -1.2686e+00 --1.1700e+03 3.7143e+02 -1.1250e+00 --1.1700e+03 4.0000e+02 -9.7493e-01 --1.1700e+03 4.2857e+02 -8.1927e-01 --1.1700e+03 4.5714e+02 -6.5883e-01 --1.1700e+03 4.8571e+02 -4.9444e-01 --1.1700e+03 5.1429e+02 -3.2688e-01 --1.1700e+03 5.4286e+02 -1.5687e-01 --1.1700e+03 5.7143e+02 1.4889e-02 --1.1700e+03 6.0000e+02 1.8776e-01 --1.1700e+03 6.2857e+02 3.6115e-01 --1.1700e+03 6.5714e+02 5.3451e-01 --1.1700e+03 6.8571e+02 7.0736e-01 --1.1700e+03 7.1429e+02 8.7926e-01 --1.1700e+03 7.4286e+02 1.0498e+00 --1.1700e+03 7.7143e+02 1.2187e+00 --1.1700e+03 8.0000e+02 1.3855e+00 --1.1700e+03 8.2857e+02 1.5501e+00 --1.1700e+03 8.5714e+02 1.7122e+00 --1.1700e+03 8.8571e+02 1.8716e+00 --1.1700e+03 9.1429e+02 2.0282e+00 --1.1700e+03 9.4286e+02 2.1819e+00 --1.1700e+03 9.7143e+02 2.3324e+00 --1.1700e+03 1.0000e+03 2.4799e+00 --1.1700e+03 1.0286e+03 2.6241e+00 --1.1700e+03 1.0571e+03 2.7651e+00 --1.1700e+03 1.0857e+03 2.9029e+00 --1.1700e+03 1.1143e+03 3.0374e+00 --1.1700e+03 1.1429e+03 3.1686e+00 --1.1700e+03 1.1714e+03 3.2967e+00 --1.1700e+03 1.2000e+03 3.4215e+00 --1.1700e+03 1.2286e+03 3.5432e+00 --1.1700e+03 1.2571e+03 3.6618e+00 --1.1700e+03 1.2857e+03 3.7773e+00 --1.1700e+03 1.3143e+03 3.8899e+00 --1.1700e+03 1.3429e+03 3.9994e+00 --1.1700e+03 1.3714e+03 4.1061e+00 --1.1700e+03 1.4000e+03 4.2100e+00 --1.1700e+03 1.4286e+03 4.3112e+00 --1.1700e+03 1.4571e+03 4.4096e+00 --1.1700e+03 1.4857e+03 4.5055e+00 --1.1700e+03 1.5143e+03 4.5988e+00 --1.1700e+03 1.5429e+03 4.6896e+00 --1.1700e+03 1.5714e+03 4.7780e+00 --1.1700e+03 1.6000e+03 4.8641e+00 --1.1700e+03 1.6286e+03 4.9479e+00 --1.1700e+03 1.6571e+03 5.0295e+00 --1.1700e+03 1.6857e+03 5.1090e+00 --1.1700e+03 1.7143e+03 5.1864e+00 --1.1700e+03 1.7429e+03 5.2618e+00 --1.1700e+03 1.7714e+03 5.3353e+00 --1.1700e+03 1.8000e+03 5.4068e+00 --1.1700e+03 1.8286e+03 5.4766e+00 --1.1700e+03 1.8571e+03 5.5445e+00 --1.1700e+03 1.8857e+03 5.6107e+00 --1.1700e+03 1.9143e+03 5.6753e+00 --1.1700e+03 1.9429e+03 5.7382e+00 --1.1700e+03 1.9714e+03 5.7996e+00 --1.1700e+03 2.0000e+03 5.8595e+00 --1.1400e+03 -2.0000e+03 5.8232e+00 --1.1400e+03 -1.9714e+03 5.7619e+00 --1.1400e+03 -1.9429e+03 5.6990e+00 --1.1400e+03 -1.9143e+03 5.6344e+00 --1.1400e+03 -1.8857e+03 5.5681e+00 --1.1400e+03 -1.8571e+03 5.5001e+00 --1.1400e+03 -1.8286e+03 5.4303e+00 --1.1400e+03 -1.8000e+03 5.3586e+00 --1.1400e+03 -1.7714e+03 5.2849e+00 --1.1400e+03 -1.7429e+03 5.2093e+00 --1.1400e+03 -1.7143e+03 5.1316e+00 --1.1400e+03 -1.6857e+03 5.0517e+00 --1.1400e+03 -1.6571e+03 4.9696e+00 --1.1400e+03 -1.6286e+03 4.8853e+00 --1.1400e+03 -1.6000e+03 4.7986e+00 --1.1400e+03 -1.5714e+03 4.7095e+00 --1.1400e+03 -1.5429e+03 4.6179e+00 --1.1400e+03 -1.5143e+03 4.5237e+00 --1.1400e+03 -1.4857e+03 4.4268e+00 --1.1400e+03 -1.4571e+03 4.3272e+00 --1.1400e+03 -1.4286e+03 4.2247e+00 --1.1400e+03 -1.4000e+03 4.1194e+00 --1.1400e+03 -1.3714e+03 4.0110e+00 --1.1400e+03 -1.3429e+03 3.8996e+00 --1.1400e+03 -1.3143e+03 3.7851e+00 --1.1400e+03 -1.2857e+03 3.6673e+00 --1.1400e+03 -1.2571e+03 3.5462e+00 --1.1400e+03 -1.2286e+03 3.4217e+00 --1.1400e+03 -1.2000e+03 3.2938e+00 --1.1400e+03 -1.1714e+03 3.1623e+00 --1.1400e+03 -1.1429e+03 3.0273e+00 --1.1400e+03 -1.1143e+03 2.8887e+00 --1.1400e+03 -1.0857e+03 2.7464e+00 --1.1400e+03 -1.0571e+03 2.6004e+00 --1.1400e+03 -1.0286e+03 2.4508e+00 --1.1400e+03 -1.0000e+03 2.2974e+00 --1.1400e+03 -9.7143e+02 2.1403e+00 --1.1400e+03 -9.4286e+02 1.9796e+00 --1.1400e+03 -9.1429e+02 1.8153e+00 --1.1400e+03 -8.8571e+02 1.6475e+00 --1.1400e+03 -8.5714e+02 1.4764e+00 --1.1400e+03 -8.2857e+02 1.3019e+00 --1.1400e+03 -8.0000e+02 1.1244e+00 --1.1400e+03 -7.7143e+02 9.4412e-01 --1.1400e+03 -7.4286e+02 7.6124e-01 --1.1400e+03 -7.1429e+02 5.7610e-01 --1.1400e+03 -6.8571e+02 3.8906e-01 --1.1400e+03 -6.5714e+02 2.0055e-01 --1.1400e+03 -6.2857e+02 1.1040e-02 --1.1400e+03 -6.0000e+02 -1.7895e-01 --1.1400e+03 -5.7143e+02 -3.6881e-01 --1.1400e+03 -5.4286e+02 -5.5791e-01 --1.1400e+03 -5.1429e+02 -7.4551e-01 --1.1400e+03 -4.8571e+02 -9.3085e-01 --1.1400e+03 -4.5714e+02 -1.1131e+00 --1.1400e+03 -4.2857e+02 -1.2914e+00 --1.1400e+03 -4.0000e+02 -1.4647e+00 --1.1400e+03 -3.7143e+02 -1.6322e+00 --1.1400e+03 -3.4286e+02 -1.7927e+00 --1.1400e+03 -3.1429e+02 -1.9454e+00 --1.1400e+03 -2.8571e+02 -2.0891e+00 --1.1400e+03 -2.5714e+02 -2.2229e+00 --1.1400e+03 -2.2857e+02 -2.3457e+00 --1.1400e+03 -2.0000e+02 -2.4566e+00 --1.1400e+03 -1.7143e+02 -2.5548e+00 --1.1400e+03 -1.4286e+02 -2.6393e+00 --1.1400e+03 -1.1429e+02 -2.7095e+00 --1.1400e+03 -8.5714e+01 -2.7648e+00 --1.1400e+03 -5.7143e+01 -2.8046e+00 --1.1400e+03 -2.8571e+01 -2.8287e+00 --1.1400e+03 0.0000e+00 -2.8367e+00 --1.1400e+03 2.8571e+01 -2.8287e+00 --1.1400e+03 5.7143e+01 -2.8046e+00 --1.1400e+03 8.5714e+01 -2.7648e+00 --1.1400e+03 1.1429e+02 -2.7095e+00 --1.1400e+03 1.4286e+02 -2.6393e+00 --1.1400e+03 1.7143e+02 -2.5548e+00 --1.1400e+03 2.0000e+02 -2.4566e+00 --1.1400e+03 2.2857e+02 -2.3457e+00 --1.1400e+03 2.5714e+02 -2.2229e+00 --1.1400e+03 2.8571e+02 -2.0891e+00 --1.1400e+03 3.1429e+02 -1.9454e+00 --1.1400e+03 3.4286e+02 -1.7927e+00 --1.1400e+03 3.7143e+02 -1.6322e+00 --1.1400e+03 4.0000e+02 -1.4647e+00 --1.1400e+03 4.2857e+02 -1.2914e+00 --1.1400e+03 4.5714e+02 -1.1131e+00 --1.1400e+03 4.8571e+02 -9.3085e-01 --1.1400e+03 5.1429e+02 -7.4551e-01 --1.1400e+03 5.4286e+02 -5.5791e-01 --1.1400e+03 5.7143e+02 -3.6881e-01 --1.1400e+03 6.0000e+02 -1.7895e-01 --1.1400e+03 6.2857e+02 1.1040e-02 --1.1400e+03 6.5714e+02 2.0055e-01 --1.1400e+03 6.8571e+02 3.8906e-01 --1.1400e+03 7.1429e+02 5.7610e-01 --1.1400e+03 7.4286e+02 7.6124e-01 --1.1400e+03 7.7143e+02 9.4412e-01 --1.1400e+03 8.0000e+02 1.1244e+00 --1.1400e+03 8.2857e+02 1.3019e+00 --1.1400e+03 8.5714e+02 1.4764e+00 --1.1400e+03 8.8571e+02 1.6475e+00 --1.1400e+03 9.1429e+02 1.8153e+00 --1.1400e+03 9.4286e+02 1.9796e+00 --1.1400e+03 9.7143e+02 2.1403e+00 --1.1400e+03 1.0000e+03 2.2974e+00 --1.1400e+03 1.0286e+03 2.4508e+00 --1.1400e+03 1.0571e+03 2.6004e+00 --1.1400e+03 1.0857e+03 2.7464e+00 --1.1400e+03 1.1143e+03 2.8887e+00 --1.1400e+03 1.1429e+03 3.0273e+00 --1.1400e+03 1.1714e+03 3.1623e+00 --1.1400e+03 1.2000e+03 3.2938e+00 --1.1400e+03 1.2286e+03 3.4217e+00 --1.1400e+03 1.2571e+03 3.5462e+00 --1.1400e+03 1.2857e+03 3.6673e+00 --1.1400e+03 1.3143e+03 3.7851e+00 --1.1400e+03 1.3429e+03 3.8996e+00 --1.1400e+03 1.3714e+03 4.0110e+00 --1.1400e+03 1.4000e+03 4.1194e+00 --1.1400e+03 1.4286e+03 4.2247e+00 --1.1400e+03 1.4571e+03 4.3272e+00 --1.1400e+03 1.4857e+03 4.4268e+00 --1.1400e+03 1.5143e+03 4.5237e+00 --1.1400e+03 1.5429e+03 4.6179e+00 --1.1400e+03 1.5714e+03 4.7095e+00 --1.1400e+03 1.6000e+03 4.7986e+00 --1.1400e+03 1.6286e+03 4.8853e+00 --1.1400e+03 1.6571e+03 4.9696e+00 --1.1400e+03 1.6857e+03 5.0517e+00 --1.1400e+03 1.7143e+03 5.1316e+00 --1.1400e+03 1.7429e+03 5.2093e+00 --1.1400e+03 1.7714e+03 5.2849e+00 --1.1400e+03 1.8000e+03 5.3586e+00 --1.1400e+03 1.8286e+03 5.4303e+00 --1.1400e+03 1.8571e+03 5.5001e+00 --1.1400e+03 1.8857e+03 5.5681e+00 --1.1400e+03 1.9143e+03 5.6344e+00 --1.1400e+03 1.9429e+03 5.6990e+00 --1.1400e+03 1.9714e+03 5.7619e+00 --1.1400e+03 2.0000e+03 5.8232e+00 --1.1100e+03 -2.0000e+03 5.7870e+00 --1.1100e+03 -1.9714e+03 5.7242e+00 --1.1100e+03 -1.9429e+03 5.6597e+00 --1.1100e+03 -1.9143e+03 5.5935e+00 --1.1100e+03 -1.8857e+03 5.5255e+00 --1.1100e+03 -1.8571e+03 5.4557e+00 --1.1100e+03 -1.8286e+03 5.3839e+00 --1.1100e+03 -1.8000e+03 5.3102e+00 --1.1100e+03 -1.7714e+03 5.2344e+00 --1.1100e+03 -1.7429e+03 5.1565e+00 --1.1100e+03 -1.7143e+03 5.0764e+00 --1.1100e+03 -1.6857e+03 4.9941e+00 --1.1100e+03 -1.6571e+03 4.9094e+00 --1.1100e+03 -1.6286e+03 4.8223e+00 --1.1100e+03 -1.6000e+03 4.7326e+00 --1.1100e+03 -1.5714e+03 4.6404e+00 --1.1100e+03 -1.5429e+03 4.5455e+00 --1.1100e+03 -1.5143e+03 4.4478e+00 --1.1100e+03 -1.4857e+03 4.3472e+00 --1.1100e+03 -1.4571e+03 4.2437e+00 --1.1100e+03 -1.4286e+03 4.1371e+00 --1.1100e+03 -1.4000e+03 4.0274e+00 --1.1100e+03 -1.3714e+03 3.9144e+00 --1.1100e+03 -1.3429e+03 3.7981e+00 --1.1100e+03 -1.3143e+03 3.6783e+00 --1.1100e+03 -1.2857e+03 3.5550e+00 --1.1100e+03 -1.2571e+03 3.4280e+00 --1.1100e+03 -1.2286e+03 3.2973e+00 --1.1100e+03 -1.2000e+03 3.1628e+00 --1.1100e+03 -1.1714e+03 3.0244e+00 --1.1100e+03 -1.1429e+03 2.8821e+00 --1.1100e+03 -1.1143e+03 2.7356e+00 --1.1100e+03 -1.0857e+03 2.5851e+00 --1.1100e+03 -1.0571e+03 2.4303e+00 --1.1100e+03 -1.0286e+03 2.2714e+00 --1.1100e+03 -1.0000e+03 2.1082e+00 --1.1100e+03 -9.7143e+02 1.9408e+00 --1.1100e+03 -9.4286e+02 1.7691e+00 --1.1100e+03 -9.1429e+02 1.5933e+00 --1.1100e+03 -8.8571e+02 1.4133e+00 --1.1100e+03 -8.5714e+02 1.2293e+00 --1.1100e+03 -8.2857e+02 1.0414e+00 --1.1100e+03 -8.0000e+02 8.4975e-01 --1.1100e+03 -7.7143e+02 6.5461e-01 --1.1100e+03 -7.4286e+02 4.5622e-01 --1.1100e+03 -7.1429e+02 2.5490e-01 --1.1100e+03 -6.8571e+02 5.1018e-02 --1.1100e+03 -6.5714e+02 -1.5498e-01 --1.1100e+03 -6.2857e+02 -3.6260e-01 --1.1100e+03 -6.0000e+02 -5.7126e-01 --1.1100e+03 -5.7143e+02 -7.8032e-01 --1.1100e+03 -5.4286e+02 -9.8905e-01 --1.1100e+03 -5.1429e+02 -1.1967e+00 --1.1100e+03 -4.8571e+02 -1.4023e+00 --1.1100e+03 -4.5714e+02 -1.6050e+00 --1.1100e+03 -4.2857e+02 -1.8038e+00 --1.1100e+03 -4.0000e+02 -1.9975e+00 --1.1100e+03 -3.7143e+02 -2.1851e+00 --1.1100e+03 -3.4286e+02 -2.3654e+00 --1.1100e+03 -3.1429e+02 -2.5372e+00 --1.1100e+03 -2.8571e+02 -2.6992e+00 --1.1100e+03 -2.5714e+02 -2.8503e+00 --1.1100e+03 -2.2857e+02 -2.9893e+00 --1.1100e+03 -2.0000e+02 -3.1150e+00 --1.1100e+03 -1.7143e+02 -3.2264e+00 --1.1100e+03 -1.4286e+02 -3.3225e+00 --1.1100e+03 -1.1429e+02 -3.4024e+00 --1.1100e+03 -8.5714e+01 -3.4653e+00 --1.1100e+03 -5.7143e+01 -3.5107e+00 --1.1100e+03 -2.8571e+01 -3.5381e+00 --1.1100e+03 0.0000e+00 -3.5473e+00 --1.1100e+03 2.8571e+01 -3.5381e+00 --1.1100e+03 5.7143e+01 -3.5107e+00 --1.1100e+03 8.5714e+01 -3.4653e+00 --1.1100e+03 1.1429e+02 -3.4024e+00 --1.1100e+03 1.4286e+02 -3.3225e+00 --1.1100e+03 1.7143e+02 -3.2264e+00 --1.1100e+03 2.0000e+02 -3.1150e+00 --1.1100e+03 2.2857e+02 -2.9893e+00 --1.1100e+03 2.5714e+02 -2.8503e+00 --1.1100e+03 2.8571e+02 -2.6992e+00 --1.1100e+03 3.1429e+02 -2.5372e+00 --1.1100e+03 3.4286e+02 -2.3654e+00 --1.1100e+03 3.7143e+02 -2.1851e+00 --1.1100e+03 4.0000e+02 -1.9975e+00 --1.1100e+03 4.2857e+02 -1.8038e+00 --1.1100e+03 4.5714e+02 -1.6050e+00 --1.1100e+03 4.8571e+02 -1.4023e+00 --1.1100e+03 5.1429e+02 -1.1967e+00 --1.1100e+03 5.4286e+02 -9.8905e-01 --1.1100e+03 5.7143e+02 -7.8032e-01 --1.1100e+03 6.0000e+02 -5.7126e-01 --1.1100e+03 6.2857e+02 -3.6260e-01 --1.1100e+03 6.5714e+02 -1.5498e-01 --1.1100e+03 6.8571e+02 5.1018e-02 --1.1100e+03 7.1429e+02 2.5490e-01 --1.1100e+03 7.4286e+02 4.5622e-01 --1.1100e+03 7.7143e+02 6.5461e-01 --1.1100e+03 8.0000e+02 8.4975e-01 --1.1100e+03 8.2857e+02 1.0414e+00 --1.1100e+03 8.5714e+02 1.2293e+00 --1.1100e+03 8.8571e+02 1.4133e+00 --1.1100e+03 9.1429e+02 1.5933e+00 --1.1100e+03 9.4286e+02 1.7691e+00 --1.1100e+03 9.7143e+02 1.9408e+00 --1.1100e+03 1.0000e+03 2.1082e+00 --1.1100e+03 1.0286e+03 2.2714e+00 --1.1100e+03 1.0571e+03 2.4303e+00 --1.1100e+03 1.0857e+03 2.5851e+00 --1.1100e+03 1.1143e+03 2.7356e+00 --1.1100e+03 1.1429e+03 2.8821e+00 --1.1100e+03 1.1714e+03 3.0244e+00 --1.1100e+03 1.2000e+03 3.1628e+00 --1.1100e+03 1.2286e+03 3.2973e+00 --1.1100e+03 1.2571e+03 3.4280e+00 --1.1100e+03 1.2857e+03 3.5550e+00 --1.1100e+03 1.3143e+03 3.6783e+00 --1.1100e+03 1.3429e+03 3.7981e+00 --1.1100e+03 1.3714e+03 3.9144e+00 --1.1100e+03 1.4000e+03 4.0274e+00 --1.1100e+03 1.4286e+03 4.1371e+00 --1.1100e+03 1.4571e+03 4.2437e+00 --1.1100e+03 1.4857e+03 4.3472e+00 --1.1100e+03 1.5143e+03 4.4478e+00 --1.1100e+03 1.5429e+03 4.5455e+00 --1.1100e+03 1.5714e+03 4.6404e+00 --1.1100e+03 1.6000e+03 4.7326e+00 --1.1100e+03 1.6286e+03 4.8223e+00 --1.1100e+03 1.6571e+03 4.9094e+00 --1.1100e+03 1.6857e+03 4.9941e+00 --1.1100e+03 1.7143e+03 5.0764e+00 --1.1100e+03 1.7429e+03 5.1565e+00 --1.1100e+03 1.7714e+03 5.2344e+00 --1.1100e+03 1.8000e+03 5.3102e+00 --1.1100e+03 1.8286e+03 5.3839e+00 --1.1100e+03 1.8571e+03 5.4557e+00 --1.1100e+03 1.8857e+03 5.5255e+00 --1.1100e+03 1.9143e+03 5.5935e+00 --1.1100e+03 1.9429e+03 5.6597e+00 --1.1100e+03 1.9714e+03 5.7242e+00 --1.1100e+03 2.0000e+03 5.7870e+00 --1.0800e+03 -2.0000e+03 5.7510e+00 --1.0800e+03 -1.9714e+03 5.6866e+00 --1.0800e+03 -1.9429e+03 5.6206e+00 --1.0800e+03 -1.9143e+03 5.5527e+00 --1.0800e+03 -1.8857e+03 5.4830e+00 --1.0800e+03 -1.8571e+03 5.4113e+00 --1.0800e+03 -1.8286e+03 5.3376e+00 --1.0800e+03 -1.8000e+03 5.2618e+00 --1.0800e+03 -1.7714e+03 5.1838e+00 --1.0800e+03 -1.7429e+03 5.1037e+00 --1.0800e+03 -1.7143e+03 5.0212e+00 --1.0800e+03 -1.6857e+03 4.9362e+00 --1.0800e+03 -1.6571e+03 4.8488e+00 --1.0800e+03 -1.6286e+03 4.7588e+00 --1.0800e+03 -1.6000e+03 4.6662e+00 --1.0800e+03 -1.5714e+03 4.5707e+00 --1.0800e+03 -1.5429e+03 4.4724e+00 --1.0800e+03 -1.5143e+03 4.3711e+00 --1.0800e+03 -1.4857e+03 4.2667e+00 --1.0800e+03 -1.4571e+03 4.1592e+00 --1.0800e+03 -1.4286e+03 4.0483e+00 --1.0800e+03 -1.4000e+03 3.9340e+00 --1.0800e+03 -1.3714e+03 3.8162e+00 --1.0800e+03 -1.3429e+03 3.6948e+00 --1.0800e+03 -1.3143e+03 3.5695e+00 --1.0800e+03 -1.2857e+03 3.4405e+00 --1.0800e+03 -1.2571e+03 3.3074e+00 --1.0800e+03 -1.2286e+03 3.1702e+00 --1.0800e+03 -1.2000e+03 3.0287e+00 --1.0800e+03 -1.1714e+03 2.8830e+00 --1.0800e+03 -1.1429e+03 2.7328e+00 --1.0800e+03 -1.1143e+03 2.5780e+00 --1.0800e+03 -1.0857e+03 2.4187e+00 --1.0800e+03 -1.0571e+03 2.2546e+00 --1.0800e+03 -1.0286e+03 2.0857e+00 --1.0800e+03 -1.0000e+03 1.9120e+00 --1.0800e+03 -9.7143e+02 1.7334e+00 --1.0800e+03 -9.4286e+02 1.5499e+00 --1.0800e+03 -9.1429e+02 1.3616e+00 --1.0800e+03 -8.8571e+02 1.1684e+00 --1.0800e+03 -8.5714e+02 9.7037e-01 --1.0800e+03 -8.2857e+02 7.6771e-01 --1.0800e+03 -8.0000e+02 5.6054e-01 --1.0800e+03 -7.7143e+02 3.4905e-01 --1.0800e+03 -7.4286e+02 1.3351e-01 --1.0800e+03 -7.1429e+02 -8.5786e-02 --1.0800e+03 -6.8571e+02 -3.0845e-01 --1.0800e+03 -6.5714e+02 -5.3402e-01 --1.0800e+03 -6.2857e+02 -7.6197e-01 --1.0800e+03 -6.0000e+02 -9.9170e-01 --1.0800e+03 -5.7143e+02 -1.2225e+00 --1.0800e+03 -5.4286e+02 -1.4536e+00 --1.0800e+03 -5.1429e+02 -1.6840e+00 --1.0800e+03 -4.8571e+02 -1.9129e+00 --1.0800e+03 -4.5714e+02 -2.1392e+00 --1.0800e+03 -4.2857e+02 -2.3616e+00 --1.0800e+03 -4.0000e+02 -2.5790e+00 --1.0800e+03 -3.7143e+02 -2.7900e+00 --1.0800e+03 -3.4286e+02 -2.9933e+00 --1.0800e+03 -3.1429e+02 -3.1875e+00 --1.0800e+03 -2.8571e+02 -3.3710e+00 --1.0800e+03 -2.5714e+02 -3.5426e+00 --1.0800e+03 -2.2857e+02 -3.7006e+00 --1.0800e+03 -2.0000e+02 -3.8439e+00 --1.0800e+03 -1.7143e+02 -3.9710e+00 --1.0800e+03 -1.4286e+02 -4.0808e+00 --1.0800e+03 -1.1429e+02 -4.1722e+00 --1.0800e+03 -8.5714e+01 -4.2443e+00 --1.0800e+03 -5.7143e+01 -4.2963e+00 --1.0800e+03 -2.8571e+01 -4.3278e+00 --1.0800e+03 0.0000e+00 -4.3383e+00 --1.0800e+03 2.8571e+01 -4.3278e+00 --1.0800e+03 5.7143e+01 -4.2963e+00 --1.0800e+03 8.5714e+01 -4.2443e+00 --1.0800e+03 1.1429e+02 -4.1722e+00 --1.0800e+03 1.4286e+02 -4.0808e+00 --1.0800e+03 1.7143e+02 -3.9710e+00 --1.0800e+03 2.0000e+02 -3.8439e+00 --1.0800e+03 2.2857e+02 -3.7006e+00 --1.0800e+03 2.5714e+02 -3.5426e+00 --1.0800e+03 2.8571e+02 -3.3710e+00 --1.0800e+03 3.1429e+02 -3.1875e+00 --1.0800e+03 3.4286e+02 -2.9933e+00 --1.0800e+03 3.7143e+02 -2.7900e+00 --1.0800e+03 4.0000e+02 -2.5790e+00 --1.0800e+03 4.2857e+02 -2.3616e+00 --1.0800e+03 4.5714e+02 -2.1392e+00 --1.0800e+03 4.8571e+02 -1.9129e+00 --1.0800e+03 5.1429e+02 -1.6840e+00 --1.0800e+03 5.4286e+02 -1.4536e+00 --1.0800e+03 5.7143e+02 -1.2225e+00 --1.0800e+03 6.0000e+02 -9.9170e-01 --1.0800e+03 6.2857e+02 -7.6197e-01 --1.0800e+03 6.5714e+02 -5.3402e-01 --1.0800e+03 6.8571e+02 -3.0845e-01 --1.0800e+03 7.1429e+02 -8.5786e-02 --1.0800e+03 7.4286e+02 1.3351e-01 --1.0800e+03 7.7143e+02 3.4905e-01 --1.0800e+03 8.0000e+02 5.6054e-01 --1.0800e+03 8.2857e+02 7.6771e-01 --1.0800e+03 8.5714e+02 9.7037e-01 --1.0800e+03 8.8571e+02 1.1684e+00 --1.0800e+03 9.1429e+02 1.3616e+00 --1.0800e+03 9.4286e+02 1.5499e+00 --1.0800e+03 9.7143e+02 1.7334e+00 --1.0800e+03 1.0000e+03 1.9120e+00 --1.0800e+03 1.0286e+03 2.0857e+00 --1.0800e+03 1.0571e+03 2.2546e+00 --1.0800e+03 1.0857e+03 2.4187e+00 --1.0800e+03 1.1143e+03 2.5780e+00 --1.0800e+03 1.1429e+03 2.7328e+00 --1.0800e+03 1.1714e+03 2.8830e+00 --1.0800e+03 1.2000e+03 3.0287e+00 --1.0800e+03 1.2286e+03 3.1702e+00 --1.0800e+03 1.2571e+03 3.3074e+00 --1.0800e+03 1.2857e+03 3.4405e+00 --1.0800e+03 1.3143e+03 3.5695e+00 --1.0800e+03 1.3429e+03 3.6948e+00 --1.0800e+03 1.3714e+03 3.8162e+00 --1.0800e+03 1.4000e+03 3.9340e+00 --1.0800e+03 1.4286e+03 4.0483e+00 --1.0800e+03 1.4571e+03 4.1592e+00 --1.0800e+03 1.4857e+03 4.2667e+00 --1.0800e+03 1.5143e+03 4.3711e+00 --1.0800e+03 1.5429e+03 4.4724e+00 --1.0800e+03 1.5714e+03 4.5707e+00 --1.0800e+03 1.6000e+03 4.6662e+00 --1.0800e+03 1.6286e+03 4.7588e+00 --1.0800e+03 1.6571e+03 4.8488e+00 --1.0800e+03 1.6857e+03 4.9362e+00 --1.0800e+03 1.7143e+03 5.0212e+00 --1.0800e+03 1.7429e+03 5.1037e+00 --1.0800e+03 1.7714e+03 5.1838e+00 --1.0800e+03 1.8000e+03 5.2618e+00 --1.0800e+03 1.8286e+03 5.3376e+00 --1.0800e+03 1.8571e+03 5.4113e+00 --1.0800e+03 1.8857e+03 5.4830e+00 --1.0800e+03 1.9143e+03 5.5527e+00 --1.0800e+03 1.9429e+03 5.6206e+00 --1.0800e+03 1.9714e+03 5.6866e+00 --1.0800e+03 2.0000e+03 5.7510e+00 --1.0500e+03 -2.0000e+03 5.7151e+00 --1.0500e+03 -1.9714e+03 5.6492e+00 --1.0500e+03 -1.9429e+03 5.5816e+00 --1.0500e+03 -1.9143e+03 5.5120e+00 --1.0500e+03 -1.8857e+03 5.4405e+00 --1.0500e+03 -1.8571e+03 5.3669e+00 --1.0500e+03 -1.8286e+03 5.2913e+00 --1.0500e+03 -1.8000e+03 5.2134e+00 --1.0500e+03 -1.7714e+03 5.1332e+00 --1.0500e+03 -1.7429e+03 5.0507e+00 --1.0500e+03 -1.7143e+03 4.9657e+00 --1.0500e+03 -1.6857e+03 4.8782e+00 --1.0500e+03 -1.6571e+03 4.7880e+00 --1.0500e+03 -1.6286e+03 4.6951e+00 --1.0500e+03 -1.6000e+03 4.5993e+00 --1.0500e+03 -1.5714e+03 4.5006e+00 --1.0500e+03 -1.5429e+03 4.3988e+00 --1.0500e+03 -1.5143e+03 4.2938e+00 --1.0500e+03 -1.4857e+03 4.1855e+00 --1.0500e+03 -1.4571e+03 4.0737e+00 --1.0500e+03 -1.4286e+03 3.9584e+00 --1.0500e+03 -1.4000e+03 3.8394e+00 --1.0500e+03 -1.3714e+03 3.7166e+00 --1.0500e+03 -1.3429e+03 3.5898e+00 --1.0500e+03 -1.3143e+03 3.4589e+00 --1.0500e+03 -1.2857e+03 3.3238e+00 --1.0500e+03 -1.2571e+03 3.1843e+00 --1.0500e+03 -1.2286e+03 3.0402e+00 --1.0500e+03 -1.2000e+03 2.8915e+00 --1.0500e+03 -1.1714e+03 2.7380e+00 --1.0500e+03 -1.1429e+03 2.5795e+00 --1.0500e+03 -1.1143e+03 2.4160e+00 --1.0500e+03 -1.0857e+03 2.2472e+00 --1.0500e+03 -1.0571e+03 2.0731e+00 --1.0500e+03 -1.0286e+03 1.8936e+00 --1.0500e+03 -1.0000e+03 1.7086e+00 --1.0500e+03 -9.7143e+02 1.5180e+00 --1.0500e+03 -9.4286e+02 1.3217e+00 --1.0500e+03 -9.1429e+02 1.1198e+00 --1.0500e+03 -8.8571e+02 9.1220e-01 --1.0500e+03 -8.5714e+02 6.9895e-01 --1.0500e+03 -8.2857e+02 4.8013e-01 --1.0500e+03 -8.0000e+02 2.5587e-01 --1.0500e+03 -7.7143e+02 2.6339e-02 --1.0500e+03 -7.4286e+02 -2.0822e-01 --1.0500e+03 -7.1429e+02 -4.4752e-01 --1.0500e+03 -6.8571e+02 -6.9116e-01 --1.0500e+03 -6.5714e+02 -9.3869e-01 --1.0500e+03 -6.2857e+02 -1.1896e+00 --1.0500e+03 -6.0000e+02 -1.4431e+00 --1.0500e+03 -5.7143e+02 -1.6986e+00 --1.0500e+03 -5.4286e+02 -1.9552e+00 --1.0500e+03 -5.1429e+02 -2.2118e+00 --1.0500e+03 -4.8571e+02 -2.4675e+00 --1.0500e+03 -4.5714e+02 -2.7209e+00 --1.0500e+03 -4.2857e+02 -2.9708e+00 --1.0500e+03 -4.0000e+02 -3.2157e+00 --1.0500e+03 -3.7143e+02 -3.4541e+00 --1.0500e+03 -3.4286e+02 -3.6844e+00 --1.0500e+03 -3.1429e+02 -3.9049e+00 --1.0500e+03 -2.8571e+02 -4.1139e+00 --1.0500e+03 -2.5714e+02 -4.3097e+00 --1.0500e+03 -2.2857e+02 -4.4905e+00 --1.0500e+03 -2.0000e+02 -4.6547e+00 --1.0500e+03 -1.7143e+02 -4.8006e+00 --1.0500e+03 -1.4286e+02 -4.9268e+00 --1.0500e+03 -1.1429e+02 -5.0320e+00 --1.0500e+03 -8.5714e+01 -5.1151e+00 --1.0500e+03 -5.7143e+01 -5.1751e+00 --1.0500e+03 -2.8571e+01 -5.2114e+00 --1.0500e+03 0.0000e+00 -5.2235e+00 --1.0500e+03 2.8571e+01 -5.2114e+00 --1.0500e+03 5.7143e+01 -5.1751e+00 --1.0500e+03 8.5714e+01 -5.1151e+00 --1.0500e+03 1.1429e+02 -5.0320e+00 --1.0500e+03 1.4286e+02 -4.9268e+00 --1.0500e+03 1.7143e+02 -4.8006e+00 --1.0500e+03 2.0000e+02 -4.6547e+00 --1.0500e+03 2.2857e+02 -4.4905e+00 --1.0500e+03 2.5714e+02 -4.3097e+00 --1.0500e+03 2.8571e+02 -4.1139e+00 --1.0500e+03 3.1429e+02 -3.9049e+00 --1.0500e+03 3.4286e+02 -3.6844e+00 --1.0500e+03 3.7143e+02 -3.4541e+00 --1.0500e+03 4.0000e+02 -3.2157e+00 --1.0500e+03 4.2857e+02 -2.9708e+00 --1.0500e+03 4.5714e+02 -2.7209e+00 --1.0500e+03 4.8571e+02 -2.4675e+00 --1.0500e+03 5.1429e+02 -2.2118e+00 --1.0500e+03 5.4286e+02 -1.9552e+00 --1.0500e+03 5.7143e+02 -1.6986e+00 --1.0500e+03 6.0000e+02 -1.4431e+00 --1.0500e+03 6.2857e+02 -1.1896e+00 --1.0500e+03 6.5714e+02 -9.3869e-01 --1.0500e+03 6.8571e+02 -6.9116e-01 --1.0500e+03 7.1429e+02 -4.4752e-01 --1.0500e+03 7.4286e+02 -2.0822e-01 --1.0500e+03 7.7143e+02 2.6339e-02 --1.0500e+03 8.0000e+02 2.5587e-01 --1.0500e+03 8.2857e+02 4.8013e-01 --1.0500e+03 8.5714e+02 6.9895e-01 --1.0500e+03 8.8571e+02 9.1220e-01 --1.0500e+03 9.1429e+02 1.1198e+00 --1.0500e+03 9.4286e+02 1.3217e+00 --1.0500e+03 9.7143e+02 1.5180e+00 --1.0500e+03 1.0000e+03 1.7086e+00 --1.0500e+03 1.0286e+03 1.8936e+00 --1.0500e+03 1.0571e+03 2.0731e+00 --1.0500e+03 1.0857e+03 2.2472e+00 --1.0500e+03 1.1143e+03 2.4160e+00 --1.0500e+03 1.1429e+03 2.5795e+00 --1.0500e+03 1.1714e+03 2.7380e+00 --1.0500e+03 1.2000e+03 2.8915e+00 --1.0500e+03 1.2286e+03 3.0402e+00 --1.0500e+03 1.2571e+03 3.1843e+00 --1.0500e+03 1.2857e+03 3.3238e+00 --1.0500e+03 1.3143e+03 3.4589e+00 --1.0500e+03 1.3429e+03 3.5898e+00 --1.0500e+03 1.3714e+03 3.7166e+00 --1.0500e+03 1.4000e+03 3.8394e+00 --1.0500e+03 1.4286e+03 3.9584e+00 --1.0500e+03 1.4571e+03 4.0737e+00 --1.0500e+03 1.4857e+03 4.1855e+00 --1.0500e+03 1.5143e+03 4.2938e+00 --1.0500e+03 1.5429e+03 4.3988e+00 --1.0500e+03 1.5714e+03 4.5006e+00 --1.0500e+03 1.6000e+03 4.5993e+00 --1.0500e+03 1.6286e+03 4.6951e+00 --1.0500e+03 1.6571e+03 4.7880e+00 --1.0500e+03 1.6857e+03 4.8782e+00 --1.0500e+03 1.7143e+03 4.9657e+00 --1.0500e+03 1.7429e+03 5.0507e+00 --1.0500e+03 1.7714e+03 5.1332e+00 --1.0500e+03 1.8000e+03 5.2134e+00 --1.0500e+03 1.8286e+03 5.2913e+00 --1.0500e+03 1.8571e+03 5.3669e+00 --1.0500e+03 1.8857e+03 5.4405e+00 --1.0500e+03 1.9143e+03 5.5120e+00 --1.0500e+03 1.9429e+03 5.5816e+00 --1.0500e+03 1.9714e+03 5.6492e+00 --1.0500e+03 2.0000e+03 5.7151e+00 --1.0200e+03 -2.0000e+03 5.6794e+00 --1.0200e+03 -1.9714e+03 5.6120e+00 --1.0200e+03 -1.9429e+03 5.5427e+00 --1.0200e+03 -1.9143e+03 5.4715e+00 --1.0200e+03 -1.8857e+03 5.3982e+00 --1.0200e+03 -1.8571e+03 5.3227e+00 --1.0200e+03 -1.8286e+03 5.2450e+00 --1.0200e+03 -1.8000e+03 5.1650e+00 --1.0200e+03 -1.7714e+03 5.0826e+00 --1.0200e+03 -1.7429e+03 4.9977e+00 --1.0200e+03 -1.7143e+03 4.9103e+00 --1.0200e+03 -1.6857e+03 4.8201e+00 --1.0200e+03 -1.6571e+03 4.7271e+00 --1.0200e+03 -1.6286e+03 4.6311e+00 --1.0200e+03 -1.6000e+03 4.5322e+00 --1.0200e+03 -1.5714e+03 4.4301e+00 --1.0200e+03 -1.5429e+03 4.3247e+00 --1.0200e+03 -1.5143e+03 4.2159e+00 --1.0200e+03 -1.4857e+03 4.1035e+00 --1.0200e+03 -1.4571e+03 3.9874e+00 --1.0200e+03 -1.4286e+03 3.8675e+00 --1.0200e+03 -1.4000e+03 3.7436e+00 --1.0200e+03 -1.3714e+03 3.6156e+00 --1.0200e+03 -1.3429e+03 3.4833e+00 --1.0200e+03 -1.3143e+03 3.3464e+00 --1.0200e+03 -1.2857e+03 3.2050e+00 --1.0200e+03 -1.2571e+03 3.0587e+00 --1.0200e+03 -1.2286e+03 2.9075e+00 --1.0200e+03 -1.2000e+03 2.7511e+00 --1.0200e+03 -1.1714e+03 2.5894e+00 --1.0200e+03 -1.1429e+03 2.4222e+00 --1.0200e+03 -1.1143e+03 2.2493e+00 --1.0200e+03 -1.0857e+03 2.0706e+00 --1.0200e+03 -1.0571e+03 1.8858e+00 --1.0200e+03 -1.0286e+03 1.6950e+00 --1.0200e+03 -1.0000e+03 1.4978e+00 --1.0200e+03 -9.7143e+02 1.2943e+00 --1.0200e+03 -9.4286e+02 1.0842e+00 --1.0200e+03 -9.1429e+02 8.6755e-01 --1.0200e+03 -8.8571e+02 6.4427e-01 --1.0200e+03 -8.5714e+02 4.1434e-01 --1.0200e+03 -8.2857e+02 1.7780e-01 --1.0200e+03 -8.0000e+02 -6.5281e-02 --1.0200e+03 -7.7143e+02 -3.1476e-01 --1.0200e+03 -7.4286e+02 -5.7042e-01 --1.0200e+03 -7.1429e+02 -8.3201e-01 --1.0200e+03 -6.8571e+02 -1.0991e+00 --1.0200e+03 -6.5714e+02 -1.3714e+00 --1.0200e+03 -6.2857e+02 -1.6481e+00 --1.0200e+03 -6.0000e+02 -1.9287e+00 --1.0200e+03 -5.7143e+02 -2.2123e+00 --1.0200e+03 -5.4286e+02 -2.4981e+00 --1.0200e+03 -5.1429e+02 -2.7848e+00 --1.0200e+03 -4.8571e+02 -3.0714e+00 --1.0200e+03 -4.5714e+02 -3.3564e+00 --1.0200e+03 -4.2857e+02 -3.6383e+00 --1.0200e+03 -4.0000e+02 -3.9154e+00 --1.0200e+03 -3.7143e+02 -4.1861e+00 --1.0200e+03 -3.4286e+02 -4.4482e+00 --1.0200e+03 -3.1429e+02 -4.7000e+00 --1.0200e+03 -2.8571e+02 -4.9393e+00 --1.0200e+03 -2.5714e+02 -5.1639e+00 --1.0200e+03 -2.2857e+02 -5.3719e+00 --1.0200e+03 -2.0000e+02 -5.5612e+00 --1.0200e+03 -1.7143e+02 -5.7298e+00 --1.0200e+03 -1.4286e+02 -5.8759e+00 --1.0200e+03 -1.1429e+02 -5.9978e+00 --1.0200e+03 -8.5714e+01 -6.0942e+00 --1.0200e+03 -5.7143e+01 -6.1639e+00 --1.0200e+03 -2.8571e+01 -6.2060e+00 --1.0200e+03 0.0000e+00 -6.2201e+00 --1.0200e+03 2.8571e+01 -6.2060e+00 --1.0200e+03 5.7143e+01 -6.1639e+00 --1.0200e+03 8.5714e+01 -6.0942e+00 --1.0200e+03 1.1429e+02 -5.9978e+00 --1.0200e+03 1.4286e+02 -5.8759e+00 --1.0200e+03 1.7143e+02 -5.7298e+00 --1.0200e+03 2.0000e+02 -5.5612e+00 --1.0200e+03 2.2857e+02 -5.3719e+00 --1.0200e+03 2.5714e+02 -5.1639e+00 --1.0200e+03 2.8571e+02 -4.9393e+00 --1.0200e+03 3.1429e+02 -4.7000e+00 --1.0200e+03 3.4286e+02 -4.4482e+00 --1.0200e+03 3.7143e+02 -4.1861e+00 --1.0200e+03 4.0000e+02 -3.9154e+00 --1.0200e+03 4.2857e+02 -3.6383e+00 --1.0200e+03 4.5714e+02 -3.3564e+00 --1.0200e+03 4.8571e+02 -3.0714e+00 --1.0200e+03 5.1429e+02 -2.7848e+00 --1.0200e+03 5.4286e+02 -2.4981e+00 --1.0200e+03 5.7143e+02 -2.2123e+00 --1.0200e+03 6.0000e+02 -1.9287e+00 --1.0200e+03 6.2857e+02 -1.6481e+00 --1.0200e+03 6.5714e+02 -1.3714e+00 --1.0200e+03 6.8571e+02 -1.0991e+00 --1.0200e+03 7.1429e+02 -8.3201e-01 --1.0200e+03 7.4286e+02 -5.7042e-01 --1.0200e+03 7.7143e+02 -3.1476e-01 --1.0200e+03 8.0000e+02 -6.5281e-02 --1.0200e+03 8.2857e+02 1.7780e-01 --1.0200e+03 8.5714e+02 4.1434e-01 --1.0200e+03 8.8571e+02 6.4427e-01 --1.0200e+03 9.1429e+02 8.6755e-01 --1.0200e+03 9.4286e+02 1.0842e+00 --1.0200e+03 9.7143e+02 1.2943e+00 --1.0200e+03 1.0000e+03 1.4978e+00 --1.0200e+03 1.0286e+03 1.6950e+00 --1.0200e+03 1.0571e+03 1.8858e+00 --1.0200e+03 1.0857e+03 2.0706e+00 --1.0200e+03 1.1143e+03 2.2493e+00 --1.0200e+03 1.1429e+03 2.4222e+00 --1.0200e+03 1.1714e+03 2.5894e+00 --1.0200e+03 1.2000e+03 2.7511e+00 --1.0200e+03 1.2286e+03 2.9075e+00 --1.0200e+03 1.2571e+03 3.0587e+00 --1.0200e+03 1.2857e+03 3.2050e+00 --1.0200e+03 1.3143e+03 3.3464e+00 --1.0200e+03 1.3429e+03 3.4833e+00 --1.0200e+03 1.3714e+03 3.6156e+00 --1.0200e+03 1.4000e+03 3.7436e+00 --1.0200e+03 1.4286e+03 3.8675e+00 --1.0200e+03 1.4571e+03 3.9874e+00 --1.0200e+03 1.4857e+03 4.1035e+00 --1.0200e+03 1.5143e+03 4.2159e+00 --1.0200e+03 1.5429e+03 4.3247e+00 --1.0200e+03 1.5714e+03 4.4301e+00 --1.0200e+03 1.6000e+03 4.5322e+00 --1.0200e+03 1.6286e+03 4.6311e+00 --1.0200e+03 1.6571e+03 4.7271e+00 --1.0200e+03 1.6857e+03 4.8201e+00 --1.0200e+03 1.7143e+03 4.9103e+00 --1.0200e+03 1.7429e+03 4.9977e+00 --1.0200e+03 1.7714e+03 5.0826e+00 --1.0200e+03 1.8000e+03 5.1650e+00 --1.0200e+03 1.8286e+03 5.2450e+00 --1.0200e+03 1.8571e+03 5.3227e+00 --1.0200e+03 1.8857e+03 5.3982e+00 --1.0200e+03 1.9143e+03 5.4715e+00 --1.0200e+03 1.9429e+03 5.5427e+00 --1.0200e+03 1.9714e+03 5.6120e+00 --1.0200e+03 2.0000e+03 5.6794e+00 --9.9000e+02 -2.0000e+03 5.6439e+00 --9.9000e+02 -1.9714e+03 5.5750e+00 --9.9000e+02 -1.9429e+03 5.5041e+00 --9.9000e+02 -1.9143e+03 5.4312e+00 --9.9000e+02 -1.8857e+03 5.3560e+00 --9.9000e+02 -1.8571e+03 5.2787e+00 --9.9000e+02 -1.8286e+03 5.1989e+00 --9.9000e+02 -1.8000e+03 5.1168e+00 --9.9000e+02 -1.7714e+03 5.0321e+00 --9.9000e+02 -1.7429e+03 4.9448e+00 --9.9000e+02 -1.7143e+03 4.8548e+00 --9.9000e+02 -1.6857e+03 4.7619e+00 --9.9000e+02 -1.6571e+03 4.6660e+00 --9.9000e+02 -1.6286e+03 4.5670e+00 --9.9000e+02 -1.6000e+03 4.4648e+00 --9.9000e+02 -1.5714e+03 4.3592e+00 --9.9000e+02 -1.5429e+03 4.2502e+00 --9.9000e+02 -1.5143e+03 4.1374e+00 --9.9000e+02 -1.4857e+03 4.0209e+00 --9.9000e+02 -1.4571e+03 3.9004e+00 --9.9000e+02 -1.4286e+03 3.7757e+00 --9.9000e+02 -1.4000e+03 3.6468e+00 --9.9000e+02 -1.3714e+03 3.5133e+00 --9.9000e+02 -1.3429e+03 3.3752e+00 --9.9000e+02 -1.3143e+03 3.2322e+00 --9.9000e+02 -1.2857e+03 3.0842e+00 --9.9000e+02 -1.2571e+03 2.9309e+00 --9.9000e+02 -1.2286e+03 2.7721e+00 --9.9000e+02 -1.2000e+03 2.6077e+00 --9.9000e+02 -1.1714e+03 2.4373e+00 --9.9000e+02 -1.1429e+03 2.2608e+00 --9.9000e+02 -1.1143e+03 2.0780e+00 --9.9000e+02 -1.0857e+03 1.8887e+00 --9.9000e+02 -1.0571e+03 1.6926e+00 --9.9000e+02 -1.0286e+03 1.4896e+00 --9.9000e+02 -1.0000e+03 1.2794e+00 --9.9000e+02 -9.7143e+02 1.0619e+00 --9.9000e+02 -9.4286e+02 8.3695e-01 --9.9000e+02 -9.1429e+02 6.0433e-01 --9.9000e+02 -8.8571e+02 3.6398e-01 --9.9000e+02 -8.5714e+02 1.1583e-01 --9.9000e+02 -8.2857e+02 -1.4017e-01 --9.9000e+02 -8.0000e+02 -4.0398e-01 --9.9000e+02 -7.7143e+02 -6.7552e-01 --9.9000e+02 -7.4286e+02 -9.5465e-01 --9.9000e+02 -7.1429e+02 -1.2411e+00 --9.9000e+02 -6.8571e+02 -1.5346e+00 --9.9000e+02 -6.5714e+02 -1.8346e+00 --9.9000e+02 -6.2857e+02 -2.1407e+00 --9.9000e+02 -6.0000e+02 -2.4521e+00 --9.9000e+02 -5.7143e+02 -2.7679e+00 --9.9000e+02 -5.4286e+02 -3.0871e+00 --9.9000e+02 -5.1429e+02 -3.4086e+00 --9.9000e+02 -4.8571e+02 -3.7311e+00 --9.9000e+02 -4.5714e+02 -4.0529e+00 --9.9000e+02 -4.2857e+02 -4.3723e+00 --9.9000e+02 -4.0000e+02 -4.6874e+00 --9.9000e+02 -3.7143e+02 -4.9961e+00 --9.9000e+02 -3.4286e+02 -5.2961e+00 --9.9000e+02 -3.1429e+02 -5.5852e+00 --9.9000e+02 -2.8571e+02 -5.8607e+00 --9.9000e+02 -2.5714e+02 -6.1201e+00 --9.9000e+02 -2.2857e+02 -6.3609e+00 --9.9000e+02 -2.0000e+02 -6.5806e+00 --9.9000e+02 -1.7143e+02 -6.7767e+00 --9.9000e+02 -1.4286e+02 -6.9469e+00 --9.9000e+02 -1.1429e+02 -7.0892e+00 --9.9000e+02 -8.5714e+01 -7.2019e+00 --9.9000e+02 -5.7143e+01 -7.2834e+00 --9.9000e+02 -2.8571e+01 -7.3327e+00 --9.9000e+02 0.0000e+00 -7.3493e+00 --9.9000e+02 2.8571e+01 -7.3327e+00 --9.9000e+02 5.7143e+01 -7.2834e+00 --9.9000e+02 8.5714e+01 -7.2019e+00 --9.9000e+02 1.1429e+02 -7.0892e+00 --9.9000e+02 1.4286e+02 -6.9469e+00 --9.9000e+02 1.7143e+02 -6.7767e+00 --9.9000e+02 2.0000e+02 -6.5806e+00 --9.9000e+02 2.2857e+02 -6.3609e+00 --9.9000e+02 2.5714e+02 -6.1201e+00 --9.9000e+02 2.8571e+02 -5.8607e+00 --9.9000e+02 3.1429e+02 -5.5852e+00 --9.9000e+02 3.4286e+02 -5.2961e+00 --9.9000e+02 3.7143e+02 -4.9961e+00 --9.9000e+02 4.0000e+02 -4.6874e+00 --9.9000e+02 4.2857e+02 -4.3723e+00 --9.9000e+02 4.5714e+02 -4.0529e+00 --9.9000e+02 4.8571e+02 -3.7311e+00 --9.9000e+02 5.1429e+02 -3.4086e+00 --9.9000e+02 5.4286e+02 -3.0871e+00 --9.9000e+02 5.7143e+02 -2.7679e+00 --9.9000e+02 6.0000e+02 -2.4521e+00 --9.9000e+02 6.2857e+02 -2.1407e+00 --9.9000e+02 6.5714e+02 -1.8346e+00 --9.9000e+02 6.8571e+02 -1.5346e+00 --9.9000e+02 7.1429e+02 -1.2411e+00 --9.9000e+02 7.4286e+02 -9.5465e-01 --9.9000e+02 7.7143e+02 -6.7552e-01 --9.9000e+02 8.0000e+02 -4.0398e-01 --9.9000e+02 8.2857e+02 -1.4017e-01 --9.9000e+02 8.5714e+02 1.1583e-01 --9.9000e+02 8.8571e+02 3.6398e-01 --9.9000e+02 9.1429e+02 6.0433e-01 --9.9000e+02 9.4286e+02 8.3695e-01 --9.9000e+02 9.7143e+02 1.0619e+00 --9.9000e+02 1.0000e+03 1.2794e+00 --9.9000e+02 1.0286e+03 1.4896e+00 --9.9000e+02 1.0571e+03 1.6926e+00 --9.9000e+02 1.0857e+03 1.8887e+00 --9.9000e+02 1.1143e+03 2.0780e+00 --9.9000e+02 1.1429e+03 2.2608e+00 --9.9000e+02 1.1714e+03 2.4373e+00 --9.9000e+02 1.2000e+03 2.6077e+00 --9.9000e+02 1.2286e+03 2.7721e+00 --9.9000e+02 1.2571e+03 2.9309e+00 --9.9000e+02 1.2857e+03 3.0842e+00 --9.9000e+02 1.3143e+03 3.2322e+00 --9.9000e+02 1.3429e+03 3.3752e+00 --9.9000e+02 1.3714e+03 3.5133e+00 --9.9000e+02 1.4000e+03 3.6468e+00 --9.9000e+02 1.4286e+03 3.7757e+00 --9.9000e+02 1.4571e+03 3.9004e+00 --9.9000e+02 1.4857e+03 4.0209e+00 --9.9000e+02 1.5143e+03 4.1374e+00 --9.9000e+02 1.5429e+03 4.2502e+00 --9.9000e+02 1.5714e+03 4.3592e+00 --9.9000e+02 1.6000e+03 4.4648e+00 --9.9000e+02 1.6286e+03 4.5670e+00 --9.9000e+02 1.6571e+03 4.6660e+00 --9.9000e+02 1.6857e+03 4.7619e+00 --9.9000e+02 1.7143e+03 4.8548e+00 --9.9000e+02 1.7429e+03 4.9448e+00 --9.9000e+02 1.7714e+03 5.0321e+00 --9.9000e+02 1.8000e+03 5.1168e+00 --9.9000e+02 1.8286e+03 5.1989e+00 --9.9000e+02 1.8571e+03 5.2787e+00 --9.9000e+02 1.8857e+03 5.3560e+00 --9.9000e+02 1.9143e+03 5.4312e+00 --9.9000e+02 1.9429e+03 5.5041e+00 --9.9000e+02 1.9714e+03 5.5750e+00 --9.9000e+02 2.0000e+03 5.6439e+00 --9.6000e+02 -2.0000e+03 5.6088e+00 --9.6000e+02 -1.9714e+03 5.5383e+00 --9.6000e+02 -1.9429e+03 5.4658e+00 --9.6000e+02 -1.9143e+03 5.3911e+00 --9.6000e+02 -1.8857e+03 5.3141e+00 --9.6000e+02 -1.8571e+03 5.2348e+00 --9.6000e+02 -1.8286e+03 5.1531e+00 --9.6000e+02 -1.8000e+03 5.0687e+00 --9.6000e+02 -1.7714e+03 4.9818e+00 --9.6000e+02 -1.7429e+03 4.8920e+00 --9.6000e+02 -1.7143e+03 4.7994e+00 --9.6000e+02 -1.6857e+03 4.7037e+00 --9.6000e+02 -1.6571e+03 4.6049e+00 --9.6000e+02 -1.6286e+03 4.5028e+00 --9.6000e+02 -1.6000e+03 4.3973e+00 --9.6000e+02 -1.5714e+03 4.2882e+00 --9.6000e+02 -1.5429e+03 4.1753e+00 --9.6000e+02 -1.5143e+03 4.0585e+00 --9.6000e+02 -1.4857e+03 3.9377e+00 --9.6000e+02 -1.4571e+03 3.8126e+00 --9.6000e+02 -1.4286e+03 3.6831e+00 --9.6000e+02 -1.4000e+03 3.5489e+00 --9.6000e+02 -1.3714e+03 3.4099e+00 --9.6000e+02 -1.3429e+03 3.2658e+00 --9.6000e+02 -1.3143e+03 3.1164e+00 --9.6000e+02 -1.2857e+03 2.9615e+00 --9.6000e+02 -1.2571e+03 2.8008e+00 --9.6000e+02 -1.2286e+03 2.6341e+00 --9.6000e+02 -1.2000e+03 2.4612e+00 --9.6000e+02 -1.1714e+03 2.2817e+00 --9.6000e+02 -1.1429e+03 2.0955e+00 --9.6000e+02 -1.1143e+03 1.9022e+00 --9.6000e+02 -1.0857e+03 1.7016e+00 --9.6000e+02 -1.0571e+03 1.4934e+00 --9.6000e+02 -1.0286e+03 1.2774e+00 --9.6000e+02 -1.0000e+03 1.0533e+00 --9.6000e+02 -9.7143e+02 8.2075e-01 --9.6000e+02 -9.4286e+02 5.7963e-01 --9.6000e+02 -9.1429e+02 3.2971e-01 --9.6000e+02 -8.8571e+02 7.0775e-02 --9.6000e+02 -8.5714e+02 -1.9731e-01 --9.6000e+02 -8.2857e+02 -4.7467e-01 --9.6000e+02 -8.0000e+02 -7.6135e-01 --9.6000e+02 -7.7143e+02 -1.0574e+00 --9.6000e+02 -7.4286e+02 -1.3626e+00 --9.6000e+02 -7.1429e+02 -1.6769e+00 --9.6000e+02 -6.8571e+02 -2.0000e+00 --9.6000e+02 -6.5714e+02 -2.3315e+00 --9.6000e+02 -6.2857e+02 -2.6708e+00 --9.6000e+02 -6.0000e+02 -3.0172e+00 --9.6000e+02 -5.7143e+02 -3.3700e+00 --9.6000e+02 -5.4286e+02 -3.7279e+00 --9.6000e+02 -5.1429e+02 -4.0897e+00 --9.6000e+02 -4.8571e+02 -4.4539e+00 --9.6000e+02 -4.5714e+02 -4.8188e+00 --9.6000e+02 -4.2857e+02 -5.1824e+00 --9.6000e+02 -4.0000e+02 -5.5424e+00 --9.6000e+02 -3.7143e+02 -5.8964e+00 --9.6000e+02 -3.4286e+02 -6.2418e+00 --9.6000e+02 -3.1429e+02 -6.5756e+00 --9.6000e+02 -2.8571e+02 -6.8948e+00 --9.6000e+02 -2.5714e+02 -7.1964e+00 --9.6000e+02 -2.2857e+02 -7.4771e+00 --9.6000e+02 -2.0000e+02 -7.7339e+00 --9.6000e+02 -1.7143e+02 -7.9636e+00 --9.6000e+02 -1.4286e+02 -8.1635e+00 --9.6000e+02 -1.1429e+02 -8.3309e+00 --9.6000e+02 -8.5714e+01 -8.4636e+00 --9.6000e+02 -5.7143e+01 -8.5597e+00 --9.6000e+02 -2.8571e+01 -8.6180e+00 --9.6000e+02 0.0000e+00 -8.6375e+00 --9.6000e+02 2.8571e+01 -8.6180e+00 --9.6000e+02 5.7143e+01 -8.5597e+00 --9.6000e+02 8.5714e+01 -8.4636e+00 --9.6000e+02 1.1429e+02 -8.3309e+00 --9.6000e+02 1.4286e+02 -8.1635e+00 --9.6000e+02 1.7143e+02 -7.9636e+00 --9.6000e+02 2.0000e+02 -7.7339e+00 --9.6000e+02 2.2857e+02 -7.4771e+00 --9.6000e+02 2.5714e+02 -7.1964e+00 --9.6000e+02 2.8571e+02 -6.8948e+00 --9.6000e+02 3.1429e+02 -6.5756e+00 --9.6000e+02 3.4286e+02 -6.2418e+00 --9.6000e+02 3.7143e+02 -5.8964e+00 --9.6000e+02 4.0000e+02 -5.5424e+00 --9.6000e+02 4.2857e+02 -5.1824e+00 --9.6000e+02 4.5714e+02 -4.8188e+00 --9.6000e+02 4.8571e+02 -4.4539e+00 --9.6000e+02 5.1429e+02 -4.0897e+00 --9.6000e+02 5.4286e+02 -3.7279e+00 --9.6000e+02 5.7143e+02 -3.3700e+00 --9.6000e+02 6.0000e+02 -3.0172e+00 --9.6000e+02 6.2857e+02 -2.6708e+00 --9.6000e+02 6.5714e+02 -2.3315e+00 --9.6000e+02 6.8571e+02 -2.0000e+00 --9.6000e+02 7.1429e+02 -1.6769e+00 --9.6000e+02 7.4286e+02 -1.3626e+00 --9.6000e+02 7.7143e+02 -1.0574e+00 --9.6000e+02 8.0000e+02 -7.6135e-01 --9.6000e+02 8.2857e+02 -4.7467e-01 --9.6000e+02 8.5714e+02 -1.9731e-01 --9.6000e+02 8.8571e+02 7.0775e-02 --9.6000e+02 9.1429e+02 3.2971e-01 --9.6000e+02 9.4286e+02 5.7963e-01 --9.6000e+02 9.7143e+02 8.2075e-01 --9.6000e+02 1.0000e+03 1.0533e+00 --9.6000e+02 1.0286e+03 1.2774e+00 --9.6000e+02 1.0571e+03 1.4934e+00 --9.6000e+02 1.0857e+03 1.7016e+00 --9.6000e+02 1.1143e+03 1.9022e+00 --9.6000e+02 1.1429e+03 2.0955e+00 --9.6000e+02 1.1714e+03 2.2817e+00 --9.6000e+02 1.2000e+03 2.4612e+00 --9.6000e+02 1.2286e+03 2.6341e+00 --9.6000e+02 1.2571e+03 2.8008e+00 --9.6000e+02 1.2857e+03 2.9615e+00 --9.6000e+02 1.3143e+03 3.1164e+00 --9.6000e+02 1.3429e+03 3.2658e+00 --9.6000e+02 1.3714e+03 3.4099e+00 --9.6000e+02 1.4000e+03 3.5489e+00 --9.6000e+02 1.4286e+03 3.6831e+00 --9.6000e+02 1.4571e+03 3.8126e+00 --9.6000e+02 1.4857e+03 3.9377e+00 --9.6000e+02 1.5143e+03 4.0585e+00 --9.6000e+02 1.5429e+03 4.1753e+00 --9.6000e+02 1.5714e+03 4.2882e+00 --9.6000e+02 1.6000e+03 4.3973e+00 --9.6000e+02 1.6286e+03 4.5028e+00 --9.6000e+02 1.6571e+03 4.6049e+00 --9.6000e+02 1.6857e+03 4.7037e+00 --9.6000e+02 1.7143e+03 4.7994e+00 --9.6000e+02 1.7429e+03 4.8920e+00 --9.6000e+02 1.7714e+03 4.9818e+00 --9.6000e+02 1.8000e+03 5.0687e+00 --9.6000e+02 1.8286e+03 5.1531e+00 --9.6000e+02 1.8571e+03 5.2348e+00 --9.6000e+02 1.8857e+03 5.3141e+00 --9.6000e+02 1.9143e+03 5.3911e+00 --9.6000e+02 1.9429e+03 5.4658e+00 --9.6000e+02 1.9714e+03 5.5383e+00 --9.6000e+02 2.0000e+03 5.6088e+00 --9.3000e+02 -2.0000e+03 5.5740e+00 --9.3000e+02 -1.9714e+03 5.5020e+00 --9.3000e+02 -1.9429e+03 5.4278e+00 --9.3000e+02 -1.9143e+03 5.3513e+00 --9.3000e+02 -1.8857e+03 5.2725e+00 --9.3000e+02 -1.8571e+03 5.1913e+00 --9.3000e+02 -1.8286e+03 5.1074e+00 --9.3000e+02 -1.8000e+03 5.0209e+00 --9.3000e+02 -1.7714e+03 4.9316e+00 --9.3000e+02 -1.7429e+03 4.8394e+00 --9.3000e+02 -1.7143e+03 4.7441e+00 --9.3000e+02 -1.6857e+03 4.6456e+00 --9.3000e+02 -1.6571e+03 4.5438e+00 --9.3000e+02 -1.6286e+03 4.4386e+00 --9.3000e+02 -1.6000e+03 4.3296e+00 --9.3000e+02 -1.5714e+03 4.2169e+00 --9.3000e+02 -1.5429e+03 4.1002e+00 --9.3000e+02 -1.5143e+03 3.9793e+00 --9.3000e+02 -1.4857e+03 3.8541e+00 --9.3000e+02 -1.4571e+03 3.7243e+00 --9.3000e+02 -1.4286e+03 3.5897e+00 --9.3000e+02 -1.4000e+03 3.4501e+00 --9.3000e+02 -1.3714e+03 3.3053e+00 --9.3000e+02 -1.3429e+03 3.1550e+00 --9.3000e+02 -1.3143e+03 2.9989e+00 --9.3000e+02 -1.2857e+03 2.8369e+00 --9.3000e+02 -1.2571e+03 2.6685e+00 --9.3000e+02 -1.2286e+03 2.4936e+00 --9.3000e+02 -1.2000e+03 2.3118e+00 --9.3000e+02 -1.1714e+03 2.1227e+00 --9.3000e+02 -1.1429e+03 1.9262e+00 --9.3000e+02 -1.1143e+03 1.7218e+00 --9.3000e+02 -1.0857e+03 1.5093e+00 --9.3000e+02 -1.0571e+03 1.2882e+00 --9.3000e+02 -1.0286e+03 1.0583e+00 --9.3000e+02 -1.0000e+03 8.1915e-01 --9.3000e+02 -9.7143e+02 5.7048e-01 --9.3000e+02 -9.4286e+02 3.1195e-01 --9.3000e+02 -9.1429e+02 4.3236e-02 --9.3000e+02 -8.8571e+02 -2.3594e-01 --9.3000e+02 -8.5714e+02 -5.2583e-01 --9.3000e+02 -8.2857e+02 -8.2665e-01 --9.3000e+02 -8.0000e+02 -1.1386e+00 --9.3000e+02 -7.7143e+02 -1.4617e+00 --9.3000e+02 -7.4286e+02 -1.7961e+00 --9.3000e+02 -7.1429e+02 -2.1415e+00 --9.3000e+02 -6.8571e+02 -2.4980e+00 --9.3000e+02 -6.5714e+02 -2.8650e+00 --9.3000e+02 -6.2857e+02 -3.2422e+00 --9.3000e+02 -6.0000e+02 -3.6288e+00 --9.3000e+02 -5.7143e+02 -4.0240e+00 --9.3000e+02 -5.4286e+02 -4.4267e+00 --9.3000e+02 -5.1429e+02 -4.8354e+00 --9.3000e+02 -4.8571e+02 -5.2486e+00 --9.3000e+02 -4.5714e+02 -5.6643e+00 --9.3000e+02 -4.2857e+02 -6.0802e+00 --9.3000e+02 -4.0000e+02 -6.4937e+00 --9.3000e+02 -3.7143e+02 -6.9019e+00 --9.3000e+02 -3.4286e+02 -7.3018e+00 --9.3000e+02 -3.1429e+02 -7.6897e+00 --9.3000e+02 -2.8571e+02 -8.0621e+00 --9.3000e+02 -2.5714e+02 -8.4151e+00 --9.3000e+02 -2.2857e+02 -8.7448e+00 --9.3000e+02 -2.0000e+02 -9.0472e+00 --9.3000e+02 -1.7143e+02 -9.3185e+00 --9.3000e+02 -1.4286e+02 -9.5551e+00 --9.3000e+02 -1.1429e+02 -9.7537e+00 --9.3000e+02 -8.5714e+01 -9.9113e+00 --9.3000e+02 -5.7143e+01 -1.0026e+01 --9.3000e+02 -2.8571e+01 -1.0095e+01 --9.3000e+02 0.0000e+00 -1.0118e+01 --9.3000e+02 2.8571e+01 -1.0095e+01 --9.3000e+02 5.7143e+01 -1.0026e+01 --9.3000e+02 8.5714e+01 -9.9113e+00 --9.3000e+02 1.1429e+02 -9.7537e+00 --9.3000e+02 1.4286e+02 -9.5551e+00 --9.3000e+02 1.7143e+02 -9.3185e+00 --9.3000e+02 2.0000e+02 -9.0472e+00 --9.3000e+02 2.2857e+02 -8.7448e+00 --9.3000e+02 2.5714e+02 -8.4151e+00 --9.3000e+02 2.8571e+02 -8.0621e+00 --9.3000e+02 3.1429e+02 -7.6897e+00 --9.3000e+02 3.4286e+02 -7.3018e+00 --9.3000e+02 3.7143e+02 -6.9019e+00 --9.3000e+02 4.0000e+02 -6.4937e+00 --9.3000e+02 4.2857e+02 -6.0802e+00 --9.3000e+02 4.5714e+02 -5.6643e+00 --9.3000e+02 4.8571e+02 -5.2486e+00 --9.3000e+02 5.1429e+02 -4.8354e+00 --9.3000e+02 5.4286e+02 -4.4267e+00 --9.3000e+02 5.7143e+02 -4.0240e+00 --9.3000e+02 6.0000e+02 -3.6288e+00 --9.3000e+02 6.2857e+02 -3.2422e+00 --9.3000e+02 6.5714e+02 -2.8650e+00 --9.3000e+02 6.8571e+02 -2.4980e+00 --9.3000e+02 7.1429e+02 -2.1415e+00 --9.3000e+02 7.4286e+02 -1.7961e+00 --9.3000e+02 7.7143e+02 -1.4617e+00 --9.3000e+02 8.0000e+02 -1.1386e+00 --9.3000e+02 8.2857e+02 -8.2665e-01 --9.3000e+02 8.5714e+02 -5.2583e-01 --9.3000e+02 8.8571e+02 -2.3594e-01 --9.3000e+02 9.1429e+02 4.3236e-02 --9.3000e+02 9.4286e+02 3.1195e-01 --9.3000e+02 9.7143e+02 5.7048e-01 --9.3000e+02 1.0000e+03 8.1915e-01 --9.3000e+02 1.0286e+03 1.0583e+00 --9.3000e+02 1.0571e+03 1.2882e+00 --9.3000e+02 1.0857e+03 1.5093e+00 --9.3000e+02 1.1143e+03 1.7218e+00 --9.3000e+02 1.1429e+03 1.9262e+00 --9.3000e+02 1.1714e+03 2.1227e+00 --9.3000e+02 1.2000e+03 2.3118e+00 --9.3000e+02 1.2286e+03 2.4936e+00 --9.3000e+02 1.2571e+03 2.6685e+00 --9.3000e+02 1.2857e+03 2.8369e+00 --9.3000e+02 1.3143e+03 2.9989e+00 --9.3000e+02 1.3429e+03 3.1550e+00 --9.3000e+02 1.3714e+03 3.3053e+00 --9.3000e+02 1.4000e+03 3.4501e+00 --9.3000e+02 1.4286e+03 3.5897e+00 --9.3000e+02 1.4571e+03 3.7243e+00 --9.3000e+02 1.4857e+03 3.8541e+00 --9.3000e+02 1.5143e+03 3.9793e+00 --9.3000e+02 1.5429e+03 4.1002e+00 --9.3000e+02 1.5714e+03 4.2169e+00 --9.3000e+02 1.6000e+03 4.3296e+00 --9.3000e+02 1.6286e+03 4.4386e+00 --9.3000e+02 1.6571e+03 4.5438e+00 --9.3000e+02 1.6857e+03 4.6456e+00 --9.3000e+02 1.7143e+03 4.7441e+00 --9.3000e+02 1.7429e+03 4.8394e+00 --9.3000e+02 1.7714e+03 4.9316e+00 --9.3000e+02 1.8000e+03 5.0209e+00 --9.3000e+02 1.8286e+03 5.1074e+00 --9.3000e+02 1.8571e+03 5.1913e+00 --9.3000e+02 1.8857e+03 5.2725e+00 --9.3000e+02 1.9143e+03 5.3513e+00 --9.3000e+02 1.9429e+03 5.4278e+00 --9.3000e+02 1.9714e+03 5.5020e+00 --9.3000e+02 2.0000e+03 5.5740e+00 --9.0000e+02 -2.0000e+03 5.5395e+00 --9.0000e+02 -1.9714e+03 5.4660e+00 --9.0000e+02 -1.9429e+03 5.3901e+00 --9.0000e+02 -1.9143e+03 5.3119e+00 --9.0000e+02 -1.8857e+03 5.2313e+00 --9.0000e+02 -1.8571e+03 5.1480e+00 --9.0000e+02 -1.8286e+03 5.0621e+00 --9.0000e+02 -1.8000e+03 4.9734e+00 --9.0000e+02 -1.7714e+03 4.8817e+00 --9.0000e+02 -1.7429e+03 4.7870e+00 --9.0000e+02 -1.7143e+03 4.6891e+00 --9.0000e+02 -1.6857e+03 4.5878e+00 --9.0000e+02 -1.6571e+03 4.4829e+00 --9.0000e+02 -1.6286e+03 4.3744e+00 --9.0000e+02 -1.6000e+03 4.2621e+00 --9.0000e+02 -1.5714e+03 4.1457e+00 --9.0000e+02 -1.5429e+03 4.0250e+00 --9.0000e+02 -1.5143e+03 3.8999e+00 --9.0000e+02 -1.4857e+03 3.7702e+00 --9.0000e+02 -1.4571e+03 3.6355e+00 --9.0000e+02 -1.4286e+03 3.4958e+00 --9.0000e+02 -1.4000e+03 3.3506e+00 --9.0000e+02 -1.3714e+03 3.1998e+00 --9.0000e+02 -1.3429e+03 3.0431e+00 --9.0000e+02 -1.3143e+03 2.8801e+00 --9.0000e+02 -1.2857e+03 2.7106e+00 --9.0000e+02 -1.2571e+03 2.5342e+00 --9.0000e+02 -1.2286e+03 2.3507e+00 --9.0000e+02 -1.2000e+03 2.1595e+00 --9.0000e+02 -1.1714e+03 1.9605e+00 --9.0000e+02 -1.1429e+03 1.7531e+00 --9.0000e+02 -1.1143e+03 1.5370e+00 --9.0000e+02 -1.0857e+03 1.3117e+00 --9.0000e+02 -1.0571e+03 1.0770e+00 --9.0000e+02 -1.0286e+03 8.3218e-01 --9.0000e+02 -1.0000e+03 5.7699e-01 --9.0000e+02 -9.7143e+02 3.1094e-01 --9.0000e+02 -9.4286e+02 3.3585e-02 --9.0000e+02 -9.1429e+02 -2.5550e-01 --9.0000e+02 -8.8571e+02 -5.5672e-01 --9.0000e+02 -8.5714e+02 -8.7046e-01 --9.0000e+02 -8.2857e+02 -1.1971e+00 --9.0000e+02 -8.0000e+02 -1.5369e+00 --9.0000e+02 -7.7143e+02 -1.8901e+00 --9.0000e+02 -7.4286e+02 -2.2570e+00 --9.0000e+02 -7.1429e+02 -2.6374e+00 --9.0000e+02 -6.8571e+02 -3.0315e+00 --9.0000e+02 -6.5714e+02 -3.4389e+00 --9.0000e+02 -6.2857e+02 -3.8593e+00 --9.0000e+02 -6.0000e+02 -4.2921e+00 --9.0000e+02 -5.7143e+02 -4.7363e+00 --9.0000e+02 -5.4286e+02 -5.1910e+00 --9.0000e+02 -5.1429e+02 -5.6546e+00 --9.0000e+02 -4.8571e+02 -6.1253e+00 --9.0000e+02 -4.5714e+02 -6.6010e+00 --9.0000e+02 -4.2857e+02 -7.0792e+00 --9.0000e+02 -4.0000e+02 -7.5568e+00 --9.0000e+02 -3.7143e+02 -8.0305e+00 --9.0000e+02 -3.4286e+02 -8.4963e+00 --9.0000e+02 -3.1429e+02 -8.9503e+00 --9.0000e+02 -2.8571e+02 -9.3878e+00 --9.0000e+02 -2.5714e+02 -9.8041e+00 --9.0000e+02 -2.2857e+02 -1.0194e+01 --9.0000e+02 -2.0000e+02 -1.0553e+01 --9.0000e+02 -1.7143e+02 -1.0876e+01 --9.0000e+02 -1.4286e+02 -1.1159e+01 --9.0000e+02 -1.1429e+02 -1.1397e+01 --9.0000e+02 -8.5714e+01 -1.1586e+01 --9.0000e+02 -5.7143e+01 -1.1723e+01 --9.0000e+02 -2.8571e+01 -1.1806e+01 --9.0000e+02 0.0000e+00 -1.1834e+01 --9.0000e+02 2.8571e+01 -1.1806e+01 --9.0000e+02 5.7143e+01 -1.1723e+01 --9.0000e+02 8.5714e+01 -1.1586e+01 --9.0000e+02 1.1429e+02 -1.1397e+01 --9.0000e+02 1.4286e+02 -1.1159e+01 --9.0000e+02 1.7143e+02 -1.0876e+01 --9.0000e+02 2.0000e+02 -1.0553e+01 --9.0000e+02 2.2857e+02 -1.0194e+01 --9.0000e+02 2.5714e+02 -9.8041e+00 --9.0000e+02 2.8571e+02 -9.3878e+00 --9.0000e+02 3.1429e+02 -8.9503e+00 --9.0000e+02 3.4286e+02 -8.4963e+00 --9.0000e+02 3.7143e+02 -8.0305e+00 --9.0000e+02 4.0000e+02 -7.5568e+00 --9.0000e+02 4.2857e+02 -7.0792e+00 --9.0000e+02 4.5714e+02 -6.6010e+00 --9.0000e+02 4.8571e+02 -6.1253e+00 --9.0000e+02 5.1429e+02 -5.6546e+00 --9.0000e+02 5.4286e+02 -5.1910e+00 --9.0000e+02 5.7143e+02 -4.7363e+00 --9.0000e+02 6.0000e+02 -4.2921e+00 --9.0000e+02 6.2857e+02 -3.8593e+00 --9.0000e+02 6.5714e+02 -3.4389e+00 --9.0000e+02 6.8571e+02 -3.0315e+00 --9.0000e+02 7.1429e+02 -2.6374e+00 --9.0000e+02 7.4286e+02 -2.2570e+00 --9.0000e+02 7.7143e+02 -1.8901e+00 --9.0000e+02 8.0000e+02 -1.5369e+00 --9.0000e+02 8.2857e+02 -1.1971e+00 --9.0000e+02 8.5714e+02 -8.7046e-01 --9.0000e+02 8.8571e+02 -5.5672e-01 --9.0000e+02 9.1429e+02 -2.5550e-01 --9.0000e+02 9.4286e+02 3.3585e-02 --9.0000e+02 9.7143e+02 3.1094e-01 --9.0000e+02 1.0000e+03 5.7699e-01 --9.0000e+02 1.0286e+03 8.3218e-01 --9.0000e+02 1.0571e+03 1.0770e+00 --9.0000e+02 1.0857e+03 1.3117e+00 --9.0000e+02 1.1143e+03 1.5370e+00 --9.0000e+02 1.1429e+03 1.7531e+00 --9.0000e+02 1.1714e+03 1.9605e+00 --9.0000e+02 1.2000e+03 2.1595e+00 --9.0000e+02 1.2286e+03 2.3507e+00 --9.0000e+02 1.2571e+03 2.5342e+00 --9.0000e+02 1.2857e+03 2.7106e+00 --9.0000e+02 1.3143e+03 2.8801e+00 --9.0000e+02 1.3429e+03 3.0431e+00 --9.0000e+02 1.3714e+03 3.1998e+00 --9.0000e+02 1.4000e+03 3.3506e+00 --9.0000e+02 1.4286e+03 3.4958e+00 --9.0000e+02 1.4571e+03 3.6355e+00 --9.0000e+02 1.4857e+03 3.7702e+00 --9.0000e+02 1.5143e+03 3.8999e+00 --9.0000e+02 1.5429e+03 4.0250e+00 --9.0000e+02 1.5714e+03 4.1457e+00 --9.0000e+02 1.6000e+03 4.2621e+00 --9.0000e+02 1.6286e+03 4.3744e+00 --9.0000e+02 1.6571e+03 4.4829e+00 --9.0000e+02 1.6857e+03 4.5878e+00 --9.0000e+02 1.7143e+03 4.6891e+00 --9.0000e+02 1.7429e+03 4.7870e+00 --9.0000e+02 1.7714e+03 4.8817e+00 --9.0000e+02 1.8000e+03 4.9734e+00 --9.0000e+02 1.8286e+03 5.0621e+00 --9.0000e+02 1.8571e+03 5.1480e+00 --9.0000e+02 1.8857e+03 5.2313e+00 --9.0000e+02 1.9143e+03 5.3119e+00 --9.0000e+02 1.9429e+03 5.3901e+00 --9.0000e+02 1.9714e+03 5.4660e+00 --9.0000e+02 2.0000e+03 5.5395e+00 --8.7000e+02 -2.0000e+03 5.5055e+00 --8.7000e+02 -1.9714e+03 5.4304e+00 --8.7000e+02 -1.9429e+03 5.3529e+00 --8.7000e+02 -1.9143e+03 5.2730e+00 --8.7000e+02 -1.8857e+03 5.1904e+00 --8.7000e+02 -1.8571e+03 5.1052e+00 --8.7000e+02 -1.8286e+03 5.0172e+00 --8.7000e+02 -1.8000e+03 4.9263e+00 --8.7000e+02 -1.7714e+03 4.8322e+00 --8.7000e+02 -1.7429e+03 4.7350e+00 --8.7000e+02 -1.7143e+03 4.6343e+00 --8.7000e+02 -1.6857e+03 4.5301e+00 --8.7000e+02 -1.6571e+03 4.4223e+00 --8.7000e+02 -1.6286e+03 4.3105e+00 --8.7000e+02 -1.6000e+03 4.1946e+00 --8.7000e+02 -1.5714e+03 4.0744e+00 --8.7000e+02 -1.5429e+03 3.9498e+00 --8.7000e+02 -1.5143e+03 3.8204e+00 --8.7000e+02 -1.4857e+03 3.6860e+00 --8.7000e+02 -1.4571e+03 3.5464e+00 --8.7000e+02 -1.4286e+03 3.4014e+00 --8.7000e+02 -1.4000e+03 3.2505e+00 --8.7000e+02 -1.3714e+03 3.0935e+00 --8.7000e+02 -1.3429e+03 2.9301e+00 --8.7000e+02 -1.3143e+03 2.7600e+00 --8.7000e+02 -1.2857e+03 2.5828e+00 --8.7000e+02 -1.2571e+03 2.3981e+00 --8.7000e+02 -1.2286e+03 2.2055e+00 --8.7000e+02 -1.2000e+03 2.0046e+00 --8.7000e+02 -1.1714e+03 1.7950e+00 --8.7000e+02 -1.1429e+03 1.5762e+00 --8.7000e+02 -1.1143e+03 1.3477e+00 --8.7000e+02 -1.0857e+03 1.1090e+00 --8.7000e+02 -1.0571e+03 8.5968e-01 --8.7000e+02 -1.0286e+03 5.9908e-01 --8.7000e+02 -1.0000e+03 3.2669e-01 --8.7000e+02 -9.7143e+02 4.1952e-02 --8.7000e+02 -9.4286e+02 -2.5571e-01 --8.7000e+02 -9.1429e+02 -5.6688e-01 --8.7000e+02 -8.8571e+02 -8.9211e-01 --8.7000e+02 -8.5714e+02 -1.2320e+00 --8.7000e+02 -8.2857e+02 -1.5870e+00 --8.7000e+02 -8.0000e+02 -1.9576e+00 --8.7000e+02 -7.7143e+02 -2.3443e+00 --8.7000e+02 -7.4286e+02 -2.7474e+00 --8.7000e+02 -7.1429e+02 -3.1672e+00 --8.7000e+02 -6.8571e+02 -3.6038e+00 --8.7000e+02 -6.5714e+02 -4.0571e+00 --8.7000e+02 -6.2857e+02 -4.5269e+00 --8.7000e+02 -6.0000e+02 -5.0127e+00 --8.7000e+02 -5.7143e+02 -5.5139e+00 --8.7000e+02 -5.4286e+02 -6.0291e+00 --8.7000e+02 -5.1429e+02 -6.5571e+00 --8.7000e+02 -4.8571e+02 -7.0959e+00 --8.7000e+02 -4.5714e+02 -7.6431e+00 --8.7000e+02 -4.2857e+02 -8.1959e+00 --8.7000e+02 -4.0000e+02 -8.7507e+00 --8.7000e+02 -3.7143e+02 -9.3037e+00 --8.7000e+02 -3.4286e+02 -9.8502e+00 --8.7000e+02 -3.1429e+02 -1.0385e+01 --8.7000e+02 -2.8571e+02 -1.0903e+01 --8.7000e+02 -2.5714e+02 -1.1398e+01 --8.7000e+02 -2.2857e+02 -1.1864e+01 --8.7000e+02 -2.0000e+02 -1.2294e+01 --8.7000e+02 -1.7143e+02 -1.2682e+01 --8.7000e+02 -1.4286e+02 -1.3022e+01 --8.7000e+02 -1.1429e+02 -1.3309e+01 --8.7000e+02 -8.5714e+01 -1.3538e+01 --8.7000e+02 -5.7143e+01 -1.3704e+01 --8.7000e+02 -2.8571e+01 -1.3806e+01 --8.7000e+02 0.0000e+00 -1.3839e+01 --8.7000e+02 2.8571e+01 -1.3806e+01 --8.7000e+02 5.7143e+01 -1.3704e+01 --8.7000e+02 8.5714e+01 -1.3538e+01 --8.7000e+02 1.1429e+02 -1.3309e+01 --8.7000e+02 1.4286e+02 -1.3022e+01 --8.7000e+02 1.7143e+02 -1.2682e+01 --8.7000e+02 2.0000e+02 -1.2294e+01 --8.7000e+02 2.2857e+02 -1.1864e+01 --8.7000e+02 2.5714e+02 -1.1398e+01 --8.7000e+02 2.8571e+02 -1.0903e+01 --8.7000e+02 3.1429e+02 -1.0385e+01 --8.7000e+02 3.4286e+02 -9.8502e+00 --8.7000e+02 3.7143e+02 -9.3037e+00 --8.7000e+02 4.0000e+02 -8.7507e+00 --8.7000e+02 4.2857e+02 -8.1959e+00 --8.7000e+02 4.5714e+02 -7.6431e+00 --8.7000e+02 4.8571e+02 -7.0959e+00 --8.7000e+02 5.1429e+02 -6.5571e+00 --8.7000e+02 5.4286e+02 -6.0291e+00 --8.7000e+02 5.7143e+02 -5.5139e+00 --8.7000e+02 6.0000e+02 -5.0127e+00 --8.7000e+02 6.2857e+02 -4.5269e+00 --8.7000e+02 6.5714e+02 -4.0571e+00 --8.7000e+02 6.8571e+02 -3.6038e+00 --8.7000e+02 7.1429e+02 -3.1672e+00 --8.7000e+02 7.4286e+02 -2.7474e+00 --8.7000e+02 7.7143e+02 -2.3443e+00 --8.7000e+02 8.0000e+02 -1.9576e+00 --8.7000e+02 8.2857e+02 -1.5870e+00 --8.7000e+02 8.5714e+02 -1.2320e+00 --8.7000e+02 8.8571e+02 -8.9211e-01 --8.7000e+02 9.1429e+02 -5.6688e-01 --8.7000e+02 9.4286e+02 -2.5571e-01 --8.7000e+02 9.7143e+02 4.1952e-02 --8.7000e+02 1.0000e+03 3.2669e-01 --8.7000e+02 1.0286e+03 5.9908e-01 --8.7000e+02 1.0571e+03 8.5968e-01 --8.7000e+02 1.0857e+03 1.1090e+00 --8.7000e+02 1.1143e+03 1.3477e+00 --8.7000e+02 1.1429e+03 1.5762e+00 --8.7000e+02 1.1714e+03 1.7950e+00 --8.7000e+02 1.2000e+03 2.0046e+00 --8.7000e+02 1.2286e+03 2.2055e+00 --8.7000e+02 1.2571e+03 2.3981e+00 --8.7000e+02 1.2857e+03 2.5828e+00 --8.7000e+02 1.3143e+03 2.7600e+00 --8.7000e+02 1.3429e+03 2.9301e+00 --8.7000e+02 1.3714e+03 3.0935e+00 --8.7000e+02 1.4000e+03 3.2505e+00 --8.7000e+02 1.4286e+03 3.4014e+00 --8.7000e+02 1.4571e+03 3.5464e+00 --8.7000e+02 1.4857e+03 3.6860e+00 --8.7000e+02 1.5143e+03 3.8204e+00 --8.7000e+02 1.5429e+03 3.9498e+00 --8.7000e+02 1.5714e+03 4.0744e+00 --8.7000e+02 1.6000e+03 4.1946e+00 --8.7000e+02 1.6286e+03 4.3105e+00 --8.7000e+02 1.6571e+03 4.4223e+00 --8.7000e+02 1.6857e+03 4.5301e+00 --8.7000e+02 1.7143e+03 4.6343e+00 --8.7000e+02 1.7429e+03 4.7350e+00 --8.7000e+02 1.7714e+03 4.8322e+00 --8.7000e+02 1.8000e+03 4.9263e+00 --8.7000e+02 1.8286e+03 5.0172e+00 --8.7000e+02 1.8571e+03 5.1052e+00 --8.7000e+02 1.8857e+03 5.1904e+00 --8.7000e+02 1.9143e+03 5.2730e+00 --8.7000e+02 1.9429e+03 5.3529e+00 --8.7000e+02 1.9714e+03 5.4304e+00 --8.7000e+02 2.0000e+03 5.5055e+00 --8.4000e+02 -2.0000e+03 5.4720e+00 --8.4000e+02 -1.9714e+03 5.3953e+00 --8.4000e+02 -1.9429e+03 5.3161e+00 --8.4000e+02 -1.9143e+03 5.2344e+00 --8.4000e+02 -1.8857e+03 5.1501e+00 --8.4000e+02 -1.8571e+03 5.0629e+00 --8.4000e+02 -1.8286e+03 4.9728e+00 --8.4000e+02 -1.8000e+03 4.8796e+00 --8.4000e+02 -1.7714e+03 4.7831e+00 --8.4000e+02 -1.7429e+03 4.6833e+00 --8.4000e+02 -1.7143e+03 4.5800e+00 --8.4000e+02 -1.6857e+03 4.4729e+00 --8.4000e+02 -1.6571e+03 4.3619e+00 --8.4000e+02 -1.6286e+03 4.2468e+00 --8.4000e+02 -1.6000e+03 4.1274e+00 --8.4000e+02 -1.5714e+03 4.0034e+00 --8.4000e+02 -1.5429e+03 3.8747e+00 --8.4000e+02 -1.5143e+03 3.7409e+00 --8.4000e+02 -1.4857e+03 3.6018e+00 --8.4000e+02 -1.4571e+03 3.4572e+00 --8.4000e+02 -1.4286e+03 3.3066e+00 --8.4000e+02 -1.4000e+03 3.1499e+00 --8.4000e+02 -1.3714e+03 2.9866e+00 --8.4000e+02 -1.3429e+03 2.8164e+00 --8.4000e+02 -1.3143e+03 2.6388e+00 --8.4000e+02 -1.2857e+03 2.4536e+00 --8.4000e+02 -1.2571e+03 2.2603e+00 --8.4000e+02 -1.2286e+03 2.0583e+00 --8.4000e+02 -1.2000e+03 1.8472e+00 --8.4000e+02 -1.1714e+03 1.6266e+00 --8.4000e+02 -1.1429e+03 1.3957e+00 --8.4000e+02 -1.1143e+03 1.1542e+00 --8.4000e+02 -1.0857e+03 9.0131e-01 --8.4000e+02 -1.0571e+03 6.3647e-01 --8.4000e+02 -1.0286e+03 3.5901e-01 --8.4000e+02 -1.0000e+03 6.8225e-02 --8.4000e+02 -9.7143e+02 -2.3659e-01 --8.4000e+02 -9.4286e+02 -5.5617e-01 --8.4000e+02 -9.1429e+02 -8.9127e-01 --8.4000e+02 -8.8571e+02 -1.2426e+00 --8.4000e+02 -8.5714e+02 -1.6111e+00 --8.4000e+02 -8.2857e+02 -1.9973e+00 --8.4000e+02 -8.0000e+02 -2.4020e+00 --8.4000e+02 -7.7143e+02 -2.8259e+00 --8.4000e+02 -7.4286e+02 -3.2696e+00 --8.4000e+02 -7.1429e+02 -3.7336e+00 --8.4000e+02 -6.8571e+02 -4.2183e+00 --8.4000e+02 -6.5714e+02 -4.7239e+00 --8.4000e+02 -6.2857e+02 -5.2504e+00 --8.4000e+02 -6.0000e+02 -5.7975e+00 --8.4000e+02 -5.7143e+02 -6.3647e+00 --8.4000e+02 -5.4286e+02 -6.9510e+00 --8.4000e+02 -5.1429e+02 -7.5549e+00 --8.4000e+02 -4.8571e+02 -8.1745e+00 --8.4000e+02 -4.5714e+02 -8.8072e+00 --8.4000e+02 -4.2857e+02 -9.4498e+00 --8.4000e+02 -4.0000e+02 -1.0098e+01 --8.4000e+02 -3.7143e+02 -1.0748e+01 --8.4000e+02 -3.4286e+02 -1.1394e+01 --8.4000e+02 -3.1429e+02 -1.2029e+01 --8.4000e+02 -2.8571e+02 -1.2647e+01 --8.4000e+02 -2.5714e+02 -1.3240e+01 --8.4000e+02 -2.2857e+02 -1.3800e+01 --8.4000e+02 -2.0000e+02 -1.4320e+01 --8.4000e+02 -1.7143e+02 -1.4791e+01 --8.4000e+02 -1.4286e+02 -1.5205e+01 --8.4000e+02 -1.1429e+02 -1.5555e+01 --8.4000e+02 -8.5714e+01 -1.5834e+01 --8.4000e+02 -5.7143e+01 -1.6038e+01 --8.4000e+02 -2.8571e+01 -1.6162e+01 --8.4000e+02 0.0000e+00 -1.6203e+01 --8.4000e+02 2.8571e+01 -1.6162e+01 --8.4000e+02 5.7143e+01 -1.6038e+01 --8.4000e+02 8.5714e+01 -1.5834e+01 --8.4000e+02 1.1429e+02 -1.5555e+01 --8.4000e+02 1.4286e+02 -1.5205e+01 --8.4000e+02 1.7143e+02 -1.4791e+01 --8.4000e+02 2.0000e+02 -1.4320e+01 --8.4000e+02 2.2857e+02 -1.3800e+01 --8.4000e+02 2.5714e+02 -1.3240e+01 --8.4000e+02 2.8571e+02 -1.2647e+01 --8.4000e+02 3.1429e+02 -1.2029e+01 --8.4000e+02 3.4286e+02 -1.1394e+01 --8.4000e+02 3.7143e+02 -1.0748e+01 --8.4000e+02 4.0000e+02 -1.0098e+01 --8.4000e+02 4.2857e+02 -9.4498e+00 --8.4000e+02 4.5714e+02 -8.8072e+00 --8.4000e+02 4.8571e+02 -8.1745e+00 --8.4000e+02 5.1429e+02 -7.5549e+00 --8.4000e+02 5.4286e+02 -6.9510e+00 --8.4000e+02 5.7143e+02 -6.3647e+00 --8.4000e+02 6.0000e+02 -5.7975e+00 --8.4000e+02 6.2857e+02 -5.2504e+00 --8.4000e+02 6.5714e+02 -4.7239e+00 --8.4000e+02 6.8571e+02 -4.2183e+00 --8.4000e+02 7.1429e+02 -3.7336e+00 --8.4000e+02 7.4286e+02 -3.2696e+00 --8.4000e+02 7.7143e+02 -2.8259e+00 --8.4000e+02 8.0000e+02 -2.4020e+00 --8.4000e+02 8.2857e+02 -1.9973e+00 --8.4000e+02 8.5714e+02 -1.6111e+00 --8.4000e+02 8.8571e+02 -1.2426e+00 --8.4000e+02 9.1429e+02 -8.9127e-01 --8.4000e+02 9.4286e+02 -5.5617e-01 --8.4000e+02 9.7143e+02 -2.3659e-01 --8.4000e+02 1.0000e+03 6.8225e-02 --8.4000e+02 1.0286e+03 3.5901e-01 --8.4000e+02 1.0571e+03 6.3647e-01 --8.4000e+02 1.0857e+03 9.0131e-01 --8.4000e+02 1.1143e+03 1.1542e+00 --8.4000e+02 1.1429e+03 1.3957e+00 --8.4000e+02 1.1714e+03 1.6266e+00 --8.4000e+02 1.2000e+03 1.8472e+00 --8.4000e+02 1.2286e+03 2.0583e+00 --8.4000e+02 1.2571e+03 2.2603e+00 --8.4000e+02 1.2857e+03 2.4536e+00 --8.4000e+02 1.3143e+03 2.6388e+00 --8.4000e+02 1.3429e+03 2.8164e+00 --8.4000e+02 1.3714e+03 2.9866e+00 --8.4000e+02 1.4000e+03 3.1499e+00 --8.4000e+02 1.4286e+03 3.3066e+00 --8.4000e+02 1.4571e+03 3.4572e+00 --8.4000e+02 1.4857e+03 3.6018e+00 --8.4000e+02 1.5143e+03 3.7409e+00 --8.4000e+02 1.5429e+03 3.8747e+00 --8.4000e+02 1.5714e+03 4.0034e+00 --8.4000e+02 1.6000e+03 4.1274e+00 --8.4000e+02 1.6286e+03 4.2468e+00 --8.4000e+02 1.6571e+03 4.3619e+00 --8.4000e+02 1.6857e+03 4.4729e+00 --8.4000e+02 1.7143e+03 4.5800e+00 --8.4000e+02 1.7429e+03 4.6833e+00 --8.4000e+02 1.7714e+03 4.7831e+00 --8.4000e+02 1.8000e+03 4.8796e+00 --8.4000e+02 1.8286e+03 4.9728e+00 --8.4000e+02 1.8571e+03 5.0629e+00 --8.4000e+02 1.8857e+03 5.1501e+00 --8.4000e+02 1.9143e+03 5.2344e+00 --8.4000e+02 1.9429e+03 5.3161e+00 --8.4000e+02 1.9714e+03 5.3953e+00 --8.4000e+02 2.0000e+03 5.4720e+00 --8.1000e+02 -2.0000e+03 5.4389e+00 --8.1000e+02 -1.9714e+03 5.3607e+00 --8.1000e+02 -1.9429e+03 5.2799e+00 --8.1000e+02 -1.9143e+03 5.1965e+00 --8.1000e+02 -1.8857e+03 5.1102e+00 --8.1000e+02 -1.8571e+03 5.0211e+00 --8.1000e+02 -1.8286e+03 4.9288e+00 --8.1000e+02 -1.8000e+03 4.8334e+00 --8.1000e+02 -1.7714e+03 4.7346e+00 --8.1000e+02 -1.7429e+03 4.6322e+00 --8.1000e+02 -1.7143e+03 4.5261e+00 --8.1000e+02 -1.6857e+03 4.4161e+00 --8.1000e+02 -1.6571e+03 4.3020e+00 --8.1000e+02 -1.6286e+03 4.1835e+00 --8.1000e+02 -1.6000e+03 4.0605e+00 --8.1000e+02 -1.5714e+03 3.9327e+00 --8.1000e+02 -1.5429e+03 3.7998e+00 --8.1000e+02 -1.5143e+03 3.6616e+00 --8.1000e+02 -1.4857e+03 3.5177e+00 --8.1000e+02 -1.4571e+03 3.3679e+00 --8.1000e+02 -1.4286e+03 3.2118e+00 --8.1000e+02 -1.4000e+03 3.0490e+00 --8.1000e+02 -1.3714e+03 2.8792e+00 --8.1000e+02 -1.3429e+03 2.7019e+00 --8.1000e+02 -1.3143e+03 2.5168e+00 --8.1000e+02 -1.2857e+03 2.3233e+00 --8.1000e+02 -1.2571e+03 2.1209e+00 --8.1000e+02 -1.2286e+03 1.9092e+00 --8.1000e+02 -1.2000e+03 1.6875e+00 --8.1000e+02 -1.1714e+03 1.4553e+00 --8.1000e+02 -1.1429e+03 1.2119e+00 --8.1000e+02 -1.1143e+03 9.5659e-01 --8.1000e+02 -1.0857e+03 6.8871e-01 --8.1000e+02 -1.0571e+03 4.0746e-01 --8.1000e+02 -1.0286e+03 1.1205e-01 --8.1000e+02 -1.0000e+03 -1.9839e-01 --8.1000e+02 -9.7143e+02 -5.2474e-01 --8.1000e+02 -9.4286e+02 -8.6794e-01 --8.1000e+02 -9.1429e+02 -1.2290e+00 --8.1000e+02 -8.8571e+02 -1.6088e+00 --8.1000e+02 -8.5714e+02 -2.0084e+00 --8.1000e+02 -8.2857e+02 -2.4290e+00 --8.1000e+02 -8.0000e+02 -2.8713e+00 --8.1000e+02 -7.7143e+02 -3.3366e+00 --8.1000e+02 -7.4286e+02 -3.8257e+00 --8.1000e+02 -7.1429e+02 -4.3395e+00 --8.1000e+02 -6.8571e+02 -4.8788e+00 --8.1000e+02 -6.5714e+02 -5.4441e+00 --8.1000e+02 -6.2857e+02 -6.0357e+00 --8.1000e+02 -6.0000e+02 -6.6539e+00 --8.1000e+02 -5.7143e+02 -7.2982e+00 --8.1000e+02 -5.4286e+02 -7.9679e+00 --8.1000e+02 -5.1429e+02 -8.6617e+00 --8.1000e+02 -4.8571e+02 -9.3777e+00 --8.1000e+02 -4.5714e+02 -1.0113e+01 --8.1000e+02 -4.2857e+02 -1.0865e+01 --8.1000e+02 -4.0000e+02 -1.1627e+01 --8.1000e+02 -3.7143e+02 -1.2396e+01 --8.1000e+02 -3.4286e+02 -1.3164e+01 --8.1000e+02 -3.1429e+02 -1.3924e+01 --8.1000e+02 -2.8571e+02 -1.4667e+01 --8.1000e+02 -2.5714e+02 -1.5384e+01 --8.1000e+02 -2.2857e+02 -1.6064e+01 --8.1000e+02 -2.0000e+02 -1.6698e+01 --8.1000e+02 -1.7143e+02 -1.7274e+01 --8.1000e+02 -1.4286e+02 -1.7782e+01 --8.1000e+02 -1.1429e+02 -1.8213e+01 --8.1000e+02 -8.5714e+01 -1.8558e+01 --8.1000e+02 -5.7143e+01 -1.8809e+01 --8.1000e+02 -2.8571e+01 -1.8963e+01 --8.1000e+02 0.0000e+00 -1.9014e+01 --8.1000e+02 2.8571e+01 -1.8963e+01 --8.1000e+02 5.7143e+01 -1.8809e+01 --8.1000e+02 8.5714e+01 -1.8558e+01 --8.1000e+02 1.1429e+02 -1.8213e+01 --8.1000e+02 1.4286e+02 -1.7782e+01 --8.1000e+02 1.7143e+02 -1.7274e+01 --8.1000e+02 2.0000e+02 -1.6698e+01 --8.1000e+02 2.2857e+02 -1.6064e+01 --8.1000e+02 2.5714e+02 -1.5384e+01 --8.1000e+02 2.8571e+02 -1.4667e+01 --8.1000e+02 3.1429e+02 -1.3924e+01 --8.1000e+02 3.4286e+02 -1.3164e+01 --8.1000e+02 3.7143e+02 -1.2396e+01 --8.1000e+02 4.0000e+02 -1.1627e+01 --8.1000e+02 4.2857e+02 -1.0865e+01 --8.1000e+02 4.5714e+02 -1.0113e+01 --8.1000e+02 4.8571e+02 -9.3777e+00 --8.1000e+02 5.1429e+02 -8.6617e+00 --8.1000e+02 5.4286e+02 -7.9679e+00 --8.1000e+02 5.7143e+02 -7.2982e+00 --8.1000e+02 6.0000e+02 -6.6539e+00 --8.1000e+02 6.2857e+02 -6.0357e+00 --8.1000e+02 6.5714e+02 -5.4441e+00 --8.1000e+02 6.8571e+02 -4.8788e+00 --8.1000e+02 7.1429e+02 -4.3395e+00 --8.1000e+02 7.4286e+02 -3.8257e+00 --8.1000e+02 7.7143e+02 -3.3366e+00 --8.1000e+02 8.0000e+02 -2.8713e+00 --8.1000e+02 8.2857e+02 -2.4290e+00 --8.1000e+02 8.5714e+02 -2.0084e+00 --8.1000e+02 8.8571e+02 -1.6088e+00 --8.1000e+02 9.1429e+02 -1.2290e+00 --8.1000e+02 9.4286e+02 -8.6794e-01 --8.1000e+02 9.7143e+02 -5.2474e-01 --8.1000e+02 1.0000e+03 -1.9839e-01 --8.1000e+02 1.0286e+03 1.1205e-01 --8.1000e+02 1.0571e+03 4.0746e-01 --8.1000e+02 1.0857e+03 6.8871e-01 --8.1000e+02 1.1143e+03 9.5659e-01 --8.1000e+02 1.1429e+03 1.2119e+00 --8.1000e+02 1.1714e+03 1.4553e+00 --8.1000e+02 1.2000e+03 1.6875e+00 --8.1000e+02 1.2286e+03 1.9092e+00 --8.1000e+02 1.2571e+03 2.1209e+00 --8.1000e+02 1.2857e+03 2.3233e+00 --8.1000e+02 1.3143e+03 2.5168e+00 --8.1000e+02 1.3429e+03 2.7019e+00 --8.1000e+02 1.3714e+03 2.8792e+00 --8.1000e+02 1.4000e+03 3.0490e+00 --8.1000e+02 1.4286e+03 3.2118e+00 --8.1000e+02 1.4571e+03 3.3679e+00 --8.1000e+02 1.4857e+03 3.5177e+00 --8.1000e+02 1.5143e+03 3.6616e+00 --8.1000e+02 1.5429e+03 3.7998e+00 --8.1000e+02 1.5714e+03 3.9327e+00 --8.1000e+02 1.6000e+03 4.0605e+00 --8.1000e+02 1.6286e+03 4.1835e+00 --8.1000e+02 1.6571e+03 4.3020e+00 --8.1000e+02 1.6857e+03 4.4161e+00 --8.1000e+02 1.7143e+03 4.5261e+00 --8.1000e+02 1.7429e+03 4.6322e+00 --8.1000e+02 1.7714e+03 4.7346e+00 --8.1000e+02 1.8000e+03 4.8334e+00 --8.1000e+02 1.8286e+03 4.9288e+00 --8.1000e+02 1.8571e+03 5.0211e+00 --8.1000e+02 1.8857e+03 5.1102e+00 --8.1000e+02 1.9143e+03 5.1965e+00 --8.1000e+02 1.9429e+03 5.2799e+00 --8.1000e+02 1.9714e+03 5.3607e+00 --8.1000e+02 2.0000e+03 5.4389e+00 --7.8000e+02 -2.0000e+03 5.4065e+00 --7.8000e+02 -1.9714e+03 5.3267e+00 --7.8000e+02 -1.9429e+03 5.2443e+00 --7.8000e+02 -1.9143e+03 5.1591e+00 --7.8000e+02 -1.8857e+03 5.0710e+00 --7.8000e+02 -1.8571e+03 4.9799e+00 --7.8000e+02 -1.8286e+03 4.8855e+00 --7.8000e+02 -1.8000e+03 4.7878e+00 --7.8000e+02 -1.7714e+03 4.6866e+00 --7.8000e+02 -1.7429e+03 4.5817e+00 --7.8000e+02 -1.7143e+03 4.4728e+00 --7.8000e+02 -1.6857e+03 4.3599e+00 --7.8000e+02 -1.6571e+03 4.2426e+00 --7.8000e+02 -1.6286e+03 4.1208e+00 --7.8000e+02 -1.6000e+03 3.9941e+00 --7.8000e+02 -1.5714e+03 3.8624e+00 --7.8000e+02 -1.5429e+03 3.7253e+00 --7.8000e+02 -1.5143e+03 3.5826e+00 --7.8000e+02 -1.4857e+03 3.4338e+00 --7.8000e+02 -1.4571e+03 3.2787e+00 --7.8000e+02 -1.4286e+03 3.1169e+00 --7.8000e+02 -1.4000e+03 2.9480e+00 --7.8000e+02 -1.3714e+03 2.7715e+00 --7.8000e+02 -1.3429e+03 2.5870e+00 --7.8000e+02 -1.3143e+03 2.3940e+00 --7.8000e+02 -1.2857e+03 2.1920e+00 --7.8000e+02 -1.2571e+03 1.9803e+00 --7.8000e+02 -1.2286e+03 1.7585e+00 --7.8000e+02 -1.2000e+03 1.5258e+00 --7.8000e+02 -1.1714e+03 1.2815e+00 --7.8000e+02 -1.1429e+03 1.0249e+00 --7.8000e+02 -1.1143e+03 7.5515e-01 --7.8000e+02 -1.0857e+03 4.7144e-01 --7.8000e+02 -1.0571e+03 1.7283e-01 --7.8000e+02 -1.0286e+03 -1.4166e-01 --7.8000e+02 -1.0000e+03 -4.7306e-01 --7.8000e+02 -9.7143e+02 -8.2250e-01 --7.8000e+02 -9.4286e+02 -1.1911e+00 --7.8000e+02 -9.1429e+02 -1.5802e+00 --7.8000e+02 -8.8571e+02 -1.9909e+00 --7.8000e+02 -8.5714e+02 -2.4247e+00 --7.8000e+02 -8.2857e+02 -2.8830e+00 --7.8000e+02 -8.0000e+02 -3.3670e+00 --7.8000e+02 -7.7143e+02 -3.8783e+00 --7.8000e+02 -7.4286e+02 -4.4183e+00 --7.8000e+02 -7.1429e+02 -4.9882e+00 --7.8000e+02 -6.8571e+02 -5.5894e+00 --7.8000e+02 -6.5714e+02 -6.2230e+00 --7.8000e+02 -6.2857e+02 -6.8897e+00 --7.8000e+02 -6.0000e+02 -7.5902e+00 --7.8000e+02 -5.7143e+02 -8.3247e+00 --7.8000e+02 -5.4286e+02 -9.0928e+00 --7.8000e+02 -5.1429e+02 -9.8934e+00 --7.8000e+02 -4.8571e+02 -1.0725e+01 --7.8000e+02 -4.5714e+02 -1.1585e+01 --7.8000e+02 -4.2857e+02 -1.2468e+01 --7.8000e+02 -4.0000e+02 -1.3371e+01 --7.8000e+02 -3.7143e+02 -1.4287e+01 --7.8000e+02 -3.4286e+02 -1.5207e+01 --7.8000e+02 -3.1429e+02 -1.6123e+01 --7.8000e+02 -2.8571e+02 -1.7024e+01 --7.8000e+02 -2.5714e+02 -1.7897e+01 --7.8000e+02 -2.2857e+02 -1.8730e+01 --7.8000e+02 -2.0000e+02 -1.9509e+01 --7.8000e+02 -1.7143e+02 -2.0220e+01 --7.8000e+02 -1.4286e+02 -2.0849e+01 --7.8000e+02 -1.1429e+02 -2.1384e+01 --7.8000e+02 -8.5714e+01 -2.1813e+01 --7.8000e+02 -5.7143e+01 -2.2126e+01 --7.8000e+02 -2.8571e+01 -2.2317e+01 --7.8000e+02 0.0000e+00 -2.2382e+01 --7.8000e+02 2.8571e+01 -2.2317e+01 --7.8000e+02 5.7143e+01 -2.2126e+01 --7.8000e+02 8.5714e+01 -2.1813e+01 --7.8000e+02 1.1429e+02 -2.1384e+01 --7.8000e+02 1.4286e+02 -2.0849e+01 --7.8000e+02 1.7143e+02 -2.0220e+01 --7.8000e+02 2.0000e+02 -1.9509e+01 --7.8000e+02 2.2857e+02 -1.8730e+01 --7.8000e+02 2.5714e+02 -1.7897e+01 --7.8000e+02 2.8571e+02 -1.7024e+01 --7.8000e+02 3.1429e+02 -1.6123e+01 --7.8000e+02 3.4286e+02 -1.5207e+01 --7.8000e+02 3.7143e+02 -1.4287e+01 --7.8000e+02 4.0000e+02 -1.3371e+01 --7.8000e+02 4.2857e+02 -1.2468e+01 --7.8000e+02 4.5714e+02 -1.1585e+01 --7.8000e+02 4.8571e+02 -1.0725e+01 --7.8000e+02 5.1429e+02 -9.8934e+00 --7.8000e+02 5.4286e+02 -9.0928e+00 --7.8000e+02 5.7143e+02 -8.3247e+00 --7.8000e+02 6.0000e+02 -7.5902e+00 --7.8000e+02 6.2857e+02 -6.8897e+00 --7.8000e+02 6.5714e+02 -6.2230e+00 --7.8000e+02 6.8571e+02 -5.5894e+00 --7.8000e+02 7.1429e+02 -4.9882e+00 --7.8000e+02 7.4286e+02 -4.4183e+00 --7.8000e+02 7.7143e+02 -3.8783e+00 --7.8000e+02 8.0000e+02 -3.3670e+00 --7.8000e+02 8.2857e+02 -2.8830e+00 --7.8000e+02 8.5714e+02 -2.4247e+00 --7.8000e+02 8.8571e+02 -1.9909e+00 --7.8000e+02 9.1429e+02 -1.5802e+00 --7.8000e+02 9.4286e+02 -1.1911e+00 --7.8000e+02 9.7143e+02 -8.2250e-01 --7.8000e+02 1.0000e+03 -4.7306e-01 --7.8000e+02 1.0286e+03 -1.4166e-01 --7.8000e+02 1.0571e+03 1.7283e-01 --7.8000e+02 1.0857e+03 4.7144e-01 --7.8000e+02 1.1143e+03 7.5515e-01 --7.8000e+02 1.1429e+03 1.0249e+00 --7.8000e+02 1.1714e+03 1.2815e+00 --7.8000e+02 1.2000e+03 1.5258e+00 --7.8000e+02 1.2286e+03 1.7585e+00 --7.8000e+02 1.2571e+03 1.9803e+00 --7.8000e+02 1.2857e+03 2.1920e+00 --7.8000e+02 1.3143e+03 2.3940e+00 --7.8000e+02 1.3429e+03 2.5870e+00 --7.8000e+02 1.3714e+03 2.7715e+00 --7.8000e+02 1.4000e+03 2.9480e+00 --7.8000e+02 1.4286e+03 3.1169e+00 --7.8000e+02 1.4571e+03 3.2787e+00 --7.8000e+02 1.4857e+03 3.4338e+00 --7.8000e+02 1.5143e+03 3.5826e+00 --7.8000e+02 1.5429e+03 3.7253e+00 --7.8000e+02 1.5714e+03 3.8624e+00 --7.8000e+02 1.6000e+03 3.9941e+00 --7.8000e+02 1.6286e+03 4.1208e+00 --7.8000e+02 1.6571e+03 4.2426e+00 --7.8000e+02 1.6857e+03 4.3599e+00 --7.8000e+02 1.7143e+03 4.4728e+00 --7.8000e+02 1.7429e+03 4.5817e+00 --7.8000e+02 1.7714e+03 4.6866e+00 --7.8000e+02 1.8000e+03 4.7878e+00 --7.8000e+02 1.8286e+03 4.8855e+00 --7.8000e+02 1.8571e+03 4.9799e+00 --7.8000e+02 1.8857e+03 5.0710e+00 --7.8000e+02 1.9143e+03 5.1591e+00 --7.8000e+02 1.9429e+03 5.2443e+00 --7.8000e+02 1.9714e+03 5.3267e+00 --7.8000e+02 2.0000e+03 5.4065e+00 --7.5000e+02 -2.0000e+03 5.3747e+00 --7.5000e+02 -1.9714e+03 5.2934e+00 --7.5000e+02 -1.9429e+03 5.2093e+00 --7.5000e+02 -1.9143e+03 5.1224e+00 --7.5000e+02 -1.8857e+03 5.0324e+00 --7.5000e+02 -1.8571e+03 4.9393e+00 --7.5000e+02 -1.8286e+03 4.8429e+00 --7.5000e+02 -1.8000e+03 4.7429e+00 --7.5000e+02 -1.7714e+03 4.6393e+00 --7.5000e+02 -1.7429e+03 4.5318e+00 --7.5000e+02 -1.7143e+03 4.4202e+00 --7.5000e+02 -1.6857e+03 4.3043e+00 --7.5000e+02 -1.6571e+03 4.1839e+00 --7.5000e+02 -1.6286e+03 4.0587e+00 --7.5000e+02 -1.6000e+03 3.9284e+00 --7.5000e+02 -1.5714e+03 3.7927e+00 --7.5000e+02 -1.5429e+03 3.6514e+00 --7.5000e+02 -1.5143e+03 3.5040e+00 --7.5000e+02 -1.4857e+03 3.3503e+00 --7.5000e+02 -1.4571e+03 3.1899e+00 --7.5000e+02 -1.4286e+03 3.0223e+00 --7.5000e+02 -1.4000e+03 2.8471e+00 --7.5000e+02 -1.3714e+03 2.6638e+00 --7.5000e+02 -1.3429e+03 2.4719e+00 --7.5000e+02 -1.3143e+03 2.2708e+00 --7.5000e+02 -1.2857e+03 2.0600e+00 --7.5000e+02 -1.2571e+03 1.8387e+00 --7.5000e+02 -1.2286e+03 1.6064e+00 --7.5000e+02 -1.2000e+03 1.3622e+00 --7.5000e+02 -1.1714e+03 1.1054e+00 --7.5000e+02 -1.1429e+03 8.3500e-01 --7.5000e+02 -1.1143e+03 5.5014e-01 --7.5000e+02 -1.0857e+03 2.4978e-01 --7.5000e+02 -1.0571e+03 -6.7162e-02 --7.5000e+02 -1.0286e+03 -4.0187e-01 --7.5000e+02 -1.0000e+03 -7.5562e-01 --7.5000e+02 -9.7143e+02 -1.1297e+00 --7.5000e+02 -9.4286e+02 -1.5257e+00 --7.5000e+02 -9.1429e+02 -1.9450e+00 --7.5000e+02 -8.8571e+02 -2.3894e+00 --7.5000e+02 -8.5714e+02 -2.8605e+00 --7.5000e+02 -8.2857e+02 -3.3602e+00 --7.5000e+02 -8.0000e+02 -3.8903e+00 --7.5000e+02 -7.7143e+02 -4.4528e+00 --7.5000e+02 -7.4286e+02 -5.0497e+00 --7.5000e+02 -7.1429e+02 -5.6830e+00 --7.5000e+02 -6.8571e+02 -6.3546e+00 --7.5000e+02 -6.5714e+02 -7.0663e+00 --7.5000e+02 -6.2857e+02 -7.8196e+00 --7.5000e+02 -6.0000e+02 -8.6160e+00 --7.5000e+02 -5.7143e+02 -9.4562e+00 --7.5000e+02 -5.4286e+02 -1.0341e+01 --7.5000e+02 -5.1429e+02 -1.1269e+01 --7.5000e+02 -4.8571e+02 -1.2239e+01 --7.5000e+02 -4.5714e+02 -1.3249e+01 --7.5000e+02 -4.2857e+02 -1.4295e+01 --7.5000e+02 -4.0000e+02 -1.5371e+01 --7.5000e+02 -3.7143e+02 -1.6468e+01 --7.5000e+02 -3.4286e+02 -1.7579e+01 --7.5000e+02 -3.1429e+02 -1.8691e+01 --7.5000e+02 -2.8571e+02 -1.9791e+01 --7.5000e+02 -2.5714e+02 -2.0862e+01 --7.5000e+02 -2.2857e+02 -2.1889e+01 --7.5000e+02 -2.0000e+02 -2.2853e+01 --7.5000e+02 -1.7143e+02 -2.3736e+01 --7.5000e+02 -1.4286e+02 -2.4520e+01 --7.5000e+02 -1.1429e+02 -2.5188e+01 --7.5000e+02 -8.5714e+01 -2.5724e+01 --7.5000e+02 -5.7143e+01 -2.6117e+01 --7.5000e+02 -2.8571e+01 -2.6357e+01 --7.5000e+02 0.0000e+00 -2.6437e+01 --7.5000e+02 2.8571e+01 -2.6357e+01 --7.5000e+02 5.7143e+01 -2.6117e+01 --7.5000e+02 8.5714e+01 -2.5724e+01 --7.5000e+02 1.1429e+02 -2.5188e+01 --7.5000e+02 1.4286e+02 -2.4520e+01 --7.5000e+02 1.7143e+02 -2.3736e+01 --7.5000e+02 2.0000e+02 -2.2853e+01 --7.5000e+02 2.2857e+02 -2.1889e+01 --7.5000e+02 2.5714e+02 -2.0862e+01 --7.5000e+02 2.8571e+02 -1.9791e+01 --7.5000e+02 3.1429e+02 -1.8691e+01 --7.5000e+02 3.4286e+02 -1.7579e+01 --7.5000e+02 3.7143e+02 -1.6468e+01 --7.5000e+02 4.0000e+02 -1.5371e+01 --7.5000e+02 4.2857e+02 -1.4295e+01 --7.5000e+02 4.5714e+02 -1.3249e+01 --7.5000e+02 4.8571e+02 -1.2239e+01 --7.5000e+02 5.1429e+02 -1.1269e+01 --7.5000e+02 5.4286e+02 -1.0341e+01 --7.5000e+02 5.7143e+02 -9.4562e+00 --7.5000e+02 6.0000e+02 -8.6160e+00 --7.5000e+02 6.2857e+02 -7.8196e+00 --7.5000e+02 6.5714e+02 -7.0663e+00 --7.5000e+02 6.8571e+02 -6.3546e+00 --7.5000e+02 7.1429e+02 -5.6830e+00 --7.5000e+02 7.4286e+02 -5.0497e+00 --7.5000e+02 7.7143e+02 -4.4528e+00 --7.5000e+02 8.0000e+02 -3.8903e+00 --7.5000e+02 8.2857e+02 -3.3602e+00 --7.5000e+02 8.5714e+02 -2.8605e+00 --7.5000e+02 8.8571e+02 -2.3894e+00 --7.5000e+02 9.1429e+02 -1.9450e+00 --7.5000e+02 9.4286e+02 -1.5257e+00 --7.5000e+02 9.7143e+02 -1.1297e+00 --7.5000e+02 1.0000e+03 -7.5562e-01 --7.5000e+02 1.0286e+03 -4.0187e-01 --7.5000e+02 1.0571e+03 -6.7162e-02 --7.5000e+02 1.0857e+03 2.4978e-01 --7.5000e+02 1.1143e+03 5.5014e-01 --7.5000e+02 1.1429e+03 8.3500e-01 --7.5000e+02 1.1714e+03 1.1054e+00 --7.5000e+02 1.2000e+03 1.3622e+00 --7.5000e+02 1.2286e+03 1.6064e+00 --7.5000e+02 1.2571e+03 1.8387e+00 --7.5000e+02 1.2857e+03 2.0600e+00 --7.5000e+02 1.3143e+03 2.2708e+00 --7.5000e+02 1.3429e+03 2.4719e+00 --7.5000e+02 1.3714e+03 2.6638e+00 --7.5000e+02 1.4000e+03 2.8471e+00 --7.5000e+02 1.4286e+03 3.0223e+00 --7.5000e+02 1.4571e+03 3.1899e+00 --7.5000e+02 1.4857e+03 3.3503e+00 --7.5000e+02 1.5143e+03 3.5040e+00 --7.5000e+02 1.5429e+03 3.6514e+00 --7.5000e+02 1.5714e+03 3.7927e+00 --7.5000e+02 1.6000e+03 3.9284e+00 --7.5000e+02 1.6286e+03 4.0587e+00 --7.5000e+02 1.6571e+03 4.1839e+00 --7.5000e+02 1.6857e+03 4.3043e+00 --7.5000e+02 1.7143e+03 4.4202e+00 --7.5000e+02 1.7429e+03 4.5318e+00 --7.5000e+02 1.7714e+03 4.6393e+00 --7.5000e+02 1.8000e+03 4.7429e+00 --7.5000e+02 1.8286e+03 4.8429e+00 --7.5000e+02 1.8571e+03 4.9393e+00 --7.5000e+02 1.8857e+03 5.0324e+00 --7.5000e+02 1.9143e+03 5.1224e+00 --7.5000e+02 1.9429e+03 5.2093e+00 --7.5000e+02 1.9714e+03 5.2934e+00 --7.5000e+02 2.0000e+03 5.3747e+00 --7.2000e+02 -2.0000e+03 5.3435e+00 --7.2000e+02 -1.9714e+03 5.2607e+00 --7.2000e+02 -1.9429e+03 5.1750e+00 --7.2000e+02 -1.9143e+03 5.0864e+00 --7.2000e+02 -1.8857e+03 4.9946e+00 --7.2000e+02 -1.8571e+03 4.8995e+00 --7.2000e+02 -1.8286e+03 4.8010e+00 --7.2000e+02 -1.8000e+03 4.6988e+00 --7.2000e+02 -1.7714e+03 4.5928e+00 --7.2000e+02 -1.7429e+03 4.4827e+00 --7.2000e+02 -1.7143e+03 4.3684e+00 --7.2000e+02 -1.6857e+03 4.2496e+00 --7.2000e+02 -1.6571e+03 4.1259e+00 --7.2000e+02 -1.6286e+03 3.9973e+00 --7.2000e+02 -1.6000e+03 3.8633e+00 --7.2000e+02 -1.5714e+03 3.7237e+00 --7.2000e+02 -1.5429e+03 3.5781e+00 --7.2000e+02 -1.5143e+03 3.4261e+00 --7.2000e+02 -1.4857e+03 3.2674e+00 --7.2000e+02 -1.4571e+03 3.1016e+00 --7.2000e+02 -1.4286e+03 2.9281e+00 --7.2000e+02 -1.4000e+03 2.7465e+00 --7.2000e+02 -1.3714e+03 2.5562e+00 --7.2000e+02 -1.3429e+03 2.3567e+00 --7.2000e+02 -1.3143e+03 2.1474e+00 --7.2000e+02 -1.2857e+03 1.9276e+00 --7.2000e+02 -1.2571e+03 1.6965e+00 --7.2000e+02 -1.2286e+03 1.4533e+00 --7.2000e+02 -1.2000e+03 1.1973e+00 --7.2000e+02 -1.1714e+03 9.2737e-01 --7.2000e+02 -1.1429e+03 6.4262e-01 --7.2000e+02 -1.1143e+03 3.4193e-01 --7.2000e+02 -1.0857e+03 2.4092e-02 --7.2000e+02 -1.0571e+03 -3.1218e-01 --7.2000e+02 -1.0286e+03 -6.6830e-01 --7.2000e+02 -1.0000e+03 -1.0458e+00 --7.2000e+02 -9.7143e+02 -1.4463e+00 --7.2000e+02 -9.4286e+02 -1.8716e+00 --7.2000e+02 -9.1429e+02 -2.3236e+00 --7.2000e+02 -8.8571e+02 -2.8044e+00 --7.2000e+02 -8.5714e+02 -3.3162e+00 --7.2000e+02 -8.2857e+02 -3.8613e+00 --7.2000e+02 -8.0000e+02 -4.4424e+00 --7.2000e+02 -7.7143e+02 -5.0618e+00 --7.2000e+02 -7.4286e+02 -5.7226e+00 --7.2000e+02 -7.1429e+02 -6.4273e+00 --7.2000e+02 -6.8571e+02 -7.1789e+00 --7.2000e+02 -6.5714e+02 -7.9801e+00 --7.2000e+02 -6.2857e+02 -8.8336e+00 --7.2000e+02 -6.0000e+02 -9.7417e+00 --7.2000e+02 -5.7143e+02 -1.0706e+01 --7.2000e+02 -5.4286e+02 -1.1729e+01 --7.2000e+02 -5.1429e+02 -1.2809e+01 --7.2000e+02 -4.8571e+02 -1.3947e+01 --7.2000e+02 -4.5714e+02 -1.5140e+01 --7.2000e+02 -4.2857e+02 -1.6385e+01 --7.2000e+02 -4.0000e+02 -1.7673e+01 --7.2000e+02 -3.7143e+02 -1.8998e+01 --7.2000e+02 -3.4286e+02 -2.0346e+01 --7.2000e+02 -3.1429e+02 -2.1704e+01 --7.2000e+02 -2.8571e+02 -2.3054e+01 --7.2000e+02 -2.5714e+02 -2.4377e+01 --7.2000e+02 -2.2857e+02 -2.5648e+01 --7.2000e+02 -2.0000e+02 -2.6846e+01 --7.2000e+02 -1.7143e+02 -2.7947e+01 --7.2000e+02 -1.4286e+02 -2.8925e+01 --7.2000e+02 -1.1429e+02 -2.9760e+01 --7.2000e+02 -8.5714e+01 -3.0431e+01 --7.2000e+02 -5.7143e+01 -3.0923e+01 --7.2000e+02 -2.8571e+01 -3.1223e+01 --7.2000e+02 0.0000e+00 -3.1323e+01 --7.2000e+02 2.8571e+01 -3.1223e+01 --7.2000e+02 5.7143e+01 -3.0923e+01 --7.2000e+02 8.5714e+01 -3.0431e+01 --7.2000e+02 1.1429e+02 -2.9760e+01 --7.2000e+02 1.4286e+02 -2.8925e+01 --7.2000e+02 1.7143e+02 -2.7947e+01 --7.2000e+02 2.0000e+02 -2.6846e+01 --7.2000e+02 2.2857e+02 -2.5648e+01 --7.2000e+02 2.5714e+02 -2.4377e+01 --7.2000e+02 2.8571e+02 -2.3054e+01 --7.2000e+02 3.1429e+02 -2.1704e+01 --7.2000e+02 3.4286e+02 -2.0346e+01 --7.2000e+02 3.7143e+02 -1.8998e+01 --7.2000e+02 4.0000e+02 -1.7673e+01 --7.2000e+02 4.2857e+02 -1.6385e+01 --7.2000e+02 4.5714e+02 -1.5140e+01 --7.2000e+02 4.8571e+02 -1.3947e+01 --7.2000e+02 5.1429e+02 -1.2809e+01 --7.2000e+02 5.4286e+02 -1.1729e+01 --7.2000e+02 5.7143e+02 -1.0706e+01 --7.2000e+02 6.0000e+02 -9.7417e+00 --7.2000e+02 6.2857e+02 -8.8336e+00 --7.2000e+02 6.5714e+02 -7.9801e+00 --7.2000e+02 6.8571e+02 -7.1789e+00 --7.2000e+02 7.1429e+02 -6.4273e+00 --7.2000e+02 7.4286e+02 -5.7226e+00 --7.2000e+02 7.7143e+02 -5.0618e+00 --7.2000e+02 8.0000e+02 -4.4424e+00 --7.2000e+02 8.2857e+02 -3.8613e+00 --7.2000e+02 8.5714e+02 -3.3162e+00 --7.2000e+02 8.8571e+02 -2.8044e+00 --7.2000e+02 9.1429e+02 -2.3236e+00 --7.2000e+02 9.4286e+02 -1.8716e+00 --7.2000e+02 9.7143e+02 -1.4463e+00 --7.2000e+02 1.0000e+03 -1.0458e+00 --7.2000e+02 1.0286e+03 -6.6830e-01 --7.2000e+02 1.0571e+03 -3.1218e-01 --7.2000e+02 1.0857e+03 2.4092e-02 --7.2000e+02 1.1143e+03 3.4193e-01 --7.2000e+02 1.1429e+03 6.4262e-01 --7.2000e+02 1.1714e+03 9.2737e-01 --7.2000e+02 1.2000e+03 1.1973e+00 --7.2000e+02 1.2286e+03 1.4533e+00 --7.2000e+02 1.2571e+03 1.6965e+00 --7.2000e+02 1.2857e+03 1.9276e+00 --7.2000e+02 1.3143e+03 2.1474e+00 --7.2000e+02 1.3429e+03 2.3567e+00 --7.2000e+02 1.3714e+03 2.5562e+00 --7.2000e+02 1.4000e+03 2.7465e+00 --7.2000e+02 1.4286e+03 2.9281e+00 --7.2000e+02 1.4571e+03 3.1016e+00 --7.2000e+02 1.4857e+03 3.2674e+00 --7.2000e+02 1.5143e+03 3.4261e+00 --7.2000e+02 1.5429e+03 3.5781e+00 --7.2000e+02 1.5714e+03 3.7237e+00 --7.2000e+02 1.6000e+03 3.8633e+00 --7.2000e+02 1.6286e+03 3.9973e+00 --7.2000e+02 1.6571e+03 4.1259e+00 --7.2000e+02 1.6857e+03 4.2496e+00 --7.2000e+02 1.7143e+03 4.3684e+00 --7.2000e+02 1.7429e+03 4.4827e+00 --7.2000e+02 1.7714e+03 4.5928e+00 --7.2000e+02 1.8000e+03 4.6988e+00 --7.2000e+02 1.8286e+03 4.8010e+00 --7.2000e+02 1.8571e+03 4.8995e+00 --7.2000e+02 1.8857e+03 4.9946e+00 --7.2000e+02 1.9143e+03 5.0864e+00 --7.2000e+02 1.9429e+03 5.1750e+00 --7.2000e+02 1.9714e+03 5.2607e+00 --7.2000e+02 2.0000e+03 5.3435e+00 --6.9000e+02 -2.0000e+03 5.3131e+00 --6.9000e+02 -1.9714e+03 5.2288e+00 --6.9000e+02 -1.9429e+03 5.1415e+00 --6.9000e+02 -1.9143e+03 5.0512e+00 --6.9000e+02 -1.8857e+03 4.9576e+00 --6.9000e+02 -1.8571e+03 4.8606e+00 --6.9000e+02 -1.8286e+03 4.7600e+00 --6.9000e+02 -1.8000e+03 4.6556e+00 --6.9000e+02 -1.7714e+03 4.5472e+00 --6.9000e+02 -1.7429e+03 4.4345e+00 --6.9000e+02 -1.7143e+03 4.3175e+00 --6.9000e+02 -1.6857e+03 4.1957e+00 --6.9000e+02 -1.6571e+03 4.0689e+00 --6.9000e+02 -1.6286e+03 3.9368e+00 --6.9000e+02 -1.6000e+03 3.7992e+00 --6.9000e+02 -1.5714e+03 3.6556e+00 --6.9000e+02 -1.5429e+03 3.5057e+00 --6.9000e+02 -1.5143e+03 3.3491e+00 --6.9000e+02 -1.4857e+03 3.1853e+00 --6.9000e+02 -1.4571e+03 3.0140e+00 --6.9000e+02 -1.4286e+03 2.8345e+00 --6.9000e+02 -1.4000e+03 2.6464e+00 --6.9000e+02 -1.3714e+03 2.4491e+00 --6.9000e+02 -1.3429e+03 2.2419e+00 --6.9000e+02 -1.3143e+03 2.0242e+00 --6.9000e+02 -1.2857e+03 1.7951e+00 --6.9000e+02 -1.2571e+03 1.5538e+00 --6.9000e+02 -1.2286e+03 1.2995e+00 --6.9000e+02 -1.2000e+03 1.0312e+00 --6.9000e+02 -1.1714e+03 7.4781e-01 --6.9000e+02 -1.1429e+03 4.4813e-01 --6.9000e+02 -1.1143e+03 1.3092e-01 --6.9000e+02 -1.0857e+03 -2.0522e-01 --6.9000e+02 -1.0571e+03 -5.6181e-01 --6.9000e+02 -1.0286e+03 -9.4053e-01 --6.9000e+02 -1.0000e+03 -1.3432e+00 --6.9000e+02 -9.7143e+02 -1.7718e+00 --6.9000e+02 -9.4286e+02 -2.2285e+00 --6.9000e+02 -9.1429e+02 -2.7157e+00 --6.9000e+02 -8.8571e+02 -3.2360e+00 --6.9000e+02 -8.5714e+02 -3.7921e+00 --6.9000e+02 -8.2857e+02 -4.3871e+00 --6.9000e+02 -8.0000e+02 -5.0243e+00 --6.9000e+02 -7.7143e+02 -5.7071e+00 --6.9000e+02 -7.4286e+02 -6.4392e+00 --6.9000e+02 -7.1429e+02 -7.2246e+00 --6.9000e+02 -6.8571e+02 -8.0672e+00 --6.9000e+02 -6.5714e+02 -8.9712e+00 --6.9000e+02 -6.2857e+02 -9.9405e+00 --6.9000e+02 -6.0000e+02 -1.0979e+01 --6.9000e+02 -5.7143e+02 -1.2090e+01 --6.9000e+02 -5.4286e+02 -1.3276e+01 --6.9000e+02 -5.1429e+02 -1.4540e+01 --6.9000e+02 -4.8571e+02 -1.5880e+01 --6.9000e+02 -4.5714e+02 -1.7296e+01 --6.9000e+02 -4.2857e+02 -1.8784e+01 --6.9000e+02 -4.0000e+02 -2.0336e+01 --6.9000e+02 -3.7143e+02 -2.1942e+01 --6.9000e+02 -3.4286e+02 -2.3586e+01 --6.9000e+02 -3.1429e+02 -2.5251e+01 --6.9000e+02 -2.8571e+02 -2.6914e+01 --6.9000e+02 -2.5714e+02 -2.8548e+01 --6.9000e+02 -2.2857e+02 -3.0123e+01 --6.9000e+02 -2.0000e+02 -3.1610e+01 --6.9000e+02 -1.7143e+02 -3.2976e+01 --6.9000e+02 -1.4286e+02 -3.4191e+01 --6.9000e+02 -1.1429e+02 -3.5227e+01 --6.9000e+02 -8.5714e+01 -3.6060e+01 --6.9000e+02 -5.7143e+01 -3.6670e+01 --6.9000e+02 -2.8571e+01 -3.7041e+01 --6.9000e+02 0.0000e+00 -3.7166e+01 --6.9000e+02 2.8571e+01 -3.7041e+01 --6.9000e+02 5.7143e+01 -3.6670e+01 --6.9000e+02 8.5714e+01 -3.6060e+01 --6.9000e+02 1.1429e+02 -3.5227e+01 --6.9000e+02 1.4286e+02 -3.4191e+01 --6.9000e+02 1.7143e+02 -3.2976e+01 --6.9000e+02 2.0000e+02 -3.1610e+01 --6.9000e+02 2.2857e+02 -3.0123e+01 --6.9000e+02 2.5714e+02 -2.8548e+01 --6.9000e+02 2.8571e+02 -2.6914e+01 --6.9000e+02 3.1429e+02 -2.5251e+01 --6.9000e+02 3.4286e+02 -2.3586e+01 --6.9000e+02 3.7143e+02 -2.1942e+01 --6.9000e+02 4.0000e+02 -2.0336e+01 --6.9000e+02 4.2857e+02 -1.8784e+01 --6.9000e+02 4.5714e+02 -1.7296e+01 --6.9000e+02 4.8571e+02 -1.5880e+01 --6.9000e+02 5.1429e+02 -1.4540e+01 --6.9000e+02 5.4286e+02 -1.3276e+01 --6.9000e+02 5.7143e+02 -1.2090e+01 --6.9000e+02 6.0000e+02 -1.0979e+01 --6.9000e+02 6.2857e+02 -9.9405e+00 --6.9000e+02 6.5714e+02 -8.9712e+00 --6.9000e+02 6.8571e+02 -8.0672e+00 --6.9000e+02 7.1429e+02 -7.2246e+00 --6.9000e+02 7.4286e+02 -6.4392e+00 --6.9000e+02 7.7143e+02 -5.7071e+00 --6.9000e+02 8.0000e+02 -5.0243e+00 --6.9000e+02 8.2857e+02 -4.3871e+00 --6.9000e+02 8.5714e+02 -3.7921e+00 --6.9000e+02 8.8571e+02 -3.2360e+00 --6.9000e+02 9.1429e+02 -2.7157e+00 --6.9000e+02 9.4286e+02 -2.2285e+00 --6.9000e+02 9.7143e+02 -1.7718e+00 --6.9000e+02 1.0000e+03 -1.3432e+00 --6.9000e+02 1.0286e+03 -9.4053e-01 --6.9000e+02 1.0571e+03 -5.6181e-01 --6.9000e+02 1.0857e+03 -2.0522e-01 --6.9000e+02 1.1143e+03 1.3092e-01 --6.9000e+02 1.1429e+03 4.4813e-01 --6.9000e+02 1.1714e+03 7.4781e-01 --6.9000e+02 1.2000e+03 1.0312e+00 --6.9000e+02 1.2286e+03 1.2995e+00 --6.9000e+02 1.2571e+03 1.5538e+00 --6.9000e+02 1.2857e+03 1.7951e+00 --6.9000e+02 1.3143e+03 2.0242e+00 --6.9000e+02 1.3429e+03 2.2419e+00 --6.9000e+02 1.3714e+03 2.4491e+00 --6.9000e+02 1.4000e+03 2.6464e+00 --6.9000e+02 1.4286e+03 2.8345e+00 --6.9000e+02 1.4571e+03 3.0140e+00 --6.9000e+02 1.4857e+03 3.1853e+00 --6.9000e+02 1.5143e+03 3.3491e+00 --6.9000e+02 1.5429e+03 3.5057e+00 --6.9000e+02 1.5714e+03 3.6556e+00 --6.9000e+02 1.6000e+03 3.7992e+00 --6.9000e+02 1.6286e+03 3.9368e+00 --6.9000e+02 1.6571e+03 4.0689e+00 --6.9000e+02 1.6857e+03 4.1957e+00 --6.9000e+02 1.7143e+03 4.3175e+00 --6.9000e+02 1.7429e+03 4.4345e+00 --6.9000e+02 1.7714e+03 4.5472e+00 --6.9000e+02 1.8000e+03 4.6556e+00 --6.9000e+02 1.8286e+03 4.7600e+00 --6.9000e+02 1.8571e+03 4.8606e+00 --6.9000e+02 1.8857e+03 4.9576e+00 --6.9000e+02 1.9143e+03 5.0512e+00 --6.9000e+02 1.9429e+03 5.1415e+00 --6.9000e+02 1.9714e+03 5.2288e+00 --6.9000e+02 2.0000e+03 5.3131e+00 --6.6000e+02 -2.0000e+03 5.2835e+00 --6.6000e+02 -1.9714e+03 5.1977e+00 --6.6000e+02 -1.9429e+03 5.1088e+00 --6.6000e+02 -1.9143e+03 5.0168e+00 --6.6000e+02 -1.8857e+03 4.9214e+00 --6.6000e+02 -1.8571e+03 4.8225e+00 --6.6000e+02 -1.8286e+03 4.7198e+00 --6.6000e+02 -1.8000e+03 4.6132e+00 --6.6000e+02 -1.7714e+03 4.5025e+00 --6.6000e+02 -1.7429e+03 4.3873e+00 --6.6000e+02 -1.7143e+03 4.2675e+00 --6.6000e+02 -1.6857e+03 4.1428e+00 --6.6000e+02 -1.6571e+03 4.0129e+00 --6.6000e+02 -1.6286e+03 3.8774e+00 --6.6000e+02 -1.6000e+03 3.7361e+00 --6.6000e+02 -1.5714e+03 3.5885e+00 --6.6000e+02 -1.5429e+03 3.4343e+00 --6.6000e+02 -1.5143e+03 3.2730e+00 --6.6000e+02 -1.4857e+03 3.1042e+00 --6.6000e+02 -1.4571e+03 2.9273e+00 --6.6000e+02 -1.4286e+03 2.7419e+00 --6.6000e+02 -1.4000e+03 2.5472e+00 --6.6000e+02 -1.3714e+03 2.3427e+00 --6.6000e+02 -1.3429e+03 2.1277e+00 --6.6000e+02 -1.3143e+03 1.9013e+00 --6.6000e+02 -1.2857e+03 1.6628e+00 --6.6000e+02 -1.2571e+03 1.4112e+00 --6.6000e+02 -1.2286e+03 1.1455e+00 --6.6000e+02 -1.2000e+03 8.6453e-01 --6.6000e+02 -1.1714e+03 5.6715e-01 --6.6000e+02 -1.1429e+03 2.5201e-01 --6.6000e+02 -1.1143e+03 -8.2383e-02 --6.6000e+02 -1.0857e+03 -4.3764e-01 --6.6000e+02 -1.0571e+03 -8.1554e-01 --6.6000e+02 -1.0286e+03 -1.2181e+00 --6.6000e+02 -1.0000e+03 -1.6473e+00 --6.6000e+02 -9.7143e+02 -2.1058e+00 --6.6000e+02 -9.4286e+02 -2.5961e+00 --6.6000e+02 -9.1429e+02 -3.1211e+00 --6.6000e+02 -8.8571e+02 -3.6840e+00 --6.6000e+02 -8.5714e+02 -4.2883e+00 --6.6000e+02 -8.2857e+02 -4.9379e+00 --6.6000e+02 -8.0000e+02 -5.6369e+00 --6.6000e+02 -7.7143e+02 -6.3900e+00 --6.6000e+02 -7.4286e+02 -7.2021e+00 --6.6000e+02 -7.1429e+02 -8.0784e+00 --6.6000e+02 -6.8571e+02 -9.0245e+00 --6.6000e+02 -6.5714e+02 -1.0046e+01 --6.6000e+02 -6.2857e+02 -1.1150e+01 --6.6000e+02 -6.0000e+02 -1.2340e+01 --6.6000e+02 -5.7143e+02 -1.3624e+01 --6.6000e+02 -5.4286e+02 -1.5005e+01 --6.6000e+02 -5.1429e+02 -1.6488e+01 --6.6000e+02 -4.8571e+02 -1.8073e+01 --6.6000e+02 -4.5714e+02 -1.9761e+01 --6.6000e+02 -4.2857e+02 -2.1547e+01 --6.6000e+02 -4.0000e+02 -2.3422e+01 --6.6000e+02 -3.7143e+02 -2.5374e+01 --6.6000e+02 -3.4286e+02 -2.7383e+01 --6.6000e+02 -3.1429e+02 -2.9425e+01 --6.6000e+02 -2.8571e+02 -3.1468e+01 --6.6000e+02 -2.5714e+02 -3.3479e+01 --6.6000e+02 -2.2857e+02 -3.5417e+01 --6.6000e+02 -2.0000e+02 -3.7243e+01 --6.6000e+02 -1.7143e+02 -3.8917e+01 --6.6000e+02 -1.4286e+02 -4.0401e+01 --6.6000e+02 -1.1429e+02 -4.1663e+01 --6.6000e+02 -8.5714e+01 -4.2674e+01 --6.6000e+02 -5.7143e+01 -4.3412e+01 --6.6000e+02 -2.8571e+01 -4.3861e+01 --6.6000e+02 0.0000e+00 -4.4012e+01 --6.6000e+02 2.8571e+01 -4.3861e+01 --6.6000e+02 5.7143e+01 -4.3412e+01 --6.6000e+02 8.5714e+01 -4.2674e+01 --6.6000e+02 1.1429e+02 -4.1663e+01 --6.6000e+02 1.4286e+02 -4.0401e+01 --6.6000e+02 1.7143e+02 -3.8917e+01 --6.6000e+02 2.0000e+02 -3.7243e+01 --6.6000e+02 2.2857e+02 -3.5417e+01 --6.6000e+02 2.5714e+02 -3.3479e+01 --6.6000e+02 2.8571e+02 -3.1468e+01 --6.6000e+02 3.1429e+02 -2.9425e+01 --6.6000e+02 3.4286e+02 -2.7383e+01 --6.6000e+02 3.7143e+02 -2.5374e+01 --6.6000e+02 4.0000e+02 -2.3422e+01 --6.6000e+02 4.2857e+02 -2.1547e+01 --6.6000e+02 4.5714e+02 -1.9761e+01 --6.6000e+02 4.8571e+02 -1.8073e+01 --6.6000e+02 5.1429e+02 -1.6488e+01 --6.6000e+02 5.4286e+02 -1.5005e+01 --6.6000e+02 5.7143e+02 -1.3624e+01 --6.6000e+02 6.0000e+02 -1.2340e+01 --6.6000e+02 6.2857e+02 -1.1150e+01 --6.6000e+02 6.5714e+02 -1.0046e+01 --6.6000e+02 6.8571e+02 -9.0245e+00 --6.6000e+02 7.1429e+02 -8.0784e+00 --6.6000e+02 7.4286e+02 -7.2021e+00 --6.6000e+02 7.7143e+02 -6.3900e+00 --6.6000e+02 8.0000e+02 -5.6369e+00 --6.6000e+02 8.2857e+02 -4.9379e+00 --6.6000e+02 8.5714e+02 -4.2883e+00 --6.6000e+02 8.8571e+02 -3.6840e+00 --6.6000e+02 9.1429e+02 -3.1211e+00 --6.6000e+02 9.4286e+02 -2.5961e+00 --6.6000e+02 9.7143e+02 -2.1058e+00 --6.6000e+02 1.0000e+03 -1.6473e+00 --6.6000e+02 1.0286e+03 -1.2181e+00 --6.6000e+02 1.0571e+03 -8.1554e-01 --6.6000e+02 1.0857e+03 -4.3764e-01 --6.6000e+02 1.1143e+03 -8.2383e-02 --6.6000e+02 1.1429e+03 2.5201e-01 --6.6000e+02 1.1714e+03 5.6715e-01 --6.6000e+02 1.2000e+03 8.6453e-01 --6.6000e+02 1.2286e+03 1.1455e+00 --6.6000e+02 1.2571e+03 1.4112e+00 --6.6000e+02 1.2857e+03 1.6628e+00 --6.6000e+02 1.3143e+03 1.9013e+00 --6.6000e+02 1.3429e+03 2.1277e+00 --6.6000e+02 1.3714e+03 2.3427e+00 --6.6000e+02 1.4000e+03 2.5472e+00 --6.6000e+02 1.4286e+03 2.7419e+00 --6.6000e+02 1.4571e+03 2.9273e+00 --6.6000e+02 1.4857e+03 3.1042e+00 --6.6000e+02 1.5143e+03 3.2730e+00 --6.6000e+02 1.5429e+03 3.4343e+00 --6.6000e+02 1.5714e+03 3.5885e+00 --6.6000e+02 1.6000e+03 3.7361e+00 --6.6000e+02 1.6286e+03 3.8774e+00 --6.6000e+02 1.6571e+03 4.0129e+00 --6.6000e+02 1.6857e+03 4.1428e+00 --6.6000e+02 1.7143e+03 4.2675e+00 --6.6000e+02 1.7429e+03 4.3873e+00 --6.6000e+02 1.7714e+03 4.5025e+00 --6.6000e+02 1.8000e+03 4.6132e+00 --6.6000e+02 1.8286e+03 4.7198e+00 --6.6000e+02 1.8571e+03 4.8225e+00 --6.6000e+02 1.8857e+03 4.9214e+00 --6.6000e+02 1.9143e+03 5.0168e+00 --6.6000e+02 1.9429e+03 5.1088e+00 --6.6000e+02 1.9714e+03 5.1977e+00 --6.6000e+02 2.0000e+03 5.2835e+00 --6.3000e+02 -2.0000e+03 5.2547e+00 --6.3000e+02 -1.9714e+03 5.1674e+00 --6.3000e+02 -1.9429e+03 5.0770e+00 --6.3000e+02 -1.9143e+03 4.9834e+00 --6.3000e+02 -1.8857e+03 4.8862e+00 --6.3000e+02 -1.8571e+03 4.7854e+00 --6.3000e+02 -1.8286e+03 4.6807e+00 --6.3000e+02 -1.8000e+03 4.5719e+00 --6.3000e+02 -1.7714e+03 4.4588e+00 --6.3000e+02 -1.7429e+03 4.3412e+00 --6.3000e+02 -1.7143e+03 4.2187e+00 --6.3000e+02 -1.6857e+03 4.0911e+00 --6.3000e+02 -1.6571e+03 3.9580e+00 --6.3000e+02 -1.6286e+03 3.8192e+00 --6.3000e+02 -1.6000e+03 3.6742e+00 --6.3000e+02 -1.5714e+03 3.5226e+00 --6.3000e+02 -1.5429e+03 3.3641e+00 --6.3000e+02 -1.5143e+03 3.1981e+00 --6.3000e+02 -1.4857e+03 3.0242e+00 --6.3000e+02 -1.4571e+03 2.8418e+00 --6.3000e+02 -1.4286e+03 2.6503e+00 --6.3000e+02 -1.4000e+03 2.4491e+00 --6.3000e+02 -1.3714e+03 2.2374e+00 --6.3000e+02 -1.3429e+03 2.0144e+00 --6.3000e+02 -1.3143e+03 1.7793e+00 --6.3000e+02 -1.2857e+03 1.5312e+00 --6.3000e+02 -1.2571e+03 1.2690e+00 --6.3000e+02 -1.2286e+03 9.9155e-01 --6.3000e+02 -1.2000e+03 6.9765e-01 --6.3000e+02 -1.1714e+03 3.8590e-01 --6.3000e+02 -1.1429e+03 5.4774e-02 --6.3000e+02 -1.1143e+03 -2.9743e-01 --6.3000e+02 -1.0857e+03 -6.7257e-01 --6.3000e+02 -1.0571e+03 -1.0727e+00 --6.3000e+02 -1.0286e+03 -1.5002e+00 --6.3000e+02 -1.0000e+03 -1.9576e+00 --6.3000e+02 -9.7143e+02 -2.4477e+00 --6.3000e+02 -9.4286e+02 -2.9738e+00 --6.3000e+02 -9.1429e+02 -3.5392e+00 --6.3000e+02 -8.8571e+02 -4.1481e+00 --6.3000e+02 -8.5714e+02 -4.8047e+00 --6.3000e+02 -8.2857e+02 -5.5139e+00 --6.3000e+02 -8.0000e+02 -6.2810e+00 --6.3000e+02 -7.7143e+02 -7.1119e+00 --6.3000e+02 -7.4286e+02 -8.0132e+00 --6.3000e+02 -7.1429e+02 -8.9919e+00 --6.3000e+02 -6.8571e+02 -1.0056e+01 --6.3000e+02 -6.5714e+02 -1.1213e+01 --6.3000e+02 -6.2857e+02 -1.2471e+01 --6.3000e+02 -6.0000e+02 -1.3839e+01 --6.3000e+02 -5.7143e+02 -1.5326e+01 --6.3000e+02 -5.4286e+02 -1.6939e+01 --6.3000e+02 -5.1429e+02 -1.8684e+01 --6.3000e+02 -4.8571e+02 -2.0565e+01 --6.3000e+02 -4.5714e+02 -2.2581e+01 --6.3000e+02 -4.2857e+02 -2.4730e+01 --6.3000e+02 -4.0000e+02 -2.6998e+01 --6.3000e+02 -3.7143e+02 -2.9370e+01 --6.3000e+02 -3.4286e+02 -3.1817e+01 --6.3000e+02 -3.1429e+02 -3.4306e+01 --6.3000e+02 -2.8571e+02 -3.6795e+01 --6.3000e+02 -2.5714e+02 -3.9236e+01 --6.3000e+02 -2.2857e+02 -4.1579e+01 --6.3000e+02 -2.0000e+02 -4.3772e+01 --6.3000e+02 -1.7143e+02 -4.5769e+01 --6.3000e+02 -1.4286e+02 -4.7526e+01 --6.3000e+02 -1.1429e+02 -4.9009e+01 --6.3000e+02 -8.5714e+01 -5.0189e+01 --6.3000e+02 -5.7143e+01 -5.1047e+01 --6.3000e+02 -2.8571e+01 -5.1566e+01 --6.3000e+02 0.0000e+00 -5.1740e+01 --6.3000e+02 2.8571e+01 -5.1566e+01 --6.3000e+02 5.7143e+01 -5.1047e+01 --6.3000e+02 8.5714e+01 -5.0189e+01 --6.3000e+02 1.1429e+02 -4.9009e+01 --6.3000e+02 1.4286e+02 -4.7526e+01 --6.3000e+02 1.7143e+02 -4.5769e+01 --6.3000e+02 2.0000e+02 -4.3772e+01 --6.3000e+02 2.2857e+02 -4.1579e+01 --6.3000e+02 2.5714e+02 -3.9236e+01 --6.3000e+02 2.8571e+02 -3.6795e+01 --6.3000e+02 3.1429e+02 -3.4306e+01 --6.3000e+02 3.4286e+02 -3.1817e+01 --6.3000e+02 3.7143e+02 -2.9370e+01 --6.3000e+02 4.0000e+02 -2.6998e+01 --6.3000e+02 4.2857e+02 -2.4730e+01 --6.3000e+02 4.5714e+02 -2.2581e+01 --6.3000e+02 4.8571e+02 -2.0565e+01 --6.3000e+02 5.1429e+02 -1.8684e+01 --6.3000e+02 5.4286e+02 -1.6939e+01 --6.3000e+02 5.7143e+02 -1.5326e+01 --6.3000e+02 6.0000e+02 -1.3839e+01 --6.3000e+02 6.2857e+02 -1.2471e+01 --6.3000e+02 6.5714e+02 -1.1213e+01 --6.3000e+02 6.8571e+02 -1.0056e+01 --6.3000e+02 7.1429e+02 -8.9919e+00 --6.3000e+02 7.4286e+02 -8.0132e+00 --6.3000e+02 7.7143e+02 -7.1119e+00 --6.3000e+02 8.0000e+02 -6.2810e+00 --6.3000e+02 8.2857e+02 -5.5139e+00 --6.3000e+02 8.5714e+02 -4.8047e+00 --6.3000e+02 8.8571e+02 -4.1481e+00 --6.3000e+02 9.1429e+02 -3.5392e+00 --6.3000e+02 9.4286e+02 -2.9738e+00 --6.3000e+02 9.7143e+02 -2.4477e+00 --6.3000e+02 1.0000e+03 -1.9576e+00 --6.3000e+02 1.0286e+03 -1.5002e+00 --6.3000e+02 1.0571e+03 -1.0727e+00 --6.3000e+02 1.0857e+03 -6.7257e-01 --6.3000e+02 1.1143e+03 -2.9743e-01 --6.3000e+02 1.1429e+03 5.4774e-02 --6.3000e+02 1.1714e+03 3.8590e-01 --6.3000e+02 1.2000e+03 6.9765e-01 --6.3000e+02 1.2286e+03 9.9155e-01 --6.3000e+02 1.2571e+03 1.2690e+00 --6.3000e+02 1.2857e+03 1.5312e+00 --6.3000e+02 1.3143e+03 1.7793e+00 --6.3000e+02 1.3429e+03 2.0144e+00 --6.3000e+02 1.3714e+03 2.2374e+00 --6.3000e+02 1.4000e+03 2.4491e+00 --6.3000e+02 1.4286e+03 2.6503e+00 --6.3000e+02 1.4571e+03 2.8418e+00 --6.3000e+02 1.4857e+03 3.0242e+00 --6.3000e+02 1.5143e+03 3.1981e+00 --6.3000e+02 1.5429e+03 3.3641e+00 --6.3000e+02 1.5714e+03 3.5226e+00 --6.3000e+02 1.6000e+03 3.6742e+00 --6.3000e+02 1.6286e+03 3.8192e+00 --6.3000e+02 1.6571e+03 3.9580e+00 --6.3000e+02 1.6857e+03 4.0911e+00 --6.3000e+02 1.7143e+03 4.2187e+00 --6.3000e+02 1.7429e+03 4.3412e+00 --6.3000e+02 1.7714e+03 4.4588e+00 --6.3000e+02 1.8000e+03 4.5719e+00 --6.3000e+02 1.8286e+03 4.6807e+00 --6.3000e+02 1.8571e+03 4.7854e+00 --6.3000e+02 1.8857e+03 4.8862e+00 --6.3000e+02 1.9143e+03 4.9834e+00 --6.3000e+02 1.9429e+03 5.0770e+00 --6.3000e+02 1.9714e+03 5.1674e+00 --6.3000e+02 2.0000e+03 5.2547e+00 --6.0000e+02 -2.0000e+03 5.2268e+00 --6.0000e+02 -1.9714e+03 5.1381e+00 --6.0000e+02 -1.9429e+03 5.0462e+00 --6.0000e+02 -1.9143e+03 4.9509e+00 --6.0000e+02 -1.8857e+03 4.8520e+00 --6.0000e+02 -1.8571e+03 4.7493e+00 --6.0000e+02 -1.8286e+03 4.6427e+00 --6.0000e+02 -1.8000e+03 4.5318e+00 --6.0000e+02 -1.7714e+03 4.4164e+00 --6.0000e+02 -1.7429e+03 4.2962e+00 --6.0000e+02 -1.7143e+03 4.1711e+00 --6.0000e+02 -1.6857e+03 4.0406e+00 --6.0000e+02 -1.6571e+03 3.9044e+00 --6.0000e+02 -1.6286e+03 3.7622e+00 --6.0000e+02 -1.6000e+03 3.6136e+00 --6.0000e+02 -1.5714e+03 3.4581e+00 --6.0000e+02 -1.5429e+03 3.2953e+00 --6.0000e+02 -1.5143e+03 3.1247e+00 --6.0000e+02 -1.4857e+03 2.9457e+00 --6.0000e+02 -1.4571e+03 2.7578e+00 --6.0000e+02 -1.4286e+03 2.5602e+00 --6.0000e+02 -1.4000e+03 2.3523e+00 --6.0000e+02 -1.3714e+03 2.1333e+00 --6.0000e+02 -1.3429e+03 1.9023e+00 --6.0000e+02 -1.3143e+03 1.6584e+00 --6.0000e+02 -1.2857e+03 1.4006e+00 --6.0000e+02 -1.2571e+03 1.1276e+00 --6.0000e+02 -1.2286e+03 8.3825e-01 --6.0000e+02 -1.2000e+03 5.3111e-01 --6.0000e+02 -1.1714e+03 2.0462e-01 --6.0000e+02 -1.1429e+03 -1.4296e-01 --6.0000e+02 -1.1143e+03 -5.1355e-01 --6.0000e+02 -1.0857e+03 -9.0932e-01 --6.0000e+02 -1.0571e+03 -1.3327e+00 --6.0000e+02 -1.0286e+03 -1.7863e+00 --6.0000e+02 -1.0000e+03 -2.2732e+00 --6.0000e+02 -9.7143e+02 -2.7967e+00 --6.0000e+02 -9.4286e+02 -3.3607e+00 --6.0000e+02 -9.1429e+02 -3.9694e+00 --6.0000e+02 -8.8571e+02 -4.6277e+00 --6.0000e+02 -8.5714e+02 -5.3408e+00 --6.0000e+02 -8.2857e+02 -6.1149e+00 --6.0000e+02 -8.0000e+02 -6.9567e+00 --6.0000e+02 -7.7143e+02 -7.8737e+00 --6.0000e+02 -7.4286e+02 -8.8745e+00 --6.0000e+02 -7.1429e+02 -9.9684e+00 --6.0000e+02 -6.8571e+02 -1.1166e+01 --6.0000e+02 -6.5714e+02 -1.2477e+01 --6.0000e+02 -6.2857e+02 -1.3914e+01 --6.0000e+02 -6.0000e+02 -1.5490e+01 --6.0000e+02 -5.7143e+02 -1.7216e+01 --6.0000e+02 -5.4286e+02 -1.9103e+01 --6.0000e+02 -5.1429e+02 -2.1161e+01 --6.0000e+02 -4.8571e+02 -2.3395e+01 --6.0000e+02 -4.5714e+02 -2.5807e+01 --6.0000e+02 -4.2857e+02 -2.8389e+01 --6.0000e+02 -4.0000e+02 -3.1126e+01 --6.0000e+02 -3.7143e+02 -3.3991e+01 --6.0000e+02 -3.4286e+02 -3.6945e+01 --6.0000e+02 -3.1429e+02 -3.9937e+01 --6.0000e+02 -2.8571e+02 -4.2910e+01 --6.0000e+02 -2.5714e+02 -4.5800e+01 --6.0000e+02 -2.2857e+02 -4.8544e+01 --6.0000e+02 -2.0000e+02 -5.1081e+01 --6.0000e+02 -1.7143e+02 -5.3362e+01 --6.0000e+02 -1.4286e+02 -5.5346e+01 --6.0000e+02 -1.1429e+02 -5.7000e+01 --6.0000e+02 -8.5714e+01 -5.8304e+01 --6.0000e+02 -5.7143e+01 -5.9244e+01 --6.0000e+02 -2.8571e+01 -5.9811e+01 --6.0000e+02 0.0000e+00 -6.0000e+01 --6.0000e+02 2.8571e+01 -5.9811e+01 --6.0000e+02 5.7143e+01 -5.9244e+01 --6.0000e+02 8.5714e+01 -5.8304e+01 --6.0000e+02 1.1429e+02 -5.7000e+01 --6.0000e+02 1.4286e+02 -5.5346e+01 --6.0000e+02 1.7143e+02 -5.3362e+01 --6.0000e+02 2.0000e+02 -5.1081e+01 --6.0000e+02 2.2857e+02 -4.8544e+01 --6.0000e+02 2.5714e+02 -4.5800e+01 --6.0000e+02 2.8571e+02 -4.2910e+01 --6.0000e+02 3.1429e+02 -3.9937e+01 --6.0000e+02 3.4286e+02 -3.6945e+01 --6.0000e+02 3.7143e+02 -3.3991e+01 --6.0000e+02 4.0000e+02 -3.1126e+01 --6.0000e+02 4.2857e+02 -2.8389e+01 --6.0000e+02 4.5714e+02 -2.5807e+01 --6.0000e+02 4.8571e+02 -2.3395e+01 --6.0000e+02 5.1429e+02 -2.1161e+01 --6.0000e+02 5.4286e+02 -1.9103e+01 --6.0000e+02 5.7143e+02 -1.7216e+01 --6.0000e+02 6.0000e+02 -1.5490e+01 --6.0000e+02 6.2857e+02 -1.3914e+01 --6.0000e+02 6.5714e+02 -1.2477e+01 --6.0000e+02 6.8571e+02 -1.1166e+01 --6.0000e+02 7.1429e+02 -9.9684e+00 --6.0000e+02 7.4286e+02 -8.8745e+00 --6.0000e+02 7.7143e+02 -7.8737e+00 --6.0000e+02 8.0000e+02 -6.9567e+00 --6.0000e+02 8.2857e+02 -6.1149e+00 --6.0000e+02 8.5714e+02 -5.3408e+00 --6.0000e+02 8.8571e+02 -4.6277e+00 --6.0000e+02 9.1429e+02 -3.9694e+00 --6.0000e+02 9.4286e+02 -3.3607e+00 --6.0000e+02 9.7143e+02 -2.7967e+00 --6.0000e+02 1.0000e+03 -2.2732e+00 --6.0000e+02 1.0286e+03 -1.7863e+00 --6.0000e+02 1.0571e+03 -1.3327e+00 --6.0000e+02 1.0857e+03 -9.0932e-01 --6.0000e+02 1.1143e+03 -5.1355e-01 --6.0000e+02 1.1429e+03 -1.4296e-01 --6.0000e+02 1.1714e+03 2.0462e-01 --6.0000e+02 1.2000e+03 5.3111e-01 --6.0000e+02 1.2286e+03 8.3825e-01 --6.0000e+02 1.2571e+03 1.1276e+00 --6.0000e+02 1.2857e+03 1.4006e+00 --6.0000e+02 1.3143e+03 1.6584e+00 --6.0000e+02 1.3429e+03 1.9023e+00 --6.0000e+02 1.3714e+03 2.1333e+00 --6.0000e+02 1.4000e+03 2.3523e+00 --6.0000e+02 1.4286e+03 2.5602e+00 --6.0000e+02 1.4571e+03 2.7578e+00 --6.0000e+02 1.4857e+03 2.9457e+00 --6.0000e+02 1.5143e+03 3.1247e+00 --6.0000e+02 1.5429e+03 3.2953e+00 --6.0000e+02 1.5714e+03 3.4581e+00 --6.0000e+02 1.6000e+03 3.6136e+00 --6.0000e+02 1.6286e+03 3.7622e+00 --6.0000e+02 1.6571e+03 3.9044e+00 --6.0000e+02 1.6857e+03 4.0406e+00 --6.0000e+02 1.7143e+03 4.1711e+00 --6.0000e+02 1.7429e+03 4.2962e+00 --6.0000e+02 1.7714e+03 4.4164e+00 --6.0000e+02 1.8000e+03 4.5318e+00 --6.0000e+02 1.8286e+03 4.6427e+00 --6.0000e+02 1.8571e+03 4.7493e+00 --6.0000e+02 1.8857e+03 4.8520e+00 --6.0000e+02 1.9143e+03 4.9509e+00 --6.0000e+02 1.9429e+03 5.0462e+00 --6.0000e+02 1.9714e+03 5.1381e+00 --6.0000e+02 2.0000e+03 5.2268e+00 --5.7000e+02 -2.0000e+03 5.1998e+00 --5.7000e+02 -1.9714e+03 5.1098e+00 --5.7000e+02 -1.9429e+03 5.0164e+00 --5.7000e+02 -1.9143e+03 4.9195e+00 --5.7000e+02 -1.8857e+03 4.8189e+00 --5.7000e+02 -1.8571e+03 4.7144e+00 --5.7000e+02 -1.8286e+03 4.6058e+00 --5.7000e+02 -1.8000e+03 4.4928e+00 --5.7000e+02 -1.7714e+03 4.3752e+00 --5.7000e+02 -1.7429e+03 4.2526e+00 --5.7000e+02 -1.7143e+03 4.1248e+00 --5.7000e+02 -1.6857e+03 3.9915e+00 --5.7000e+02 -1.6571e+03 3.8523e+00 --5.7000e+02 -1.6286e+03 3.7067e+00 --5.7000e+02 -1.6000e+03 3.5545e+00 --5.7000e+02 -1.5714e+03 3.3951e+00 --5.7000e+02 -1.5429e+03 3.2281e+00 --5.7000e+02 -1.5143e+03 3.0528e+00 --5.7000e+02 -1.4857e+03 2.8688e+00 --5.7000e+02 -1.4571e+03 2.6753e+00 --5.7000e+02 -1.4286e+03 2.4717e+00 --5.7000e+02 -1.4000e+03 2.2572e+00 --5.7000e+02 -1.3714e+03 2.0309e+00 --5.7000e+02 -1.3429e+03 1.7919e+00 --5.7000e+02 -1.3143e+03 1.5391e+00 --5.7000e+02 -1.2857e+03 1.2714e+00 --5.7000e+02 -1.2571e+03 9.8756e-01 --5.7000e+02 -1.2286e+03 6.8611e-01 --5.7000e+02 -1.2000e+03 3.6548e-01 --5.7000e+02 -1.1714e+03 2.3931e-02 --5.7000e+02 -1.1429e+03 -3.4051e-01 --5.7000e+02 -1.1143e+03 -7.3003e-01 --5.7000e+02 -1.0857e+03 -1.1471e+00 --5.7000e+02 -1.0571e+03 -1.5945e+00 --5.7000e+02 -1.0286e+03 -2.0753e+00 --5.7000e+02 -1.0000e+03 -2.5931e+00 --5.7000e+02 -9.7143e+02 -3.1518e+00 --5.7000e+02 -9.4286e+02 -3.7560e+00 --5.7000e+02 -9.1429e+02 -4.4107e+00 --5.7000e+02 -8.8571e+02 -5.1218e+00 --5.7000e+02 -8.5714e+02 -5.8959e+00 --5.7000e+02 -8.2857e+02 -6.7404e+00 --5.7000e+02 -8.0000e+02 -7.6638e+00 --5.7000e+02 -7.7143e+02 -8.6758e+00 --5.7000e+02 -7.4286e+02 -9.7872e+00 --5.7000e+02 -7.1429e+02 -1.1010e+01 --5.7000e+02 -6.8571e+02 -1.2358e+01 --5.7000e+02 -6.5714e+02 -1.3846e+01 --5.7000e+02 -6.2857e+02 -1.5490e+01 --5.7000e+02 -6.0000e+02 -1.7306e+01 --5.7000e+02 -5.7143e+02 -1.9312e+01 --5.7000e+02 -5.4286e+02 -2.1523e+01 --5.7000e+02 -5.1429e+02 -2.3951e+01 --5.7000e+02 -4.8571e+02 -2.6604e+01 --5.7000e+02 -4.5714e+02 -2.9481e+01 --5.7000e+02 -4.2857e+02 -3.2571e+01 --5.7000e+02 -4.0000e+02 -3.5846e+01 --5.7000e+02 -3.7143e+02 -3.9263e+01 --5.7000e+02 -3.4286e+02 -4.2763e+01 --5.7000e+02 -3.1429e+02 -4.6274e+01 --5.7000e+02 -2.8571e+02 -4.9714e+01 --5.7000e+02 -2.5714e+02 -5.3006e+01 --5.7000e+02 -2.2857e+02 -5.6077e+01 --5.7000e+02 -2.0000e+02 -5.8867e+01 --5.7000e+02 -1.7143e+02 -6.1331e+01 --5.7000e+02 -1.4286e+02 -6.3439e+01 --5.7000e+02 -1.1429e+02 -6.5173e+01 --5.7000e+02 -8.5714e+01 -6.6524e+01 --5.7000e+02 -5.7143e+01 -6.7489e+01 --5.7000e+02 -2.8571e+01 -6.8067e+01 --5.7000e+02 0.0000e+00 -6.8260e+01 --5.7000e+02 2.8571e+01 -6.8067e+01 --5.7000e+02 5.7143e+01 -6.7489e+01 --5.7000e+02 8.5714e+01 -6.6524e+01 --5.7000e+02 1.1429e+02 -6.5173e+01 --5.7000e+02 1.4286e+02 -6.3439e+01 --5.7000e+02 1.7143e+02 -6.1331e+01 --5.7000e+02 2.0000e+02 -5.8867e+01 --5.7000e+02 2.2857e+02 -5.6077e+01 --5.7000e+02 2.5714e+02 -5.3006e+01 --5.7000e+02 2.8571e+02 -4.9714e+01 --5.7000e+02 3.1429e+02 -4.6274e+01 --5.7000e+02 3.4286e+02 -4.2763e+01 --5.7000e+02 3.7143e+02 -3.9263e+01 --5.7000e+02 4.0000e+02 -3.5846e+01 --5.7000e+02 4.2857e+02 -3.2571e+01 --5.7000e+02 4.5714e+02 -2.9481e+01 --5.7000e+02 4.8571e+02 -2.6604e+01 --5.7000e+02 5.1429e+02 -2.3951e+01 --5.7000e+02 5.4286e+02 -2.1523e+01 --5.7000e+02 5.7143e+02 -1.9312e+01 --5.7000e+02 6.0000e+02 -1.7306e+01 --5.7000e+02 6.2857e+02 -1.5490e+01 --5.7000e+02 6.5714e+02 -1.3846e+01 --5.7000e+02 6.8571e+02 -1.2358e+01 --5.7000e+02 7.1429e+02 -1.1010e+01 --5.7000e+02 7.4286e+02 -9.7872e+00 --5.7000e+02 7.7143e+02 -8.6758e+00 --5.7000e+02 8.0000e+02 -7.6638e+00 --5.7000e+02 8.2857e+02 -6.7404e+00 --5.7000e+02 8.5714e+02 -5.8959e+00 --5.7000e+02 8.8571e+02 -5.1218e+00 --5.7000e+02 9.1429e+02 -4.4107e+00 --5.7000e+02 9.4286e+02 -3.7560e+00 --5.7000e+02 9.7143e+02 -3.1518e+00 --5.7000e+02 1.0000e+03 -2.5931e+00 --5.7000e+02 1.0286e+03 -2.0753e+00 --5.7000e+02 1.0571e+03 -1.5945e+00 --5.7000e+02 1.0857e+03 -1.1471e+00 --5.7000e+02 1.1143e+03 -7.3003e-01 --5.7000e+02 1.1429e+03 -3.4051e-01 --5.7000e+02 1.1714e+03 2.3931e-02 --5.7000e+02 1.2000e+03 3.6548e-01 --5.7000e+02 1.2286e+03 6.8611e-01 --5.7000e+02 1.2571e+03 9.8756e-01 --5.7000e+02 1.2857e+03 1.2714e+00 --5.7000e+02 1.3143e+03 1.5391e+00 --5.7000e+02 1.3429e+03 1.7919e+00 --5.7000e+02 1.3714e+03 2.0309e+00 --5.7000e+02 1.4000e+03 2.2572e+00 --5.7000e+02 1.4286e+03 2.4717e+00 --5.7000e+02 1.4571e+03 2.6753e+00 --5.7000e+02 1.4857e+03 2.8688e+00 --5.7000e+02 1.5143e+03 3.0528e+00 --5.7000e+02 1.5429e+03 3.2281e+00 --5.7000e+02 1.5714e+03 3.3951e+00 --5.7000e+02 1.6000e+03 3.5545e+00 --5.7000e+02 1.6286e+03 3.7067e+00 --5.7000e+02 1.6571e+03 3.8523e+00 --5.7000e+02 1.6857e+03 3.9915e+00 --5.7000e+02 1.7143e+03 4.1248e+00 --5.7000e+02 1.7429e+03 4.2526e+00 --5.7000e+02 1.7714e+03 4.3752e+00 --5.7000e+02 1.8000e+03 4.4928e+00 --5.7000e+02 1.8286e+03 4.6058e+00 --5.7000e+02 1.8571e+03 4.7144e+00 --5.7000e+02 1.8857e+03 4.8189e+00 --5.7000e+02 1.9143e+03 4.9195e+00 --5.7000e+02 1.9429e+03 5.0164e+00 --5.7000e+02 1.9714e+03 5.1098e+00 --5.7000e+02 2.0000e+03 5.1998e+00 --5.4000e+02 -2.0000e+03 5.1739e+00 --5.4000e+02 -1.9714e+03 5.0824e+00 --5.4000e+02 -1.9429e+03 4.9876e+00 --5.4000e+02 -1.9143e+03 4.8892e+00 --5.4000e+02 -1.8857e+03 4.7870e+00 --5.4000e+02 -1.8571e+03 4.6807e+00 --5.4000e+02 -1.8286e+03 4.5702e+00 --5.4000e+02 -1.8000e+03 4.4551e+00 --5.4000e+02 -1.7714e+03 4.3353e+00 --5.4000e+02 -1.7429e+03 4.2104e+00 --5.4000e+02 -1.7143e+03 4.0800e+00 --5.4000e+02 -1.6857e+03 3.9439e+00 --5.4000e+02 -1.6571e+03 3.8017e+00 --5.4000e+02 -1.6286e+03 3.6529e+00 --5.4000e+02 -1.6000e+03 3.4971e+00 --5.4000e+02 -1.5714e+03 3.3339e+00 --5.4000e+02 -1.5429e+03 3.1627e+00 --5.4000e+02 -1.5143e+03 2.9828e+00 --5.4000e+02 -1.4857e+03 2.7938e+00 --5.4000e+02 -1.4571e+03 2.5948e+00 --5.4000e+02 -1.4286e+03 2.3852e+00 --5.4000e+02 -1.4000e+03 2.1641e+00 --5.4000e+02 -1.3714e+03 1.9305e+00 --5.4000e+02 -1.3429e+03 1.6834e+00 --5.4000e+02 -1.3143e+03 1.4218e+00 --5.4000e+02 -1.2857e+03 1.1442e+00 --5.4000e+02 -1.2571e+03 8.4936e-01 --5.4000e+02 -1.2286e+03 5.3567e-01 --5.4000e+02 -1.2000e+03 2.0138e-01 --5.4000e+02 -1.1714e+03 -1.5549e-01 --5.4000e+02 -1.1429e+03 -5.3713e-01 --5.4000e+02 -1.1143e+03 -9.4603e-01 --5.4000e+02 -1.0857e+03 -1.3850e+00 --5.4000e+02 -1.0571e+03 -1.8572e+00 --5.4000e+02 -1.0286e+03 -2.3662e+00 --5.4000e+02 -1.0000e+03 -2.9162e+00 --5.4000e+02 -9.7143e+02 -3.5117e+00 --5.4000e+02 -9.4286e+02 -4.1582e+00 --5.4000e+02 -9.1429e+02 -4.8617e+00 --5.4000e+02 -8.8571e+02 -5.6292e+00 --5.4000e+02 -8.5714e+02 -6.4686e+00 --5.4000e+02 -8.2857e+02 -7.3893e+00 --5.4000e+02 -8.0000e+02 -8.4017e+00 --5.4000e+02 -7.7143e+02 -9.5180e+00 --5.4000e+02 -7.4286e+02 -1.0752e+01 --5.4000e+02 -7.1429e+02 -1.2119e+01 --5.4000e+02 -6.8571e+02 -1.3637e+01 --5.4000e+02 -6.5714e+02 -1.5326e+01 --5.4000e+02 -6.2857e+02 -1.7207e+01 --5.4000e+02 -6.0000e+02 -1.9301e+01 --5.4000e+02 -5.7143e+02 -2.1632e+01 --5.4000e+02 -5.4286e+02 -2.4220e+01 --5.4000e+02 -5.1429e+02 -2.7081e+01 --5.4000e+02 -4.8571e+02 -3.0221e+01 --5.4000e+02 -4.5714e+02 -3.3633e+01 --5.4000e+02 -4.2857e+02 -3.7293e+01 --5.4000e+02 -4.0000e+02 -4.1153e+01 --5.4000e+02 -3.7143e+02 -4.5144e+01 --5.4000e+02 -3.4286e+02 -4.9175e+01 --5.4000e+02 -3.1429e+02 -5.3147e+01 --5.4000e+02 -2.8571e+02 -5.6961e+01 --5.4000e+02 -2.5714e+02 -6.0529e+01 --5.4000e+02 -2.2857e+02 -6.3784e+01 --5.4000e+02 -2.0000e+02 -6.6677e+01 --5.4000e+02 -1.7143e+02 -6.9182e+01 --5.4000e+02 -1.4286e+02 -7.1289e+01 --5.4000e+02 -1.1429e+02 -7.2998e+01 --5.4000e+02 -8.5714e+01 -7.4315e+01 --5.4000e+02 -5.7143e+01 -7.5247e+01 --5.4000e+02 -2.8571e+01 -7.5803e+01 --5.4000e+02 0.0000e+00 -7.5988e+01 --5.4000e+02 2.8571e+01 -7.5803e+01 --5.4000e+02 5.7143e+01 -7.5247e+01 --5.4000e+02 8.5714e+01 -7.4315e+01 --5.4000e+02 1.1429e+02 -7.2998e+01 --5.4000e+02 1.4286e+02 -7.1289e+01 --5.4000e+02 1.7143e+02 -6.9182e+01 --5.4000e+02 2.0000e+02 -6.6677e+01 --5.4000e+02 2.2857e+02 -6.3784e+01 --5.4000e+02 2.5714e+02 -6.0529e+01 --5.4000e+02 2.8571e+02 -5.6961e+01 --5.4000e+02 3.1429e+02 -5.3147e+01 --5.4000e+02 3.4286e+02 -4.9175e+01 --5.4000e+02 3.7143e+02 -4.5144e+01 --5.4000e+02 4.0000e+02 -4.1153e+01 --5.4000e+02 4.2857e+02 -3.7293e+01 --5.4000e+02 4.5714e+02 -3.3633e+01 --5.4000e+02 4.8571e+02 -3.0221e+01 --5.4000e+02 5.1429e+02 -2.7081e+01 --5.4000e+02 5.4286e+02 -2.4220e+01 --5.4000e+02 5.7143e+02 -2.1632e+01 --5.4000e+02 6.0000e+02 -1.9301e+01 --5.4000e+02 6.2857e+02 -1.7207e+01 --5.4000e+02 6.5714e+02 -1.5326e+01 --5.4000e+02 6.8571e+02 -1.3637e+01 --5.4000e+02 7.1429e+02 -1.2119e+01 --5.4000e+02 7.4286e+02 -1.0752e+01 --5.4000e+02 7.7143e+02 -9.5180e+00 --5.4000e+02 8.0000e+02 -8.4017e+00 --5.4000e+02 8.2857e+02 -7.3893e+00 --5.4000e+02 8.5714e+02 -6.4686e+00 --5.4000e+02 8.8571e+02 -5.6292e+00 --5.4000e+02 9.1429e+02 -4.8617e+00 --5.4000e+02 9.4286e+02 -4.1582e+00 --5.4000e+02 9.7143e+02 -3.5117e+00 --5.4000e+02 1.0000e+03 -2.9162e+00 --5.4000e+02 1.0286e+03 -2.3662e+00 --5.4000e+02 1.0571e+03 -1.8572e+00 --5.4000e+02 1.0857e+03 -1.3850e+00 --5.4000e+02 1.1143e+03 -9.4603e-01 --5.4000e+02 1.1429e+03 -5.3713e-01 --5.4000e+02 1.1714e+03 -1.5549e-01 --5.4000e+02 1.2000e+03 2.0138e-01 --5.4000e+02 1.2286e+03 5.3567e-01 --5.4000e+02 1.2571e+03 8.4936e-01 --5.4000e+02 1.2857e+03 1.1442e+00 --5.4000e+02 1.3143e+03 1.4218e+00 --5.4000e+02 1.3429e+03 1.6834e+00 --5.4000e+02 1.3714e+03 1.9305e+00 --5.4000e+02 1.4000e+03 2.1641e+00 --5.4000e+02 1.4286e+03 2.3852e+00 --5.4000e+02 1.4571e+03 2.5948e+00 --5.4000e+02 1.4857e+03 2.7938e+00 --5.4000e+02 1.5143e+03 2.9828e+00 --5.4000e+02 1.5429e+03 3.1627e+00 --5.4000e+02 1.5714e+03 3.3339e+00 --5.4000e+02 1.6000e+03 3.4971e+00 --5.4000e+02 1.6286e+03 3.6529e+00 --5.4000e+02 1.6571e+03 3.8017e+00 --5.4000e+02 1.6857e+03 3.9439e+00 --5.4000e+02 1.7143e+03 4.0800e+00 --5.4000e+02 1.7429e+03 4.2104e+00 --5.4000e+02 1.7714e+03 4.3353e+00 --5.4000e+02 1.8000e+03 4.4551e+00 --5.4000e+02 1.8286e+03 4.5702e+00 --5.4000e+02 1.8571e+03 4.6807e+00 --5.4000e+02 1.8857e+03 4.7870e+00 --5.4000e+02 1.9143e+03 4.8892e+00 --5.4000e+02 1.9429e+03 4.9876e+00 --5.4000e+02 1.9714e+03 5.0824e+00 --5.4000e+02 2.0000e+03 5.1739e+00 --5.1000e+02 -2.0000e+03 5.1489e+00 --5.1000e+02 -1.9714e+03 5.0562e+00 --5.1000e+02 -1.9429e+03 4.9600e+00 --5.1000e+02 -1.9143e+03 4.8601e+00 --5.1000e+02 -1.8857e+03 4.7563e+00 --5.1000e+02 -1.8571e+03 4.6483e+00 --5.1000e+02 -1.8286e+03 4.5359e+00 --5.1000e+02 -1.8000e+03 4.4189e+00 --5.1000e+02 -1.7714e+03 4.2969e+00 --5.1000e+02 -1.7429e+03 4.1696e+00 --5.1000e+02 -1.7143e+03 4.0368e+00 --5.1000e+02 -1.6857e+03 3.8980e+00 --5.1000e+02 -1.6571e+03 3.7528e+00 --5.1000e+02 -1.6286e+03 3.6008e+00 --5.1000e+02 -1.6000e+03 3.4416e+00 --5.1000e+02 -1.5714e+03 3.2746e+00 --5.1000e+02 -1.5429e+03 3.0992e+00 --5.1000e+02 -1.5143e+03 2.9149e+00 --5.1000e+02 -1.4857e+03 2.7209e+00 --5.1000e+02 -1.4571e+03 2.5166e+00 --5.1000e+02 -1.4286e+03 2.3010e+00 --5.1000e+02 -1.4000e+03 2.0733e+00 --5.1000e+02 -1.3714e+03 1.8324e+00 --5.1000e+02 -1.3429e+03 1.5774e+00 --5.1000e+02 -1.3143e+03 1.3068e+00 --5.1000e+02 -1.2857e+03 1.0194e+00 --5.1000e+02 -1.2571e+03 7.1354e-01 --5.1000e+02 -1.2286e+03 3.8755e-01 --5.1000e+02 -1.2000e+03 3.9469e-02 --5.1000e+02 -1.1714e+03 -3.3289e-01 --5.1000e+02 -1.1429e+03 -7.3199e-01 --5.1000e+02 -1.1143e+03 -1.1606e+00 --5.1000e+02 -1.0857e+03 -1.6220e+00 --5.1000e+02 -1.0571e+03 -2.1197e+00 --5.1000e+02 -1.0286e+03 -2.6578e+00 --5.1000e+02 -1.0000e+03 -3.2412e+00 --5.1000e+02 -9.7143e+02 -3.8751e+00 --5.1000e+02 -9.4286e+02 -4.5659e+00 --5.1000e+02 -9.1429e+02 -5.3208e+00 --5.1000e+02 -8.8571e+02 -6.1480e+00 --5.1000e+02 -8.5714e+02 -7.0573e+00 --5.1000e+02 -8.2857e+02 -8.0600e+00 --5.1000e+02 -8.0000e+02 -9.1688e+00 --5.1000e+02 -7.7143e+02 -1.0399e+01 --5.1000e+02 -7.4286e+02 -1.1768e+01 --5.1000e+02 -7.1429e+02 -1.3296e+01 --5.1000e+02 -6.8571e+02 -1.5005e+01 --5.1000e+02 -6.5714e+02 -1.6921e+01 --5.1000e+02 -6.2857e+02 -1.9071e+01 --5.1000e+02 -6.0000e+02 -2.1485e+01 --5.1000e+02 -5.7143e+02 -2.4191e+01 --5.1000e+02 -5.4286e+02 -2.7214e+01 --5.1000e+02 -5.1429e+02 -3.0569e+01 --5.1000e+02 -4.8571e+02 -3.4259e+01 --5.1000e+02 -4.5714e+02 -3.8261e+01 --5.1000e+02 -4.2857e+02 -4.2526e+01 --5.1000e+02 -4.0000e+02 -4.6973e+01 --5.1000e+02 -3.7143e+02 -5.1493e+01 --5.1000e+02 -3.4286e+02 -5.5963e+01 --5.1000e+02 -3.1429e+02 -6.0261e+01 --5.1000e+02 -2.8571e+02 -6.4282e+01 --5.1000e+02 -2.5714e+02 -6.7948e+01 --5.1000e+02 -2.2857e+02 -7.1211e+01 --5.1000e+02 -2.0000e+02 -7.4050e+01 --5.1000e+02 -1.7143e+02 -7.6465e+01 --5.1000e+02 -1.4286e+02 -7.8465e+01 --5.1000e+02 -1.1429e+02 -8.0068e+01 --5.1000e+02 -8.5714e+01 -8.1292e+01 --5.1000e+02 -5.7143e+01 -8.2153e+01 --5.1000e+02 -2.8571e+01 -8.2664e+01 --5.1000e+02 0.0000e+00 -8.2834e+01 --5.1000e+02 2.8571e+01 -8.2664e+01 --5.1000e+02 5.7143e+01 -8.2153e+01 --5.1000e+02 8.5714e+01 -8.1292e+01 --5.1000e+02 1.1429e+02 -8.0068e+01 --5.1000e+02 1.4286e+02 -7.8465e+01 --5.1000e+02 1.7143e+02 -7.6465e+01 --5.1000e+02 2.0000e+02 -7.4050e+01 --5.1000e+02 2.2857e+02 -7.1211e+01 --5.1000e+02 2.5714e+02 -6.7948e+01 --5.1000e+02 2.8571e+02 -6.4282e+01 --5.1000e+02 3.1429e+02 -6.0261e+01 --5.1000e+02 3.4286e+02 -5.5963e+01 --5.1000e+02 3.7143e+02 -5.1493e+01 --5.1000e+02 4.0000e+02 -4.6973e+01 --5.1000e+02 4.2857e+02 -4.2526e+01 --5.1000e+02 4.5714e+02 -3.8261e+01 --5.1000e+02 4.8571e+02 -3.4259e+01 --5.1000e+02 5.1429e+02 -3.0569e+01 --5.1000e+02 5.4286e+02 -2.7214e+01 --5.1000e+02 5.7143e+02 -2.4191e+01 --5.1000e+02 6.0000e+02 -2.1485e+01 --5.1000e+02 6.2857e+02 -1.9071e+01 --5.1000e+02 6.5714e+02 -1.6921e+01 --5.1000e+02 6.8571e+02 -1.5005e+01 --5.1000e+02 7.1429e+02 -1.3296e+01 --5.1000e+02 7.4286e+02 -1.1768e+01 --5.1000e+02 7.7143e+02 -1.0399e+01 --5.1000e+02 8.0000e+02 -9.1688e+00 --5.1000e+02 8.2857e+02 -8.0600e+00 --5.1000e+02 8.5714e+02 -7.0573e+00 --5.1000e+02 8.8571e+02 -6.1480e+00 --5.1000e+02 9.1429e+02 -5.3208e+00 --5.1000e+02 9.4286e+02 -4.5659e+00 --5.1000e+02 9.7143e+02 -3.8751e+00 --5.1000e+02 1.0000e+03 -3.2412e+00 --5.1000e+02 1.0286e+03 -2.6578e+00 --5.1000e+02 1.0571e+03 -2.1197e+00 --5.1000e+02 1.0857e+03 -1.6220e+00 --5.1000e+02 1.1143e+03 -1.1606e+00 --5.1000e+02 1.1429e+03 -7.3199e-01 --5.1000e+02 1.1714e+03 -3.3289e-01 --5.1000e+02 1.2000e+03 3.9469e-02 --5.1000e+02 1.2286e+03 3.8755e-01 --5.1000e+02 1.2571e+03 7.1354e-01 --5.1000e+02 1.2857e+03 1.0194e+00 --5.1000e+02 1.3143e+03 1.3068e+00 --5.1000e+02 1.3429e+03 1.5774e+00 --5.1000e+02 1.3714e+03 1.8324e+00 --5.1000e+02 1.4000e+03 2.0733e+00 --5.1000e+02 1.4286e+03 2.3010e+00 --5.1000e+02 1.4571e+03 2.5166e+00 --5.1000e+02 1.4857e+03 2.7209e+00 --5.1000e+02 1.5143e+03 2.9149e+00 --5.1000e+02 1.5429e+03 3.0992e+00 --5.1000e+02 1.5714e+03 3.2746e+00 --5.1000e+02 1.6000e+03 3.4416e+00 --5.1000e+02 1.6286e+03 3.6008e+00 --5.1000e+02 1.6571e+03 3.7528e+00 --5.1000e+02 1.6857e+03 3.8980e+00 --5.1000e+02 1.7143e+03 4.0368e+00 --5.1000e+02 1.7429e+03 4.1696e+00 --5.1000e+02 1.7714e+03 4.2969e+00 --5.1000e+02 1.8000e+03 4.4189e+00 --5.1000e+02 1.8286e+03 4.5359e+00 --5.1000e+02 1.8571e+03 4.6483e+00 --5.1000e+02 1.8857e+03 4.7563e+00 --5.1000e+02 1.9143e+03 4.8601e+00 --5.1000e+02 1.9429e+03 4.9600e+00 --5.1000e+02 1.9714e+03 5.0562e+00 --5.1000e+02 2.0000e+03 5.1489e+00 --4.8000e+02 -2.0000e+03 5.1251e+00 --4.8000e+02 -1.9714e+03 5.0312e+00 --4.8000e+02 -1.9429e+03 4.9336e+00 --4.8000e+02 -1.9143e+03 4.8322e+00 --4.8000e+02 -1.8857e+03 4.7269e+00 --4.8000e+02 -1.8571e+03 4.6172e+00 --4.8000e+02 -1.8286e+03 4.5030e+00 --4.8000e+02 -1.8000e+03 4.3841e+00 --4.8000e+02 -1.7714e+03 4.2600e+00 --4.8000e+02 -1.7429e+03 4.1305e+00 --4.8000e+02 -1.7143e+03 3.9952e+00 --4.8000e+02 -1.6857e+03 3.8538e+00 --4.8000e+02 -1.6571e+03 3.7058e+00 --4.8000e+02 -1.6286e+03 3.5507e+00 --4.8000e+02 -1.6000e+03 3.3881e+00 --4.8000e+02 -1.5714e+03 3.2174e+00 --4.8000e+02 -1.5429e+03 3.0380e+00 --4.8000e+02 -1.5143e+03 2.8492e+00 --4.8000e+02 -1.4857e+03 2.6504e+00 --4.8000e+02 -1.4571e+03 2.4407e+00 --4.8000e+02 -1.4286e+03 2.2193e+00 --4.8000e+02 -1.4000e+03 1.9851e+00 --4.8000e+02 -1.3714e+03 1.7371e+00 --4.8000e+02 -1.3429e+03 1.4741e+00 --4.8000e+02 -1.3143e+03 1.1948e+00 --4.8000e+02 -1.2857e+03 8.9750e-01 --4.8000e+02 -1.2571e+03 5.8067e-01 --4.8000e+02 -1.2286e+03 2.4238e-01 --4.8000e+02 -1.2000e+03 -1.1953e-01 --4.8000e+02 -1.1714e+03 -5.0747e-01 --4.8000e+02 -1.1429e+03 -9.2420e-01 --4.8000e+02 -1.1143e+03 -1.3729e+00 --4.8000e+02 -1.0857e+03 -1.8570e+00 --4.8000e+02 -1.0571e+03 -2.3807e+00 --4.8000e+02 -1.0286e+03 -2.9487e+00 --4.8000e+02 -1.0000e+03 -3.5665e+00 --4.8000e+02 -9.7143e+02 -4.2402e+00 --4.8000e+02 -9.4286e+02 -4.9771e+00 --4.8000e+02 -9.1429e+02 -5.7858e+00 --4.8000e+02 -8.8571e+02 -6.6762e+00 --4.8000e+02 -8.5714e+02 -7.6597e+00 --4.8000e+02 -8.2857e+02 -8.7500e+00 --4.8000e+02 -8.0000e+02 -9.9629e+00 --4.8000e+02 -7.7143e+02 -1.1317e+01 --4.8000e+02 -7.4286e+02 -1.2834e+01 --4.8000e+02 -7.1429e+02 -1.4539e+01 --4.8000e+02 -6.8571e+02 -1.6461e+01 --4.8000e+02 -6.5714e+02 -1.8632e+01 --4.8000e+02 -6.2857e+02 -2.1087e+01 --4.8000e+02 -6.0000e+02 -2.3864e+01 --4.8000e+02 -5.7143e+02 -2.6995e+01 --4.8000e+02 -5.4286e+02 -3.0509e+01 --4.8000e+02 -5.1429e+02 -3.4416e+01 --4.8000e+02 -4.8571e+02 -3.8702e+01 --4.8000e+02 -4.5714e+02 -4.3318e+01 --4.8000e+02 -4.2857e+02 -4.8174e+01 --4.8000e+02 -4.0000e+02 -5.3142e+01 --4.8000e+02 -3.7143e+02 -5.8072e+01 --4.8000e+02 -3.4286e+02 -6.2817e+01 --4.8000e+02 -3.1429e+02 -6.7250e+01 --4.8000e+02 -2.8571e+02 -7.1284e+01 --4.8000e+02 -2.5714e+02 -7.4870e+01 --4.8000e+02 -2.2857e+02 -7.7993e+01 --4.8000e+02 -2.0000e+02 -8.0662e+01 --4.8000e+02 -1.7143e+02 -8.2899e+01 --4.8000e+02 -1.4286e+02 -8.4731e+01 --4.8000e+02 -1.1429e+02 -8.6187e+01 --4.8000e+02 -8.5714e+01 -8.7292e+01 --4.8000e+02 -5.7143e+01 -8.8066e+01 --4.8000e+02 -2.8571e+01 -8.8525e+01 --4.8000e+02 0.0000e+00 -8.8677e+01 --4.8000e+02 2.8571e+01 -8.8525e+01 --4.8000e+02 5.7143e+01 -8.8066e+01 --4.8000e+02 8.5714e+01 -8.7292e+01 --4.8000e+02 1.1429e+02 -8.6187e+01 --4.8000e+02 1.4286e+02 -8.4731e+01 --4.8000e+02 1.7143e+02 -8.2899e+01 --4.8000e+02 2.0000e+02 -8.0662e+01 --4.8000e+02 2.2857e+02 -7.7993e+01 --4.8000e+02 2.5714e+02 -7.4870e+01 --4.8000e+02 2.8571e+02 -7.1284e+01 --4.8000e+02 3.1429e+02 -6.7250e+01 --4.8000e+02 3.4286e+02 -6.2817e+01 --4.8000e+02 3.7143e+02 -5.8072e+01 --4.8000e+02 4.0000e+02 -5.3142e+01 --4.8000e+02 4.2857e+02 -4.8174e+01 --4.8000e+02 4.5714e+02 -4.3318e+01 --4.8000e+02 4.8571e+02 -3.8702e+01 --4.8000e+02 5.1429e+02 -3.4416e+01 --4.8000e+02 5.4286e+02 -3.0509e+01 --4.8000e+02 5.7143e+02 -2.6995e+01 --4.8000e+02 6.0000e+02 -2.3864e+01 --4.8000e+02 6.2857e+02 -2.1087e+01 --4.8000e+02 6.5714e+02 -1.8632e+01 --4.8000e+02 6.8571e+02 -1.6461e+01 --4.8000e+02 7.1429e+02 -1.4539e+01 --4.8000e+02 7.4286e+02 -1.2834e+01 --4.8000e+02 7.7143e+02 -1.1317e+01 --4.8000e+02 8.0000e+02 -9.9629e+00 --4.8000e+02 8.2857e+02 -8.7500e+00 --4.8000e+02 8.5714e+02 -7.6597e+00 --4.8000e+02 8.8571e+02 -6.6762e+00 --4.8000e+02 9.1429e+02 -5.7858e+00 --4.8000e+02 9.4286e+02 -4.9771e+00 --4.8000e+02 9.7143e+02 -4.2402e+00 --4.8000e+02 1.0000e+03 -3.5665e+00 --4.8000e+02 1.0286e+03 -2.9487e+00 --4.8000e+02 1.0571e+03 -2.3807e+00 --4.8000e+02 1.0857e+03 -1.8570e+00 --4.8000e+02 1.1143e+03 -1.3729e+00 --4.8000e+02 1.1429e+03 -9.2420e-01 --4.8000e+02 1.1714e+03 -5.0747e-01 --4.8000e+02 1.2000e+03 -1.1953e-01 --4.8000e+02 1.2286e+03 2.4238e-01 --4.8000e+02 1.2571e+03 5.8067e-01 --4.8000e+02 1.2857e+03 8.9750e-01 --4.8000e+02 1.3143e+03 1.1948e+00 --4.8000e+02 1.3429e+03 1.4741e+00 --4.8000e+02 1.3714e+03 1.7371e+00 --4.8000e+02 1.4000e+03 1.9851e+00 --4.8000e+02 1.4286e+03 2.2193e+00 --4.8000e+02 1.4571e+03 2.4407e+00 --4.8000e+02 1.4857e+03 2.6504e+00 --4.8000e+02 1.5143e+03 2.8492e+00 --4.8000e+02 1.5429e+03 3.0380e+00 --4.8000e+02 1.5714e+03 3.2174e+00 --4.8000e+02 1.6000e+03 3.3881e+00 --4.8000e+02 1.6286e+03 3.5507e+00 --4.8000e+02 1.6571e+03 3.7058e+00 --4.8000e+02 1.6857e+03 3.8538e+00 --4.8000e+02 1.7143e+03 3.9952e+00 --4.8000e+02 1.7429e+03 4.1305e+00 --4.8000e+02 1.7714e+03 4.2600e+00 --4.8000e+02 1.8000e+03 4.3841e+00 --4.8000e+02 1.8286e+03 4.5030e+00 --4.8000e+02 1.8571e+03 4.6172e+00 --4.8000e+02 1.8857e+03 4.7269e+00 --4.8000e+02 1.9143e+03 4.8322e+00 --4.8000e+02 1.9429e+03 4.9336e+00 --4.8000e+02 1.9714e+03 5.0312e+00 --4.8000e+02 2.0000e+03 5.1251e+00 --4.5000e+02 -2.0000e+03 5.1025e+00 --4.5000e+02 -1.9714e+03 5.0073e+00 --4.5000e+02 -1.9429e+03 4.9085e+00 --4.5000e+02 -1.9143e+03 4.8057e+00 --4.5000e+02 -1.8857e+03 4.6988e+00 --4.5000e+02 -1.8571e+03 4.5876e+00 --4.5000e+02 -1.8286e+03 4.4717e+00 --4.5000e+02 -1.8000e+03 4.3509e+00 --4.5000e+02 -1.7714e+03 4.2248e+00 --4.5000e+02 -1.7429e+03 4.0931e+00 --4.5000e+02 -1.7143e+03 3.9555e+00 --4.5000e+02 -1.6857e+03 3.8115e+00 --4.5000e+02 -1.6571e+03 3.6607e+00 --4.5000e+02 -1.6286e+03 3.5026e+00 --4.5000e+02 -1.6000e+03 3.3367e+00 --4.5000e+02 -1.5714e+03 3.1624e+00 --4.5000e+02 -1.5429e+03 2.9791e+00 --4.5000e+02 -1.5143e+03 2.7861e+00 --4.5000e+02 -1.4857e+03 2.5825e+00 --4.5000e+02 -1.4571e+03 2.3676e+00 --4.5000e+02 -1.4286e+03 2.1405e+00 --4.5000e+02 -1.4000e+03 1.9000e+00 --4.5000e+02 -1.3714e+03 1.6449e+00 --4.5000e+02 -1.3429e+03 1.3741e+00 --4.5000e+02 -1.3143e+03 1.0861e+00 --4.5000e+02 -1.2857e+03 7.7908e-01 --4.5000e+02 -1.2571e+03 4.5137e-01 --4.5000e+02 -1.2286e+03 1.0084e-01 --4.5000e+02 -1.2000e+03 -2.7485e-01 --4.5000e+02 -1.1714e+03 -6.7838e-01 --4.5000e+02 -1.1429e+03 -1.1128e+00 --4.5000e+02 -1.1143e+03 -1.5816e+00 --4.5000e+02 -1.0857e+03 -2.0888e+00 --4.5000e+02 -1.0571e+03 -2.6389e+00 --4.5000e+02 -1.0286e+03 -3.2373e+00 --4.5000e+02 -1.0000e+03 -3.8903e+00 --4.5000e+02 -9.7143e+02 -4.6049e+00 --4.5000e+02 -9.4286e+02 -5.3897e+00 --4.5000e+02 -9.1429e+02 -6.2545e+00 --4.5000e+02 -8.8571e+02 -7.2109e+00 --4.5000e+02 -8.5714e+02 -8.2728e+00 --4.5000e+02 -8.2857e+02 -9.4562e+00 --4.5000e+02 -8.0000e+02 -1.0781e+01 --4.5000e+02 -7.7143e+02 -1.2268e+01 --4.5000e+02 -7.4286e+02 -1.3947e+01 --4.5000e+02 -7.1429e+02 -1.5846e+01 --4.5000e+02 -6.8571e+02 -1.8003e+01 --4.5000e+02 -6.5714e+02 -2.0459e+01 --4.5000e+02 -6.2857e+02 -2.3255e+01 --4.5000e+02 -6.0000e+02 -2.6437e+01 --4.5000e+02 -5.7143e+02 -3.0043e+01 --4.5000e+02 -5.4286e+02 -3.4096e+01 --4.5000e+02 -5.1429e+02 -3.8595e+01 --4.5000e+02 -4.8571e+02 -4.3494e+01 --4.5000e+02 -4.5714e+02 -4.8699e+01 --4.5000e+02 -4.2857e+02 -5.4067e+01 --4.5000e+02 -4.0000e+02 -5.9421e+01 --4.5000e+02 -3.7143e+02 -6.4582e+01 --4.5000e+02 -3.4286e+02 -6.9403e+01 --4.5000e+02 -3.1429e+02 -7.3780e+01 --4.5000e+02 -2.8571e+02 -7.7663e+01 --4.5000e+02 -2.5714e+02 -8.1042e+01 --4.5000e+02 -2.2857e+02 -8.3935e+01 --4.5000e+02 -2.0000e+02 -8.6375e+01 --4.5000e+02 -1.7143e+02 -8.8400e+01 --4.5000e+02 -1.4286e+02 -9.0046e+01 --4.5000e+02 -1.1429e+02 -9.1348e+01 --4.5000e+02 -8.5714e+01 -9.2333e+01 --4.5000e+02 -5.7143e+01 -9.3021e+01 --4.5000e+02 -2.8571e+01 -9.3428e+01 --4.5000e+02 0.0000e+00 -9.3563e+01 --4.5000e+02 2.8571e+01 -9.3428e+01 --4.5000e+02 5.7143e+01 -9.3021e+01 --4.5000e+02 8.5714e+01 -9.2333e+01 --4.5000e+02 1.1429e+02 -9.1348e+01 --4.5000e+02 1.4286e+02 -9.0046e+01 --4.5000e+02 1.7143e+02 -8.8400e+01 --4.5000e+02 2.0000e+02 -8.6375e+01 --4.5000e+02 2.2857e+02 -8.3935e+01 --4.5000e+02 2.5714e+02 -8.1042e+01 --4.5000e+02 2.8571e+02 -7.7663e+01 --4.5000e+02 3.1429e+02 -7.3780e+01 --4.5000e+02 3.4286e+02 -6.9403e+01 --4.5000e+02 3.7143e+02 -6.4582e+01 --4.5000e+02 4.0000e+02 -5.9421e+01 --4.5000e+02 4.2857e+02 -5.4067e+01 --4.5000e+02 4.5714e+02 -4.8699e+01 --4.5000e+02 4.8571e+02 -4.3494e+01 --4.5000e+02 5.1429e+02 -3.8595e+01 --4.5000e+02 5.4286e+02 -3.4096e+01 --4.5000e+02 5.7143e+02 -3.0043e+01 --4.5000e+02 6.0000e+02 -2.6437e+01 --4.5000e+02 6.2857e+02 -2.3255e+01 --4.5000e+02 6.5714e+02 -2.0459e+01 --4.5000e+02 6.8571e+02 -1.8003e+01 --4.5000e+02 7.1429e+02 -1.5846e+01 --4.5000e+02 7.4286e+02 -1.3947e+01 --4.5000e+02 7.7143e+02 -1.2268e+01 --4.5000e+02 8.0000e+02 -1.0781e+01 --4.5000e+02 8.2857e+02 -9.4562e+00 --4.5000e+02 8.5714e+02 -8.2728e+00 --4.5000e+02 8.8571e+02 -7.2109e+00 --4.5000e+02 9.1429e+02 -6.2545e+00 --4.5000e+02 9.4286e+02 -5.3897e+00 --4.5000e+02 9.7143e+02 -4.6049e+00 --4.5000e+02 1.0000e+03 -3.8903e+00 --4.5000e+02 1.0286e+03 -3.2373e+00 --4.5000e+02 1.0571e+03 -2.6389e+00 --4.5000e+02 1.0857e+03 -2.0888e+00 --4.5000e+02 1.1143e+03 -1.5816e+00 --4.5000e+02 1.1429e+03 -1.1128e+00 --4.5000e+02 1.1714e+03 -6.7838e-01 --4.5000e+02 1.2000e+03 -2.7485e-01 --4.5000e+02 1.2286e+03 1.0084e-01 --4.5000e+02 1.2571e+03 4.5137e-01 --4.5000e+02 1.2857e+03 7.7908e-01 --4.5000e+02 1.3143e+03 1.0861e+00 --4.5000e+02 1.3429e+03 1.3741e+00 --4.5000e+02 1.3714e+03 1.6449e+00 --4.5000e+02 1.4000e+03 1.9000e+00 --4.5000e+02 1.4286e+03 2.1405e+00 --4.5000e+02 1.4571e+03 2.3676e+00 --4.5000e+02 1.4857e+03 2.5825e+00 --4.5000e+02 1.5143e+03 2.7861e+00 --4.5000e+02 1.5429e+03 2.9791e+00 --4.5000e+02 1.5714e+03 3.1624e+00 --4.5000e+02 1.6000e+03 3.3367e+00 --4.5000e+02 1.6286e+03 3.5026e+00 --4.5000e+02 1.6571e+03 3.6607e+00 --4.5000e+02 1.6857e+03 3.8115e+00 --4.5000e+02 1.7143e+03 3.9555e+00 --4.5000e+02 1.7429e+03 4.0931e+00 --4.5000e+02 1.7714e+03 4.2248e+00 --4.5000e+02 1.8000e+03 4.3509e+00 --4.5000e+02 1.8286e+03 4.4717e+00 --4.5000e+02 1.8571e+03 4.5876e+00 --4.5000e+02 1.8857e+03 4.6988e+00 --4.5000e+02 1.9143e+03 4.8057e+00 --4.5000e+02 1.9429e+03 4.9085e+00 --4.5000e+02 1.9714e+03 5.0073e+00 --4.5000e+02 2.0000e+03 5.1025e+00 --4.2000e+02 -2.0000e+03 5.0810e+00 --4.2000e+02 -1.9714e+03 4.9847e+00 --4.2000e+02 -1.9429e+03 4.8846e+00 --4.2000e+02 -1.9143e+03 4.7805e+00 --4.2000e+02 -1.8857e+03 4.6723e+00 --4.2000e+02 -1.8571e+03 4.5595e+00 --4.2000e+02 -1.8286e+03 4.4419e+00 --4.2000e+02 -1.8000e+03 4.3193e+00 --4.2000e+02 -1.7714e+03 4.1913e+00 --4.2000e+02 -1.7429e+03 4.0576e+00 --4.2000e+02 -1.7143e+03 3.9177e+00 --4.2000e+02 -1.6857e+03 3.7712e+00 --4.2000e+02 -1.6571e+03 3.6178e+00 --4.2000e+02 -1.6286e+03 3.4568e+00 --4.2000e+02 -1.6000e+03 3.2877e+00 --4.2000e+02 -1.5714e+03 3.1100e+00 --4.2000e+02 -1.5429e+03 2.9228e+00 --4.2000e+02 -1.5143e+03 2.7256e+00 --4.2000e+02 -1.4857e+03 2.5175e+00 --4.2000e+02 -1.4571e+03 2.2976e+00 --4.2000e+02 -1.4286e+03 2.0648e+00 --4.2000e+02 -1.4000e+03 1.8181e+00 --4.2000e+02 -1.3714e+03 1.5563e+00 --4.2000e+02 -1.3429e+03 1.2778e+00 --4.2000e+02 -1.3143e+03 9.8120e-01 --4.2000e+02 -1.2857e+03 6.6468e-01 --4.2000e+02 -1.2571e+03 3.2625e-01 --4.2000e+02 -1.2286e+03 -3.6358e-02 --4.2000e+02 -1.2000e+03 -4.2570e-01 --4.2000e+02 -1.1714e+03 -8.4472e-01 --4.2000e+02 -1.1429e+03 -1.2968e+00 --4.2000e+02 -1.1143e+03 -1.7857e+00 --4.2000e+02 -1.0857e+03 -2.3160e+00 --4.2000e+02 -1.0571e+03 -2.8927e+00 --4.2000e+02 -1.0286e+03 -3.5220e+00 --4.2000e+02 -1.0000e+03 -4.2107e+00 --4.2000e+02 -9.7143e+02 -4.9672e+00 --4.2000e+02 -9.4286e+02 -5.8011e+00 --4.2000e+02 -9.1429e+02 -6.7238e+00 --4.2000e+02 -8.8571e+02 -7.7490e+00 --4.2000e+02 -8.5714e+02 -8.8928e+00 --4.2000e+02 -8.2857e+02 -1.0175e+01 --4.2000e+02 -8.0000e+02 -1.1617e+01 --4.2000e+02 -7.7143e+02 -1.3248e+01 --4.2000e+02 -7.4286e+02 -1.5101e+01 --4.2000e+02 -7.1429e+02 -1.7212e+01 --4.2000e+02 -6.8571e+02 -1.9627e+01 --4.2000e+02 -6.5714e+02 -2.2395e+01 --4.2000e+02 -6.2857e+02 -2.5567e+01 --4.2000e+02 -6.0000e+02 -2.9195e+01 --4.2000e+02 -5.7143e+02 -3.3316e+01 --4.2000e+02 -5.4286e+02 -3.7943e+01 --4.2000e+02 -5.1429e+02 -4.3044e+01 --4.2000e+02 -4.8571e+02 -4.8529e+01 --4.2000e+02 -4.5714e+02 -5.4242e+01 --4.2000e+02 -4.2857e+02 -5.9983e+01 --4.2000e+02 -4.0000e+02 -6.5542e+01 --4.2000e+02 -3.7143e+02 -7.0739e+01 --4.2000e+02 -3.4286e+02 -7.5455e+01 --4.2000e+02 -3.1429e+02 -7.9631e+01 --4.2000e+02 -2.8571e+02 -8.3261e+01 --4.2000e+02 -2.5714e+02 -8.6370e+01 --4.2000e+02 -2.2857e+02 -8.9001e+01 --4.2000e+02 -2.0000e+02 -9.1202e+01 --4.2000e+02 -1.7143e+02 -9.3017e+01 --4.2000e+02 -1.4286e+02 -9.4489e+01 --4.2000e+02 -1.1429e+02 -9.5649e+01 --4.2000e+02 -8.5714e+01 -9.6525e+01 --4.2000e+02 -5.7143e+01 -9.7137e+01 --4.2000e+02 -2.8571e+01 -9.7499e+01 --4.2000e+02 0.0000e+00 -9.7618e+01 --4.2000e+02 2.8571e+01 -9.7499e+01 --4.2000e+02 5.7143e+01 -9.7137e+01 --4.2000e+02 8.5714e+01 -9.6525e+01 --4.2000e+02 1.1429e+02 -9.5649e+01 --4.2000e+02 1.4286e+02 -9.4489e+01 --4.2000e+02 1.7143e+02 -9.3017e+01 --4.2000e+02 2.0000e+02 -9.1202e+01 --4.2000e+02 2.2857e+02 -8.9001e+01 --4.2000e+02 2.5714e+02 -8.6370e+01 --4.2000e+02 2.8571e+02 -8.3261e+01 --4.2000e+02 3.1429e+02 -7.9631e+01 --4.2000e+02 3.4286e+02 -7.5455e+01 --4.2000e+02 3.7143e+02 -7.0739e+01 --4.2000e+02 4.0000e+02 -6.5542e+01 --4.2000e+02 4.2857e+02 -5.9983e+01 --4.2000e+02 4.5714e+02 -5.4242e+01 --4.2000e+02 4.8571e+02 -4.8529e+01 --4.2000e+02 5.1429e+02 -4.3044e+01 --4.2000e+02 5.4286e+02 -3.7943e+01 --4.2000e+02 5.7143e+02 -3.3316e+01 --4.2000e+02 6.0000e+02 -2.9195e+01 --4.2000e+02 6.2857e+02 -2.5567e+01 --4.2000e+02 6.5714e+02 -2.2395e+01 --4.2000e+02 6.8571e+02 -1.9627e+01 --4.2000e+02 7.1429e+02 -1.7212e+01 --4.2000e+02 7.4286e+02 -1.5101e+01 --4.2000e+02 7.7143e+02 -1.3248e+01 --4.2000e+02 8.0000e+02 -1.1617e+01 --4.2000e+02 8.2857e+02 -1.0175e+01 --4.2000e+02 8.5714e+02 -8.8928e+00 --4.2000e+02 8.8571e+02 -7.7490e+00 --4.2000e+02 9.1429e+02 -6.7238e+00 --4.2000e+02 9.4286e+02 -5.8011e+00 --4.2000e+02 9.7143e+02 -4.9672e+00 --4.2000e+02 1.0000e+03 -4.2107e+00 --4.2000e+02 1.0286e+03 -3.5220e+00 --4.2000e+02 1.0571e+03 -2.8927e+00 --4.2000e+02 1.0857e+03 -2.3160e+00 --4.2000e+02 1.1143e+03 -1.7857e+00 --4.2000e+02 1.1429e+03 -1.2968e+00 --4.2000e+02 1.1714e+03 -8.4472e-01 --4.2000e+02 1.2000e+03 -4.2570e-01 --4.2000e+02 1.2286e+03 -3.6358e-02 --4.2000e+02 1.2571e+03 3.2625e-01 --4.2000e+02 1.2857e+03 6.6468e-01 --4.2000e+02 1.3143e+03 9.8120e-01 --4.2000e+02 1.3429e+03 1.2778e+00 --4.2000e+02 1.3714e+03 1.5563e+00 --4.2000e+02 1.4000e+03 1.8181e+00 --4.2000e+02 1.4286e+03 2.0648e+00 --4.2000e+02 1.4571e+03 2.2976e+00 --4.2000e+02 1.4857e+03 2.5175e+00 --4.2000e+02 1.5143e+03 2.7256e+00 --4.2000e+02 1.5429e+03 2.9228e+00 --4.2000e+02 1.5714e+03 3.1100e+00 --4.2000e+02 1.6000e+03 3.2877e+00 --4.2000e+02 1.6286e+03 3.4568e+00 --4.2000e+02 1.6571e+03 3.6178e+00 --4.2000e+02 1.6857e+03 3.7712e+00 --4.2000e+02 1.7143e+03 3.9177e+00 --4.2000e+02 1.7429e+03 4.0576e+00 --4.2000e+02 1.7714e+03 4.1913e+00 --4.2000e+02 1.8000e+03 4.3193e+00 --4.2000e+02 1.8286e+03 4.4419e+00 --4.2000e+02 1.8571e+03 4.5595e+00 --4.2000e+02 1.8857e+03 4.6723e+00 --4.2000e+02 1.9143e+03 4.7805e+00 --4.2000e+02 1.9429e+03 4.8846e+00 --4.2000e+02 1.9714e+03 4.9847e+00 --4.2000e+02 2.0000e+03 5.0810e+00 --3.9000e+02 -2.0000e+03 5.0608e+00 --3.9000e+02 -1.9714e+03 4.9634e+00 --3.9000e+02 -1.9429e+03 4.8622e+00 --3.9000e+02 -1.9143e+03 4.7568e+00 --3.9000e+02 -1.8857e+03 4.6472e+00 --3.9000e+02 -1.8571e+03 4.5329e+00 --3.9000e+02 -1.8286e+03 4.4138e+00 --3.9000e+02 -1.8000e+03 4.2895e+00 --3.9000e+02 -1.7714e+03 4.1597e+00 --3.9000e+02 -1.7429e+03 4.0240e+00 --3.9000e+02 -1.7143e+03 3.8819e+00 --3.9000e+02 -1.6857e+03 3.7331e+00 --3.9000e+02 -1.6571e+03 3.5771e+00 --3.9000e+02 -1.6286e+03 3.4133e+00 --3.9000e+02 -1.6000e+03 3.2412e+00 --3.9000e+02 -1.5714e+03 3.0601e+00 --3.9000e+02 -1.5429e+03 2.8694e+00 --3.9000e+02 -1.5143e+03 2.6682e+00 --3.9000e+02 -1.4857e+03 2.4556e+00 --3.9000e+02 -1.4571e+03 2.2308e+00 --3.9000e+02 -1.4286e+03 1.9927e+00 --3.9000e+02 -1.4000e+03 1.7400e+00 --3.9000e+02 -1.3714e+03 1.4715e+00 --3.9000e+02 -1.3429e+03 1.1856e+00 --3.9000e+02 -1.3143e+03 8.8070e-01 --3.9000e+02 -1.2857e+03 5.5486e-01 --3.9000e+02 -1.2571e+03 2.0595e-01 --3.9000e+02 -1.2286e+03 -1.6850e-01 --3.9000e+02 -1.2000e+03 -5.7126e-01 --3.9000e+02 -1.1714e+03 -1.0055e+00 --3.9000e+02 -1.1429e+03 -1.4750e+00 --3.9000e+02 -1.1143e+03 -1.9839e+00 --3.9000e+02 -1.0857e+03 -2.5372e+00 --3.9000e+02 -1.0571e+03 -3.1405e+00 --3.9000e+02 -1.0286e+03 -3.8007e+00 --3.9000e+02 -1.0000e+03 -4.5256e+00 --3.9000e+02 -9.7143e+02 -5.3245e+00 --3.9000e+02 -9.4286e+02 -6.2084e+00 --3.9000e+02 -9.1429e+02 -7.1905e+00 --3.9000e+02 -8.8571e+02 -8.2866e+00 --3.9000e+02 -8.5714e+02 -9.5155e+00 --3.9000e+02 -8.2857e+02 -1.0900e+01 --3.9000e+02 -8.0000e+02 -1.2467e+01 --3.9000e+02 -7.7143e+02 -1.4250e+01 --3.9000e+02 -7.4286e+02 -1.6289e+01 --3.9000e+02 -7.1429e+02 -1.8628e+01 --3.9000e+02 -6.8571e+02 -2.1322e+01 --3.9000e+02 -6.5714e+02 -2.4428e+01 --3.9000e+02 -6.2857e+02 -2.8009e+01 --3.9000e+02 -6.0000e+02 -3.2116e+01 --3.9000e+02 -5.7143e+02 -3.6781e+01 --3.9000e+02 -5.4286e+02 -4.1991e+01 --3.9000e+02 -5.1429e+02 -4.7669e+01 --3.9000e+02 -4.8571e+02 -5.3662e+01 --3.9000e+02 -4.5714e+02 -5.9750e+01 --3.9000e+02 -4.2857e+02 -6.5690e+01 --3.9000e+02 -4.0000e+02 -7.1268e+01 --3.9000e+02 -3.7143e+02 -7.6336e+01 --3.9000e+02 -3.4286e+02 -8.0822e+01 --3.9000e+02 -3.1429e+02 -8.4717e+01 --3.9000e+02 -2.8571e+02 -8.8054e+01 --3.9000e+02 -2.5714e+02 -9.0882e+01 --3.9000e+02 -2.2857e+02 -9.3258e+01 --3.9000e+02 -2.0000e+02 -9.5237e+01 --3.9000e+02 -1.7143e+02 -9.6865e+01 --3.9000e+02 -1.4286e+02 -9.8183e+01 --3.9000e+02 -1.1429e+02 -9.9222e+01 --3.9000e+02 -8.5714e+01 -1.0001e+02 --3.9000e+02 -5.7143e+01 -1.0055e+02 --3.9000e+02 -2.8571e+01 -1.0088e+02 --3.9000e+02 0.0000e+00 -1.0099e+02 --3.9000e+02 2.8571e+01 -1.0088e+02 --3.9000e+02 5.7143e+01 -1.0055e+02 --3.9000e+02 8.5714e+01 -1.0001e+02 --3.9000e+02 1.1429e+02 -9.9222e+01 --3.9000e+02 1.4286e+02 -9.8183e+01 --3.9000e+02 1.7143e+02 -9.6865e+01 --3.9000e+02 2.0000e+02 -9.5237e+01 --3.9000e+02 2.2857e+02 -9.3258e+01 --3.9000e+02 2.5714e+02 -9.0882e+01 --3.9000e+02 2.8571e+02 -8.8054e+01 --3.9000e+02 3.1429e+02 -8.4717e+01 --3.9000e+02 3.4286e+02 -8.0822e+01 --3.9000e+02 3.7143e+02 -7.6336e+01 --3.9000e+02 4.0000e+02 -7.1268e+01 --3.9000e+02 4.2857e+02 -6.5690e+01 --3.9000e+02 4.5714e+02 -5.9750e+01 --3.9000e+02 4.8571e+02 -5.3662e+01 --3.9000e+02 5.1429e+02 -4.7669e+01 --3.9000e+02 5.4286e+02 -4.1991e+01 --3.9000e+02 5.7143e+02 -3.6781e+01 --3.9000e+02 6.0000e+02 -3.2116e+01 --3.9000e+02 6.2857e+02 -2.8009e+01 --3.9000e+02 6.5714e+02 -2.4428e+01 --3.9000e+02 6.8571e+02 -2.1322e+01 --3.9000e+02 7.1429e+02 -1.8628e+01 --3.9000e+02 7.4286e+02 -1.6289e+01 --3.9000e+02 7.7143e+02 -1.4250e+01 --3.9000e+02 8.0000e+02 -1.2467e+01 --3.9000e+02 8.2857e+02 -1.0900e+01 --3.9000e+02 8.5714e+02 -9.5155e+00 --3.9000e+02 8.8571e+02 -8.2866e+00 --3.9000e+02 9.1429e+02 -7.1905e+00 --3.9000e+02 9.4286e+02 -6.2084e+00 --3.9000e+02 9.7143e+02 -5.3245e+00 --3.9000e+02 1.0000e+03 -4.5256e+00 --3.9000e+02 1.0286e+03 -3.8007e+00 --3.9000e+02 1.0571e+03 -3.1405e+00 --3.9000e+02 1.0857e+03 -2.5372e+00 --3.9000e+02 1.1143e+03 -1.9839e+00 --3.9000e+02 1.1429e+03 -1.4750e+00 --3.9000e+02 1.1714e+03 -1.0055e+00 --3.9000e+02 1.2000e+03 -5.7126e-01 --3.9000e+02 1.2286e+03 -1.6850e-01 --3.9000e+02 1.2571e+03 2.0595e-01 --3.9000e+02 1.2857e+03 5.5486e-01 --3.9000e+02 1.3143e+03 8.8070e-01 --3.9000e+02 1.3429e+03 1.1856e+00 --3.9000e+02 1.3714e+03 1.4715e+00 --3.9000e+02 1.4000e+03 1.7400e+00 --3.9000e+02 1.4286e+03 1.9927e+00 --3.9000e+02 1.4571e+03 2.2308e+00 --3.9000e+02 1.4857e+03 2.4556e+00 --3.9000e+02 1.5143e+03 2.6682e+00 --3.9000e+02 1.5429e+03 2.8694e+00 --3.9000e+02 1.5714e+03 3.0601e+00 --3.9000e+02 1.6000e+03 3.2412e+00 --3.9000e+02 1.6286e+03 3.4133e+00 --3.9000e+02 1.6571e+03 3.5771e+00 --3.9000e+02 1.6857e+03 3.7331e+00 --3.9000e+02 1.7143e+03 3.8819e+00 --3.9000e+02 1.7429e+03 4.0240e+00 --3.9000e+02 1.7714e+03 4.1597e+00 --3.9000e+02 1.8000e+03 4.2895e+00 --3.9000e+02 1.8286e+03 4.4138e+00 --3.9000e+02 1.8571e+03 4.5329e+00 --3.9000e+02 1.8857e+03 4.6472e+00 --3.9000e+02 1.9143e+03 4.7568e+00 --3.9000e+02 1.9429e+03 4.8622e+00 --3.9000e+02 1.9714e+03 4.9634e+00 --3.9000e+02 2.0000e+03 5.0608e+00 --3.6000e+02 -2.0000e+03 5.0419e+00 --3.6000e+02 -1.9714e+03 4.9435e+00 --3.6000e+02 -1.9429e+03 4.8411e+00 --3.6000e+02 -1.9143e+03 4.7346e+00 --3.6000e+02 -1.8857e+03 4.6237e+00 --3.6000e+02 -1.8571e+03 4.5081e+00 --3.6000e+02 -1.8286e+03 4.3875e+00 --3.6000e+02 -1.8000e+03 4.2616e+00 --3.6000e+02 -1.7714e+03 4.1300e+00 --3.6000e+02 -1.7429e+03 3.9924e+00 --3.6000e+02 -1.7143e+03 3.8483e+00 --3.6000e+02 -1.6857e+03 3.6973e+00 --3.6000e+02 -1.6571e+03 3.5388e+00 --3.6000e+02 -1.6286e+03 3.3724e+00 --3.6000e+02 -1.6000e+03 3.1974e+00 --3.6000e+02 -1.5714e+03 3.0132e+00 --3.6000e+02 -1.5429e+03 2.8189e+00 --3.6000e+02 -1.5143e+03 2.6139e+00 --3.6000e+02 -1.4857e+03 2.3971e+00 --3.6000e+02 -1.4571e+03 2.1677e+00 --3.6000e+02 -1.4286e+03 1.9244e+00 --3.6000e+02 -1.4000e+03 1.6659e+00 --3.6000e+02 -1.3714e+03 1.3910e+00 --3.6000e+02 -1.3429e+03 1.0980e+00 --3.6000e+02 -1.3143e+03 7.8505e-01 --3.6000e+02 -1.2857e+03 4.5020e-01 --3.6000e+02 -1.2571e+03 9.1122e-02 --3.6000e+02 -1.2286e+03 -2.9483e-01 --3.6000e+02 -1.2000e+03 -7.1066e-01 --3.6000e+02 -1.1714e+03 -1.1599e+00 --3.6000e+02 -1.1429e+03 -1.6464e+00 --3.6000e+02 -1.1143e+03 -2.1749e+00 --3.6000e+02 -1.0857e+03 -2.7509e+00 --3.6000e+02 -1.0571e+03 -3.3806e+00 --3.6000e+02 -1.0286e+03 -4.0716e+00 --3.6000e+02 -1.0000e+03 -4.8325e+00 --3.6000e+02 -9.7143e+02 -5.6739e+00 --3.6000e+02 -9.4286e+02 -6.6084e+00 --3.6000e+02 -9.1429e+02 -7.6508e+00 --3.6000e+02 -8.8571e+02 -8.8192e+00 --3.6000e+02 -8.5714e+02 -1.0136e+01 --3.6000e+02 -8.2857e+02 -1.1626e+01 --3.6000e+02 -8.0000e+02 -1.3324e+01 --3.6000e+02 -7.7143e+02 -1.5266e+01 --3.6000e+02 -7.4286e+02 -1.7501e+01 --3.6000e+02 -7.1429e+02 -2.0082e+01 --3.6000e+02 -6.8571e+02 -2.3073e+01 --3.6000e+02 -6.5714e+02 -2.6542e+01 --3.6000e+02 -6.2857e+02 -3.0556e+01 --3.6000e+02 -6.0000e+02 -3.5164e+01 --3.6000e+02 -5.7143e+02 -4.0381e+01 --3.6000e+02 -5.4286e+02 -4.6154e+01 --3.6000e+02 -5.1429e+02 -5.2343e+01 --3.6000e+02 -4.8571e+02 -5.8724e+01 --3.6000e+02 -4.5714e+02 -6.5027e+01 --3.6000e+02 -4.2857e+02 -7.0993e+01 --3.6000e+02 -4.0000e+02 -7.6439e+01 --3.6000e+02 -3.7143e+02 -8.1268e+01 --3.6000e+02 -3.4286e+02 -8.5461e+01 --3.6000e+02 -3.1429e+02 -8.9051e+01 --3.6000e+02 -2.8571e+02 -9.2096e+01 --3.6000e+02 -2.5714e+02 -9.4663e+01 --3.6000e+02 -2.2857e+02 -9.6812e+01 --3.6000e+02 -2.0000e+02 -9.8598e+01 --3.6000e+02 -1.7143e+02 -1.0007e+02 --3.6000e+02 -1.4286e+02 -1.0126e+02 --3.6000e+02 -1.1429e+02 -1.0220e+02 --3.6000e+02 -8.5714e+01 -1.0291e+02 --3.6000e+02 -5.7143e+01 -1.0341e+02 --3.6000e+02 -2.8571e+01 -1.0370e+02 --3.6000e+02 0.0000e+00 -1.0380e+02 --3.6000e+02 2.8571e+01 -1.0370e+02 --3.6000e+02 5.7143e+01 -1.0341e+02 --3.6000e+02 8.5714e+01 -1.0291e+02 --3.6000e+02 1.1429e+02 -1.0220e+02 --3.6000e+02 1.4286e+02 -1.0126e+02 --3.6000e+02 1.7143e+02 -1.0007e+02 --3.6000e+02 2.0000e+02 -9.8598e+01 --3.6000e+02 2.2857e+02 -9.6812e+01 --3.6000e+02 2.5714e+02 -9.4663e+01 --3.6000e+02 2.8571e+02 -9.2096e+01 --3.6000e+02 3.1429e+02 -8.9051e+01 --3.6000e+02 3.4286e+02 -8.5461e+01 --3.6000e+02 3.7143e+02 -8.1268e+01 --3.6000e+02 4.0000e+02 -7.6439e+01 --3.6000e+02 4.2857e+02 -7.0993e+01 --3.6000e+02 4.5714e+02 -6.5027e+01 --3.6000e+02 4.8571e+02 -5.8724e+01 --3.6000e+02 5.1429e+02 -5.2343e+01 --3.6000e+02 5.4286e+02 -4.6154e+01 --3.6000e+02 5.7143e+02 -4.0381e+01 --3.6000e+02 6.0000e+02 -3.5164e+01 --3.6000e+02 6.2857e+02 -3.0556e+01 --3.6000e+02 6.5714e+02 -2.6542e+01 --3.6000e+02 6.8571e+02 -2.3073e+01 --3.6000e+02 7.1429e+02 -2.0082e+01 --3.6000e+02 7.4286e+02 -1.7501e+01 --3.6000e+02 7.7143e+02 -1.5266e+01 --3.6000e+02 8.0000e+02 -1.3324e+01 --3.6000e+02 8.2857e+02 -1.1626e+01 --3.6000e+02 8.5714e+02 -1.0136e+01 --3.6000e+02 8.8571e+02 -8.8192e+00 --3.6000e+02 9.1429e+02 -7.6508e+00 --3.6000e+02 9.4286e+02 -6.6084e+00 --3.6000e+02 9.7143e+02 -5.6739e+00 --3.6000e+02 1.0000e+03 -4.8325e+00 --3.6000e+02 1.0286e+03 -4.0716e+00 --3.6000e+02 1.0571e+03 -3.3806e+00 --3.6000e+02 1.0857e+03 -2.7509e+00 --3.6000e+02 1.1143e+03 -2.1749e+00 --3.6000e+02 1.1429e+03 -1.6464e+00 --3.6000e+02 1.1714e+03 -1.1599e+00 --3.6000e+02 1.2000e+03 -7.1066e-01 --3.6000e+02 1.2286e+03 -2.9483e-01 --3.6000e+02 1.2571e+03 9.1122e-02 --3.6000e+02 1.2857e+03 4.5020e-01 --3.6000e+02 1.3143e+03 7.8505e-01 --3.6000e+02 1.3429e+03 1.0980e+00 --3.6000e+02 1.3714e+03 1.3910e+00 --3.6000e+02 1.4000e+03 1.6659e+00 --3.6000e+02 1.4286e+03 1.9244e+00 --3.6000e+02 1.4571e+03 2.1677e+00 --3.6000e+02 1.4857e+03 2.3971e+00 --3.6000e+02 1.5143e+03 2.6139e+00 --3.6000e+02 1.5429e+03 2.8189e+00 --3.6000e+02 1.5714e+03 3.0132e+00 --3.6000e+02 1.6000e+03 3.1974e+00 --3.6000e+02 1.6286e+03 3.3724e+00 --3.6000e+02 1.6571e+03 3.5388e+00 --3.6000e+02 1.6857e+03 3.6973e+00 --3.6000e+02 1.7143e+03 3.8483e+00 --3.6000e+02 1.7429e+03 3.9924e+00 --3.6000e+02 1.7714e+03 4.1300e+00 --3.6000e+02 1.8000e+03 4.2616e+00 --3.6000e+02 1.8286e+03 4.3875e+00 --3.6000e+02 1.8571e+03 4.5081e+00 --3.6000e+02 1.8857e+03 4.6237e+00 --3.6000e+02 1.9143e+03 4.7346e+00 --3.6000e+02 1.9429e+03 4.8411e+00 --3.6000e+02 1.9714e+03 4.9435e+00 --3.6000e+02 2.0000e+03 5.0419e+00 --3.3000e+02 -2.0000e+03 5.0243e+00 --3.3000e+02 -1.9714e+03 4.9249e+00 --3.3000e+02 -1.9429e+03 4.8215e+00 --3.3000e+02 -1.9143e+03 4.7139e+00 --3.3000e+02 -1.8857e+03 4.6018e+00 --3.3000e+02 -1.8571e+03 4.4849e+00 --3.3000e+02 -1.8286e+03 4.3629e+00 --3.3000e+02 -1.8000e+03 4.2355e+00 --3.3000e+02 -1.7714e+03 4.1023e+00 --3.3000e+02 -1.7429e+03 3.9629e+00 --3.3000e+02 -1.7143e+03 3.8169e+00 --3.3000e+02 -1.6857e+03 3.6638e+00 --3.3000e+02 -1.6571e+03 3.5031e+00 --3.3000e+02 -1.6286e+03 3.3342e+00 --3.3000e+02 -1.6000e+03 3.1565e+00 --3.3000e+02 -1.5714e+03 2.9692e+00 --3.3000e+02 -1.5429e+03 2.7717e+00 --3.3000e+02 -1.5143e+03 2.5630e+00 --3.3000e+02 -1.4857e+03 2.3423e+00 --3.3000e+02 -1.4571e+03 2.1084e+00 --3.3000e+02 -1.4286e+03 1.8601e+00 --3.3000e+02 -1.4000e+03 1.5963e+00 --3.3000e+02 -1.3714e+03 1.3152e+00 --3.3000e+02 -1.3429e+03 1.0154e+00 --3.3000e+02 -1.3143e+03 6.9477e-01 --3.3000e+02 -1.2857e+03 3.5128e-01 --3.3000e+02 -1.2571e+03 -1.7572e-02 --3.3000e+02 -1.2286e+03 -4.1460e-01 --3.3000e+02 -1.2000e+03 -8.4306e-01 --3.3000e+02 -1.1714e+03 -1.3067e+00 --3.3000e+02 -1.1429e+03 -1.8098e+00 --3.3000e+02 -1.1143e+03 -2.3575e+00 --3.3000e+02 -1.0857e+03 -2.9556e+00 --3.3000e+02 -1.0571e+03 -3.6112e+00 --3.3000e+02 -1.0286e+03 -4.3324e+00 --3.3000e+02 -1.0000e+03 -5.1290e+00 --3.3000e+02 -9.7143e+02 -6.0127e+00 --3.3000e+02 -9.4286e+02 -6.9975e+00 --3.3000e+02 -9.1429e+02 -8.1004e+00 --3.3000e+02 -8.8571e+02 -9.3419e+00 --3.3000e+02 -8.5714e+02 -1.0747e+01 --3.3000e+02 -8.2857e+02 -1.2346e+01 --3.3000e+02 -8.0000e+02 -1.4177e+01 --3.3000e+02 -7.7143e+02 -1.6285e+01 --3.3000e+02 -7.4286e+02 -1.8724e+01 --3.3000e+02 -7.1429e+02 -2.1559e+01 --3.3000e+02 -6.8571e+02 -2.4863e+01 --3.3000e+02 -6.5714e+02 -2.8711e+01 --3.3000e+02 -6.2857e+02 -3.3173e+01 --3.3000e+02 -6.0000e+02 -3.8290e+01 --3.3000e+02 -5.7143e+02 -4.4043e+01 --3.3000e+02 -5.4286e+02 -5.0326e+01 --3.3000e+02 -5.1429e+02 -5.6925e+01 --3.3000e+02 -4.8571e+02 -6.3554e+01 --3.3000e+02 -4.5714e+02 -6.9913e+01 --3.3000e+02 -4.2857e+02 -7.5768e+01 --3.3000e+02 -4.0000e+02 -8.0982e+01 --3.3000e+02 -3.7143e+02 -8.5519e+01 --3.3000e+02 -3.4286e+02 -8.9404e+01 --3.3000e+02 -3.1429e+02 -9.2701e+01 --3.3000e+02 -2.8571e+02 -9.5482e+01 --3.3000e+02 -2.5714e+02 -9.7819e+01 --3.3000e+02 -2.2857e+02 -9.9775e+01 --3.3000e+02 -2.0000e+02 -1.0140e+02 --3.3000e+02 -1.7143e+02 -1.0274e+02 --3.3000e+02 -1.4286e+02 -1.0383e+02 --3.3000e+02 -1.1429e+02 -1.0469e+02 --3.3000e+02 -8.5714e+01 -1.0534e+02 --3.3000e+02 -5.7143e+01 -1.0580e+02 --3.3000e+02 -2.8571e+01 -1.0607e+02 --3.3000e+02 0.0000e+00 -1.0616e+02 --3.3000e+02 2.8571e+01 -1.0607e+02 --3.3000e+02 5.7143e+01 -1.0580e+02 --3.3000e+02 8.5714e+01 -1.0534e+02 --3.3000e+02 1.1429e+02 -1.0469e+02 --3.3000e+02 1.4286e+02 -1.0383e+02 --3.3000e+02 1.7143e+02 -1.0274e+02 --3.3000e+02 2.0000e+02 -1.0140e+02 --3.3000e+02 2.2857e+02 -9.9775e+01 --3.3000e+02 2.5714e+02 -9.7819e+01 --3.3000e+02 2.8571e+02 -9.5482e+01 --3.3000e+02 3.1429e+02 -9.2701e+01 --3.3000e+02 3.4286e+02 -8.9404e+01 --3.3000e+02 3.7143e+02 -8.5519e+01 --3.3000e+02 4.0000e+02 -8.0982e+01 --3.3000e+02 4.2857e+02 -7.5768e+01 --3.3000e+02 4.5714e+02 -6.9913e+01 --3.3000e+02 4.8571e+02 -6.3554e+01 --3.3000e+02 5.1429e+02 -5.6925e+01 --3.3000e+02 5.4286e+02 -5.0326e+01 --3.3000e+02 5.7143e+02 -4.4043e+01 --3.3000e+02 6.0000e+02 -3.8290e+01 --3.3000e+02 6.2857e+02 -3.3173e+01 --3.3000e+02 6.5714e+02 -2.8711e+01 --3.3000e+02 6.8571e+02 -2.4863e+01 --3.3000e+02 7.1429e+02 -2.1559e+01 --3.3000e+02 7.4286e+02 -1.8724e+01 --3.3000e+02 7.7143e+02 -1.6285e+01 --3.3000e+02 8.0000e+02 -1.4177e+01 --3.3000e+02 8.2857e+02 -1.2346e+01 --3.3000e+02 8.5714e+02 -1.0747e+01 --3.3000e+02 8.8571e+02 -9.3419e+00 --3.3000e+02 9.1429e+02 -8.1004e+00 --3.3000e+02 9.4286e+02 -6.9975e+00 --3.3000e+02 9.7143e+02 -6.0127e+00 --3.3000e+02 1.0000e+03 -5.1290e+00 --3.3000e+02 1.0286e+03 -4.3324e+00 --3.3000e+02 1.0571e+03 -3.6112e+00 --3.3000e+02 1.0857e+03 -2.9556e+00 --3.3000e+02 1.1143e+03 -2.3575e+00 --3.3000e+02 1.1429e+03 -1.8098e+00 --3.3000e+02 1.1714e+03 -1.3067e+00 --3.3000e+02 1.2000e+03 -8.4306e-01 --3.3000e+02 1.2286e+03 -4.1460e-01 --3.3000e+02 1.2571e+03 -1.7572e-02 --3.3000e+02 1.2857e+03 3.5128e-01 --3.3000e+02 1.3143e+03 6.9477e-01 --3.3000e+02 1.3429e+03 1.0154e+00 --3.3000e+02 1.3714e+03 1.3152e+00 --3.3000e+02 1.4000e+03 1.5963e+00 --3.3000e+02 1.4286e+03 1.8601e+00 --3.3000e+02 1.4571e+03 2.1084e+00 --3.3000e+02 1.4857e+03 2.3423e+00 --3.3000e+02 1.5143e+03 2.5630e+00 --3.3000e+02 1.5429e+03 2.7717e+00 --3.3000e+02 1.5714e+03 2.9692e+00 --3.3000e+02 1.6000e+03 3.1565e+00 --3.3000e+02 1.6286e+03 3.3342e+00 --3.3000e+02 1.6571e+03 3.5031e+00 --3.3000e+02 1.6857e+03 3.6638e+00 --3.3000e+02 1.7143e+03 3.8169e+00 --3.3000e+02 1.7429e+03 3.9629e+00 --3.3000e+02 1.7714e+03 4.1023e+00 --3.3000e+02 1.8000e+03 4.2355e+00 --3.3000e+02 1.8286e+03 4.3629e+00 --3.3000e+02 1.8571e+03 4.4849e+00 --3.3000e+02 1.8857e+03 4.6018e+00 --3.3000e+02 1.9143e+03 4.7139e+00 --3.3000e+02 1.9429e+03 4.8215e+00 --3.3000e+02 1.9714e+03 4.9249e+00 --3.3000e+02 2.0000e+03 5.0243e+00 --3.0000e+02 -2.0000e+03 5.0081e+00 --3.0000e+02 -1.9714e+03 4.9079e+00 --3.0000e+02 -1.9429e+03 4.8035e+00 --3.0000e+02 -1.9143e+03 4.6949e+00 --3.0000e+02 -1.8857e+03 4.5816e+00 --3.0000e+02 -1.8571e+03 4.4635e+00 --3.0000e+02 -1.8286e+03 4.3403e+00 --3.0000e+02 -1.8000e+03 4.2114e+00 --3.0000e+02 -1.7714e+03 4.0767e+00 --3.0000e+02 -1.7429e+03 3.9357e+00 --3.0000e+02 -1.7143e+03 3.7879e+00 --3.0000e+02 -1.6857e+03 3.6328e+00 --3.0000e+02 -1.6571e+03 3.4700e+00 --3.0000e+02 -1.6286e+03 3.2988e+00 --3.0000e+02 -1.6000e+03 3.1185e+00 --3.0000e+02 -1.5714e+03 2.9285e+00 --3.0000e+02 -1.5429e+03 2.7278e+00 --3.0000e+02 -1.5143e+03 2.5158e+00 --3.0000e+02 -1.4857e+03 2.2913e+00 --3.0000e+02 -1.4571e+03 2.0532e+00 --3.0000e+02 -1.4286e+03 1.8004e+00 --3.0000e+02 -1.4000e+03 1.5313e+00 --3.0000e+02 -1.3714e+03 1.2445e+00 --3.0000e+02 -1.3429e+03 9.3820e-01 --3.0000e+02 -1.3143e+03 6.1034e-01 --3.0000e+02 -1.2857e+03 2.5865e-01 --3.0000e+02 -1.2571e+03 -1.1948e-01 --3.0000e+02 -1.2286e+03 -5.2707e-01 --3.0000e+02 -1.2000e+03 -9.6758e-01 --3.0000e+02 -1.1714e+03 -1.4450e+00 --3.0000e+02 -1.1429e+03 -1.9640e+00 --3.0000e+02 -1.1143e+03 -2.5301e+00 --3.0000e+02 -1.0857e+03 -3.1497e+00 --3.0000e+02 -1.0571e+03 -3.8303e+00 --3.0000e+02 -1.0286e+03 -4.5809e+00 --3.0000e+02 -1.0000e+03 -5.4124e+00 --3.0000e+02 -9.7143e+02 -6.3375e+00 --3.0000e+02 -9.4286e+02 -7.3720e+00 --3.0000e+02 -9.1429e+02 -8.5348e+00 --3.0000e+02 -8.8571e+02 -9.8491e+00 --3.0000e+02 -8.5714e+02 -1.1343e+01 --3.0000e+02 -8.2857e+02 -1.3052e+01 --3.0000e+02 -8.0000e+02 -1.5019e+01 --3.0000e+02 -7.7143e+02 -1.7295e+01 --3.0000e+02 -7.4286e+02 -1.9944e+01 --3.0000e+02 -7.1429e+02 -2.3040e+01 --3.0000e+02 -6.8571e+02 -2.6665e+01 --3.0000e+02 -6.5714e+02 -3.0902e+01 --3.0000e+02 -6.2857e+02 -3.5816e+01 --3.0000e+02 -6.0000e+02 -4.1430e+01 --3.0000e+02 -5.7143e+02 -4.7681e+01 --3.0000e+02 -5.4286e+02 -5.4393e+01 --3.0000e+02 -5.1429e+02 -6.1284e+01 --3.0000e+02 -4.8571e+02 -6.8019e+01 --3.0000e+02 -4.5714e+02 -7.4307e+01 --3.0000e+02 -4.2857e+02 -7.9957e+01 --3.0000e+02 -4.0000e+02 -8.4893e+01 --3.0000e+02 -3.7143e+02 -8.9128e+01 --3.0000e+02 -3.4286e+02 -9.2722e+01 --3.0000e+02 -3.1429e+02 -9.5755e+01 --3.0000e+02 -2.8571e+02 -9.8309e+01 --3.0000e+02 -2.5714e+02 -1.0045e+02 --3.0000e+02 -2.2857e+02 -1.0225e+02 --3.0000e+02 -2.0000e+02 -1.0375e+02 --3.0000e+02 -1.7143e+02 -1.0499e+02 --3.0000e+02 -1.4286e+02 -1.0599e+02 --3.0000e+02 -1.1429e+02 -1.0679e+02 --3.0000e+02 -8.5714e+01 -1.0740e+02 --3.0000e+02 -5.7143e+01 -1.0783e+02 --3.0000e+02 -2.8571e+01 -1.0808e+02 --3.0000e+02 0.0000e+00 -1.0817e+02 --3.0000e+02 2.8571e+01 -1.0808e+02 --3.0000e+02 5.7143e+01 -1.0783e+02 --3.0000e+02 8.5714e+01 -1.0740e+02 --3.0000e+02 1.1429e+02 -1.0679e+02 --3.0000e+02 1.4286e+02 -1.0599e+02 --3.0000e+02 1.7143e+02 -1.0499e+02 --3.0000e+02 2.0000e+02 -1.0375e+02 --3.0000e+02 2.2857e+02 -1.0225e+02 --3.0000e+02 2.5714e+02 -1.0045e+02 --3.0000e+02 2.8571e+02 -9.8309e+01 --3.0000e+02 3.1429e+02 -9.5755e+01 --3.0000e+02 3.4286e+02 -9.2722e+01 --3.0000e+02 3.7143e+02 -8.9128e+01 --3.0000e+02 4.0000e+02 -8.4893e+01 --3.0000e+02 4.2857e+02 -7.9957e+01 --3.0000e+02 4.5714e+02 -7.4307e+01 --3.0000e+02 4.8571e+02 -6.8019e+01 --3.0000e+02 5.1429e+02 -6.1284e+01 --3.0000e+02 5.4286e+02 -5.4393e+01 --3.0000e+02 5.7143e+02 -4.7681e+01 --3.0000e+02 6.0000e+02 -4.1430e+01 --3.0000e+02 6.2857e+02 -3.5816e+01 --3.0000e+02 6.5714e+02 -3.0902e+01 --3.0000e+02 6.8571e+02 -2.6665e+01 --3.0000e+02 7.1429e+02 -2.3040e+01 --3.0000e+02 7.4286e+02 -1.9944e+01 --3.0000e+02 7.7143e+02 -1.7295e+01 --3.0000e+02 8.0000e+02 -1.5019e+01 --3.0000e+02 8.2857e+02 -1.3052e+01 --3.0000e+02 8.5714e+02 -1.1343e+01 --3.0000e+02 8.8571e+02 -9.8491e+00 --3.0000e+02 9.1429e+02 -8.5348e+00 --3.0000e+02 9.4286e+02 -7.3720e+00 --3.0000e+02 9.7143e+02 -6.3375e+00 --3.0000e+02 1.0000e+03 -5.4124e+00 --3.0000e+02 1.0286e+03 -4.5809e+00 --3.0000e+02 1.0571e+03 -3.8303e+00 --3.0000e+02 1.0857e+03 -3.1497e+00 --3.0000e+02 1.1143e+03 -2.5301e+00 --3.0000e+02 1.1429e+03 -1.9640e+00 --3.0000e+02 1.1714e+03 -1.4450e+00 --3.0000e+02 1.2000e+03 -9.6758e-01 --3.0000e+02 1.2286e+03 -5.2707e-01 --3.0000e+02 1.2571e+03 -1.1948e-01 --3.0000e+02 1.2857e+03 2.5865e-01 --3.0000e+02 1.3143e+03 6.1034e-01 --3.0000e+02 1.3429e+03 9.3820e-01 --3.0000e+02 1.3714e+03 1.2445e+00 --3.0000e+02 1.4000e+03 1.5313e+00 --3.0000e+02 1.4286e+03 1.8004e+00 --3.0000e+02 1.4571e+03 2.0532e+00 --3.0000e+02 1.4857e+03 2.2913e+00 --3.0000e+02 1.5143e+03 2.5158e+00 --3.0000e+02 1.5429e+03 2.7278e+00 --3.0000e+02 1.5714e+03 2.9285e+00 --3.0000e+02 1.6000e+03 3.1185e+00 --3.0000e+02 1.6286e+03 3.2988e+00 --3.0000e+02 1.6571e+03 3.4700e+00 --3.0000e+02 1.6857e+03 3.6328e+00 --3.0000e+02 1.7143e+03 3.7879e+00 --3.0000e+02 1.7429e+03 3.9357e+00 --3.0000e+02 1.7714e+03 4.0767e+00 --3.0000e+02 1.8000e+03 4.2114e+00 --3.0000e+02 1.8286e+03 4.3403e+00 --3.0000e+02 1.8571e+03 4.4635e+00 --3.0000e+02 1.8857e+03 4.5816e+00 --3.0000e+02 1.9143e+03 4.6949e+00 --3.0000e+02 1.9429e+03 4.8035e+00 --3.0000e+02 1.9714e+03 4.9079e+00 --3.0000e+02 2.0000e+03 5.0081e+00 --2.7000e+02 -2.0000e+03 4.9934e+00 --2.7000e+02 -1.9714e+03 4.8923e+00 --2.7000e+02 -1.9429e+03 4.7870e+00 --2.7000e+02 -1.9143e+03 4.6774e+00 --2.7000e+02 -1.8857e+03 4.5632e+00 --2.7000e+02 -1.8571e+03 4.4440e+00 --2.7000e+02 -1.8286e+03 4.3195e+00 --2.7000e+02 -1.8000e+03 4.1894e+00 --2.7000e+02 -1.7714e+03 4.0533e+00 --2.7000e+02 -1.7429e+03 3.9108e+00 --2.7000e+02 -1.7143e+03 3.7613e+00 --2.7000e+02 -1.6857e+03 3.6045e+00 --2.7000e+02 -1.6571e+03 3.4397e+00 --2.7000e+02 -1.6286e+03 3.2663e+00 --2.7000e+02 -1.6000e+03 3.0837e+00 --2.7000e+02 -1.5714e+03 2.8910e+00 --2.7000e+02 -1.5429e+03 2.6876e+00 --2.7000e+02 -1.5143e+03 2.4724e+00 --2.7000e+02 -1.4857e+03 2.2444e+00 --2.7000e+02 -1.4571e+03 2.0024e+00 --2.7000e+02 -1.4286e+03 1.7453e+00 --2.7000e+02 -1.4000e+03 1.4714e+00 --2.7000e+02 -1.3714e+03 1.1793e+00 --2.7000e+02 -1.3429e+03 8.6691e-01 --2.7000e+02 -1.3143e+03 5.3225e-01 --2.7000e+02 -1.2857e+03 1.7288e-01 --2.7000e+02 -1.2571e+03 -2.1397e-01 --2.7000e+02 -1.2286e+03 -6.3149e-01 --2.7000e+02 -1.2000e+03 -1.0834e+00 --2.7000e+02 -1.1714e+03 -1.5739e+00 --2.7000e+02 -1.1429e+03 -2.1080e+00 --2.7000e+02 -1.1143e+03 -2.6915e+00 --2.7000e+02 -1.0857e+03 -3.3315e+00 --2.7000e+02 -1.0571e+03 -4.0360e+00 --2.7000e+02 -1.0286e+03 -4.8149e+00 --2.7000e+02 -1.0000e+03 -5.6799e+00 --2.7000e+02 -9.7143e+02 -6.6452e+00 --2.7000e+02 -9.4286e+02 -7.7279e+00 --2.7000e+02 -9.1429e+02 -8.9492e+00 --2.7000e+02 -8.8571e+02 -1.0335e+01 --2.7000e+02 -8.5714e+02 -1.1917e+01 --2.7000e+02 -8.2857e+02 -1.3735e+01 --2.7000e+02 -8.0000e+02 -1.5837e+01 --2.7000e+02 -7.7143e+02 -1.8282e+01 --2.7000e+02 -7.4286e+02 -2.1143e+01 --2.7000e+02 -7.1429e+02 -2.4503e+01 --2.7000e+02 -6.8571e+02 -2.8451e+01 --2.7000e+02 -6.5714e+02 -3.3075e+01 --2.7000e+02 -6.2857e+02 -3.8433e+01 --2.7000e+02 -6.0000e+02 -4.4514e+01 --2.7000e+02 -5.7143e+02 -5.1202e+01 --2.7000e+02 -5.4286e+02 -5.8248e+01 --2.7000e+02 -5.1429e+02 -6.5308e+01 --2.7000e+02 -4.8571e+02 -7.2033e+01 --2.7000e+02 -4.5714e+02 -7.8162e+01 --2.7000e+02 -4.2857e+02 -8.3561e+01 --2.7000e+02 -4.0000e+02 -8.8210e+01 --2.7000e+02 -3.7143e+02 -9.2160e+01 --2.7000e+02 -3.4286e+02 -9.5495e+01 --2.7000e+02 -3.1429e+02 -9.8302e+01 --2.7000e+02 -2.8571e+02 -1.0067e+02 --2.7000e+02 -2.5714e+02 -1.0265e+02 --2.7000e+02 -2.2857e+02 -1.0432e+02 --2.7000e+02 -2.0000e+02 -1.0572e+02 --2.7000e+02 -1.7143e+02 -1.0688e+02 --2.7000e+02 -1.4286e+02 -1.0782e+02 --2.7000e+02 -1.1429e+02 -1.0858e+02 --2.7000e+02 -8.5714e+01 -1.0915e+02 --2.7000e+02 -5.7143e+01 -1.0956e+02 --2.7000e+02 -2.8571e+01 -1.0980e+02 --2.7000e+02 0.0000e+00 -1.0988e+02 --2.7000e+02 2.8571e+01 -1.0980e+02 --2.7000e+02 5.7143e+01 -1.0956e+02 --2.7000e+02 8.5714e+01 -1.0915e+02 --2.7000e+02 1.1429e+02 -1.0858e+02 --2.7000e+02 1.4286e+02 -1.0782e+02 --2.7000e+02 1.7143e+02 -1.0688e+02 --2.7000e+02 2.0000e+02 -1.0572e+02 --2.7000e+02 2.2857e+02 -1.0432e+02 --2.7000e+02 2.5714e+02 -1.0265e+02 --2.7000e+02 2.8571e+02 -1.0067e+02 --2.7000e+02 3.1429e+02 -9.8302e+01 --2.7000e+02 3.4286e+02 -9.5495e+01 --2.7000e+02 3.7143e+02 -9.2160e+01 --2.7000e+02 4.0000e+02 -8.8210e+01 --2.7000e+02 4.2857e+02 -8.3561e+01 --2.7000e+02 4.5714e+02 -7.8162e+01 --2.7000e+02 4.8571e+02 -7.2033e+01 --2.7000e+02 5.1429e+02 -6.5308e+01 --2.7000e+02 5.4286e+02 -5.8248e+01 --2.7000e+02 5.7143e+02 -5.1202e+01 --2.7000e+02 6.0000e+02 -4.4514e+01 --2.7000e+02 6.2857e+02 -3.8433e+01 --2.7000e+02 6.5714e+02 -3.3075e+01 --2.7000e+02 6.8571e+02 -2.8451e+01 --2.7000e+02 7.1429e+02 -2.4503e+01 --2.7000e+02 7.4286e+02 -2.1143e+01 --2.7000e+02 7.7143e+02 -1.8282e+01 --2.7000e+02 8.0000e+02 -1.5837e+01 --2.7000e+02 8.2857e+02 -1.3735e+01 --2.7000e+02 8.5714e+02 -1.1917e+01 --2.7000e+02 8.8571e+02 -1.0335e+01 --2.7000e+02 9.1429e+02 -8.9492e+00 --2.7000e+02 9.4286e+02 -7.7279e+00 --2.7000e+02 9.7143e+02 -6.6452e+00 --2.7000e+02 1.0000e+03 -5.6799e+00 --2.7000e+02 1.0286e+03 -4.8149e+00 --2.7000e+02 1.0571e+03 -4.0360e+00 --2.7000e+02 1.0857e+03 -3.3315e+00 --2.7000e+02 1.1143e+03 -2.6915e+00 --2.7000e+02 1.1429e+03 -2.1080e+00 --2.7000e+02 1.1714e+03 -1.5739e+00 --2.7000e+02 1.2000e+03 -1.0834e+00 --2.7000e+02 1.2286e+03 -6.3149e-01 --2.7000e+02 1.2571e+03 -2.1397e-01 --2.7000e+02 1.2857e+03 1.7288e-01 --2.7000e+02 1.3143e+03 5.3225e-01 --2.7000e+02 1.3429e+03 8.6691e-01 --2.7000e+02 1.3714e+03 1.1793e+00 --2.7000e+02 1.4000e+03 1.4714e+00 --2.7000e+02 1.4286e+03 1.7453e+00 --2.7000e+02 1.4571e+03 2.0024e+00 --2.7000e+02 1.4857e+03 2.2444e+00 --2.7000e+02 1.5143e+03 2.4724e+00 --2.7000e+02 1.5429e+03 2.6876e+00 --2.7000e+02 1.5714e+03 2.8910e+00 --2.7000e+02 1.6000e+03 3.0837e+00 --2.7000e+02 1.6286e+03 3.2663e+00 --2.7000e+02 1.6571e+03 3.4397e+00 --2.7000e+02 1.6857e+03 3.6045e+00 --2.7000e+02 1.7143e+03 3.7613e+00 --2.7000e+02 1.7429e+03 3.9108e+00 --2.7000e+02 1.7714e+03 4.0533e+00 --2.7000e+02 1.8000e+03 4.1894e+00 --2.7000e+02 1.8286e+03 4.3195e+00 --2.7000e+02 1.8571e+03 4.4440e+00 --2.7000e+02 1.8857e+03 4.5632e+00 --2.7000e+02 1.9143e+03 4.6774e+00 --2.7000e+02 1.9429e+03 4.7870e+00 --2.7000e+02 1.9714e+03 4.8923e+00 --2.7000e+02 2.0000e+03 4.9934e+00 --2.4000e+02 -2.0000e+03 4.9801e+00 --2.4000e+02 -1.9714e+03 4.8782e+00 --2.4000e+02 -1.9429e+03 4.7722e+00 --2.4000e+02 -1.9143e+03 4.6617e+00 --2.4000e+02 -1.8857e+03 4.5466e+00 --2.4000e+02 -1.8571e+03 4.4264e+00 --2.4000e+02 -1.8286e+03 4.3008e+00 --2.4000e+02 -1.8000e+03 4.1696e+00 --2.4000e+02 -1.7714e+03 4.0322e+00 --2.4000e+02 -1.7429e+03 3.8882e+00 --2.4000e+02 -1.7143e+03 3.7373e+00 --2.4000e+02 -1.6857e+03 3.5788e+00 --2.4000e+02 -1.6571e+03 3.4122e+00 --2.4000e+02 -1.6286e+03 3.2369e+00 --2.4000e+02 -1.6000e+03 3.0521e+00 --2.4000e+02 -1.5714e+03 2.8571e+00 --2.4000e+02 -1.5429e+03 2.6510e+00 --2.4000e+02 -1.5143e+03 2.4329e+00 --2.4000e+02 -1.4857e+03 2.2018e+00 --2.4000e+02 -1.4571e+03 1.9563e+00 --2.4000e+02 -1.4286e+03 1.6952e+00 --2.4000e+02 -1.4000e+03 1.4169e+00 --2.4000e+02 -1.3714e+03 1.1198e+00 --2.4000e+02 -1.3429e+03 8.0189e-01 --2.4000e+02 -1.3143e+03 4.6097e-01 --2.4000e+02 -1.2857e+03 9.4502e-02 --2.4000e+02 -1.2571e+03 -3.0042e-01 --2.4000e+02 -1.2286e+03 -7.2715e-01 --2.4000e+02 -1.2000e+03 -1.1896e+00 --2.4000e+02 -1.1714e+03 -1.6922e+00 --2.4000e+02 -1.1429e+03 -2.2404e+00 --2.4000e+02 -1.1143e+03 -2.8403e+00 --2.4000e+02 -1.0857e+03 -3.4995e+00 --2.4000e+02 -1.0571e+03 -4.2265e+00 --2.4000e+02 -1.0286e+03 -5.0321e+00 --2.4000e+02 -1.0000e+03 -5.9288e+00 --2.4000e+02 -9.7143e+02 -6.9322e+00 --2.4000e+02 -9.4286e+02 -8.0611e+00 --2.4000e+02 -9.1429e+02 -9.3385e+00 --2.4000e+02 -8.8571e+02 -1.0793e+01 --2.4000e+02 -8.5714e+02 -1.2460e+01 --2.4000e+02 -8.2857e+02 -1.4384e+01 --2.4000e+02 -8.0000e+02 -1.6618e+01 --2.4000e+02 -7.7143e+02 -1.9230e+01 --2.4000e+02 -7.4286e+02 -2.2300e+01 --2.4000e+02 -7.1429e+02 -2.5920e+01 --2.4000e+02 -6.8571e+02 -3.0187e+01 --2.4000e+02 -6.5714e+02 -3.5188e+01 --2.4000e+02 -6.2857e+02 -4.0964e+01 --2.4000e+02 -6.0000e+02 -4.7468e+01 --2.4000e+02 -5.7143e+02 -5.4518e+01 --2.4000e+02 -5.4286e+02 -6.1797e+01 --2.4000e+02 -5.1429e+02 -6.8923e+01 --2.4000e+02 -4.8571e+02 -7.5553e+01 --2.4000e+02 -4.5714e+02 -8.1476e+01 --2.4000e+02 -4.2857e+02 -8.6613e+01 --2.4000e+02 -4.0000e+02 -9.0991e+01 --2.4000e+02 -3.7143e+02 -9.4689e+01 --2.4000e+02 -3.4286e+02 -9.7801e+01 --2.4000e+02 -3.1429e+02 -1.0042e+02 --2.4000e+02 -2.8571e+02 -1.0263e+02 --2.4000e+02 -2.5714e+02 -1.0449e+02 --2.4000e+02 -2.2857e+02 -1.0606e+02 --2.4000e+02 -2.0000e+02 -1.0738e+02 --2.4000e+02 -1.7143e+02 -1.0847e+02 --2.4000e+02 -1.4286e+02 -1.0938e+02 --2.4000e+02 -1.1429e+02 -1.1010e+02 --2.4000e+02 -8.5714e+01 -1.1066e+02 --2.4000e+02 -5.7143e+01 -1.1105e+02 --2.4000e+02 -2.8571e+01 -1.1128e+02 --2.4000e+02 0.0000e+00 -1.1136e+02 --2.4000e+02 2.8571e+01 -1.1128e+02 --2.4000e+02 5.7143e+01 -1.1105e+02 --2.4000e+02 8.5714e+01 -1.1066e+02 --2.4000e+02 1.1429e+02 -1.1010e+02 --2.4000e+02 1.4286e+02 -1.0938e+02 --2.4000e+02 1.7143e+02 -1.0847e+02 --2.4000e+02 2.0000e+02 -1.0738e+02 --2.4000e+02 2.2857e+02 -1.0606e+02 --2.4000e+02 2.5714e+02 -1.0449e+02 --2.4000e+02 2.8571e+02 -1.0263e+02 --2.4000e+02 3.1429e+02 -1.0042e+02 --2.4000e+02 3.4286e+02 -9.7801e+01 --2.4000e+02 3.7143e+02 -9.4689e+01 --2.4000e+02 4.0000e+02 -9.0991e+01 --2.4000e+02 4.2857e+02 -8.6613e+01 --2.4000e+02 4.5714e+02 -8.1476e+01 --2.4000e+02 4.8571e+02 -7.5553e+01 --2.4000e+02 5.1429e+02 -6.8923e+01 --2.4000e+02 5.4286e+02 -6.1797e+01 --2.4000e+02 5.7143e+02 -5.4518e+01 --2.4000e+02 6.0000e+02 -4.7468e+01 --2.4000e+02 6.2857e+02 -4.0964e+01 --2.4000e+02 6.5714e+02 -3.5188e+01 --2.4000e+02 6.8571e+02 -3.0187e+01 --2.4000e+02 7.1429e+02 -2.5920e+01 --2.4000e+02 7.4286e+02 -2.2300e+01 --2.4000e+02 7.7143e+02 -1.9230e+01 --2.4000e+02 8.0000e+02 -1.6618e+01 --2.4000e+02 8.2857e+02 -1.4384e+01 --2.4000e+02 8.5714e+02 -1.2460e+01 --2.4000e+02 8.8571e+02 -1.0793e+01 --2.4000e+02 9.1429e+02 -9.3385e+00 --2.4000e+02 9.4286e+02 -8.0611e+00 --2.4000e+02 9.7143e+02 -6.9322e+00 --2.4000e+02 1.0000e+03 -5.9288e+00 --2.4000e+02 1.0286e+03 -5.0321e+00 --2.4000e+02 1.0571e+03 -4.2265e+00 --2.4000e+02 1.0857e+03 -3.4995e+00 --2.4000e+02 1.1143e+03 -2.8403e+00 --2.4000e+02 1.1429e+03 -2.2404e+00 --2.4000e+02 1.1714e+03 -1.6922e+00 --2.4000e+02 1.2000e+03 -1.1896e+00 --2.4000e+02 1.2286e+03 -7.2715e-01 --2.4000e+02 1.2571e+03 -3.0042e-01 --2.4000e+02 1.2857e+03 9.4502e-02 --2.4000e+02 1.3143e+03 4.6097e-01 --2.4000e+02 1.3429e+03 8.0189e-01 --2.4000e+02 1.3714e+03 1.1198e+00 --2.4000e+02 1.4000e+03 1.4169e+00 --2.4000e+02 1.4286e+03 1.6952e+00 --2.4000e+02 1.4571e+03 1.9563e+00 --2.4000e+02 1.4857e+03 2.2018e+00 --2.4000e+02 1.5143e+03 2.4329e+00 --2.4000e+02 1.5429e+03 2.6510e+00 --2.4000e+02 1.5714e+03 2.8571e+00 --2.4000e+02 1.6000e+03 3.0521e+00 --2.4000e+02 1.6286e+03 3.2369e+00 --2.4000e+02 1.6571e+03 3.4122e+00 --2.4000e+02 1.6857e+03 3.5788e+00 --2.4000e+02 1.7143e+03 3.7373e+00 --2.4000e+02 1.7429e+03 3.8882e+00 --2.4000e+02 1.7714e+03 4.0322e+00 --2.4000e+02 1.8000e+03 4.1696e+00 --2.4000e+02 1.8286e+03 4.3008e+00 --2.4000e+02 1.8571e+03 4.4264e+00 --2.4000e+02 1.8857e+03 4.5466e+00 --2.4000e+02 1.9143e+03 4.6617e+00 --2.4000e+02 1.9429e+03 4.7722e+00 --2.4000e+02 1.9714e+03 4.8782e+00 --2.4000e+02 2.0000e+03 4.9801e+00 --2.1000e+02 -2.0000e+03 4.9682e+00 --2.1000e+02 -1.9714e+03 4.8657e+00 --2.1000e+02 -1.9429e+03 4.7590e+00 --2.1000e+02 -1.9143e+03 4.6478e+00 --2.1000e+02 -1.8857e+03 4.5318e+00 --2.1000e+02 -1.8571e+03 4.4107e+00 --2.1000e+02 -1.8286e+03 4.2842e+00 --2.1000e+02 -1.8000e+03 4.1519e+00 --2.1000e+02 -1.7714e+03 4.0133e+00 --2.1000e+02 -1.7429e+03 3.8682e+00 --2.1000e+02 -1.7143e+03 3.7159e+00 --2.1000e+02 -1.6857e+03 3.5559e+00 --2.1000e+02 -1.6571e+03 3.3877e+00 --2.1000e+02 -1.6286e+03 3.2106e+00 --2.1000e+02 -1.6000e+03 3.0239e+00 --2.1000e+02 -1.5714e+03 2.8268e+00 --2.1000e+02 -1.5429e+03 2.6184e+00 --2.1000e+02 -1.5143e+03 2.3977e+00 --2.1000e+02 -1.4857e+03 2.1636e+00 --2.1000e+02 -1.4571e+03 1.9150e+00 --2.1000e+02 -1.4286e+03 1.6503e+00 --2.1000e+02 -1.4000e+03 1.3681e+00 --2.1000e+02 -1.3714e+03 1.0665e+00 --2.1000e+02 -1.3429e+03 7.4354e-01 --2.1000e+02 -1.3143e+03 3.9693e-01 --2.1000e+02 -1.2857e+03 2.4017e-02 --2.1000e+02 -1.2571e+03 -3.7824e-01 --2.1000e+02 -1.2286e+03 -8.1336e-01 --2.1000e+02 -1.2000e+03 -1.2854e+00 --2.1000e+02 -1.1714e+03 -1.7992e+00 --2.1000e+02 -1.1429e+03 -2.3603e+00 --2.1000e+02 -1.1143e+03 -2.9752e+00 --2.1000e+02 -1.0857e+03 -3.6520e+00 --2.1000e+02 -1.0571e+03 -4.3998e+00 --2.1000e+02 -1.0286e+03 -5.2301e+00 --2.1000e+02 -1.0000e+03 -6.1564e+00 --2.1000e+02 -9.7143e+02 -7.1954e+00 --2.1000e+02 -9.4286e+02 -8.3674e+00 --2.1000e+02 -9.1429e+02 -9.6976e+00 --2.1000e+02 -8.8571e+02 -1.1217e+01 --2.1000e+02 -8.5714e+02 -1.2965e+01 --2.1000e+02 -8.2857e+02 -1.4990e+01 --2.1000e+02 -8.0000e+02 -1.7351e+01 --2.1000e+02 -7.7143e+02 -2.0123e+01 --2.1000e+02 -7.4286e+02 -2.3394e+01 --2.1000e+02 -7.1429e+02 -2.7264e+01 --2.1000e+02 -6.8571e+02 -3.1836e+01 --2.1000e+02 -6.5714e+02 -3.7191e+01 --2.1000e+02 -6.2857e+02 -4.3351e+01 --2.1000e+02 -6.0000e+02 -5.0220e+01 --2.1000e+02 -5.7143e+02 -5.7553e+01 --2.1000e+02 -5.4286e+02 -6.4976e+01 --2.1000e+02 -5.1429e+02 -7.2086e+01 --2.1000e+02 -4.8571e+02 -7.8573e+01 --2.1000e+02 -4.5714e+02 -8.4273e+01 --2.1000e+02 -4.2857e+02 -8.9162e+01 --2.1000e+02 -4.0000e+02 -9.3298e+01 --2.1000e+02 -3.7143e+02 -9.6779e+01 --2.1000e+02 -3.4286e+02 -9.9706e+01 --2.1000e+02 -3.1429e+02 -1.0217e+02 --2.1000e+02 -2.8571e+02 -1.0425e+02 --2.1000e+02 -2.5714e+02 -1.0602e+02 --2.1000e+02 -2.2857e+02 -1.0751e+02 --2.1000e+02 -2.0000e+02 -1.0877e+02 --2.1000e+02 -1.7143e+02 -1.0982e+02 --2.1000e+02 -1.4286e+02 -1.1070e+02 --2.1000e+02 -1.1429e+02 -1.1140e+02 --2.1000e+02 -8.5714e+01 -1.1195e+02 --2.1000e+02 -5.7143e+01 -1.1234e+02 --2.1000e+02 -2.8571e+01 -1.1257e+02 --2.1000e+02 0.0000e+00 -1.1265e+02 --2.1000e+02 2.8571e+01 -1.1257e+02 --2.1000e+02 5.7143e+01 -1.1234e+02 --2.1000e+02 8.5714e+01 -1.1195e+02 --2.1000e+02 1.1429e+02 -1.1140e+02 --2.1000e+02 1.4286e+02 -1.1070e+02 --2.1000e+02 1.7143e+02 -1.0982e+02 --2.1000e+02 2.0000e+02 -1.0877e+02 --2.1000e+02 2.2857e+02 -1.0751e+02 --2.1000e+02 2.5714e+02 -1.0602e+02 --2.1000e+02 2.8571e+02 -1.0425e+02 --2.1000e+02 3.1429e+02 -1.0217e+02 --2.1000e+02 3.4286e+02 -9.9706e+01 --2.1000e+02 3.7143e+02 -9.6779e+01 --2.1000e+02 4.0000e+02 -9.3298e+01 --2.1000e+02 4.2857e+02 -8.9162e+01 --2.1000e+02 4.5714e+02 -8.4273e+01 --2.1000e+02 4.8571e+02 -7.8573e+01 --2.1000e+02 5.1429e+02 -7.2086e+01 --2.1000e+02 5.4286e+02 -6.4976e+01 --2.1000e+02 5.7143e+02 -5.7553e+01 --2.1000e+02 6.0000e+02 -5.0220e+01 --2.1000e+02 6.2857e+02 -4.3351e+01 --2.1000e+02 6.5714e+02 -3.7191e+01 --2.1000e+02 6.8571e+02 -3.1836e+01 --2.1000e+02 7.1429e+02 -2.7264e+01 --2.1000e+02 7.4286e+02 -2.3394e+01 --2.1000e+02 7.7143e+02 -2.0123e+01 --2.1000e+02 8.0000e+02 -1.7351e+01 --2.1000e+02 8.2857e+02 -1.4990e+01 --2.1000e+02 8.5714e+02 -1.2965e+01 --2.1000e+02 8.8571e+02 -1.1217e+01 --2.1000e+02 9.1429e+02 -9.6976e+00 --2.1000e+02 9.4286e+02 -8.3674e+00 --2.1000e+02 9.7143e+02 -7.1954e+00 --2.1000e+02 1.0000e+03 -6.1564e+00 --2.1000e+02 1.0286e+03 -5.2301e+00 --2.1000e+02 1.0571e+03 -4.3998e+00 --2.1000e+02 1.0857e+03 -3.6520e+00 --2.1000e+02 1.1143e+03 -2.9752e+00 --2.1000e+02 1.1429e+03 -2.3603e+00 --2.1000e+02 1.1714e+03 -1.7992e+00 --2.1000e+02 1.2000e+03 -1.2854e+00 --2.1000e+02 1.2286e+03 -8.1336e-01 --2.1000e+02 1.2571e+03 -3.7824e-01 --2.1000e+02 1.2857e+03 2.4017e-02 --2.1000e+02 1.3143e+03 3.9693e-01 --2.1000e+02 1.3429e+03 7.4354e-01 --2.1000e+02 1.3714e+03 1.0665e+00 --2.1000e+02 1.4000e+03 1.3681e+00 --2.1000e+02 1.4286e+03 1.6503e+00 --2.1000e+02 1.4571e+03 1.9150e+00 --2.1000e+02 1.4857e+03 2.1636e+00 --2.1000e+02 1.5143e+03 2.3977e+00 --2.1000e+02 1.5429e+03 2.6184e+00 --2.1000e+02 1.5714e+03 2.8268e+00 --2.1000e+02 1.6000e+03 3.0239e+00 --2.1000e+02 1.6286e+03 3.2106e+00 --2.1000e+02 1.6571e+03 3.3877e+00 --2.1000e+02 1.6857e+03 3.5559e+00 --2.1000e+02 1.7143e+03 3.7159e+00 --2.1000e+02 1.7429e+03 3.8682e+00 --2.1000e+02 1.7714e+03 4.0133e+00 --2.1000e+02 1.8000e+03 4.1519e+00 --2.1000e+02 1.8286e+03 4.2842e+00 --2.1000e+02 1.8571e+03 4.4107e+00 --2.1000e+02 1.8857e+03 4.5318e+00 --2.1000e+02 1.9143e+03 4.6478e+00 --2.1000e+02 1.9429e+03 4.7590e+00 --2.1000e+02 1.9714e+03 4.8657e+00 --2.1000e+02 2.0000e+03 4.9682e+00 --1.8000e+02 -2.0000e+03 4.9579e+00 --1.8000e+02 -1.9714e+03 4.8548e+00 --1.8000e+02 -1.9429e+03 4.7475e+00 --1.8000e+02 -1.9143e+03 4.6356e+00 --1.8000e+02 -1.8857e+03 4.5189e+00 --1.8000e+02 -1.8571e+03 4.3970e+00 --1.8000e+02 -1.8286e+03 4.2696e+00 --1.8000e+02 -1.8000e+03 4.1364e+00 --1.8000e+02 -1.7714e+03 3.9969e+00 --1.8000e+02 -1.7429e+03 3.8507e+00 --1.8000e+02 -1.7143e+03 3.6972e+00 --1.8000e+02 -1.6857e+03 3.5359e+00 --1.8000e+02 -1.6571e+03 3.3663e+00 --1.8000e+02 -1.6286e+03 3.1877e+00 --1.8000e+02 -1.6000e+03 2.9993e+00 --1.8000e+02 -1.5714e+03 2.8003e+00 --1.8000e+02 -1.5429e+03 2.5898e+00 --1.8000e+02 -1.5143e+03 2.3668e+00 --1.8000e+02 -1.4857e+03 2.1302e+00 --1.8000e+02 -1.4571e+03 1.8787e+00 --1.8000e+02 -1.4286e+03 1.6109e+00 --1.8000e+02 -1.4000e+03 1.3251e+00 --1.8000e+02 -1.3714e+03 1.0196e+00 --1.8000e+02 -1.3429e+03 6.9220e-01 --1.8000e+02 -1.3143e+03 3.4054e-01 --1.8000e+02 -1.2857e+03 -3.8104e-02 --1.8000e+02 -1.2571e+03 -4.4690e-01 --1.8000e+02 -1.2286e+03 -8.8950e-01 --1.8000e+02 -1.2000e+03 -1.3702e+00 --1.8000e+02 -1.1714e+03 -1.8939e+00 --1.8000e+02 -1.1429e+03 -2.4665e+00 --1.8000e+02 -1.1143e+03 -3.0950e+00 --1.8000e+02 -1.0857e+03 -3.7876e+00 --1.8000e+02 -1.0571e+03 -4.5542e+00 --1.8000e+02 -1.0286e+03 -5.4069e+00 --1.8000e+02 -1.0000e+03 -6.3600e+00 --1.8000e+02 -9.7143e+02 -7.4313e+00 --1.8000e+02 -9.4286e+02 -8.6428e+00 --1.8000e+02 -9.1429e+02 -1.0021e+01 --1.8000e+02 -8.8571e+02 -1.1601e+01 --1.8000e+02 -8.5714e+02 -1.3423e+01 --1.8000e+02 -8.2857e+02 -1.5542e+01 --1.8000e+02 -8.0000e+02 -1.8021e+01 --1.8000e+02 -7.7143e+02 -2.0943e+01 --1.8000e+02 -7.4286e+02 -2.4402e+01 --1.8000e+02 -7.1429e+02 -2.8506e+01 --1.8000e+02 -6.8571e+02 -3.3360e+01 --1.8000e+02 -6.5714e+02 -3.9039e+01 --1.8000e+02 -6.2857e+02 -4.5536e+01 --1.8000e+02 -6.0000e+02 -5.2708e+01 --1.8000e+02 -5.7143e+02 -6.0248e+01 --1.8000e+02 -5.4286e+02 -6.7742e+01 --1.8000e+02 -5.1429e+02 -7.4786e+01 --1.8000e+02 -4.8571e+02 -8.1106e+01 --1.8000e+02 -4.5714e+02 -8.6592e+01 --1.8000e+02 -4.2857e+02 -9.1259e+01 --1.8000e+02 -4.0000e+02 -9.5189e+01 --1.8000e+02 -3.7143e+02 -9.8490e+01 --1.8000e+02 -3.4286e+02 -1.0127e+02 --1.8000e+02 -3.1429e+02 -1.0361e+02 --1.8000e+02 -2.8571e+02 -1.0559e+02 --1.8000e+02 -2.5714e+02 -1.0728e+02 --1.8000e+02 -2.2857e+02 -1.0871e+02 --1.8000e+02 -2.0000e+02 -1.0993e+02 --1.8000e+02 -1.7143e+02 -1.1096e+02 --1.8000e+02 -1.4286e+02 -1.1182e+02 --1.8000e+02 -1.1429e+02 -1.1252e+02 --1.8000e+02 -8.5714e+01 -1.1307e+02 --1.8000e+02 -5.7143e+01 -1.1346e+02 --1.8000e+02 -2.8571e+01 -1.1370e+02 --1.8000e+02 0.0000e+00 -1.1378e+02 --1.8000e+02 2.8571e+01 -1.1370e+02 --1.8000e+02 5.7143e+01 -1.1346e+02 --1.8000e+02 8.5714e+01 -1.1307e+02 --1.8000e+02 1.1429e+02 -1.1252e+02 --1.8000e+02 1.4286e+02 -1.1182e+02 --1.8000e+02 1.7143e+02 -1.1096e+02 --1.8000e+02 2.0000e+02 -1.0993e+02 --1.8000e+02 2.2857e+02 -1.0871e+02 --1.8000e+02 2.5714e+02 -1.0728e+02 --1.8000e+02 2.8571e+02 -1.0559e+02 --1.8000e+02 3.1429e+02 -1.0361e+02 --1.8000e+02 3.4286e+02 -1.0127e+02 --1.8000e+02 3.7143e+02 -9.8490e+01 --1.8000e+02 4.0000e+02 -9.5189e+01 --1.8000e+02 4.2857e+02 -9.1259e+01 --1.8000e+02 4.5714e+02 -8.6592e+01 --1.8000e+02 4.8571e+02 -8.1106e+01 --1.8000e+02 5.1429e+02 -7.4786e+01 --1.8000e+02 5.4286e+02 -6.7742e+01 --1.8000e+02 5.7143e+02 -6.0248e+01 --1.8000e+02 6.0000e+02 -5.2708e+01 --1.8000e+02 6.2857e+02 -4.5536e+01 --1.8000e+02 6.5714e+02 -3.9039e+01 --1.8000e+02 6.8571e+02 -3.3360e+01 --1.8000e+02 7.1429e+02 -2.8506e+01 --1.8000e+02 7.4286e+02 -2.4402e+01 --1.8000e+02 7.7143e+02 -2.0943e+01 --1.8000e+02 8.0000e+02 -1.8021e+01 --1.8000e+02 8.2857e+02 -1.5542e+01 --1.8000e+02 8.5714e+02 -1.3423e+01 --1.8000e+02 8.8571e+02 -1.1601e+01 --1.8000e+02 9.1429e+02 -1.0021e+01 --1.8000e+02 9.4286e+02 -8.6428e+00 --1.8000e+02 9.7143e+02 -7.4313e+00 --1.8000e+02 1.0000e+03 -6.3600e+00 --1.8000e+02 1.0286e+03 -5.4069e+00 --1.8000e+02 1.0571e+03 -4.5542e+00 --1.8000e+02 1.0857e+03 -3.7876e+00 --1.8000e+02 1.1143e+03 -3.0950e+00 --1.8000e+02 1.1429e+03 -2.4665e+00 --1.8000e+02 1.1714e+03 -1.8939e+00 --1.8000e+02 1.2000e+03 -1.3702e+00 --1.8000e+02 1.2286e+03 -8.8950e-01 --1.8000e+02 1.2571e+03 -4.4690e-01 --1.8000e+02 1.2857e+03 -3.8104e-02 --1.8000e+02 1.3143e+03 3.4054e-01 --1.8000e+02 1.3429e+03 6.9220e-01 --1.8000e+02 1.3714e+03 1.0196e+00 --1.8000e+02 1.4000e+03 1.3251e+00 --1.8000e+02 1.4286e+03 1.6109e+00 --1.8000e+02 1.4571e+03 1.8787e+00 --1.8000e+02 1.4857e+03 2.1302e+00 --1.8000e+02 1.5143e+03 2.3668e+00 --1.8000e+02 1.5429e+03 2.5898e+00 --1.8000e+02 1.5714e+03 2.8003e+00 --1.8000e+02 1.6000e+03 2.9993e+00 --1.8000e+02 1.6286e+03 3.1877e+00 --1.8000e+02 1.6571e+03 3.3663e+00 --1.8000e+02 1.6857e+03 3.5359e+00 --1.8000e+02 1.7143e+03 3.6972e+00 --1.8000e+02 1.7429e+03 3.8507e+00 --1.8000e+02 1.7714e+03 3.9969e+00 --1.8000e+02 1.8000e+03 4.1364e+00 --1.8000e+02 1.8286e+03 4.2696e+00 --1.8000e+02 1.8571e+03 4.3970e+00 --1.8000e+02 1.8857e+03 4.5189e+00 --1.8000e+02 1.9143e+03 4.6356e+00 --1.8000e+02 1.9429e+03 4.7475e+00 --1.8000e+02 1.9714e+03 4.8548e+00 --1.8000e+02 2.0000e+03 4.9579e+00 --1.5000e+02 -2.0000e+03 4.9491e+00 --1.5000e+02 -1.9714e+03 4.8456e+00 --1.5000e+02 -1.9429e+03 4.7377e+00 --1.5000e+02 -1.9143e+03 4.6252e+00 --1.5000e+02 -1.8857e+03 4.5079e+00 --1.5000e+02 -1.8571e+03 4.3854e+00 --1.5000e+02 -1.8286e+03 4.2573e+00 --1.5000e+02 -1.8000e+03 4.1233e+00 --1.5000e+02 -1.7714e+03 3.9829e+00 --1.5000e+02 -1.7429e+03 3.8357e+00 --1.5000e+02 -1.7143e+03 3.6812e+00 --1.5000e+02 -1.6857e+03 3.5189e+00 --1.5000e+02 -1.6571e+03 3.3481e+00 --1.5000e+02 -1.6286e+03 3.1681e+00 --1.5000e+02 -1.6000e+03 2.9782e+00 --1.5000e+02 -1.5714e+03 2.7776e+00 --1.5000e+02 -1.5429e+03 2.5654e+00 --1.5000e+02 -1.5143e+03 2.3404e+00 --1.5000e+02 -1.4857e+03 2.1017e+00 --1.5000e+02 -1.4571e+03 1.8477e+00 --1.5000e+02 -1.4286e+03 1.5772e+00 --1.5000e+02 -1.4000e+03 1.2884e+00 --1.5000e+02 -1.3714e+03 9.7945e-01 --1.5000e+02 -1.3429e+03 6.4819e-01 --1.5000e+02 -1.3143e+03 2.9217e-01 --1.5000e+02 -1.2857e+03 -9.1437e-02 --1.5000e+02 -1.2571e+03 -5.0589e-01 --1.5000e+02 -1.2286e+03 -9.5498e-01 --1.5000e+02 -1.2000e+03 -1.4431e+00 --1.5000e+02 -1.1714e+03 -1.9755e+00 --1.5000e+02 -1.1429e+03 -2.5582e+00 --1.5000e+02 -1.1143e+03 -3.1985e+00 --1.5000e+02 -1.0857e+03 -3.9049e+00 --1.5000e+02 -1.0571e+03 -4.6880e+00 --1.5000e+02 -1.0286e+03 -5.5603e+00 --1.5000e+02 -1.0000e+03 -6.5370e+00 --1.5000e+02 -9.7143e+02 -7.6370e+00 --1.5000e+02 -9.4286e+02 -8.8833e+00 --1.5000e+02 -9.1429e+02 -1.0305e+01 --1.5000e+02 -8.8571e+02 -1.1938e+01 --1.5000e+02 -8.5714e+02 -1.3827e+01 --1.5000e+02 -8.2857e+02 -1.6030e+01 --1.5000e+02 -8.0000e+02 -1.8616e+01 --1.5000e+02 -7.7143e+02 -2.1672e+01 --1.5000e+02 -7.4286e+02 -2.5301e+01 --1.5000e+02 -7.1429e+02 -2.9616e+01 --1.5000e+02 -6.8571e+02 -3.4723e+01 --1.5000e+02 -6.5714e+02 -4.0685e+01 --1.5000e+02 -6.2857e+02 -4.7468e+01 --1.5000e+02 -6.0000e+02 -5.4880e+01 --1.5000e+02 -5.7143e+02 -6.2563e+01 --1.5000e+02 -5.4286e+02 -7.0074e+01 --1.5000e+02 -5.1429e+02 -7.7025e+01 --1.5000e+02 -4.8571e+02 -8.3182e+01 --1.5000e+02 -4.5714e+02 -8.8476e+01 --1.5000e+02 -4.2857e+02 -9.2953e+01 --1.5000e+02 -4.0000e+02 -9.6713e+01 --1.5000e+02 -3.7143e+02 -9.9869e+01 --1.5000e+02 -3.4286e+02 -1.0252e+02 --1.5000e+02 -3.1429e+02 -1.0477e+02 --1.5000e+02 -2.8571e+02 -1.0668e+02 --1.5000e+02 -2.5714e+02 -1.0831e+02 --1.5000e+02 -2.2857e+02 -1.0970e+02 --1.5000e+02 -2.0000e+02 -1.1089e+02 --1.5000e+02 -1.7143e+02 -1.1191e+02 --1.5000e+02 -1.4286e+02 -1.1276e+02 --1.5000e+02 -1.1429e+02 -1.1347e+02 --1.5000e+02 -8.5714e+01 -1.1403e+02 --1.5000e+02 -5.7143e+01 -1.1444e+02 --1.5000e+02 -2.8571e+01 -1.1469e+02 --1.5000e+02 0.0000e+00 -1.1478e+02 --1.5000e+02 2.8571e+01 -1.1469e+02 --1.5000e+02 5.7143e+01 -1.1444e+02 --1.5000e+02 8.5714e+01 -1.1403e+02 --1.5000e+02 1.1429e+02 -1.1347e+02 --1.5000e+02 1.4286e+02 -1.1276e+02 --1.5000e+02 1.7143e+02 -1.1191e+02 --1.5000e+02 2.0000e+02 -1.1089e+02 --1.5000e+02 2.2857e+02 -1.0970e+02 --1.5000e+02 2.5714e+02 -1.0831e+02 --1.5000e+02 2.8571e+02 -1.0668e+02 --1.5000e+02 3.1429e+02 -1.0477e+02 --1.5000e+02 3.4286e+02 -1.0252e+02 --1.5000e+02 3.7143e+02 -9.9869e+01 --1.5000e+02 4.0000e+02 -9.6713e+01 --1.5000e+02 4.2857e+02 -9.2953e+01 --1.5000e+02 4.5714e+02 -8.8476e+01 --1.5000e+02 4.8571e+02 -8.3182e+01 --1.5000e+02 5.1429e+02 -7.7025e+01 --1.5000e+02 5.4286e+02 -7.0074e+01 --1.5000e+02 5.7143e+02 -6.2563e+01 --1.5000e+02 6.0000e+02 -5.4880e+01 --1.5000e+02 6.2857e+02 -4.7468e+01 --1.5000e+02 6.5714e+02 -4.0685e+01 --1.5000e+02 6.8571e+02 -3.4723e+01 --1.5000e+02 7.1429e+02 -2.9616e+01 --1.5000e+02 7.4286e+02 -2.5301e+01 --1.5000e+02 7.7143e+02 -2.1672e+01 --1.5000e+02 8.0000e+02 -1.8616e+01 --1.5000e+02 8.2857e+02 -1.6030e+01 --1.5000e+02 8.5714e+02 -1.3827e+01 --1.5000e+02 8.8571e+02 -1.1938e+01 --1.5000e+02 9.1429e+02 -1.0305e+01 --1.5000e+02 9.4286e+02 -8.8833e+00 --1.5000e+02 9.7143e+02 -7.6370e+00 --1.5000e+02 1.0000e+03 -6.5370e+00 --1.5000e+02 1.0286e+03 -5.5603e+00 --1.5000e+02 1.0571e+03 -4.6880e+00 --1.5000e+02 1.0857e+03 -3.9049e+00 --1.5000e+02 1.1143e+03 -3.1985e+00 --1.5000e+02 1.1429e+03 -2.5582e+00 --1.5000e+02 1.1714e+03 -1.9755e+00 --1.5000e+02 1.2000e+03 -1.4431e+00 --1.5000e+02 1.2286e+03 -9.5498e-01 --1.5000e+02 1.2571e+03 -5.0589e-01 --1.5000e+02 1.2857e+03 -9.1437e-02 --1.5000e+02 1.3143e+03 2.9217e-01 --1.5000e+02 1.3429e+03 6.4819e-01 --1.5000e+02 1.3714e+03 9.7945e-01 --1.5000e+02 1.4000e+03 1.2884e+00 --1.5000e+02 1.4286e+03 1.5772e+00 --1.5000e+02 1.4571e+03 1.8477e+00 --1.5000e+02 1.4857e+03 2.1017e+00 --1.5000e+02 1.5143e+03 2.3404e+00 --1.5000e+02 1.5429e+03 2.5654e+00 --1.5000e+02 1.5714e+03 2.7776e+00 --1.5000e+02 1.6000e+03 2.9782e+00 --1.5000e+02 1.6286e+03 3.1681e+00 --1.5000e+02 1.6571e+03 3.3481e+00 --1.5000e+02 1.6857e+03 3.5189e+00 --1.5000e+02 1.7143e+03 3.6812e+00 --1.5000e+02 1.7429e+03 3.8357e+00 --1.5000e+02 1.7714e+03 3.9829e+00 --1.5000e+02 1.8000e+03 4.1233e+00 --1.5000e+02 1.8286e+03 4.2573e+00 --1.5000e+02 1.8571e+03 4.3854e+00 --1.5000e+02 1.8857e+03 4.5079e+00 --1.5000e+02 1.9143e+03 4.6252e+00 --1.5000e+02 1.9429e+03 4.7377e+00 --1.5000e+02 1.9714e+03 4.8456e+00 --1.5000e+02 2.0000e+03 4.9491e+00 --1.2000e+02 -2.0000e+03 4.9419e+00 --1.2000e+02 -1.9714e+03 4.8379e+00 --1.2000e+02 -1.9429e+03 4.7296e+00 --1.2000e+02 -1.9143e+03 4.6167e+00 --1.2000e+02 -1.8857e+03 4.4989e+00 --1.2000e+02 -1.8571e+03 4.3758e+00 --1.2000e+02 -1.8286e+03 4.2471e+00 --1.2000e+02 -1.8000e+03 4.1124e+00 --1.2000e+02 -1.7714e+03 3.9714e+00 --1.2000e+02 -1.7429e+03 3.8234e+00 --1.2000e+02 -1.7143e+03 3.6681e+00 --1.2000e+02 -1.6857e+03 3.5049e+00 --1.2000e+02 -1.6571e+03 3.3330e+00 --1.2000e+02 -1.6286e+03 3.1520e+00 --1.2000e+02 -1.6000e+03 2.9609e+00 --1.2000e+02 -1.5714e+03 2.7590e+00 --1.2000e+02 -1.5429e+03 2.5452e+00 --1.2000e+02 -1.5143e+03 2.3187e+00 --1.2000e+02 -1.4857e+03 2.0781e+00 --1.2000e+02 -1.4571e+03 1.8222e+00 --1.2000e+02 -1.4286e+03 1.5494e+00 --1.2000e+02 -1.4000e+03 1.2580e+00 --1.2000e+02 -1.3714e+03 9.4624e-01 --1.2000e+02 -1.3429e+03 6.1178e-01 --1.2000e+02 -1.3143e+03 2.5213e-01 --1.2000e+02 -1.2857e+03 -1.3561e-01 --1.2000e+02 -1.2571e+03 -5.5478e-01 --1.2000e+02 -1.2286e+03 -1.0093e+00 --1.2000e+02 -1.2000e+03 -1.5037e+00 --1.2000e+02 -1.1714e+03 -2.0433e+00 --1.2000e+02 -1.1429e+03 -2.6344e+00 --1.2000e+02 -1.1143e+03 -3.2846e+00 --1.2000e+02 -1.0857e+03 -4.0028e+00 --1.2000e+02 -1.0571e+03 -4.7997e+00 --1.2000e+02 -1.0286e+03 -5.6885e+00 --1.2000e+02 -1.0000e+03 -6.6852e+00 --1.2000e+02 -9.7143e+02 -7.8095e+00 --1.2000e+02 -9.4286e+02 -9.0855e+00 --1.2000e+02 -9.1429e+02 -1.0544e+01 --1.2000e+02 -8.8571e+02 -1.2222e+01 --1.2000e+02 -8.5714e+02 -1.4169e+01 --1.2000e+02 -8.2857e+02 -1.6444e+01 --1.2000e+02 -8.0000e+02 -1.9122e+01 --1.2000e+02 -7.7143e+02 -2.2295e+01 --1.2000e+02 -7.4286e+02 -2.6071e+01 --1.2000e+02 -7.1429e+02 -3.0568e+01 --1.2000e+02 -6.8571e+02 -3.5889e+01 --1.2000e+02 -6.5714e+02 -4.2089e+01 --1.2000e+02 -6.2857e+02 -4.9103e+01 --1.2000e+02 -6.0000e+02 -5.6697e+01 --1.2000e+02 -5.7143e+02 -6.4471e+01 --1.2000e+02 -5.4286e+02 -7.1969e+01 --1.2000e+02 -5.1429e+02 -7.8821e+01 --1.2000e+02 -4.8571e+02 -8.4829e+01 --1.2000e+02 -4.5714e+02 -8.9961e+01 --1.2000e+02 -4.2857e+02 -9.4285e+01 --1.2000e+02 -4.0000e+02 -9.7910e+01 --1.2000e+02 -3.7143e+02 -1.0095e+02 --1.2000e+02 -3.4286e+02 -1.0352e+02 --1.2000e+02 -3.1429e+02 -1.0569e+02 --1.2000e+02 -2.8571e+02 -1.0754e+02 --1.2000e+02 -2.5714e+02 -1.0913e+02 --1.2000e+02 -2.2857e+02 -1.1049e+02 --1.2000e+02 -2.0000e+02 -1.1167e+02 --1.2000e+02 -1.7143e+02 -1.1268e+02 --1.2000e+02 -1.4286e+02 -1.1354e+02 --1.2000e+02 -1.1429e+02 -1.1427e+02 --1.2000e+02 -8.5714e+01 -1.1486e+02 --1.2000e+02 -5.7143e+01 -1.1529e+02 --1.2000e+02 -2.8571e+01 -1.1557e+02 --1.2000e+02 0.0000e+00 -1.1566e+02 --1.2000e+02 2.8571e+01 -1.1557e+02 --1.2000e+02 5.7143e+01 -1.1529e+02 --1.2000e+02 8.5714e+01 -1.1486e+02 --1.2000e+02 1.1429e+02 -1.1427e+02 --1.2000e+02 1.4286e+02 -1.1354e+02 --1.2000e+02 1.7143e+02 -1.1268e+02 --1.2000e+02 2.0000e+02 -1.1167e+02 --1.2000e+02 2.2857e+02 -1.1049e+02 --1.2000e+02 2.5714e+02 -1.0913e+02 --1.2000e+02 2.8571e+02 -1.0754e+02 --1.2000e+02 3.1429e+02 -1.0569e+02 --1.2000e+02 3.4286e+02 -1.0352e+02 --1.2000e+02 3.7143e+02 -1.0095e+02 --1.2000e+02 4.0000e+02 -9.7910e+01 --1.2000e+02 4.2857e+02 -9.4285e+01 --1.2000e+02 4.5714e+02 -8.9961e+01 --1.2000e+02 4.8571e+02 -8.4829e+01 --1.2000e+02 5.1429e+02 -7.8821e+01 --1.2000e+02 5.4286e+02 -7.1969e+01 --1.2000e+02 5.7143e+02 -6.4471e+01 --1.2000e+02 6.0000e+02 -5.6697e+01 --1.2000e+02 6.2857e+02 -4.9103e+01 --1.2000e+02 6.5714e+02 -4.2089e+01 --1.2000e+02 6.8571e+02 -3.5889e+01 --1.2000e+02 7.1429e+02 -3.0568e+01 --1.2000e+02 7.4286e+02 -2.6071e+01 --1.2000e+02 7.7143e+02 -2.2295e+01 --1.2000e+02 8.0000e+02 -1.9122e+01 --1.2000e+02 8.2857e+02 -1.6444e+01 --1.2000e+02 8.5714e+02 -1.4169e+01 --1.2000e+02 8.8571e+02 -1.2222e+01 --1.2000e+02 9.1429e+02 -1.0544e+01 --1.2000e+02 9.4286e+02 -9.0855e+00 --1.2000e+02 9.7143e+02 -7.8095e+00 --1.2000e+02 1.0000e+03 -6.6852e+00 --1.2000e+02 1.0286e+03 -5.6885e+00 --1.2000e+02 1.0571e+03 -4.7997e+00 --1.2000e+02 1.0857e+03 -4.0028e+00 --1.2000e+02 1.1143e+03 -3.2846e+00 --1.2000e+02 1.1429e+03 -2.6344e+00 --1.2000e+02 1.1714e+03 -2.0433e+00 --1.2000e+02 1.2000e+03 -1.5037e+00 --1.2000e+02 1.2286e+03 -1.0093e+00 --1.2000e+02 1.2571e+03 -5.5478e-01 --1.2000e+02 1.2857e+03 -1.3561e-01 --1.2000e+02 1.3143e+03 2.5213e-01 --1.2000e+02 1.3429e+03 6.1178e-01 --1.2000e+02 1.3714e+03 9.4624e-01 --1.2000e+02 1.4000e+03 1.2580e+00 --1.2000e+02 1.4286e+03 1.5494e+00 --1.2000e+02 1.4571e+03 1.8222e+00 --1.2000e+02 1.4857e+03 2.0781e+00 --1.2000e+02 1.5143e+03 2.3187e+00 --1.2000e+02 1.5429e+03 2.5452e+00 --1.2000e+02 1.5714e+03 2.7590e+00 --1.2000e+02 1.6000e+03 2.9609e+00 --1.2000e+02 1.6286e+03 3.1520e+00 --1.2000e+02 1.6571e+03 3.3330e+00 --1.2000e+02 1.6857e+03 3.5049e+00 --1.2000e+02 1.7143e+03 3.6681e+00 --1.2000e+02 1.7429e+03 3.8234e+00 --1.2000e+02 1.7714e+03 3.9714e+00 --1.2000e+02 1.8000e+03 4.1124e+00 --1.2000e+02 1.8286e+03 4.2471e+00 --1.2000e+02 1.8571e+03 4.3758e+00 --1.2000e+02 1.8857e+03 4.4989e+00 --1.2000e+02 1.9143e+03 4.6167e+00 --1.2000e+02 1.9429e+03 4.7296e+00 --1.2000e+02 1.9714e+03 4.8379e+00 --1.2000e+02 2.0000e+03 4.9419e+00 --9.0000e+01 -2.0000e+03 4.9363e+00 --9.0000e+01 -1.9714e+03 4.8320e+00 --9.0000e+01 -1.9429e+03 4.7233e+00 --9.0000e+01 -1.9143e+03 4.6100e+00 --9.0000e+01 -1.8857e+03 4.4918e+00 --9.0000e+01 -1.8571e+03 4.3683e+00 --9.0000e+01 -1.8286e+03 4.2391e+00 --9.0000e+01 -1.8000e+03 4.1040e+00 --9.0000e+01 -1.7714e+03 3.9624e+00 --9.0000e+01 -1.7429e+03 3.8138e+00 --9.0000e+01 -1.7143e+03 3.6579e+00 --9.0000e+01 -1.6857e+03 3.4939e+00 --9.0000e+01 -1.6571e+03 3.3213e+00 --9.0000e+01 -1.6286e+03 3.1393e+00 --9.0000e+01 -1.6000e+03 2.9473e+00 --9.0000e+01 -1.5714e+03 2.7444e+00 --9.0000e+01 -1.5429e+03 2.5295e+00 --9.0000e+01 -1.5143e+03 2.3016e+00 --9.0000e+01 -1.4857e+03 2.0596e+00 --9.0000e+01 -1.4571e+03 1.8021e+00 --9.0000e+01 -1.4286e+03 1.5276e+00 --9.0000e+01 -1.4000e+03 1.2342e+00 --9.0000e+01 -1.3714e+03 9.2020e-01 --9.0000e+01 -1.3429e+03 5.8321e-01 --9.0000e+01 -1.3143e+03 2.2070e-01 --9.0000e+01 -1.2857e+03 -1.7030e-01 --9.0000e+01 -1.2571e+03 -5.9321e-01 --9.0000e+01 -1.2286e+03 -1.0520e+00 --9.0000e+01 -1.2000e+03 -1.5513e+00 --9.0000e+01 -1.1714e+03 -2.0967e+00 --9.0000e+01 -1.1429e+03 -2.6945e+00 --9.0000e+01 -1.1143e+03 -3.3525e+00 --9.0000e+01 -1.0857e+03 -4.0800e+00 --9.0000e+01 -1.0571e+03 -4.8880e+00 --9.0000e+01 -1.0286e+03 -5.7901e+00 --9.0000e+01 -1.0000e+03 -6.8027e+00 --9.0000e+01 -9.7143e+02 -7.9464e+00 --9.0000e+01 -9.4286e+02 -9.2463e+00 --9.0000e+01 -9.1429e+02 -1.0734e+01 --9.0000e+01 -8.8571e+02 -1.2449e+01 --9.0000e+01 -8.5714e+02 -1.4442e+01 --9.0000e+01 -8.2857e+02 -1.6776e+01 --9.0000e+01 -8.0000e+02 -1.9529e+01 --9.0000e+01 -7.7143e+02 -2.2796e+01 --9.0000e+01 -7.4286e+02 -2.6692e+01 --9.0000e+01 -7.1429e+02 -3.1335e+01 --9.0000e+01 -6.8571e+02 -3.6830e+01 --9.0000e+01 -6.5714e+02 -4.3217e+01 --9.0000e+01 -6.2857e+02 -5.0408e+01 --9.0000e+01 -6.0000e+02 -5.8132e+01 --9.0000e+01 -5.7143e+02 -6.5960e+01 --9.0000e+01 -5.4286e+02 -7.3430e+01 --9.0000e+01 -5.1429e+02 -8.0191e+01 --9.0000e+01 -4.8571e+02 -8.6078e+01 --9.0000e+01 -4.5714e+02 -9.1083e+01 --9.0000e+01 -4.2857e+02 -9.5288e+01 --9.0000e+01 -4.0000e+02 -9.8810e+01 --9.0000e+01 -3.7143e+02 -1.0177e+02 --9.0000e+01 -3.4286e+02 -1.0426e+02 --9.0000e+01 -3.1429e+02 -1.0638e+02 --9.0000e+01 -2.8571e+02 -1.0819e+02 --9.0000e+01 -2.5714e+02 -1.0975e+02 --9.0000e+01 -2.2857e+02 -1.1110e+02 --9.0000e+01 -2.0000e+02 -1.1227e+02 --9.0000e+01 -1.7143e+02 -1.1329e+02 --9.0000e+01 -1.4286e+02 -1.1416e+02 --9.0000e+01 -1.1429e+02 -1.1492e+02 --9.0000e+01 -8.5714e+01 -1.1554e+02 --9.0000e+01 -5.7143e+01 -1.1603e+02 --9.0000e+01 -2.8571e+01 -1.1634e+02 --9.0000e+01 0.0000e+00 -1.1645e+02 --9.0000e+01 2.8571e+01 -1.1634e+02 --9.0000e+01 5.7143e+01 -1.1603e+02 --9.0000e+01 8.5714e+01 -1.1554e+02 --9.0000e+01 1.1429e+02 -1.1492e+02 --9.0000e+01 1.4286e+02 -1.1416e+02 --9.0000e+01 1.7143e+02 -1.1329e+02 --9.0000e+01 2.0000e+02 -1.1227e+02 --9.0000e+01 2.2857e+02 -1.1110e+02 --9.0000e+01 2.5714e+02 -1.0975e+02 --9.0000e+01 2.8571e+02 -1.0819e+02 --9.0000e+01 3.1429e+02 -1.0638e+02 --9.0000e+01 3.4286e+02 -1.0426e+02 --9.0000e+01 3.7143e+02 -1.0177e+02 --9.0000e+01 4.0000e+02 -9.8810e+01 --9.0000e+01 4.2857e+02 -9.5288e+01 --9.0000e+01 4.5714e+02 -9.1083e+01 --9.0000e+01 4.8571e+02 -8.6078e+01 --9.0000e+01 5.1429e+02 -8.0191e+01 --9.0000e+01 5.4286e+02 -7.3430e+01 --9.0000e+01 5.7143e+02 -6.5960e+01 --9.0000e+01 6.0000e+02 -5.8132e+01 --9.0000e+01 6.2857e+02 -5.0408e+01 --9.0000e+01 6.5714e+02 -4.3217e+01 --9.0000e+01 6.8571e+02 -3.6830e+01 --9.0000e+01 7.1429e+02 -3.1335e+01 --9.0000e+01 7.4286e+02 -2.6692e+01 --9.0000e+01 7.7143e+02 -2.2796e+01 --9.0000e+01 8.0000e+02 -1.9529e+01 --9.0000e+01 8.2857e+02 -1.6776e+01 --9.0000e+01 8.5714e+02 -1.4442e+01 --9.0000e+01 8.8571e+02 -1.2449e+01 --9.0000e+01 9.1429e+02 -1.0734e+01 --9.0000e+01 9.4286e+02 -9.2463e+00 --9.0000e+01 9.7143e+02 -7.9464e+00 --9.0000e+01 1.0000e+03 -6.8027e+00 --9.0000e+01 1.0286e+03 -5.7901e+00 --9.0000e+01 1.0571e+03 -4.8880e+00 --9.0000e+01 1.0857e+03 -4.0800e+00 --9.0000e+01 1.1143e+03 -3.3525e+00 --9.0000e+01 1.1429e+03 -2.6945e+00 --9.0000e+01 1.1714e+03 -2.0967e+00 --9.0000e+01 1.2000e+03 -1.5513e+00 --9.0000e+01 1.2286e+03 -1.0520e+00 --9.0000e+01 1.2571e+03 -5.9321e-01 --9.0000e+01 1.2857e+03 -1.7030e-01 --9.0000e+01 1.3143e+03 2.2070e-01 --9.0000e+01 1.3429e+03 5.8321e-01 --9.0000e+01 1.3714e+03 9.2020e-01 --9.0000e+01 1.4000e+03 1.2342e+00 --9.0000e+01 1.4286e+03 1.5276e+00 --9.0000e+01 1.4571e+03 1.8021e+00 --9.0000e+01 1.4857e+03 2.0596e+00 --9.0000e+01 1.5143e+03 2.3016e+00 --9.0000e+01 1.5429e+03 2.5295e+00 --9.0000e+01 1.5714e+03 2.7444e+00 --9.0000e+01 1.6000e+03 2.9473e+00 --9.0000e+01 1.6286e+03 3.1393e+00 --9.0000e+01 1.6571e+03 3.3213e+00 --9.0000e+01 1.6857e+03 3.4939e+00 --9.0000e+01 1.7143e+03 3.6579e+00 --9.0000e+01 1.7429e+03 3.8138e+00 --9.0000e+01 1.7714e+03 3.9624e+00 --9.0000e+01 1.8000e+03 4.1040e+00 --9.0000e+01 1.8286e+03 4.2391e+00 --9.0000e+01 1.8571e+03 4.3683e+00 --9.0000e+01 1.8857e+03 4.4918e+00 --9.0000e+01 1.9143e+03 4.6100e+00 --9.0000e+01 1.9429e+03 4.7233e+00 --9.0000e+01 1.9714e+03 4.8320e+00 --9.0000e+01 2.0000e+03 4.9363e+00 --6.0000e+01 -2.0000e+03 4.9323e+00 --6.0000e+01 -1.9714e+03 4.8277e+00 --6.0000e+01 -1.9429e+03 4.7188e+00 --6.0000e+01 -1.9143e+03 4.6053e+00 --6.0000e+01 -1.8857e+03 4.4867e+00 --6.0000e+01 -1.8571e+03 4.3629e+00 --6.0000e+01 -1.8286e+03 4.2334e+00 --6.0000e+01 -1.8000e+03 4.0979e+00 --6.0000e+01 -1.7714e+03 3.9559e+00 --6.0000e+01 -1.7429e+03 3.8069e+00 --6.0000e+01 -1.7143e+03 3.6505e+00 --6.0000e+01 -1.6857e+03 3.4860e+00 --6.0000e+01 -1.6571e+03 3.3128e+00 --6.0000e+01 -1.6286e+03 3.1303e+00 --6.0000e+01 -1.6000e+03 2.9376e+00 --6.0000e+01 -1.5714e+03 2.7339e+00 --6.0000e+01 -1.5429e+03 2.5182e+00 --6.0000e+01 -1.5143e+03 2.2894e+00 --6.0000e+01 -1.4857e+03 2.0464e+00 --6.0000e+01 -1.4571e+03 1.7877e+00 --6.0000e+01 -1.4286e+03 1.5119e+00 --6.0000e+01 -1.4000e+03 1.2171e+00 --6.0000e+01 -1.3714e+03 9.0148e-01 --6.0000e+01 -1.3429e+03 5.6267e-01 --6.0000e+01 -1.3143e+03 1.9809e-01 --6.0000e+01 -1.2857e+03 -1.9527e-01 --6.0000e+01 -1.2571e+03 -6.2087e-01 --6.0000e+01 -1.2286e+03 -1.0828e+00 --6.0000e+01 -1.2000e+03 -1.5857e+00 --6.0000e+01 -1.1714e+03 -2.1352e+00 --6.0000e+01 -1.1429e+03 -2.7379e+00 --6.0000e+01 -1.1143e+03 -3.4016e+00 --6.0000e+01 -1.0857e+03 -4.1358e+00 --6.0000e+01 -1.0571e+03 -4.9518e+00 --6.0000e+01 -1.0286e+03 -5.8635e+00 --6.0000e+01 -1.0000e+03 -6.8878e+00 --6.0000e+01 -9.7143e+02 -8.0457e+00 --6.0000e+01 -9.4286e+02 -9.3630e+00 --6.0000e+01 -9.1429e+02 -1.0872e+01 --6.0000e+01 -8.8571e+02 -1.2615e+01 --6.0000e+01 -8.5714e+02 -1.4642e+01 --6.0000e+01 -8.2857e+02 -1.7019e+01 --6.0000e+01 -8.0000e+02 -1.9826e+01 --6.0000e+01 -7.7143e+02 -2.3164e+01 --6.0000e+01 -7.4286e+02 -2.7148e+01 --6.0000e+01 -7.1429e+02 -3.1899e+01 --6.0000e+01 -6.8571e+02 -3.7520e+01 --6.0000e+01 -6.5714e+02 -4.4042e+01 --6.0000e+01 -6.2857e+02 -5.1357e+01 --6.0000e+01 -6.0000e+02 -5.9167e+01 --6.0000e+01 -5.7143e+02 -6.7024e+01 --6.0000e+01 -5.4286e+02 -7.4464e+01 --6.0000e+01 -5.1429e+02 -8.1155e+01 --6.0000e+01 -4.8571e+02 -8.6952e+01 --6.0000e+01 -4.5714e+02 -9.1865e+01 --6.0000e+01 -4.2857e+02 -9.5987e+01 --6.0000e+01 -4.0000e+02 -9.9438e+01 --6.0000e+01 -3.7143e+02 -1.0234e+02 --6.0000e+01 -3.4286e+02 -1.0479e+02 --6.0000e+01 -3.1429e+02 -1.0687e+02 --6.0000e+01 -2.8571e+02 -1.0865e+02 --6.0000e+01 -2.5714e+02 -1.1019e+02 --6.0000e+01 -2.2857e+02 -1.1153e+02 --6.0000e+01 -2.0000e+02 -1.1270e+02 --6.0000e+01 -1.7143e+02 -1.1372e+02 --6.0000e+01 -1.4286e+02 -1.1462e+02 --6.0000e+01 -1.1429e+02 -1.1540e+02 --6.0000e+01 -8.5714e+01 -1.1608e+02 --6.0000e+01 -5.7143e+01 -1.1663e+02 --6.0000e+01 -2.8571e+01 -1.1702e+02 --6.0000e+01 0.0000e+00 -1.1716e+02 --6.0000e+01 2.8571e+01 -1.1702e+02 --6.0000e+01 5.7143e+01 -1.1663e+02 --6.0000e+01 8.5714e+01 -1.1608e+02 --6.0000e+01 1.1429e+02 -1.1540e+02 --6.0000e+01 1.4286e+02 -1.1462e+02 --6.0000e+01 1.7143e+02 -1.1372e+02 --6.0000e+01 2.0000e+02 -1.1270e+02 --6.0000e+01 2.2857e+02 -1.1153e+02 --6.0000e+01 2.5714e+02 -1.1019e+02 --6.0000e+01 2.8571e+02 -1.0865e+02 --6.0000e+01 3.1429e+02 -1.0687e+02 --6.0000e+01 3.4286e+02 -1.0479e+02 --6.0000e+01 3.7143e+02 -1.0234e+02 --6.0000e+01 4.0000e+02 -9.9438e+01 --6.0000e+01 4.2857e+02 -9.5987e+01 --6.0000e+01 4.5714e+02 -9.1865e+01 --6.0000e+01 4.8571e+02 -8.6952e+01 --6.0000e+01 5.1429e+02 -8.1155e+01 --6.0000e+01 5.4286e+02 -7.4464e+01 --6.0000e+01 5.7143e+02 -6.7024e+01 --6.0000e+01 6.0000e+02 -5.9167e+01 --6.0000e+01 6.2857e+02 -5.1357e+01 --6.0000e+01 6.5714e+02 -4.4042e+01 --6.0000e+01 6.8571e+02 -3.7520e+01 --6.0000e+01 7.1429e+02 -3.1899e+01 --6.0000e+01 7.4286e+02 -2.7148e+01 --6.0000e+01 7.7143e+02 -2.3164e+01 --6.0000e+01 8.0000e+02 -1.9826e+01 --6.0000e+01 8.2857e+02 -1.7019e+01 --6.0000e+01 8.5714e+02 -1.4642e+01 --6.0000e+01 8.8571e+02 -1.2615e+01 --6.0000e+01 9.1429e+02 -1.0872e+01 --6.0000e+01 9.4286e+02 -9.3630e+00 --6.0000e+01 9.7143e+02 -8.0457e+00 --6.0000e+01 1.0000e+03 -6.8878e+00 --6.0000e+01 1.0286e+03 -5.8635e+00 --6.0000e+01 1.0571e+03 -4.9518e+00 --6.0000e+01 1.0857e+03 -4.1358e+00 --6.0000e+01 1.1143e+03 -3.4016e+00 --6.0000e+01 1.1429e+03 -2.7379e+00 --6.0000e+01 1.1714e+03 -2.1352e+00 --6.0000e+01 1.2000e+03 -1.5857e+00 --6.0000e+01 1.2286e+03 -1.0828e+00 --6.0000e+01 1.2571e+03 -6.2087e-01 --6.0000e+01 1.2857e+03 -1.9527e-01 --6.0000e+01 1.3143e+03 1.9809e-01 --6.0000e+01 1.3429e+03 5.6267e-01 --6.0000e+01 1.3714e+03 9.0148e-01 --6.0000e+01 1.4000e+03 1.2171e+00 --6.0000e+01 1.4286e+03 1.5119e+00 --6.0000e+01 1.4571e+03 1.7877e+00 --6.0000e+01 1.4857e+03 2.0464e+00 --6.0000e+01 1.5143e+03 2.2894e+00 --6.0000e+01 1.5429e+03 2.5182e+00 --6.0000e+01 1.5714e+03 2.7339e+00 --6.0000e+01 1.6000e+03 2.9376e+00 --6.0000e+01 1.6286e+03 3.1303e+00 --6.0000e+01 1.6571e+03 3.3128e+00 --6.0000e+01 1.6857e+03 3.4860e+00 --6.0000e+01 1.7143e+03 3.6505e+00 --6.0000e+01 1.7429e+03 3.8069e+00 --6.0000e+01 1.7714e+03 3.9559e+00 --6.0000e+01 1.8000e+03 4.0979e+00 --6.0000e+01 1.8286e+03 4.2334e+00 --6.0000e+01 1.8571e+03 4.3629e+00 --6.0000e+01 1.8857e+03 4.4867e+00 --6.0000e+01 1.9143e+03 4.6053e+00 --6.0000e+01 1.9429e+03 4.7188e+00 --6.0000e+01 1.9714e+03 4.8277e+00 --6.0000e+01 2.0000e+03 4.9323e+00 --3.0000e+01 -2.0000e+03 4.9299e+00 --3.0000e+01 -1.9714e+03 4.8252e+00 --3.0000e+01 -1.9429e+03 4.7161e+00 --3.0000e+01 -1.9143e+03 4.6024e+00 --3.0000e+01 -1.8857e+03 4.4837e+00 --3.0000e+01 -1.8571e+03 4.3597e+00 --3.0000e+01 -1.8286e+03 4.2300e+00 --3.0000e+01 -1.8000e+03 4.0943e+00 --3.0000e+01 -1.7714e+03 3.9520e+00 --3.0000e+01 -1.7429e+03 3.8028e+00 --3.0000e+01 -1.7143e+03 3.6461e+00 --3.0000e+01 -1.6857e+03 3.4813e+00 --3.0000e+01 -1.6571e+03 3.3078e+00 --3.0000e+01 -1.6286e+03 3.1249e+00 --3.0000e+01 -1.6000e+03 2.9317e+00 --3.0000e+01 -1.5714e+03 2.7276e+00 --3.0000e+01 -1.5429e+03 2.5114e+00 --3.0000e+01 -1.5143e+03 2.2820e+00 --3.0000e+01 -1.4857e+03 2.0384e+00 --3.0000e+01 -1.4571e+03 1.7790e+00 --3.0000e+01 -1.4286e+03 1.5024e+00 --3.0000e+01 -1.4000e+03 1.2068e+00 --3.0000e+01 -1.3714e+03 8.9021e-01 --3.0000e+01 -1.3429e+03 5.5029e-01 --3.0000e+01 -1.3143e+03 1.8446e-01 --3.0000e+01 -1.2857e+03 -2.1032e-01 --3.0000e+01 -1.2571e+03 -6.3755e-01 --3.0000e+01 -1.2286e+03 -1.1013e+00 --3.0000e+01 -1.2000e+03 -1.6064e+00 --3.0000e+01 -1.1714e+03 -2.1584e+00 --3.0000e+01 -1.1429e+03 -2.7641e+00 --3.0000e+01 -1.1143e+03 -3.4312e+00 --3.0000e+01 -1.0857e+03 -4.1695e+00 --3.0000e+01 -1.0571e+03 -4.9904e+00 --3.0000e+01 -1.0286e+03 -5.9080e+00 --3.0000e+01 -1.0000e+03 -6.9394e+00 --3.0000e+01 -9.7143e+02 -8.1059e+00 --3.0000e+01 -9.4286e+02 -9.4338e+00 --3.0000e+01 -9.1429e+02 -1.0956e+01 --3.0000e+01 -8.8571e+02 -1.2715e+01 --3.0000e+01 -8.5714e+02 -1.4763e+01 --3.0000e+01 -8.2857e+02 -1.7166e+01 --3.0000e+01 -8.0000e+02 -2.0008e+01 --3.0000e+01 -7.7143e+02 -2.3388e+01 --3.0000e+01 -7.4286e+02 -2.7426e+01 --3.0000e+01 -7.1429e+02 -3.2244e+01 --3.0000e+01 -6.8571e+02 -3.7942e+01 --3.0000e+01 -6.5714e+02 -4.4545e+01 --3.0000e+01 -6.2857e+02 -5.1932e+01 --3.0000e+01 -6.0000e+02 -5.9791e+01 --3.0000e+01 -5.7143e+02 -6.7662e+01 --3.0000e+01 -5.4286e+02 -7.5081e+01 --3.0000e+01 -5.1429e+02 -8.1727e+01 --3.0000e+01 -4.8571e+02 -8.7469e+01 --3.0000e+01 -4.5714e+02 -9.2327e+01 --3.0000e+01 -4.2857e+02 -9.6400e+01 --3.0000e+01 -4.0000e+02 -9.9809e+01 --3.0000e+01 -3.7143e+02 -1.0267e+02 --3.0000e+01 -3.4286e+02 -1.0509e+02 --3.0000e+01 -3.1429e+02 -1.0716e+02 --3.0000e+01 -2.8571e+02 -1.0892e+02 --3.0000e+01 -2.5714e+02 -1.1046e+02 --3.0000e+01 -2.2857e+02 -1.1179e+02 --3.0000e+01 -2.0000e+02 -1.1296e+02 --3.0000e+01 -1.7143e+02 -1.1399e+02 --3.0000e+01 -1.4286e+02 -1.1490e+02 --3.0000e+01 -1.1429e+02 -1.1571e+02 --3.0000e+01 -8.5714e+01 -1.1643e+02 --3.0000e+01 -5.7143e+01 -1.1706e+02 --3.0000e+01 -2.8571e+01 -1.1757e+02 --3.0000e+01 0.0000e+00 -1.1780e+02 --3.0000e+01 2.8571e+01 -1.1757e+02 --3.0000e+01 5.7143e+01 -1.1706e+02 --3.0000e+01 8.5714e+01 -1.1643e+02 --3.0000e+01 1.1429e+02 -1.1571e+02 --3.0000e+01 1.4286e+02 -1.1490e+02 --3.0000e+01 1.7143e+02 -1.1399e+02 --3.0000e+01 2.0000e+02 -1.1296e+02 --3.0000e+01 2.2857e+02 -1.1179e+02 --3.0000e+01 2.5714e+02 -1.1046e+02 --3.0000e+01 2.8571e+02 -1.0892e+02 --3.0000e+01 3.1429e+02 -1.0716e+02 --3.0000e+01 3.4286e+02 -1.0509e+02 --3.0000e+01 3.7143e+02 -1.0267e+02 --3.0000e+01 4.0000e+02 -9.9809e+01 --3.0000e+01 4.2857e+02 -9.6400e+01 --3.0000e+01 4.5714e+02 -9.2327e+01 --3.0000e+01 4.8571e+02 -8.7469e+01 --3.0000e+01 5.1429e+02 -8.1727e+01 --3.0000e+01 5.4286e+02 -7.5081e+01 --3.0000e+01 5.7143e+02 -6.7662e+01 --3.0000e+01 6.0000e+02 -5.9791e+01 --3.0000e+01 6.2857e+02 -5.1932e+01 --3.0000e+01 6.5714e+02 -4.4545e+01 --3.0000e+01 6.8571e+02 -3.7942e+01 --3.0000e+01 7.1429e+02 -3.2244e+01 --3.0000e+01 7.4286e+02 -2.7426e+01 --3.0000e+01 7.7143e+02 -2.3388e+01 --3.0000e+01 8.0000e+02 -2.0008e+01 --3.0000e+01 8.2857e+02 -1.7166e+01 --3.0000e+01 8.5714e+02 -1.4763e+01 --3.0000e+01 8.8571e+02 -1.2715e+01 --3.0000e+01 9.1429e+02 -1.0956e+01 --3.0000e+01 9.4286e+02 -9.4338e+00 --3.0000e+01 9.7143e+02 -8.1059e+00 --3.0000e+01 1.0000e+03 -6.9394e+00 --3.0000e+01 1.0286e+03 -5.9080e+00 --3.0000e+01 1.0571e+03 -4.9904e+00 --3.0000e+01 1.0857e+03 -4.1695e+00 --3.0000e+01 1.1143e+03 -3.4312e+00 --3.0000e+01 1.1429e+03 -2.7641e+00 --3.0000e+01 1.1714e+03 -2.1584e+00 --3.0000e+01 1.2000e+03 -1.6064e+00 --3.0000e+01 1.2286e+03 -1.1013e+00 --3.0000e+01 1.2571e+03 -6.3755e-01 --3.0000e+01 1.2857e+03 -2.1032e-01 --3.0000e+01 1.3143e+03 1.8446e-01 --3.0000e+01 1.3429e+03 5.5029e-01 --3.0000e+01 1.3714e+03 8.9021e-01 --3.0000e+01 1.4000e+03 1.2068e+00 --3.0000e+01 1.4286e+03 1.5024e+00 --3.0000e+01 1.4571e+03 1.7790e+00 --3.0000e+01 1.4857e+03 2.0384e+00 --3.0000e+01 1.5143e+03 2.2820e+00 --3.0000e+01 1.5429e+03 2.5114e+00 --3.0000e+01 1.5714e+03 2.7276e+00 --3.0000e+01 1.6000e+03 2.9317e+00 --3.0000e+01 1.6286e+03 3.1249e+00 --3.0000e+01 1.6571e+03 3.3078e+00 --3.0000e+01 1.6857e+03 3.4813e+00 --3.0000e+01 1.7143e+03 3.6461e+00 --3.0000e+01 1.7429e+03 3.8028e+00 --3.0000e+01 1.7714e+03 3.9520e+00 --3.0000e+01 1.8000e+03 4.0943e+00 --3.0000e+01 1.8286e+03 4.2300e+00 --3.0000e+01 1.8571e+03 4.3597e+00 --3.0000e+01 1.8857e+03 4.4837e+00 --3.0000e+01 1.9143e+03 4.6024e+00 --3.0000e+01 1.9429e+03 4.7161e+00 --3.0000e+01 1.9714e+03 4.8252e+00 --3.0000e+01 2.0000e+03 4.9299e+00 -0.0000e+00 -2.0000e+03 4.9290e+00 -0.0000e+00 -1.9714e+03 4.8243e+00 -0.0000e+00 -1.9429e+03 4.7152e+00 -0.0000e+00 -1.9143e+03 4.6015e+00 -0.0000e+00 -1.8857e+03 4.4827e+00 -0.0000e+00 -1.8571e+03 4.3586e+00 -0.0000e+00 -1.8286e+03 4.2289e+00 -0.0000e+00 -1.8000e+03 4.0931e+00 -0.0000e+00 -1.7714e+03 3.9507e+00 -0.0000e+00 -1.7429e+03 3.8014e+00 -0.0000e+00 -1.7143e+03 3.6446e+00 -0.0000e+00 -1.6857e+03 3.4797e+00 -0.0000e+00 -1.6571e+03 3.3061e+00 -0.0000e+00 -1.6286e+03 3.1230e+00 -0.0000e+00 -1.6000e+03 2.9298e+00 -0.0000e+00 -1.5714e+03 2.7255e+00 -0.0000e+00 -1.5429e+03 2.5091e+00 -0.0000e+00 -1.5143e+03 2.2796e+00 -0.0000e+00 -1.4857e+03 2.0357e+00 -0.0000e+00 -1.4571e+03 1.7761e+00 -0.0000e+00 -1.4286e+03 1.4993e+00 -0.0000e+00 -1.4000e+03 1.2034e+00 -0.0000e+00 -1.3714e+03 8.8644e-01 -0.0000e+00 -1.3429e+03 5.4616e-01 -0.0000e+00 -1.3143e+03 1.7990e-01 -0.0000e+00 -1.2857e+03 -2.1535e-01 -0.0000e+00 -1.2571e+03 -6.4313e-01 -0.0000e+00 -1.2286e+03 -1.1075e+00 -0.0000e+00 -1.2000e+03 -1.6133e+00 -0.0000e+00 -1.1714e+03 -2.1662e+00 -0.0000e+00 -1.1429e+03 -2.7728e+00 -0.0000e+00 -1.1143e+03 -3.4412e+00 -0.0000e+00 -1.0857e+03 -4.1808e+00 -0.0000e+00 -1.0571e+03 -5.0034e+00 -0.0000e+00 -1.0286e+03 -5.9229e+00 -0.0000e+00 -1.0000e+03 -6.9567e+00 -0.0000e+00 -9.7143e+02 -8.1260e+00 -0.0000e+00 -9.4286e+02 -9.4576e+00 -0.0000e+00 -9.1429e+02 -1.0985e+01 -0.0000e+00 -8.8571e+02 -1.2749e+01 -0.0000e+00 -8.5714e+02 -1.4804e+01 -0.0000e+00 -8.2857e+02 -1.7216e+01 -0.0000e+00 -8.0000e+02 -2.0069e+01 -0.0000e+00 -7.7143e+02 -2.3464e+01 -0.0000e+00 -7.4286e+02 -2.7520e+01 -0.0000e+00 -7.1429e+02 -3.2360e+01 -0.0000e+00 -6.8571e+02 -3.8083e+01 -0.0000e+00 -6.5714e+02 -4.4714e+01 -0.0000e+00 -6.2857e+02 -5.2125e+01 -0.0000e+00 -6.0000e+02 -6.0000e+01 -0.0000e+00 -5.7143e+02 -6.7875e+01 -0.0000e+00 -5.4286e+02 -7.5286e+01 -0.0000e+00 -5.1429e+02 -8.1917e+01 -0.0000e+00 -4.8571e+02 -8.7640e+01 -0.0000e+00 -4.5714e+02 -9.2480e+01 -0.0000e+00 -4.2857e+02 -9.6536e+01 -0.0000e+00 -4.0000e+02 -9.9931e+01 -0.0000e+00 -3.7143e+02 -1.0278e+02 -0.0000e+00 -3.4286e+02 -1.0520e+02 -0.0000e+00 -3.1429e+02 -1.0725e+02 -0.0000e+00 -2.8571e+02 -1.0902e+02 -0.0000e+00 -2.5714e+02 -1.1054e+02 -0.0000e+00 -2.2857e+02 -1.1187e+02 -0.0000e+00 -2.0000e+02 -1.1304e+02 -0.0000e+00 -1.7143e+02 -1.1408e+02 -0.0000e+00 -1.4286e+02 -1.1500e+02 -0.0000e+00 -1.1429e+02 -1.1582e+02 -0.0000e+00 -8.5714e+01 -1.1656e+02 -0.0000e+00 -5.7143e+01 -1.1723e+02 -0.0000e+00 -2.8571e+01 -1.1783e+02 -0.0000e+00 0.0000e+00 -1.1839e+02 -0.0000e+00 2.8571e+01 -1.1783e+02 -0.0000e+00 5.7143e+01 -1.1723e+02 -0.0000e+00 8.5714e+01 -1.1656e+02 -0.0000e+00 1.1429e+02 -1.1582e+02 -0.0000e+00 1.4286e+02 -1.1500e+02 -0.0000e+00 1.7143e+02 -1.1408e+02 -0.0000e+00 2.0000e+02 -1.1304e+02 -0.0000e+00 2.2857e+02 -1.1187e+02 -0.0000e+00 2.5714e+02 -1.1054e+02 -0.0000e+00 2.8571e+02 -1.0902e+02 -0.0000e+00 3.1429e+02 -1.0725e+02 -0.0000e+00 3.4286e+02 -1.0520e+02 -0.0000e+00 3.7143e+02 -1.0278e+02 -0.0000e+00 4.0000e+02 -9.9931e+01 -0.0000e+00 4.2857e+02 -9.6536e+01 -0.0000e+00 4.5714e+02 -9.2480e+01 -0.0000e+00 4.8571e+02 -8.7640e+01 -0.0000e+00 5.1429e+02 -8.1917e+01 -0.0000e+00 5.4286e+02 -7.5286e+01 -0.0000e+00 5.7143e+02 -6.7875e+01 -0.0000e+00 6.0000e+02 -6.0000e+01 -0.0000e+00 6.2857e+02 -5.2125e+01 -0.0000e+00 6.5714e+02 -4.4714e+01 -0.0000e+00 6.8571e+02 -3.8083e+01 -0.0000e+00 7.1429e+02 -3.2360e+01 -0.0000e+00 7.4286e+02 -2.7520e+01 -0.0000e+00 7.7143e+02 -2.3464e+01 -0.0000e+00 8.0000e+02 -2.0069e+01 -0.0000e+00 8.2857e+02 -1.7216e+01 -0.0000e+00 8.5714e+02 -1.4804e+01 -0.0000e+00 8.8571e+02 -1.2749e+01 -0.0000e+00 9.1429e+02 -1.0985e+01 -0.0000e+00 9.4286e+02 -9.4576e+00 -0.0000e+00 9.7143e+02 -8.1260e+00 -0.0000e+00 1.0000e+03 -6.9567e+00 -0.0000e+00 1.0286e+03 -5.9229e+00 -0.0000e+00 1.0571e+03 -5.0034e+00 -0.0000e+00 1.0857e+03 -4.1808e+00 -0.0000e+00 1.1143e+03 -3.4412e+00 -0.0000e+00 1.1429e+03 -2.7728e+00 -0.0000e+00 1.1714e+03 -2.1662e+00 -0.0000e+00 1.2000e+03 -1.6133e+00 -0.0000e+00 1.2286e+03 -1.1075e+00 -0.0000e+00 1.2571e+03 -6.4313e-01 -0.0000e+00 1.2857e+03 -2.1535e-01 -0.0000e+00 1.3143e+03 1.7990e-01 -0.0000e+00 1.3429e+03 5.4616e-01 -0.0000e+00 1.3714e+03 8.8644e-01 -0.0000e+00 1.4000e+03 1.2034e+00 -0.0000e+00 1.4286e+03 1.4993e+00 -0.0000e+00 1.4571e+03 1.7761e+00 -0.0000e+00 1.4857e+03 2.0357e+00 -0.0000e+00 1.5143e+03 2.2796e+00 -0.0000e+00 1.5429e+03 2.5091e+00 -0.0000e+00 1.5714e+03 2.7255e+00 -0.0000e+00 1.6000e+03 2.9298e+00 -0.0000e+00 1.6286e+03 3.1230e+00 -0.0000e+00 1.6571e+03 3.3061e+00 -0.0000e+00 1.6857e+03 3.4797e+00 -0.0000e+00 1.7143e+03 3.6446e+00 -0.0000e+00 1.7429e+03 3.8014e+00 -0.0000e+00 1.7714e+03 3.9507e+00 -0.0000e+00 1.8000e+03 4.0931e+00 -0.0000e+00 1.8286e+03 4.2289e+00 -0.0000e+00 1.8571e+03 4.3586e+00 -0.0000e+00 1.8857e+03 4.4827e+00 -0.0000e+00 1.9143e+03 4.6015e+00 -0.0000e+00 1.9429e+03 4.7152e+00 -0.0000e+00 1.9714e+03 4.8243e+00 -0.0000e+00 2.0000e+03 4.9290e+00 -3.0000e+01 -2.0000e+03 4.9299e+00 -3.0000e+01 -1.9714e+03 4.8252e+00 -3.0000e+01 -1.9429e+03 4.7161e+00 -3.0000e+01 -1.9143e+03 4.6024e+00 -3.0000e+01 -1.8857e+03 4.4837e+00 -3.0000e+01 -1.8571e+03 4.3597e+00 -3.0000e+01 -1.8286e+03 4.2300e+00 -3.0000e+01 -1.8000e+03 4.0943e+00 -3.0000e+01 -1.7714e+03 3.9520e+00 -3.0000e+01 -1.7429e+03 3.8028e+00 -3.0000e+01 -1.7143e+03 3.6461e+00 -3.0000e+01 -1.6857e+03 3.4813e+00 -3.0000e+01 -1.6571e+03 3.3078e+00 -3.0000e+01 -1.6286e+03 3.1249e+00 -3.0000e+01 -1.6000e+03 2.9317e+00 -3.0000e+01 -1.5714e+03 2.7276e+00 -3.0000e+01 -1.5429e+03 2.5114e+00 -3.0000e+01 -1.5143e+03 2.2820e+00 -3.0000e+01 -1.4857e+03 2.0384e+00 -3.0000e+01 -1.4571e+03 1.7790e+00 -3.0000e+01 -1.4286e+03 1.5024e+00 -3.0000e+01 -1.4000e+03 1.2068e+00 -3.0000e+01 -1.3714e+03 8.9021e-01 -3.0000e+01 -1.3429e+03 5.5029e-01 -3.0000e+01 -1.3143e+03 1.8446e-01 -3.0000e+01 -1.2857e+03 -2.1032e-01 -3.0000e+01 -1.2571e+03 -6.3755e-01 -3.0000e+01 -1.2286e+03 -1.1013e+00 -3.0000e+01 -1.2000e+03 -1.6064e+00 -3.0000e+01 -1.1714e+03 -2.1584e+00 -3.0000e+01 -1.1429e+03 -2.7641e+00 -3.0000e+01 -1.1143e+03 -3.4312e+00 -3.0000e+01 -1.0857e+03 -4.1695e+00 -3.0000e+01 -1.0571e+03 -4.9904e+00 -3.0000e+01 -1.0286e+03 -5.9080e+00 -3.0000e+01 -1.0000e+03 -6.9394e+00 -3.0000e+01 -9.7143e+02 -8.1059e+00 -3.0000e+01 -9.4286e+02 -9.4338e+00 -3.0000e+01 -9.1429e+02 -1.0956e+01 -3.0000e+01 -8.8571e+02 -1.2715e+01 -3.0000e+01 -8.5714e+02 -1.4763e+01 -3.0000e+01 -8.2857e+02 -1.7166e+01 -3.0000e+01 -8.0000e+02 -2.0008e+01 -3.0000e+01 -7.7143e+02 -2.3388e+01 -3.0000e+01 -7.4286e+02 -2.7426e+01 -3.0000e+01 -7.1429e+02 -3.2244e+01 -3.0000e+01 -6.8571e+02 -3.7942e+01 -3.0000e+01 -6.5714e+02 -4.4545e+01 -3.0000e+01 -6.2857e+02 -5.1932e+01 -3.0000e+01 -6.0000e+02 -5.9791e+01 -3.0000e+01 -5.7143e+02 -6.7662e+01 -3.0000e+01 -5.4286e+02 -7.5081e+01 -3.0000e+01 -5.1429e+02 -8.1727e+01 -3.0000e+01 -4.8571e+02 -8.7469e+01 -3.0000e+01 -4.5714e+02 -9.2327e+01 -3.0000e+01 -4.2857e+02 -9.6400e+01 -3.0000e+01 -4.0000e+02 -9.9809e+01 -3.0000e+01 -3.7143e+02 -1.0267e+02 -3.0000e+01 -3.4286e+02 -1.0509e+02 -3.0000e+01 -3.1429e+02 -1.0716e+02 -3.0000e+01 -2.8571e+02 -1.0892e+02 -3.0000e+01 -2.5714e+02 -1.1046e+02 -3.0000e+01 -2.2857e+02 -1.1179e+02 -3.0000e+01 -2.0000e+02 -1.1296e+02 -3.0000e+01 -1.7143e+02 -1.1399e+02 -3.0000e+01 -1.4286e+02 -1.1490e+02 -3.0000e+01 -1.1429e+02 -1.1571e+02 -3.0000e+01 -8.5714e+01 -1.1643e+02 -3.0000e+01 -5.7143e+01 -1.1706e+02 -3.0000e+01 -2.8571e+01 -1.1757e+02 -3.0000e+01 0.0000e+00 -1.1780e+02 -3.0000e+01 2.8571e+01 -1.1757e+02 -3.0000e+01 5.7143e+01 -1.1706e+02 -3.0000e+01 8.5714e+01 -1.1643e+02 -3.0000e+01 1.1429e+02 -1.1571e+02 -3.0000e+01 1.4286e+02 -1.1490e+02 -3.0000e+01 1.7143e+02 -1.1399e+02 -3.0000e+01 2.0000e+02 -1.1296e+02 -3.0000e+01 2.2857e+02 -1.1179e+02 -3.0000e+01 2.5714e+02 -1.1046e+02 -3.0000e+01 2.8571e+02 -1.0892e+02 -3.0000e+01 3.1429e+02 -1.0716e+02 -3.0000e+01 3.4286e+02 -1.0509e+02 -3.0000e+01 3.7143e+02 -1.0267e+02 -3.0000e+01 4.0000e+02 -9.9809e+01 -3.0000e+01 4.2857e+02 -9.6400e+01 -3.0000e+01 4.5714e+02 -9.2327e+01 -3.0000e+01 4.8571e+02 -8.7469e+01 -3.0000e+01 5.1429e+02 -8.1727e+01 -3.0000e+01 5.4286e+02 -7.5081e+01 -3.0000e+01 5.7143e+02 -6.7662e+01 -3.0000e+01 6.0000e+02 -5.9791e+01 -3.0000e+01 6.2857e+02 -5.1932e+01 -3.0000e+01 6.5714e+02 -4.4545e+01 -3.0000e+01 6.8571e+02 -3.7942e+01 -3.0000e+01 7.1429e+02 -3.2244e+01 -3.0000e+01 7.4286e+02 -2.7426e+01 -3.0000e+01 7.7143e+02 -2.3388e+01 -3.0000e+01 8.0000e+02 -2.0008e+01 -3.0000e+01 8.2857e+02 -1.7166e+01 -3.0000e+01 8.5714e+02 -1.4763e+01 -3.0000e+01 8.8571e+02 -1.2715e+01 -3.0000e+01 9.1429e+02 -1.0956e+01 -3.0000e+01 9.4286e+02 -9.4338e+00 -3.0000e+01 9.7143e+02 -8.1059e+00 -3.0000e+01 1.0000e+03 -6.9394e+00 -3.0000e+01 1.0286e+03 -5.9080e+00 -3.0000e+01 1.0571e+03 -4.9904e+00 -3.0000e+01 1.0857e+03 -4.1695e+00 -3.0000e+01 1.1143e+03 -3.4312e+00 -3.0000e+01 1.1429e+03 -2.7641e+00 -3.0000e+01 1.1714e+03 -2.1584e+00 -3.0000e+01 1.2000e+03 -1.6064e+00 -3.0000e+01 1.2286e+03 -1.1013e+00 -3.0000e+01 1.2571e+03 -6.3755e-01 -3.0000e+01 1.2857e+03 -2.1032e-01 -3.0000e+01 1.3143e+03 1.8446e-01 -3.0000e+01 1.3429e+03 5.5029e-01 -3.0000e+01 1.3714e+03 8.9021e-01 -3.0000e+01 1.4000e+03 1.2068e+00 -3.0000e+01 1.4286e+03 1.5024e+00 -3.0000e+01 1.4571e+03 1.7790e+00 -3.0000e+01 1.4857e+03 2.0384e+00 -3.0000e+01 1.5143e+03 2.2820e+00 -3.0000e+01 1.5429e+03 2.5114e+00 -3.0000e+01 1.5714e+03 2.7276e+00 -3.0000e+01 1.6000e+03 2.9317e+00 -3.0000e+01 1.6286e+03 3.1249e+00 -3.0000e+01 1.6571e+03 3.3078e+00 -3.0000e+01 1.6857e+03 3.4813e+00 -3.0000e+01 1.7143e+03 3.6461e+00 -3.0000e+01 1.7429e+03 3.8028e+00 -3.0000e+01 1.7714e+03 3.9520e+00 -3.0000e+01 1.8000e+03 4.0943e+00 -3.0000e+01 1.8286e+03 4.2300e+00 -3.0000e+01 1.8571e+03 4.3597e+00 -3.0000e+01 1.8857e+03 4.4837e+00 -3.0000e+01 1.9143e+03 4.6024e+00 -3.0000e+01 1.9429e+03 4.7161e+00 -3.0000e+01 1.9714e+03 4.8252e+00 -3.0000e+01 2.0000e+03 4.9299e+00 -6.0000e+01 -2.0000e+03 4.9323e+00 -6.0000e+01 -1.9714e+03 4.8277e+00 -6.0000e+01 -1.9429e+03 4.7188e+00 -6.0000e+01 -1.9143e+03 4.6053e+00 -6.0000e+01 -1.8857e+03 4.4867e+00 -6.0000e+01 -1.8571e+03 4.3629e+00 -6.0000e+01 -1.8286e+03 4.2334e+00 -6.0000e+01 -1.8000e+03 4.0979e+00 -6.0000e+01 -1.7714e+03 3.9559e+00 -6.0000e+01 -1.7429e+03 3.8069e+00 -6.0000e+01 -1.7143e+03 3.6505e+00 -6.0000e+01 -1.6857e+03 3.4860e+00 -6.0000e+01 -1.6571e+03 3.3128e+00 -6.0000e+01 -1.6286e+03 3.1303e+00 -6.0000e+01 -1.6000e+03 2.9376e+00 -6.0000e+01 -1.5714e+03 2.7339e+00 -6.0000e+01 -1.5429e+03 2.5182e+00 -6.0000e+01 -1.5143e+03 2.2894e+00 -6.0000e+01 -1.4857e+03 2.0464e+00 -6.0000e+01 -1.4571e+03 1.7877e+00 -6.0000e+01 -1.4286e+03 1.5119e+00 -6.0000e+01 -1.4000e+03 1.2171e+00 -6.0000e+01 -1.3714e+03 9.0148e-01 -6.0000e+01 -1.3429e+03 5.6267e-01 -6.0000e+01 -1.3143e+03 1.9809e-01 -6.0000e+01 -1.2857e+03 -1.9527e-01 -6.0000e+01 -1.2571e+03 -6.2087e-01 -6.0000e+01 -1.2286e+03 -1.0828e+00 -6.0000e+01 -1.2000e+03 -1.5857e+00 -6.0000e+01 -1.1714e+03 -2.1352e+00 -6.0000e+01 -1.1429e+03 -2.7379e+00 -6.0000e+01 -1.1143e+03 -3.4016e+00 -6.0000e+01 -1.0857e+03 -4.1358e+00 -6.0000e+01 -1.0571e+03 -4.9518e+00 -6.0000e+01 -1.0286e+03 -5.8635e+00 -6.0000e+01 -1.0000e+03 -6.8878e+00 -6.0000e+01 -9.7143e+02 -8.0457e+00 -6.0000e+01 -9.4286e+02 -9.3630e+00 -6.0000e+01 -9.1429e+02 -1.0872e+01 -6.0000e+01 -8.8571e+02 -1.2615e+01 -6.0000e+01 -8.5714e+02 -1.4642e+01 -6.0000e+01 -8.2857e+02 -1.7019e+01 -6.0000e+01 -8.0000e+02 -1.9826e+01 -6.0000e+01 -7.7143e+02 -2.3164e+01 -6.0000e+01 -7.4286e+02 -2.7148e+01 -6.0000e+01 -7.1429e+02 -3.1899e+01 -6.0000e+01 -6.8571e+02 -3.7520e+01 -6.0000e+01 -6.5714e+02 -4.4042e+01 -6.0000e+01 -6.2857e+02 -5.1357e+01 -6.0000e+01 -6.0000e+02 -5.9167e+01 -6.0000e+01 -5.7143e+02 -6.7024e+01 -6.0000e+01 -5.4286e+02 -7.4464e+01 -6.0000e+01 -5.1429e+02 -8.1155e+01 -6.0000e+01 -4.8571e+02 -8.6952e+01 -6.0000e+01 -4.5714e+02 -9.1865e+01 -6.0000e+01 -4.2857e+02 -9.5987e+01 -6.0000e+01 -4.0000e+02 -9.9438e+01 -6.0000e+01 -3.7143e+02 -1.0234e+02 -6.0000e+01 -3.4286e+02 -1.0479e+02 -6.0000e+01 -3.1429e+02 -1.0687e+02 -6.0000e+01 -2.8571e+02 -1.0865e+02 -6.0000e+01 -2.5714e+02 -1.1019e+02 -6.0000e+01 -2.2857e+02 -1.1153e+02 -6.0000e+01 -2.0000e+02 -1.1270e+02 -6.0000e+01 -1.7143e+02 -1.1372e+02 -6.0000e+01 -1.4286e+02 -1.1462e+02 -6.0000e+01 -1.1429e+02 -1.1540e+02 -6.0000e+01 -8.5714e+01 -1.1608e+02 -6.0000e+01 -5.7143e+01 -1.1663e+02 -6.0000e+01 -2.8571e+01 -1.1702e+02 -6.0000e+01 0.0000e+00 -1.1716e+02 -6.0000e+01 2.8571e+01 -1.1702e+02 -6.0000e+01 5.7143e+01 -1.1663e+02 -6.0000e+01 8.5714e+01 -1.1608e+02 -6.0000e+01 1.1429e+02 -1.1540e+02 -6.0000e+01 1.4286e+02 -1.1462e+02 -6.0000e+01 1.7143e+02 -1.1372e+02 -6.0000e+01 2.0000e+02 -1.1270e+02 -6.0000e+01 2.2857e+02 -1.1153e+02 -6.0000e+01 2.5714e+02 -1.1019e+02 -6.0000e+01 2.8571e+02 -1.0865e+02 -6.0000e+01 3.1429e+02 -1.0687e+02 -6.0000e+01 3.4286e+02 -1.0479e+02 -6.0000e+01 3.7143e+02 -1.0234e+02 -6.0000e+01 4.0000e+02 -9.9438e+01 -6.0000e+01 4.2857e+02 -9.5987e+01 -6.0000e+01 4.5714e+02 -9.1865e+01 -6.0000e+01 4.8571e+02 -8.6952e+01 -6.0000e+01 5.1429e+02 -8.1155e+01 -6.0000e+01 5.4286e+02 -7.4464e+01 -6.0000e+01 5.7143e+02 -6.7024e+01 -6.0000e+01 6.0000e+02 -5.9167e+01 -6.0000e+01 6.2857e+02 -5.1357e+01 -6.0000e+01 6.5714e+02 -4.4042e+01 -6.0000e+01 6.8571e+02 -3.7520e+01 -6.0000e+01 7.1429e+02 -3.1899e+01 -6.0000e+01 7.4286e+02 -2.7148e+01 -6.0000e+01 7.7143e+02 -2.3164e+01 -6.0000e+01 8.0000e+02 -1.9826e+01 -6.0000e+01 8.2857e+02 -1.7019e+01 -6.0000e+01 8.5714e+02 -1.4642e+01 -6.0000e+01 8.8571e+02 -1.2615e+01 -6.0000e+01 9.1429e+02 -1.0872e+01 -6.0000e+01 9.4286e+02 -9.3630e+00 -6.0000e+01 9.7143e+02 -8.0457e+00 -6.0000e+01 1.0000e+03 -6.8878e+00 -6.0000e+01 1.0286e+03 -5.8635e+00 -6.0000e+01 1.0571e+03 -4.9518e+00 -6.0000e+01 1.0857e+03 -4.1358e+00 -6.0000e+01 1.1143e+03 -3.4016e+00 -6.0000e+01 1.1429e+03 -2.7379e+00 -6.0000e+01 1.1714e+03 -2.1352e+00 -6.0000e+01 1.2000e+03 -1.5857e+00 -6.0000e+01 1.2286e+03 -1.0828e+00 -6.0000e+01 1.2571e+03 -6.2087e-01 -6.0000e+01 1.2857e+03 -1.9527e-01 -6.0000e+01 1.3143e+03 1.9809e-01 -6.0000e+01 1.3429e+03 5.6267e-01 -6.0000e+01 1.3714e+03 9.0148e-01 -6.0000e+01 1.4000e+03 1.2171e+00 -6.0000e+01 1.4286e+03 1.5119e+00 -6.0000e+01 1.4571e+03 1.7877e+00 -6.0000e+01 1.4857e+03 2.0464e+00 -6.0000e+01 1.5143e+03 2.2894e+00 -6.0000e+01 1.5429e+03 2.5182e+00 -6.0000e+01 1.5714e+03 2.7339e+00 -6.0000e+01 1.6000e+03 2.9376e+00 -6.0000e+01 1.6286e+03 3.1303e+00 -6.0000e+01 1.6571e+03 3.3128e+00 -6.0000e+01 1.6857e+03 3.4860e+00 -6.0000e+01 1.7143e+03 3.6505e+00 -6.0000e+01 1.7429e+03 3.8069e+00 -6.0000e+01 1.7714e+03 3.9559e+00 -6.0000e+01 1.8000e+03 4.0979e+00 -6.0000e+01 1.8286e+03 4.2334e+00 -6.0000e+01 1.8571e+03 4.3629e+00 -6.0000e+01 1.8857e+03 4.4867e+00 -6.0000e+01 1.9143e+03 4.6053e+00 -6.0000e+01 1.9429e+03 4.7188e+00 -6.0000e+01 1.9714e+03 4.8277e+00 -6.0000e+01 2.0000e+03 4.9323e+00 -9.0000e+01 -2.0000e+03 4.9363e+00 -9.0000e+01 -1.9714e+03 4.8320e+00 -9.0000e+01 -1.9429e+03 4.7233e+00 -9.0000e+01 -1.9143e+03 4.6100e+00 -9.0000e+01 -1.8857e+03 4.4918e+00 -9.0000e+01 -1.8571e+03 4.3683e+00 -9.0000e+01 -1.8286e+03 4.2391e+00 -9.0000e+01 -1.8000e+03 4.1040e+00 -9.0000e+01 -1.7714e+03 3.9624e+00 -9.0000e+01 -1.7429e+03 3.8138e+00 -9.0000e+01 -1.7143e+03 3.6579e+00 -9.0000e+01 -1.6857e+03 3.4939e+00 -9.0000e+01 -1.6571e+03 3.3213e+00 -9.0000e+01 -1.6286e+03 3.1393e+00 -9.0000e+01 -1.6000e+03 2.9473e+00 -9.0000e+01 -1.5714e+03 2.7444e+00 -9.0000e+01 -1.5429e+03 2.5295e+00 -9.0000e+01 -1.5143e+03 2.3016e+00 -9.0000e+01 -1.4857e+03 2.0596e+00 -9.0000e+01 -1.4571e+03 1.8021e+00 -9.0000e+01 -1.4286e+03 1.5276e+00 -9.0000e+01 -1.4000e+03 1.2342e+00 -9.0000e+01 -1.3714e+03 9.2020e-01 -9.0000e+01 -1.3429e+03 5.8321e-01 -9.0000e+01 -1.3143e+03 2.2070e-01 -9.0000e+01 -1.2857e+03 -1.7030e-01 -9.0000e+01 -1.2571e+03 -5.9321e-01 -9.0000e+01 -1.2286e+03 -1.0520e+00 -9.0000e+01 -1.2000e+03 -1.5513e+00 -9.0000e+01 -1.1714e+03 -2.0967e+00 -9.0000e+01 -1.1429e+03 -2.6945e+00 -9.0000e+01 -1.1143e+03 -3.3525e+00 -9.0000e+01 -1.0857e+03 -4.0800e+00 -9.0000e+01 -1.0571e+03 -4.8880e+00 -9.0000e+01 -1.0286e+03 -5.7901e+00 -9.0000e+01 -1.0000e+03 -6.8027e+00 -9.0000e+01 -9.7143e+02 -7.9464e+00 -9.0000e+01 -9.4286e+02 -9.2463e+00 -9.0000e+01 -9.1429e+02 -1.0734e+01 -9.0000e+01 -8.8571e+02 -1.2449e+01 -9.0000e+01 -8.5714e+02 -1.4442e+01 -9.0000e+01 -8.2857e+02 -1.6776e+01 -9.0000e+01 -8.0000e+02 -1.9529e+01 -9.0000e+01 -7.7143e+02 -2.2796e+01 -9.0000e+01 -7.4286e+02 -2.6692e+01 -9.0000e+01 -7.1429e+02 -3.1335e+01 -9.0000e+01 -6.8571e+02 -3.6830e+01 -9.0000e+01 -6.5714e+02 -4.3217e+01 -9.0000e+01 -6.2857e+02 -5.0408e+01 -9.0000e+01 -6.0000e+02 -5.8132e+01 -9.0000e+01 -5.7143e+02 -6.5960e+01 -9.0000e+01 -5.4286e+02 -7.3430e+01 -9.0000e+01 -5.1429e+02 -8.0191e+01 -9.0000e+01 -4.8571e+02 -8.6078e+01 -9.0000e+01 -4.5714e+02 -9.1083e+01 -9.0000e+01 -4.2857e+02 -9.5288e+01 -9.0000e+01 -4.0000e+02 -9.8810e+01 -9.0000e+01 -3.7143e+02 -1.0177e+02 -9.0000e+01 -3.4286e+02 -1.0426e+02 -9.0000e+01 -3.1429e+02 -1.0638e+02 -9.0000e+01 -2.8571e+02 -1.0819e+02 -9.0000e+01 -2.5714e+02 -1.0975e+02 -9.0000e+01 -2.2857e+02 -1.1110e+02 -9.0000e+01 -2.0000e+02 -1.1227e+02 -9.0000e+01 -1.7143e+02 -1.1329e+02 -9.0000e+01 -1.4286e+02 -1.1416e+02 -9.0000e+01 -1.1429e+02 -1.1492e+02 -9.0000e+01 -8.5714e+01 -1.1554e+02 -9.0000e+01 -5.7143e+01 -1.1603e+02 -9.0000e+01 -2.8571e+01 -1.1634e+02 -9.0000e+01 0.0000e+00 -1.1645e+02 -9.0000e+01 2.8571e+01 -1.1634e+02 -9.0000e+01 5.7143e+01 -1.1603e+02 -9.0000e+01 8.5714e+01 -1.1554e+02 -9.0000e+01 1.1429e+02 -1.1492e+02 -9.0000e+01 1.4286e+02 -1.1416e+02 -9.0000e+01 1.7143e+02 -1.1329e+02 -9.0000e+01 2.0000e+02 -1.1227e+02 -9.0000e+01 2.2857e+02 -1.1110e+02 -9.0000e+01 2.5714e+02 -1.0975e+02 -9.0000e+01 2.8571e+02 -1.0819e+02 -9.0000e+01 3.1429e+02 -1.0638e+02 -9.0000e+01 3.4286e+02 -1.0426e+02 -9.0000e+01 3.7143e+02 -1.0177e+02 -9.0000e+01 4.0000e+02 -9.8810e+01 -9.0000e+01 4.2857e+02 -9.5288e+01 -9.0000e+01 4.5714e+02 -9.1083e+01 -9.0000e+01 4.8571e+02 -8.6078e+01 -9.0000e+01 5.1429e+02 -8.0191e+01 -9.0000e+01 5.4286e+02 -7.3430e+01 -9.0000e+01 5.7143e+02 -6.5960e+01 -9.0000e+01 6.0000e+02 -5.8132e+01 -9.0000e+01 6.2857e+02 -5.0408e+01 -9.0000e+01 6.5714e+02 -4.3217e+01 -9.0000e+01 6.8571e+02 -3.6830e+01 -9.0000e+01 7.1429e+02 -3.1335e+01 -9.0000e+01 7.4286e+02 -2.6692e+01 -9.0000e+01 7.7143e+02 -2.2796e+01 -9.0000e+01 8.0000e+02 -1.9529e+01 -9.0000e+01 8.2857e+02 -1.6776e+01 -9.0000e+01 8.5714e+02 -1.4442e+01 -9.0000e+01 8.8571e+02 -1.2449e+01 -9.0000e+01 9.1429e+02 -1.0734e+01 -9.0000e+01 9.4286e+02 -9.2463e+00 -9.0000e+01 9.7143e+02 -7.9464e+00 -9.0000e+01 1.0000e+03 -6.8027e+00 -9.0000e+01 1.0286e+03 -5.7901e+00 -9.0000e+01 1.0571e+03 -4.8880e+00 -9.0000e+01 1.0857e+03 -4.0800e+00 -9.0000e+01 1.1143e+03 -3.3525e+00 -9.0000e+01 1.1429e+03 -2.6945e+00 -9.0000e+01 1.1714e+03 -2.0967e+00 -9.0000e+01 1.2000e+03 -1.5513e+00 -9.0000e+01 1.2286e+03 -1.0520e+00 -9.0000e+01 1.2571e+03 -5.9321e-01 -9.0000e+01 1.2857e+03 -1.7030e-01 -9.0000e+01 1.3143e+03 2.2070e-01 -9.0000e+01 1.3429e+03 5.8321e-01 -9.0000e+01 1.3714e+03 9.2020e-01 -9.0000e+01 1.4000e+03 1.2342e+00 -9.0000e+01 1.4286e+03 1.5276e+00 -9.0000e+01 1.4571e+03 1.8021e+00 -9.0000e+01 1.4857e+03 2.0596e+00 -9.0000e+01 1.5143e+03 2.3016e+00 -9.0000e+01 1.5429e+03 2.5295e+00 -9.0000e+01 1.5714e+03 2.7444e+00 -9.0000e+01 1.6000e+03 2.9473e+00 -9.0000e+01 1.6286e+03 3.1393e+00 -9.0000e+01 1.6571e+03 3.3213e+00 -9.0000e+01 1.6857e+03 3.4939e+00 -9.0000e+01 1.7143e+03 3.6579e+00 -9.0000e+01 1.7429e+03 3.8138e+00 -9.0000e+01 1.7714e+03 3.9624e+00 -9.0000e+01 1.8000e+03 4.1040e+00 -9.0000e+01 1.8286e+03 4.2391e+00 -9.0000e+01 1.8571e+03 4.3683e+00 -9.0000e+01 1.8857e+03 4.4918e+00 -9.0000e+01 1.9143e+03 4.6100e+00 -9.0000e+01 1.9429e+03 4.7233e+00 -9.0000e+01 1.9714e+03 4.8320e+00 -9.0000e+01 2.0000e+03 4.9363e+00 -1.2000e+02 -2.0000e+03 4.9419e+00 -1.2000e+02 -1.9714e+03 4.8379e+00 -1.2000e+02 -1.9429e+03 4.7296e+00 -1.2000e+02 -1.9143e+03 4.6167e+00 -1.2000e+02 -1.8857e+03 4.4989e+00 -1.2000e+02 -1.8571e+03 4.3758e+00 -1.2000e+02 -1.8286e+03 4.2471e+00 -1.2000e+02 -1.8000e+03 4.1124e+00 -1.2000e+02 -1.7714e+03 3.9714e+00 -1.2000e+02 -1.7429e+03 3.8234e+00 -1.2000e+02 -1.7143e+03 3.6681e+00 -1.2000e+02 -1.6857e+03 3.5049e+00 -1.2000e+02 -1.6571e+03 3.3330e+00 -1.2000e+02 -1.6286e+03 3.1520e+00 -1.2000e+02 -1.6000e+03 2.9609e+00 -1.2000e+02 -1.5714e+03 2.7590e+00 -1.2000e+02 -1.5429e+03 2.5452e+00 -1.2000e+02 -1.5143e+03 2.3187e+00 -1.2000e+02 -1.4857e+03 2.0781e+00 -1.2000e+02 -1.4571e+03 1.8222e+00 -1.2000e+02 -1.4286e+03 1.5494e+00 -1.2000e+02 -1.4000e+03 1.2580e+00 -1.2000e+02 -1.3714e+03 9.4624e-01 -1.2000e+02 -1.3429e+03 6.1178e-01 -1.2000e+02 -1.3143e+03 2.5213e-01 -1.2000e+02 -1.2857e+03 -1.3561e-01 -1.2000e+02 -1.2571e+03 -5.5478e-01 -1.2000e+02 -1.2286e+03 -1.0093e+00 -1.2000e+02 -1.2000e+03 -1.5037e+00 -1.2000e+02 -1.1714e+03 -2.0433e+00 -1.2000e+02 -1.1429e+03 -2.6344e+00 -1.2000e+02 -1.1143e+03 -3.2846e+00 -1.2000e+02 -1.0857e+03 -4.0028e+00 -1.2000e+02 -1.0571e+03 -4.7997e+00 -1.2000e+02 -1.0286e+03 -5.6885e+00 -1.2000e+02 -1.0000e+03 -6.6852e+00 -1.2000e+02 -9.7143e+02 -7.8095e+00 -1.2000e+02 -9.4286e+02 -9.0855e+00 -1.2000e+02 -9.1429e+02 -1.0544e+01 -1.2000e+02 -8.8571e+02 -1.2222e+01 -1.2000e+02 -8.5714e+02 -1.4169e+01 -1.2000e+02 -8.2857e+02 -1.6444e+01 -1.2000e+02 -8.0000e+02 -1.9122e+01 -1.2000e+02 -7.7143e+02 -2.2295e+01 -1.2000e+02 -7.4286e+02 -2.6071e+01 -1.2000e+02 -7.1429e+02 -3.0568e+01 -1.2000e+02 -6.8571e+02 -3.5889e+01 -1.2000e+02 -6.5714e+02 -4.2089e+01 -1.2000e+02 -6.2857e+02 -4.9103e+01 -1.2000e+02 -6.0000e+02 -5.6697e+01 -1.2000e+02 -5.7143e+02 -6.4471e+01 -1.2000e+02 -5.4286e+02 -7.1969e+01 -1.2000e+02 -5.1429e+02 -7.8821e+01 -1.2000e+02 -4.8571e+02 -8.4829e+01 -1.2000e+02 -4.5714e+02 -8.9961e+01 -1.2000e+02 -4.2857e+02 -9.4285e+01 -1.2000e+02 -4.0000e+02 -9.7910e+01 -1.2000e+02 -3.7143e+02 -1.0095e+02 -1.2000e+02 -3.4286e+02 -1.0352e+02 -1.2000e+02 -3.1429e+02 -1.0569e+02 -1.2000e+02 -2.8571e+02 -1.0754e+02 -1.2000e+02 -2.5714e+02 -1.0913e+02 -1.2000e+02 -2.2857e+02 -1.1049e+02 -1.2000e+02 -2.0000e+02 -1.1167e+02 -1.2000e+02 -1.7143e+02 -1.1268e+02 -1.2000e+02 -1.4286e+02 -1.1354e+02 -1.2000e+02 -1.1429e+02 -1.1427e+02 -1.2000e+02 -8.5714e+01 -1.1486e+02 -1.2000e+02 -5.7143e+01 -1.1529e+02 -1.2000e+02 -2.8571e+01 -1.1557e+02 -1.2000e+02 0.0000e+00 -1.1566e+02 -1.2000e+02 2.8571e+01 -1.1557e+02 -1.2000e+02 5.7143e+01 -1.1529e+02 -1.2000e+02 8.5714e+01 -1.1486e+02 -1.2000e+02 1.1429e+02 -1.1427e+02 -1.2000e+02 1.4286e+02 -1.1354e+02 -1.2000e+02 1.7143e+02 -1.1268e+02 -1.2000e+02 2.0000e+02 -1.1167e+02 -1.2000e+02 2.2857e+02 -1.1049e+02 -1.2000e+02 2.5714e+02 -1.0913e+02 -1.2000e+02 2.8571e+02 -1.0754e+02 -1.2000e+02 3.1429e+02 -1.0569e+02 -1.2000e+02 3.4286e+02 -1.0352e+02 -1.2000e+02 3.7143e+02 -1.0095e+02 -1.2000e+02 4.0000e+02 -9.7910e+01 -1.2000e+02 4.2857e+02 -9.4285e+01 -1.2000e+02 4.5714e+02 -8.9961e+01 -1.2000e+02 4.8571e+02 -8.4829e+01 -1.2000e+02 5.1429e+02 -7.8821e+01 -1.2000e+02 5.4286e+02 -7.1969e+01 -1.2000e+02 5.7143e+02 -6.4471e+01 -1.2000e+02 6.0000e+02 -5.6697e+01 -1.2000e+02 6.2857e+02 -4.9103e+01 -1.2000e+02 6.5714e+02 -4.2089e+01 -1.2000e+02 6.8571e+02 -3.5889e+01 -1.2000e+02 7.1429e+02 -3.0568e+01 -1.2000e+02 7.4286e+02 -2.6071e+01 -1.2000e+02 7.7143e+02 -2.2295e+01 -1.2000e+02 8.0000e+02 -1.9122e+01 -1.2000e+02 8.2857e+02 -1.6444e+01 -1.2000e+02 8.5714e+02 -1.4169e+01 -1.2000e+02 8.8571e+02 -1.2222e+01 -1.2000e+02 9.1429e+02 -1.0544e+01 -1.2000e+02 9.4286e+02 -9.0855e+00 -1.2000e+02 9.7143e+02 -7.8095e+00 -1.2000e+02 1.0000e+03 -6.6852e+00 -1.2000e+02 1.0286e+03 -5.6885e+00 -1.2000e+02 1.0571e+03 -4.7997e+00 -1.2000e+02 1.0857e+03 -4.0028e+00 -1.2000e+02 1.1143e+03 -3.2846e+00 -1.2000e+02 1.1429e+03 -2.6344e+00 -1.2000e+02 1.1714e+03 -2.0433e+00 -1.2000e+02 1.2000e+03 -1.5037e+00 -1.2000e+02 1.2286e+03 -1.0093e+00 -1.2000e+02 1.2571e+03 -5.5478e-01 -1.2000e+02 1.2857e+03 -1.3561e-01 -1.2000e+02 1.3143e+03 2.5213e-01 -1.2000e+02 1.3429e+03 6.1178e-01 -1.2000e+02 1.3714e+03 9.4624e-01 -1.2000e+02 1.4000e+03 1.2580e+00 -1.2000e+02 1.4286e+03 1.5494e+00 -1.2000e+02 1.4571e+03 1.8222e+00 -1.2000e+02 1.4857e+03 2.0781e+00 -1.2000e+02 1.5143e+03 2.3187e+00 -1.2000e+02 1.5429e+03 2.5452e+00 -1.2000e+02 1.5714e+03 2.7590e+00 -1.2000e+02 1.6000e+03 2.9609e+00 -1.2000e+02 1.6286e+03 3.1520e+00 -1.2000e+02 1.6571e+03 3.3330e+00 -1.2000e+02 1.6857e+03 3.5049e+00 -1.2000e+02 1.7143e+03 3.6681e+00 -1.2000e+02 1.7429e+03 3.8234e+00 -1.2000e+02 1.7714e+03 3.9714e+00 -1.2000e+02 1.8000e+03 4.1124e+00 -1.2000e+02 1.8286e+03 4.2471e+00 -1.2000e+02 1.8571e+03 4.3758e+00 -1.2000e+02 1.8857e+03 4.4989e+00 -1.2000e+02 1.9143e+03 4.6167e+00 -1.2000e+02 1.9429e+03 4.7296e+00 -1.2000e+02 1.9714e+03 4.8379e+00 -1.2000e+02 2.0000e+03 4.9419e+00 -1.5000e+02 -2.0000e+03 4.9491e+00 -1.5000e+02 -1.9714e+03 4.8456e+00 -1.5000e+02 -1.9429e+03 4.7377e+00 -1.5000e+02 -1.9143e+03 4.6252e+00 -1.5000e+02 -1.8857e+03 4.5079e+00 -1.5000e+02 -1.8571e+03 4.3854e+00 -1.5000e+02 -1.8286e+03 4.2573e+00 -1.5000e+02 -1.8000e+03 4.1233e+00 -1.5000e+02 -1.7714e+03 3.9829e+00 -1.5000e+02 -1.7429e+03 3.8357e+00 -1.5000e+02 -1.7143e+03 3.6812e+00 -1.5000e+02 -1.6857e+03 3.5189e+00 -1.5000e+02 -1.6571e+03 3.3481e+00 -1.5000e+02 -1.6286e+03 3.1681e+00 -1.5000e+02 -1.6000e+03 2.9782e+00 -1.5000e+02 -1.5714e+03 2.7776e+00 -1.5000e+02 -1.5429e+03 2.5654e+00 -1.5000e+02 -1.5143e+03 2.3404e+00 -1.5000e+02 -1.4857e+03 2.1017e+00 -1.5000e+02 -1.4571e+03 1.8477e+00 -1.5000e+02 -1.4286e+03 1.5772e+00 -1.5000e+02 -1.4000e+03 1.2884e+00 -1.5000e+02 -1.3714e+03 9.7945e-01 -1.5000e+02 -1.3429e+03 6.4819e-01 -1.5000e+02 -1.3143e+03 2.9217e-01 -1.5000e+02 -1.2857e+03 -9.1437e-02 -1.5000e+02 -1.2571e+03 -5.0589e-01 -1.5000e+02 -1.2286e+03 -9.5498e-01 -1.5000e+02 -1.2000e+03 -1.4431e+00 -1.5000e+02 -1.1714e+03 -1.9755e+00 -1.5000e+02 -1.1429e+03 -2.5582e+00 -1.5000e+02 -1.1143e+03 -3.1985e+00 -1.5000e+02 -1.0857e+03 -3.9049e+00 -1.5000e+02 -1.0571e+03 -4.6880e+00 -1.5000e+02 -1.0286e+03 -5.5603e+00 -1.5000e+02 -1.0000e+03 -6.5370e+00 -1.5000e+02 -9.7143e+02 -7.6370e+00 -1.5000e+02 -9.4286e+02 -8.8833e+00 -1.5000e+02 -9.1429e+02 -1.0305e+01 -1.5000e+02 -8.8571e+02 -1.1938e+01 -1.5000e+02 -8.5714e+02 -1.3827e+01 -1.5000e+02 -8.2857e+02 -1.6030e+01 -1.5000e+02 -8.0000e+02 -1.8616e+01 -1.5000e+02 -7.7143e+02 -2.1672e+01 -1.5000e+02 -7.4286e+02 -2.5301e+01 -1.5000e+02 -7.1429e+02 -2.9616e+01 -1.5000e+02 -6.8571e+02 -3.4723e+01 -1.5000e+02 -6.5714e+02 -4.0685e+01 -1.5000e+02 -6.2857e+02 -4.7468e+01 -1.5000e+02 -6.0000e+02 -5.4880e+01 -1.5000e+02 -5.7143e+02 -6.2563e+01 -1.5000e+02 -5.4286e+02 -7.0074e+01 -1.5000e+02 -5.1429e+02 -7.7025e+01 -1.5000e+02 -4.8571e+02 -8.3182e+01 -1.5000e+02 -4.5714e+02 -8.8476e+01 -1.5000e+02 -4.2857e+02 -9.2953e+01 -1.5000e+02 -4.0000e+02 -9.6713e+01 -1.5000e+02 -3.7143e+02 -9.9869e+01 -1.5000e+02 -3.4286e+02 -1.0252e+02 -1.5000e+02 -3.1429e+02 -1.0477e+02 -1.5000e+02 -2.8571e+02 -1.0668e+02 -1.5000e+02 -2.5714e+02 -1.0831e+02 -1.5000e+02 -2.2857e+02 -1.0970e+02 -1.5000e+02 -2.0000e+02 -1.1089e+02 -1.5000e+02 -1.7143e+02 -1.1191e+02 -1.5000e+02 -1.4286e+02 -1.1276e+02 -1.5000e+02 -1.1429e+02 -1.1347e+02 -1.5000e+02 -8.5714e+01 -1.1403e+02 -1.5000e+02 -5.7143e+01 -1.1444e+02 -1.5000e+02 -2.8571e+01 -1.1469e+02 -1.5000e+02 0.0000e+00 -1.1478e+02 -1.5000e+02 2.8571e+01 -1.1469e+02 -1.5000e+02 5.7143e+01 -1.1444e+02 -1.5000e+02 8.5714e+01 -1.1403e+02 -1.5000e+02 1.1429e+02 -1.1347e+02 -1.5000e+02 1.4286e+02 -1.1276e+02 -1.5000e+02 1.7143e+02 -1.1191e+02 -1.5000e+02 2.0000e+02 -1.1089e+02 -1.5000e+02 2.2857e+02 -1.0970e+02 -1.5000e+02 2.5714e+02 -1.0831e+02 -1.5000e+02 2.8571e+02 -1.0668e+02 -1.5000e+02 3.1429e+02 -1.0477e+02 -1.5000e+02 3.4286e+02 -1.0252e+02 -1.5000e+02 3.7143e+02 -9.9869e+01 -1.5000e+02 4.0000e+02 -9.6713e+01 -1.5000e+02 4.2857e+02 -9.2953e+01 -1.5000e+02 4.5714e+02 -8.8476e+01 -1.5000e+02 4.8571e+02 -8.3182e+01 -1.5000e+02 5.1429e+02 -7.7025e+01 -1.5000e+02 5.4286e+02 -7.0074e+01 -1.5000e+02 5.7143e+02 -6.2563e+01 -1.5000e+02 6.0000e+02 -5.4880e+01 -1.5000e+02 6.2857e+02 -4.7468e+01 -1.5000e+02 6.5714e+02 -4.0685e+01 -1.5000e+02 6.8571e+02 -3.4723e+01 -1.5000e+02 7.1429e+02 -2.9616e+01 -1.5000e+02 7.4286e+02 -2.5301e+01 -1.5000e+02 7.7143e+02 -2.1672e+01 -1.5000e+02 8.0000e+02 -1.8616e+01 -1.5000e+02 8.2857e+02 -1.6030e+01 -1.5000e+02 8.5714e+02 -1.3827e+01 -1.5000e+02 8.8571e+02 -1.1938e+01 -1.5000e+02 9.1429e+02 -1.0305e+01 -1.5000e+02 9.4286e+02 -8.8833e+00 -1.5000e+02 9.7143e+02 -7.6370e+00 -1.5000e+02 1.0000e+03 -6.5370e+00 -1.5000e+02 1.0286e+03 -5.5603e+00 -1.5000e+02 1.0571e+03 -4.6880e+00 -1.5000e+02 1.0857e+03 -3.9049e+00 -1.5000e+02 1.1143e+03 -3.1985e+00 -1.5000e+02 1.1429e+03 -2.5582e+00 -1.5000e+02 1.1714e+03 -1.9755e+00 -1.5000e+02 1.2000e+03 -1.4431e+00 -1.5000e+02 1.2286e+03 -9.5498e-01 -1.5000e+02 1.2571e+03 -5.0589e-01 -1.5000e+02 1.2857e+03 -9.1437e-02 -1.5000e+02 1.3143e+03 2.9217e-01 -1.5000e+02 1.3429e+03 6.4819e-01 -1.5000e+02 1.3714e+03 9.7945e-01 -1.5000e+02 1.4000e+03 1.2884e+00 -1.5000e+02 1.4286e+03 1.5772e+00 -1.5000e+02 1.4571e+03 1.8477e+00 -1.5000e+02 1.4857e+03 2.1017e+00 -1.5000e+02 1.5143e+03 2.3404e+00 -1.5000e+02 1.5429e+03 2.5654e+00 -1.5000e+02 1.5714e+03 2.7776e+00 -1.5000e+02 1.6000e+03 2.9782e+00 -1.5000e+02 1.6286e+03 3.1681e+00 -1.5000e+02 1.6571e+03 3.3481e+00 -1.5000e+02 1.6857e+03 3.5189e+00 -1.5000e+02 1.7143e+03 3.6812e+00 -1.5000e+02 1.7429e+03 3.8357e+00 -1.5000e+02 1.7714e+03 3.9829e+00 -1.5000e+02 1.8000e+03 4.1233e+00 -1.5000e+02 1.8286e+03 4.2573e+00 -1.5000e+02 1.8571e+03 4.3854e+00 -1.5000e+02 1.8857e+03 4.5079e+00 -1.5000e+02 1.9143e+03 4.6252e+00 -1.5000e+02 1.9429e+03 4.7377e+00 -1.5000e+02 1.9714e+03 4.8456e+00 -1.5000e+02 2.0000e+03 4.9491e+00 -1.8000e+02 -2.0000e+03 4.9579e+00 -1.8000e+02 -1.9714e+03 4.8548e+00 -1.8000e+02 -1.9429e+03 4.7475e+00 -1.8000e+02 -1.9143e+03 4.6356e+00 -1.8000e+02 -1.8857e+03 4.5189e+00 -1.8000e+02 -1.8571e+03 4.3970e+00 -1.8000e+02 -1.8286e+03 4.2696e+00 -1.8000e+02 -1.8000e+03 4.1364e+00 -1.8000e+02 -1.7714e+03 3.9969e+00 -1.8000e+02 -1.7429e+03 3.8507e+00 -1.8000e+02 -1.7143e+03 3.6972e+00 -1.8000e+02 -1.6857e+03 3.5359e+00 -1.8000e+02 -1.6571e+03 3.3663e+00 -1.8000e+02 -1.6286e+03 3.1877e+00 -1.8000e+02 -1.6000e+03 2.9993e+00 -1.8000e+02 -1.5714e+03 2.8003e+00 -1.8000e+02 -1.5429e+03 2.5898e+00 -1.8000e+02 -1.5143e+03 2.3668e+00 -1.8000e+02 -1.4857e+03 2.1302e+00 -1.8000e+02 -1.4571e+03 1.8787e+00 -1.8000e+02 -1.4286e+03 1.6109e+00 -1.8000e+02 -1.4000e+03 1.3251e+00 -1.8000e+02 -1.3714e+03 1.0196e+00 -1.8000e+02 -1.3429e+03 6.9220e-01 -1.8000e+02 -1.3143e+03 3.4054e-01 -1.8000e+02 -1.2857e+03 -3.8104e-02 -1.8000e+02 -1.2571e+03 -4.4690e-01 -1.8000e+02 -1.2286e+03 -8.8950e-01 -1.8000e+02 -1.2000e+03 -1.3702e+00 -1.8000e+02 -1.1714e+03 -1.8939e+00 -1.8000e+02 -1.1429e+03 -2.4665e+00 -1.8000e+02 -1.1143e+03 -3.0950e+00 -1.8000e+02 -1.0857e+03 -3.7876e+00 -1.8000e+02 -1.0571e+03 -4.5542e+00 -1.8000e+02 -1.0286e+03 -5.4069e+00 -1.8000e+02 -1.0000e+03 -6.3600e+00 -1.8000e+02 -9.7143e+02 -7.4313e+00 -1.8000e+02 -9.4286e+02 -8.6428e+00 -1.8000e+02 -9.1429e+02 -1.0021e+01 -1.8000e+02 -8.8571e+02 -1.1601e+01 -1.8000e+02 -8.5714e+02 -1.3423e+01 -1.8000e+02 -8.2857e+02 -1.5542e+01 -1.8000e+02 -8.0000e+02 -1.8021e+01 -1.8000e+02 -7.7143e+02 -2.0943e+01 -1.8000e+02 -7.4286e+02 -2.4402e+01 -1.8000e+02 -7.1429e+02 -2.8506e+01 -1.8000e+02 -6.8571e+02 -3.3360e+01 -1.8000e+02 -6.5714e+02 -3.9039e+01 -1.8000e+02 -6.2857e+02 -4.5536e+01 -1.8000e+02 -6.0000e+02 -5.2708e+01 -1.8000e+02 -5.7143e+02 -6.0248e+01 -1.8000e+02 -5.4286e+02 -6.7742e+01 -1.8000e+02 -5.1429e+02 -7.4786e+01 -1.8000e+02 -4.8571e+02 -8.1106e+01 -1.8000e+02 -4.5714e+02 -8.6592e+01 -1.8000e+02 -4.2857e+02 -9.1259e+01 -1.8000e+02 -4.0000e+02 -9.5189e+01 -1.8000e+02 -3.7143e+02 -9.8490e+01 -1.8000e+02 -3.4286e+02 -1.0127e+02 -1.8000e+02 -3.1429e+02 -1.0361e+02 -1.8000e+02 -2.8571e+02 -1.0559e+02 -1.8000e+02 -2.5714e+02 -1.0728e+02 -1.8000e+02 -2.2857e+02 -1.0871e+02 -1.8000e+02 -2.0000e+02 -1.0993e+02 -1.8000e+02 -1.7143e+02 -1.1096e+02 -1.8000e+02 -1.4286e+02 -1.1182e+02 -1.8000e+02 -1.1429e+02 -1.1252e+02 -1.8000e+02 -8.5714e+01 -1.1307e+02 -1.8000e+02 -5.7143e+01 -1.1346e+02 -1.8000e+02 -2.8571e+01 -1.1370e+02 -1.8000e+02 0.0000e+00 -1.1378e+02 -1.8000e+02 2.8571e+01 -1.1370e+02 -1.8000e+02 5.7143e+01 -1.1346e+02 -1.8000e+02 8.5714e+01 -1.1307e+02 -1.8000e+02 1.1429e+02 -1.1252e+02 -1.8000e+02 1.4286e+02 -1.1182e+02 -1.8000e+02 1.7143e+02 -1.1096e+02 -1.8000e+02 2.0000e+02 -1.0993e+02 -1.8000e+02 2.2857e+02 -1.0871e+02 -1.8000e+02 2.5714e+02 -1.0728e+02 -1.8000e+02 2.8571e+02 -1.0559e+02 -1.8000e+02 3.1429e+02 -1.0361e+02 -1.8000e+02 3.4286e+02 -1.0127e+02 -1.8000e+02 3.7143e+02 -9.8490e+01 -1.8000e+02 4.0000e+02 -9.5189e+01 -1.8000e+02 4.2857e+02 -9.1259e+01 -1.8000e+02 4.5714e+02 -8.6592e+01 -1.8000e+02 4.8571e+02 -8.1106e+01 -1.8000e+02 5.1429e+02 -7.4786e+01 -1.8000e+02 5.4286e+02 -6.7742e+01 -1.8000e+02 5.7143e+02 -6.0248e+01 -1.8000e+02 6.0000e+02 -5.2708e+01 -1.8000e+02 6.2857e+02 -4.5536e+01 -1.8000e+02 6.5714e+02 -3.9039e+01 -1.8000e+02 6.8571e+02 -3.3360e+01 -1.8000e+02 7.1429e+02 -2.8506e+01 -1.8000e+02 7.4286e+02 -2.4402e+01 -1.8000e+02 7.7143e+02 -2.0943e+01 -1.8000e+02 8.0000e+02 -1.8021e+01 -1.8000e+02 8.2857e+02 -1.5542e+01 -1.8000e+02 8.5714e+02 -1.3423e+01 -1.8000e+02 8.8571e+02 -1.1601e+01 -1.8000e+02 9.1429e+02 -1.0021e+01 -1.8000e+02 9.4286e+02 -8.6428e+00 -1.8000e+02 9.7143e+02 -7.4313e+00 -1.8000e+02 1.0000e+03 -6.3600e+00 -1.8000e+02 1.0286e+03 -5.4069e+00 -1.8000e+02 1.0571e+03 -4.5542e+00 -1.8000e+02 1.0857e+03 -3.7876e+00 -1.8000e+02 1.1143e+03 -3.0950e+00 -1.8000e+02 1.1429e+03 -2.4665e+00 -1.8000e+02 1.1714e+03 -1.8939e+00 -1.8000e+02 1.2000e+03 -1.3702e+00 -1.8000e+02 1.2286e+03 -8.8950e-01 -1.8000e+02 1.2571e+03 -4.4690e-01 -1.8000e+02 1.2857e+03 -3.8104e-02 -1.8000e+02 1.3143e+03 3.4054e-01 -1.8000e+02 1.3429e+03 6.9220e-01 -1.8000e+02 1.3714e+03 1.0196e+00 -1.8000e+02 1.4000e+03 1.3251e+00 -1.8000e+02 1.4286e+03 1.6109e+00 -1.8000e+02 1.4571e+03 1.8787e+00 -1.8000e+02 1.4857e+03 2.1302e+00 -1.8000e+02 1.5143e+03 2.3668e+00 -1.8000e+02 1.5429e+03 2.5898e+00 -1.8000e+02 1.5714e+03 2.8003e+00 -1.8000e+02 1.6000e+03 2.9993e+00 -1.8000e+02 1.6286e+03 3.1877e+00 -1.8000e+02 1.6571e+03 3.3663e+00 -1.8000e+02 1.6857e+03 3.5359e+00 -1.8000e+02 1.7143e+03 3.6972e+00 -1.8000e+02 1.7429e+03 3.8507e+00 -1.8000e+02 1.7714e+03 3.9969e+00 -1.8000e+02 1.8000e+03 4.1364e+00 -1.8000e+02 1.8286e+03 4.2696e+00 -1.8000e+02 1.8571e+03 4.3970e+00 -1.8000e+02 1.8857e+03 4.5189e+00 -1.8000e+02 1.9143e+03 4.6356e+00 -1.8000e+02 1.9429e+03 4.7475e+00 -1.8000e+02 1.9714e+03 4.8548e+00 -1.8000e+02 2.0000e+03 4.9579e+00 -2.1000e+02 -2.0000e+03 4.9682e+00 -2.1000e+02 -1.9714e+03 4.8657e+00 -2.1000e+02 -1.9429e+03 4.7590e+00 -2.1000e+02 -1.9143e+03 4.6478e+00 -2.1000e+02 -1.8857e+03 4.5318e+00 -2.1000e+02 -1.8571e+03 4.4107e+00 -2.1000e+02 -1.8286e+03 4.2842e+00 -2.1000e+02 -1.8000e+03 4.1519e+00 -2.1000e+02 -1.7714e+03 4.0133e+00 -2.1000e+02 -1.7429e+03 3.8682e+00 -2.1000e+02 -1.7143e+03 3.7159e+00 -2.1000e+02 -1.6857e+03 3.5559e+00 -2.1000e+02 -1.6571e+03 3.3877e+00 -2.1000e+02 -1.6286e+03 3.2106e+00 -2.1000e+02 -1.6000e+03 3.0239e+00 -2.1000e+02 -1.5714e+03 2.8268e+00 -2.1000e+02 -1.5429e+03 2.6184e+00 -2.1000e+02 -1.5143e+03 2.3977e+00 -2.1000e+02 -1.4857e+03 2.1636e+00 -2.1000e+02 -1.4571e+03 1.9150e+00 -2.1000e+02 -1.4286e+03 1.6503e+00 -2.1000e+02 -1.4000e+03 1.3681e+00 -2.1000e+02 -1.3714e+03 1.0665e+00 -2.1000e+02 -1.3429e+03 7.4354e-01 -2.1000e+02 -1.3143e+03 3.9693e-01 -2.1000e+02 -1.2857e+03 2.4017e-02 -2.1000e+02 -1.2571e+03 -3.7824e-01 -2.1000e+02 -1.2286e+03 -8.1336e-01 -2.1000e+02 -1.2000e+03 -1.2854e+00 -2.1000e+02 -1.1714e+03 -1.7992e+00 -2.1000e+02 -1.1429e+03 -2.3603e+00 -2.1000e+02 -1.1143e+03 -2.9752e+00 -2.1000e+02 -1.0857e+03 -3.6520e+00 -2.1000e+02 -1.0571e+03 -4.3998e+00 -2.1000e+02 -1.0286e+03 -5.2301e+00 -2.1000e+02 -1.0000e+03 -6.1564e+00 -2.1000e+02 -9.7143e+02 -7.1954e+00 -2.1000e+02 -9.4286e+02 -8.3674e+00 -2.1000e+02 -9.1429e+02 -9.6976e+00 -2.1000e+02 -8.8571e+02 -1.1217e+01 -2.1000e+02 -8.5714e+02 -1.2965e+01 -2.1000e+02 -8.2857e+02 -1.4990e+01 -2.1000e+02 -8.0000e+02 -1.7351e+01 -2.1000e+02 -7.7143e+02 -2.0123e+01 -2.1000e+02 -7.4286e+02 -2.3394e+01 -2.1000e+02 -7.1429e+02 -2.7264e+01 -2.1000e+02 -6.8571e+02 -3.1836e+01 -2.1000e+02 -6.5714e+02 -3.7191e+01 -2.1000e+02 -6.2857e+02 -4.3351e+01 -2.1000e+02 -6.0000e+02 -5.0220e+01 -2.1000e+02 -5.7143e+02 -5.7553e+01 -2.1000e+02 -5.4286e+02 -6.4976e+01 -2.1000e+02 -5.1429e+02 -7.2086e+01 -2.1000e+02 -4.8571e+02 -7.8573e+01 -2.1000e+02 -4.5714e+02 -8.4273e+01 -2.1000e+02 -4.2857e+02 -8.9162e+01 -2.1000e+02 -4.0000e+02 -9.3298e+01 -2.1000e+02 -3.7143e+02 -9.6779e+01 -2.1000e+02 -3.4286e+02 -9.9706e+01 -2.1000e+02 -3.1429e+02 -1.0217e+02 -2.1000e+02 -2.8571e+02 -1.0425e+02 -2.1000e+02 -2.5714e+02 -1.0602e+02 -2.1000e+02 -2.2857e+02 -1.0751e+02 -2.1000e+02 -2.0000e+02 -1.0877e+02 -2.1000e+02 -1.7143e+02 -1.0982e+02 -2.1000e+02 -1.4286e+02 -1.1070e+02 -2.1000e+02 -1.1429e+02 -1.1140e+02 -2.1000e+02 -8.5714e+01 -1.1195e+02 -2.1000e+02 -5.7143e+01 -1.1234e+02 -2.1000e+02 -2.8571e+01 -1.1257e+02 -2.1000e+02 0.0000e+00 -1.1265e+02 -2.1000e+02 2.8571e+01 -1.1257e+02 -2.1000e+02 5.7143e+01 -1.1234e+02 -2.1000e+02 8.5714e+01 -1.1195e+02 -2.1000e+02 1.1429e+02 -1.1140e+02 -2.1000e+02 1.4286e+02 -1.1070e+02 -2.1000e+02 1.7143e+02 -1.0982e+02 -2.1000e+02 2.0000e+02 -1.0877e+02 -2.1000e+02 2.2857e+02 -1.0751e+02 -2.1000e+02 2.5714e+02 -1.0602e+02 -2.1000e+02 2.8571e+02 -1.0425e+02 -2.1000e+02 3.1429e+02 -1.0217e+02 -2.1000e+02 3.4286e+02 -9.9706e+01 -2.1000e+02 3.7143e+02 -9.6779e+01 -2.1000e+02 4.0000e+02 -9.3298e+01 -2.1000e+02 4.2857e+02 -8.9162e+01 -2.1000e+02 4.5714e+02 -8.4273e+01 -2.1000e+02 4.8571e+02 -7.8573e+01 -2.1000e+02 5.1429e+02 -7.2086e+01 -2.1000e+02 5.4286e+02 -6.4976e+01 -2.1000e+02 5.7143e+02 -5.7553e+01 -2.1000e+02 6.0000e+02 -5.0220e+01 -2.1000e+02 6.2857e+02 -4.3351e+01 -2.1000e+02 6.5714e+02 -3.7191e+01 -2.1000e+02 6.8571e+02 -3.1836e+01 -2.1000e+02 7.1429e+02 -2.7264e+01 -2.1000e+02 7.4286e+02 -2.3394e+01 -2.1000e+02 7.7143e+02 -2.0123e+01 -2.1000e+02 8.0000e+02 -1.7351e+01 -2.1000e+02 8.2857e+02 -1.4990e+01 -2.1000e+02 8.5714e+02 -1.2965e+01 -2.1000e+02 8.8571e+02 -1.1217e+01 -2.1000e+02 9.1429e+02 -9.6976e+00 -2.1000e+02 9.4286e+02 -8.3674e+00 -2.1000e+02 9.7143e+02 -7.1954e+00 -2.1000e+02 1.0000e+03 -6.1564e+00 -2.1000e+02 1.0286e+03 -5.2301e+00 -2.1000e+02 1.0571e+03 -4.3998e+00 -2.1000e+02 1.0857e+03 -3.6520e+00 -2.1000e+02 1.1143e+03 -2.9752e+00 -2.1000e+02 1.1429e+03 -2.3603e+00 -2.1000e+02 1.1714e+03 -1.7992e+00 -2.1000e+02 1.2000e+03 -1.2854e+00 -2.1000e+02 1.2286e+03 -8.1336e-01 -2.1000e+02 1.2571e+03 -3.7824e-01 -2.1000e+02 1.2857e+03 2.4017e-02 -2.1000e+02 1.3143e+03 3.9693e-01 -2.1000e+02 1.3429e+03 7.4354e-01 -2.1000e+02 1.3714e+03 1.0665e+00 -2.1000e+02 1.4000e+03 1.3681e+00 -2.1000e+02 1.4286e+03 1.6503e+00 -2.1000e+02 1.4571e+03 1.9150e+00 -2.1000e+02 1.4857e+03 2.1636e+00 -2.1000e+02 1.5143e+03 2.3977e+00 -2.1000e+02 1.5429e+03 2.6184e+00 -2.1000e+02 1.5714e+03 2.8268e+00 -2.1000e+02 1.6000e+03 3.0239e+00 -2.1000e+02 1.6286e+03 3.2106e+00 -2.1000e+02 1.6571e+03 3.3877e+00 -2.1000e+02 1.6857e+03 3.5559e+00 -2.1000e+02 1.7143e+03 3.7159e+00 -2.1000e+02 1.7429e+03 3.8682e+00 -2.1000e+02 1.7714e+03 4.0133e+00 -2.1000e+02 1.8000e+03 4.1519e+00 -2.1000e+02 1.8286e+03 4.2842e+00 -2.1000e+02 1.8571e+03 4.4107e+00 -2.1000e+02 1.8857e+03 4.5318e+00 -2.1000e+02 1.9143e+03 4.6478e+00 -2.1000e+02 1.9429e+03 4.7590e+00 -2.1000e+02 1.9714e+03 4.8657e+00 -2.1000e+02 2.0000e+03 4.9682e+00 -2.4000e+02 -2.0000e+03 4.9801e+00 -2.4000e+02 -1.9714e+03 4.8782e+00 -2.4000e+02 -1.9429e+03 4.7722e+00 -2.4000e+02 -1.9143e+03 4.6617e+00 -2.4000e+02 -1.8857e+03 4.5466e+00 -2.4000e+02 -1.8571e+03 4.4264e+00 -2.4000e+02 -1.8286e+03 4.3008e+00 -2.4000e+02 -1.8000e+03 4.1696e+00 -2.4000e+02 -1.7714e+03 4.0322e+00 -2.4000e+02 -1.7429e+03 3.8882e+00 -2.4000e+02 -1.7143e+03 3.7373e+00 -2.4000e+02 -1.6857e+03 3.5788e+00 -2.4000e+02 -1.6571e+03 3.4122e+00 -2.4000e+02 -1.6286e+03 3.2369e+00 -2.4000e+02 -1.6000e+03 3.0521e+00 -2.4000e+02 -1.5714e+03 2.8571e+00 -2.4000e+02 -1.5429e+03 2.6510e+00 -2.4000e+02 -1.5143e+03 2.4329e+00 -2.4000e+02 -1.4857e+03 2.2018e+00 -2.4000e+02 -1.4571e+03 1.9563e+00 -2.4000e+02 -1.4286e+03 1.6952e+00 -2.4000e+02 -1.4000e+03 1.4169e+00 -2.4000e+02 -1.3714e+03 1.1198e+00 -2.4000e+02 -1.3429e+03 8.0189e-01 -2.4000e+02 -1.3143e+03 4.6097e-01 -2.4000e+02 -1.2857e+03 9.4502e-02 -2.4000e+02 -1.2571e+03 -3.0042e-01 -2.4000e+02 -1.2286e+03 -7.2715e-01 -2.4000e+02 -1.2000e+03 -1.1896e+00 -2.4000e+02 -1.1714e+03 -1.6922e+00 -2.4000e+02 -1.1429e+03 -2.2404e+00 -2.4000e+02 -1.1143e+03 -2.8403e+00 -2.4000e+02 -1.0857e+03 -3.4995e+00 -2.4000e+02 -1.0571e+03 -4.2265e+00 -2.4000e+02 -1.0286e+03 -5.0321e+00 -2.4000e+02 -1.0000e+03 -5.9288e+00 -2.4000e+02 -9.7143e+02 -6.9322e+00 -2.4000e+02 -9.4286e+02 -8.0611e+00 -2.4000e+02 -9.1429e+02 -9.3385e+00 -2.4000e+02 -8.8571e+02 -1.0793e+01 -2.4000e+02 -8.5714e+02 -1.2460e+01 -2.4000e+02 -8.2857e+02 -1.4384e+01 -2.4000e+02 -8.0000e+02 -1.6618e+01 -2.4000e+02 -7.7143e+02 -1.9230e+01 -2.4000e+02 -7.4286e+02 -2.2300e+01 -2.4000e+02 -7.1429e+02 -2.5920e+01 -2.4000e+02 -6.8571e+02 -3.0187e+01 -2.4000e+02 -6.5714e+02 -3.5188e+01 -2.4000e+02 -6.2857e+02 -4.0964e+01 -2.4000e+02 -6.0000e+02 -4.7468e+01 -2.4000e+02 -5.7143e+02 -5.4518e+01 -2.4000e+02 -5.4286e+02 -6.1797e+01 -2.4000e+02 -5.1429e+02 -6.8923e+01 -2.4000e+02 -4.8571e+02 -7.5553e+01 -2.4000e+02 -4.5714e+02 -8.1476e+01 -2.4000e+02 -4.2857e+02 -8.6613e+01 -2.4000e+02 -4.0000e+02 -9.0991e+01 -2.4000e+02 -3.7143e+02 -9.4689e+01 -2.4000e+02 -3.4286e+02 -9.7801e+01 -2.4000e+02 -3.1429e+02 -1.0042e+02 -2.4000e+02 -2.8571e+02 -1.0263e+02 -2.4000e+02 -2.5714e+02 -1.0449e+02 -2.4000e+02 -2.2857e+02 -1.0606e+02 -2.4000e+02 -2.0000e+02 -1.0738e+02 -2.4000e+02 -1.7143e+02 -1.0847e+02 -2.4000e+02 -1.4286e+02 -1.0938e+02 -2.4000e+02 -1.1429e+02 -1.1010e+02 -2.4000e+02 -8.5714e+01 -1.1066e+02 -2.4000e+02 -5.7143e+01 -1.1105e+02 -2.4000e+02 -2.8571e+01 -1.1128e+02 -2.4000e+02 0.0000e+00 -1.1136e+02 -2.4000e+02 2.8571e+01 -1.1128e+02 -2.4000e+02 5.7143e+01 -1.1105e+02 -2.4000e+02 8.5714e+01 -1.1066e+02 -2.4000e+02 1.1429e+02 -1.1010e+02 -2.4000e+02 1.4286e+02 -1.0938e+02 -2.4000e+02 1.7143e+02 -1.0847e+02 -2.4000e+02 2.0000e+02 -1.0738e+02 -2.4000e+02 2.2857e+02 -1.0606e+02 -2.4000e+02 2.5714e+02 -1.0449e+02 -2.4000e+02 2.8571e+02 -1.0263e+02 -2.4000e+02 3.1429e+02 -1.0042e+02 -2.4000e+02 3.4286e+02 -9.7801e+01 -2.4000e+02 3.7143e+02 -9.4689e+01 -2.4000e+02 4.0000e+02 -9.0991e+01 -2.4000e+02 4.2857e+02 -8.6613e+01 -2.4000e+02 4.5714e+02 -8.1476e+01 -2.4000e+02 4.8571e+02 -7.5553e+01 -2.4000e+02 5.1429e+02 -6.8923e+01 -2.4000e+02 5.4286e+02 -6.1797e+01 -2.4000e+02 5.7143e+02 -5.4518e+01 -2.4000e+02 6.0000e+02 -4.7468e+01 -2.4000e+02 6.2857e+02 -4.0964e+01 -2.4000e+02 6.5714e+02 -3.5188e+01 -2.4000e+02 6.8571e+02 -3.0187e+01 -2.4000e+02 7.1429e+02 -2.5920e+01 -2.4000e+02 7.4286e+02 -2.2300e+01 -2.4000e+02 7.7143e+02 -1.9230e+01 -2.4000e+02 8.0000e+02 -1.6618e+01 -2.4000e+02 8.2857e+02 -1.4384e+01 -2.4000e+02 8.5714e+02 -1.2460e+01 -2.4000e+02 8.8571e+02 -1.0793e+01 -2.4000e+02 9.1429e+02 -9.3385e+00 -2.4000e+02 9.4286e+02 -8.0611e+00 -2.4000e+02 9.7143e+02 -6.9322e+00 -2.4000e+02 1.0000e+03 -5.9288e+00 -2.4000e+02 1.0286e+03 -5.0321e+00 -2.4000e+02 1.0571e+03 -4.2265e+00 -2.4000e+02 1.0857e+03 -3.4995e+00 -2.4000e+02 1.1143e+03 -2.8403e+00 -2.4000e+02 1.1429e+03 -2.2404e+00 -2.4000e+02 1.1714e+03 -1.6922e+00 -2.4000e+02 1.2000e+03 -1.1896e+00 -2.4000e+02 1.2286e+03 -7.2715e-01 -2.4000e+02 1.2571e+03 -3.0042e-01 -2.4000e+02 1.2857e+03 9.4502e-02 -2.4000e+02 1.3143e+03 4.6097e-01 -2.4000e+02 1.3429e+03 8.0189e-01 -2.4000e+02 1.3714e+03 1.1198e+00 -2.4000e+02 1.4000e+03 1.4169e+00 -2.4000e+02 1.4286e+03 1.6952e+00 -2.4000e+02 1.4571e+03 1.9563e+00 -2.4000e+02 1.4857e+03 2.2018e+00 -2.4000e+02 1.5143e+03 2.4329e+00 -2.4000e+02 1.5429e+03 2.6510e+00 -2.4000e+02 1.5714e+03 2.8571e+00 -2.4000e+02 1.6000e+03 3.0521e+00 -2.4000e+02 1.6286e+03 3.2369e+00 -2.4000e+02 1.6571e+03 3.4122e+00 -2.4000e+02 1.6857e+03 3.5788e+00 -2.4000e+02 1.7143e+03 3.7373e+00 -2.4000e+02 1.7429e+03 3.8882e+00 -2.4000e+02 1.7714e+03 4.0322e+00 -2.4000e+02 1.8000e+03 4.1696e+00 -2.4000e+02 1.8286e+03 4.3008e+00 -2.4000e+02 1.8571e+03 4.4264e+00 -2.4000e+02 1.8857e+03 4.5466e+00 -2.4000e+02 1.9143e+03 4.6617e+00 -2.4000e+02 1.9429e+03 4.7722e+00 -2.4000e+02 1.9714e+03 4.8782e+00 -2.4000e+02 2.0000e+03 4.9801e+00 -2.7000e+02 -2.0000e+03 4.9934e+00 -2.7000e+02 -1.9714e+03 4.8923e+00 -2.7000e+02 -1.9429e+03 4.7870e+00 -2.7000e+02 -1.9143e+03 4.6774e+00 -2.7000e+02 -1.8857e+03 4.5632e+00 -2.7000e+02 -1.8571e+03 4.4440e+00 -2.7000e+02 -1.8286e+03 4.3195e+00 -2.7000e+02 -1.8000e+03 4.1894e+00 -2.7000e+02 -1.7714e+03 4.0533e+00 -2.7000e+02 -1.7429e+03 3.9108e+00 -2.7000e+02 -1.7143e+03 3.7613e+00 -2.7000e+02 -1.6857e+03 3.6045e+00 -2.7000e+02 -1.6571e+03 3.4397e+00 -2.7000e+02 -1.6286e+03 3.2663e+00 -2.7000e+02 -1.6000e+03 3.0837e+00 -2.7000e+02 -1.5714e+03 2.8910e+00 -2.7000e+02 -1.5429e+03 2.6876e+00 -2.7000e+02 -1.5143e+03 2.4724e+00 -2.7000e+02 -1.4857e+03 2.2444e+00 -2.7000e+02 -1.4571e+03 2.0024e+00 -2.7000e+02 -1.4286e+03 1.7453e+00 -2.7000e+02 -1.4000e+03 1.4714e+00 -2.7000e+02 -1.3714e+03 1.1793e+00 -2.7000e+02 -1.3429e+03 8.6691e-01 -2.7000e+02 -1.3143e+03 5.3225e-01 -2.7000e+02 -1.2857e+03 1.7288e-01 -2.7000e+02 -1.2571e+03 -2.1397e-01 -2.7000e+02 -1.2286e+03 -6.3149e-01 -2.7000e+02 -1.2000e+03 -1.0834e+00 -2.7000e+02 -1.1714e+03 -1.5739e+00 -2.7000e+02 -1.1429e+03 -2.1080e+00 -2.7000e+02 -1.1143e+03 -2.6915e+00 -2.7000e+02 -1.0857e+03 -3.3315e+00 -2.7000e+02 -1.0571e+03 -4.0360e+00 -2.7000e+02 -1.0286e+03 -4.8149e+00 -2.7000e+02 -1.0000e+03 -5.6799e+00 -2.7000e+02 -9.7143e+02 -6.6452e+00 -2.7000e+02 -9.4286e+02 -7.7279e+00 -2.7000e+02 -9.1429e+02 -8.9492e+00 -2.7000e+02 -8.8571e+02 -1.0335e+01 -2.7000e+02 -8.5714e+02 -1.1917e+01 -2.7000e+02 -8.2857e+02 -1.3735e+01 -2.7000e+02 -8.0000e+02 -1.5837e+01 -2.7000e+02 -7.7143e+02 -1.8282e+01 -2.7000e+02 -7.4286e+02 -2.1143e+01 -2.7000e+02 -7.1429e+02 -2.4503e+01 -2.7000e+02 -6.8571e+02 -2.8451e+01 -2.7000e+02 -6.5714e+02 -3.3075e+01 -2.7000e+02 -6.2857e+02 -3.8433e+01 -2.7000e+02 -6.0000e+02 -4.4514e+01 -2.7000e+02 -5.7143e+02 -5.1202e+01 -2.7000e+02 -5.4286e+02 -5.8248e+01 -2.7000e+02 -5.1429e+02 -6.5308e+01 -2.7000e+02 -4.8571e+02 -7.2033e+01 -2.7000e+02 -4.5714e+02 -7.8162e+01 -2.7000e+02 -4.2857e+02 -8.3561e+01 -2.7000e+02 -4.0000e+02 -8.8210e+01 -2.7000e+02 -3.7143e+02 -9.2160e+01 -2.7000e+02 -3.4286e+02 -9.5495e+01 -2.7000e+02 -3.1429e+02 -9.8302e+01 -2.7000e+02 -2.8571e+02 -1.0067e+02 -2.7000e+02 -2.5714e+02 -1.0265e+02 -2.7000e+02 -2.2857e+02 -1.0432e+02 -2.7000e+02 -2.0000e+02 -1.0572e+02 -2.7000e+02 -1.7143e+02 -1.0688e+02 -2.7000e+02 -1.4286e+02 -1.0782e+02 -2.7000e+02 -1.1429e+02 -1.0858e+02 -2.7000e+02 -8.5714e+01 -1.0915e+02 -2.7000e+02 -5.7143e+01 -1.0956e+02 -2.7000e+02 -2.8571e+01 -1.0980e+02 -2.7000e+02 0.0000e+00 -1.0988e+02 -2.7000e+02 2.8571e+01 -1.0980e+02 -2.7000e+02 5.7143e+01 -1.0956e+02 -2.7000e+02 8.5714e+01 -1.0915e+02 -2.7000e+02 1.1429e+02 -1.0858e+02 -2.7000e+02 1.4286e+02 -1.0782e+02 -2.7000e+02 1.7143e+02 -1.0688e+02 -2.7000e+02 2.0000e+02 -1.0572e+02 -2.7000e+02 2.2857e+02 -1.0432e+02 -2.7000e+02 2.5714e+02 -1.0265e+02 -2.7000e+02 2.8571e+02 -1.0067e+02 -2.7000e+02 3.1429e+02 -9.8302e+01 -2.7000e+02 3.4286e+02 -9.5495e+01 -2.7000e+02 3.7143e+02 -9.2160e+01 -2.7000e+02 4.0000e+02 -8.8210e+01 -2.7000e+02 4.2857e+02 -8.3561e+01 -2.7000e+02 4.5714e+02 -7.8162e+01 -2.7000e+02 4.8571e+02 -7.2033e+01 -2.7000e+02 5.1429e+02 -6.5308e+01 -2.7000e+02 5.4286e+02 -5.8248e+01 -2.7000e+02 5.7143e+02 -5.1202e+01 -2.7000e+02 6.0000e+02 -4.4514e+01 -2.7000e+02 6.2857e+02 -3.8433e+01 -2.7000e+02 6.5714e+02 -3.3075e+01 -2.7000e+02 6.8571e+02 -2.8451e+01 -2.7000e+02 7.1429e+02 -2.4503e+01 -2.7000e+02 7.4286e+02 -2.1143e+01 -2.7000e+02 7.7143e+02 -1.8282e+01 -2.7000e+02 8.0000e+02 -1.5837e+01 -2.7000e+02 8.2857e+02 -1.3735e+01 -2.7000e+02 8.5714e+02 -1.1917e+01 -2.7000e+02 8.8571e+02 -1.0335e+01 -2.7000e+02 9.1429e+02 -8.9492e+00 -2.7000e+02 9.4286e+02 -7.7279e+00 -2.7000e+02 9.7143e+02 -6.6452e+00 -2.7000e+02 1.0000e+03 -5.6799e+00 -2.7000e+02 1.0286e+03 -4.8149e+00 -2.7000e+02 1.0571e+03 -4.0360e+00 -2.7000e+02 1.0857e+03 -3.3315e+00 -2.7000e+02 1.1143e+03 -2.6915e+00 -2.7000e+02 1.1429e+03 -2.1080e+00 -2.7000e+02 1.1714e+03 -1.5739e+00 -2.7000e+02 1.2000e+03 -1.0834e+00 -2.7000e+02 1.2286e+03 -6.3149e-01 -2.7000e+02 1.2571e+03 -2.1397e-01 -2.7000e+02 1.2857e+03 1.7288e-01 -2.7000e+02 1.3143e+03 5.3225e-01 -2.7000e+02 1.3429e+03 8.6691e-01 -2.7000e+02 1.3714e+03 1.1793e+00 -2.7000e+02 1.4000e+03 1.4714e+00 -2.7000e+02 1.4286e+03 1.7453e+00 -2.7000e+02 1.4571e+03 2.0024e+00 -2.7000e+02 1.4857e+03 2.2444e+00 -2.7000e+02 1.5143e+03 2.4724e+00 -2.7000e+02 1.5429e+03 2.6876e+00 -2.7000e+02 1.5714e+03 2.8910e+00 -2.7000e+02 1.6000e+03 3.0837e+00 -2.7000e+02 1.6286e+03 3.2663e+00 -2.7000e+02 1.6571e+03 3.4397e+00 -2.7000e+02 1.6857e+03 3.6045e+00 -2.7000e+02 1.7143e+03 3.7613e+00 -2.7000e+02 1.7429e+03 3.9108e+00 -2.7000e+02 1.7714e+03 4.0533e+00 -2.7000e+02 1.8000e+03 4.1894e+00 -2.7000e+02 1.8286e+03 4.3195e+00 -2.7000e+02 1.8571e+03 4.4440e+00 -2.7000e+02 1.8857e+03 4.5632e+00 -2.7000e+02 1.9143e+03 4.6774e+00 -2.7000e+02 1.9429e+03 4.7870e+00 -2.7000e+02 1.9714e+03 4.8923e+00 -2.7000e+02 2.0000e+03 4.9934e+00 -3.0000e+02 -2.0000e+03 5.0081e+00 -3.0000e+02 -1.9714e+03 4.9079e+00 -3.0000e+02 -1.9429e+03 4.8035e+00 -3.0000e+02 -1.9143e+03 4.6949e+00 -3.0000e+02 -1.8857e+03 4.5816e+00 -3.0000e+02 -1.8571e+03 4.4635e+00 -3.0000e+02 -1.8286e+03 4.3403e+00 -3.0000e+02 -1.8000e+03 4.2114e+00 -3.0000e+02 -1.7714e+03 4.0767e+00 -3.0000e+02 -1.7429e+03 3.9357e+00 -3.0000e+02 -1.7143e+03 3.7879e+00 -3.0000e+02 -1.6857e+03 3.6328e+00 -3.0000e+02 -1.6571e+03 3.4700e+00 -3.0000e+02 -1.6286e+03 3.2988e+00 -3.0000e+02 -1.6000e+03 3.1185e+00 -3.0000e+02 -1.5714e+03 2.9285e+00 -3.0000e+02 -1.5429e+03 2.7278e+00 -3.0000e+02 -1.5143e+03 2.5158e+00 -3.0000e+02 -1.4857e+03 2.2913e+00 -3.0000e+02 -1.4571e+03 2.0532e+00 -3.0000e+02 -1.4286e+03 1.8004e+00 -3.0000e+02 -1.4000e+03 1.5313e+00 -3.0000e+02 -1.3714e+03 1.2445e+00 -3.0000e+02 -1.3429e+03 9.3820e-01 -3.0000e+02 -1.3143e+03 6.1034e-01 -3.0000e+02 -1.2857e+03 2.5865e-01 -3.0000e+02 -1.2571e+03 -1.1948e-01 -3.0000e+02 -1.2286e+03 -5.2707e-01 -3.0000e+02 -1.2000e+03 -9.6758e-01 -3.0000e+02 -1.1714e+03 -1.4450e+00 -3.0000e+02 -1.1429e+03 -1.9640e+00 -3.0000e+02 -1.1143e+03 -2.5301e+00 -3.0000e+02 -1.0857e+03 -3.1497e+00 -3.0000e+02 -1.0571e+03 -3.8303e+00 -3.0000e+02 -1.0286e+03 -4.5809e+00 -3.0000e+02 -1.0000e+03 -5.4124e+00 -3.0000e+02 -9.7143e+02 -6.3375e+00 -3.0000e+02 -9.4286e+02 -7.3720e+00 -3.0000e+02 -9.1429e+02 -8.5348e+00 -3.0000e+02 -8.8571e+02 -9.8491e+00 -3.0000e+02 -8.5714e+02 -1.1343e+01 -3.0000e+02 -8.2857e+02 -1.3052e+01 -3.0000e+02 -8.0000e+02 -1.5019e+01 -3.0000e+02 -7.7143e+02 -1.7295e+01 -3.0000e+02 -7.4286e+02 -1.9944e+01 -3.0000e+02 -7.1429e+02 -2.3040e+01 -3.0000e+02 -6.8571e+02 -2.6665e+01 -3.0000e+02 -6.5714e+02 -3.0902e+01 -3.0000e+02 -6.2857e+02 -3.5816e+01 -3.0000e+02 -6.0000e+02 -4.1430e+01 -3.0000e+02 -5.7143e+02 -4.7681e+01 -3.0000e+02 -5.4286e+02 -5.4393e+01 -3.0000e+02 -5.1429e+02 -6.1284e+01 -3.0000e+02 -4.8571e+02 -6.8019e+01 -3.0000e+02 -4.5714e+02 -7.4307e+01 -3.0000e+02 -4.2857e+02 -7.9957e+01 -3.0000e+02 -4.0000e+02 -8.4893e+01 -3.0000e+02 -3.7143e+02 -8.9128e+01 -3.0000e+02 -3.4286e+02 -9.2722e+01 -3.0000e+02 -3.1429e+02 -9.5755e+01 -3.0000e+02 -2.8571e+02 -9.8309e+01 -3.0000e+02 -2.5714e+02 -1.0045e+02 -3.0000e+02 -2.2857e+02 -1.0225e+02 -3.0000e+02 -2.0000e+02 -1.0375e+02 -3.0000e+02 -1.7143e+02 -1.0499e+02 -3.0000e+02 -1.4286e+02 -1.0599e+02 -3.0000e+02 -1.1429e+02 -1.0679e+02 -3.0000e+02 -8.5714e+01 -1.0740e+02 -3.0000e+02 -5.7143e+01 -1.0783e+02 -3.0000e+02 -2.8571e+01 -1.0808e+02 -3.0000e+02 0.0000e+00 -1.0817e+02 -3.0000e+02 2.8571e+01 -1.0808e+02 -3.0000e+02 5.7143e+01 -1.0783e+02 -3.0000e+02 8.5714e+01 -1.0740e+02 -3.0000e+02 1.1429e+02 -1.0679e+02 -3.0000e+02 1.4286e+02 -1.0599e+02 -3.0000e+02 1.7143e+02 -1.0499e+02 -3.0000e+02 2.0000e+02 -1.0375e+02 -3.0000e+02 2.2857e+02 -1.0225e+02 -3.0000e+02 2.5714e+02 -1.0045e+02 -3.0000e+02 2.8571e+02 -9.8309e+01 -3.0000e+02 3.1429e+02 -9.5755e+01 -3.0000e+02 3.4286e+02 -9.2722e+01 -3.0000e+02 3.7143e+02 -8.9128e+01 -3.0000e+02 4.0000e+02 -8.4893e+01 -3.0000e+02 4.2857e+02 -7.9957e+01 -3.0000e+02 4.5714e+02 -7.4307e+01 -3.0000e+02 4.8571e+02 -6.8019e+01 -3.0000e+02 5.1429e+02 -6.1284e+01 -3.0000e+02 5.4286e+02 -5.4393e+01 -3.0000e+02 5.7143e+02 -4.7681e+01 -3.0000e+02 6.0000e+02 -4.1430e+01 -3.0000e+02 6.2857e+02 -3.5816e+01 -3.0000e+02 6.5714e+02 -3.0902e+01 -3.0000e+02 6.8571e+02 -2.6665e+01 -3.0000e+02 7.1429e+02 -2.3040e+01 -3.0000e+02 7.4286e+02 -1.9944e+01 -3.0000e+02 7.7143e+02 -1.7295e+01 -3.0000e+02 8.0000e+02 -1.5019e+01 -3.0000e+02 8.2857e+02 -1.3052e+01 -3.0000e+02 8.5714e+02 -1.1343e+01 -3.0000e+02 8.8571e+02 -9.8491e+00 -3.0000e+02 9.1429e+02 -8.5348e+00 -3.0000e+02 9.4286e+02 -7.3720e+00 -3.0000e+02 9.7143e+02 -6.3375e+00 -3.0000e+02 1.0000e+03 -5.4124e+00 -3.0000e+02 1.0286e+03 -4.5809e+00 -3.0000e+02 1.0571e+03 -3.8303e+00 -3.0000e+02 1.0857e+03 -3.1497e+00 -3.0000e+02 1.1143e+03 -2.5301e+00 -3.0000e+02 1.1429e+03 -1.9640e+00 -3.0000e+02 1.1714e+03 -1.4450e+00 -3.0000e+02 1.2000e+03 -9.6758e-01 -3.0000e+02 1.2286e+03 -5.2707e-01 -3.0000e+02 1.2571e+03 -1.1948e-01 -3.0000e+02 1.2857e+03 2.5865e-01 -3.0000e+02 1.3143e+03 6.1034e-01 -3.0000e+02 1.3429e+03 9.3820e-01 -3.0000e+02 1.3714e+03 1.2445e+00 -3.0000e+02 1.4000e+03 1.5313e+00 -3.0000e+02 1.4286e+03 1.8004e+00 -3.0000e+02 1.4571e+03 2.0532e+00 -3.0000e+02 1.4857e+03 2.2913e+00 -3.0000e+02 1.5143e+03 2.5158e+00 -3.0000e+02 1.5429e+03 2.7278e+00 -3.0000e+02 1.5714e+03 2.9285e+00 -3.0000e+02 1.6000e+03 3.1185e+00 -3.0000e+02 1.6286e+03 3.2988e+00 -3.0000e+02 1.6571e+03 3.4700e+00 -3.0000e+02 1.6857e+03 3.6328e+00 -3.0000e+02 1.7143e+03 3.7879e+00 -3.0000e+02 1.7429e+03 3.9357e+00 -3.0000e+02 1.7714e+03 4.0767e+00 -3.0000e+02 1.8000e+03 4.2114e+00 -3.0000e+02 1.8286e+03 4.3403e+00 -3.0000e+02 1.8571e+03 4.4635e+00 -3.0000e+02 1.8857e+03 4.5816e+00 -3.0000e+02 1.9143e+03 4.6949e+00 -3.0000e+02 1.9429e+03 4.8035e+00 -3.0000e+02 1.9714e+03 4.9079e+00 -3.0000e+02 2.0000e+03 5.0081e+00 -3.3000e+02 -2.0000e+03 5.0243e+00 -3.3000e+02 -1.9714e+03 4.9249e+00 -3.3000e+02 -1.9429e+03 4.8215e+00 -3.3000e+02 -1.9143e+03 4.7139e+00 -3.3000e+02 -1.8857e+03 4.6018e+00 -3.3000e+02 -1.8571e+03 4.4849e+00 -3.3000e+02 -1.8286e+03 4.3629e+00 -3.3000e+02 -1.8000e+03 4.2355e+00 -3.3000e+02 -1.7714e+03 4.1023e+00 -3.3000e+02 -1.7429e+03 3.9629e+00 -3.3000e+02 -1.7143e+03 3.8169e+00 -3.3000e+02 -1.6857e+03 3.6638e+00 -3.3000e+02 -1.6571e+03 3.5031e+00 -3.3000e+02 -1.6286e+03 3.3342e+00 -3.3000e+02 -1.6000e+03 3.1565e+00 -3.3000e+02 -1.5714e+03 2.9692e+00 -3.3000e+02 -1.5429e+03 2.7717e+00 -3.3000e+02 -1.5143e+03 2.5630e+00 -3.3000e+02 -1.4857e+03 2.3423e+00 -3.3000e+02 -1.4571e+03 2.1084e+00 -3.3000e+02 -1.4286e+03 1.8601e+00 -3.3000e+02 -1.4000e+03 1.5963e+00 -3.3000e+02 -1.3714e+03 1.3152e+00 -3.3000e+02 -1.3429e+03 1.0154e+00 -3.3000e+02 -1.3143e+03 6.9477e-01 -3.3000e+02 -1.2857e+03 3.5128e-01 -3.3000e+02 -1.2571e+03 -1.7572e-02 -3.3000e+02 -1.2286e+03 -4.1460e-01 -3.3000e+02 -1.2000e+03 -8.4306e-01 -3.3000e+02 -1.1714e+03 -1.3067e+00 -3.3000e+02 -1.1429e+03 -1.8098e+00 -3.3000e+02 -1.1143e+03 -2.3575e+00 -3.3000e+02 -1.0857e+03 -2.9556e+00 -3.3000e+02 -1.0571e+03 -3.6112e+00 -3.3000e+02 -1.0286e+03 -4.3324e+00 -3.3000e+02 -1.0000e+03 -5.1290e+00 -3.3000e+02 -9.7143e+02 -6.0127e+00 -3.3000e+02 -9.4286e+02 -6.9975e+00 -3.3000e+02 -9.1429e+02 -8.1004e+00 -3.3000e+02 -8.8571e+02 -9.3419e+00 -3.3000e+02 -8.5714e+02 -1.0747e+01 -3.3000e+02 -8.2857e+02 -1.2346e+01 -3.3000e+02 -8.0000e+02 -1.4177e+01 -3.3000e+02 -7.7143e+02 -1.6285e+01 -3.3000e+02 -7.4286e+02 -1.8724e+01 -3.3000e+02 -7.1429e+02 -2.1559e+01 -3.3000e+02 -6.8571e+02 -2.4863e+01 -3.3000e+02 -6.5714e+02 -2.8711e+01 -3.3000e+02 -6.2857e+02 -3.3173e+01 -3.3000e+02 -6.0000e+02 -3.8290e+01 -3.3000e+02 -5.7143e+02 -4.4043e+01 -3.3000e+02 -5.4286e+02 -5.0326e+01 -3.3000e+02 -5.1429e+02 -5.6925e+01 -3.3000e+02 -4.8571e+02 -6.3554e+01 -3.3000e+02 -4.5714e+02 -6.9913e+01 -3.3000e+02 -4.2857e+02 -7.5768e+01 -3.3000e+02 -4.0000e+02 -8.0982e+01 -3.3000e+02 -3.7143e+02 -8.5519e+01 -3.3000e+02 -3.4286e+02 -8.9404e+01 -3.3000e+02 -3.1429e+02 -9.2701e+01 -3.3000e+02 -2.8571e+02 -9.5482e+01 -3.3000e+02 -2.5714e+02 -9.7819e+01 -3.3000e+02 -2.2857e+02 -9.9775e+01 -3.3000e+02 -2.0000e+02 -1.0140e+02 -3.3000e+02 -1.7143e+02 -1.0274e+02 -3.3000e+02 -1.4286e+02 -1.0383e+02 -3.3000e+02 -1.1429e+02 -1.0469e+02 -3.3000e+02 -8.5714e+01 -1.0534e+02 -3.3000e+02 -5.7143e+01 -1.0580e+02 -3.3000e+02 -2.8571e+01 -1.0607e+02 -3.3000e+02 0.0000e+00 -1.0616e+02 -3.3000e+02 2.8571e+01 -1.0607e+02 -3.3000e+02 5.7143e+01 -1.0580e+02 -3.3000e+02 8.5714e+01 -1.0534e+02 -3.3000e+02 1.1429e+02 -1.0469e+02 -3.3000e+02 1.4286e+02 -1.0383e+02 -3.3000e+02 1.7143e+02 -1.0274e+02 -3.3000e+02 2.0000e+02 -1.0140e+02 -3.3000e+02 2.2857e+02 -9.9775e+01 -3.3000e+02 2.5714e+02 -9.7819e+01 -3.3000e+02 2.8571e+02 -9.5482e+01 -3.3000e+02 3.1429e+02 -9.2701e+01 -3.3000e+02 3.4286e+02 -8.9404e+01 -3.3000e+02 3.7143e+02 -8.5519e+01 -3.3000e+02 4.0000e+02 -8.0982e+01 -3.3000e+02 4.2857e+02 -7.5768e+01 -3.3000e+02 4.5714e+02 -6.9913e+01 -3.3000e+02 4.8571e+02 -6.3554e+01 -3.3000e+02 5.1429e+02 -5.6925e+01 -3.3000e+02 5.4286e+02 -5.0326e+01 -3.3000e+02 5.7143e+02 -4.4043e+01 -3.3000e+02 6.0000e+02 -3.8290e+01 -3.3000e+02 6.2857e+02 -3.3173e+01 -3.3000e+02 6.5714e+02 -2.8711e+01 -3.3000e+02 6.8571e+02 -2.4863e+01 -3.3000e+02 7.1429e+02 -2.1559e+01 -3.3000e+02 7.4286e+02 -1.8724e+01 -3.3000e+02 7.7143e+02 -1.6285e+01 -3.3000e+02 8.0000e+02 -1.4177e+01 -3.3000e+02 8.2857e+02 -1.2346e+01 -3.3000e+02 8.5714e+02 -1.0747e+01 -3.3000e+02 8.8571e+02 -9.3419e+00 -3.3000e+02 9.1429e+02 -8.1004e+00 -3.3000e+02 9.4286e+02 -6.9975e+00 -3.3000e+02 9.7143e+02 -6.0127e+00 -3.3000e+02 1.0000e+03 -5.1290e+00 -3.3000e+02 1.0286e+03 -4.3324e+00 -3.3000e+02 1.0571e+03 -3.6112e+00 -3.3000e+02 1.0857e+03 -2.9556e+00 -3.3000e+02 1.1143e+03 -2.3575e+00 -3.3000e+02 1.1429e+03 -1.8098e+00 -3.3000e+02 1.1714e+03 -1.3067e+00 -3.3000e+02 1.2000e+03 -8.4306e-01 -3.3000e+02 1.2286e+03 -4.1460e-01 -3.3000e+02 1.2571e+03 -1.7572e-02 -3.3000e+02 1.2857e+03 3.5128e-01 -3.3000e+02 1.3143e+03 6.9477e-01 -3.3000e+02 1.3429e+03 1.0154e+00 -3.3000e+02 1.3714e+03 1.3152e+00 -3.3000e+02 1.4000e+03 1.5963e+00 -3.3000e+02 1.4286e+03 1.8601e+00 -3.3000e+02 1.4571e+03 2.1084e+00 -3.3000e+02 1.4857e+03 2.3423e+00 -3.3000e+02 1.5143e+03 2.5630e+00 -3.3000e+02 1.5429e+03 2.7717e+00 -3.3000e+02 1.5714e+03 2.9692e+00 -3.3000e+02 1.6000e+03 3.1565e+00 -3.3000e+02 1.6286e+03 3.3342e+00 -3.3000e+02 1.6571e+03 3.5031e+00 -3.3000e+02 1.6857e+03 3.6638e+00 -3.3000e+02 1.7143e+03 3.8169e+00 -3.3000e+02 1.7429e+03 3.9629e+00 -3.3000e+02 1.7714e+03 4.1023e+00 -3.3000e+02 1.8000e+03 4.2355e+00 -3.3000e+02 1.8286e+03 4.3629e+00 -3.3000e+02 1.8571e+03 4.4849e+00 -3.3000e+02 1.8857e+03 4.6018e+00 -3.3000e+02 1.9143e+03 4.7139e+00 -3.3000e+02 1.9429e+03 4.8215e+00 -3.3000e+02 1.9714e+03 4.9249e+00 -3.3000e+02 2.0000e+03 5.0243e+00 -3.6000e+02 -2.0000e+03 5.0419e+00 -3.6000e+02 -1.9714e+03 4.9435e+00 -3.6000e+02 -1.9429e+03 4.8411e+00 -3.6000e+02 -1.9143e+03 4.7346e+00 -3.6000e+02 -1.8857e+03 4.6237e+00 -3.6000e+02 -1.8571e+03 4.5081e+00 -3.6000e+02 -1.8286e+03 4.3875e+00 -3.6000e+02 -1.8000e+03 4.2616e+00 -3.6000e+02 -1.7714e+03 4.1300e+00 -3.6000e+02 -1.7429e+03 3.9924e+00 -3.6000e+02 -1.7143e+03 3.8483e+00 -3.6000e+02 -1.6857e+03 3.6973e+00 -3.6000e+02 -1.6571e+03 3.5388e+00 -3.6000e+02 -1.6286e+03 3.3724e+00 -3.6000e+02 -1.6000e+03 3.1974e+00 -3.6000e+02 -1.5714e+03 3.0132e+00 -3.6000e+02 -1.5429e+03 2.8189e+00 -3.6000e+02 -1.5143e+03 2.6139e+00 -3.6000e+02 -1.4857e+03 2.3971e+00 -3.6000e+02 -1.4571e+03 2.1677e+00 -3.6000e+02 -1.4286e+03 1.9244e+00 -3.6000e+02 -1.4000e+03 1.6659e+00 -3.6000e+02 -1.3714e+03 1.3910e+00 -3.6000e+02 -1.3429e+03 1.0980e+00 -3.6000e+02 -1.3143e+03 7.8505e-01 -3.6000e+02 -1.2857e+03 4.5020e-01 -3.6000e+02 -1.2571e+03 9.1122e-02 -3.6000e+02 -1.2286e+03 -2.9483e-01 -3.6000e+02 -1.2000e+03 -7.1066e-01 -3.6000e+02 -1.1714e+03 -1.1599e+00 -3.6000e+02 -1.1429e+03 -1.6464e+00 -3.6000e+02 -1.1143e+03 -2.1749e+00 -3.6000e+02 -1.0857e+03 -2.7509e+00 -3.6000e+02 -1.0571e+03 -3.3806e+00 -3.6000e+02 -1.0286e+03 -4.0716e+00 -3.6000e+02 -1.0000e+03 -4.8325e+00 -3.6000e+02 -9.7143e+02 -5.6739e+00 -3.6000e+02 -9.4286e+02 -6.6084e+00 -3.6000e+02 -9.1429e+02 -7.6508e+00 -3.6000e+02 -8.8571e+02 -8.8192e+00 -3.6000e+02 -8.5714e+02 -1.0136e+01 -3.6000e+02 -8.2857e+02 -1.1626e+01 -3.6000e+02 -8.0000e+02 -1.3324e+01 -3.6000e+02 -7.7143e+02 -1.5266e+01 -3.6000e+02 -7.4286e+02 -1.7501e+01 -3.6000e+02 -7.1429e+02 -2.0082e+01 -3.6000e+02 -6.8571e+02 -2.3073e+01 -3.6000e+02 -6.5714e+02 -2.6542e+01 -3.6000e+02 -6.2857e+02 -3.0556e+01 -3.6000e+02 -6.0000e+02 -3.5164e+01 -3.6000e+02 -5.7143e+02 -4.0381e+01 -3.6000e+02 -5.4286e+02 -4.6154e+01 -3.6000e+02 -5.1429e+02 -5.2343e+01 -3.6000e+02 -4.8571e+02 -5.8724e+01 -3.6000e+02 -4.5714e+02 -6.5027e+01 -3.6000e+02 -4.2857e+02 -7.0993e+01 -3.6000e+02 -4.0000e+02 -7.6439e+01 -3.6000e+02 -3.7143e+02 -8.1268e+01 -3.6000e+02 -3.4286e+02 -8.5461e+01 -3.6000e+02 -3.1429e+02 -8.9051e+01 -3.6000e+02 -2.8571e+02 -9.2096e+01 -3.6000e+02 -2.5714e+02 -9.4663e+01 -3.6000e+02 -2.2857e+02 -9.6812e+01 -3.6000e+02 -2.0000e+02 -9.8598e+01 -3.6000e+02 -1.7143e+02 -1.0007e+02 -3.6000e+02 -1.4286e+02 -1.0126e+02 -3.6000e+02 -1.1429e+02 -1.0220e+02 -3.6000e+02 -8.5714e+01 -1.0291e+02 -3.6000e+02 -5.7143e+01 -1.0341e+02 -3.6000e+02 -2.8571e+01 -1.0370e+02 -3.6000e+02 0.0000e+00 -1.0380e+02 -3.6000e+02 2.8571e+01 -1.0370e+02 -3.6000e+02 5.7143e+01 -1.0341e+02 -3.6000e+02 8.5714e+01 -1.0291e+02 -3.6000e+02 1.1429e+02 -1.0220e+02 -3.6000e+02 1.4286e+02 -1.0126e+02 -3.6000e+02 1.7143e+02 -1.0007e+02 -3.6000e+02 2.0000e+02 -9.8598e+01 -3.6000e+02 2.2857e+02 -9.6812e+01 -3.6000e+02 2.5714e+02 -9.4663e+01 -3.6000e+02 2.8571e+02 -9.2096e+01 -3.6000e+02 3.1429e+02 -8.9051e+01 -3.6000e+02 3.4286e+02 -8.5461e+01 -3.6000e+02 3.7143e+02 -8.1268e+01 -3.6000e+02 4.0000e+02 -7.6439e+01 -3.6000e+02 4.2857e+02 -7.0993e+01 -3.6000e+02 4.5714e+02 -6.5027e+01 -3.6000e+02 4.8571e+02 -5.8724e+01 -3.6000e+02 5.1429e+02 -5.2343e+01 -3.6000e+02 5.4286e+02 -4.6154e+01 -3.6000e+02 5.7143e+02 -4.0381e+01 -3.6000e+02 6.0000e+02 -3.5164e+01 -3.6000e+02 6.2857e+02 -3.0556e+01 -3.6000e+02 6.5714e+02 -2.6542e+01 -3.6000e+02 6.8571e+02 -2.3073e+01 -3.6000e+02 7.1429e+02 -2.0082e+01 -3.6000e+02 7.4286e+02 -1.7501e+01 -3.6000e+02 7.7143e+02 -1.5266e+01 -3.6000e+02 8.0000e+02 -1.3324e+01 -3.6000e+02 8.2857e+02 -1.1626e+01 -3.6000e+02 8.5714e+02 -1.0136e+01 -3.6000e+02 8.8571e+02 -8.8192e+00 -3.6000e+02 9.1429e+02 -7.6508e+00 -3.6000e+02 9.4286e+02 -6.6084e+00 -3.6000e+02 9.7143e+02 -5.6739e+00 -3.6000e+02 1.0000e+03 -4.8325e+00 -3.6000e+02 1.0286e+03 -4.0716e+00 -3.6000e+02 1.0571e+03 -3.3806e+00 -3.6000e+02 1.0857e+03 -2.7509e+00 -3.6000e+02 1.1143e+03 -2.1749e+00 -3.6000e+02 1.1429e+03 -1.6464e+00 -3.6000e+02 1.1714e+03 -1.1599e+00 -3.6000e+02 1.2000e+03 -7.1066e-01 -3.6000e+02 1.2286e+03 -2.9483e-01 -3.6000e+02 1.2571e+03 9.1122e-02 -3.6000e+02 1.2857e+03 4.5020e-01 -3.6000e+02 1.3143e+03 7.8505e-01 -3.6000e+02 1.3429e+03 1.0980e+00 -3.6000e+02 1.3714e+03 1.3910e+00 -3.6000e+02 1.4000e+03 1.6659e+00 -3.6000e+02 1.4286e+03 1.9244e+00 -3.6000e+02 1.4571e+03 2.1677e+00 -3.6000e+02 1.4857e+03 2.3971e+00 -3.6000e+02 1.5143e+03 2.6139e+00 -3.6000e+02 1.5429e+03 2.8189e+00 -3.6000e+02 1.5714e+03 3.0132e+00 -3.6000e+02 1.6000e+03 3.1974e+00 -3.6000e+02 1.6286e+03 3.3724e+00 -3.6000e+02 1.6571e+03 3.5388e+00 -3.6000e+02 1.6857e+03 3.6973e+00 -3.6000e+02 1.7143e+03 3.8483e+00 -3.6000e+02 1.7429e+03 3.9924e+00 -3.6000e+02 1.7714e+03 4.1300e+00 -3.6000e+02 1.8000e+03 4.2616e+00 -3.6000e+02 1.8286e+03 4.3875e+00 -3.6000e+02 1.8571e+03 4.5081e+00 -3.6000e+02 1.8857e+03 4.6237e+00 -3.6000e+02 1.9143e+03 4.7346e+00 -3.6000e+02 1.9429e+03 4.8411e+00 -3.6000e+02 1.9714e+03 4.9435e+00 -3.6000e+02 2.0000e+03 5.0419e+00 -3.9000e+02 -2.0000e+03 5.0608e+00 -3.9000e+02 -1.9714e+03 4.9634e+00 -3.9000e+02 -1.9429e+03 4.8622e+00 -3.9000e+02 -1.9143e+03 4.7568e+00 -3.9000e+02 -1.8857e+03 4.6472e+00 -3.9000e+02 -1.8571e+03 4.5329e+00 -3.9000e+02 -1.8286e+03 4.4138e+00 -3.9000e+02 -1.8000e+03 4.2895e+00 -3.9000e+02 -1.7714e+03 4.1597e+00 -3.9000e+02 -1.7429e+03 4.0240e+00 -3.9000e+02 -1.7143e+03 3.8819e+00 -3.9000e+02 -1.6857e+03 3.7331e+00 -3.9000e+02 -1.6571e+03 3.5771e+00 -3.9000e+02 -1.6286e+03 3.4133e+00 -3.9000e+02 -1.6000e+03 3.2412e+00 -3.9000e+02 -1.5714e+03 3.0601e+00 -3.9000e+02 -1.5429e+03 2.8694e+00 -3.9000e+02 -1.5143e+03 2.6682e+00 -3.9000e+02 -1.4857e+03 2.4556e+00 -3.9000e+02 -1.4571e+03 2.2308e+00 -3.9000e+02 -1.4286e+03 1.9927e+00 -3.9000e+02 -1.4000e+03 1.7400e+00 -3.9000e+02 -1.3714e+03 1.4715e+00 -3.9000e+02 -1.3429e+03 1.1856e+00 -3.9000e+02 -1.3143e+03 8.8070e-01 -3.9000e+02 -1.2857e+03 5.5486e-01 -3.9000e+02 -1.2571e+03 2.0595e-01 -3.9000e+02 -1.2286e+03 -1.6850e-01 -3.9000e+02 -1.2000e+03 -5.7126e-01 -3.9000e+02 -1.1714e+03 -1.0055e+00 -3.9000e+02 -1.1429e+03 -1.4750e+00 -3.9000e+02 -1.1143e+03 -1.9839e+00 -3.9000e+02 -1.0857e+03 -2.5372e+00 -3.9000e+02 -1.0571e+03 -3.1405e+00 -3.9000e+02 -1.0286e+03 -3.8007e+00 -3.9000e+02 -1.0000e+03 -4.5256e+00 -3.9000e+02 -9.7143e+02 -5.3245e+00 -3.9000e+02 -9.4286e+02 -6.2084e+00 -3.9000e+02 -9.1429e+02 -7.1905e+00 -3.9000e+02 -8.8571e+02 -8.2866e+00 -3.9000e+02 -8.5714e+02 -9.5155e+00 -3.9000e+02 -8.2857e+02 -1.0900e+01 -3.9000e+02 -8.0000e+02 -1.2467e+01 -3.9000e+02 -7.7143e+02 -1.4250e+01 -3.9000e+02 -7.4286e+02 -1.6289e+01 -3.9000e+02 -7.1429e+02 -1.8628e+01 -3.9000e+02 -6.8571e+02 -2.1322e+01 -3.9000e+02 -6.5714e+02 -2.4428e+01 -3.9000e+02 -6.2857e+02 -2.8009e+01 -3.9000e+02 -6.0000e+02 -3.2116e+01 -3.9000e+02 -5.7143e+02 -3.6781e+01 -3.9000e+02 -5.4286e+02 -4.1991e+01 -3.9000e+02 -5.1429e+02 -4.7669e+01 -3.9000e+02 -4.8571e+02 -5.3662e+01 -3.9000e+02 -4.5714e+02 -5.9750e+01 -3.9000e+02 -4.2857e+02 -6.5690e+01 -3.9000e+02 -4.0000e+02 -7.1268e+01 -3.9000e+02 -3.7143e+02 -7.6336e+01 -3.9000e+02 -3.4286e+02 -8.0822e+01 -3.9000e+02 -3.1429e+02 -8.4717e+01 -3.9000e+02 -2.8571e+02 -8.8054e+01 -3.9000e+02 -2.5714e+02 -9.0882e+01 -3.9000e+02 -2.2857e+02 -9.3258e+01 -3.9000e+02 -2.0000e+02 -9.5237e+01 -3.9000e+02 -1.7143e+02 -9.6865e+01 -3.9000e+02 -1.4286e+02 -9.8183e+01 -3.9000e+02 -1.1429e+02 -9.9222e+01 -3.9000e+02 -8.5714e+01 -1.0001e+02 -3.9000e+02 -5.7143e+01 -1.0055e+02 -3.9000e+02 -2.8571e+01 -1.0088e+02 -3.9000e+02 0.0000e+00 -1.0099e+02 -3.9000e+02 2.8571e+01 -1.0088e+02 -3.9000e+02 5.7143e+01 -1.0055e+02 -3.9000e+02 8.5714e+01 -1.0001e+02 -3.9000e+02 1.1429e+02 -9.9222e+01 -3.9000e+02 1.4286e+02 -9.8183e+01 -3.9000e+02 1.7143e+02 -9.6865e+01 -3.9000e+02 2.0000e+02 -9.5237e+01 -3.9000e+02 2.2857e+02 -9.3258e+01 -3.9000e+02 2.5714e+02 -9.0882e+01 -3.9000e+02 2.8571e+02 -8.8054e+01 -3.9000e+02 3.1429e+02 -8.4717e+01 -3.9000e+02 3.4286e+02 -8.0822e+01 -3.9000e+02 3.7143e+02 -7.6336e+01 -3.9000e+02 4.0000e+02 -7.1268e+01 -3.9000e+02 4.2857e+02 -6.5690e+01 -3.9000e+02 4.5714e+02 -5.9750e+01 -3.9000e+02 4.8571e+02 -5.3662e+01 -3.9000e+02 5.1429e+02 -4.7669e+01 -3.9000e+02 5.4286e+02 -4.1991e+01 -3.9000e+02 5.7143e+02 -3.6781e+01 -3.9000e+02 6.0000e+02 -3.2116e+01 -3.9000e+02 6.2857e+02 -2.8009e+01 -3.9000e+02 6.5714e+02 -2.4428e+01 -3.9000e+02 6.8571e+02 -2.1322e+01 -3.9000e+02 7.1429e+02 -1.8628e+01 -3.9000e+02 7.4286e+02 -1.6289e+01 -3.9000e+02 7.7143e+02 -1.4250e+01 -3.9000e+02 8.0000e+02 -1.2467e+01 -3.9000e+02 8.2857e+02 -1.0900e+01 -3.9000e+02 8.5714e+02 -9.5155e+00 -3.9000e+02 8.8571e+02 -8.2866e+00 -3.9000e+02 9.1429e+02 -7.1905e+00 -3.9000e+02 9.4286e+02 -6.2084e+00 -3.9000e+02 9.7143e+02 -5.3245e+00 -3.9000e+02 1.0000e+03 -4.5256e+00 -3.9000e+02 1.0286e+03 -3.8007e+00 -3.9000e+02 1.0571e+03 -3.1405e+00 -3.9000e+02 1.0857e+03 -2.5372e+00 -3.9000e+02 1.1143e+03 -1.9839e+00 -3.9000e+02 1.1429e+03 -1.4750e+00 -3.9000e+02 1.1714e+03 -1.0055e+00 -3.9000e+02 1.2000e+03 -5.7126e-01 -3.9000e+02 1.2286e+03 -1.6850e-01 -3.9000e+02 1.2571e+03 2.0595e-01 -3.9000e+02 1.2857e+03 5.5486e-01 -3.9000e+02 1.3143e+03 8.8070e-01 -3.9000e+02 1.3429e+03 1.1856e+00 -3.9000e+02 1.3714e+03 1.4715e+00 -3.9000e+02 1.4000e+03 1.7400e+00 -3.9000e+02 1.4286e+03 1.9927e+00 -3.9000e+02 1.4571e+03 2.2308e+00 -3.9000e+02 1.4857e+03 2.4556e+00 -3.9000e+02 1.5143e+03 2.6682e+00 -3.9000e+02 1.5429e+03 2.8694e+00 -3.9000e+02 1.5714e+03 3.0601e+00 -3.9000e+02 1.6000e+03 3.2412e+00 -3.9000e+02 1.6286e+03 3.4133e+00 -3.9000e+02 1.6571e+03 3.5771e+00 -3.9000e+02 1.6857e+03 3.7331e+00 -3.9000e+02 1.7143e+03 3.8819e+00 -3.9000e+02 1.7429e+03 4.0240e+00 -3.9000e+02 1.7714e+03 4.1597e+00 -3.9000e+02 1.8000e+03 4.2895e+00 -3.9000e+02 1.8286e+03 4.4138e+00 -3.9000e+02 1.8571e+03 4.5329e+00 -3.9000e+02 1.8857e+03 4.6472e+00 -3.9000e+02 1.9143e+03 4.7568e+00 -3.9000e+02 1.9429e+03 4.8622e+00 -3.9000e+02 1.9714e+03 4.9634e+00 -3.9000e+02 2.0000e+03 5.0608e+00 -4.2000e+02 -2.0000e+03 5.0810e+00 -4.2000e+02 -1.9714e+03 4.9847e+00 -4.2000e+02 -1.9429e+03 4.8846e+00 -4.2000e+02 -1.9143e+03 4.7805e+00 -4.2000e+02 -1.8857e+03 4.6723e+00 -4.2000e+02 -1.8571e+03 4.5595e+00 -4.2000e+02 -1.8286e+03 4.4419e+00 -4.2000e+02 -1.8000e+03 4.3193e+00 -4.2000e+02 -1.7714e+03 4.1913e+00 -4.2000e+02 -1.7429e+03 4.0576e+00 -4.2000e+02 -1.7143e+03 3.9177e+00 -4.2000e+02 -1.6857e+03 3.7712e+00 -4.2000e+02 -1.6571e+03 3.6178e+00 -4.2000e+02 -1.6286e+03 3.4568e+00 -4.2000e+02 -1.6000e+03 3.2877e+00 -4.2000e+02 -1.5714e+03 3.1100e+00 -4.2000e+02 -1.5429e+03 2.9228e+00 -4.2000e+02 -1.5143e+03 2.7256e+00 -4.2000e+02 -1.4857e+03 2.5175e+00 -4.2000e+02 -1.4571e+03 2.2976e+00 -4.2000e+02 -1.4286e+03 2.0648e+00 -4.2000e+02 -1.4000e+03 1.8181e+00 -4.2000e+02 -1.3714e+03 1.5563e+00 -4.2000e+02 -1.3429e+03 1.2778e+00 -4.2000e+02 -1.3143e+03 9.8120e-01 -4.2000e+02 -1.2857e+03 6.6468e-01 -4.2000e+02 -1.2571e+03 3.2625e-01 -4.2000e+02 -1.2286e+03 -3.6358e-02 -4.2000e+02 -1.2000e+03 -4.2570e-01 -4.2000e+02 -1.1714e+03 -8.4472e-01 -4.2000e+02 -1.1429e+03 -1.2968e+00 -4.2000e+02 -1.1143e+03 -1.7857e+00 -4.2000e+02 -1.0857e+03 -2.3160e+00 -4.2000e+02 -1.0571e+03 -2.8927e+00 -4.2000e+02 -1.0286e+03 -3.5220e+00 -4.2000e+02 -1.0000e+03 -4.2107e+00 -4.2000e+02 -9.7143e+02 -4.9672e+00 -4.2000e+02 -9.4286e+02 -5.8011e+00 -4.2000e+02 -9.1429e+02 -6.7238e+00 -4.2000e+02 -8.8571e+02 -7.7490e+00 -4.2000e+02 -8.5714e+02 -8.8928e+00 -4.2000e+02 -8.2857e+02 -1.0175e+01 -4.2000e+02 -8.0000e+02 -1.1617e+01 -4.2000e+02 -7.7143e+02 -1.3248e+01 -4.2000e+02 -7.4286e+02 -1.5101e+01 -4.2000e+02 -7.1429e+02 -1.7212e+01 -4.2000e+02 -6.8571e+02 -1.9627e+01 -4.2000e+02 -6.5714e+02 -2.2395e+01 -4.2000e+02 -6.2857e+02 -2.5567e+01 -4.2000e+02 -6.0000e+02 -2.9195e+01 -4.2000e+02 -5.7143e+02 -3.3316e+01 -4.2000e+02 -5.4286e+02 -3.7943e+01 -4.2000e+02 -5.1429e+02 -4.3044e+01 -4.2000e+02 -4.8571e+02 -4.8529e+01 -4.2000e+02 -4.5714e+02 -5.4242e+01 -4.2000e+02 -4.2857e+02 -5.9983e+01 -4.2000e+02 -4.0000e+02 -6.5542e+01 -4.2000e+02 -3.7143e+02 -7.0739e+01 -4.2000e+02 -3.4286e+02 -7.5455e+01 -4.2000e+02 -3.1429e+02 -7.9631e+01 -4.2000e+02 -2.8571e+02 -8.3261e+01 -4.2000e+02 -2.5714e+02 -8.6370e+01 -4.2000e+02 -2.2857e+02 -8.9001e+01 -4.2000e+02 -2.0000e+02 -9.1202e+01 -4.2000e+02 -1.7143e+02 -9.3017e+01 -4.2000e+02 -1.4286e+02 -9.4489e+01 -4.2000e+02 -1.1429e+02 -9.5649e+01 -4.2000e+02 -8.5714e+01 -9.6525e+01 -4.2000e+02 -5.7143e+01 -9.7137e+01 -4.2000e+02 -2.8571e+01 -9.7499e+01 -4.2000e+02 0.0000e+00 -9.7618e+01 -4.2000e+02 2.8571e+01 -9.7499e+01 -4.2000e+02 5.7143e+01 -9.7137e+01 -4.2000e+02 8.5714e+01 -9.6525e+01 -4.2000e+02 1.1429e+02 -9.5649e+01 -4.2000e+02 1.4286e+02 -9.4489e+01 -4.2000e+02 1.7143e+02 -9.3017e+01 -4.2000e+02 2.0000e+02 -9.1202e+01 -4.2000e+02 2.2857e+02 -8.9001e+01 -4.2000e+02 2.5714e+02 -8.6370e+01 -4.2000e+02 2.8571e+02 -8.3261e+01 -4.2000e+02 3.1429e+02 -7.9631e+01 -4.2000e+02 3.4286e+02 -7.5455e+01 -4.2000e+02 3.7143e+02 -7.0739e+01 -4.2000e+02 4.0000e+02 -6.5542e+01 -4.2000e+02 4.2857e+02 -5.9983e+01 -4.2000e+02 4.5714e+02 -5.4242e+01 -4.2000e+02 4.8571e+02 -4.8529e+01 -4.2000e+02 5.1429e+02 -4.3044e+01 -4.2000e+02 5.4286e+02 -3.7943e+01 -4.2000e+02 5.7143e+02 -3.3316e+01 -4.2000e+02 6.0000e+02 -2.9195e+01 -4.2000e+02 6.2857e+02 -2.5567e+01 -4.2000e+02 6.5714e+02 -2.2395e+01 -4.2000e+02 6.8571e+02 -1.9627e+01 -4.2000e+02 7.1429e+02 -1.7212e+01 -4.2000e+02 7.4286e+02 -1.5101e+01 -4.2000e+02 7.7143e+02 -1.3248e+01 -4.2000e+02 8.0000e+02 -1.1617e+01 -4.2000e+02 8.2857e+02 -1.0175e+01 -4.2000e+02 8.5714e+02 -8.8928e+00 -4.2000e+02 8.8571e+02 -7.7490e+00 -4.2000e+02 9.1429e+02 -6.7238e+00 -4.2000e+02 9.4286e+02 -5.8011e+00 -4.2000e+02 9.7143e+02 -4.9672e+00 -4.2000e+02 1.0000e+03 -4.2107e+00 -4.2000e+02 1.0286e+03 -3.5220e+00 -4.2000e+02 1.0571e+03 -2.8927e+00 -4.2000e+02 1.0857e+03 -2.3160e+00 -4.2000e+02 1.1143e+03 -1.7857e+00 -4.2000e+02 1.1429e+03 -1.2968e+00 -4.2000e+02 1.1714e+03 -8.4472e-01 -4.2000e+02 1.2000e+03 -4.2570e-01 -4.2000e+02 1.2286e+03 -3.6358e-02 -4.2000e+02 1.2571e+03 3.2625e-01 -4.2000e+02 1.2857e+03 6.6468e-01 -4.2000e+02 1.3143e+03 9.8120e-01 -4.2000e+02 1.3429e+03 1.2778e+00 -4.2000e+02 1.3714e+03 1.5563e+00 -4.2000e+02 1.4000e+03 1.8181e+00 -4.2000e+02 1.4286e+03 2.0648e+00 -4.2000e+02 1.4571e+03 2.2976e+00 -4.2000e+02 1.4857e+03 2.5175e+00 -4.2000e+02 1.5143e+03 2.7256e+00 -4.2000e+02 1.5429e+03 2.9228e+00 -4.2000e+02 1.5714e+03 3.1100e+00 -4.2000e+02 1.6000e+03 3.2877e+00 -4.2000e+02 1.6286e+03 3.4568e+00 -4.2000e+02 1.6571e+03 3.6178e+00 -4.2000e+02 1.6857e+03 3.7712e+00 -4.2000e+02 1.7143e+03 3.9177e+00 -4.2000e+02 1.7429e+03 4.0576e+00 -4.2000e+02 1.7714e+03 4.1913e+00 -4.2000e+02 1.8000e+03 4.3193e+00 -4.2000e+02 1.8286e+03 4.4419e+00 -4.2000e+02 1.8571e+03 4.5595e+00 -4.2000e+02 1.8857e+03 4.6723e+00 -4.2000e+02 1.9143e+03 4.7805e+00 -4.2000e+02 1.9429e+03 4.8846e+00 -4.2000e+02 1.9714e+03 4.9847e+00 -4.2000e+02 2.0000e+03 5.0810e+00 -4.5000e+02 -2.0000e+03 5.1025e+00 -4.5000e+02 -1.9714e+03 5.0073e+00 -4.5000e+02 -1.9429e+03 4.9085e+00 -4.5000e+02 -1.9143e+03 4.8057e+00 -4.5000e+02 -1.8857e+03 4.6988e+00 -4.5000e+02 -1.8571e+03 4.5876e+00 -4.5000e+02 -1.8286e+03 4.4717e+00 -4.5000e+02 -1.8000e+03 4.3509e+00 -4.5000e+02 -1.7714e+03 4.2248e+00 -4.5000e+02 -1.7429e+03 4.0931e+00 -4.5000e+02 -1.7143e+03 3.9555e+00 -4.5000e+02 -1.6857e+03 3.8115e+00 -4.5000e+02 -1.6571e+03 3.6607e+00 -4.5000e+02 -1.6286e+03 3.5026e+00 -4.5000e+02 -1.6000e+03 3.3367e+00 -4.5000e+02 -1.5714e+03 3.1624e+00 -4.5000e+02 -1.5429e+03 2.9791e+00 -4.5000e+02 -1.5143e+03 2.7861e+00 -4.5000e+02 -1.4857e+03 2.5825e+00 -4.5000e+02 -1.4571e+03 2.3676e+00 -4.5000e+02 -1.4286e+03 2.1405e+00 -4.5000e+02 -1.4000e+03 1.9000e+00 -4.5000e+02 -1.3714e+03 1.6449e+00 -4.5000e+02 -1.3429e+03 1.3741e+00 -4.5000e+02 -1.3143e+03 1.0861e+00 -4.5000e+02 -1.2857e+03 7.7908e-01 -4.5000e+02 -1.2571e+03 4.5137e-01 -4.5000e+02 -1.2286e+03 1.0084e-01 -4.5000e+02 -1.2000e+03 -2.7485e-01 -4.5000e+02 -1.1714e+03 -6.7838e-01 -4.5000e+02 -1.1429e+03 -1.1128e+00 -4.5000e+02 -1.1143e+03 -1.5816e+00 -4.5000e+02 -1.0857e+03 -2.0888e+00 -4.5000e+02 -1.0571e+03 -2.6389e+00 -4.5000e+02 -1.0286e+03 -3.2373e+00 -4.5000e+02 -1.0000e+03 -3.8903e+00 -4.5000e+02 -9.7143e+02 -4.6049e+00 -4.5000e+02 -9.4286e+02 -5.3897e+00 -4.5000e+02 -9.1429e+02 -6.2545e+00 -4.5000e+02 -8.8571e+02 -7.2109e+00 -4.5000e+02 -8.5714e+02 -8.2728e+00 -4.5000e+02 -8.2857e+02 -9.4562e+00 -4.5000e+02 -8.0000e+02 -1.0781e+01 -4.5000e+02 -7.7143e+02 -1.2268e+01 -4.5000e+02 -7.4286e+02 -1.3947e+01 -4.5000e+02 -7.1429e+02 -1.5846e+01 -4.5000e+02 -6.8571e+02 -1.8003e+01 -4.5000e+02 -6.5714e+02 -2.0459e+01 -4.5000e+02 -6.2857e+02 -2.3255e+01 -4.5000e+02 -6.0000e+02 -2.6437e+01 -4.5000e+02 -5.7143e+02 -3.0043e+01 -4.5000e+02 -5.4286e+02 -3.4096e+01 -4.5000e+02 -5.1429e+02 -3.8595e+01 -4.5000e+02 -4.8571e+02 -4.3494e+01 -4.5000e+02 -4.5714e+02 -4.8699e+01 -4.5000e+02 -4.2857e+02 -5.4067e+01 -4.5000e+02 -4.0000e+02 -5.9421e+01 -4.5000e+02 -3.7143e+02 -6.4582e+01 -4.5000e+02 -3.4286e+02 -6.9403e+01 -4.5000e+02 -3.1429e+02 -7.3780e+01 -4.5000e+02 -2.8571e+02 -7.7663e+01 -4.5000e+02 -2.5714e+02 -8.1042e+01 -4.5000e+02 -2.2857e+02 -8.3935e+01 -4.5000e+02 -2.0000e+02 -8.6375e+01 -4.5000e+02 -1.7143e+02 -8.8400e+01 -4.5000e+02 -1.4286e+02 -9.0046e+01 -4.5000e+02 -1.1429e+02 -9.1348e+01 -4.5000e+02 -8.5714e+01 -9.2333e+01 -4.5000e+02 -5.7143e+01 -9.3021e+01 -4.5000e+02 -2.8571e+01 -9.3428e+01 -4.5000e+02 0.0000e+00 -9.3563e+01 -4.5000e+02 2.8571e+01 -9.3428e+01 -4.5000e+02 5.7143e+01 -9.3021e+01 -4.5000e+02 8.5714e+01 -9.2333e+01 -4.5000e+02 1.1429e+02 -9.1348e+01 -4.5000e+02 1.4286e+02 -9.0046e+01 -4.5000e+02 1.7143e+02 -8.8400e+01 -4.5000e+02 2.0000e+02 -8.6375e+01 -4.5000e+02 2.2857e+02 -8.3935e+01 -4.5000e+02 2.5714e+02 -8.1042e+01 -4.5000e+02 2.8571e+02 -7.7663e+01 -4.5000e+02 3.1429e+02 -7.3780e+01 -4.5000e+02 3.4286e+02 -6.9403e+01 -4.5000e+02 3.7143e+02 -6.4582e+01 -4.5000e+02 4.0000e+02 -5.9421e+01 -4.5000e+02 4.2857e+02 -5.4067e+01 -4.5000e+02 4.5714e+02 -4.8699e+01 -4.5000e+02 4.8571e+02 -4.3494e+01 -4.5000e+02 5.1429e+02 -3.8595e+01 -4.5000e+02 5.4286e+02 -3.4096e+01 -4.5000e+02 5.7143e+02 -3.0043e+01 -4.5000e+02 6.0000e+02 -2.6437e+01 -4.5000e+02 6.2857e+02 -2.3255e+01 -4.5000e+02 6.5714e+02 -2.0459e+01 -4.5000e+02 6.8571e+02 -1.8003e+01 -4.5000e+02 7.1429e+02 -1.5846e+01 -4.5000e+02 7.4286e+02 -1.3947e+01 -4.5000e+02 7.7143e+02 -1.2268e+01 -4.5000e+02 8.0000e+02 -1.0781e+01 -4.5000e+02 8.2857e+02 -9.4562e+00 -4.5000e+02 8.5714e+02 -8.2728e+00 -4.5000e+02 8.8571e+02 -7.2109e+00 -4.5000e+02 9.1429e+02 -6.2545e+00 -4.5000e+02 9.4286e+02 -5.3897e+00 -4.5000e+02 9.7143e+02 -4.6049e+00 -4.5000e+02 1.0000e+03 -3.8903e+00 -4.5000e+02 1.0286e+03 -3.2373e+00 -4.5000e+02 1.0571e+03 -2.6389e+00 -4.5000e+02 1.0857e+03 -2.0888e+00 -4.5000e+02 1.1143e+03 -1.5816e+00 -4.5000e+02 1.1429e+03 -1.1128e+00 -4.5000e+02 1.1714e+03 -6.7838e-01 -4.5000e+02 1.2000e+03 -2.7485e-01 -4.5000e+02 1.2286e+03 1.0084e-01 -4.5000e+02 1.2571e+03 4.5137e-01 -4.5000e+02 1.2857e+03 7.7908e-01 -4.5000e+02 1.3143e+03 1.0861e+00 -4.5000e+02 1.3429e+03 1.3741e+00 -4.5000e+02 1.3714e+03 1.6449e+00 -4.5000e+02 1.4000e+03 1.9000e+00 -4.5000e+02 1.4286e+03 2.1405e+00 -4.5000e+02 1.4571e+03 2.3676e+00 -4.5000e+02 1.4857e+03 2.5825e+00 -4.5000e+02 1.5143e+03 2.7861e+00 -4.5000e+02 1.5429e+03 2.9791e+00 -4.5000e+02 1.5714e+03 3.1624e+00 -4.5000e+02 1.6000e+03 3.3367e+00 -4.5000e+02 1.6286e+03 3.5026e+00 -4.5000e+02 1.6571e+03 3.6607e+00 -4.5000e+02 1.6857e+03 3.8115e+00 -4.5000e+02 1.7143e+03 3.9555e+00 -4.5000e+02 1.7429e+03 4.0931e+00 -4.5000e+02 1.7714e+03 4.2248e+00 -4.5000e+02 1.8000e+03 4.3509e+00 -4.5000e+02 1.8286e+03 4.4717e+00 -4.5000e+02 1.8571e+03 4.5876e+00 -4.5000e+02 1.8857e+03 4.6988e+00 -4.5000e+02 1.9143e+03 4.8057e+00 -4.5000e+02 1.9429e+03 4.9085e+00 -4.5000e+02 1.9714e+03 5.0073e+00 -4.5000e+02 2.0000e+03 5.1025e+00 -4.8000e+02 -2.0000e+03 5.1251e+00 -4.8000e+02 -1.9714e+03 5.0312e+00 -4.8000e+02 -1.9429e+03 4.9336e+00 -4.8000e+02 -1.9143e+03 4.8322e+00 -4.8000e+02 -1.8857e+03 4.7269e+00 -4.8000e+02 -1.8571e+03 4.6172e+00 -4.8000e+02 -1.8286e+03 4.5030e+00 -4.8000e+02 -1.8000e+03 4.3841e+00 -4.8000e+02 -1.7714e+03 4.2600e+00 -4.8000e+02 -1.7429e+03 4.1305e+00 -4.8000e+02 -1.7143e+03 3.9952e+00 -4.8000e+02 -1.6857e+03 3.8538e+00 -4.8000e+02 -1.6571e+03 3.7058e+00 -4.8000e+02 -1.6286e+03 3.5507e+00 -4.8000e+02 -1.6000e+03 3.3881e+00 -4.8000e+02 -1.5714e+03 3.2174e+00 -4.8000e+02 -1.5429e+03 3.0380e+00 -4.8000e+02 -1.5143e+03 2.8492e+00 -4.8000e+02 -1.4857e+03 2.6504e+00 -4.8000e+02 -1.4571e+03 2.4407e+00 -4.8000e+02 -1.4286e+03 2.2193e+00 -4.8000e+02 -1.4000e+03 1.9851e+00 -4.8000e+02 -1.3714e+03 1.7371e+00 -4.8000e+02 -1.3429e+03 1.4741e+00 -4.8000e+02 -1.3143e+03 1.1948e+00 -4.8000e+02 -1.2857e+03 8.9750e-01 -4.8000e+02 -1.2571e+03 5.8067e-01 -4.8000e+02 -1.2286e+03 2.4238e-01 -4.8000e+02 -1.2000e+03 -1.1953e-01 -4.8000e+02 -1.1714e+03 -5.0747e-01 -4.8000e+02 -1.1429e+03 -9.2420e-01 -4.8000e+02 -1.1143e+03 -1.3729e+00 -4.8000e+02 -1.0857e+03 -1.8570e+00 -4.8000e+02 -1.0571e+03 -2.3807e+00 -4.8000e+02 -1.0286e+03 -2.9487e+00 -4.8000e+02 -1.0000e+03 -3.5665e+00 -4.8000e+02 -9.7143e+02 -4.2402e+00 -4.8000e+02 -9.4286e+02 -4.9771e+00 -4.8000e+02 -9.1429e+02 -5.7858e+00 -4.8000e+02 -8.8571e+02 -6.6762e+00 -4.8000e+02 -8.5714e+02 -7.6597e+00 -4.8000e+02 -8.2857e+02 -8.7500e+00 -4.8000e+02 -8.0000e+02 -9.9629e+00 -4.8000e+02 -7.7143e+02 -1.1317e+01 -4.8000e+02 -7.4286e+02 -1.2834e+01 -4.8000e+02 -7.1429e+02 -1.4539e+01 -4.8000e+02 -6.8571e+02 -1.6461e+01 -4.8000e+02 -6.5714e+02 -1.8632e+01 -4.8000e+02 -6.2857e+02 -2.1087e+01 -4.8000e+02 -6.0000e+02 -2.3864e+01 -4.8000e+02 -5.7143e+02 -2.6995e+01 -4.8000e+02 -5.4286e+02 -3.0509e+01 -4.8000e+02 -5.1429e+02 -3.4416e+01 -4.8000e+02 -4.8571e+02 -3.8702e+01 -4.8000e+02 -4.5714e+02 -4.3318e+01 -4.8000e+02 -4.2857e+02 -4.8174e+01 -4.8000e+02 -4.0000e+02 -5.3142e+01 -4.8000e+02 -3.7143e+02 -5.8072e+01 -4.8000e+02 -3.4286e+02 -6.2817e+01 -4.8000e+02 -3.1429e+02 -6.7250e+01 -4.8000e+02 -2.8571e+02 -7.1284e+01 -4.8000e+02 -2.5714e+02 -7.4870e+01 -4.8000e+02 -2.2857e+02 -7.7993e+01 -4.8000e+02 -2.0000e+02 -8.0662e+01 -4.8000e+02 -1.7143e+02 -8.2899e+01 -4.8000e+02 -1.4286e+02 -8.4731e+01 -4.8000e+02 -1.1429e+02 -8.6187e+01 -4.8000e+02 -8.5714e+01 -8.7292e+01 -4.8000e+02 -5.7143e+01 -8.8066e+01 -4.8000e+02 -2.8571e+01 -8.8525e+01 -4.8000e+02 0.0000e+00 -8.8677e+01 -4.8000e+02 2.8571e+01 -8.8525e+01 -4.8000e+02 5.7143e+01 -8.8066e+01 -4.8000e+02 8.5714e+01 -8.7292e+01 -4.8000e+02 1.1429e+02 -8.6187e+01 -4.8000e+02 1.4286e+02 -8.4731e+01 -4.8000e+02 1.7143e+02 -8.2899e+01 -4.8000e+02 2.0000e+02 -8.0662e+01 -4.8000e+02 2.2857e+02 -7.7993e+01 -4.8000e+02 2.5714e+02 -7.4870e+01 -4.8000e+02 2.8571e+02 -7.1284e+01 -4.8000e+02 3.1429e+02 -6.7250e+01 -4.8000e+02 3.4286e+02 -6.2817e+01 -4.8000e+02 3.7143e+02 -5.8072e+01 -4.8000e+02 4.0000e+02 -5.3142e+01 -4.8000e+02 4.2857e+02 -4.8174e+01 -4.8000e+02 4.5714e+02 -4.3318e+01 -4.8000e+02 4.8571e+02 -3.8702e+01 -4.8000e+02 5.1429e+02 -3.4416e+01 -4.8000e+02 5.4286e+02 -3.0509e+01 -4.8000e+02 5.7143e+02 -2.6995e+01 -4.8000e+02 6.0000e+02 -2.3864e+01 -4.8000e+02 6.2857e+02 -2.1087e+01 -4.8000e+02 6.5714e+02 -1.8632e+01 -4.8000e+02 6.8571e+02 -1.6461e+01 -4.8000e+02 7.1429e+02 -1.4539e+01 -4.8000e+02 7.4286e+02 -1.2834e+01 -4.8000e+02 7.7143e+02 -1.1317e+01 -4.8000e+02 8.0000e+02 -9.9629e+00 -4.8000e+02 8.2857e+02 -8.7500e+00 -4.8000e+02 8.5714e+02 -7.6597e+00 -4.8000e+02 8.8571e+02 -6.6762e+00 -4.8000e+02 9.1429e+02 -5.7858e+00 -4.8000e+02 9.4286e+02 -4.9771e+00 -4.8000e+02 9.7143e+02 -4.2402e+00 -4.8000e+02 1.0000e+03 -3.5665e+00 -4.8000e+02 1.0286e+03 -2.9487e+00 -4.8000e+02 1.0571e+03 -2.3807e+00 -4.8000e+02 1.0857e+03 -1.8570e+00 -4.8000e+02 1.1143e+03 -1.3729e+00 -4.8000e+02 1.1429e+03 -9.2420e-01 -4.8000e+02 1.1714e+03 -5.0747e-01 -4.8000e+02 1.2000e+03 -1.1953e-01 -4.8000e+02 1.2286e+03 2.4238e-01 -4.8000e+02 1.2571e+03 5.8067e-01 -4.8000e+02 1.2857e+03 8.9750e-01 -4.8000e+02 1.3143e+03 1.1948e+00 -4.8000e+02 1.3429e+03 1.4741e+00 -4.8000e+02 1.3714e+03 1.7371e+00 -4.8000e+02 1.4000e+03 1.9851e+00 -4.8000e+02 1.4286e+03 2.2193e+00 -4.8000e+02 1.4571e+03 2.4407e+00 -4.8000e+02 1.4857e+03 2.6504e+00 -4.8000e+02 1.5143e+03 2.8492e+00 -4.8000e+02 1.5429e+03 3.0380e+00 -4.8000e+02 1.5714e+03 3.2174e+00 -4.8000e+02 1.6000e+03 3.3881e+00 -4.8000e+02 1.6286e+03 3.5507e+00 -4.8000e+02 1.6571e+03 3.7058e+00 -4.8000e+02 1.6857e+03 3.8538e+00 -4.8000e+02 1.7143e+03 3.9952e+00 -4.8000e+02 1.7429e+03 4.1305e+00 -4.8000e+02 1.7714e+03 4.2600e+00 -4.8000e+02 1.8000e+03 4.3841e+00 -4.8000e+02 1.8286e+03 4.5030e+00 -4.8000e+02 1.8571e+03 4.6172e+00 -4.8000e+02 1.8857e+03 4.7269e+00 -4.8000e+02 1.9143e+03 4.8322e+00 -4.8000e+02 1.9429e+03 4.9336e+00 -4.8000e+02 1.9714e+03 5.0312e+00 -4.8000e+02 2.0000e+03 5.1251e+00 -5.1000e+02 -2.0000e+03 5.1489e+00 -5.1000e+02 -1.9714e+03 5.0562e+00 -5.1000e+02 -1.9429e+03 4.9600e+00 -5.1000e+02 -1.9143e+03 4.8601e+00 -5.1000e+02 -1.8857e+03 4.7563e+00 -5.1000e+02 -1.8571e+03 4.6483e+00 -5.1000e+02 -1.8286e+03 4.5359e+00 -5.1000e+02 -1.8000e+03 4.4189e+00 -5.1000e+02 -1.7714e+03 4.2969e+00 -5.1000e+02 -1.7429e+03 4.1696e+00 -5.1000e+02 -1.7143e+03 4.0368e+00 -5.1000e+02 -1.6857e+03 3.8980e+00 -5.1000e+02 -1.6571e+03 3.7528e+00 -5.1000e+02 -1.6286e+03 3.6008e+00 -5.1000e+02 -1.6000e+03 3.4416e+00 -5.1000e+02 -1.5714e+03 3.2746e+00 -5.1000e+02 -1.5429e+03 3.0992e+00 -5.1000e+02 -1.5143e+03 2.9149e+00 -5.1000e+02 -1.4857e+03 2.7209e+00 -5.1000e+02 -1.4571e+03 2.5166e+00 -5.1000e+02 -1.4286e+03 2.3010e+00 -5.1000e+02 -1.4000e+03 2.0733e+00 -5.1000e+02 -1.3714e+03 1.8324e+00 -5.1000e+02 -1.3429e+03 1.5774e+00 -5.1000e+02 -1.3143e+03 1.3068e+00 -5.1000e+02 -1.2857e+03 1.0194e+00 -5.1000e+02 -1.2571e+03 7.1354e-01 -5.1000e+02 -1.2286e+03 3.8755e-01 -5.1000e+02 -1.2000e+03 3.9469e-02 -5.1000e+02 -1.1714e+03 -3.3289e-01 -5.1000e+02 -1.1429e+03 -7.3199e-01 -5.1000e+02 -1.1143e+03 -1.1606e+00 -5.1000e+02 -1.0857e+03 -1.6220e+00 -5.1000e+02 -1.0571e+03 -2.1197e+00 -5.1000e+02 -1.0286e+03 -2.6578e+00 -5.1000e+02 -1.0000e+03 -3.2412e+00 -5.1000e+02 -9.7143e+02 -3.8751e+00 -5.1000e+02 -9.4286e+02 -4.5659e+00 -5.1000e+02 -9.1429e+02 -5.3208e+00 -5.1000e+02 -8.8571e+02 -6.1480e+00 -5.1000e+02 -8.5714e+02 -7.0573e+00 -5.1000e+02 -8.2857e+02 -8.0600e+00 -5.1000e+02 -8.0000e+02 -9.1688e+00 -5.1000e+02 -7.7143e+02 -1.0399e+01 -5.1000e+02 -7.4286e+02 -1.1768e+01 -5.1000e+02 -7.1429e+02 -1.3296e+01 -5.1000e+02 -6.8571e+02 -1.5005e+01 -5.1000e+02 -6.5714e+02 -1.6921e+01 -5.1000e+02 -6.2857e+02 -1.9071e+01 -5.1000e+02 -6.0000e+02 -2.1485e+01 -5.1000e+02 -5.7143e+02 -2.4191e+01 -5.1000e+02 -5.4286e+02 -2.7214e+01 -5.1000e+02 -5.1429e+02 -3.0569e+01 -5.1000e+02 -4.8571e+02 -3.4259e+01 -5.1000e+02 -4.5714e+02 -3.8261e+01 -5.1000e+02 -4.2857e+02 -4.2526e+01 -5.1000e+02 -4.0000e+02 -4.6973e+01 -5.1000e+02 -3.7143e+02 -5.1493e+01 -5.1000e+02 -3.4286e+02 -5.5963e+01 -5.1000e+02 -3.1429e+02 -6.0261e+01 -5.1000e+02 -2.8571e+02 -6.4282e+01 -5.1000e+02 -2.5714e+02 -6.7948e+01 -5.1000e+02 -2.2857e+02 -7.1211e+01 -5.1000e+02 -2.0000e+02 -7.4050e+01 -5.1000e+02 -1.7143e+02 -7.6465e+01 -5.1000e+02 -1.4286e+02 -7.8465e+01 -5.1000e+02 -1.1429e+02 -8.0068e+01 -5.1000e+02 -8.5714e+01 -8.1292e+01 -5.1000e+02 -5.7143e+01 -8.2153e+01 -5.1000e+02 -2.8571e+01 -8.2664e+01 -5.1000e+02 0.0000e+00 -8.2834e+01 -5.1000e+02 2.8571e+01 -8.2664e+01 -5.1000e+02 5.7143e+01 -8.2153e+01 -5.1000e+02 8.5714e+01 -8.1292e+01 -5.1000e+02 1.1429e+02 -8.0068e+01 -5.1000e+02 1.4286e+02 -7.8465e+01 -5.1000e+02 1.7143e+02 -7.6465e+01 -5.1000e+02 2.0000e+02 -7.4050e+01 -5.1000e+02 2.2857e+02 -7.1211e+01 -5.1000e+02 2.5714e+02 -6.7948e+01 -5.1000e+02 2.8571e+02 -6.4282e+01 -5.1000e+02 3.1429e+02 -6.0261e+01 -5.1000e+02 3.4286e+02 -5.5963e+01 -5.1000e+02 3.7143e+02 -5.1493e+01 -5.1000e+02 4.0000e+02 -4.6973e+01 -5.1000e+02 4.2857e+02 -4.2526e+01 -5.1000e+02 4.5714e+02 -3.8261e+01 -5.1000e+02 4.8571e+02 -3.4259e+01 -5.1000e+02 5.1429e+02 -3.0569e+01 -5.1000e+02 5.4286e+02 -2.7214e+01 -5.1000e+02 5.7143e+02 -2.4191e+01 -5.1000e+02 6.0000e+02 -2.1485e+01 -5.1000e+02 6.2857e+02 -1.9071e+01 -5.1000e+02 6.5714e+02 -1.6921e+01 -5.1000e+02 6.8571e+02 -1.5005e+01 -5.1000e+02 7.1429e+02 -1.3296e+01 -5.1000e+02 7.4286e+02 -1.1768e+01 -5.1000e+02 7.7143e+02 -1.0399e+01 -5.1000e+02 8.0000e+02 -9.1688e+00 -5.1000e+02 8.2857e+02 -8.0600e+00 -5.1000e+02 8.5714e+02 -7.0573e+00 -5.1000e+02 8.8571e+02 -6.1480e+00 -5.1000e+02 9.1429e+02 -5.3208e+00 -5.1000e+02 9.4286e+02 -4.5659e+00 -5.1000e+02 9.7143e+02 -3.8751e+00 -5.1000e+02 1.0000e+03 -3.2412e+00 -5.1000e+02 1.0286e+03 -2.6578e+00 -5.1000e+02 1.0571e+03 -2.1197e+00 -5.1000e+02 1.0857e+03 -1.6220e+00 -5.1000e+02 1.1143e+03 -1.1606e+00 -5.1000e+02 1.1429e+03 -7.3199e-01 -5.1000e+02 1.1714e+03 -3.3289e-01 -5.1000e+02 1.2000e+03 3.9469e-02 -5.1000e+02 1.2286e+03 3.8755e-01 -5.1000e+02 1.2571e+03 7.1354e-01 -5.1000e+02 1.2857e+03 1.0194e+00 -5.1000e+02 1.3143e+03 1.3068e+00 -5.1000e+02 1.3429e+03 1.5774e+00 -5.1000e+02 1.3714e+03 1.8324e+00 -5.1000e+02 1.4000e+03 2.0733e+00 -5.1000e+02 1.4286e+03 2.3010e+00 -5.1000e+02 1.4571e+03 2.5166e+00 -5.1000e+02 1.4857e+03 2.7209e+00 -5.1000e+02 1.5143e+03 2.9149e+00 -5.1000e+02 1.5429e+03 3.0992e+00 -5.1000e+02 1.5714e+03 3.2746e+00 -5.1000e+02 1.6000e+03 3.4416e+00 -5.1000e+02 1.6286e+03 3.6008e+00 -5.1000e+02 1.6571e+03 3.7528e+00 -5.1000e+02 1.6857e+03 3.8980e+00 -5.1000e+02 1.7143e+03 4.0368e+00 -5.1000e+02 1.7429e+03 4.1696e+00 -5.1000e+02 1.7714e+03 4.2969e+00 -5.1000e+02 1.8000e+03 4.4189e+00 -5.1000e+02 1.8286e+03 4.5359e+00 -5.1000e+02 1.8571e+03 4.6483e+00 -5.1000e+02 1.8857e+03 4.7563e+00 -5.1000e+02 1.9143e+03 4.8601e+00 -5.1000e+02 1.9429e+03 4.9600e+00 -5.1000e+02 1.9714e+03 5.0562e+00 -5.1000e+02 2.0000e+03 5.1489e+00 -5.4000e+02 -2.0000e+03 5.1739e+00 -5.4000e+02 -1.9714e+03 5.0824e+00 -5.4000e+02 -1.9429e+03 4.9876e+00 -5.4000e+02 -1.9143e+03 4.8892e+00 -5.4000e+02 -1.8857e+03 4.7870e+00 -5.4000e+02 -1.8571e+03 4.6807e+00 -5.4000e+02 -1.8286e+03 4.5702e+00 -5.4000e+02 -1.8000e+03 4.4551e+00 -5.4000e+02 -1.7714e+03 4.3353e+00 -5.4000e+02 -1.7429e+03 4.2104e+00 -5.4000e+02 -1.7143e+03 4.0800e+00 -5.4000e+02 -1.6857e+03 3.9439e+00 -5.4000e+02 -1.6571e+03 3.8017e+00 -5.4000e+02 -1.6286e+03 3.6529e+00 -5.4000e+02 -1.6000e+03 3.4971e+00 -5.4000e+02 -1.5714e+03 3.3339e+00 -5.4000e+02 -1.5429e+03 3.1627e+00 -5.4000e+02 -1.5143e+03 2.9828e+00 -5.4000e+02 -1.4857e+03 2.7938e+00 -5.4000e+02 -1.4571e+03 2.5948e+00 -5.4000e+02 -1.4286e+03 2.3852e+00 -5.4000e+02 -1.4000e+03 2.1641e+00 -5.4000e+02 -1.3714e+03 1.9305e+00 -5.4000e+02 -1.3429e+03 1.6834e+00 -5.4000e+02 -1.3143e+03 1.4218e+00 -5.4000e+02 -1.2857e+03 1.1442e+00 -5.4000e+02 -1.2571e+03 8.4936e-01 -5.4000e+02 -1.2286e+03 5.3567e-01 -5.4000e+02 -1.2000e+03 2.0138e-01 -5.4000e+02 -1.1714e+03 -1.5549e-01 -5.4000e+02 -1.1429e+03 -5.3713e-01 -5.4000e+02 -1.1143e+03 -9.4603e-01 -5.4000e+02 -1.0857e+03 -1.3850e+00 -5.4000e+02 -1.0571e+03 -1.8572e+00 -5.4000e+02 -1.0286e+03 -2.3662e+00 -5.4000e+02 -1.0000e+03 -2.9162e+00 -5.4000e+02 -9.7143e+02 -3.5117e+00 -5.4000e+02 -9.4286e+02 -4.1582e+00 -5.4000e+02 -9.1429e+02 -4.8617e+00 -5.4000e+02 -8.8571e+02 -5.6292e+00 -5.4000e+02 -8.5714e+02 -6.4686e+00 -5.4000e+02 -8.2857e+02 -7.3893e+00 -5.4000e+02 -8.0000e+02 -8.4017e+00 -5.4000e+02 -7.7143e+02 -9.5180e+00 -5.4000e+02 -7.4286e+02 -1.0752e+01 -5.4000e+02 -7.1429e+02 -1.2119e+01 -5.4000e+02 -6.8571e+02 -1.3637e+01 -5.4000e+02 -6.5714e+02 -1.5326e+01 -5.4000e+02 -6.2857e+02 -1.7207e+01 -5.4000e+02 -6.0000e+02 -1.9301e+01 -5.4000e+02 -5.7143e+02 -2.1632e+01 -5.4000e+02 -5.4286e+02 -2.4220e+01 -5.4000e+02 -5.1429e+02 -2.7081e+01 -5.4000e+02 -4.8571e+02 -3.0221e+01 -5.4000e+02 -4.5714e+02 -3.3633e+01 -5.4000e+02 -4.2857e+02 -3.7293e+01 -5.4000e+02 -4.0000e+02 -4.1153e+01 -5.4000e+02 -3.7143e+02 -4.5144e+01 -5.4000e+02 -3.4286e+02 -4.9175e+01 -5.4000e+02 -3.1429e+02 -5.3147e+01 -5.4000e+02 -2.8571e+02 -5.6961e+01 -5.4000e+02 -2.5714e+02 -6.0529e+01 -5.4000e+02 -2.2857e+02 -6.3784e+01 -5.4000e+02 -2.0000e+02 -6.6677e+01 -5.4000e+02 -1.7143e+02 -6.9182e+01 -5.4000e+02 -1.4286e+02 -7.1289e+01 -5.4000e+02 -1.1429e+02 -7.2998e+01 -5.4000e+02 -8.5714e+01 -7.4315e+01 -5.4000e+02 -5.7143e+01 -7.5247e+01 -5.4000e+02 -2.8571e+01 -7.5803e+01 -5.4000e+02 0.0000e+00 -7.5988e+01 -5.4000e+02 2.8571e+01 -7.5803e+01 -5.4000e+02 5.7143e+01 -7.5247e+01 -5.4000e+02 8.5714e+01 -7.4315e+01 -5.4000e+02 1.1429e+02 -7.2998e+01 -5.4000e+02 1.4286e+02 -7.1289e+01 -5.4000e+02 1.7143e+02 -6.9182e+01 -5.4000e+02 2.0000e+02 -6.6677e+01 -5.4000e+02 2.2857e+02 -6.3784e+01 -5.4000e+02 2.5714e+02 -6.0529e+01 -5.4000e+02 2.8571e+02 -5.6961e+01 -5.4000e+02 3.1429e+02 -5.3147e+01 -5.4000e+02 3.4286e+02 -4.9175e+01 -5.4000e+02 3.7143e+02 -4.5144e+01 -5.4000e+02 4.0000e+02 -4.1153e+01 -5.4000e+02 4.2857e+02 -3.7293e+01 -5.4000e+02 4.5714e+02 -3.3633e+01 -5.4000e+02 4.8571e+02 -3.0221e+01 -5.4000e+02 5.1429e+02 -2.7081e+01 -5.4000e+02 5.4286e+02 -2.4220e+01 -5.4000e+02 5.7143e+02 -2.1632e+01 -5.4000e+02 6.0000e+02 -1.9301e+01 -5.4000e+02 6.2857e+02 -1.7207e+01 -5.4000e+02 6.5714e+02 -1.5326e+01 -5.4000e+02 6.8571e+02 -1.3637e+01 -5.4000e+02 7.1429e+02 -1.2119e+01 -5.4000e+02 7.4286e+02 -1.0752e+01 -5.4000e+02 7.7143e+02 -9.5180e+00 -5.4000e+02 8.0000e+02 -8.4017e+00 -5.4000e+02 8.2857e+02 -7.3893e+00 -5.4000e+02 8.5714e+02 -6.4686e+00 -5.4000e+02 8.8571e+02 -5.6292e+00 -5.4000e+02 9.1429e+02 -4.8617e+00 -5.4000e+02 9.4286e+02 -4.1582e+00 -5.4000e+02 9.7143e+02 -3.5117e+00 -5.4000e+02 1.0000e+03 -2.9162e+00 -5.4000e+02 1.0286e+03 -2.3662e+00 -5.4000e+02 1.0571e+03 -1.8572e+00 -5.4000e+02 1.0857e+03 -1.3850e+00 -5.4000e+02 1.1143e+03 -9.4603e-01 -5.4000e+02 1.1429e+03 -5.3713e-01 -5.4000e+02 1.1714e+03 -1.5549e-01 -5.4000e+02 1.2000e+03 2.0138e-01 -5.4000e+02 1.2286e+03 5.3567e-01 -5.4000e+02 1.2571e+03 8.4936e-01 -5.4000e+02 1.2857e+03 1.1442e+00 -5.4000e+02 1.3143e+03 1.4218e+00 -5.4000e+02 1.3429e+03 1.6834e+00 -5.4000e+02 1.3714e+03 1.9305e+00 -5.4000e+02 1.4000e+03 2.1641e+00 -5.4000e+02 1.4286e+03 2.3852e+00 -5.4000e+02 1.4571e+03 2.5948e+00 -5.4000e+02 1.4857e+03 2.7938e+00 -5.4000e+02 1.5143e+03 2.9828e+00 -5.4000e+02 1.5429e+03 3.1627e+00 -5.4000e+02 1.5714e+03 3.3339e+00 -5.4000e+02 1.6000e+03 3.4971e+00 -5.4000e+02 1.6286e+03 3.6529e+00 -5.4000e+02 1.6571e+03 3.8017e+00 -5.4000e+02 1.6857e+03 3.9439e+00 -5.4000e+02 1.7143e+03 4.0800e+00 -5.4000e+02 1.7429e+03 4.2104e+00 -5.4000e+02 1.7714e+03 4.3353e+00 -5.4000e+02 1.8000e+03 4.4551e+00 -5.4000e+02 1.8286e+03 4.5702e+00 -5.4000e+02 1.8571e+03 4.6807e+00 -5.4000e+02 1.8857e+03 4.7870e+00 -5.4000e+02 1.9143e+03 4.8892e+00 -5.4000e+02 1.9429e+03 4.9876e+00 -5.4000e+02 1.9714e+03 5.0824e+00 -5.4000e+02 2.0000e+03 5.1739e+00 -5.7000e+02 -2.0000e+03 5.1998e+00 -5.7000e+02 -1.9714e+03 5.1098e+00 -5.7000e+02 -1.9429e+03 5.0164e+00 -5.7000e+02 -1.9143e+03 4.9195e+00 -5.7000e+02 -1.8857e+03 4.8189e+00 -5.7000e+02 -1.8571e+03 4.7144e+00 -5.7000e+02 -1.8286e+03 4.6058e+00 -5.7000e+02 -1.8000e+03 4.4928e+00 -5.7000e+02 -1.7714e+03 4.3752e+00 -5.7000e+02 -1.7429e+03 4.2526e+00 -5.7000e+02 -1.7143e+03 4.1248e+00 -5.7000e+02 -1.6857e+03 3.9915e+00 -5.7000e+02 -1.6571e+03 3.8523e+00 -5.7000e+02 -1.6286e+03 3.7067e+00 -5.7000e+02 -1.6000e+03 3.5545e+00 -5.7000e+02 -1.5714e+03 3.3951e+00 -5.7000e+02 -1.5429e+03 3.2281e+00 -5.7000e+02 -1.5143e+03 3.0528e+00 -5.7000e+02 -1.4857e+03 2.8688e+00 -5.7000e+02 -1.4571e+03 2.6753e+00 -5.7000e+02 -1.4286e+03 2.4717e+00 -5.7000e+02 -1.4000e+03 2.2572e+00 -5.7000e+02 -1.3714e+03 2.0309e+00 -5.7000e+02 -1.3429e+03 1.7919e+00 -5.7000e+02 -1.3143e+03 1.5391e+00 -5.7000e+02 -1.2857e+03 1.2714e+00 -5.7000e+02 -1.2571e+03 9.8756e-01 -5.7000e+02 -1.2286e+03 6.8611e-01 -5.7000e+02 -1.2000e+03 3.6548e-01 -5.7000e+02 -1.1714e+03 2.3931e-02 -5.7000e+02 -1.1429e+03 -3.4051e-01 -5.7000e+02 -1.1143e+03 -7.3003e-01 -5.7000e+02 -1.0857e+03 -1.1471e+00 -5.7000e+02 -1.0571e+03 -1.5945e+00 -5.7000e+02 -1.0286e+03 -2.0753e+00 -5.7000e+02 -1.0000e+03 -2.5931e+00 -5.7000e+02 -9.7143e+02 -3.1518e+00 -5.7000e+02 -9.4286e+02 -3.7560e+00 -5.7000e+02 -9.1429e+02 -4.4107e+00 -5.7000e+02 -8.8571e+02 -5.1218e+00 -5.7000e+02 -8.5714e+02 -5.8959e+00 -5.7000e+02 -8.2857e+02 -6.7404e+00 -5.7000e+02 -8.0000e+02 -7.6638e+00 -5.7000e+02 -7.7143e+02 -8.6758e+00 -5.7000e+02 -7.4286e+02 -9.7872e+00 -5.7000e+02 -7.1429e+02 -1.1010e+01 -5.7000e+02 -6.8571e+02 -1.2358e+01 -5.7000e+02 -6.5714e+02 -1.3846e+01 -5.7000e+02 -6.2857e+02 -1.5490e+01 -5.7000e+02 -6.0000e+02 -1.7306e+01 -5.7000e+02 -5.7143e+02 -1.9312e+01 -5.7000e+02 -5.4286e+02 -2.1523e+01 -5.7000e+02 -5.1429e+02 -2.3951e+01 -5.7000e+02 -4.8571e+02 -2.6604e+01 -5.7000e+02 -4.5714e+02 -2.9481e+01 -5.7000e+02 -4.2857e+02 -3.2571e+01 -5.7000e+02 -4.0000e+02 -3.5846e+01 -5.7000e+02 -3.7143e+02 -3.9263e+01 -5.7000e+02 -3.4286e+02 -4.2763e+01 -5.7000e+02 -3.1429e+02 -4.6274e+01 -5.7000e+02 -2.8571e+02 -4.9714e+01 -5.7000e+02 -2.5714e+02 -5.3006e+01 -5.7000e+02 -2.2857e+02 -5.6077e+01 -5.7000e+02 -2.0000e+02 -5.8867e+01 -5.7000e+02 -1.7143e+02 -6.1331e+01 -5.7000e+02 -1.4286e+02 -6.3439e+01 -5.7000e+02 -1.1429e+02 -6.5173e+01 -5.7000e+02 -8.5714e+01 -6.6524e+01 -5.7000e+02 -5.7143e+01 -6.7489e+01 -5.7000e+02 -2.8571e+01 -6.8067e+01 -5.7000e+02 0.0000e+00 -6.8260e+01 -5.7000e+02 2.8571e+01 -6.8067e+01 -5.7000e+02 5.7143e+01 -6.7489e+01 -5.7000e+02 8.5714e+01 -6.6524e+01 -5.7000e+02 1.1429e+02 -6.5173e+01 -5.7000e+02 1.4286e+02 -6.3439e+01 -5.7000e+02 1.7143e+02 -6.1331e+01 -5.7000e+02 2.0000e+02 -5.8867e+01 -5.7000e+02 2.2857e+02 -5.6077e+01 -5.7000e+02 2.5714e+02 -5.3006e+01 -5.7000e+02 2.8571e+02 -4.9714e+01 -5.7000e+02 3.1429e+02 -4.6274e+01 -5.7000e+02 3.4286e+02 -4.2763e+01 -5.7000e+02 3.7143e+02 -3.9263e+01 -5.7000e+02 4.0000e+02 -3.5846e+01 -5.7000e+02 4.2857e+02 -3.2571e+01 -5.7000e+02 4.5714e+02 -2.9481e+01 -5.7000e+02 4.8571e+02 -2.6604e+01 -5.7000e+02 5.1429e+02 -2.3951e+01 -5.7000e+02 5.4286e+02 -2.1523e+01 -5.7000e+02 5.7143e+02 -1.9312e+01 -5.7000e+02 6.0000e+02 -1.7306e+01 -5.7000e+02 6.2857e+02 -1.5490e+01 -5.7000e+02 6.5714e+02 -1.3846e+01 -5.7000e+02 6.8571e+02 -1.2358e+01 -5.7000e+02 7.1429e+02 -1.1010e+01 -5.7000e+02 7.4286e+02 -9.7872e+00 -5.7000e+02 7.7143e+02 -8.6758e+00 -5.7000e+02 8.0000e+02 -7.6638e+00 -5.7000e+02 8.2857e+02 -6.7404e+00 -5.7000e+02 8.5714e+02 -5.8959e+00 -5.7000e+02 8.8571e+02 -5.1218e+00 -5.7000e+02 9.1429e+02 -4.4107e+00 -5.7000e+02 9.4286e+02 -3.7560e+00 -5.7000e+02 9.7143e+02 -3.1518e+00 -5.7000e+02 1.0000e+03 -2.5931e+00 -5.7000e+02 1.0286e+03 -2.0753e+00 -5.7000e+02 1.0571e+03 -1.5945e+00 -5.7000e+02 1.0857e+03 -1.1471e+00 -5.7000e+02 1.1143e+03 -7.3003e-01 -5.7000e+02 1.1429e+03 -3.4051e-01 -5.7000e+02 1.1714e+03 2.3931e-02 -5.7000e+02 1.2000e+03 3.6548e-01 -5.7000e+02 1.2286e+03 6.8611e-01 -5.7000e+02 1.2571e+03 9.8756e-01 -5.7000e+02 1.2857e+03 1.2714e+00 -5.7000e+02 1.3143e+03 1.5391e+00 -5.7000e+02 1.3429e+03 1.7919e+00 -5.7000e+02 1.3714e+03 2.0309e+00 -5.7000e+02 1.4000e+03 2.2572e+00 -5.7000e+02 1.4286e+03 2.4717e+00 -5.7000e+02 1.4571e+03 2.6753e+00 -5.7000e+02 1.4857e+03 2.8688e+00 -5.7000e+02 1.5143e+03 3.0528e+00 -5.7000e+02 1.5429e+03 3.2281e+00 -5.7000e+02 1.5714e+03 3.3951e+00 -5.7000e+02 1.6000e+03 3.5545e+00 -5.7000e+02 1.6286e+03 3.7067e+00 -5.7000e+02 1.6571e+03 3.8523e+00 -5.7000e+02 1.6857e+03 3.9915e+00 -5.7000e+02 1.7143e+03 4.1248e+00 -5.7000e+02 1.7429e+03 4.2526e+00 -5.7000e+02 1.7714e+03 4.3752e+00 -5.7000e+02 1.8000e+03 4.4928e+00 -5.7000e+02 1.8286e+03 4.6058e+00 -5.7000e+02 1.8571e+03 4.7144e+00 -5.7000e+02 1.8857e+03 4.8189e+00 -5.7000e+02 1.9143e+03 4.9195e+00 -5.7000e+02 1.9429e+03 5.0164e+00 -5.7000e+02 1.9714e+03 5.1098e+00 -5.7000e+02 2.0000e+03 5.1998e+00 -6.0000e+02 -2.0000e+03 5.2268e+00 -6.0000e+02 -1.9714e+03 5.1381e+00 -6.0000e+02 -1.9429e+03 5.0462e+00 -6.0000e+02 -1.9143e+03 4.9509e+00 -6.0000e+02 -1.8857e+03 4.8520e+00 -6.0000e+02 -1.8571e+03 4.7493e+00 -6.0000e+02 -1.8286e+03 4.6427e+00 -6.0000e+02 -1.8000e+03 4.5318e+00 -6.0000e+02 -1.7714e+03 4.4164e+00 -6.0000e+02 -1.7429e+03 4.2962e+00 -6.0000e+02 -1.7143e+03 4.1711e+00 -6.0000e+02 -1.6857e+03 4.0406e+00 -6.0000e+02 -1.6571e+03 3.9044e+00 -6.0000e+02 -1.6286e+03 3.7622e+00 -6.0000e+02 -1.6000e+03 3.6136e+00 -6.0000e+02 -1.5714e+03 3.4581e+00 -6.0000e+02 -1.5429e+03 3.2953e+00 -6.0000e+02 -1.5143e+03 3.1247e+00 -6.0000e+02 -1.4857e+03 2.9457e+00 -6.0000e+02 -1.4571e+03 2.7578e+00 -6.0000e+02 -1.4286e+03 2.5602e+00 -6.0000e+02 -1.4000e+03 2.3523e+00 -6.0000e+02 -1.3714e+03 2.1333e+00 -6.0000e+02 -1.3429e+03 1.9023e+00 -6.0000e+02 -1.3143e+03 1.6584e+00 -6.0000e+02 -1.2857e+03 1.4006e+00 -6.0000e+02 -1.2571e+03 1.1276e+00 -6.0000e+02 -1.2286e+03 8.3825e-01 -6.0000e+02 -1.2000e+03 5.3111e-01 -6.0000e+02 -1.1714e+03 2.0462e-01 -6.0000e+02 -1.1429e+03 -1.4296e-01 -6.0000e+02 -1.1143e+03 -5.1355e-01 -6.0000e+02 -1.0857e+03 -9.0932e-01 -6.0000e+02 -1.0571e+03 -1.3327e+00 -6.0000e+02 -1.0286e+03 -1.7863e+00 -6.0000e+02 -1.0000e+03 -2.2732e+00 -6.0000e+02 -9.7143e+02 -2.7967e+00 -6.0000e+02 -9.4286e+02 -3.3607e+00 -6.0000e+02 -9.1429e+02 -3.9694e+00 -6.0000e+02 -8.8571e+02 -4.6277e+00 -6.0000e+02 -8.5714e+02 -5.3408e+00 -6.0000e+02 -8.2857e+02 -6.1149e+00 -6.0000e+02 -8.0000e+02 -6.9567e+00 -6.0000e+02 -7.7143e+02 -7.8737e+00 -6.0000e+02 -7.4286e+02 -8.8745e+00 -6.0000e+02 -7.1429e+02 -9.9684e+00 -6.0000e+02 -6.8571e+02 -1.1166e+01 -6.0000e+02 -6.5714e+02 -1.2477e+01 -6.0000e+02 -6.2857e+02 -1.3914e+01 -6.0000e+02 -6.0000e+02 -1.5490e+01 -6.0000e+02 -5.7143e+02 -1.7216e+01 -6.0000e+02 -5.4286e+02 -1.9103e+01 -6.0000e+02 -5.1429e+02 -2.1161e+01 -6.0000e+02 -4.8571e+02 -2.3395e+01 -6.0000e+02 -4.5714e+02 -2.5807e+01 -6.0000e+02 -4.2857e+02 -2.8389e+01 -6.0000e+02 -4.0000e+02 -3.1126e+01 -6.0000e+02 -3.7143e+02 -3.3991e+01 -6.0000e+02 -3.4286e+02 -3.6945e+01 -6.0000e+02 -3.1429e+02 -3.9937e+01 -6.0000e+02 -2.8571e+02 -4.2910e+01 -6.0000e+02 -2.5714e+02 -4.5800e+01 -6.0000e+02 -2.2857e+02 -4.8544e+01 -6.0000e+02 -2.0000e+02 -5.1081e+01 -6.0000e+02 -1.7143e+02 -5.3362e+01 -6.0000e+02 -1.4286e+02 -5.5346e+01 -6.0000e+02 -1.1429e+02 -5.7000e+01 -6.0000e+02 -8.5714e+01 -5.8304e+01 -6.0000e+02 -5.7143e+01 -5.9244e+01 -6.0000e+02 -2.8571e+01 -5.9811e+01 -6.0000e+02 0.0000e+00 -6.0000e+01 -6.0000e+02 2.8571e+01 -5.9811e+01 -6.0000e+02 5.7143e+01 -5.9244e+01 -6.0000e+02 8.5714e+01 -5.8304e+01 -6.0000e+02 1.1429e+02 -5.7000e+01 -6.0000e+02 1.4286e+02 -5.5346e+01 -6.0000e+02 1.7143e+02 -5.3362e+01 -6.0000e+02 2.0000e+02 -5.1081e+01 -6.0000e+02 2.2857e+02 -4.8544e+01 -6.0000e+02 2.5714e+02 -4.5800e+01 -6.0000e+02 2.8571e+02 -4.2910e+01 -6.0000e+02 3.1429e+02 -3.9937e+01 -6.0000e+02 3.4286e+02 -3.6945e+01 -6.0000e+02 3.7143e+02 -3.3991e+01 -6.0000e+02 4.0000e+02 -3.1126e+01 -6.0000e+02 4.2857e+02 -2.8389e+01 -6.0000e+02 4.5714e+02 -2.5807e+01 -6.0000e+02 4.8571e+02 -2.3395e+01 -6.0000e+02 5.1429e+02 -2.1161e+01 -6.0000e+02 5.4286e+02 -1.9103e+01 -6.0000e+02 5.7143e+02 -1.7216e+01 -6.0000e+02 6.0000e+02 -1.5490e+01 -6.0000e+02 6.2857e+02 -1.3914e+01 -6.0000e+02 6.5714e+02 -1.2477e+01 -6.0000e+02 6.8571e+02 -1.1166e+01 -6.0000e+02 7.1429e+02 -9.9684e+00 -6.0000e+02 7.4286e+02 -8.8745e+00 -6.0000e+02 7.7143e+02 -7.8737e+00 -6.0000e+02 8.0000e+02 -6.9567e+00 -6.0000e+02 8.2857e+02 -6.1149e+00 -6.0000e+02 8.5714e+02 -5.3408e+00 -6.0000e+02 8.8571e+02 -4.6277e+00 -6.0000e+02 9.1429e+02 -3.9694e+00 -6.0000e+02 9.4286e+02 -3.3607e+00 -6.0000e+02 9.7143e+02 -2.7967e+00 -6.0000e+02 1.0000e+03 -2.2732e+00 -6.0000e+02 1.0286e+03 -1.7863e+00 -6.0000e+02 1.0571e+03 -1.3327e+00 -6.0000e+02 1.0857e+03 -9.0932e-01 -6.0000e+02 1.1143e+03 -5.1355e-01 -6.0000e+02 1.1429e+03 -1.4296e-01 -6.0000e+02 1.1714e+03 2.0462e-01 -6.0000e+02 1.2000e+03 5.3111e-01 -6.0000e+02 1.2286e+03 8.3825e-01 -6.0000e+02 1.2571e+03 1.1276e+00 -6.0000e+02 1.2857e+03 1.4006e+00 -6.0000e+02 1.3143e+03 1.6584e+00 -6.0000e+02 1.3429e+03 1.9023e+00 -6.0000e+02 1.3714e+03 2.1333e+00 -6.0000e+02 1.4000e+03 2.3523e+00 -6.0000e+02 1.4286e+03 2.5602e+00 -6.0000e+02 1.4571e+03 2.7578e+00 -6.0000e+02 1.4857e+03 2.9457e+00 -6.0000e+02 1.5143e+03 3.1247e+00 -6.0000e+02 1.5429e+03 3.2953e+00 -6.0000e+02 1.5714e+03 3.4581e+00 -6.0000e+02 1.6000e+03 3.6136e+00 -6.0000e+02 1.6286e+03 3.7622e+00 -6.0000e+02 1.6571e+03 3.9044e+00 -6.0000e+02 1.6857e+03 4.0406e+00 -6.0000e+02 1.7143e+03 4.1711e+00 -6.0000e+02 1.7429e+03 4.2962e+00 -6.0000e+02 1.7714e+03 4.4164e+00 -6.0000e+02 1.8000e+03 4.5318e+00 -6.0000e+02 1.8286e+03 4.6427e+00 -6.0000e+02 1.8571e+03 4.7493e+00 -6.0000e+02 1.8857e+03 4.8520e+00 -6.0000e+02 1.9143e+03 4.9509e+00 -6.0000e+02 1.9429e+03 5.0462e+00 -6.0000e+02 1.9714e+03 5.1381e+00 -6.0000e+02 2.0000e+03 5.2268e+00 -6.3000e+02 -2.0000e+03 5.2547e+00 -6.3000e+02 -1.9714e+03 5.1674e+00 -6.3000e+02 -1.9429e+03 5.0770e+00 -6.3000e+02 -1.9143e+03 4.9834e+00 -6.3000e+02 -1.8857e+03 4.8862e+00 -6.3000e+02 -1.8571e+03 4.7854e+00 -6.3000e+02 -1.8286e+03 4.6807e+00 -6.3000e+02 -1.8000e+03 4.5719e+00 -6.3000e+02 -1.7714e+03 4.4588e+00 -6.3000e+02 -1.7429e+03 4.3412e+00 -6.3000e+02 -1.7143e+03 4.2187e+00 -6.3000e+02 -1.6857e+03 4.0911e+00 -6.3000e+02 -1.6571e+03 3.9580e+00 -6.3000e+02 -1.6286e+03 3.8192e+00 -6.3000e+02 -1.6000e+03 3.6742e+00 -6.3000e+02 -1.5714e+03 3.5226e+00 -6.3000e+02 -1.5429e+03 3.3641e+00 -6.3000e+02 -1.5143e+03 3.1981e+00 -6.3000e+02 -1.4857e+03 3.0242e+00 -6.3000e+02 -1.4571e+03 2.8418e+00 -6.3000e+02 -1.4286e+03 2.6503e+00 -6.3000e+02 -1.4000e+03 2.4491e+00 -6.3000e+02 -1.3714e+03 2.2374e+00 -6.3000e+02 -1.3429e+03 2.0144e+00 -6.3000e+02 -1.3143e+03 1.7793e+00 -6.3000e+02 -1.2857e+03 1.5312e+00 -6.3000e+02 -1.2571e+03 1.2690e+00 -6.3000e+02 -1.2286e+03 9.9155e-01 -6.3000e+02 -1.2000e+03 6.9765e-01 -6.3000e+02 -1.1714e+03 3.8590e-01 -6.3000e+02 -1.1429e+03 5.4774e-02 -6.3000e+02 -1.1143e+03 -2.9743e-01 -6.3000e+02 -1.0857e+03 -6.7257e-01 -6.3000e+02 -1.0571e+03 -1.0727e+00 -6.3000e+02 -1.0286e+03 -1.5002e+00 -6.3000e+02 -1.0000e+03 -1.9576e+00 -6.3000e+02 -9.7143e+02 -2.4477e+00 -6.3000e+02 -9.4286e+02 -2.9738e+00 -6.3000e+02 -9.1429e+02 -3.5392e+00 -6.3000e+02 -8.8571e+02 -4.1481e+00 -6.3000e+02 -8.5714e+02 -4.8047e+00 -6.3000e+02 -8.2857e+02 -5.5139e+00 -6.3000e+02 -8.0000e+02 -6.2810e+00 -6.3000e+02 -7.7143e+02 -7.1119e+00 -6.3000e+02 -7.4286e+02 -8.0132e+00 -6.3000e+02 -7.1429e+02 -8.9919e+00 -6.3000e+02 -6.8571e+02 -1.0056e+01 -6.3000e+02 -6.5714e+02 -1.1213e+01 -6.3000e+02 -6.2857e+02 -1.2471e+01 -6.3000e+02 -6.0000e+02 -1.3839e+01 -6.3000e+02 -5.7143e+02 -1.5326e+01 -6.3000e+02 -5.4286e+02 -1.6939e+01 -6.3000e+02 -5.1429e+02 -1.8684e+01 -6.3000e+02 -4.8571e+02 -2.0565e+01 -6.3000e+02 -4.5714e+02 -2.2581e+01 -6.3000e+02 -4.2857e+02 -2.4730e+01 -6.3000e+02 -4.0000e+02 -2.6998e+01 -6.3000e+02 -3.7143e+02 -2.9370e+01 -6.3000e+02 -3.4286e+02 -3.1817e+01 -6.3000e+02 -3.1429e+02 -3.4306e+01 -6.3000e+02 -2.8571e+02 -3.6795e+01 -6.3000e+02 -2.5714e+02 -3.9236e+01 -6.3000e+02 -2.2857e+02 -4.1579e+01 -6.3000e+02 -2.0000e+02 -4.3772e+01 -6.3000e+02 -1.7143e+02 -4.5769e+01 -6.3000e+02 -1.4286e+02 -4.7526e+01 -6.3000e+02 -1.1429e+02 -4.9009e+01 -6.3000e+02 -8.5714e+01 -5.0189e+01 -6.3000e+02 -5.7143e+01 -5.1047e+01 -6.3000e+02 -2.8571e+01 -5.1566e+01 -6.3000e+02 0.0000e+00 -5.1740e+01 -6.3000e+02 2.8571e+01 -5.1566e+01 -6.3000e+02 5.7143e+01 -5.1047e+01 -6.3000e+02 8.5714e+01 -5.0189e+01 -6.3000e+02 1.1429e+02 -4.9009e+01 -6.3000e+02 1.4286e+02 -4.7526e+01 -6.3000e+02 1.7143e+02 -4.5769e+01 -6.3000e+02 2.0000e+02 -4.3772e+01 -6.3000e+02 2.2857e+02 -4.1579e+01 -6.3000e+02 2.5714e+02 -3.9236e+01 -6.3000e+02 2.8571e+02 -3.6795e+01 -6.3000e+02 3.1429e+02 -3.4306e+01 -6.3000e+02 3.4286e+02 -3.1817e+01 -6.3000e+02 3.7143e+02 -2.9370e+01 -6.3000e+02 4.0000e+02 -2.6998e+01 -6.3000e+02 4.2857e+02 -2.4730e+01 -6.3000e+02 4.5714e+02 -2.2581e+01 -6.3000e+02 4.8571e+02 -2.0565e+01 -6.3000e+02 5.1429e+02 -1.8684e+01 -6.3000e+02 5.4286e+02 -1.6939e+01 -6.3000e+02 5.7143e+02 -1.5326e+01 -6.3000e+02 6.0000e+02 -1.3839e+01 -6.3000e+02 6.2857e+02 -1.2471e+01 -6.3000e+02 6.5714e+02 -1.1213e+01 -6.3000e+02 6.8571e+02 -1.0056e+01 -6.3000e+02 7.1429e+02 -8.9919e+00 -6.3000e+02 7.4286e+02 -8.0132e+00 -6.3000e+02 7.7143e+02 -7.1119e+00 -6.3000e+02 8.0000e+02 -6.2810e+00 -6.3000e+02 8.2857e+02 -5.5139e+00 -6.3000e+02 8.5714e+02 -4.8047e+00 -6.3000e+02 8.8571e+02 -4.1481e+00 -6.3000e+02 9.1429e+02 -3.5392e+00 -6.3000e+02 9.4286e+02 -2.9738e+00 -6.3000e+02 9.7143e+02 -2.4477e+00 -6.3000e+02 1.0000e+03 -1.9576e+00 -6.3000e+02 1.0286e+03 -1.5002e+00 -6.3000e+02 1.0571e+03 -1.0727e+00 -6.3000e+02 1.0857e+03 -6.7257e-01 -6.3000e+02 1.1143e+03 -2.9743e-01 -6.3000e+02 1.1429e+03 5.4774e-02 -6.3000e+02 1.1714e+03 3.8590e-01 -6.3000e+02 1.2000e+03 6.9765e-01 -6.3000e+02 1.2286e+03 9.9155e-01 -6.3000e+02 1.2571e+03 1.2690e+00 -6.3000e+02 1.2857e+03 1.5312e+00 -6.3000e+02 1.3143e+03 1.7793e+00 -6.3000e+02 1.3429e+03 2.0144e+00 -6.3000e+02 1.3714e+03 2.2374e+00 -6.3000e+02 1.4000e+03 2.4491e+00 -6.3000e+02 1.4286e+03 2.6503e+00 -6.3000e+02 1.4571e+03 2.8418e+00 -6.3000e+02 1.4857e+03 3.0242e+00 -6.3000e+02 1.5143e+03 3.1981e+00 -6.3000e+02 1.5429e+03 3.3641e+00 -6.3000e+02 1.5714e+03 3.5226e+00 -6.3000e+02 1.6000e+03 3.6742e+00 -6.3000e+02 1.6286e+03 3.8192e+00 -6.3000e+02 1.6571e+03 3.9580e+00 -6.3000e+02 1.6857e+03 4.0911e+00 -6.3000e+02 1.7143e+03 4.2187e+00 -6.3000e+02 1.7429e+03 4.3412e+00 -6.3000e+02 1.7714e+03 4.4588e+00 -6.3000e+02 1.8000e+03 4.5719e+00 -6.3000e+02 1.8286e+03 4.6807e+00 -6.3000e+02 1.8571e+03 4.7854e+00 -6.3000e+02 1.8857e+03 4.8862e+00 -6.3000e+02 1.9143e+03 4.9834e+00 -6.3000e+02 1.9429e+03 5.0770e+00 -6.3000e+02 1.9714e+03 5.1674e+00 -6.3000e+02 2.0000e+03 5.2547e+00 -6.6000e+02 -2.0000e+03 5.2835e+00 -6.6000e+02 -1.9714e+03 5.1977e+00 -6.6000e+02 -1.9429e+03 5.1088e+00 -6.6000e+02 -1.9143e+03 5.0168e+00 -6.6000e+02 -1.8857e+03 4.9214e+00 -6.6000e+02 -1.8571e+03 4.8225e+00 -6.6000e+02 -1.8286e+03 4.7198e+00 -6.6000e+02 -1.8000e+03 4.6132e+00 -6.6000e+02 -1.7714e+03 4.5025e+00 -6.6000e+02 -1.7429e+03 4.3873e+00 -6.6000e+02 -1.7143e+03 4.2675e+00 -6.6000e+02 -1.6857e+03 4.1428e+00 -6.6000e+02 -1.6571e+03 4.0129e+00 -6.6000e+02 -1.6286e+03 3.8774e+00 -6.6000e+02 -1.6000e+03 3.7361e+00 -6.6000e+02 -1.5714e+03 3.5885e+00 -6.6000e+02 -1.5429e+03 3.4343e+00 -6.6000e+02 -1.5143e+03 3.2730e+00 -6.6000e+02 -1.4857e+03 3.1042e+00 -6.6000e+02 -1.4571e+03 2.9273e+00 -6.6000e+02 -1.4286e+03 2.7419e+00 -6.6000e+02 -1.4000e+03 2.5472e+00 -6.6000e+02 -1.3714e+03 2.3427e+00 -6.6000e+02 -1.3429e+03 2.1277e+00 -6.6000e+02 -1.3143e+03 1.9013e+00 -6.6000e+02 -1.2857e+03 1.6628e+00 -6.6000e+02 -1.2571e+03 1.4112e+00 -6.6000e+02 -1.2286e+03 1.1455e+00 -6.6000e+02 -1.2000e+03 8.6453e-01 -6.6000e+02 -1.1714e+03 5.6715e-01 -6.6000e+02 -1.1429e+03 2.5201e-01 -6.6000e+02 -1.1143e+03 -8.2383e-02 -6.6000e+02 -1.0857e+03 -4.3764e-01 -6.6000e+02 -1.0571e+03 -8.1554e-01 -6.6000e+02 -1.0286e+03 -1.2181e+00 -6.6000e+02 -1.0000e+03 -1.6473e+00 -6.6000e+02 -9.7143e+02 -2.1058e+00 -6.6000e+02 -9.4286e+02 -2.5961e+00 -6.6000e+02 -9.1429e+02 -3.1211e+00 -6.6000e+02 -8.8571e+02 -3.6840e+00 -6.6000e+02 -8.5714e+02 -4.2883e+00 -6.6000e+02 -8.2857e+02 -4.9379e+00 -6.6000e+02 -8.0000e+02 -5.6369e+00 -6.6000e+02 -7.7143e+02 -6.3900e+00 -6.6000e+02 -7.4286e+02 -7.2021e+00 -6.6000e+02 -7.1429e+02 -8.0784e+00 -6.6000e+02 -6.8571e+02 -9.0245e+00 -6.6000e+02 -6.5714e+02 -1.0046e+01 -6.6000e+02 -6.2857e+02 -1.1150e+01 -6.6000e+02 -6.0000e+02 -1.2340e+01 -6.6000e+02 -5.7143e+02 -1.3624e+01 -6.6000e+02 -5.4286e+02 -1.5005e+01 -6.6000e+02 -5.1429e+02 -1.6488e+01 -6.6000e+02 -4.8571e+02 -1.8073e+01 -6.6000e+02 -4.5714e+02 -1.9761e+01 -6.6000e+02 -4.2857e+02 -2.1547e+01 -6.6000e+02 -4.0000e+02 -2.3422e+01 -6.6000e+02 -3.7143e+02 -2.5374e+01 -6.6000e+02 -3.4286e+02 -2.7383e+01 -6.6000e+02 -3.1429e+02 -2.9425e+01 -6.6000e+02 -2.8571e+02 -3.1468e+01 -6.6000e+02 -2.5714e+02 -3.3479e+01 -6.6000e+02 -2.2857e+02 -3.5417e+01 -6.6000e+02 -2.0000e+02 -3.7243e+01 -6.6000e+02 -1.7143e+02 -3.8917e+01 -6.6000e+02 -1.4286e+02 -4.0401e+01 -6.6000e+02 -1.1429e+02 -4.1663e+01 -6.6000e+02 -8.5714e+01 -4.2674e+01 -6.6000e+02 -5.7143e+01 -4.3412e+01 -6.6000e+02 -2.8571e+01 -4.3861e+01 -6.6000e+02 0.0000e+00 -4.4012e+01 -6.6000e+02 2.8571e+01 -4.3861e+01 -6.6000e+02 5.7143e+01 -4.3412e+01 -6.6000e+02 8.5714e+01 -4.2674e+01 -6.6000e+02 1.1429e+02 -4.1663e+01 -6.6000e+02 1.4286e+02 -4.0401e+01 -6.6000e+02 1.7143e+02 -3.8917e+01 -6.6000e+02 2.0000e+02 -3.7243e+01 -6.6000e+02 2.2857e+02 -3.5417e+01 -6.6000e+02 2.5714e+02 -3.3479e+01 -6.6000e+02 2.8571e+02 -3.1468e+01 -6.6000e+02 3.1429e+02 -2.9425e+01 -6.6000e+02 3.4286e+02 -2.7383e+01 -6.6000e+02 3.7143e+02 -2.5374e+01 -6.6000e+02 4.0000e+02 -2.3422e+01 -6.6000e+02 4.2857e+02 -2.1547e+01 -6.6000e+02 4.5714e+02 -1.9761e+01 -6.6000e+02 4.8571e+02 -1.8073e+01 -6.6000e+02 5.1429e+02 -1.6488e+01 -6.6000e+02 5.4286e+02 -1.5005e+01 -6.6000e+02 5.7143e+02 -1.3624e+01 -6.6000e+02 6.0000e+02 -1.2340e+01 -6.6000e+02 6.2857e+02 -1.1150e+01 -6.6000e+02 6.5714e+02 -1.0046e+01 -6.6000e+02 6.8571e+02 -9.0245e+00 -6.6000e+02 7.1429e+02 -8.0784e+00 -6.6000e+02 7.4286e+02 -7.2021e+00 -6.6000e+02 7.7143e+02 -6.3900e+00 -6.6000e+02 8.0000e+02 -5.6369e+00 -6.6000e+02 8.2857e+02 -4.9379e+00 -6.6000e+02 8.5714e+02 -4.2883e+00 -6.6000e+02 8.8571e+02 -3.6840e+00 -6.6000e+02 9.1429e+02 -3.1211e+00 -6.6000e+02 9.4286e+02 -2.5961e+00 -6.6000e+02 9.7143e+02 -2.1058e+00 -6.6000e+02 1.0000e+03 -1.6473e+00 -6.6000e+02 1.0286e+03 -1.2181e+00 -6.6000e+02 1.0571e+03 -8.1554e-01 -6.6000e+02 1.0857e+03 -4.3764e-01 -6.6000e+02 1.1143e+03 -8.2383e-02 -6.6000e+02 1.1429e+03 2.5201e-01 -6.6000e+02 1.1714e+03 5.6715e-01 -6.6000e+02 1.2000e+03 8.6453e-01 -6.6000e+02 1.2286e+03 1.1455e+00 -6.6000e+02 1.2571e+03 1.4112e+00 -6.6000e+02 1.2857e+03 1.6628e+00 -6.6000e+02 1.3143e+03 1.9013e+00 -6.6000e+02 1.3429e+03 2.1277e+00 -6.6000e+02 1.3714e+03 2.3427e+00 -6.6000e+02 1.4000e+03 2.5472e+00 -6.6000e+02 1.4286e+03 2.7419e+00 -6.6000e+02 1.4571e+03 2.9273e+00 -6.6000e+02 1.4857e+03 3.1042e+00 -6.6000e+02 1.5143e+03 3.2730e+00 -6.6000e+02 1.5429e+03 3.4343e+00 -6.6000e+02 1.5714e+03 3.5885e+00 -6.6000e+02 1.6000e+03 3.7361e+00 -6.6000e+02 1.6286e+03 3.8774e+00 -6.6000e+02 1.6571e+03 4.0129e+00 -6.6000e+02 1.6857e+03 4.1428e+00 -6.6000e+02 1.7143e+03 4.2675e+00 -6.6000e+02 1.7429e+03 4.3873e+00 -6.6000e+02 1.7714e+03 4.5025e+00 -6.6000e+02 1.8000e+03 4.6132e+00 -6.6000e+02 1.8286e+03 4.7198e+00 -6.6000e+02 1.8571e+03 4.8225e+00 -6.6000e+02 1.8857e+03 4.9214e+00 -6.6000e+02 1.9143e+03 5.0168e+00 -6.6000e+02 1.9429e+03 5.1088e+00 -6.6000e+02 1.9714e+03 5.1977e+00 -6.6000e+02 2.0000e+03 5.2835e+00 -6.9000e+02 -2.0000e+03 5.3131e+00 -6.9000e+02 -1.9714e+03 5.2288e+00 -6.9000e+02 -1.9429e+03 5.1415e+00 -6.9000e+02 -1.9143e+03 5.0512e+00 -6.9000e+02 -1.8857e+03 4.9576e+00 -6.9000e+02 -1.8571e+03 4.8606e+00 -6.9000e+02 -1.8286e+03 4.7600e+00 -6.9000e+02 -1.8000e+03 4.6556e+00 -6.9000e+02 -1.7714e+03 4.5472e+00 -6.9000e+02 -1.7429e+03 4.4345e+00 -6.9000e+02 -1.7143e+03 4.3175e+00 -6.9000e+02 -1.6857e+03 4.1957e+00 -6.9000e+02 -1.6571e+03 4.0689e+00 -6.9000e+02 -1.6286e+03 3.9368e+00 -6.9000e+02 -1.6000e+03 3.7992e+00 -6.9000e+02 -1.5714e+03 3.6556e+00 -6.9000e+02 -1.5429e+03 3.5057e+00 -6.9000e+02 -1.5143e+03 3.3491e+00 -6.9000e+02 -1.4857e+03 3.1853e+00 -6.9000e+02 -1.4571e+03 3.0140e+00 -6.9000e+02 -1.4286e+03 2.8345e+00 -6.9000e+02 -1.4000e+03 2.6464e+00 -6.9000e+02 -1.3714e+03 2.4491e+00 -6.9000e+02 -1.3429e+03 2.2419e+00 -6.9000e+02 -1.3143e+03 2.0242e+00 -6.9000e+02 -1.2857e+03 1.7951e+00 -6.9000e+02 -1.2571e+03 1.5538e+00 -6.9000e+02 -1.2286e+03 1.2995e+00 -6.9000e+02 -1.2000e+03 1.0312e+00 -6.9000e+02 -1.1714e+03 7.4781e-01 -6.9000e+02 -1.1429e+03 4.4813e-01 -6.9000e+02 -1.1143e+03 1.3092e-01 -6.9000e+02 -1.0857e+03 -2.0522e-01 -6.9000e+02 -1.0571e+03 -5.6181e-01 -6.9000e+02 -1.0286e+03 -9.4053e-01 -6.9000e+02 -1.0000e+03 -1.3432e+00 -6.9000e+02 -9.7143e+02 -1.7718e+00 -6.9000e+02 -9.4286e+02 -2.2285e+00 -6.9000e+02 -9.1429e+02 -2.7157e+00 -6.9000e+02 -8.8571e+02 -3.2360e+00 -6.9000e+02 -8.5714e+02 -3.7921e+00 -6.9000e+02 -8.2857e+02 -4.3871e+00 -6.9000e+02 -8.0000e+02 -5.0243e+00 -6.9000e+02 -7.7143e+02 -5.7071e+00 -6.9000e+02 -7.4286e+02 -6.4392e+00 -6.9000e+02 -7.1429e+02 -7.2246e+00 -6.9000e+02 -6.8571e+02 -8.0672e+00 -6.9000e+02 -6.5714e+02 -8.9712e+00 -6.9000e+02 -6.2857e+02 -9.9405e+00 -6.9000e+02 -6.0000e+02 -1.0979e+01 -6.9000e+02 -5.7143e+02 -1.2090e+01 -6.9000e+02 -5.4286e+02 -1.3276e+01 -6.9000e+02 -5.1429e+02 -1.4540e+01 -6.9000e+02 -4.8571e+02 -1.5880e+01 -6.9000e+02 -4.5714e+02 -1.7296e+01 -6.9000e+02 -4.2857e+02 -1.8784e+01 -6.9000e+02 -4.0000e+02 -2.0336e+01 -6.9000e+02 -3.7143e+02 -2.1942e+01 -6.9000e+02 -3.4286e+02 -2.3586e+01 -6.9000e+02 -3.1429e+02 -2.5251e+01 -6.9000e+02 -2.8571e+02 -2.6914e+01 -6.9000e+02 -2.5714e+02 -2.8548e+01 -6.9000e+02 -2.2857e+02 -3.0123e+01 -6.9000e+02 -2.0000e+02 -3.1610e+01 -6.9000e+02 -1.7143e+02 -3.2976e+01 -6.9000e+02 -1.4286e+02 -3.4191e+01 -6.9000e+02 -1.1429e+02 -3.5227e+01 -6.9000e+02 -8.5714e+01 -3.6060e+01 -6.9000e+02 -5.7143e+01 -3.6670e+01 -6.9000e+02 -2.8571e+01 -3.7041e+01 -6.9000e+02 0.0000e+00 -3.7166e+01 -6.9000e+02 2.8571e+01 -3.7041e+01 -6.9000e+02 5.7143e+01 -3.6670e+01 -6.9000e+02 8.5714e+01 -3.6060e+01 -6.9000e+02 1.1429e+02 -3.5227e+01 -6.9000e+02 1.4286e+02 -3.4191e+01 -6.9000e+02 1.7143e+02 -3.2976e+01 -6.9000e+02 2.0000e+02 -3.1610e+01 -6.9000e+02 2.2857e+02 -3.0123e+01 -6.9000e+02 2.5714e+02 -2.8548e+01 -6.9000e+02 2.8571e+02 -2.6914e+01 -6.9000e+02 3.1429e+02 -2.5251e+01 -6.9000e+02 3.4286e+02 -2.3586e+01 -6.9000e+02 3.7143e+02 -2.1942e+01 -6.9000e+02 4.0000e+02 -2.0336e+01 -6.9000e+02 4.2857e+02 -1.8784e+01 -6.9000e+02 4.5714e+02 -1.7296e+01 -6.9000e+02 4.8571e+02 -1.5880e+01 -6.9000e+02 5.1429e+02 -1.4540e+01 -6.9000e+02 5.4286e+02 -1.3276e+01 -6.9000e+02 5.7143e+02 -1.2090e+01 -6.9000e+02 6.0000e+02 -1.0979e+01 -6.9000e+02 6.2857e+02 -9.9405e+00 -6.9000e+02 6.5714e+02 -8.9712e+00 -6.9000e+02 6.8571e+02 -8.0672e+00 -6.9000e+02 7.1429e+02 -7.2246e+00 -6.9000e+02 7.4286e+02 -6.4392e+00 -6.9000e+02 7.7143e+02 -5.7071e+00 -6.9000e+02 8.0000e+02 -5.0243e+00 -6.9000e+02 8.2857e+02 -4.3871e+00 -6.9000e+02 8.5714e+02 -3.7921e+00 -6.9000e+02 8.8571e+02 -3.2360e+00 -6.9000e+02 9.1429e+02 -2.7157e+00 -6.9000e+02 9.4286e+02 -2.2285e+00 -6.9000e+02 9.7143e+02 -1.7718e+00 -6.9000e+02 1.0000e+03 -1.3432e+00 -6.9000e+02 1.0286e+03 -9.4053e-01 -6.9000e+02 1.0571e+03 -5.6181e-01 -6.9000e+02 1.0857e+03 -2.0522e-01 -6.9000e+02 1.1143e+03 1.3092e-01 -6.9000e+02 1.1429e+03 4.4813e-01 -6.9000e+02 1.1714e+03 7.4781e-01 -6.9000e+02 1.2000e+03 1.0312e+00 -6.9000e+02 1.2286e+03 1.2995e+00 -6.9000e+02 1.2571e+03 1.5538e+00 -6.9000e+02 1.2857e+03 1.7951e+00 -6.9000e+02 1.3143e+03 2.0242e+00 -6.9000e+02 1.3429e+03 2.2419e+00 -6.9000e+02 1.3714e+03 2.4491e+00 -6.9000e+02 1.4000e+03 2.6464e+00 -6.9000e+02 1.4286e+03 2.8345e+00 -6.9000e+02 1.4571e+03 3.0140e+00 -6.9000e+02 1.4857e+03 3.1853e+00 -6.9000e+02 1.5143e+03 3.3491e+00 -6.9000e+02 1.5429e+03 3.5057e+00 -6.9000e+02 1.5714e+03 3.6556e+00 -6.9000e+02 1.6000e+03 3.7992e+00 -6.9000e+02 1.6286e+03 3.9368e+00 -6.9000e+02 1.6571e+03 4.0689e+00 -6.9000e+02 1.6857e+03 4.1957e+00 -6.9000e+02 1.7143e+03 4.3175e+00 -6.9000e+02 1.7429e+03 4.4345e+00 -6.9000e+02 1.7714e+03 4.5472e+00 -6.9000e+02 1.8000e+03 4.6556e+00 -6.9000e+02 1.8286e+03 4.7600e+00 -6.9000e+02 1.8571e+03 4.8606e+00 -6.9000e+02 1.8857e+03 4.9576e+00 -6.9000e+02 1.9143e+03 5.0512e+00 -6.9000e+02 1.9429e+03 5.1415e+00 -6.9000e+02 1.9714e+03 5.2288e+00 -6.9000e+02 2.0000e+03 5.3131e+00 -7.2000e+02 -2.0000e+03 5.3435e+00 -7.2000e+02 -1.9714e+03 5.2607e+00 -7.2000e+02 -1.9429e+03 5.1750e+00 -7.2000e+02 -1.9143e+03 5.0864e+00 -7.2000e+02 -1.8857e+03 4.9946e+00 -7.2000e+02 -1.8571e+03 4.8995e+00 -7.2000e+02 -1.8286e+03 4.8010e+00 -7.2000e+02 -1.8000e+03 4.6988e+00 -7.2000e+02 -1.7714e+03 4.5928e+00 -7.2000e+02 -1.7429e+03 4.4827e+00 -7.2000e+02 -1.7143e+03 4.3684e+00 -7.2000e+02 -1.6857e+03 4.2496e+00 -7.2000e+02 -1.6571e+03 4.1259e+00 -7.2000e+02 -1.6286e+03 3.9973e+00 -7.2000e+02 -1.6000e+03 3.8633e+00 -7.2000e+02 -1.5714e+03 3.7237e+00 -7.2000e+02 -1.5429e+03 3.5781e+00 -7.2000e+02 -1.5143e+03 3.4261e+00 -7.2000e+02 -1.4857e+03 3.2674e+00 -7.2000e+02 -1.4571e+03 3.1016e+00 -7.2000e+02 -1.4286e+03 2.9281e+00 -7.2000e+02 -1.4000e+03 2.7465e+00 -7.2000e+02 -1.3714e+03 2.5562e+00 -7.2000e+02 -1.3429e+03 2.3567e+00 -7.2000e+02 -1.3143e+03 2.1474e+00 -7.2000e+02 -1.2857e+03 1.9276e+00 -7.2000e+02 -1.2571e+03 1.6965e+00 -7.2000e+02 -1.2286e+03 1.4533e+00 -7.2000e+02 -1.2000e+03 1.1973e+00 -7.2000e+02 -1.1714e+03 9.2737e-01 -7.2000e+02 -1.1429e+03 6.4262e-01 -7.2000e+02 -1.1143e+03 3.4193e-01 -7.2000e+02 -1.0857e+03 2.4092e-02 -7.2000e+02 -1.0571e+03 -3.1218e-01 -7.2000e+02 -1.0286e+03 -6.6830e-01 -7.2000e+02 -1.0000e+03 -1.0458e+00 -7.2000e+02 -9.7143e+02 -1.4463e+00 -7.2000e+02 -9.4286e+02 -1.8716e+00 -7.2000e+02 -9.1429e+02 -2.3236e+00 -7.2000e+02 -8.8571e+02 -2.8044e+00 -7.2000e+02 -8.5714e+02 -3.3162e+00 -7.2000e+02 -8.2857e+02 -3.8613e+00 -7.2000e+02 -8.0000e+02 -4.4424e+00 -7.2000e+02 -7.7143e+02 -5.0618e+00 -7.2000e+02 -7.4286e+02 -5.7226e+00 -7.2000e+02 -7.1429e+02 -6.4273e+00 -7.2000e+02 -6.8571e+02 -7.1789e+00 -7.2000e+02 -6.5714e+02 -7.9801e+00 -7.2000e+02 -6.2857e+02 -8.8336e+00 -7.2000e+02 -6.0000e+02 -9.7417e+00 -7.2000e+02 -5.7143e+02 -1.0706e+01 -7.2000e+02 -5.4286e+02 -1.1729e+01 -7.2000e+02 -5.1429e+02 -1.2809e+01 -7.2000e+02 -4.8571e+02 -1.3947e+01 -7.2000e+02 -4.5714e+02 -1.5140e+01 -7.2000e+02 -4.2857e+02 -1.6385e+01 -7.2000e+02 -4.0000e+02 -1.7673e+01 -7.2000e+02 -3.7143e+02 -1.8998e+01 -7.2000e+02 -3.4286e+02 -2.0346e+01 -7.2000e+02 -3.1429e+02 -2.1704e+01 -7.2000e+02 -2.8571e+02 -2.3054e+01 -7.2000e+02 -2.5714e+02 -2.4377e+01 -7.2000e+02 -2.2857e+02 -2.5648e+01 -7.2000e+02 -2.0000e+02 -2.6846e+01 -7.2000e+02 -1.7143e+02 -2.7947e+01 -7.2000e+02 -1.4286e+02 -2.8925e+01 -7.2000e+02 -1.1429e+02 -2.9760e+01 -7.2000e+02 -8.5714e+01 -3.0431e+01 -7.2000e+02 -5.7143e+01 -3.0923e+01 -7.2000e+02 -2.8571e+01 -3.1223e+01 -7.2000e+02 0.0000e+00 -3.1323e+01 -7.2000e+02 2.8571e+01 -3.1223e+01 -7.2000e+02 5.7143e+01 -3.0923e+01 -7.2000e+02 8.5714e+01 -3.0431e+01 -7.2000e+02 1.1429e+02 -2.9760e+01 -7.2000e+02 1.4286e+02 -2.8925e+01 -7.2000e+02 1.7143e+02 -2.7947e+01 -7.2000e+02 2.0000e+02 -2.6846e+01 -7.2000e+02 2.2857e+02 -2.5648e+01 -7.2000e+02 2.5714e+02 -2.4377e+01 -7.2000e+02 2.8571e+02 -2.3054e+01 -7.2000e+02 3.1429e+02 -2.1704e+01 -7.2000e+02 3.4286e+02 -2.0346e+01 -7.2000e+02 3.7143e+02 -1.8998e+01 -7.2000e+02 4.0000e+02 -1.7673e+01 -7.2000e+02 4.2857e+02 -1.6385e+01 -7.2000e+02 4.5714e+02 -1.5140e+01 -7.2000e+02 4.8571e+02 -1.3947e+01 -7.2000e+02 5.1429e+02 -1.2809e+01 -7.2000e+02 5.4286e+02 -1.1729e+01 -7.2000e+02 5.7143e+02 -1.0706e+01 -7.2000e+02 6.0000e+02 -9.7417e+00 -7.2000e+02 6.2857e+02 -8.8336e+00 -7.2000e+02 6.5714e+02 -7.9801e+00 -7.2000e+02 6.8571e+02 -7.1789e+00 -7.2000e+02 7.1429e+02 -6.4273e+00 -7.2000e+02 7.4286e+02 -5.7226e+00 -7.2000e+02 7.7143e+02 -5.0618e+00 -7.2000e+02 8.0000e+02 -4.4424e+00 -7.2000e+02 8.2857e+02 -3.8613e+00 -7.2000e+02 8.5714e+02 -3.3162e+00 -7.2000e+02 8.8571e+02 -2.8044e+00 -7.2000e+02 9.1429e+02 -2.3236e+00 -7.2000e+02 9.4286e+02 -1.8716e+00 -7.2000e+02 9.7143e+02 -1.4463e+00 -7.2000e+02 1.0000e+03 -1.0458e+00 -7.2000e+02 1.0286e+03 -6.6830e-01 -7.2000e+02 1.0571e+03 -3.1218e-01 -7.2000e+02 1.0857e+03 2.4092e-02 -7.2000e+02 1.1143e+03 3.4193e-01 -7.2000e+02 1.1429e+03 6.4262e-01 -7.2000e+02 1.1714e+03 9.2737e-01 -7.2000e+02 1.2000e+03 1.1973e+00 -7.2000e+02 1.2286e+03 1.4533e+00 -7.2000e+02 1.2571e+03 1.6965e+00 -7.2000e+02 1.2857e+03 1.9276e+00 -7.2000e+02 1.3143e+03 2.1474e+00 -7.2000e+02 1.3429e+03 2.3567e+00 -7.2000e+02 1.3714e+03 2.5562e+00 -7.2000e+02 1.4000e+03 2.7465e+00 -7.2000e+02 1.4286e+03 2.9281e+00 -7.2000e+02 1.4571e+03 3.1016e+00 -7.2000e+02 1.4857e+03 3.2674e+00 -7.2000e+02 1.5143e+03 3.4261e+00 -7.2000e+02 1.5429e+03 3.5781e+00 -7.2000e+02 1.5714e+03 3.7237e+00 -7.2000e+02 1.6000e+03 3.8633e+00 -7.2000e+02 1.6286e+03 3.9973e+00 -7.2000e+02 1.6571e+03 4.1259e+00 -7.2000e+02 1.6857e+03 4.2496e+00 -7.2000e+02 1.7143e+03 4.3684e+00 -7.2000e+02 1.7429e+03 4.4827e+00 -7.2000e+02 1.7714e+03 4.5928e+00 -7.2000e+02 1.8000e+03 4.6988e+00 -7.2000e+02 1.8286e+03 4.8010e+00 -7.2000e+02 1.8571e+03 4.8995e+00 -7.2000e+02 1.8857e+03 4.9946e+00 -7.2000e+02 1.9143e+03 5.0864e+00 -7.2000e+02 1.9429e+03 5.1750e+00 -7.2000e+02 1.9714e+03 5.2607e+00 -7.2000e+02 2.0000e+03 5.3435e+00 -7.5000e+02 -2.0000e+03 5.3747e+00 -7.5000e+02 -1.9714e+03 5.2934e+00 -7.5000e+02 -1.9429e+03 5.2093e+00 -7.5000e+02 -1.9143e+03 5.1224e+00 -7.5000e+02 -1.8857e+03 5.0324e+00 -7.5000e+02 -1.8571e+03 4.9393e+00 -7.5000e+02 -1.8286e+03 4.8429e+00 -7.5000e+02 -1.8000e+03 4.7429e+00 -7.5000e+02 -1.7714e+03 4.6393e+00 -7.5000e+02 -1.7429e+03 4.5318e+00 -7.5000e+02 -1.7143e+03 4.4202e+00 -7.5000e+02 -1.6857e+03 4.3043e+00 -7.5000e+02 -1.6571e+03 4.1839e+00 -7.5000e+02 -1.6286e+03 4.0587e+00 -7.5000e+02 -1.6000e+03 3.9284e+00 -7.5000e+02 -1.5714e+03 3.7927e+00 -7.5000e+02 -1.5429e+03 3.6514e+00 -7.5000e+02 -1.5143e+03 3.5040e+00 -7.5000e+02 -1.4857e+03 3.3503e+00 -7.5000e+02 -1.4571e+03 3.1899e+00 -7.5000e+02 -1.4286e+03 3.0223e+00 -7.5000e+02 -1.4000e+03 2.8471e+00 -7.5000e+02 -1.3714e+03 2.6638e+00 -7.5000e+02 -1.3429e+03 2.4719e+00 -7.5000e+02 -1.3143e+03 2.2708e+00 -7.5000e+02 -1.2857e+03 2.0600e+00 -7.5000e+02 -1.2571e+03 1.8387e+00 -7.5000e+02 -1.2286e+03 1.6064e+00 -7.5000e+02 -1.2000e+03 1.3622e+00 -7.5000e+02 -1.1714e+03 1.1054e+00 -7.5000e+02 -1.1429e+03 8.3500e-01 -7.5000e+02 -1.1143e+03 5.5014e-01 -7.5000e+02 -1.0857e+03 2.4978e-01 -7.5000e+02 -1.0571e+03 -6.7162e-02 -7.5000e+02 -1.0286e+03 -4.0187e-01 -7.5000e+02 -1.0000e+03 -7.5562e-01 -7.5000e+02 -9.7143e+02 -1.1297e+00 -7.5000e+02 -9.4286e+02 -1.5257e+00 -7.5000e+02 -9.1429e+02 -1.9450e+00 -7.5000e+02 -8.8571e+02 -2.3894e+00 -7.5000e+02 -8.5714e+02 -2.8605e+00 -7.5000e+02 -8.2857e+02 -3.3602e+00 -7.5000e+02 -8.0000e+02 -3.8903e+00 -7.5000e+02 -7.7143e+02 -4.4528e+00 -7.5000e+02 -7.4286e+02 -5.0497e+00 -7.5000e+02 -7.1429e+02 -5.6830e+00 -7.5000e+02 -6.8571e+02 -6.3546e+00 -7.5000e+02 -6.5714e+02 -7.0663e+00 -7.5000e+02 -6.2857e+02 -7.8196e+00 -7.5000e+02 -6.0000e+02 -8.6160e+00 -7.5000e+02 -5.7143e+02 -9.4562e+00 -7.5000e+02 -5.4286e+02 -1.0341e+01 -7.5000e+02 -5.1429e+02 -1.1269e+01 -7.5000e+02 -4.8571e+02 -1.2239e+01 -7.5000e+02 -4.5714e+02 -1.3249e+01 -7.5000e+02 -4.2857e+02 -1.4295e+01 -7.5000e+02 -4.0000e+02 -1.5371e+01 -7.5000e+02 -3.7143e+02 -1.6468e+01 -7.5000e+02 -3.4286e+02 -1.7579e+01 -7.5000e+02 -3.1429e+02 -1.8691e+01 -7.5000e+02 -2.8571e+02 -1.9791e+01 -7.5000e+02 -2.5714e+02 -2.0862e+01 -7.5000e+02 -2.2857e+02 -2.1889e+01 -7.5000e+02 -2.0000e+02 -2.2853e+01 -7.5000e+02 -1.7143e+02 -2.3736e+01 -7.5000e+02 -1.4286e+02 -2.4520e+01 -7.5000e+02 -1.1429e+02 -2.5188e+01 -7.5000e+02 -8.5714e+01 -2.5724e+01 -7.5000e+02 -5.7143e+01 -2.6117e+01 -7.5000e+02 -2.8571e+01 -2.6357e+01 -7.5000e+02 0.0000e+00 -2.6437e+01 -7.5000e+02 2.8571e+01 -2.6357e+01 -7.5000e+02 5.7143e+01 -2.6117e+01 -7.5000e+02 8.5714e+01 -2.5724e+01 -7.5000e+02 1.1429e+02 -2.5188e+01 -7.5000e+02 1.4286e+02 -2.4520e+01 -7.5000e+02 1.7143e+02 -2.3736e+01 -7.5000e+02 2.0000e+02 -2.2853e+01 -7.5000e+02 2.2857e+02 -2.1889e+01 -7.5000e+02 2.5714e+02 -2.0862e+01 -7.5000e+02 2.8571e+02 -1.9791e+01 -7.5000e+02 3.1429e+02 -1.8691e+01 -7.5000e+02 3.4286e+02 -1.7579e+01 -7.5000e+02 3.7143e+02 -1.6468e+01 -7.5000e+02 4.0000e+02 -1.5371e+01 -7.5000e+02 4.2857e+02 -1.4295e+01 -7.5000e+02 4.5714e+02 -1.3249e+01 -7.5000e+02 4.8571e+02 -1.2239e+01 -7.5000e+02 5.1429e+02 -1.1269e+01 -7.5000e+02 5.4286e+02 -1.0341e+01 -7.5000e+02 5.7143e+02 -9.4562e+00 -7.5000e+02 6.0000e+02 -8.6160e+00 -7.5000e+02 6.2857e+02 -7.8196e+00 -7.5000e+02 6.5714e+02 -7.0663e+00 -7.5000e+02 6.8571e+02 -6.3546e+00 -7.5000e+02 7.1429e+02 -5.6830e+00 -7.5000e+02 7.4286e+02 -5.0497e+00 -7.5000e+02 7.7143e+02 -4.4528e+00 -7.5000e+02 8.0000e+02 -3.8903e+00 -7.5000e+02 8.2857e+02 -3.3602e+00 -7.5000e+02 8.5714e+02 -2.8605e+00 -7.5000e+02 8.8571e+02 -2.3894e+00 -7.5000e+02 9.1429e+02 -1.9450e+00 -7.5000e+02 9.4286e+02 -1.5257e+00 -7.5000e+02 9.7143e+02 -1.1297e+00 -7.5000e+02 1.0000e+03 -7.5562e-01 -7.5000e+02 1.0286e+03 -4.0187e-01 -7.5000e+02 1.0571e+03 -6.7162e-02 -7.5000e+02 1.0857e+03 2.4978e-01 -7.5000e+02 1.1143e+03 5.5014e-01 -7.5000e+02 1.1429e+03 8.3500e-01 -7.5000e+02 1.1714e+03 1.1054e+00 -7.5000e+02 1.2000e+03 1.3622e+00 -7.5000e+02 1.2286e+03 1.6064e+00 -7.5000e+02 1.2571e+03 1.8387e+00 -7.5000e+02 1.2857e+03 2.0600e+00 -7.5000e+02 1.3143e+03 2.2708e+00 -7.5000e+02 1.3429e+03 2.4719e+00 -7.5000e+02 1.3714e+03 2.6638e+00 -7.5000e+02 1.4000e+03 2.8471e+00 -7.5000e+02 1.4286e+03 3.0223e+00 -7.5000e+02 1.4571e+03 3.1899e+00 -7.5000e+02 1.4857e+03 3.3503e+00 -7.5000e+02 1.5143e+03 3.5040e+00 -7.5000e+02 1.5429e+03 3.6514e+00 -7.5000e+02 1.5714e+03 3.7927e+00 -7.5000e+02 1.6000e+03 3.9284e+00 -7.5000e+02 1.6286e+03 4.0587e+00 -7.5000e+02 1.6571e+03 4.1839e+00 -7.5000e+02 1.6857e+03 4.3043e+00 -7.5000e+02 1.7143e+03 4.4202e+00 -7.5000e+02 1.7429e+03 4.5318e+00 -7.5000e+02 1.7714e+03 4.6393e+00 -7.5000e+02 1.8000e+03 4.7429e+00 -7.5000e+02 1.8286e+03 4.8429e+00 -7.5000e+02 1.8571e+03 4.9393e+00 -7.5000e+02 1.8857e+03 5.0324e+00 -7.5000e+02 1.9143e+03 5.1224e+00 -7.5000e+02 1.9429e+03 5.2093e+00 -7.5000e+02 1.9714e+03 5.2934e+00 -7.5000e+02 2.0000e+03 5.3747e+00 -7.8000e+02 -2.0000e+03 5.4065e+00 -7.8000e+02 -1.9714e+03 5.3267e+00 -7.8000e+02 -1.9429e+03 5.2443e+00 -7.8000e+02 -1.9143e+03 5.1591e+00 -7.8000e+02 -1.8857e+03 5.0710e+00 -7.8000e+02 -1.8571e+03 4.9799e+00 -7.8000e+02 -1.8286e+03 4.8855e+00 -7.8000e+02 -1.8000e+03 4.7878e+00 -7.8000e+02 -1.7714e+03 4.6866e+00 -7.8000e+02 -1.7429e+03 4.5817e+00 -7.8000e+02 -1.7143e+03 4.4728e+00 -7.8000e+02 -1.6857e+03 4.3599e+00 -7.8000e+02 -1.6571e+03 4.2426e+00 -7.8000e+02 -1.6286e+03 4.1208e+00 -7.8000e+02 -1.6000e+03 3.9941e+00 -7.8000e+02 -1.5714e+03 3.8624e+00 -7.8000e+02 -1.5429e+03 3.7253e+00 -7.8000e+02 -1.5143e+03 3.5826e+00 -7.8000e+02 -1.4857e+03 3.4338e+00 -7.8000e+02 -1.4571e+03 3.2787e+00 -7.8000e+02 -1.4286e+03 3.1169e+00 -7.8000e+02 -1.4000e+03 2.9480e+00 -7.8000e+02 -1.3714e+03 2.7715e+00 -7.8000e+02 -1.3429e+03 2.5870e+00 -7.8000e+02 -1.3143e+03 2.3940e+00 -7.8000e+02 -1.2857e+03 2.1920e+00 -7.8000e+02 -1.2571e+03 1.9803e+00 -7.8000e+02 -1.2286e+03 1.7585e+00 -7.8000e+02 -1.2000e+03 1.5258e+00 -7.8000e+02 -1.1714e+03 1.2815e+00 -7.8000e+02 -1.1429e+03 1.0249e+00 -7.8000e+02 -1.1143e+03 7.5515e-01 -7.8000e+02 -1.0857e+03 4.7144e-01 -7.8000e+02 -1.0571e+03 1.7283e-01 -7.8000e+02 -1.0286e+03 -1.4166e-01 -7.8000e+02 -1.0000e+03 -4.7306e-01 -7.8000e+02 -9.7143e+02 -8.2250e-01 -7.8000e+02 -9.4286e+02 -1.1911e+00 -7.8000e+02 -9.1429e+02 -1.5802e+00 -7.8000e+02 -8.8571e+02 -1.9909e+00 -7.8000e+02 -8.5714e+02 -2.4247e+00 -7.8000e+02 -8.2857e+02 -2.8830e+00 -7.8000e+02 -8.0000e+02 -3.3670e+00 -7.8000e+02 -7.7143e+02 -3.8783e+00 -7.8000e+02 -7.4286e+02 -4.4183e+00 -7.8000e+02 -7.1429e+02 -4.9882e+00 -7.8000e+02 -6.8571e+02 -5.5894e+00 -7.8000e+02 -6.5714e+02 -6.2230e+00 -7.8000e+02 -6.2857e+02 -6.8897e+00 -7.8000e+02 -6.0000e+02 -7.5902e+00 -7.8000e+02 -5.7143e+02 -8.3247e+00 -7.8000e+02 -5.4286e+02 -9.0928e+00 -7.8000e+02 -5.1429e+02 -9.8934e+00 -7.8000e+02 -4.8571e+02 -1.0725e+01 -7.8000e+02 -4.5714e+02 -1.1585e+01 -7.8000e+02 -4.2857e+02 -1.2468e+01 -7.8000e+02 -4.0000e+02 -1.3371e+01 -7.8000e+02 -3.7143e+02 -1.4287e+01 -7.8000e+02 -3.4286e+02 -1.5207e+01 -7.8000e+02 -3.1429e+02 -1.6123e+01 -7.8000e+02 -2.8571e+02 -1.7024e+01 -7.8000e+02 -2.5714e+02 -1.7897e+01 -7.8000e+02 -2.2857e+02 -1.8730e+01 -7.8000e+02 -2.0000e+02 -1.9509e+01 -7.8000e+02 -1.7143e+02 -2.0220e+01 -7.8000e+02 -1.4286e+02 -2.0849e+01 -7.8000e+02 -1.1429e+02 -2.1384e+01 -7.8000e+02 -8.5714e+01 -2.1813e+01 -7.8000e+02 -5.7143e+01 -2.2126e+01 -7.8000e+02 -2.8571e+01 -2.2317e+01 -7.8000e+02 0.0000e+00 -2.2382e+01 -7.8000e+02 2.8571e+01 -2.2317e+01 -7.8000e+02 5.7143e+01 -2.2126e+01 -7.8000e+02 8.5714e+01 -2.1813e+01 -7.8000e+02 1.1429e+02 -2.1384e+01 -7.8000e+02 1.4286e+02 -2.0849e+01 -7.8000e+02 1.7143e+02 -2.0220e+01 -7.8000e+02 2.0000e+02 -1.9509e+01 -7.8000e+02 2.2857e+02 -1.8730e+01 -7.8000e+02 2.5714e+02 -1.7897e+01 -7.8000e+02 2.8571e+02 -1.7024e+01 -7.8000e+02 3.1429e+02 -1.6123e+01 -7.8000e+02 3.4286e+02 -1.5207e+01 -7.8000e+02 3.7143e+02 -1.4287e+01 -7.8000e+02 4.0000e+02 -1.3371e+01 -7.8000e+02 4.2857e+02 -1.2468e+01 -7.8000e+02 4.5714e+02 -1.1585e+01 -7.8000e+02 4.8571e+02 -1.0725e+01 -7.8000e+02 5.1429e+02 -9.8934e+00 -7.8000e+02 5.4286e+02 -9.0928e+00 -7.8000e+02 5.7143e+02 -8.3247e+00 -7.8000e+02 6.0000e+02 -7.5902e+00 -7.8000e+02 6.2857e+02 -6.8897e+00 -7.8000e+02 6.5714e+02 -6.2230e+00 -7.8000e+02 6.8571e+02 -5.5894e+00 -7.8000e+02 7.1429e+02 -4.9882e+00 -7.8000e+02 7.4286e+02 -4.4183e+00 -7.8000e+02 7.7143e+02 -3.8783e+00 -7.8000e+02 8.0000e+02 -3.3670e+00 -7.8000e+02 8.2857e+02 -2.8830e+00 -7.8000e+02 8.5714e+02 -2.4247e+00 -7.8000e+02 8.8571e+02 -1.9909e+00 -7.8000e+02 9.1429e+02 -1.5802e+00 -7.8000e+02 9.4286e+02 -1.1911e+00 -7.8000e+02 9.7143e+02 -8.2250e-01 -7.8000e+02 1.0000e+03 -4.7306e-01 -7.8000e+02 1.0286e+03 -1.4166e-01 -7.8000e+02 1.0571e+03 1.7283e-01 -7.8000e+02 1.0857e+03 4.7144e-01 -7.8000e+02 1.1143e+03 7.5515e-01 -7.8000e+02 1.1429e+03 1.0249e+00 -7.8000e+02 1.1714e+03 1.2815e+00 -7.8000e+02 1.2000e+03 1.5258e+00 -7.8000e+02 1.2286e+03 1.7585e+00 -7.8000e+02 1.2571e+03 1.9803e+00 -7.8000e+02 1.2857e+03 2.1920e+00 -7.8000e+02 1.3143e+03 2.3940e+00 -7.8000e+02 1.3429e+03 2.5870e+00 -7.8000e+02 1.3714e+03 2.7715e+00 -7.8000e+02 1.4000e+03 2.9480e+00 -7.8000e+02 1.4286e+03 3.1169e+00 -7.8000e+02 1.4571e+03 3.2787e+00 -7.8000e+02 1.4857e+03 3.4338e+00 -7.8000e+02 1.5143e+03 3.5826e+00 -7.8000e+02 1.5429e+03 3.7253e+00 -7.8000e+02 1.5714e+03 3.8624e+00 -7.8000e+02 1.6000e+03 3.9941e+00 -7.8000e+02 1.6286e+03 4.1208e+00 -7.8000e+02 1.6571e+03 4.2426e+00 -7.8000e+02 1.6857e+03 4.3599e+00 -7.8000e+02 1.7143e+03 4.4728e+00 -7.8000e+02 1.7429e+03 4.5817e+00 -7.8000e+02 1.7714e+03 4.6866e+00 -7.8000e+02 1.8000e+03 4.7878e+00 -7.8000e+02 1.8286e+03 4.8855e+00 -7.8000e+02 1.8571e+03 4.9799e+00 -7.8000e+02 1.8857e+03 5.0710e+00 -7.8000e+02 1.9143e+03 5.1591e+00 -7.8000e+02 1.9429e+03 5.2443e+00 -7.8000e+02 1.9714e+03 5.3267e+00 -7.8000e+02 2.0000e+03 5.4065e+00 -8.1000e+02 -2.0000e+03 5.4389e+00 -8.1000e+02 -1.9714e+03 5.3607e+00 -8.1000e+02 -1.9429e+03 5.2799e+00 -8.1000e+02 -1.9143e+03 5.1965e+00 -8.1000e+02 -1.8857e+03 5.1102e+00 -8.1000e+02 -1.8571e+03 5.0211e+00 -8.1000e+02 -1.8286e+03 4.9288e+00 -8.1000e+02 -1.8000e+03 4.8334e+00 -8.1000e+02 -1.7714e+03 4.7346e+00 -8.1000e+02 -1.7429e+03 4.6322e+00 -8.1000e+02 -1.7143e+03 4.5261e+00 -8.1000e+02 -1.6857e+03 4.4161e+00 -8.1000e+02 -1.6571e+03 4.3020e+00 -8.1000e+02 -1.6286e+03 4.1835e+00 -8.1000e+02 -1.6000e+03 4.0605e+00 -8.1000e+02 -1.5714e+03 3.9327e+00 -8.1000e+02 -1.5429e+03 3.7998e+00 -8.1000e+02 -1.5143e+03 3.6616e+00 -8.1000e+02 -1.4857e+03 3.5177e+00 -8.1000e+02 -1.4571e+03 3.3679e+00 -8.1000e+02 -1.4286e+03 3.2118e+00 -8.1000e+02 -1.4000e+03 3.0490e+00 -8.1000e+02 -1.3714e+03 2.8792e+00 -8.1000e+02 -1.3429e+03 2.7019e+00 -8.1000e+02 -1.3143e+03 2.5168e+00 -8.1000e+02 -1.2857e+03 2.3233e+00 -8.1000e+02 -1.2571e+03 2.1209e+00 -8.1000e+02 -1.2286e+03 1.9092e+00 -8.1000e+02 -1.2000e+03 1.6875e+00 -8.1000e+02 -1.1714e+03 1.4553e+00 -8.1000e+02 -1.1429e+03 1.2119e+00 -8.1000e+02 -1.1143e+03 9.5659e-01 -8.1000e+02 -1.0857e+03 6.8871e-01 -8.1000e+02 -1.0571e+03 4.0746e-01 -8.1000e+02 -1.0286e+03 1.1205e-01 -8.1000e+02 -1.0000e+03 -1.9839e-01 -8.1000e+02 -9.7143e+02 -5.2474e-01 -8.1000e+02 -9.4286e+02 -8.6794e-01 -8.1000e+02 -9.1429e+02 -1.2290e+00 -8.1000e+02 -8.8571e+02 -1.6088e+00 -8.1000e+02 -8.5714e+02 -2.0084e+00 -8.1000e+02 -8.2857e+02 -2.4290e+00 -8.1000e+02 -8.0000e+02 -2.8713e+00 -8.1000e+02 -7.7143e+02 -3.3366e+00 -8.1000e+02 -7.4286e+02 -3.8257e+00 -8.1000e+02 -7.1429e+02 -4.3395e+00 -8.1000e+02 -6.8571e+02 -4.8788e+00 -8.1000e+02 -6.5714e+02 -5.4441e+00 -8.1000e+02 -6.2857e+02 -6.0357e+00 -8.1000e+02 -6.0000e+02 -6.6539e+00 -8.1000e+02 -5.7143e+02 -7.2982e+00 -8.1000e+02 -5.4286e+02 -7.9679e+00 -8.1000e+02 -5.1429e+02 -8.6617e+00 -8.1000e+02 -4.8571e+02 -9.3777e+00 -8.1000e+02 -4.5714e+02 -1.0113e+01 -8.1000e+02 -4.2857e+02 -1.0865e+01 -8.1000e+02 -4.0000e+02 -1.1627e+01 -8.1000e+02 -3.7143e+02 -1.2396e+01 -8.1000e+02 -3.4286e+02 -1.3164e+01 -8.1000e+02 -3.1429e+02 -1.3924e+01 -8.1000e+02 -2.8571e+02 -1.4667e+01 -8.1000e+02 -2.5714e+02 -1.5384e+01 -8.1000e+02 -2.2857e+02 -1.6064e+01 -8.1000e+02 -2.0000e+02 -1.6698e+01 -8.1000e+02 -1.7143e+02 -1.7274e+01 -8.1000e+02 -1.4286e+02 -1.7782e+01 -8.1000e+02 -1.1429e+02 -1.8213e+01 -8.1000e+02 -8.5714e+01 -1.8558e+01 -8.1000e+02 -5.7143e+01 -1.8809e+01 -8.1000e+02 -2.8571e+01 -1.8963e+01 -8.1000e+02 0.0000e+00 -1.9014e+01 -8.1000e+02 2.8571e+01 -1.8963e+01 -8.1000e+02 5.7143e+01 -1.8809e+01 -8.1000e+02 8.5714e+01 -1.8558e+01 -8.1000e+02 1.1429e+02 -1.8213e+01 -8.1000e+02 1.4286e+02 -1.7782e+01 -8.1000e+02 1.7143e+02 -1.7274e+01 -8.1000e+02 2.0000e+02 -1.6698e+01 -8.1000e+02 2.2857e+02 -1.6064e+01 -8.1000e+02 2.5714e+02 -1.5384e+01 -8.1000e+02 2.8571e+02 -1.4667e+01 -8.1000e+02 3.1429e+02 -1.3924e+01 -8.1000e+02 3.4286e+02 -1.3164e+01 -8.1000e+02 3.7143e+02 -1.2396e+01 -8.1000e+02 4.0000e+02 -1.1627e+01 -8.1000e+02 4.2857e+02 -1.0865e+01 -8.1000e+02 4.5714e+02 -1.0113e+01 -8.1000e+02 4.8571e+02 -9.3777e+00 -8.1000e+02 5.1429e+02 -8.6617e+00 -8.1000e+02 5.4286e+02 -7.9679e+00 -8.1000e+02 5.7143e+02 -7.2982e+00 -8.1000e+02 6.0000e+02 -6.6539e+00 -8.1000e+02 6.2857e+02 -6.0357e+00 -8.1000e+02 6.5714e+02 -5.4441e+00 -8.1000e+02 6.8571e+02 -4.8788e+00 -8.1000e+02 7.1429e+02 -4.3395e+00 -8.1000e+02 7.4286e+02 -3.8257e+00 -8.1000e+02 7.7143e+02 -3.3366e+00 -8.1000e+02 8.0000e+02 -2.8713e+00 -8.1000e+02 8.2857e+02 -2.4290e+00 -8.1000e+02 8.5714e+02 -2.0084e+00 -8.1000e+02 8.8571e+02 -1.6088e+00 -8.1000e+02 9.1429e+02 -1.2290e+00 -8.1000e+02 9.4286e+02 -8.6794e-01 -8.1000e+02 9.7143e+02 -5.2474e-01 -8.1000e+02 1.0000e+03 -1.9839e-01 -8.1000e+02 1.0286e+03 1.1205e-01 -8.1000e+02 1.0571e+03 4.0746e-01 -8.1000e+02 1.0857e+03 6.8871e-01 -8.1000e+02 1.1143e+03 9.5659e-01 -8.1000e+02 1.1429e+03 1.2119e+00 -8.1000e+02 1.1714e+03 1.4553e+00 -8.1000e+02 1.2000e+03 1.6875e+00 -8.1000e+02 1.2286e+03 1.9092e+00 -8.1000e+02 1.2571e+03 2.1209e+00 -8.1000e+02 1.2857e+03 2.3233e+00 -8.1000e+02 1.3143e+03 2.5168e+00 -8.1000e+02 1.3429e+03 2.7019e+00 -8.1000e+02 1.3714e+03 2.8792e+00 -8.1000e+02 1.4000e+03 3.0490e+00 -8.1000e+02 1.4286e+03 3.2118e+00 -8.1000e+02 1.4571e+03 3.3679e+00 -8.1000e+02 1.4857e+03 3.5177e+00 -8.1000e+02 1.5143e+03 3.6616e+00 -8.1000e+02 1.5429e+03 3.7998e+00 -8.1000e+02 1.5714e+03 3.9327e+00 -8.1000e+02 1.6000e+03 4.0605e+00 -8.1000e+02 1.6286e+03 4.1835e+00 -8.1000e+02 1.6571e+03 4.3020e+00 -8.1000e+02 1.6857e+03 4.4161e+00 -8.1000e+02 1.7143e+03 4.5261e+00 -8.1000e+02 1.7429e+03 4.6322e+00 -8.1000e+02 1.7714e+03 4.7346e+00 -8.1000e+02 1.8000e+03 4.8334e+00 -8.1000e+02 1.8286e+03 4.9288e+00 -8.1000e+02 1.8571e+03 5.0211e+00 -8.1000e+02 1.8857e+03 5.1102e+00 -8.1000e+02 1.9143e+03 5.1965e+00 -8.1000e+02 1.9429e+03 5.2799e+00 -8.1000e+02 1.9714e+03 5.3607e+00 -8.1000e+02 2.0000e+03 5.4389e+00 -8.4000e+02 -2.0000e+03 5.4720e+00 -8.4000e+02 -1.9714e+03 5.3953e+00 -8.4000e+02 -1.9429e+03 5.3161e+00 -8.4000e+02 -1.9143e+03 5.2344e+00 -8.4000e+02 -1.8857e+03 5.1501e+00 -8.4000e+02 -1.8571e+03 5.0629e+00 -8.4000e+02 -1.8286e+03 4.9728e+00 -8.4000e+02 -1.8000e+03 4.8796e+00 -8.4000e+02 -1.7714e+03 4.7831e+00 -8.4000e+02 -1.7429e+03 4.6833e+00 -8.4000e+02 -1.7143e+03 4.5800e+00 -8.4000e+02 -1.6857e+03 4.4729e+00 -8.4000e+02 -1.6571e+03 4.3619e+00 -8.4000e+02 -1.6286e+03 4.2468e+00 -8.4000e+02 -1.6000e+03 4.1274e+00 -8.4000e+02 -1.5714e+03 4.0034e+00 -8.4000e+02 -1.5429e+03 3.8747e+00 -8.4000e+02 -1.5143e+03 3.7409e+00 -8.4000e+02 -1.4857e+03 3.6018e+00 -8.4000e+02 -1.4571e+03 3.4572e+00 -8.4000e+02 -1.4286e+03 3.3066e+00 -8.4000e+02 -1.4000e+03 3.1499e+00 -8.4000e+02 -1.3714e+03 2.9866e+00 -8.4000e+02 -1.3429e+03 2.8164e+00 -8.4000e+02 -1.3143e+03 2.6388e+00 -8.4000e+02 -1.2857e+03 2.4536e+00 -8.4000e+02 -1.2571e+03 2.2603e+00 -8.4000e+02 -1.2286e+03 2.0583e+00 -8.4000e+02 -1.2000e+03 1.8472e+00 -8.4000e+02 -1.1714e+03 1.6266e+00 -8.4000e+02 -1.1429e+03 1.3957e+00 -8.4000e+02 -1.1143e+03 1.1542e+00 -8.4000e+02 -1.0857e+03 9.0131e-01 -8.4000e+02 -1.0571e+03 6.3647e-01 -8.4000e+02 -1.0286e+03 3.5901e-01 -8.4000e+02 -1.0000e+03 6.8225e-02 -8.4000e+02 -9.7143e+02 -2.3659e-01 -8.4000e+02 -9.4286e+02 -5.5617e-01 -8.4000e+02 -9.1429e+02 -8.9127e-01 -8.4000e+02 -8.8571e+02 -1.2426e+00 -8.4000e+02 -8.5714e+02 -1.6111e+00 -8.4000e+02 -8.2857e+02 -1.9973e+00 -8.4000e+02 -8.0000e+02 -2.4020e+00 -8.4000e+02 -7.7143e+02 -2.8259e+00 -8.4000e+02 -7.4286e+02 -3.2696e+00 -8.4000e+02 -7.1429e+02 -3.7336e+00 -8.4000e+02 -6.8571e+02 -4.2183e+00 -8.4000e+02 -6.5714e+02 -4.7239e+00 -8.4000e+02 -6.2857e+02 -5.2504e+00 -8.4000e+02 -6.0000e+02 -5.7975e+00 -8.4000e+02 -5.7143e+02 -6.3647e+00 -8.4000e+02 -5.4286e+02 -6.9510e+00 -8.4000e+02 -5.1429e+02 -7.5549e+00 -8.4000e+02 -4.8571e+02 -8.1745e+00 -8.4000e+02 -4.5714e+02 -8.8072e+00 -8.4000e+02 -4.2857e+02 -9.4498e+00 -8.4000e+02 -4.0000e+02 -1.0098e+01 -8.4000e+02 -3.7143e+02 -1.0748e+01 -8.4000e+02 -3.4286e+02 -1.1394e+01 -8.4000e+02 -3.1429e+02 -1.2029e+01 -8.4000e+02 -2.8571e+02 -1.2647e+01 -8.4000e+02 -2.5714e+02 -1.3240e+01 -8.4000e+02 -2.2857e+02 -1.3800e+01 -8.4000e+02 -2.0000e+02 -1.4320e+01 -8.4000e+02 -1.7143e+02 -1.4791e+01 -8.4000e+02 -1.4286e+02 -1.5205e+01 -8.4000e+02 -1.1429e+02 -1.5555e+01 -8.4000e+02 -8.5714e+01 -1.5834e+01 -8.4000e+02 -5.7143e+01 -1.6038e+01 -8.4000e+02 -2.8571e+01 -1.6162e+01 -8.4000e+02 0.0000e+00 -1.6203e+01 -8.4000e+02 2.8571e+01 -1.6162e+01 -8.4000e+02 5.7143e+01 -1.6038e+01 -8.4000e+02 8.5714e+01 -1.5834e+01 -8.4000e+02 1.1429e+02 -1.5555e+01 -8.4000e+02 1.4286e+02 -1.5205e+01 -8.4000e+02 1.7143e+02 -1.4791e+01 -8.4000e+02 2.0000e+02 -1.4320e+01 -8.4000e+02 2.2857e+02 -1.3800e+01 -8.4000e+02 2.5714e+02 -1.3240e+01 -8.4000e+02 2.8571e+02 -1.2647e+01 -8.4000e+02 3.1429e+02 -1.2029e+01 -8.4000e+02 3.4286e+02 -1.1394e+01 -8.4000e+02 3.7143e+02 -1.0748e+01 -8.4000e+02 4.0000e+02 -1.0098e+01 -8.4000e+02 4.2857e+02 -9.4498e+00 -8.4000e+02 4.5714e+02 -8.8072e+00 -8.4000e+02 4.8571e+02 -8.1745e+00 -8.4000e+02 5.1429e+02 -7.5549e+00 -8.4000e+02 5.4286e+02 -6.9510e+00 -8.4000e+02 5.7143e+02 -6.3647e+00 -8.4000e+02 6.0000e+02 -5.7975e+00 -8.4000e+02 6.2857e+02 -5.2504e+00 -8.4000e+02 6.5714e+02 -4.7239e+00 -8.4000e+02 6.8571e+02 -4.2183e+00 -8.4000e+02 7.1429e+02 -3.7336e+00 -8.4000e+02 7.4286e+02 -3.2696e+00 -8.4000e+02 7.7143e+02 -2.8259e+00 -8.4000e+02 8.0000e+02 -2.4020e+00 -8.4000e+02 8.2857e+02 -1.9973e+00 -8.4000e+02 8.5714e+02 -1.6111e+00 -8.4000e+02 8.8571e+02 -1.2426e+00 -8.4000e+02 9.1429e+02 -8.9127e-01 -8.4000e+02 9.4286e+02 -5.5617e-01 -8.4000e+02 9.7143e+02 -2.3659e-01 -8.4000e+02 1.0000e+03 6.8225e-02 -8.4000e+02 1.0286e+03 3.5901e-01 -8.4000e+02 1.0571e+03 6.3647e-01 -8.4000e+02 1.0857e+03 9.0131e-01 -8.4000e+02 1.1143e+03 1.1542e+00 -8.4000e+02 1.1429e+03 1.3957e+00 -8.4000e+02 1.1714e+03 1.6266e+00 -8.4000e+02 1.2000e+03 1.8472e+00 -8.4000e+02 1.2286e+03 2.0583e+00 -8.4000e+02 1.2571e+03 2.2603e+00 -8.4000e+02 1.2857e+03 2.4536e+00 -8.4000e+02 1.3143e+03 2.6388e+00 -8.4000e+02 1.3429e+03 2.8164e+00 -8.4000e+02 1.3714e+03 2.9866e+00 -8.4000e+02 1.4000e+03 3.1499e+00 -8.4000e+02 1.4286e+03 3.3066e+00 -8.4000e+02 1.4571e+03 3.4572e+00 -8.4000e+02 1.4857e+03 3.6018e+00 -8.4000e+02 1.5143e+03 3.7409e+00 -8.4000e+02 1.5429e+03 3.8747e+00 -8.4000e+02 1.5714e+03 4.0034e+00 -8.4000e+02 1.6000e+03 4.1274e+00 -8.4000e+02 1.6286e+03 4.2468e+00 -8.4000e+02 1.6571e+03 4.3619e+00 -8.4000e+02 1.6857e+03 4.4729e+00 -8.4000e+02 1.7143e+03 4.5800e+00 -8.4000e+02 1.7429e+03 4.6833e+00 -8.4000e+02 1.7714e+03 4.7831e+00 -8.4000e+02 1.8000e+03 4.8796e+00 -8.4000e+02 1.8286e+03 4.9728e+00 -8.4000e+02 1.8571e+03 5.0629e+00 -8.4000e+02 1.8857e+03 5.1501e+00 -8.4000e+02 1.9143e+03 5.2344e+00 -8.4000e+02 1.9429e+03 5.3161e+00 -8.4000e+02 1.9714e+03 5.3953e+00 -8.4000e+02 2.0000e+03 5.4720e+00 -8.7000e+02 -2.0000e+03 5.5055e+00 -8.7000e+02 -1.9714e+03 5.4304e+00 -8.7000e+02 -1.9429e+03 5.3529e+00 -8.7000e+02 -1.9143e+03 5.2730e+00 -8.7000e+02 -1.8857e+03 5.1904e+00 -8.7000e+02 -1.8571e+03 5.1052e+00 -8.7000e+02 -1.8286e+03 5.0172e+00 -8.7000e+02 -1.8000e+03 4.9263e+00 -8.7000e+02 -1.7714e+03 4.8322e+00 -8.7000e+02 -1.7429e+03 4.7350e+00 -8.7000e+02 -1.7143e+03 4.6343e+00 -8.7000e+02 -1.6857e+03 4.5301e+00 -8.7000e+02 -1.6571e+03 4.4223e+00 -8.7000e+02 -1.6286e+03 4.3105e+00 -8.7000e+02 -1.6000e+03 4.1946e+00 -8.7000e+02 -1.5714e+03 4.0744e+00 -8.7000e+02 -1.5429e+03 3.9498e+00 -8.7000e+02 -1.5143e+03 3.8204e+00 -8.7000e+02 -1.4857e+03 3.6860e+00 -8.7000e+02 -1.4571e+03 3.5464e+00 -8.7000e+02 -1.4286e+03 3.4014e+00 -8.7000e+02 -1.4000e+03 3.2505e+00 -8.7000e+02 -1.3714e+03 3.0935e+00 -8.7000e+02 -1.3429e+03 2.9301e+00 -8.7000e+02 -1.3143e+03 2.7600e+00 -8.7000e+02 -1.2857e+03 2.5828e+00 -8.7000e+02 -1.2571e+03 2.3981e+00 -8.7000e+02 -1.2286e+03 2.2055e+00 -8.7000e+02 -1.2000e+03 2.0046e+00 -8.7000e+02 -1.1714e+03 1.7950e+00 -8.7000e+02 -1.1429e+03 1.5762e+00 -8.7000e+02 -1.1143e+03 1.3477e+00 -8.7000e+02 -1.0857e+03 1.1090e+00 -8.7000e+02 -1.0571e+03 8.5968e-01 -8.7000e+02 -1.0286e+03 5.9908e-01 -8.7000e+02 -1.0000e+03 3.2669e-01 -8.7000e+02 -9.7143e+02 4.1952e-02 -8.7000e+02 -9.4286e+02 -2.5571e-01 -8.7000e+02 -9.1429e+02 -5.6688e-01 -8.7000e+02 -8.8571e+02 -8.9211e-01 -8.7000e+02 -8.5714e+02 -1.2320e+00 -8.7000e+02 -8.2857e+02 -1.5870e+00 -8.7000e+02 -8.0000e+02 -1.9576e+00 -8.7000e+02 -7.7143e+02 -2.3443e+00 -8.7000e+02 -7.4286e+02 -2.7474e+00 -8.7000e+02 -7.1429e+02 -3.1672e+00 -8.7000e+02 -6.8571e+02 -3.6038e+00 -8.7000e+02 -6.5714e+02 -4.0571e+00 -8.7000e+02 -6.2857e+02 -4.5269e+00 -8.7000e+02 -6.0000e+02 -5.0127e+00 -8.7000e+02 -5.7143e+02 -5.5139e+00 -8.7000e+02 -5.4286e+02 -6.0291e+00 -8.7000e+02 -5.1429e+02 -6.5571e+00 -8.7000e+02 -4.8571e+02 -7.0959e+00 -8.7000e+02 -4.5714e+02 -7.6431e+00 -8.7000e+02 -4.2857e+02 -8.1959e+00 -8.7000e+02 -4.0000e+02 -8.7507e+00 -8.7000e+02 -3.7143e+02 -9.3037e+00 -8.7000e+02 -3.4286e+02 -9.8502e+00 -8.7000e+02 -3.1429e+02 -1.0385e+01 -8.7000e+02 -2.8571e+02 -1.0903e+01 -8.7000e+02 -2.5714e+02 -1.1398e+01 -8.7000e+02 -2.2857e+02 -1.1864e+01 -8.7000e+02 -2.0000e+02 -1.2294e+01 -8.7000e+02 -1.7143e+02 -1.2682e+01 -8.7000e+02 -1.4286e+02 -1.3022e+01 -8.7000e+02 -1.1429e+02 -1.3309e+01 -8.7000e+02 -8.5714e+01 -1.3538e+01 -8.7000e+02 -5.7143e+01 -1.3704e+01 -8.7000e+02 -2.8571e+01 -1.3806e+01 -8.7000e+02 0.0000e+00 -1.3839e+01 -8.7000e+02 2.8571e+01 -1.3806e+01 -8.7000e+02 5.7143e+01 -1.3704e+01 -8.7000e+02 8.5714e+01 -1.3538e+01 -8.7000e+02 1.1429e+02 -1.3309e+01 -8.7000e+02 1.4286e+02 -1.3022e+01 -8.7000e+02 1.7143e+02 -1.2682e+01 -8.7000e+02 2.0000e+02 -1.2294e+01 -8.7000e+02 2.2857e+02 -1.1864e+01 -8.7000e+02 2.5714e+02 -1.1398e+01 -8.7000e+02 2.8571e+02 -1.0903e+01 -8.7000e+02 3.1429e+02 -1.0385e+01 -8.7000e+02 3.4286e+02 -9.8502e+00 -8.7000e+02 3.7143e+02 -9.3037e+00 -8.7000e+02 4.0000e+02 -8.7507e+00 -8.7000e+02 4.2857e+02 -8.1959e+00 -8.7000e+02 4.5714e+02 -7.6431e+00 -8.7000e+02 4.8571e+02 -7.0959e+00 -8.7000e+02 5.1429e+02 -6.5571e+00 -8.7000e+02 5.4286e+02 -6.0291e+00 -8.7000e+02 5.7143e+02 -5.5139e+00 -8.7000e+02 6.0000e+02 -5.0127e+00 -8.7000e+02 6.2857e+02 -4.5269e+00 -8.7000e+02 6.5714e+02 -4.0571e+00 -8.7000e+02 6.8571e+02 -3.6038e+00 -8.7000e+02 7.1429e+02 -3.1672e+00 -8.7000e+02 7.4286e+02 -2.7474e+00 -8.7000e+02 7.7143e+02 -2.3443e+00 -8.7000e+02 8.0000e+02 -1.9576e+00 -8.7000e+02 8.2857e+02 -1.5870e+00 -8.7000e+02 8.5714e+02 -1.2320e+00 -8.7000e+02 8.8571e+02 -8.9211e-01 -8.7000e+02 9.1429e+02 -5.6688e-01 -8.7000e+02 9.4286e+02 -2.5571e-01 -8.7000e+02 9.7143e+02 4.1952e-02 -8.7000e+02 1.0000e+03 3.2669e-01 -8.7000e+02 1.0286e+03 5.9908e-01 -8.7000e+02 1.0571e+03 8.5968e-01 -8.7000e+02 1.0857e+03 1.1090e+00 -8.7000e+02 1.1143e+03 1.3477e+00 -8.7000e+02 1.1429e+03 1.5762e+00 -8.7000e+02 1.1714e+03 1.7950e+00 -8.7000e+02 1.2000e+03 2.0046e+00 -8.7000e+02 1.2286e+03 2.2055e+00 -8.7000e+02 1.2571e+03 2.3981e+00 -8.7000e+02 1.2857e+03 2.5828e+00 -8.7000e+02 1.3143e+03 2.7600e+00 -8.7000e+02 1.3429e+03 2.9301e+00 -8.7000e+02 1.3714e+03 3.0935e+00 -8.7000e+02 1.4000e+03 3.2505e+00 -8.7000e+02 1.4286e+03 3.4014e+00 -8.7000e+02 1.4571e+03 3.5464e+00 -8.7000e+02 1.4857e+03 3.6860e+00 -8.7000e+02 1.5143e+03 3.8204e+00 -8.7000e+02 1.5429e+03 3.9498e+00 -8.7000e+02 1.5714e+03 4.0744e+00 -8.7000e+02 1.6000e+03 4.1946e+00 -8.7000e+02 1.6286e+03 4.3105e+00 -8.7000e+02 1.6571e+03 4.4223e+00 -8.7000e+02 1.6857e+03 4.5301e+00 -8.7000e+02 1.7143e+03 4.6343e+00 -8.7000e+02 1.7429e+03 4.7350e+00 -8.7000e+02 1.7714e+03 4.8322e+00 -8.7000e+02 1.8000e+03 4.9263e+00 -8.7000e+02 1.8286e+03 5.0172e+00 -8.7000e+02 1.8571e+03 5.1052e+00 -8.7000e+02 1.8857e+03 5.1904e+00 -8.7000e+02 1.9143e+03 5.2730e+00 -8.7000e+02 1.9429e+03 5.3529e+00 -8.7000e+02 1.9714e+03 5.4304e+00 -8.7000e+02 2.0000e+03 5.5055e+00 -9.0000e+02 -2.0000e+03 5.5395e+00 -9.0000e+02 -1.9714e+03 5.4660e+00 -9.0000e+02 -1.9429e+03 5.3901e+00 -9.0000e+02 -1.9143e+03 5.3119e+00 -9.0000e+02 -1.8857e+03 5.2313e+00 -9.0000e+02 -1.8571e+03 5.1480e+00 -9.0000e+02 -1.8286e+03 5.0621e+00 -9.0000e+02 -1.8000e+03 4.9734e+00 -9.0000e+02 -1.7714e+03 4.8817e+00 -9.0000e+02 -1.7429e+03 4.7870e+00 -9.0000e+02 -1.7143e+03 4.6891e+00 -9.0000e+02 -1.6857e+03 4.5878e+00 -9.0000e+02 -1.6571e+03 4.4829e+00 -9.0000e+02 -1.6286e+03 4.3744e+00 -9.0000e+02 -1.6000e+03 4.2621e+00 -9.0000e+02 -1.5714e+03 4.1457e+00 -9.0000e+02 -1.5429e+03 4.0250e+00 -9.0000e+02 -1.5143e+03 3.8999e+00 -9.0000e+02 -1.4857e+03 3.7702e+00 -9.0000e+02 -1.4571e+03 3.6355e+00 -9.0000e+02 -1.4286e+03 3.4958e+00 -9.0000e+02 -1.4000e+03 3.3506e+00 -9.0000e+02 -1.3714e+03 3.1998e+00 -9.0000e+02 -1.3429e+03 3.0431e+00 -9.0000e+02 -1.3143e+03 2.8801e+00 -9.0000e+02 -1.2857e+03 2.7106e+00 -9.0000e+02 -1.2571e+03 2.5342e+00 -9.0000e+02 -1.2286e+03 2.3507e+00 -9.0000e+02 -1.2000e+03 2.1595e+00 -9.0000e+02 -1.1714e+03 1.9605e+00 -9.0000e+02 -1.1429e+03 1.7531e+00 -9.0000e+02 -1.1143e+03 1.5370e+00 -9.0000e+02 -1.0857e+03 1.3117e+00 -9.0000e+02 -1.0571e+03 1.0770e+00 -9.0000e+02 -1.0286e+03 8.3218e-01 -9.0000e+02 -1.0000e+03 5.7699e-01 -9.0000e+02 -9.7143e+02 3.1094e-01 -9.0000e+02 -9.4286e+02 3.3585e-02 -9.0000e+02 -9.1429e+02 -2.5550e-01 -9.0000e+02 -8.8571e+02 -5.5672e-01 -9.0000e+02 -8.5714e+02 -8.7046e-01 -9.0000e+02 -8.2857e+02 -1.1971e+00 -9.0000e+02 -8.0000e+02 -1.5369e+00 -9.0000e+02 -7.7143e+02 -1.8901e+00 -9.0000e+02 -7.4286e+02 -2.2570e+00 -9.0000e+02 -7.1429e+02 -2.6374e+00 -9.0000e+02 -6.8571e+02 -3.0315e+00 -9.0000e+02 -6.5714e+02 -3.4389e+00 -9.0000e+02 -6.2857e+02 -3.8593e+00 -9.0000e+02 -6.0000e+02 -4.2921e+00 -9.0000e+02 -5.7143e+02 -4.7363e+00 -9.0000e+02 -5.4286e+02 -5.1910e+00 -9.0000e+02 -5.1429e+02 -5.6546e+00 -9.0000e+02 -4.8571e+02 -6.1253e+00 -9.0000e+02 -4.5714e+02 -6.6010e+00 -9.0000e+02 -4.2857e+02 -7.0792e+00 -9.0000e+02 -4.0000e+02 -7.5568e+00 -9.0000e+02 -3.7143e+02 -8.0305e+00 -9.0000e+02 -3.4286e+02 -8.4963e+00 -9.0000e+02 -3.1429e+02 -8.9503e+00 -9.0000e+02 -2.8571e+02 -9.3878e+00 -9.0000e+02 -2.5714e+02 -9.8041e+00 -9.0000e+02 -2.2857e+02 -1.0194e+01 -9.0000e+02 -2.0000e+02 -1.0553e+01 -9.0000e+02 -1.7143e+02 -1.0876e+01 -9.0000e+02 -1.4286e+02 -1.1159e+01 -9.0000e+02 -1.1429e+02 -1.1397e+01 -9.0000e+02 -8.5714e+01 -1.1586e+01 -9.0000e+02 -5.7143e+01 -1.1723e+01 -9.0000e+02 -2.8571e+01 -1.1806e+01 -9.0000e+02 0.0000e+00 -1.1834e+01 -9.0000e+02 2.8571e+01 -1.1806e+01 -9.0000e+02 5.7143e+01 -1.1723e+01 -9.0000e+02 8.5714e+01 -1.1586e+01 -9.0000e+02 1.1429e+02 -1.1397e+01 -9.0000e+02 1.4286e+02 -1.1159e+01 -9.0000e+02 1.7143e+02 -1.0876e+01 -9.0000e+02 2.0000e+02 -1.0553e+01 -9.0000e+02 2.2857e+02 -1.0194e+01 -9.0000e+02 2.5714e+02 -9.8041e+00 -9.0000e+02 2.8571e+02 -9.3878e+00 -9.0000e+02 3.1429e+02 -8.9503e+00 -9.0000e+02 3.4286e+02 -8.4963e+00 -9.0000e+02 3.7143e+02 -8.0305e+00 -9.0000e+02 4.0000e+02 -7.5568e+00 -9.0000e+02 4.2857e+02 -7.0792e+00 -9.0000e+02 4.5714e+02 -6.6010e+00 -9.0000e+02 4.8571e+02 -6.1253e+00 -9.0000e+02 5.1429e+02 -5.6546e+00 -9.0000e+02 5.4286e+02 -5.1910e+00 -9.0000e+02 5.7143e+02 -4.7363e+00 -9.0000e+02 6.0000e+02 -4.2921e+00 -9.0000e+02 6.2857e+02 -3.8593e+00 -9.0000e+02 6.5714e+02 -3.4389e+00 -9.0000e+02 6.8571e+02 -3.0315e+00 -9.0000e+02 7.1429e+02 -2.6374e+00 -9.0000e+02 7.4286e+02 -2.2570e+00 -9.0000e+02 7.7143e+02 -1.8901e+00 -9.0000e+02 8.0000e+02 -1.5369e+00 -9.0000e+02 8.2857e+02 -1.1971e+00 -9.0000e+02 8.5714e+02 -8.7046e-01 -9.0000e+02 8.8571e+02 -5.5672e-01 -9.0000e+02 9.1429e+02 -2.5550e-01 -9.0000e+02 9.4286e+02 3.3585e-02 -9.0000e+02 9.7143e+02 3.1094e-01 -9.0000e+02 1.0000e+03 5.7699e-01 -9.0000e+02 1.0286e+03 8.3218e-01 -9.0000e+02 1.0571e+03 1.0770e+00 -9.0000e+02 1.0857e+03 1.3117e+00 -9.0000e+02 1.1143e+03 1.5370e+00 -9.0000e+02 1.1429e+03 1.7531e+00 -9.0000e+02 1.1714e+03 1.9605e+00 -9.0000e+02 1.2000e+03 2.1595e+00 -9.0000e+02 1.2286e+03 2.3507e+00 -9.0000e+02 1.2571e+03 2.5342e+00 -9.0000e+02 1.2857e+03 2.7106e+00 -9.0000e+02 1.3143e+03 2.8801e+00 -9.0000e+02 1.3429e+03 3.0431e+00 -9.0000e+02 1.3714e+03 3.1998e+00 -9.0000e+02 1.4000e+03 3.3506e+00 -9.0000e+02 1.4286e+03 3.4958e+00 -9.0000e+02 1.4571e+03 3.6355e+00 -9.0000e+02 1.4857e+03 3.7702e+00 -9.0000e+02 1.5143e+03 3.8999e+00 -9.0000e+02 1.5429e+03 4.0250e+00 -9.0000e+02 1.5714e+03 4.1457e+00 -9.0000e+02 1.6000e+03 4.2621e+00 -9.0000e+02 1.6286e+03 4.3744e+00 -9.0000e+02 1.6571e+03 4.4829e+00 -9.0000e+02 1.6857e+03 4.5878e+00 -9.0000e+02 1.7143e+03 4.6891e+00 -9.0000e+02 1.7429e+03 4.7870e+00 -9.0000e+02 1.7714e+03 4.8817e+00 -9.0000e+02 1.8000e+03 4.9734e+00 -9.0000e+02 1.8286e+03 5.0621e+00 -9.0000e+02 1.8571e+03 5.1480e+00 -9.0000e+02 1.8857e+03 5.2313e+00 -9.0000e+02 1.9143e+03 5.3119e+00 -9.0000e+02 1.9429e+03 5.3901e+00 -9.0000e+02 1.9714e+03 5.4660e+00 -9.0000e+02 2.0000e+03 5.5395e+00 -9.3000e+02 -2.0000e+03 5.5740e+00 -9.3000e+02 -1.9714e+03 5.5020e+00 -9.3000e+02 -1.9429e+03 5.4278e+00 -9.3000e+02 -1.9143e+03 5.3513e+00 -9.3000e+02 -1.8857e+03 5.2725e+00 -9.3000e+02 -1.8571e+03 5.1913e+00 -9.3000e+02 -1.8286e+03 5.1074e+00 -9.3000e+02 -1.8000e+03 5.0209e+00 -9.3000e+02 -1.7714e+03 4.9316e+00 -9.3000e+02 -1.7429e+03 4.8394e+00 -9.3000e+02 -1.7143e+03 4.7441e+00 -9.3000e+02 -1.6857e+03 4.6456e+00 -9.3000e+02 -1.6571e+03 4.5438e+00 -9.3000e+02 -1.6286e+03 4.4386e+00 -9.3000e+02 -1.6000e+03 4.3296e+00 -9.3000e+02 -1.5714e+03 4.2169e+00 -9.3000e+02 -1.5429e+03 4.1002e+00 -9.3000e+02 -1.5143e+03 3.9793e+00 -9.3000e+02 -1.4857e+03 3.8541e+00 -9.3000e+02 -1.4571e+03 3.7243e+00 -9.3000e+02 -1.4286e+03 3.5897e+00 -9.3000e+02 -1.4000e+03 3.4501e+00 -9.3000e+02 -1.3714e+03 3.3053e+00 -9.3000e+02 -1.3429e+03 3.1550e+00 -9.3000e+02 -1.3143e+03 2.9989e+00 -9.3000e+02 -1.2857e+03 2.8369e+00 -9.3000e+02 -1.2571e+03 2.6685e+00 -9.3000e+02 -1.2286e+03 2.4936e+00 -9.3000e+02 -1.2000e+03 2.3118e+00 -9.3000e+02 -1.1714e+03 2.1227e+00 -9.3000e+02 -1.1429e+03 1.9262e+00 -9.3000e+02 -1.1143e+03 1.7218e+00 -9.3000e+02 -1.0857e+03 1.5093e+00 -9.3000e+02 -1.0571e+03 1.2882e+00 -9.3000e+02 -1.0286e+03 1.0583e+00 -9.3000e+02 -1.0000e+03 8.1915e-01 -9.3000e+02 -9.7143e+02 5.7048e-01 -9.3000e+02 -9.4286e+02 3.1195e-01 -9.3000e+02 -9.1429e+02 4.3236e-02 -9.3000e+02 -8.8571e+02 -2.3594e-01 -9.3000e+02 -8.5714e+02 -5.2583e-01 -9.3000e+02 -8.2857e+02 -8.2665e-01 -9.3000e+02 -8.0000e+02 -1.1386e+00 -9.3000e+02 -7.7143e+02 -1.4617e+00 -9.3000e+02 -7.4286e+02 -1.7961e+00 -9.3000e+02 -7.1429e+02 -2.1415e+00 -9.3000e+02 -6.8571e+02 -2.4980e+00 -9.3000e+02 -6.5714e+02 -2.8650e+00 -9.3000e+02 -6.2857e+02 -3.2422e+00 -9.3000e+02 -6.0000e+02 -3.6288e+00 -9.3000e+02 -5.7143e+02 -4.0240e+00 -9.3000e+02 -5.4286e+02 -4.4267e+00 -9.3000e+02 -5.1429e+02 -4.8354e+00 -9.3000e+02 -4.8571e+02 -5.2486e+00 -9.3000e+02 -4.5714e+02 -5.6643e+00 -9.3000e+02 -4.2857e+02 -6.0802e+00 -9.3000e+02 -4.0000e+02 -6.4937e+00 -9.3000e+02 -3.7143e+02 -6.9019e+00 -9.3000e+02 -3.4286e+02 -7.3018e+00 -9.3000e+02 -3.1429e+02 -7.6897e+00 -9.3000e+02 -2.8571e+02 -8.0621e+00 -9.3000e+02 -2.5714e+02 -8.4151e+00 -9.3000e+02 -2.2857e+02 -8.7448e+00 -9.3000e+02 -2.0000e+02 -9.0472e+00 -9.3000e+02 -1.7143e+02 -9.3185e+00 -9.3000e+02 -1.4286e+02 -9.5551e+00 -9.3000e+02 -1.1429e+02 -9.7537e+00 -9.3000e+02 -8.5714e+01 -9.9113e+00 -9.3000e+02 -5.7143e+01 -1.0026e+01 -9.3000e+02 -2.8571e+01 -1.0095e+01 -9.3000e+02 0.0000e+00 -1.0118e+01 -9.3000e+02 2.8571e+01 -1.0095e+01 -9.3000e+02 5.7143e+01 -1.0026e+01 -9.3000e+02 8.5714e+01 -9.9113e+00 -9.3000e+02 1.1429e+02 -9.7537e+00 -9.3000e+02 1.4286e+02 -9.5551e+00 -9.3000e+02 1.7143e+02 -9.3185e+00 -9.3000e+02 2.0000e+02 -9.0472e+00 -9.3000e+02 2.2857e+02 -8.7448e+00 -9.3000e+02 2.5714e+02 -8.4151e+00 -9.3000e+02 2.8571e+02 -8.0621e+00 -9.3000e+02 3.1429e+02 -7.6897e+00 -9.3000e+02 3.4286e+02 -7.3018e+00 -9.3000e+02 3.7143e+02 -6.9019e+00 -9.3000e+02 4.0000e+02 -6.4937e+00 -9.3000e+02 4.2857e+02 -6.0802e+00 -9.3000e+02 4.5714e+02 -5.6643e+00 -9.3000e+02 4.8571e+02 -5.2486e+00 -9.3000e+02 5.1429e+02 -4.8354e+00 -9.3000e+02 5.4286e+02 -4.4267e+00 -9.3000e+02 5.7143e+02 -4.0240e+00 -9.3000e+02 6.0000e+02 -3.6288e+00 -9.3000e+02 6.2857e+02 -3.2422e+00 -9.3000e+02 6.5714e+02 -2.8650e+00 -9.3000e+02 6.8571e+02 -2.4980e+00 -9.3000e+02 7.1429e+02 -2.1415e+00 -9.3000e+02 7.4286e+02 -1.7961e+00 -9.3000e+02 7.7143e+02 -1.4617e+00 -9.3000e+02 8.0000e+02 -1.1386e+00 -9.3000e+02 8.2857e+02 -8.2665e-01 -9.3000e+02 8.5714e+02 -5.2583e-01 -9.3000e+02 8.8571e+02 -2.3594e-01 -9.3000e+02 9.1429e+02 4.3236e-02 -9.3000e+02 9.4286e+02 3.1195e-01 -9.3000e+02 9.7143e+02 5.7048e-01 -9.3000e+02 1.0000e+03 8.1915e-01 -9.3000e+02 1.0286e+03 1.0583e+00 -9.3000e+02 1.0571e+03 1.2882e+00 -9.3000e+02 1.0857e+03 1.5093e+00 -9.3000e+02 1.1143e+03 1.7218e+00 -9.3000e+02 1.1429e+03 1.9262e+00 -9.3000e+02 1.1714e+03 2.1227e+00 -9.3000e+02 1.2000e+03 2.3118e+00 -9.3000e+02 1.2286e+03 2.4936e+00 -9.3000e+02 1.2571e+03 2.6685e+00 -9.3000e+02 1.2857e+03 2.8369e+00 -9.3000e+02 1.3143e+03 2.9989e+00 -9.3000e+02 1.3429e+03 3.1550e+00 -9.3000e+02 1.3714e+03 3.3053e+00 -9.3000e+02 1.4000e+03 3.4501e+00 -9.3000e+02 1.4286e+03 3.5897e+00 -9.3000e+02 1.4571e+03 3.7243e+00 -9.3000e+02 1.4857e+03 3.8541e+00 -9.3000e+02 1.5143e+03 3.9793e+00 -9.3000e+02 1.5429e+03 4.1002e+00 -9.3000e+02 1.5714e+03 4.2169e+00 -9.3000e+02 1.6000e+03 4.3296e+00 -9.3000e+02 1.6286e+03 4.4386e+00 -9.3000e+02 1.6571e+03 4.5438e+00 -9.3000e+02 1.6857e+03 4.6456e+00 -9.3000e+02 1.7143e+03 4.7441e+00 -9.3000e+02 1.7429e+03 4.8394e+00 -9.3000e+02 1.7714e+03 4.9316e+00 -9.3000e+02 1.8000e+03 5.0209e+00 -9.3000e+02 1.8286e+03 5.1074e+00 -9.3000e+02 1.8571e+03 5.1913e+00 -9.3000e+02 1.8857e+03 5.2725e+00 -9.3000e+02 1.9143e+03 5.3513e+00 -9.3000e+02 1.9429e+03 5.4278e+00 -9.3000e+02 1.9714e+03 5.5020e+00 -9.3000e+02 2.0000e+03 5.5740e+00 -9.6000e+02 -2.0000e+03 5.6088e+00 -9.6000e+02 -1.9714e+03 5.5383e+00 -9.6000e+02 -1.9429e+03 5.4658e+00 -9.6000e+02 -1.9143e+03 5.3911e+00 -9.6000e+02 -1.8857e+03 5.3141e+00 -9.6000e+02 -1.8571e+03 5.2348e+00 -9.6000e+02 -1.8286e+03 5.1531e+00 -9.6000e+02 -1.8000e+03 5.0687e+00 -9.6000e+02 -1.7714e+03 4.9818e+00 -9.6000e+02 -1.7429e+03 4.8920e+00 -9.6000e+02 -1.7143e+03 4.7994e+00 -9.6000e+02 -1.6857e+03 4.7037e+00 -9.6000e+02 -1.6571e+03 4.6049e+00 -9.6000e+02 -1.6286e+03 4.5028e+00 -9.6000e+02 -1.6000e+03 4.3973e+00 -9.6000e+02 -1.5714e+03 4.2882e+00 -9.6000e+02 -1.5429e+03 4.1753e+00 -9.6000e+02 -1.5143e+03 4.0585e+00 -9.6000e+02 -1.4857e+03 3.9377e+00 -9.6000e+02 -1.4571e+03 3.8126e+00 -9.6000e+02 -1.4286e+03 3.6831e+00 -9.6000e+02 -1.4000e+03 3.5489e+00 -9.6000e+02 -1.3714e+03 3.4099e+00 -9.6000e+02 -1.3429e+03 3.2658e+00 -9.6000e+02 -1.3143e+03 3.1164e+00 -9.6000e+02 -1.2857e+03 2.9615e+00 -9.6000e+02 -1.2571e+03 2.8008e+00 -9.6000e+02 -1.2286e+03 2.6341e+00 -9.6000e+02 -1.2000e+03 2.4612e+00 -9.6000e+02 -1.1714e+03 2.2817e+00 -9.6000e+02 -1.1429e+03 2.0955e+00 -9.6000e+02 -1.1143e+03 1.9022e+00 -9.6000e+02 -1.0857e+03 1.7016e+00 -9.6000e+02 -1.0571e+03 1.4934e+00 -9.6000e+02 -1.0286e+03 1.2774e+00 -9.6000e+02 -1.0000e+03 1.0533e+00 -9.6000e+02 -9.7143e+02 8.2075e-01 -9.6000e+02 -9.4286e+02 5.7963e-01 -9.6000e+02 -9.1429e+02 3.2971e-01 -9.6000e+02 -8.8571e+02 7.0775e-02 -9.6000e+02 -8.5714e+02 -1.9731e-01 -9.6000e+02 -8.2857e+02 -4.7467e-01 -9.6000e+02 -8.0000e+02 -7.6135e-01 -9.6000e+02 -7.7143e+02 -1.0574e+00 -9.6000e+02 -7.4286e+02 -1.3626e+00 -9.6000e+02 -7.1429e+02 -1.6769e+00 -9.6000e+02 -6.8571e+02 -2.0000e+00 -9.6000e+02 -6.5714e+02 -2.3315e+00 -9.6000e+02 -6.2857e+02 -2.6708e+00 -9.6000e+02 -6.0000e+02 -3.0172e+00 -9.6000e+02 -5.7143e+02 -3.3700e+00 -9.6000e+02 -5.4286e+02 -3.7279e+00 -9.6000e+02 -5.1429e+02 -4.0897e+00 -9.6000e+02 -4.8571e+02 -4.4539e+00 -9.6000e+02 -4.5714e+02 -4.8188e+00 -9.6000e+02 -4.2857e+02 -5.1824e+00 -9.6000e+02 -4.0000e+02 -5.5424e+00 -9.6000e+02 -3.7143e+02 -5.8964e+00 -9.6000e+02 -3.4286e+02 -6.2418e+00 -9.6000e+02 -3.1429e+02 -6.5756e+00 -9.6000e+02 -2.8571e+02 -6.8948e+00 -9.6000e+02 -2.5714e+02 -7.1964e+00 -9.6000e+02 -2.2857e+02 -7.4771e+00 -9.6000e+02 -2.0000e+02 -7.7339e+00 -9.6000e+02 -1.7143e+02 -7.9636e+00 -9.6000e+02 -1.4286e+02 -8.1635e+00 -9.6000e+02 -1.1429e+02 -8.3309e+00 -9.6000e+02 -8.5714e+01 -8.4636e+00 -9.6000e+02 -5.7143e+01 -8.5597e+00 -9.6000e+02 -2.8571e+01 -8.6180e+00 -9.6000e+02 0.0000e+00 -8.6375e+00 -9.6000e+02 2.8571e+01 -8.6180e+00 -9.6000e+02 5.7143e+01 -8.5597e+00 -9.6000e+02 8.5714e+01 -8.4636e+00 -9.6000e+02 1.1429e+02 -8.3309e+00 -9.6000e+02 1.4286e+02 -8.1635e+00 -9.6000e+02 1.7143e+02 -7.9636e+00 -9.6000e+02 2.0000e+02 -7.7339e+00 -9.6000e+02 2.2857e+02 -7.4771e+00 -9.6000e+02 2.5714e+02 -7.1964e+00 -9.6000e+02 2.8571e+02 -6.8948e+00 -9.6000e+02 3.1429e+02 -6.5756e+00 -9.6000e+02 3.4286e+02 -6.2418e+00 -9.6000e+02 3.7143e+02 -5.8964e+00 -9.6000e+02 4.0000e+02 -5.5424e+00 -9.6000e+02 4.2857e+02 -5.1824e+00 -9.6000e+02 4.5714e+02 -4.8188e+00 -9.6000e+02 4.8571e+02 -4.4539e+00 -9.6000e+02 5.1429e+02 -4.0897e+00 -9.6000e+02 5.4286e+02 -3.7279e+00 -9.6000e+02 5.7143e+02 -3.3700e+00 -9.6000e+02 6.0000e+02 -3.0172e+00 -9.6000e+02 6.2857e+02 -2.6708e+00 -9.6000e+02 6.5714e+02 -2.3315e+00 -9.6000e+02 6.8571e+02 -2.0000e+00 -9.6000e+02 7.1429e+02 -1.6769e+00 -9.6000e+02 7.4286e+02 -1.3626e+00 -9.6000e+02 7.7143e+02 -1.0574e+00 -9.6000e+02 8.0000e+02 -7.6135e-01 -9.6000e+02 8.2857e+02 -4.7467e-01 -9.6000e+02 8.5714e+02 -1.9731e-01 -9.6000e+02 8.8571e+02 7.0775e-02 -9.6000e+02 9.1429e+02 3.2971e-01 -9.6000e+02 9.4286e+02 5.7963e-01 -9.6000e+02 9.7143e+02 8.2075e-01 -9.6000e+02 1.0000e+03 1.0533e+00 -9.6000e+02 1.0286e+03 1.2774e+00 -9.6000e+02 1.0571e+03 1.4934e+00 -9.6000e+02 1.0857e+03 1.7016e+00 -9.6000e+02 1.1143e+03 1.9022e+00 -9.6000e+02 1.1429e+03 2.0955e+00 -9.6000e+02 1.1714e+03 2.2817e+00 -9.6000e+02 1.2000e+03 2.4612e+00 -9.6000e+02 1.2286e+03 2.6341e+00 -9.6000e+02 1.2571e+03 2.8008e+00 -9.6000e+02 1.2857e+03 2.9615e+00 -9.6000e+02 1.3143e+03 3.1164e+00 -9.6000e+02 1.3429e+03 3.2658e+00 -9.6000e+02 1.3714e+03 3.4099e+00 -9.6000e+02 1.4000e+03 3.5489e+00 -9.6000e+02 1.4286e+03 3.6831e+00 -9.6000e+02 1.4571e+03 3.8126e+00 -9.6000e+02 1.4857e+03 3.9377e+00 -9.6000e+02 1.5143e+03 4.0585e+00 -9.6000e+02 1.5429e+03 4.1753e+00 -9.6000e+02 1.5714e+03 4.2882e+00 -9.6000e+02 1.6000e+03 4.3973e+00 -9.6000e+02 1.6286e+03 4.5028e+00 -9.6000e+02 1.6571e+03 4.6049e+00 -9.6000e+02 1.6857e+03 4.7037e+00 -9.6000e+02 1.7143e+03 4.7994e+00 -9.6000e+02 1.7429e+03 4.8920e+00 -9.6000e+02 1.7714e+03 4.9818e+00 -9.6000e+02 1.8000e+03 5.0687e+00 -9.6000e+02 1.8286e+03 5.1531e+00 -9.6000e+02 1.8571e+03 5.2348e+00 -9.6000e+02 1.8857e+03 5.3141e+00 -9.6000e+02 1.9143e+03 5.3911e+00 -9.6000e+02 1.9429e+03 5.4658e+00 -9.6000e+02 1.9714e+03 5.5383e+00 -9.6000e+02 2.0000e+03 5.6088e+00 -9.9000e+02 -2.0000e+03 5.6439e+00 -9.9000e+02 -1.9714e+03 5.5750e+00 -9.9000e+02 -1.9429e+03 5.5041e+00 -9.9000e+02 -1.9143e+03 5.4312e+00 -9.9000e+02 -1.8857e+03 5.3560e+00 -9.9000e+02 -1.8571e+03 5.2787e+00 -9.9000e+02 -1.8286e+03 5.1989e+00 -9.9000e+02 -1.8000e+03 5.1168e+00 -9.9000e+02 -1.7714e+03 5.0321e+00 -9.9000e+02 -1.7429e+03 4.9448e+00 -9.9000e+02 -1.7143e+03 4.8548e+00 -9.9000e+02 -1.6857e+03 4.7619e+00 -9.9000e+02 -1.6571e+03 4.6660e+00 -9.9000e+02 -1.6286e+03 4.5670e+00 -9.9000e+02 -1.6000e+03 4.4648e+00 -9.9000e+02 -1.5714e+03 4.3592e+00 -9.9000e+02 -1.5429e+03 4.2502e+00 -9.9000e+02 -1.5143e+03 4.1374e+00 -9.9000e+02 -1.4857e+03 4.0209e+00 -9.9000e+02 -1.4571e+03 3.9004e+00 -9.9000e+02 -1.4286e+03 3.7757e+00 -9.9000e+02 -1.4000e+03 3.6468e+00 -9.9000e+02 -1.3714e+03 3.5133e+00 -9.9000e+02 -1.3429e+03 3.3752e+00 -9.9000e+02 -1.3143e+03 3.2322e+00 -9.9000e+02 -1.2857e+03 3.0842e+00 -9.9000e+02 -1.2571e+03 2.9309e+00 -9.9000e+02 -1.2286e+03 2.7721e+00 -9.9000e+02 -1.2000e+03 2.6077e+00 -9.9000e+02 -1.1714e+03 2.4373e+00 -9.9000e+02 -1.1429e+03 2.2608e+00 -9.9000e+02 -1.1143e+03 2.0780e+00 -9.9000e+02 -1.0857e+03 1.8887e+00 -9.9000e+02 -1.0571e+03 1.6926e+00 -9.9000e+02 -1.0286e+03 1.4896e+00 -9.9000e+02 -1.0000e+03 1.2794e+00 -9.9000e+02 -9.7143e+02 1.0619e+00 -9.9000e+02 -9.4286e+02 8.3695e-01 -9.9000e+02 -9.1429e+02 6.0433e-01 -9.9000e+02 -8.8571e+02 3.6398e-01 -9.9000e+02 -8.5714e+02 1.1583e-01 -9.9000e+02 -8.2857e+02 -1.4017e-01 -9.9000e+02 -8.0000e+02 -4.0398e-01 -9.9000e+02 -7.7143e+02 -6.7552e-01 -9.9000e+02 -7.4286e+02 -9.5465e-01 -9.9000e+02 -7.1429e+02 -1.2411e+00 -9.9000e+02 -6.8571e+02 -1.5346e+00 -9.9000e+02 -6.5714e+02 -1.8346e+00 -9.9000e+02 -6.2857e+02 -2.1407e+00 -9.9000e+02 -6.0000e+02 -2.4521e+00 -9.9000e+02 -5.7143e+02 -2.7679e+00 -9.9000e+02 -5.4286e+02 -3.0871e+00 -9.9000e+02 -5.1429e+02 -3.4086e+00 -9.9000e+02 -4.8571e+02 -3.7311e+00 -9.9000e+02 -4.5714e+02 -4.0529e+00 -9.9000e+02 -4.2857e+02 -4.3723e+00 -9.9000e+02 -4.0000e+02 -4.6874e+00 -9.9000e+02 -3.7143e+02 -4.9961e+00 -9.9000e+02 -3.4286e+02 -5.2961e+00 -9.9000e+02 -3.1429e+02 -5.5852e+00 -9.9000e+02 -2.8571e+02 -5.8607e+00 -9.9000e+02 -2.5714e+02 -6.1201e+00 -9.9000e+02 -2.2857e+02 -6.3609e+00 -9.9000e+02 -2.0000e+02 -6.5806e+00 -9.9000e+02 -1.7143e+02 -6.7767e+00 -9.9000e+02 -1.4286e+02 -6.9469e+00 -9.9000e+02 -1.1429e+02 -7.0892e+00 -9.9000e+02 -8.5714e+01 -7.2019e+00 -9.9000e+02 -5.7143e+01 -7.2834e+00 -9.9000e+02 -2.8571e+01 -7.3327e+00 -9.9000e+02 0.0000e+00 -7.3493e+00 -9.9000e+02 2.8571e+01 -7.3327e+00 -9.9000e+02 5.7143e+01 -7.2834e+00 -9.9000e+02 8.5714e+01 -7.2019e+00 -9.9000e+02 1.1429e+02 -7.0892e+00 -9.9000e+02 1.4286e+02 -6.9469e+00 -9.9000e+02 1.7143e+02 -6.7767e+00 -9.9000e+02 2.0000e+02 -6.5806e+00 -9.9000e+02 2.2857e+02 -6.3609e+00 -9.9000e+02 2.5714e+02 -6.1201e+00 -9.9000e+02 2.8571e+02 -5.8607e+00 -9.9000e+02 3.1429e+02 -5.5852e+00 -9.9000e+02 3.4286e+02 -5.2961e+00 -9.9000e+02 3.7143e+02 -4.9961e+00 -9.9000e+02 4.0000e+02 -4.6874e+00 -9.9000e+02 4.2857e+02 -4.3723e+00 -9.9000e+02 4.5714e+02 -4.0529e+00 -9.9000e+02 4.8571e+02 -3.7311e+00 -9.9000e+02 5.1429e+02 -3.4086e+00 -9.9000e+02 5.4286e+02 -3.0871e+00 -9.9000e+02 5.7143e+02 -2.7679e+00 -9.9000e+02 6.0000e+02 -2.4521e+00 -9.9000e+02 6.2857e+02 -2.1407e+00 -9.9000e+02 6.5714e+02 -1.8346e+00 -9.9000e+02 6.8571e+02 -1.5346e+00 -9.9000e+02 7.1429e+02 -1.2411e+00 -9.9000e+02 7.4286e+02 -9.5465e-01 -9.9000e+02 7.7143e+02 -6.7552e-01 -9.9000e+02 8.0000e+02 -4.0398e-01 -9.9000e+02 8.2857e+02 -1.4017e-01 -9.9000e+02 8.5714e+02 1.1583e-01 -9.9000e+02 8.8571e+02 3.6398e-01 -9.9000e+02 9.1429e+02 6.0433e-01 -9.9000e+02 9.4286e+02 8.3695e-01 -9.9000e+02 9.7143e+02 1.0619e+00 -9.9000e+02 1.0000e+03 1.2794e+00 -9.9000e+02 1.0286e+03 1.4896e+00 -9.9000e+02 1.0571e+03 1.6926e+00 -9.9000e+02 1.0857e+03 1.8887e+00 -9.9000e+02 1.1143e+03 2.0780e+00 -9.9000e+02 1.1429e+03 2.2608e+00 -9.9000e+02 1.1714e+03 2.4373e+00 -9.9000e+02 1.2000e+03 2.6077e+00 -9.9000e+02 1.2286e+03 2.7721e+00 -9.9000e+02 1.2571e+03 2.9309e+00 -9.9000e+02 1.2857e+03 3.0842e+00 -9.9000e+02 1.3143e+03 3.2322e+00 -9.9000e+02 1.3429e+03 3.3752e+00 -9.9000e+02 1.3714e+03 3.5133e+00 -9.9000e+02 1.4000e+03 3.6468e+00 -9.9000e+02 1.4286e+03 3.7757e+00 -9.9000e+02 1.4571e+03 3.9004e+00 -9.9000e+02 1.4857e+03 4.0209e+00 -9.9000e+02 1.5143e+03 4.1374e+00 -9.9000e+02 1.5429e+03 4.2502e+00 -9.9000e+02 1.5714e+03 4.3592e+00 -9.9000e+02 1.6000e+03 4.4648e+00 -9.9000e+02 1.6286e+03 4.5670e+00 -9.9000e+02 1.6571e+03 4.6660e+00 -9.9000e+02 1.6857e+03 4.7619e+00 -9.9000e+02 1.7143e+03 4.8548e+00 -9.9000e+02 1.7429e+03 4.9448e+00 -9.9000e+02 1.7714e+03 5.0321e+00 -9.9000e+02 1.8000e+03 5.1168e+00 -9.9000e+02 1.8286e+03 5.1989e+00 -9.9000e+02 1.8571e+03 5.2787e+00 -9.9000e+02 1.8857e+03 5.3560e+00 -9.9000e+02 1.9143e+03 5.4312e+00 -9.9000e+02 1.9429e+03 5.5041e+00 -9.9000e+02 1.9714e+03 5.5750e+00 -9.9000e+02 2.0000e+03 5.6439e+00 -1.0200e+03 -2.0000e+03 5.6794e+00 -1.0200e+03 -1.9714e+03 5.6120e+00 -1.0200e+03 -1.9429e+03 5.5427e+00 -1.0200e+03 -1.9143e+03 5.4715e+00 -1.0200e+03 -1.8857e+03 5.3982e+00 -1.0200e+03 -1.8571e+03 5.3227e+00 -1.0200e+03 -1.8286e+03 5.2450e+00 -1.0200e+03 -1.8000e+03 5.1650e+00 -1.0200e+03 -1.7714e+03 5.0826e+00 -1.0200e+03 -1.7429e+03 4.9977e+00 -1.0200e+03 -1.7143e+03 4.9103e+00 -1.0200e+03 -1.6857e+03 4.8201e+00 -1.0200e+03 -1.6571e+03 4.7271e+00 -1.0200e+03 -1.6286e+03 4.6311e+00 -1.0200e+03 -1.6000e+03 4.5322e+00 -1.0200e+03 -1.5714e+03 4.4301e+00 -1.0200e+03 -1.5429e+03 4.3247e+00 -1.0200e+03 -1.5143e+03 4.2159e+00 -1.0200e+03 -1.4857e+03 4.1035e+00 -1.0200e+03 -1.4571e+03 3.9874e+00 -1.0200e+03 -1.4286e+03 3.8675e+00 -1.0200e+03 -1.4000e+03 3.7436e+00 -1.0200e+03 -1.3714e+03 3.6156e+00 -1.0200e+03 -1.3429e+03 3.4833e+00 -1.0200e+03 -1.3143e+03 3.3464e+00 -1.0200e+03 -1.2857e+03 3.2050e+00 -1.0200e+03 -1.2571e+03 3.0587e+00 -1.0200e+03 -1.2286e+03 2.9075e+00 -1.0200e+03 -1.2000e+03 2.7511e+00 -1.0200e+03 -1.1714e+03 2.5894e+00 -1.0200e+03 -1.1429e+03 2.4222e+00 -1.0200e+03 -1.1143e+03 2.2493e+00 -1.0200e+03 -1.0857e+03 2.0706e+00 -1.0200e+03 -1.0571e+03 1.8858e+00 -1.0200e+03 -1.0286e+03 1.6950e+00 -1.0200e+03 -1.0000e+03 1.4978e+00 -1.0200e+03 -9.7143e+02 1.2943e+00 -1.0200e+03 -9.4286e+02 1.0842e+00 -1.0200e+03 -9.1429e+02 8.6755e-01 -1.0200e+03 -8.8571e+02 6.4427e-01 -1.0200e+03 -8.5714e+02 4.1434e-01 -1.0200e+03 -8.2857e+02 1.7780e-01 -1.0200e+03 -8.0000e+02 -6.5281e-02 -1.0200e+03 -7.7143e+02 -3.1476e-01 -1.0200e+03 -7.4286e+02 -5.7042e-01 -1.0200e+03 -7.1429e+02 -8.3201e-01 -1.0200e+03 -6.8571e+02 -1.0991e+00 -1.0200e+03 -6.5714e+02 -1.3714e+00 -1.0200e+03 -6.2857e+02 -1.6481e+00 -1.0200e+03 -6.0000e+02 -1.9287e+00 -1.0200e+03 -5.7143e+02 -2.2123e+00 -1.0200e+03 -5.4286e+02 -2.4981e+00 -1.0200e+03 -5.1429e+02 -2.7848e+00 -1.0200e+03 -4.8571e+02 -3.0714e+00 -1.0200e+03 -4.5714e+02 -3.3564e+00 -1.0200e+03 -4.2857e+02 -3.6383e+00 -1.0200e+03 -4.0000e+02 -3.9154e+00 -1.0200e+03 -3.7143e+02 -4.1861e+00 -1.0200e+03 -3.4286e+02 -4.4482e+00 -1.0200e+03 -3.1429e+02 -4.7000e+00 -1.0200e+03 -2.8571e+02 -4.9393e+00 -1.0200e+03 -2.5714e+02 -5.1639e+00 -1.0200e+03 -2.2857e+02 -5.3719e+00 -1.0200e+03 -2.0000e+02 -5.5612e+00 -1.0200e+03 -1.7143e+02 -5.7298e+00 -1.0200e+03 -1.4286e+02 -5.8759e+00 -1.0200e+03 -1.1429e+02 -5.9978e+00 -1.0200e+03 -8.5714e+01 -6.0942e+00 -1.0200e+03 -5.7143e+01 -6.1639e+00 -1.0200e+03 -2.8571e+01 -6.2060e+00 -1.0200e+03 0.0000e+00 -6.2201e+00 -1.0200e+03 2.8571e+01 -6.2060e+00 -1.0200e+03 5.7143e+01 -6.1639e+00 -1.0200e+03 8.5714e+01 -6.0942e+00 -1.0200e+03 1.1429e+02 -5.9978e+00 -1.0200e+03 1.4286e+02 -5.8759e+00 -1.0200e+03 1.7143e+02 -5.7298e+00 -1.0200e+03 2.0000e+02 -5.5612e+00 -1.0200e+03 2.2857e+02 -5.3719e+00 -1.0200e+03 2.5714e+02 -5.1639e+00 -1.0200e+03 2.8571e+02 -4.9393e+00 -1.0200e+03 3.1429e+02 -4.7000e+00 -1.0200e+03 3.4286e+02 -4.4482e+00 -1.0200e+03 3.7143e+02 -4.1861e+00 -1.0200e+03 4.0000e+02 -3.9154e+00 -1.0200e+03 4.2857e+02 -3.6383e+00 -1.0200e+03 4.5714e+02 -3.3564e+00 -1.0200e+03 4.8571e+02 -3.0714e+00 -1.0200e+03 5.1429e+02 -2.7848e+00 -1.0200e+03 5.4286e+02 -2.4981e+00 -1.0200e+03 5.7143e+02 -2.2123e+00 -1.0200e+03 6.0000e+02 -1.9287e+00 -1.0200e+03 6.2857e+02 -1.6481e+00 -1.0200e+03 6.5714e+02 -1.3714e+00 -1.0200e+03 6.8571e+02 -1.0991e+00 -1.0200e+03 7.1429e+02 -8.3201e-01 -1.0200e+03 7.4286e+02 -5.7042e-01 -1.0200e+03 7.7143e+02 -3.1476e-01 -1.0200e+03 8.0000e+02 -6.5281e-02 -1.0200e+03 8.2857e+02 1.7780e-01 -1.0200e+03 8.5714e+02 4.1434e-01 -1.0200e+03 8.8571e+02 6.4427e-01 -1.0200e+03 9.1429e+02 8.6755e-01 -1.0200e+03 9.4286e+02 1.0842e+00 -1.0200e+03 9.7143e+02 1.2943e+00 -1.0200e+03 1.0000e+03 1.4978e+00 -1.0200e+03 1.0286e+03 1.6950e+00 -1.0200e+03 1.0571e+03 1.8858e+00 -1.0200e+03 1.0857e+03 2.0706e+00 -1.0200e+03 1.1143e+03 2.2493e+00 -1.0200e+03 1.1429e+03 2.4222e+00 -1.0200e+03 1.1714e+03 2.5894e+00 -1.0200e+03 1.2000e+03 2.7511e+00 -1.0200e+03 1.2286e+03 2.9075e+00 -1.0200e+03 1.2571e+03 3.0587e+00 -1.0200e+03 1.2857e+03 3.2050e+00 -1.0200e+03 1.3143e+03 3.3464e+00 -1.0200e+03 1.3429e+03 3.4833e+00 -1.0200e+03 1.3714e+03 3.6156e+00 -1.0200e+03 1.4000e+03 3.7436e+00 -1.0200e+03 1.4286e+03 3.8675e+00 -1.0200e+03 1.4571e+03 3.9874e+00 -1.0200e+03 1.4857e+03 4.1035e+00 -1.0200e+03 1.5143e+03 4.2159e+00 -1.0200e+03 1.5429e+03 4.3247e+00 -1.0200e+03 1.5714e+03 4.4301e+00 -1.0200e+03 1.6000e+03 4.5322e+00 -1.0200e+03 1.6286e+03 4.6311e+00 -1.0200e+03 1.6571e+03 4.7271e+00 -1.0200e+03 1.6857e+03 4.8201e+00 -1.0200e+03 1.7143e+03 4.9103e+00 -1.0200e+03 1.7429e+03 4.9977e+00 -1.0200e+03 1.7714e+03 5.0826e+00 -1.0200e+03 1.8000e+03 5.1650e+00 -1.0200e+03 1.8286e+03 5.2450e+00 -1.0200e+03 1.8571e+03 5.3227e+00 -1.0200e+03 1.8857e+03 5.3982e+00 -1.0200e+03 1.9143e+03 5.4715e+00 -1.0200e+03 1.9429e+03 5.5427e+00 -1.0200e+03 1.9714e+03 5.6120e+00 -1.0200e+03 2.0000e+03 5.6794e+00 -1.0500e+03 -2.0000e+03 5.7151e+00 -1.0500e+03 -1.9714e+03 5.6492e+00 -1.0500e+03 -1.9429e+03 5.5816e+00 -1.0500e+03 -1.9143e+03 5.5120e+00 -1.0500e+03 -1.8857e+03 5.4405e+00 -1.0500e+03 -1.8571e+03 5.3669e+00 -1.0500e+03 -1.8286e+03 5.2913e+00 -1.0500e+03 -1.8000e+03 5.2134e+00 -1.0500e+03 -1.7714e+03 5.1332e+00 -1.0500e+03 -1.7429e+03 5.0507e+00 -1.0500e+03 -1.7143e+03 4.9657e+00 -1.0500e+03 -1.6857e+03 4.8782e+00 -1.0500e+03 -1.6571e+03 4.7880e+00 -1.0500e+03 -1.6286e+03 4.6951e+00 -1.0500e+03 -1.6000e+03 4.5993e+00 -1.0500e+03 -1.5714e+03 4.5006e+00 -1.0500e+03 -1.5429e+03 4.3988e+00 -1.0500e+03 -1.5143e+03 4.2938e+00 -1.0500e+03 -1.4857e+03 4.1855e+00 -1.0500e+03 -1.4571e+03 4.0737e+00 -1.0500e+03 -1.4286e+03 3.9584e+00 -1.0500e+03 -1.4000e+03 3.8394e+00 -1.0500e+03 -1.3714e+03 3.7166e+00 -1.0500e+03 -1.3429e+03 3.5898e+00 -1.0500e+03 -1.3143e+03 3.4589e+00 -1.0500e+03 -1.2857e+03 3.3238e+00 -1.0500e+03 -1.2571e+03 3.1843e+00 -1.0500e+03 -1.2286e+03 3.0402e+00 -1.0500e+03 -1.2000e+03 2.8915e+00 -1.0500e+03 -1.1714e+03 2.7380e+00 -1.0500e+03 -1.1429e+03 2.5795e+00 -1.0500e+03 -1.1143e+03 2.4160e+00 -1.0500e+03 -1.0857e+03 2.2472e+00 -1.0500e+03 -1.0571e+03 2.0731e+00 -1.0500e+03 -1.0286e+03 1.8936e+00 -1.0500e+03 -1.0000e+03 1.7086e+00 -1.0500e+03 -9.7143e+02 1.5180e+00 -1.0500e+03 -9.4286e+02 1.3217e+00 -1.0500e+03 -9.1429e+02 1.1198e+00 -1.0500e+03 -8.8571e+02 9.1220e-01 -1.0500e+03 -8.5714e+02 6.9895e-01 -1.0500e+03 -8.2857e+02 4.8013e-01 -1.0500e+03 -8.0000e+02 2.5587e-01 -1.0500e+03 -7.7143e+02 2.6339e-02 -1.0500e+03 -7.4286e+02 -2.0822e-01 -1.0500e+03 -7.1429e+02 -4.4752e-01 -1.0500e+03 -6.8571e+02 -6.9116e-01 -1.0500e+03 -6.5714e+02 -9.3869e-01 -1.0500e+03 -6.2857e+02 -1.1896e+00 -1.0500e+03 -6.0000e+02 -1.4431e+00 -1.0500e+03 -5.7143e+02 -1.6986e+00 -1.0500e+03 -5.4286e+02 -1.9552e+00 -1.0500e+03 -5.1429e+02 -2.2118e+00 -1.0500e+03 -4.8571e+02 -2.4675e+00 -1.0500e+03 -4.5714e+02 -2.7209e+00 -1.0500e+03 -4.2857e+02 -2.9708e+00 -1.0500e+03 -4.0000e+02 -3.2157e+00 -1.0500e+03 -3.7143e+02 -3.4541e+00 -1.0500e+03 -3.4286e+02 -3.6844e+00 -1.0500e+03 -3.1429e+02 -3.9049e+00 -1.0500e+03 -2.8571e+02 -4.1139e+00 -1.0500e+03 -2.5714e+02 -4.3097e+00 -1.0500e+03 -2.2857e+02 -4.4905e+00 -1.0500e+03 -2.0000e+02 -4.6547e+00 -1.0500e+03 -1.7143e+02 -4.8006e+00 -1.0500e+03 -1.4286e+02 -4.9268e+00 -1.0500e+03 -1.1429e+02 -5.0320e+00 -1.0500e+03 -8.5714e+01 -5.1151e+00 -1.0500e+03 -5.7143e+01 -5.1751e+00 -1.0500e+03 -2.8571e+01 -5.2114e+00 -1.0500e+03 0.0000e+00 -5.2235e+00 -1.0500e+03 2.8571e+01 -5.2114e+00 -1.0500e+03 5.7143e+01 -5.1751e+00 -1.0500e+03 8.5714e+01 -5.1151e+00 -1.0500e+03 1.1429e+02 -5.0320e+00 -1.0500e+03 1.4286e+02 -4.9268e+00 -1.0500e+03 1.7143e+02 -4.8006e+00 -1.0500e+03 2.0000e+02 -4.6547e+00 -1.0500e+03 2.2857e+02 -4.4905e+00 -1.0500e+03 2.5714e+02 -4.3097e+00 -1.0500e+03 2.8571e+02 -4.1139e+00 -1.0500e+03 3.1429e+02 -3.9049e+00 -1.0500e+03 3.4286e+02 -3.6844e+00 -1.0500e+03 3.7143e+02 -3.4541e+00 -1.0500e+03 4.0000e+02 -3.2157e+00 -1.0500e+03 4.2857e+02 -2.9708e+00 -1.0500e+03 4.5714e+02 -2.7209e+00 -1.0500e+03 4.8571e+02 -2.4675e+00 -1.0500e+03 5.1429e+02 -2.2118e+00 -1.0500e+03 5.4286e+02 -1.9552e+00 -1.0500e+03 5.7143e+02 -1.6986e+00 -1.0500e+03 6.0000e+02 -1.4431e+00 -1.0500e+03 6.2857e+02 -1.1896e+00 -1.0500e+03 6.5714e+02 -9.3869e-01 -1.0500e+03 6.8571e+02 -6.9116e-01 -1.0500e+03 7.1429e+02 -4.4752e-01 -1.0500e+03 7.4286e+02 -2.0822e-01 -1.0500e+03 7.7143e+02 2.6339e-02 -1.0500e+03 8.0000e+02 2.5587e-01 -1.0500e+03 8.2857e+02 4.8013e-01 -1.0500e+03 8.5714e+02 6.9895e-01 -1.0500e+03 8.8571e+02 9.1220e-01 -1.0500e+03 9.1429e+02 1.1198e+00 -1.0500e+03 9.4286e+02 1.3217e+00 -1.0500e+03 9.7143e+02 1.5180e+00 -1.0500e+03 1.0000e+03 1.7086e+00 -1.0500e+03 1.0286e+03 1.8936e+00 -1.0500e+03 1.0571e+03 2.0731e+00 -1.0500e+03 1.0857e+03 2.2472e+00 -1.0500e+03 1.1143e+03 2.4160e+00 -1.0500e+03 1.1429e+03 2.5795e+00 -1.0500e+03 1.1714e+03 2.7380e+00 -1.0500e+03 1.2000e+03 2.8915e+00 -1.0500e+03 1.2286e+03 3.0402e+00 -1.0500e+03 1.2571e+03 3.1843e+00 -1.0500e+03 1.2857e+03 3.3238e+00 -1.0500e+03 1.3143e+03 3.4589e+00 -1.0500e+03 1.3429e+03 3.5898e+00 -1.0500e+03 1.3714e+03 3.7166e+00 -1.0500e+03 1.4000e+03 3.8394e+00 -1.0500e+03 1.4286e+03 3.9584e+00 -1.0500e+03 1.4571e+03 4.0737e+00 -1.0500e+03 1.4857e+03 4.1855e+00 -1.0500e+03 1.5143e+03 4.2938e+00 -1.0500e+03 1.5429e+03 4.3988e+00 -1.0500e+03 1.5714e+03 4.5006e+00 -1.0500e+03 1.6000e+03 4.5993e+00 -1.0500e+03 1.6286e+03 4.6951e+00 -1.0500e+03 1.6571e+03 4.7880e+00 -1.0500e+03 1.6857e+03 4.8782e+00 -1.0500e+03 1.7143e+03 4.9657e+00 -1.0500e+03 1.7429e+03 5.0507e+00 -1.0500e+03 1.7714e+03 5.1332e+00 -1.0500e+03 1.8000e+03 5.2134e+00 -1.0500e+03 1.8286e+03 5.2913e+00 -1.0500e+03 1.8571e+03 5.3669e+00 -1.0500e+03 1.8857e+03 5.4405e+00 -1.0500e+03 1.9143e+03 5.5120e+00 -1.0500e+03 1.9429e+03 5.5816e+00 -1.0500e+03 1.9714e+03 5.6492e+00 -1.0500e+03 2.0000e+03 5.7151e+00 -1.0800e+03 -2.0000e+03 5.7510e+00 -1.0800e+03 -1.9714e+03 5.6866e+00 -1.0800e+03 -1.9429e+03 5.6206e+00 -1.0800e+03 -1.9143e+03 5.5527e+00 -1.0800e+03 -1.8857e+03 5.4830e+00 -1.0800e+03 -1.8571e+03 5.4113e+00 -1.0800e+03 -1.8286e+03 5.3376e+00 -1.0800e+03 -1.8000e+03 5.2618e+00 -1.0800e+03 -1.7714e+03 5.1838e+00 -1.0800e+03 -1.7429e+03 5.1037e+00 -1.0800e+03 -1.7143e+03 5.0212e+00 -1.0800e+03 -1.6857e+03 4.9362e+00 -1.0800e+03 -1.6571e+03 4.8488e+00 -1.0800e+03 -1.6286e+03 4.7588e+00 -1.0800e+03 -1.6000e+03 4.6662e+00 -1.0800e+03 -1.5714e+03 4.5707e+00 -1.0800e+03 -1.5429e+03 4.4724e+00 -1.0800e+03 -1.5143e+03 4.3711e+00 -1.0800e+03 -1.4857e+03 4.2667e+00 -1.0800e+03 -1.4571e+03 4.1592e+00 -1.0800e+03 -1.4286e+03 4.0483e+00 -1.0800e+03 -1.4000e+03 3.9340e+00 -1.0800e+03 -1.3714e+03 3.8162e+00 -1.0800e+03 -1.3429e+03 3.6948e+00 -1.0800e+03 -1.3143e+03 3.5695e+00 -1.0800e+03 -1.2857e+03 3.4405e+00 -1.0800e+03 -1.2571e+03 3.3074e+00 -1.0800e+03 -1.2286e+03 3.1702e+00 -1.0800e+03 -1.2000e+03 3.0287e+00 -1.0800e+03 -1.1714e+03 2.8830e+00 -1.0800e+03 -1.1429e+03 2.7328e+00 -1.0800e+03 -1.1143e+03 2.5780e+00 -1.0800e+03 -1.0857e+03 2.4187e+00 -1.0800e+03 -1.0571e+03 2.2546e+00 -1.0800e+03 -1.0286e+03 2.0857e+00 -1.0800e+03 -1.0000e+03 1.9120e+00 -1.0800e+03 -9.7143e+02 1.7334e+00 -1.0800e+03 -9.4286e+02 1.5499e+00 -1.0800e+03 -9.1429e+02 1.3616e+00 -1.0800e+03 -8.8571e+02 1.1684e+00 -1.0800e+03 -8.5714e+02 9.7037e-01 -1.0800e+03 -8.2857e+02 7.6771e-01 -1.0800e+03 -8.0000e+02 5.6054e-01 -1.0800e+03 -7.7143e+02 3.4905e-01 -1.0800e+03 -7.4286e+02 1.3351e-01 -1.0800e+03 -7.1429e+02 -8.5786e-02 -1.0800e+03 -6.8571e+02 -3.0845e-01 -1.0800e+03 -6.5714e+02 -5.3402e-01 -1.0800e+03 -6.2857e+02 -7.6197e-01 -1.0800e+03 -6.0000e+02 -9.9170e-01 -1.0800e+03 -5.7143e+02 -1.2225e+00 -1.0800e+03 -5.4286e+02 -1.4536e+00 -1.0800e+03 -5.1429e+02 -1.6840e+00 -1.0800e+03 -4.8571e+02 -1.9129e+00 -1.0800e+03 -4.5714e+02 -2.1392e+00 -1.0800e+03 -4.2857e+02 -2.3616e+00 -1.0800e+03 -4.0000e+02 -2.5790e+00 -1.0800e+03 -3.7143e+02 -2.7900e+00 -1.0800e+03 -3.4286e+02 -2.9933e+00 -1.0800e+03 -3.1429e+02 -3.1875e+00 -1.0800e+03 -2.8571e+02 -3.3710e+00 -1.0800e+03 -2.5714e+02 -3.5426e+00 -1.0800e+03 -2.2857e+02 -3.7006e+00 -1.0800e+03 -2.0000e+02 -3.8439e+00 -1.0800e+03 -1.7143e+02 -3.9710e+00 -1.0800e+03 -1.4286e+02 -4.0808e+00 -1.0800e+03 -1.1429e+02 -4.1722e+00 -1.0800e+03 -8.5714e+01 -4.2443e+00 -1.0800e+03 -5.7143e+01 -4.2963e+00 -1.0800e+03 -2.8571e+01 -4.3278e+00 -1.0800e+03 0.0000e+00 -4.3383e+00 -1.0800e+03 2.8571e+01 -4.3278e+00 -1.0800e+03 5.7143e+01 -4.2963e+00 -1.0800e+03 8.5714e+01 -4.2443e+00 -1.0800e+03 1.1429e+02 -4.1722e+00 -1.0800e+03 1.4286e+02 -4.0808e+00 -1.0800e+03 1.7143e+02 -3.9710e+00 -1.0800e+03 2.0000e+02 -3.8439e+00 -1.0800e+03 2.2857e+02 -3.7006e+00 -1.0800e+03 2.5714e+02 -3.5426e+00 -1.0800e+03 2.8571e+02 -3.3710e+00 -1.0800e+03 3.1429e+02 -3.1875e+00 -1.0800e+03 3.4286e+02 -2.9933e+00 -1.0800e+03 3.7143e+02 -2.7900e+00 -1.0800e+03 4.0000e+02 -2.5790e+00 -1.0800e+03 4.2857e+02 -2.3616e+00 -1.0800e+03 4.5714e+02 -2.1392e+00 -1.0800e+03 4.8571e+02 -1.9129e+00 -1.0800e+03 5.1429e+02 -1.6840e+00 -1.0800e+03 5.4286e+02 -1.4536e+00 -1.0800e+03 5.7143e+02 -1.2225e+00 -1.0800e+03 6.0000e+02 -9.9170e-01 -1.0800e+03 6.2857e+02 -7.6197e-01 -1.0800e+03 6.5714e+02 -5.3402e-01 -1.0800e+03 6.8571e+02 -3.0845e-01 -1.0800e+03 7.1429e+02 -8.5786e-02 -1.0800e+03 7.4286e+02 1.3351e-01 -1.0800e+03 7.7143e+02 3.4905e-01 -1.0800e+03 8.0000e+02 5.6054e-01 -1.0800e+03 8.2857e+02 7.6771e-01 -1.0800e+03 8.5714e+02 9.7037e-01 -1.0800e+03 8.8571e+02 1.1684e+00 -1.0800e+03 9.1429e+02 1.3616e+00 -1.0800e+03 9.4286e+02 1.5499e+00 -1.0800e+03 9.7143e+02 1.7334e+00 -1.0800e+03 1.0000e+03 1.9120e+00 -1.0800e+03 1.0286e+03 2.0857e+00 -1.0800e+03 1.0571e+03 2.2546e+00 -1.0800e+03 1.0857e+03 2.4187e+00 -1.0800e+03 1.1143e+03 2.5780e+00 -1.0800e+03 1.1429e+03 2.7328e+00 -1.0800e+03 1.1714e+03 2.8830e+00 -1.0800e+03 1.2000e+03 3.0287e+00 -1.0800e+03 1.2286e+03 3.1702e+00 -1.0800e+03 1.2571e+03 3.3074e+00 -1.0800e+03 1.2857e+03 3.4405e+00 -1.0800e+03 1.3143e+03 3.5695e+00 -1.0800e+03 1.3429e+03 3.6948e+00 -1.0800e+03 1.3714e+03 3.8162e+00 -1.0800e+03 1.4000e+03 3.9340e+00 -1.0800e+03 1.4286e+03 4.0483e+00 -1.0800e+03 1.4571e+03 4.1592e+00 -1.0800e+03 1.4857e+03 4.2667e+00 -1.0800e+03 1.5143e+03 4.3711e+00 -1.0800e+03 1.5429e+03 4.4724e+00 -1.0800e+03 1.5714e+03 4.5707e+00 -1.0800e+03 1.6000e+03 4.6662e+00 -1.0800e+03 1.6286e+03 4.7588e+00 -1.0800e+03 1.6571e+03 4.8488e+00 -1.0800e+03 1.6857e+03 4.9362e+00 -1.0800e+03 1.7143e+03 5.0212e+00 -1.0800e+03 1.7429e+03 5.1037e+00 -1.0800e+03 1.7714e+03 5.1838e+00 -1.0800e+03 1.8000e+03 5.2618e+00 -1.0800e+03 1.8286e+03 5.3376e+00 -1.0800e+03 1.8571e+03 5.4113e+00 -1.0800e+03 1.8857e+03 5.4830e+00 -1.0800e+03 1.9143e+03 5.5527e+00 -1.0800e+03 1.9429e+03 5.6206e+00 -1.0800e+03 1.9714e+03 5.6866e+00 -1.0800e+03 2.0000e+03 5.7510e+00 -1.1100e+03 -2.0000e+03 5.7870e+00 -1.1100e+03 -1.9714e+03 5.7242e+00 -1.1100e+03 -1.9429e+03 5.6597e+00 -1.1100e+03 -1.9143e+03 5.5935e+00 -1.1100e+03 -1.8857e+03 5.5255e+00 -1.1100e+03 -1.8571e+03 5.4557e+00 -1.1100e+03 -1.8286e+03 5.3839e+00 -1.1100e+03 -1.8000e+03 5.3102e+00 -1.1100e+03 -1.7714e+03 5.2344e+00 -1.1100e+03 -1.7429e+03 5.1565e+00 -1.1100e+03 -1.7143e+03 5.0764e+00 -1.1100e+03 -1.6857e+03 4.9941e+00 -1.1100e+03 -1.6571e+03 4.9094e+00 -1.1100e+03 -1.6286e+03 4.8223e+00 -1.1100e+03 -1.6000e+03 4.7326e+00 -1.1100e+03 -1.5714e+03 4.6404e+00 -1.1100e+03 -1.5429e+03 4.5455e+00 -1.1100e+03 -1.5143e+03 4.4478e+00 -1.1100e+03 -1.4857e+03 4.3472e+00 -1.1100e+03 -1.4571e+03 4.2437e+00 -1.1100e+03 -1.4286e+03 4.1371e+00 -1.1100e+03 -1.4000e+03 4.0274e+00 -1.1100e+03 -1.3714e+03 3.9144e+00 -1.1100e+03 -1.3429e+03 3.7981e+00 -1.1100e+03 -1.3143e+03 3.6783e+00 -1.1100e+03 -1.2857e+03 3.5550e+00 -1.1100e+03 -1.2571e+03 3.4280e+00 -1.1100e+03 -1.2286e+03 3.2973e+00 -1.1100e+03 -1.2000e+03 3.1628e+00 -1.1100e+03 -1.1714e+03 3.0244e+00 -1.1100e+03 -1.1429e+03 2.8821e+00 -1.1100e+03 -1.1143e+03 2.7356e+00 -1.1100e+03 -1.0857e+03 2.5851e+00 -1.1100e+03 -1.0571e+03 2.4303e+00 -1.1100e+03 -1.0286e+03 2.2714e+00 -1.1100e+03 -1.0000e+03 2.1082e+00 -1.1100e+03 -9.7143e+02 1.9408e+00 -1.1100e+03 -9.4286e+02 1.7691e+00 -1.1100e+03 -9.1429e+02 1.5933e+00 -1.1100e+03 -8.8571e+02 1.4133e+00 -1.1100e+03 -8.5714e+02 1.2293e+00 -1.1100e+03 -8.2857e+02 1.0414e+00 -1.1100e+03 -8.0000e+02 8.4975e-01 -1.1100e+03 -7.7143e+02 6.5461e-01 -1.1100e+03 -7.4286e+02 4.5622e-01 -1.1100e+03 -7.1429e+02 2.5490e-01 -1.1100e+03 -6.8571e+02 5.1018e-02 -1.1100e+03 -6.5714e+02 -1.5498e-01 -1.1100e+03 -6.2857e+02 -3.6260e-01 -1.1100e+03 -6.0000e+02 -5.7126e-01 -1.1100e+03 -5.7143e+02 -7.8032e-01 -1.1100e+03 -5.4286e+02 -9.8905e-01 -1.1100e+03 -5.1429e+02 -1.1967e+00 -1.1100e+03 -4.8571e+02 -1.4023e+00 -1.1100e+03 -4.5714e+02 -1.6050e+00 -1.1100e+03 -4.2857e+02 -1.8038e+00 -1.1100e+03 -4.0000e+02 -1.9975e+00 -1.1100e+03 -3.7143e+02 -2.1851e+00 -1.1100e+03 -3.4286e+02 -2.3654e+00 -1.1100e+03 -3.1429e+02 -2.5372e+00 -1.1100e+03 -2.8571e+02 -2.6992e+00 -1.1100e+03 -2.5714e+02 -2.8503e+00 -1.1100e+03 -2.2857e+02 -2.9893e+00 -1.1100e+03 -2.0000e+02 -3.1150e+00 -1.1100e+03 -1.7143e+02 -3.2264e+00 -1.1100e+03 -1.4286e+02 -3.3225e+00 -1.1100e+03 -1.1429e+02 -3.4024e+00 -1.1100e+03 -8.5714e+01 -3.4653e+00 -1.1100e+03 -5.7143e+01 -3.5107e+00 -1.1100e+03 -2.8571e+01 -3.5381e+00 -1.1100e+03 0.0000e+00 -3.5473e+00 -1.1100e+03 2.8571e+01 -3.5381e+00 -1.1100e+03 5.7143e+01 -3.5107e+00 -1.1100e+03 8.5714e+01 -3.4653e+00 -1.1100e+03 1.1429e+02 -3.4024e+00 -1.1100e+03 1.4286e+02 -3.3225e+00 -1.1100e+03 1.7143e+02 -3.2264e+00 -1.1100e+03 2.0000e+02 -3.1150e+00 -1.1100e+03 2.2857e+02 -2.9893e+00 -1.1100e+03 2.5714e+02 -2.8503e+00 -1.1100e+03 2.8571e+02 -2.6992e+00 -1.1100e+03 3.1429e+02 -2.5372e+00 -1.1100e+03 3.4286e+02 -2.3654e+00 -1.1100e+03 3.7143e+02 -2.1851e+00 -1.1100e+03 4.0000e+02 -1.9975e+00 -1.1100e+03 4.2857e+02 -1.8038e+00 -1.1100e+03 4.5714e+02 -1.6050e+00 -1.1100e+03 4.8571e+02 -1.4023e+00 -1.1100e+03 5.1429e+02 -1.1967e+00 -1.1100e+03 5.4286e+02 -9.8905e-01 -1.1100e+03 5.7143e+02 -7.8032e-01 -1.1100e+03 6.0000e+02 -5.7126e-01 -1.1100e+03 6.2857e+02 -3.6260e-01 -1.1100e+03 6.5714e+02 -1.5498e-01 -1.1100e+03 6.8571e+02 5.1018e-02 -1.1100e+03 7.1429e+02 2.5490e-01 -1.1100e+03 7.4286e+02 4.5622e-01 -1.1100e+03 7.7143e+02 6.5461e-01 -1.1100e+03 8.0000e+02 8.4975e-01 -1.1100e+03 8.2857e+02 1.0414e+00 -1.1100e+03 8.5714e+02 1.2293e+00 -1.1100e+03 8.8571e+02 1.4133e+00 -1.1100e+03 9.1429e+02 1.5933e+00 -1.1100e+03 9.4286e+02 1.7691e+00 -1.1100e+03 9.7143e+02 1.9408e+00 -1.1100e+03 1.0000e+03 2.1082e+00 -1.1100e+03 1.0286e+03 2.2714e+00 -1.1100e+03 1.0571e+03 2.4303e+00 -1.1100e+03 1.0857e+03 2.5851e+00 -1.1100e+03 1.1143e+03 2.7356e+00 -1.1100e+03 1.1429e+03 2.8821e+00 -1.1100e+03 1.1714e+03 3.0244e+00 -1.1100e+03 1.2000e+03 3.1628e+00 -1.1100e+03 1.2286e+03 3.2973e+00 -1.1100e+03 1.2571e+03 3.4280e+00 -1.1100e+03 1.2857e+03 3.5550e+00 -1.1100e+03 1.3143e+03 3.6783e+00 -1.1100e+03 1.3429e+03 3.7981e+00 -1.1100e+03 1.3714e+03 3.9144e+00 -1.1100e+03 1.4000e+03 4.0274e+00 -1.1100e+03 1.4286e+03 4.1371e+00 -1.1100e+03 1.4571e+03 4.2437e+00 -1.1100e+03 1.4857e+03 4.3472e+00 -1.1100e+03 1.5143e+03 4.4478e+00 -1.1100e+03 1.5429e+03 4.5455e+00 -1.1100e+03 1.5714e+03 4.6404e+00 -1.1100e+03 1.6000e+03 4.7326e+00 -1.1100e+03 1.6286e+03 4.8223e+00 -1.1100e+03 1.6571e+03 4.9094e+00 -1.1100e+03 1.6857e+03 4.9941e+00 -1.1100e+03 1.7143e+03 5.0764e+00 -1.1100e+03 1.7429e+03 5.1565e+00 -1.1100e+03 1.7714e+03 5.2344e+00 -1.1100e+03 1.8000e+03 5.3102e+00 -1.1100e+03 1.8286e+03 5.3839e+00 -1.1100e+03 1.8571e+03 5.4557e+00 -1.1100e+03 1.8857e+03 5.5255e+00 -1.1100e+03 1.9143e+03 5.5935e+00 -1.1100e+03 1.9429e+03 5.6597e+00 -1.1100e+03 1.9714e+03 5.7242e+00 -1.1100e+03 2.0000e+03 5.7870e+00 -1.1400e+03 -2.0000e+03 5.8232e+00 -1.1400e+03 -1.9714e+03 5.7619e+00 -1.1400e+03 -1.9429e+03 5.6990e+00 -1.1400e+03 -1.9143e+03 5.6344e+00 -1.1400e+03 -1.8857e+03 5.5681e+00 -1.1400e+03 -1.8571e+03 5.5001e+00 -1.1400e+03 -1.8286e+03 5.4303e+00 -1.1400e+03 -1.8000e+03 5.3586e+00 -1.1400e+03 -1.7714e+03 5.2849e+00 -1.1400e+03 -1.7429e+03 5.2093e+00 -1.1400e+03 -1.7143e+03 5.1316e+00 -1.1400e+03 -1.6857e+03 5.0517e+00 -1.1400e+03 -1.6571e+03 4.9696e+00 -1.1400e+03 -1.6286e+03 4.8853e+00 -1.1400e+03 -1.6000e+03 4.7986e+00 -1.1400e+03 -1.5714e+03 4.7095e+00 -1.1400e+03 -1.5429e+03 4.6179e+00 -1.1400e+03 -1.5143e+03 4.5237e+00 -1.1400e+03 -1.4857e+03 4.4268e+00 -1.1400e+03 -1.4571e+03 4.3272e+00 -1.1400e+03 -1.4286e+03 4.2247e+00 -1.1400e+03 -1.4000e+03 4.1194e+00 -1.1400e+03 -1.3714e+03 4.0110e+00 -1.1400e+03 -1.3429e+03 3.8996e+00 -1.1400e+03 -1.3143e+03 3.7851e+00 -1.1400e+03 -1.2857e+03 3.6673e+00 -1.1400e+03 -1.2571e+03 3.5462e+00 -1.1400e+03 -1.2286e+03 3.4217e+00 -1.1400e+03 -1.2000e+03 3.2938e+00 -1.1400e+03 -1.1714e+03 3.1623e+00 -1.1400e+03 -1.1429e+03 3.0273e+00 -1.1400e+03 -1.1143e+03 2.8887e+00 -1.1400e+03 -1.0857e+03 2.7464e+00 -1.1400e+03 -1.0571e+03 2.6004e+00 -1.1400e+03 -1.0286e+03 2.4508e+00 -1.1400e+03 -1.0000e+03 2.2974e+00 -1.1400e+03 -9.7143e+02 2.1403e+00 -1.1400e+03 -9.4286e+02 1.9796e+00 -1.1400e+03 -9.1429e+02 1.8153e+00 -1.1400e+03 -8.8571e+02 1.6475e+00 -1.1400e+03 -8.5714e+02 1.4764e+00 -1.1400e+03 -8.2857e+02 1.3019e+00 -1.1400e+03 -8.0000e+02 1.1244e+00 -1.1400e+03 -7.7143e+02 9.4412e-01 -1.1400e+03 -7.4286e+02 7.6124e-01 -1.1400e+03 -7.1429e+02 5.7610e-01 -1.1400e+03 -6.8571e+02 3.8906e-01 -1.1400e+03 -6.5714e+02 2.0055e-01 -1.1400e+03 -6.2857e+02 1.1040e-02 -1.1400e+03 -6.0000e+02 -1.7895e-01 -1.1400e+03 -5.7143e+02 -3.6881e-01 -1.1400e+03 -5.4286e+02 -5.5791e-01 -1.1400e+03 -5.1429e+02 -7.4551e-01 -1.1400e+03 -4.8571e+02 -9.3085e-01 -1.1400e+03 -4.5714e+02 -1.1131e+00 -1.1400e+03 -4.2857e+02 -1.2914e+00 -1.1400e+03 -4.0000e+02 -1.4647e+00 -1.1400e+03 -3.7143e+02 -1.6322e+00 -1.1400e+03 -3.4286e+02 -1.7927e+00 -1.1400e+03 -3.1429e+02 -1.9454e+00 -1.1400e+03 -2.8571e+02 -2.0891e+00 -1.1400e+03 -2.5714e+02 -2.2229e+00 -1.1400e+03 -2.2857e+02 -2.3457e+00 -1.1400e+03 -2.0000e+02 -2.4566e+00 -1.1400e+03 -1.7143e+02 -2.5548e+00 -1.1400e+03 -1.4286e+02 -2.6393e+00 -1.1400e+03 -1.1429e+02 -2.7095e+00 -1.1400e+03 -8.5714e+01 -2.7648e+00 -1.1400e+03 -5.7143e+01 -2.8046e+00 -1.1400e+03 -2.8571e+01 -2.8287e+00 -1.1400e+03 0.0000e+00 -2.8367e+00 -1.1400e+03 2.8571e+01 -2.8287e+00 -1.1400e+03 5.7143e+01 -2.8046e+00 -1.1400e+03 8.5714e+01 -2.7648e+00 -1.1400e+03 1.1429e+02 -2.7095e+00 -1.1400e+03 1.4286e+02 -2.6393e+00 -1.1400e+03 1.7143e+02 -2.5548e+00 -1.1400e+03 2.0000e+02 -2.4566e+00 -1.1400e+03 2.2857e+02 -2.3457e+00 -1.1400e+03 2.5714e+02 -2.2229e+00 -1.1400e+03 2.8571e+02 -2.0891e+00 -1.1400e+03 3.1429e+02 -1.9454e+00 -1.1400e+03 3.4286e+02 -1.7927e+00 -1.1400e+03 3.7143e+02 -1.6322e+00 -1.1400e+03 4.0000e+02 -1.4647e+00 -1.1400e+03 4.2857e+02 -1.2914e+00 -1.1400e+03 4.5714e+02 -1.1131e+00 -1.1400e+03 4.8571e+02 -9.3085e-01 -1.1400e+03 5.1429e+02 -7.4551e-01 -1.1400e+03 5.4286e+02 -5.5791e-01 -1.1400e+03 5.7143e+02 -3.6881e-01 -1.1400e+03 6.0000e+02 -1.7895e-01 -1.1400e+03 6.2857e+02 1.1040e-02 -1.1400e+03 6.5714e+02 2.0055e-01 -1.1400e+03 6.8571e+02 3.8906e-01 -1.1400e+03 7.1429e+02 5.7610e-01 -1.1400e+03 7.4286e+02 7.6124e-01 -1.1400e+03 7.7143e+02 9.4412e-01 -1.1400e+03 8.0000e+02 1.1244e+00 -1.1400e+03 8.2857e+02 1.3019e+00 -1.1400e+03 8.5714e+02 1.4764e+00 -1.1400e+03 8.8571e+02 1.6475e+00 -1.1400e+03 9.1429e+02 1.8153e+00 -1.1400e+03 9.4286e+02 1.9796e+00 -1.1400e+03 9.7143e+02 2.1403e+00 -1.1400e+03 1.0000e+03 2.2974e+00 -1.1400e+03 1.0286e+03 2.4508e+00 -1.1400e+03 1.0571e+03 2.6004e+00 -1.1400e+03 1.0857e+03 2.7464e+00 -1.1400e+03 1.1143e+03 2.8887e+00 -1.1400e+03 1.1429e+03 3.0273e+00 -1.1400e+03 1.1714e+03 3.1623e+00 -1.1400e+03 1.2000e+03 3.2938e+00 -1.1400e+03 1.2286e+03 3.4217e+00 -1.1400e+03 1.2571e+03 3.5462e+00 -1.1400e+03 1.2857e+03 3.6673e+00 -1.1400e+03 1.3143e+03 3.7851e+00 -1.1400e+03 1.3429e+03 3.8996e+00 -1.1400e+03 1.3714e+03 4.0110e+00 -1.1400e+03 1.4000e+03 4.1194e+00 -1.1400e+03 1.4286e+03 4.2247e+00 -1.1400e+03 1.4571e+03 4.3272e+00 -1.1400e+03 1.4857e+03 4.4268e+00 -1.1400e+03 1.5143e+03 4.5237e+00 -1.1400e+03 1.5429e+03 4.6179e+00 -1.1400e+03 1.5714e+03 4.7095e+00 -1.1400e+03 1.6000e+03 4.7986e+00 -1.1400e+03 1.6286e+03 4.8853e+00 -1.1400e+03 1.6571e+03 4.9696e+00 -1.1400e+03 1.6857e+03 5.0517e+00 -1.1400e+03 1.7143e+03 5.1316e+00 -1.1400e+03 1.7429e+03 5.2093e+00 -1.1400e+03 1.7714e+03 5.2849e+00 -1.1400e+03 1.8000e+03 5.3586e+00 -1.1400e+03 1.8286e+03 5.4303e+00 -1.1400e+03 1.8571e+03 5.5001e+00 -1.1400e+03 1.8857e+03 5.5681e+00 -1.1400e+03 1.9143e+03 5.6344e+00 -1.1400e+03 1.9429e+03 5.6990e+00 -1.1400e+03 1.9714e+03 5.7619e+00 -1.1400e+03 2.0000e+03 5.8232e+00 -1.1700e+03 -2.0000e+03 5.8595e+00 -1.1700e+03 -1.9714e+03 5.7996e+00 -1.1700e+03 -1.9429e+03 5.7382e+00 -1.1700e+03 -1.9143e+03 5.6753e+00 -1.1700e+03 -1.8857e+03 5.6107e+00 -1.1700e+03 -1.8571e+03 5.5445e+00 -1.1700e+03 -1.8286e+03 5.4766e+00 -1.1700e+03 -1.8000e+03 5.4068e+00 -1.1700e+03 -1.7714e+03 5.3353e+00 -1.1700e+03 -1.7429e+03 5.2618e+00 -1.1700e+03 -1.7143e+03 5.1864e+00 -1.1700e+03 -1.6857e+03 5.1090e+00 -1.1700e+03 -1.6571e+03 5.0295e+00 -1.1700e+03 -1.6286e+03 4.9479e+00 -1.1700e+03 -1.6000e+03 4.8641e+00 -1.1700e+03 -1.5714e+03 4.7780e+00 -1.1700e+03 -1.5429e+03 4.6896e+00 -1.1700e+03 -1.5143e+03 4.5988e+00 -1.1700e+03 -1.4857e+03 4.5055e+00 -1.1700e+03 -1.4571e+03 4.4096e+00 -1.1700e+03 -1.4286e+03 4.3112e+00 -1.1700e+03 -1.4000e+03 4.2100e+00 -1.1700e+03 -1.3714e+03 4.1061e+00 -1.1700e+03 -1.3429e+03 3.9994e+00 -1.1700e+03 -1.3143e+03 3.8899e+00 -1.1700e+03 -1.2857e+03 3.7773e+00 -1.1700e+03 -1.2571e+03 3.6618e+00 -1.1700e+03 -1.2286e+03 3.5432e+00 -1.1700e+03 -1.2000e+03 3.4215e+00 -1.1700e+03 -1.1714e+03 3.2967e+00 -1.1700e+03 -1.1429e+03 3.1686e+00 -1.1700e+03 -1.1143e+03 3.0374e+00 -1.1700e+03 -1.0857e+03 2.9029e+00 -1.1700e+03 -1.0571e+03 2.7651e+00 -1.1700e+03 -1.0286e+03 2.6241e+00 -1.1700e+03 -1.0000e+03 2.4799e+00 -1.1700e+03 -9.7143e+02 2.3324e+00 -1.1700e+03 -9.4286e+02 2.1819e+00 -1.1700e+03 -9.1429e+02 2.0282e+00 -1.1700e+03 -8.8571e+02 1.8716e+00 -1.1700e+03 -8.5714e+02 1.7122e+00 -1.1700e+03 -8.2857e+02 1.5501e+00 -1.1700e+03 -8.0000e+02 1.3855e+00 -1.1700e+03 -7.7143e+02 1.2187e+00 -1.1700e+03 -7.4286e+02 1.0498e+00 -1.1700e+03 -7.1429e+02 8.7926e-01 -1.1700e+03 -6.8571e+02 7.0736e-01 -1.1700e+03 -6.5714e+02 5.3451e-01 -1.1700e+03 -6.2857e+02 3.6115e-01 -1.1700e+03 -6.0000e+02 1.8776e-01 -1.1700e+03 -5.7143e+02 1.4889e-02 -1.1700e+03 -5.4286e+02 -1.5687e-01 -1.1700e+03 -5.1429e+02 -3.2688e-01 -1.1700e+03 -4.8571e+02 -4.9444e-01 -1.1700e+03 -4.5714e+02 -6.5883e-01 -1.1700e+03 -4.2857e+02 -8.1927e-01 -1.1700e+03 -4.0000e+02 -9.7493e-01 -1.1700e+03 -3.7143e+02 -1.1250e+00 -1.1700e+03 -3.4286e+02 -1.2686e+00 -1.1700e+03 -3.1429e+02 -1.4048e+00 -1.1700e+03 -2.8571e+02 -1.5329e+00 -1.1700e+03 -2.5714e+02 -1.6518e+00 -1.1700e+03 -2.2857e+02 -1.7609e+00 -1.1700e+03 -2.0000e+02 -1.8592e+00 -1.1700e+03 -1.7143e+02 -1.9461e+00 -1.1700e+03 -1.4286e+02 -2.0209e+00 -1.1700e+03 -1.1429e+02 -2.0829e+00 -1.1700e+03 -8.5714e+01 -2.1317e+00 -1.1700e+03 -5.7143e+01 -2.1669e+00 -1.1700e+03 -2.8571e+01 -2.1881e+00 -1.1700e+03 0.0000e+00 -2.1952e+00 -1.1700e+03 2.8571e+01 -2.1881e+00 -1.1700e+03 5.7143e+01 -2.1669e+00 -1.1700e+03 8.5714e+01 -2.1317e+00 -1.1700e+03 1.1429e+02 -2.0829e+00 -1.1700e+03 1.4286e+02 -2.0209e+00 -1.1700e+03 1.7143e+02 -1.9461e+00 -1.1700e+03 2.0000e+02 -1.8592e+00 -1.1700e+03 2.2857e+02 -1.7609e+00 -1.1700e+03 2.5714e+02 -1.6518e+00 -1.1700e+03 2.8571e+02 -1.5329e+00 -1.1700e+03 3.1429e+02 -1.4048e+00 -1.1700e+03 3.4286e+02 -1.2686e+00 -1.1700e+03 3.7143e+02 -1.1250e+00 -1.1700e+03 4.0000e+02 -9.7493e-01 -1.1700e+03 4.2857e+02 -8.1927e-01 -1.1700e+03 4.5714e+02 -6.5883e-01 -1.1700e+03 4.8571e+02 -4.9444e-01 -1.1700e+03 5.1429e+02 -3.2688e-01 -1.1700e+03 5.4286e+02 -1.5687e-01 -1.1700e+03 5.7143e+02 1.4889e-02 -1.1700e+03 6.0000e+02 1.8776e-01 -1.1700e+03 6.2857e+02 3.6115e-01 -1.1700e+03 6.5714e+02 5.3451e-01 -1.1700e+03 6.8571e+02 7.0736e-01 -1.1700e+03 7.1429e+02 8.7926e-01 -1.1700e+03 7.4286e+02 1.0498e+00 -1.1700e+03 7.7143e+02 1.2187e+00 -1.1700e+03 8.0000e+02 1.3855e+00 -1.1700e+03 8.2857e+02 1.5501e+00 -1.1700e+03 8.5714e+02 1.7122e+00 -1.1700e+03 8.8571e+02 1.8716e+00 -1.1700e+03 9.1429e+02 2.0282e+00 -1.1700e+03 9.4286e+02 2.1819e+00 -1.1700e+03 9.7143e+02 2.3324e+00 -1.1700e+03 1.0000e+03 2.4799e+00 -1.1700e+03 1.0286e+03 2.6241e+00 -1.1700e+03 1.0571e+03 2.7651e+00 -1.1700e+03 1.0857e+03 2.9029e+00 -1.1700e+03 1.1143e+03 3.0374e+00 -1.1700e+03 1.1429e+03 3.1686e+00 -1.1700e+03 1.1714e+03 3.2967e+00 -1.1700e+03 1.2000e+03 3.4215e+00 -1.1700e+03 1.2286e+03 3.5432e+00 -1.1700e+03 1.2571e+03 3.6618e+00 -1.1700e+03 1.2857e+03 3.7773e+00 -1.1700e+03 1.3143e+03 3.8899e+00 -1.1700e+03 1.3429e+03 3.9994e+00 -1.1700e+03 1.3714e+03 4.1061e+00 -1.1700e+03 1.4000e+03 4.2100e+00 -1.1700e+03 1.4286e+03 4.3112e+00 -1.1700e+03 1.4571e+03 4.4096e+00 -1.1700e+03 1.4857e+03 4.5055e+00 -1.1700e+03 1.5143e+03 4.5988e+00 -1.1700e+03 1.5429e+03 4.6896e+00 -1.1700e+03 1.5714e+03 4.7780e+00 -1.1700e+03 1.6000e+03 4.8641e+00 -1.1700e+03 1.6286e+03 4.9479e+00 -1.1700e+03 1.6571e+03 5.0295e+00 -1.1700e+03 1.6857e+03 5.1090e+00 -1.1700e+03 1.7143e+03 5.1864e+00 -1.1700e+03 1.7429e+03 5.2618e+00 -1.1700e+03 1.7714e+03 5.3353e+00 -1.1700e+03 1.8000e+03 5.4068e+00 -1.1700e+03 1.8286e+03 5.4766e+00 -1.1700e+03 1.8571e+03 5.5445e+00 -1.1700e+03 1.8857e+03 5.6107e+00 -1.1700e+03 1.9143e+03 5.6753e+00 -1.1700e+03 1.9429e+03 5.7382e+00 -1.1700e+03 1.9714e+03 5.7996e+00 -1.1700e+03 2.0000e+03 5.8595e+00 -1.2000e+03 -2.0000e+03 5.8958e+00 -1.2000e+03 -1.9714e+03 5.8374e+00 -1.2000e+03 -1.9429e+03 5.7776e+00 -1.2000e+03 -1.9143e+03 5.7162e+00 -1.2000e+03 -1.8857e+03 5.6533e+00 -1.2000e+03 -1.8571e+03 5.5888e+00 -1.2000e+03 -1.8286e+03 5.5227e+00 -1.2000e+03 -1.8000e+03 5.4550e+00 -1.2000e+03 -1.7714e+03 5.3854e+00 -1.2000e+03 -1.7429e+03 5.3142e+00 -1.2000e+03 -1.7143e+03 5.2410e+00 -1.2000e+03 -1.6857e+03 5.1660e+00 -1.2000e+03 -1.6571e+03 5.0890e+00 -1.2000e+03 -1.6286e+03 5.0101e+00 -1.2000e+03 -1.6000e+03 4.9290e+00 -1.2000e+03 -1.5714e+03 4.8459e+00 -1.2000e+03 -1.5429e+03 4.7606e+00 -1.2000e+03 -1.5143e+03 4.6730e+00 -1.2000e+03 -1.4857e+03 4.5832e+00 -1.2000e+03 -1.4571e+03 4.4910e+00 -1.2000e+03 -1.4286e+03 4.3963e+00 -1.2000e+03 -1.4000e+03 4.2993e+00 -1.2000e+03 -1.3714e+03 4.1996e+00 -1.2000e+03 -1.3429e+03 4.0975e+00 -1.2000e+03 -1.3143e+03 3.9926e+00 -1.2000e+03 -1.2857e+03 3.8852e+00 -1.2000e+03 -1.2571e+03 3.7749e+00 -1.2000e+03 -1.2286e+03 3.6619e+00 -1.2000e+03 -1.2000e+03 3.5462e+00 -1.2000e+03 -1.1714e+03 3.4275e+00 -1.2000e+03 -1.1429e+03 3.3061e+00 -1.2000e+03 -1.1143e+03 3.1817e+00 -1.2000e+03 -1.0857e+03 3.0545e+00 -1.2000e+03 -1.0571e+03 2.9245e+00 -1.2000e+03 -1.0286e+03 2.7915e+00 -1.2000e+03 -1.0000e+03 2.6558e+00 -1.2000e+03 -9.7143e+02 2.5173e+00 -1.2000e+03 -9.4286e+02 2.3761e+00 -1.2000e+03 -9.1429e+02 2.2324e+00 -1.2000e+03 -8.8571e+02 2.0861e+00 -1.2000e+03 -8.5714e+02 1.9375e+00 -1.2000e+03 -8.2857e+02 1.7866e+00 -1.2000e+03 -8.0000e+02 1.6338e+00 -1.2000e+03 -7.7143e+02 1.4792e+00 -1.2000e+03 -7.4286e+02 1.3231e+00 -1.2000e+03 -7.1429e+02 1.1657e+00 -1.2000e+03 -6.8571e+02 1.0074e+00 -1.2000e+03 -6.5714e+02 8.4863e-01 -1.2000e+03 -6.2857e+02 6.8971e-01 -1.2000e+03 -6.0000e+02 5.3111e-01 -1.2000e+03 -5.7143e+02 3.7334e-01 -1.2000e+03 -5.4286e+02 2.1692e-01 -1.2000e+03 -5.1429e+02 6.2437e-02 -1.2000e+03 -4.8571e+02 -8.9501e-02 -1.2000e+03 -4.5714e+02 -2.3824e-01 -1.2000e+03 -4.2857e+02 -3.8310e-01 -1.2000e+03 -4.0000e+02 -5.2338e-01 -1.2000e+03 -3.7143e+02 -6.5834e-01 -1.2000e+03 -3.4286e+02 -7.8723e-01 -1.2000e+03 -3.1429e+02 -9.0932e-01 -1.2000e+03 -2.8571e+02 -1.0239e+00 -1.2000e+03 -2.5714e+02 -1.1301e+00 -1.2000e+03 -2.2857e+02 -1.2274e+00 -1.2000e+03 -2.0000e+02 -1.3149e+00 -1.2000e+03 -1.7143e+02 -1.3922e+00 -1.2000e+03 -1.4286e+02 -1.4587e+00 -1.2000e+03 -1.1429e+02 -1.5138e+00 -1.2000e+03 -8.5714e+01 -1.5571e+00 -1.2000e+03 -5.7143e+01 -1.5882e+00 -1.2000e+03 -2.8571e+01 -1.6070e+00 -1.2000e+03 0.0000e+00 -1.6133e+00 -1.2000e+03 2.8571e+01 -1.6070e+00 -1.2000e+03 5.7143e+01 -1.5882e+00 -1.2000e+03 8.5714e+01 -1.5571e+00 -1.2000e+03 1.1429e+02 -1.5138e+00 -1.2000e+03 1.4286e+02 -1.4587e+00 -1.2000e+03 1.7143e+02 -1.3922e+00 -1.2000e+03 2.0000e+02 -1.3149e+00 -1.2000e+03 2.2857e+02 -1.2274e+00 -1.2000e+03 2.5714e+02 -1.1301e+00 -1.2000e+03 2.8571e+02 -1.0239e+00 -1.2000e+03 3.1429e+02 -9.0932e-01 -1.2000e+03 3.4286e+02 -7.8723e-01 -1.2000e+03 3.7143e+02 -6.5834e-01 -1.2000e+03 4.0000e+02 -5.2338e-01 -1.2000e+03 4.2857e+02 -3.8310e-01 -1.2000e+03 4.5714e+02 -2.3824e-01 -1.2000e+03 4.8571e+02 -8.9501e-02 -1.2000e+03 5.1429e+02 6.2437e-02 -1.2000e+03 5.4286e+02 2.1692e-01 -1.2000e+03 5.7143e+02 3.7334e-01 -1.2000e+03 6.0000e+02 5.3111e-01 -1.2000e+03 6.2857e+02 6.8971e-01 -1.2000e+03 6.5714e+02 8.4863e-01 -1.2000e+03 6.8571e+02 1.0074e+00 -1.2000e+03 7.1429e+02 1.1657e+00 -1.2000e+03 7.4286e+02 1.3231e+00 -1.2000e+03 7.7143e+02 1.4792e+00 -1.2000e+03 8.0000e+02 1.6338e+00 -1.2000e+03 8.2857e+02 1.7866e+00 -1.2000e+03 8.5714e+02 1.9375e+00 -1.2000e+03 8.8571e+02 2.0861e+00 -1.2000e+03 9.1429e+02 2.2324e+00 -1.2000e+03 9.4286e+02 2.3761e+00 -1.2000e+03 9.7143e+02 2.5173e+00 -1.2000e+03 1.0000e+03 2.6558e+00 -1.2000e+03 1.0286e+03 2.7915e+00 -1.2000e+03 1.0571e+03 2.9245e+00 -1.2000e+03 1.0857e+03 3.0545e+00 -1.2000e+03 1.1143e+03 3.1817e+00 -1.2000e+03 1.1429e+03 3.3061e+00 -1.2000e+03 1.1714e+03 3.4275e+00 -1.2000e+03 1.2000e+03 3.5462e+00 -1.2000e+03 1.2286e+03 3.6619e+00 -1.2000e+03 1.2571e+03 3.7749e+00 -1.2000e+03 1.2857e+03 3.8852e+00 -1.2000e+03 1.3143e+03 3.9926e+00 -1.2000e+03 1.3429e+03 4.0975e+00 -1.2000e+03 1.3714e+03 4.1996e+00 -1.2000e+03 1.4000e+03 4.2993e+00 -1.2000e+03 1.4286e+03 4.3963e+00 -1.2000e+03 1.4571e+03 4.4910e+00 -1.2000e+03 1.4857e+03 4.5832e+00 -1.2000e+03 1.5143e+03 4.6730e+00 -1.2000e+03 1.5429e+03 4.7606e+00 -1.2000e+03 1.5714e+03 4.8459e+00 -1.2000e+03 1.6000e+03 4.9290e+00 -1.2000e+03 1.6286e+03 5.0101e+00 -1.2000e+03 1.6571e+03 5.0890e+00 -1.2000e+03 1.6857e+03 5.1660e+00 -1.2000e+03 1.7143e+03 5.2410e+00 -1.2000e+03 1.7429e+03 5.3142e+00 -1.2000e+03 1.7714e+03 5.3854e+00 -1.2000e+03 1.8000e+03 5.4550e+00 -1.2000e+03 1.8286e+03 5.5227e+00 -1.2000e+03 1.8571e+03 5.5888e+00 -1.2000e+03 1.8857e+03 5.6533e+00 -1.2000e+03 1.9143e+03 5.7162e+00 -1.2000e+03 1.9429e+03 5.7776e+00 -1.2000e+03 1.9714e+03 5.8374e+00 -1.2000e+03 2.0000e+03 5.8958e+00 -1.2300e+03 -2.0000e+03 5.9322e+00 -1.2300e+03 -1.9714e+03 5.8752e+00 -1.2300e+03 -1.9429e+03 5.8168e+00 -1.2300e+03 -1.9143e+03 5.7571e+00 -1.2300e+03 -1.8857e+03 5.6958e+00 -1.2300e+03 -1.8571e+03 5.6331e+00 -1.2300e+03 -1.8286e+03 5.5688e+00 -1.2300e+03 -1.8000e+03 5.5029e+00 -1.2300e+03 -1.7714e+03 5.4354e+00 -1.2300e+03 -1.7429e+03 5.3662e+00 -1.2300e+03 -1.7143e+03 5.2953e+00 -1.2300e+03 -1.6857e+03 5.2226e+00 -1.2300e+03 -1.6571e+03 5.1481e+00 -1.2300e+03 -1.6286e+03 5.0717e+00 -1.2300e+03 -1.6000e+03 4.9934e+00 -1.2300e+03 -1.5714e+03 4.9131e+00 -1.2300e+03 -1.5429e+03 4.8308e+00 -1.2300e+03 -1.5143e+03 4.7464e+00 -1.2300e+03 -1.4857e+03 4.6598e+00 -1.2300e+03 -1.4571e+03 4.5711e+00 -1.2300e+03 -1.4286e+03 4.4802e+00 -1.2300e+03 -1.4000e+03 4.3870e+00 -1.2300e+03 -1.3714e+03 4.2915e+00 -1.2300e+03 -1.3429e+03 4.1937e+00 -1.2300e+03 -1.3143e+03 4.0934e+00 -1.2300e+03 -1.2857e+03 3.9907e+00 -1.2300e+03 -1.2571e+03 3.8855e+00 -1.2300e+03 -1.2286e+03 3.7779e+00 -1.2300e+03 -1.2000e+03 3.6677e+00 -1.2300e+03 -1.1714e+03 3.5549e+00 -1.2300e+03 -1.1429e+03 3.4397e+00 -1.2300e+03 -1.1143e+03 3.3219e+00 -1.2300e+03 -1.0857e+03 3.2015e+00 -1.2300e+03 -1.0571e+03 3.0786e+00 -1.2300e+03 -1.0286e+03 2.9533e+00 -1.2300e+03 -1.0000e+03 2.8255e+00 -1.2300e+03 -9.7143e+02 2.6953e+00 -1.2300e+03 -9.4286e+02 2.5628e+00 -1.2300e+03 -9.1429e+02 2.4282e+00 -1.2300e+03 -8.8571e+02 2.2914e+00 -1.2300e+03 -8.5714e+02 2.1527e+00 -1.2300e+03 -8.2857e+02 2.0122e+00 -1.2300e+03 -8.0000e+02 1.8701e+00 -1.2300e+03 -7.7143e+02 1.7267e+00 -1.2300e+03 -7.4286e+02 1.5821e+00 -1.2300e+03 -7.1429e+02 1.4366e+00 -1.2300e+03 -6.8571e+02 1.2907e+00 -1.2300e+03 -6.5714e+02 1.1445e+00 -1.2300e+03 -6.2857e+02 9.9851e-01 -1.2300e+03 -6.0000e+02 8.5313e-01 -1.2300e+03 -5.7143e+02 7.0880e-01 -1.2300e+03 -5.4286e+02 5.6600e-01 -1.2300e+03 -5.1429e+02 4.2526e-01 -1.2300e+03 -4.8571e+02 2.8711e-01 -1.2300e+03 -4.5714e+02 1.5213e-01 -1.2300e+03 -4.2857e+02 2.0919e-02 -1.2300e+03 -4.0000e+02 -1.0590e-01 -1.2300e+03 -3.7143e+02 -2.2768e-01 -1.2300e+03 -3.4286e+02 -3.4380e-01 -1.2300e+03 -3.1429e+02 -4.5361e-01 -1.2300e+03 -2.8571e+02 -5.5646e-01 -1.2300e+03 -2.5714e+02 -6.5173e-01 -1.2300e+03 -2.2857e+02 -7.3882e-01 -1.2300e+03 -2.0000e+02 -8.1716e-01 -1.2300e+03 -1.7143e+02 -8.8622e-01 -1.2300e+03 -1.4286e+02 -9.4552e-01 -1.2300e+03 -1.1429e+02 -9.9465e-01 -1.2300e+03 -8.5714e+01 -1.0332e+00 -1.2300e+03 -5.7143e+01 -1.0610e+00 -1.2300e+03 -2.8571e+01 -1.0778e+00 -1.2300e+03 0.0000e+00 -1.0834e+00 -1.2300e+03 2.8571e+01 -1.0778e+00 -1.2300e+03 5.7143e+01 -1.0610e+00 -1.2300e+03 8.5714e+01 -1.0332e+00 -1.2300e+03 1.1429e+02 -9.9465e-01 -1.2300e+03 1.4286e+02 -9.4552e-01 -1.2300e+03 1.7143e+02 -8.8622e-01 -1.2300e+03 2.0000e+02 -8.1716e-01 -1.2300e+03 2.2857e+02 -7.3882e-01 -1.2300e+03 2.5714e+02 -6.5173e-01 -1.2300e+03 2.8571e+02 -5.5646e-01 -1.2300e+03 3.1429e+02 -4.5361e-01 -1.2300e+03 3.4286e+02 -3.4380e-01 -1.2300e+03 3.7143e+02 -2.2768e-01 -1.2300e+03 4.0000e+02 -1.0590e-01 -1.2300e+03 4.2857e+02 2.0919e-02 -1.2300e+03 4.5714e+02 1.5213e-01 -1.2300e+03 4.8571e+02 2.8711e-01 -1.2300e+03 5.1429e+02 4.2526e-01 -1.2300e+03 5.4286e+02 5.6600e-01 -1.2300e+03 5.7143e+02 7.0880e-01 -1.2300e+03 6.0000e+02 8.5313e-01 -1.2300e+03 6.2857e+02 9.9851e-01 -1.2300e+03 6.5714e+02 1.1445e+00 -1.2300e+03 6.8571e+02 1.2907e+00 -1.2300e+03 7.1429e+02 1.4366e+00 -1.2300e+03 7.4286e+02 1.5821e+00 -1.2300e+03 7.7143e+02 1.7267e+00 -1.2300e+03 8.0000e+02 1.8701e+00 -1.2300e+03 8.2857e+02 2.0122e+00 -1.2300e+03 8.5714e+02 2.1527e+00 -1.2300e+03 8.8571e+02 2.2914e+00 -1.2300e+03 9.1429e+02 2.4282e+00 -1.2300e+03 9.4286e+02 2.5628e+00 -1.2300e+03 9.7143e+02 2.6953e+00 -1.2300e+03 1.0000e+03 2.8255e+00 -1.2300e+03 1.0286e+03 2.9533e+00 -1.2300e+03 1.0571e+03 3.0786e+00 -1.2300e+03 1.0857e+03 3.2015e+00 -1.2300e+03 1.1143e+03 3.3219e+00 -1.2300e+03 1.1429e+03 3.4397e+00 -1.2300e+03 1.1714e+03 3.5549e+00 -1.2300e+03 1.2000e+03 3.6677e+00 -1.2300e+03 1.2286e+03 3.7779e+00 -1.2300e+03 1.2571e+03 3.8855e+00 -1.2300e+03 1.2857e+03 3.9907e+00 -1.2300e+03 1.3143e+03 4.0934e+00 -1.2300e+03 1.3429e+03 4.1937e+00 -1.2300e+03 1.3714e+03 4.2915e+00 -1.2300e+03 1.4000e+03 4.3870e+00 -1.2300e+03 1.4286e+03 4.4802e+00 -1.2300e+03 1.4571e+03 4.5711e+00 -1.2300e+03 1.4857e+03 4.6598e+00 -1.2300e+03 1.5143e+03 4.7464e+00 -1.2300e+03 1.5429e+03 4.8308e+00 -1.2300e+03 1.5714e+03 4.9131e+00 -1.2300e+03 1.6000e+03 4.9934e+00 -1.2300e+03 1.6286e+03 5.0717e+00 -1.2300e+03 1.6571e+03 5.1481e+00 -1.2300e+03 1.6857e+03 5.2226e+00 -1.2300e+03 1.7143e+03 5.2953e+00 -1.2300e+03 1.7429e+03 5.3662e+00 -1.2300e+03 1.7714e+03 5.4354e+00 -1.2300e+03 1.8000e+03 5.5029e+00 -1.2300e+03 1.8286e+03 5.5688e+00 -1.2300e+03 1.8571e+03 5.6331e+00 -1.2300e+03 1.8857e+03 5.6958e+00 -1.2300e+03 1.9143e+03 5.7571e+00 -1.2300e+03 1.9429e+03 5.8168e+00 -1.2300e+03 1.9714e+03 5.8752e+00 -1.2300e+03 2.0000e+03 5.9322e+00 -1.2600e+03 -2.0000e+03 5.9686e+00 -1.2600e+03 -1.9714e+03 5.9130e+00 -1.2600e+03 -1.9429e+03 5.8561e+00 -1.2600e+03 -1.9143e+03 5.7978e+00 -1.2600e+03 -1.8857e+03 5.7382e+00 -1.2600e+03 -1.8571e+03 5.6772e+00 -1.2600e+03 -1.8286e+03 5.6146e+00 -1.2600e+03 -1.8000e+03 5.5506e+00 -1.2600e+03 -1.7714e+03 5.4851e+00 -1.2600e+03 -1.7429e+03 5.4180e+00 -1.2600e+03 -1.7143e+03 5.3492e+00 -1.2600e+03 -1.6857e+03 5.2788e+00 -1.2600e+03 -1.6571e+03 5.2066e+00 -1.2600e+03 -1.6286e+03 5.1327e+00 -1.2600e+03 -1.6000e+03 5.0570e+00 -1.2600e+03 -1.5714e+03 4.9795e+00 -1.2600e+03 -1.5429e+03 4.9001e+00 -1.2600e+03 -1.5143e+03 4.8188e+00 -1.2600e+03 -1.4857e+03 4.7355e+00 -1.2600e+03 -1.4571e+03 4.6501e+00 -1.2600e+03 -1.4286e+03 4.5628e+00 -1.2600e+03 -1.4000e+03 4.4733e+00 -1.2600e+03 -1.3714e+03 4.3817e+00 -1.2600e+03 -1.3429e+03 4.2880e+00 -1.2600e+03 -1.3143e+03 4.1921e+00 -1.2600e+03 -1.2857e+03 4.0939e+00 -1.2600e+03 -1.2571e+03 3.9936e+00 -1.2600e+03 -1.2286e+03 3.8910e+00 -1.2600e+03 -1.2000e+03 3.7861e+00 -1.2600e+03 -1.1714e+03 3.6789e+00 -1.2600e+03 -1.1429e+03 3.5695e+00 -1.2600e+03 -1.1143e+03 3.4578e+00 -1.2600e+03 -1.0857e+03 3.3439e+00 -1.2600e+03 -1.0571e+03 3.2278e+00 -1.2600e+03 -1.0286e+03 3.1095e+00 -1.2600e+03 -1.0000e+03 2.9891e+00 -1.2600e+03 -9.7143e+02 2.8666e+00 -1.2600e+03 -9.4286e+02 2.7422e+00 -1.2600e+03 -9.1429e+02 2.6160e+00 -1.2600e+03 -8.8571e+02 2.4880e+00 -1.2600e+03 -8.5714e+02 2.3584e+00 -1.2600e+03 -8.2857e+02 2.2274e+00 -1.2600e+03 -8.0000e+02 2.0951e+00 -1.2600e+03 -7.7143e+02 1.9619e+00 -1.2600e+03 -7.4286e+02 1.8278e+00 -1.2600e+03 -7.1429e+02 1.6931e+00 -1.2600e+03 -6.8571e+02 1.5583e+00 -1.2600e+03 -6.5714e+02 1.4235e+00 -1.2600e+03 -6.2857e+02 1.2892e+00 -1.2600e+03 -6.0000e+02 1.1556e+00 -1.2600e+03 -5.7143e+02 1.0233e+00 -1.2600e+03 -5.4286e+02 8.9262e-01 -1.2600e+03 -5.1429e+02 7.6407e-01 -1.2600e+03 -4.8571e+02 6.3812e-01 -1.2600e+03 -4.5714e+02 5.1529e-01 -1.2600e+03 -4.2857e+02 3.9610e-01 -1.2600e+03 -4.0000e+02 2.8110e-01 -1.2600e+03 -3.7143e+02 1.7084e-01 -1.2600e+03 -3.4286e+02 6.5885e-02 -1.2600e+03 -3.1429e+02 -3.3215e-02 -1.2600e+03 -2.8571e+02 -1.2591e-01 -1.2600e+03 -2.5714e+02 -2.1165e-01 -1.2600e+03 -2.2857e+02 -2.8994e-01 -1.2600e+03 -2.0000e+02 -3.6029e-01 -1.2600e+03 -1.7143e+02 -4.2224e-01 -1.2600e+03 -1.4286e+02 -4.7540e-01 -1.2600e+03 -1.1429e+02 -5.1940e-01 -1.2600e+03 -8.5714e+01 -5.5394e-01 -1.2600e+03 -5.7143e+01 -5.7879e-01 -1.2600e+03 -2.8571e+01 -5.9377e-01 -1.2600e+03 0.0000e+00 -5.9877e-01 -1.2600e+03 2.8571e+01 -5.9377e-01 -1.2600e+03 5.7143e+01 -5.7879e-01 -1.2600e+03 8.5714e+01 -5.5394e-01 -1.2600e+03 1.1429e+02 -5.1940e-01 -1.2600e+03 1.4286e+02 -4.7540e-01 -1.2600e+03 1.7143e+02 -4.2224e-01 -1.2600e+03 2.0000e+02 -3.6029e-01 -1.2600e+03 2.2857e+02 -2.8994e-01 -1.2600e+03 2.5714e+02 -2.1165e-01 -1.2600e+03 2.8571e+02 -1.2591e-01 -1.2600e+03 3.1429e+02 -3.3215e-02 -1.2600e+03 3.4286e+02 6.5885e-02 -1.2600e+03 3.7143e+02 1.7084e-01 -1.2600e+03 4.0000e+02 2.8110e-01 -1.2600e+03 4.2857e+02 3.9610e-01 -1.2600e+03 4.5714e+02 5.1529e-01 -1.2600e+03 4.8571e+02 6.3812e-01 -1.2600e+03 5.1429e+02 7.6407e-01 -1.2600e+03 5.4286e+02 8.9262e-01 -1.2600e+03 5.7143e+02 1.0233e+00 -1.2600e+03 6.0000e+02 1.1556e+00 -1.2600e+03 6.2857e+02 1.2892e+00 -1.2600e+03 6.5714e+02 1.4235e+00 -1.2600e+03 6.8571e+02 1.5583e+00 -1.2600e+03 7.1429e+02 1.6931e+00 -1.2600e+03 7.4286e+02 1.8278e+00 -1.2600e+03 7.7143e+02 1.9619e+00 -1.2600e+03 8.0000e+02 2.0951e+00 -1.2600e+03 8.2857e+02 2.2274e+00 -1.2600e+03 8.5714e+02 2.3584e+00 -1.2600e+03 8.8571e+02 2.4880e+00 -1.2600e+03 9.1429e+02 2.6160e+00 -1.2600e+03 9.4286e+02 2.7422e+00 -1.2600e+03 9.7143e+02 2.8666e+00 -1.2600e+03 1.0000e+03 2.9891e+00 -1.2600e+03 1.0286e+03 3.1095e+00 -1.2600e+03 1.0571e+03 3.2278e+00 -1.2600e+03 1.0857e+03 3.3439e+00 -1.2600e+03 1.1143e+03 3.4578e+00 -1.2600e+03 1.1429e+03 3.5695e+00 -1.2600e+03 1.1714e+03 3.6789e+00 -1.2600e+03 1.2000e+03 3.7861e+00 -1.2600e+03 1.2286e+03 3.8910e+00 -1.2600e+03 1.2571e+03 3.9936e+00 -1.2600e+03 1.2857e+03 4.0939e+00 -1.2600e+03 1.3143e+03 4.1921e+00 -1.2600e+03 1.3429e+03 4.2880e+00 -1.2600e+03 1.3714e+03 4.3817e+00 -1.2600e+03 1.4000e+03 4.4733e+00 -1.2600e+03 1.4286e+03 4.5628e+00 -1.2600e+03 1.4571e+03 4.6501e+00 -1.2600e+03 1.4857e+03 4.7355e+00 -1.2600e+03 1.5143e+03 4.8188e+00 -1.2600e+03 1.5429e+03 4.9001e+00 -1.2600e+03 1.5714e+03 4.9795e+00 -1.2600e+03 1.6000e+03 5.0570e+00 -1.2600e+03 1.6286e+03 5.1327e+00 -1.2600e+03 1.6571e+03 5.2066e+00 -1.2600e+03 1.6857e+03 5.2788e+00 -1.2600e+03 1.7143e+03 5.3492e+00 -1.2600e+03 1.7429e+03 5.4180e+00 -1.2600e+03 1.7714e+03 5.4851e+00 -1.2600e+03 1.8000e+03 5.5506e+00 -1.2600e+03 1.8286e+03 5.6146e+00 -1.2600e+03 1.8571e+03 5.6772e+00 -1.2600e+03 1.8857e+03 5.7382e+00 -1.2600e+03 1.9143e+03 5.7978e+00 -1.2600e+03 1.9429e+03 5.8561e+00 -1.2600e+03 1.9714e+03 5.9130e+00 -1.2600e+03 2.0000e+03 5.9686e+00 -1.2900e+03 -2.0000e+03 6.0049e+00 -1.2900e+03 -1.9714e+03 5.9507e+00 -1.2900e+03 -1.9429e+03 5.8952e+00 -1.2900e+03 -1.9143e+03 5.8385e+00 -1.2900e+03 -1.8857e+03 5.7805e+00 -1.2900e+03 -1.8571e+03 5.7211e+00 -1.2900e+03 -1.8286e+03 5.6603e+00 -1.2900e+03 -1.8000e+03 5.5981e+00 -1.2900e+03 -1.7714e+03 5.5345e+00 -1.2900e+03 -1.7429e+03 5.4693e+00 -1.2900e+03 -1.7143e+03 5.4027e+00 -1.2900e+03 -1.6857e+03 5.3345e+00 -1.2900e+03 -1.6571e+03 5.2646e+00 -1.2900e+03 -1.6286e+03 5.1932e+00 -1.2900e+03 -1.6000e+03 5.1200e+00 -1.2900e+03 -1.5714e+03 5.0452e+00 -1.2900e+03 -1.5429e+03 4.9686e+00 -1.2900e+03 -1.5143e+03 4.8902e+00 -1.2900e+03 -1.4857e+03 4.8100e+00 -1.2900e+03 -1.4571e+03 4.7279e+00 -1.2900e+03 -1.4286e+03 4.6440e+00 -1.2900e+03 -1.4000e+03 4.5581e+00 -1.2900e+03 -1.3714e+03 4.4703e+00 -1.2900e+03 -1.3429e+03 4.3805e+00 -1.2900e+03 -1.3143e+03 4.2887e+00 -1.2900e+03 -1.2857e+03 4.1949e+00 -1.2900e+03 -1.2571e+03 4.0991e+00 -1.2900e+03 -1.2286e+03 4.0013e+00 -1.2900e+03 -1.2000e+03 3.9014e+00 -1.2900e+03 -1.1714e+03 3.7996e+00 -1.2900e+03 -1.1429e+03 3.6957e+00 -1.2900e+03 -1.1143e+03 3.5898e+00 -1.2900e+03 -1.0857e+03 3.4819e+00 -1.2900e+03 -1.0571e+03 3.3721e+00 -1.2900e+03 -1.0286e+03 3.2604e+00 -1.2900e+03 -1.0000e+03 3.1469e+00 -1.2900e+03 -9.7143e+02 3.0316e+00 -1.2900e+03 -9.4286e+02 2.9147e+00 -1.2900e+03 -9.1429e+02 2.7962e+00 -1.2900e+03 -8.8571e+02 2.6764e+00 -1.2900e+03 -8.5714e+02 2.5552e+00 -1.2900e+03 -8.2857e+02 2.4329e+00 -1.2900e+03 -8.0000e+02 2.3096e+00 -1.2900e+03 -7.7143e+02 2.1856e+00 -1.2900e+03 -7.4286e+02 2.0611e+00 -1.2900e+03 -7.1429e+02 1.9363e+00 -1.2900e+03 -6.8571e+02 1.8115e+00 -1.2900e+03 -6.5714e+02 1.6870e+00 -1.2900e+03 -6.2857e+02 1.5631e+00 -1.2900e+03 -6.0000e+02 1.4402e+00 -1.2900e+03 -5.7143e+02 1.3186e+00 -1.2900e+03 -5.4286e+02 1.1988e+00 -1.2900e+03 -5.1429e+02 1.0811e+00 -1.2900e+03 -4.8571e+02 9.6596e-01 -1.2900e+03 -4.5714e+02 8.5389e-01 -1.2900e+03 -4.2857e+02 7.4532e-01 -1.2900e+03 -4.0000e+02 6.4073e-01 -1.2900e+03 -3.7143e+02 5.4061e-01 -1.2900e+03 -3.4286e+02 4.4544e-01 -1.2900e+03 -3.1429e+02 3.5570e-01 -1.2900e+03 -2.8571e+02 2.7188e-01 -1.2900e+03 -2.5714e+02 1.9443e-01 -1.2900e+03 -2.2857e+02 1.2380e-01 -1.2900e+03 -2.0000e+02 6.0403e-02 -1.2900e+03 -1.7143e+02 4.6172e-03 -1.2900e+03 -1.4286e+02 -4.3208e-02 -1.2900e+03 -1.1429e+02 -8.2769e-02 -1.2900e+03 -8.5714e+01 -1.1381e-01 -1.2900e+03 -5.7143e+01 -1.3613e-01 -1.2900e+03 -2.8571e+01 -1.4959e-01 -1.2900e+03 0.0000e+00 -1.5408e-01 -1.2900e+03 2.8571e+01 -1.4959e-01 -1.2900e+03 5.7143e+01 -1.3613e-01 -1.2900e+03 8.5714e+01 -1.1381e-01 -1.2900e+03 1.1429e+02 -8.2769e-02 -1.2900e+03 1.4286e+02 -4.3208e-02 -1.2900e+03 1.7143e+02 4.6172e-03 -1.2900e+03 2.0000e+02 6.0403e-02 -1.2900e+03 2.2857e+02 1.2380e-01 -1.2900e+03 2.5714e+02 1.9443e-01 -1.2900e+03 2.8571e+02 2.7188e-01 -1.2900e+03 3.1429e+02 3.5570e-01 -1.2900e+03 3.4286e+02 4.4544e-01 -1.2900e+03 3.7143e+02 5.4061e-01 -1.2900e+03 4.0000e+02 6.4073e-01 -1.2900e+03 4.2857e+02 7.4532e-01 -1.2900e+03 4.5714e+02 8.5389e-01 -1.2900e+03 4.8571e+02 9.6596e-01 -1.2900e+03 5.1429e+02 1.0811e+00 -1.2900e+03 5.4286e+02 1.1988e+00 -1.2900e+03 5.7143e+02 1.3186e+00 -1.2900e+03 6.0000e+02 1.4402e+00 -1.2900e+03 6.2857e+02 1.5631e+00 -1.2900e+03 6.5714e+02 1.6870e+00 -1.2900e+03 6.8571e+02 1.8115e+00 -1.2900e+03 7.1429e+02 1.9363e+00 -1.2900e+03 7.4286e+02 2.0611e+00 -1.2900e+03 7.7143e+02 2.1856e+00 -1.2900e+03 8.0000e+02 2.3096e+00 -1.2900e+03 8.2857e+02 2.4329e+00 -1.2900e+03 8.5714e+02 2.5552e+00 -1.2900e+03 8.8571e+02 2.6764e+00 -1.2900e+03 9.1429e+02 2.7962e+00 -1.2900e+03 9.4286e+02 2.9147e+00 -1.2900e+03 9.7143e+02 3.0316e+00 -1.2900e+03 1.0000e+03 3.1469e+00 -1.2900e+03 1.0286e+03 3.2604e+00 -1.2900e+03 1.0571e+03 3.3721e+00 -1.2900e+03 1.0857e+03 3.4819e+00 -1.2900e+03 1.1143e+03 3.5898e+00 -1.2900e+03 1.1429e+03 3.6957e+00 -1.2900e+03 1.1714e+03 3.7996e+00 -1.2900e+03 1.2000e+03 3.9014e+00 -1.2900e+03 1.2286e+03 4.0013e+00 -1.2900e+03 1.2571e+03 4.0991e+00 -1.2900e+03 1.2857e+03 4.1949e+00 -1.2900e+03 1.3143e+03 4.2887e+00 -1.2900e+03 1.3429e+03 4.3805e+00 -1.2900e+03 1.3714e+03 4.4703e+00 -1.2900e+03 1.4000e+03 4.5581e+00 -1.2900e+03 1.4286e+03 4.6440e+00 -1.2900e+03 1.4571e+03 4.7279e+00 -1.2900e+03 1.4857e+03 4.8100e+00 -1.2900e+03 1.5143e+03 4.8902e+00 -1.2900e+03 1.5429e+03 4.9686e+00 -1.2900e+03 1.5714e+03 5.0452e+00 -1.2900e+03 1.6000e+03 5.1200e+00 -1.2900e+03 1.6286e+03 5.1932e+00 -1.2900e+03 1.6571e+03 5.2646e+00 -1.2900e+03 1.6857e+03 5.3345e+00 -1.2900e+03 1.7143e+03 5.4027e+00 -1.2900e+03 1.7429e+03 5.4693e+00 -1.2900e+03 1.7714e+03 5.5345e+00 -1.2900e+03 1.8000e+03 5.5981e+00 -1.2900e+03 1.8286e+03 5.6603e+00 -1.2900e+03 1.8571e+03 5.7211e+00 -1.2900e+03 1.8857e+03 5.7805e+00 -1.2900e+03 1.9143e+03 5.8385e+00 -1.2900e+03 1.9429e+03 5.8952e+00 -1.2900e+03 1.9714e+03 5.9507e+00 -1.2900e+03 2.0000e+03 6.0049e+00 -1.3200e+03 -2.0000e+03 6.0412e+00 -1.3200e+03 -1.9714e+03 5.9883e+00 -1.3200e+03 -1.9429e+03 5.9343e+00 -1.3200e+03 -1.9143e+03 5.8790e+00 -1.3200e+03 -1.8857e+03 5.8225e+00 -1.3200e+03 -1.8571e+03 5.7648e+00 -1.3200e+03 -1.8286e+03 5.7057e+00 -1.3200e+03 -1.8000e+03 5.6453e+00 -1.3200e+03 -1.7714e+03 5.5835e+00 -1.3200e+03 -1.7429e+03 5.5204e+00 -1.3200e+03 -1.7143e+03 5.4557e+00 -1.3200e+03 -1.6857e+03 5.3897e+00 -1.3200e+03 -1.6571e+03 5.3221e+00 -1.3200e+03 -1.6286e+03 5.2530e+00 -1.3200e+03 -1.6000e+03 5.1823e+00 -1.3200e+03 -1.5714e+03 5.1100e+00 -1.3200e+03 -1.5429e+03 5.0362e+00 -1.3200e+03 -1.5143e+03 4.9606e+00 -1.3200e+03 -1.4857e+03 4.8834e+00 -1.3200e+03 -1.4571e+03 4.8044e+00 -1.3200e+03 -1.4286e+03 4.7238e+00 -1.3200e+03 -1.4000e+03 4.6413e+00 -1.3200e+03 -1.3714e+03 4.5571e+00 -1.3200e+03 -1.3429e+03 4.4711e+00 -1.3200e+03 -1.3143e+03 4.3833e+00 -1.3200e+03 -1.2857e+03 4.2937e+00 -1.3200e+03 -1.2571e+03 4.2022e+00 -1.3200e+03 -1.2286e+03 4.1089e+00 -1.3200e+03 -1.2000e+03 4.0138e+00 -1.3200e+03 -1.1714e+03 3.9169e+00 -1.3200e+03 -1.1429e+03 3.8182e+00 -1.3200e+03 -1.1143e+03 3.7177e+00 -1.3200e+03 -1.0857e+03 3.6155e+00 -1.3200e+03 -1.0571e+03 3.5117e+00 -1.3200e+03 -1.0286e+03 3.4061e+00 -1.3200e+03 -1.0000e+03 3.2991e+00 -1.3200e+03 -9.7143e+02 3.1905e+00 -1.3200e+03 -9.4286e+02 3.0805e+00 -1.3200e+03 -9.1429e+02 2.9693e+00 -1.3200e+03 -8.8571e+02 2.8569e+00 -1.3200e+03 -8.5714e+02 2.7434e+00 -1.3200e+03 -8.2857e+02 2.6291e+00 -1.3200e+03 -8.0000e+02 2.5141e+00 -1.3200e+03 -7.7143e+02 2.3986e+00 -1.3200e+03 -7.4286e+02 2.2828e+00 -1.3200e+03 -7.1429e+02 2.1669e+00 -1.3200e+03 -6.8571e+02 2.0513e+00 -1.3200e+03 -6.5714e+02 1.9361e+00 -1.3200e+03 -6.2857e+02 1.8216e+00 -1.3200e+03 -6.0000e+02 1.7083e+00 -1.3200e+03 -5.7143e+02 1.5964e+00 -1.3200e+03 -5.4286e+02 1.4862e+00 -1.3200e+03 -5.1429e+02 1.3782e+00 -1.3200e+03 -4.8571e+02 1.2728e+00 -1.3200e+03 -4.5714e+02 1.1702e+00 -1.3200e+03 -4.2857e+02 1.0711e+00 -1.3200e+03 -4.0000e+02 9.7570e-01 -1.3200e+03 -3.7143e+02 8.8452e-01 -1.3200e+03 -3.4286e+02 7.9797e-01 -1.3200e+03 -3.1429e+02 7.1647e-01 -1.3200e+03 -2.8571e+02 6.4043e-01 -1.3200e+03 -2.5714e+02 5.7025e-01 -1.3200e+03 -2.2857e+02 5.0631e-01 -1.3200e+03 -2.0000e+02 4.4897e-01 -1.3200e+03 -1.7143e+02 3.9856e-01 -1.3200e+03 -1.4286e+02 3.5538e-01 -1.3200e+03 -1.1429e+02 3.1968e-01 -1.3200e+03 -8.5714e+01 2.9168e-01 -1.3200e+03 -5.7143e+01 2.7155e-01 -1.3200e+03 -2.8571e+01 2.5942e-01 -1.3200e+03 0.0000e+00 2.5537e-01 -1.3200e+03 2.8571e+01 2.5942e-01 -1.3200e+03 5.7143e+01 2.7155e-01 -1.3200e+03 8.5714e+01 2.9168e-01 -1.3200e+03 1.1429e+02 3.1968e-01 -1.3200e+03 1.4286e+02 3.5538e-01 -1.3200e+03 1.7143e+02 3.9856e-01 -1.3200e+03 2.0000e+02 4.4897e-01 -1.3200e+03 2.2857e+02 5.0631e-01 -1.3200e+03 2.5714e+02 5.7025e-01 -1.3200e+03 2.8571e+02 6.4043e-01 -1.3200e+03 3.1429e+02 7.1647e-01 -1.3200e+03 3.4286e+02 7.9797e-01 -1.3200e+03 3.7143e+02 8.8452e-01 -1.3200e+03 4.0000e+02 9.7570e-01 -1.3200e+03 4.2857e+02 1.0711e+00 -1.3200e+03 4.5714e+02 1.1702e+00 -1.3200e+03 4.8571e+02 1.2728e+00 -1.3200e+03 5.1429e+02 1.3782e+00 -1.3200e+03 5.4286e+02 1.4862e+00 -1.3200e+03 5.7143e+02 1.5964e+00 -1.3200e+03 6.0000e+02 1.7083e+00 -1.3200e+03 6.2857e+02 1.8216e+00 -1.3200e+03 6.5714e+02 1.9361e+00 -1.3200e+03 6.8571e+02 2.0513e+00 -1.3200e+03 7.1429e+02 2.1669e+00 -1.3200e+03 7.4286e+02 2.2828e+00 -1.3200e+03 7.7143e+02 2.3986e+00 -1.3200e+03 8.0000e+02 2.5141e+00 -1.3200e+03 8.2857e+02 2.6291e+00 -1.3200e+03 8.5714e+02 2.7434e+00 -1.3200e+03 8.8571e+02 2.8569e+00 -1.3200e+03 9.1429e+02 2.9693e+00 -1.3200e+03 9.4286e+02 3.0805e+00 -1.3200e+03 9.7143e+02 3.1905e+00 -1.3200e+03 1.0000e+03 3.2991e+00 -1.3200e+03 1.0286e+03 3.4061e+00 -1.3200e+03 1.0571e+03 3.5117e+00 -1.3200e+03 1.0857e+03 3.6155e+00 -1.3200e+03 1.1143e+03 3.7177e+00 -1.3200e+03 1.1429e+03 3.8182e+00 -1.3200e+03 1.1714e+03 3.9169e+00 -1.3200e+03 1.2000e+03 4.0138e+00 -1.3200e+03 1.2286e+03 4.1089e+00 -1.3200e+03 1.2571e+03 4.2022e+00 -1.3200e+03 1.2857e+03 4.2937e+00 -1.3200e+03 1.3143e+03 4.3833e+00 -1.3200e+03 1.3429e+03 4.4711e+00 -1.3200e+03 1.3714e+03 4.5571e+00 -1.3200e+03 1.4000e+03 4.6413e+00 -1.3200e+03 1.4286e+03 4.7238e+00 -1.3200e+03 1.4571e+03 4.8044e+00 -1.3200e+03 1.4857e+03 4.8834e+00 -1.3200e+03 1.5143e+03 4.9606e+00 -1.3200e+03 1.5429e+03 5.0362e+00 -1.3200e+03 1.5714e+03 5.1100e+00 -1.3200e+03 1.6000e+03 5.1823e+00 -1.3200e+03 1.6286e+03 5.2530e+00 -1.3200e+03 1.6571e+03 5.3221e+00 -1.3200e+03 1.6857e+03 5.3897e+00 -1.3200e+03 1.7143e+03 5.4557e+00 -1.3200e+03 1.7429e+03 5.5204e+00 -1.3200e+03 1.7714e+03 5.5835e+00 -1.3200e+03 1.8000e+03 5.6453e+00 -1.3200e+03 1.8286e+03 5.7057e+00 -1.3200e+03 1.8571e+03 5.7648e+00 -1.3200e+03 1.8857e+03 5.8225e+00 -1.3200e+03 1.9143e+03 5.8790e+00 -1.3200e+03 1.9429e+03 5.9343e+00 -1.3200e+03 1.9714e+03 5.9883e+00 -1.3200e+03 2.0000e+03 6.0412e+00 -1.3500e+03 -2.0000e+03 6.0773e+00 -1.3500e+03 -1.9714e+03 6.0258e+00 -1.3500e+03 -1.9429e+03 5.9732e+00 -1.3500e+03 -1.9143e+03 5.9194e+00 -1.3500e+03 -1.8857e+03 5.8644e+00 -1.3500e+03 -1.8571e+03 5.8082e+00 -1.3500e+03 -1.8286e+03 5.7508e+00 -1.3500e+03 -1.8000e+03 5.6922e+00 -1.3500e+03 -1.7714e+03 5.6322e+00 -1.3500e+03 -1.7429e+03 5.5709e+00 -1.3500e+03 -1.7143e+03 5.5083e+00 -1.3500e+03 -1.6857e+03 5.4443e+00 -1.3500e+03 -1.6571e+03 5.3789e+00 -1.3500e+03 -1.6286e+03 5.3121e+00 -1.3500e+03 -1.6000e+03 5.2438e+00 -1.3500e+03 -1.5714e+03 5.1741e+00 -1.3500e+03 -1.5429e+03 5.1028e+00 -1.3500e+03 -1.5143e+03 5.0300e+00 -1.3500e+03 -1.4857e+03 4.9556e+00 -1.3500e+03 -1.4571e+03 4.8797e+00 -1.3500e+03 -1.4286e+03 4.8022e+00 -1.3500e+03 -1.4000e+03 4.7231e+00 -1.3500e+03 -1.3714e+03 4.6423e+00 -1.3500e+03 -1.3429e+03 4.5599e+00 -1.3500e+03 -1.3143e+03 4.4758e+00 -1.3500e+03 -1.2857e+03 4.3902e+00 -1.3500e+03 -1.2571e+03 4.3028e+00 -1.3500e+03 -1.2286e+03 4.2139e+00 -1.3500e+03 -1.2000e+03 4.1233e+00 -1.3500e+03 -1.1714e+03 4.0310e+00 -1.3500e+03 -1.1429e+03 3.9372e+00 -1.3500e+03 -1.1143e+03 3.8419e+00 -1.3500e+03 -1.0857e+03 3.7450e+00 -1.3500e+03 -1.0571e+03 3.6467e+00 -1.3500e+03 -1.0286e+03 3.5469e+00 -1.3500e+03 -1.0000e+03 3.4459e+00 -1.3500e+03 -9.7143e+02 3.3435e+00 -1.3500e+03 -9.4286e+02 3.2400e+00 -1.3500e+03 -9.1429e+02 3.1355e+00 -1.3500e+03 -8.8571e+02 3.0300e+00 -1.3500e+03 -8.5714e+02 2.9237e+00 -1.3500e+03 -8.2857e+02 2.8167e+00 -1.3500e+03 -8.0000e+02 2.7093e+00 -1.3500e+03 -7.7143e+02 2.6016e+00 -1.3500e+03 -7.4286e+02 2.4937e+00 -1.3500e+03 -7.1429e+02 2.3860e+00 -1.3500e+03 -6.8571e+02 2.2786e+00 -1.3500e+03 -6.5714e+02 2.1718e+00 -1.3500e+03 -6.2857e+02 2.0660e+00 -1.3500e+03 -6.0000e+02 1.9612e+00 -1.3500e+03 -5.7143e+02 1.8580e+00 -1.3500e+03 -5.4286e+02 1.7566e+00 -1.3500e+03 -5.1429e+02 1.6572e+00 -1.3500e+03 -4.8571e+02 1.5604e+00 -1.3500e+03 -4.5714e+02 1.4664e+00 -1.3500e+03 -4.2857e+02 1.3756e+00 -1.3500e+03 -4.0000e+02 1.2884e+00 -1.3500e+03 -3.7143e+02 1.2051e+00 -1.3500e+03 -3.4286e+02 1.1262e+00 -1.3500e+03 -3.1429e+02 1.0520e+00 -1.3500e+03 -2.8571e+02 9.8277e-01 -1.3500e+03 -2.5714e+02 9.1898e-01 -1.3500e+03 -2.2857e+02 8.6092e-01 -1.3500e+03 -2.0000e+02 8.0890e-01 -1.3500e+03 -1.7143e+02 7.6319e-01 -1.3500e+03 -1.4286e+02 7.2407e-01 -1.3500e+03 -1.1429e+02 6.9174e-01 -1.3500e+03 -8.5714e+01 6.6640e-01 -1.3500e+03 -5.7143e+01 6.4819e-01 -1.3500e+03 -2.8571e+01 6.3722e-01 -1.3500e+03 0.0000e+00 6.3355e-01 -1.3500e+03 2.8571e+01 6.3722e-01 -1.3500e+03 5.7143e+01 6.4819e-01 -1.3500e+03 8.5714e+01 6.6640e-01 -1.3500e+03 1.1429e+02 6.9174e-01 -1.3500e+03 1.4286e+02 7.2407e-01 -1.3500e+03 1.7143e+02 7.6319e-01 -1.3500e+03 2.0000e+02 8.0890e-01 -1.3500e+03 2.2857e+02 8.6092e-01 -1.3500e+03 2.5714e+02 9.1898e-01 -1.3500e+03 2.8571e+02 9.8277e-01 -1.3500e+03 3.1429e+02 1.0520e+00 -1.3500e+03 3.4286e+02 1.1262e+00 -1.3500e+03 3.7143e+02 1.2051e+00 -1.3500e+03 4.0000e+02 1.2884e+00 -1.3500e+03 4.2857e+02 1.3756e+00 -1.3500e+03 4.5714e+02 1.4664e+00 -1.3500e+03 4.8571e+02 1.5604e+00 -1.3500e+03 5.1429e+02 1.6572e+00 -1.3500e+03 5.4286e+02 1.7566e+00 -1.3500e+03 5.7143e+02 1.8580e+00 -1.3500e+03 6.0000e+02 1.9612e+00 -1.3500e+03 6.2857e+02 2.0660e+00 -1.3500e+03 6.5714e+02 2.1718e+00 -1.3500e+03 6.8571e+02 2.2786e+00 -1.3500e+03 7.1429e+02 2.3860e+00 -1.3500e+03 7.4286e+02 2.4937e+00 -1.3500e+03 7.7143e+02 2.6016e+00 -1.3500e+03 8.0000e+02 2.7093e+00 -1.3500e+03 8.2857e+02 2.8167e+00 -1.3500e+03 8.5714e+02 2.9237e+00 -1.3500e+03 8.8571e+02 3.0300e+00 -1.3500e+03 9.1429e+02 3.1355e+00 -1.3500e+03 9.4286e+02 3.2400e+00 -1.3500e+03 9.7143e+02 3.3435e+00 -1.3500e+03 1.0000e+03 3.4459e+00 -1.3500e+03 1.0286e+03 3.5469e+00 -1.3500e+03 1.0571e+03 3.6467e+00 -1.3500e+03 1.0857e+03 3.7450e+00 -1.3500e+03 1.1143e+03 3.8419e+00 -1.3500e+03 1.1429e+03 3.9372e+00 -1.3500e+03 1.1714e+03 4.0310e+00 -1.3500e+03 1.2000e+03 4.1233e+00 -1.3500e+03 1.2286e+03 4.2139e+00 -1.3500e+03 1.2571e+03 4.3028e+00 -1.3500e+03 1.2857e+03 4.3902e+00 -1.3500e+03 1.3143e+03 4.4758e+00 -1.3500e+03 1.3429e+03 4.5599e+00 -1.3500e+03 1.3714e+03 4.6423e+00 -1.3500e+03 1.4000e+03 4.7231e+00 -1.3500e+03 1.4286e+03 4.8022e+00 -1.3500e+03 1.4571e+03 4.8797e+00 -1.3500e+03 1.4857e+03 4.9556e+00 -1.3500e+03 1.5143e+03 5.0300e+00 -1.3500e+03 1.5429e+03 5.1028e+00 -1.3500e+03 1.5714e+03 5.1741e+00 -1.3500e+03 1.6000e+03 5.2438e+00 -1.3500e+03 1.6286e+03 5.3121e+00 -1.3500e+03 1.6571e+03 5.3789e+00 -1.3500e+03 1.6857e+03 5.4443e+00 -1.3500e+03 1.7143e+03 5.5083e+00 -1.3500e+03 1.7429e+03 5.5709e+00 -1.3500e+03 1.7714e+03 5.6322e+00 -1.3500e+03 1.8000e+03 5.6922e+00 -1.3500e+03 1.8286e+03 5.7508e+00 -1.3500e+03 1.8571e+03 5.8082e+00 -1.3500e+03 1.8857e+03 5.8644e+00 -1.3500e+03 1.9143e+03 5.9194e+00 -1.3500e+03 1.9429e+03 5.9732e+00 -1.3500e+03 1.9714e+03 6.0258e+00 -1.3500e+03 2.0000e+03 6.0773e+00 -1.3800e+03 -2.0000e+03 6.1134e+00 -1.3800e+03 -1.9714e+03 6.0632e+00 -1.3800e+03 -1.9429e+03 6.0119e+00 -1.3800e+03 -1.9143e+03 5.9595e+00 -1.3800e+03 -1.8857e+03 5.9061e+00 -1.3800e+03 -1.8571e+03 5.8514e+00 -1.3800e+03 -1.8286e+03 5.7957e+00 -1.3800e+03 -1.8000e+03 5.7387e+00 -1.3800e+03 -1.7714e+03 5.6805e+00 -1.3800e+03 -1.7429e+03 5.6211e+00 -1.3800e+03 -1.7143e+03 5.5604e+00 -1.3800e+03 -1.6857e+03 5.4984e+00 -1.3800e+03 -1.6571e+03 5.4352e+00 -1.3800e+03 -1.6286e+03 5.3706e+00 -1.3800e+03 -1.6000e+03 5.3046e+00 -1.3800e+03 -1.5714e+03 5.2373e+00 -1.3800e+03 -1.5429e+03 5.1685e+00 -1.3800e+03 -1.5143e+03 5.0984e+00 -1.3800e+03 -1.4857e+03 5.0268e+00 -1.3800e+03 -1.4571e+03 4.9537e+00 -1.3800e+03 -1.4286e+03 4.8792e+00 -1.3800e+03 -1.4000e+03 4.8032e+00 -1.3800e+03 -1.3714e+03 4.7258e+00 -1.3800e+03 -1.3429e+03 4.6468e+00 -1.3800e+03 -1.3143e+03 4.5664e+00 -1.3800e+03 -1.2857e+03 4.4844e+00 -1.3800e+03 -1.2571e+03 4.4010e+00 -1.3800e+03 -1.2286e+03 4.3161e+00 -1.3800e+03 -1.2000e+03 4.2298e+00 -1.3800e+03 -1.1714e+03 4.1420e+00 -1.3800e+03 -1.1429e+03 4.0529e+00 -1.3800e+03 -1.1143e+03 3.9623e+00 -1.3800e+03 -1.0857e+03 3.8705e+00 -1.3800e+03 -1.0571e+03 3.7773e+00 -1.3800e+03 -1.0286e+03 3.6830e+00 -1.3800e+03 -1.0000e+03 3.5875e+00 -1.3800e+03 -9.7143e+02 3.4910e+00 -1.3800e+03 -9.4286e+02 3.3935e+00 -1.3800e+03 -9.1429e+02 3.2951e+00 -1.3800e+03 -8.8571e+02 3.1960e+00 -1.3800e+03 -8.5714e+02 3.0963e+00 -1.3800e+03 -8.2857e+02 2.9962e+00 -1.3800e+03 -8.0000e+02 2.8957e+00 -1.3800e+03 -7.7143e+02 2.7951e+00 -1.3800e+03 -7.4286e+02 2.6945e+00 -1.3800e+03 -7.1429e+02 2.5942e+00 -1.3800e+03 -6.8571e+02 2.4944e+00 -1.3800e+03 -6.5714e+02 2.3953e+00 -1.3800e+03 -6.2857e+02 2.2971e+00 -1.3800e+03 -6.0000e+02 2.2002e+00 -1.3800e+03 -5.7143e+02 2.1048e+00 -1.3800e+03 -5.4286e+02 2.0112e+00 -1.3800e+03 -5.1429e+02 1.9197e+00 -1.3800e+03 -4.8571e+02 1.8306e+00 -1.3800e+03 -4.5714e+02 1.7442e+00 -1.3800e+03 -4.2857e+02 1.6609e+00 -1.3800e+03 -4.0000e+02 1.5809e+00 -1.3800e+03 -3.7143e+02 1.5047e+00 -1.3800e+03 -3.4286e+02 1.4325e+00 -1.3800e+03 -3.1429e+02 1.3647e+00 -1.3800e+03 -2.8571e+02 1.3016e+00 -1.3800e+03 -2.5714e+02 1.2434e+00 -1.3800e+03 -2.2857e+02 1.1905e+00 -1.3800e+03 -2.0000e+02 1.1432e+00 -1.3800e+03 -1.7143e+02 1.1016e+00 -1.3800e+03 -1.4286e+02 1.0661e+00 -1.3800e+03 -1.1429e+02 1.0367e+00 -1.3800e+03 -8.5714e+01 1.0137e+00 -1.3800e+03 -5.7143e+01 9.9714e-01 -1.3800e+03 -2.8571e+01 9.8719e-01 -1.3800e+03 0.0000e+00 9.8386e-01 -1.3800e+03 2.8571e+01 9.8719e-01 -1.3800e+03 5.7143e+01 9.9714e-01 -1.3800e+03 8.5714e+01 1.0137e+00 -1.3800e+03 1.1429e+02 1.0367e+00 -1.3800e+03 1.4286e+02 1.0661e+00 -1.3800e+03 1.7143e+02 1.1016e+00 -1.3800e+03 2.0000e+02 1.1432e+00 -1.3800e+03 2.2857e+02 1.1905e+00 -1.3800e+03 2.5714e+02 1.2434e+00 -1.3800e+03 2.8571e+02 1.3016e+00 -1.3800e+03 3.1429e+02 1.3647e+00 -1.3800e+03 3.4286e+02 1.4325e+00 -1.3800e+03 3.7143e+02 1.5047e+00 -1.3800e+03 4.0000e+02 1.5809e+00 -1.3800e+03 4.2857e+02 1.6609e+00 -1.3800e+03 4.5714e+02 1.7442e+00 -1.3800e+03 4.8571e+02 1.8306e+00 -1.3800e+03 5.1429e+02 1.9197e+00 -1.3800e+03 5.4286e+02 2.0112e+00 -1.3800e+03 5.7143e+02 2.1048e+00 -1.3800e+03 6.0000e+02 2.2002e+00 -1.3800e+03 6.2857e+02 2.2971e+00 -1.3800e+03 6.5714e+02 2.3953e+00 -1.3800e+03 6.8571e+02 2.4944e+00 -1.3800e+03 7.1429e+02 2.5942e+00 -1.3800e+03 7.4286e+02 2.6945e+00 -1.3800e+03 7.7143e+02 2.7951e+00 -1.3800e+03 8.0000e+02 2.8957e+00 -1.3800e+03 8.2857e+02 2.9962e+00 -1.3800e+03 8.5714e+02 3.0963e+00 -1.3800e+03 8.8571e+02 3.1960e+00 -1.3800e+03 9.1429e+02 3.2951e+00 -1.3800e+03 9.4286e+02 3.3935e+00 -1.3800e+03 9.7143e+02 3.4910e+00 -1.3800e+03 1.0000e+03 3.5875e+00 -1.3800e+03 1.0286e+03 3.6830e+00 -1.3800e+03 1.0571e+03 3.7773e+00 -1.3800e+03 1.0857e+03 3.8705e+00 -1.3800e+03 1.1143e+03 3.9623e+00 -1.3800e+03 1.1429e+03 4.0529e+00 -1.3800e+03 1.1714e+03 4.1420e+00 -1.3800e+03 1.2000e+03 4.2298e+00 -1.3800e+03 1.2286e+03 4.3161e+00 -1.3800e+03 1.2571e+03 4.4010e+00 -1.3800e+03 1.2857e+03 4.4844e+00 -1.3800e+03 1.3143e+03 4.5664e+00 -1.3800e+03 1.3429e+03 4.6468e+00 -1.3800e+03 1.3714e+03 4.7258e+00 -1.3800e+03 1.4000e+03 4.8032e+00 -1.3800e+03 1.4286e+03 4.8792e+00 -1.3800e+03 1.4571e+03 4.9537e+00 -1.3800e+03 1.4857e+03 5.0268e+00 -1.3800e+03 1.5143e+03 5.0984e+00 -1.3800e+03 1.5429e+03 5.1685e+00 -1.3800e+03 1.5714e+03 5.2373e+00 -1.3800e+03 1.6000e+03 5.3046e+00 -1.3800e+03 1.6286e+03 5.3706e+00 -1.3800e+03 1.6571e+03 5.4352e+00 -1.3800e+03 1.6857e+03 5.4984e+00 -1.3800e+03 1.7143e+03 5.5604e+00 -1.3800e+03 1.7429e+03 5.6211e+00 -1.3800e+03 1.7714e+03 5.6805e+00 -1.3800e+03 1.8000e+03 5.7387e+00 -1.3800e+03 1.8286e+03 5.7957e+00 -1.3800e+03 1.8571e+03 5.8514e+00 -1.3800e+03 1.8857e+03 5.9061e+00 -1.3800e+03 1.9143e+03 5.9595e+00 -1.3800e+03 1.9429e+03 6.0119e+00 -1.3800e+03 1.9714e+03 6.0632e+00 -1.3800e+03 2.0000e+03 6.1134e+00 -1.4100e+03 -2.0000e+03 6.1493e+00 -1.4100e+03 -1.9714e+03 6.1004e+00 -1.4100e+03 -1.9429e+03 6.0505e+00 -1.4100e+03 -1.9143e+03 5.9995e+00 -1.4100e+03 -1.8857e+03 5.9475e+00 -1.4100e+03 -1.8571e+03 5.8944e+00 -1.4100e+03 -1.8286e+03 5.8402e+00 -1.4100e+03 -1.8000e+03 5.7849e+00 -1.4100e+03 -1.7714e+03 5.7284e+00 -1.4100e+03 -1.7429e+03 5.6708e+00 -1.4100e+03 -1.7143e+03 5.6120e+00 -1.4100e+03 -1.6857e+03 5.5520e+00 -1.4100e+03 -1.6571e+03 5.4908e+00 -1.4100e+03 -1.6286e+03 5.4283e+00 -1.4100e+03 -1.6000e+03 5.3646e+00 -1.4100e+03 -1.5714e+03 5.2995e+00 -1.4100e+03 -1.5429e+03 5.2332e+00 -1.4100e+03 -1.5143e+03 5.1656e+00 -1.4100e+03 -1.4857e+03 5.0967e+00 -1.4100e+03 -1.4571e+03 5.0264e+00 -1.4100e+03 -1.4286e+03 4.9548e+00 -1.4100e+03 -1.4000e+03 4.8819e+00 -1.4100e+03 -1.3714e+03 4.8076e+00 -1.4100e+03 -1.3429e+03 4.7319e+00 -1.4100e+03 -1.3143e+03 4.6549e+00 -1.4100e+03 -1.2857e+03 4.5765e+00 -1.4100e+03 -1.2571e+03 4.4968e+00 -1.4100e+03 -1.2286e+03 4.4158e+00 -1.4100e+03 -1.2000e+03 4.3335e+00 -1.4100e+03 -1.1714e+03 4.2499e+00 -1.4100e+03 -1.1429e+03 4.1651e+00 -1.4100e+03 -1.1143e+03 4.0791e+00 -1.4100e+03 -1.0857e+03 3.9920e+00 -1.4100e+03 -1.0571e+03 3.9037e+00 -1.4100e+03 -1.0286e+03 3.8144e+00 -1.4100e+03 -1.0000e+03 3.7242e+00 -1.4100e+03 -9.7143e+02 3.6331e+00 -1.4100e+03 -9.4286e+02 3.5412e+00 -1.4100e+03 -9.1429e+02 3.4486e+00 -1.4100e+03 -8.8571e+02 3.3554e+00 -1.4100e+03 -8.5714e+02 3.2618e+00 -1.4100e+03 -8.2857e+02 3.1679e+00 -1.4100e+03 -8.0000e+02 3.0738e+00 -1.4100e+03 -7.7143e+02 2.9797e+00 -1.4100e+03 -7.4286e+02 2.8858e+00 -1.4100e+03 -7.1429e+02 2.7923e+00 -1.4100e+03 -6.8571e+02 2.6994e+00 -1.4100e+03 -6.5714e+02 2.6073e+00 -1.4100e+03 -6.2857e+02 2.5161e+00 -1.4100e+03 -6.0000e+02 2.4263e+00 -1.4100e+03 -5.7143e+02 2.3380e+00 -1.4100e+03 -5.4286e+02 2.2514e+00 -1.4100e+03 -5.1429e+02 2.1669e+00 -1.4100e+03 -4.8571e+02 2.0847e+00 -1.4100e+03 -4.5714e+02 2.0052e+00 -1.4100e+03 -4.2857e+02 1.9285e+00 -1.4100e+03 -4.0000e+02 1.8551e+00 -1.4100e+03 -3.7143e+02 1.7851e+00 -1.4100e+03 -3.4286e+02 1.7190e+00 -1.4100e+03 -3.1429e+02 1.6569e+00 -1.4100e+03 -2.8571e+02 1.5991e+00 -1.4100e+03 -2.5714e+02 1.5459e+00 -1.4100e+03 -2.2857e+02 1.4976e+00 -1.4100e+03 -2.0000e+02 1.4544e+00 -1.4100e+03 -1.7143e+02 1.4165e+00 -1.4100e+03 -1.4286e+02 1.3841e+00 -1.4100e+03 -1.1429e+02 1.3573e+00 -1.4100e+03 -8.5714e+01 1.3364e+00 -1.4100e+03 -5.7143e+01 1.3213e+00 -1.4100e+03 -2.8571e+01 1.3123e+00 -1.4100e+03 0.0000e+00 1.3092e+00 -1.4100e+03 2.8571e+01 1.3123e+00 -1.4100e+03 5.7143e+01 1.3213e+00 -1.4100e+03 8.5714e+01 1.3364e+00 -1.4100e+03 1.1429e+02 1.3573e+00 -1.4100e+03 1.4286e+02 1.3841e+00 -1.4100e+03 1.7143e+02 1.4165e+00 -1.4100e+03 2.0000e+02 1.4544e+00 -1.4100e+03 2.2857e+02 1.4976e+00 -1.4100e+03 2.5714e+02 1.5459e+00 -1.4100e+03 2.8571e+02 1.5991e+00 -1.4100e+03 3.1429e+02 1.6569e+00 -1.4100e+03 3.4286e+02 1.7190e+00 -1.4100e+03 3.7143e+02 1.7851e+00 -1.4100e+03 4.0000e+02 1.8551e+00 -1.4100e+03 4.2857e+02 1.9285e+00 -1.4100e+03 4.5714e+02 2.0052e+00 -1.4100e+03 4.8571e+02 2.0847e+00 -1.4100e+03 5.1429e+02 2.1669e+00 -1.4100e+03 5.4286e+02 2.2514e+00 -1.4100e+03 5.7143e+02 2.3380e+00 -1.4100e+03 6.0000e+02 2.4263e+00 -1.4100e+03 6.2857e+02 2.5161e+00 -1.4100e+03 6.5714e+02 2.6073e+00 -1.4100e+03 6.8571e+02 2.6994e+00 -1.4100e+03 7.1429e+02 2.7923e+00 -1.4100e+03 7.4286e+02 2.8858e+00 -1.4100e+03 7.7143e+02 2.9797e+00 -1.4100e+03 8.0000e+02 3.0738e+00 -1.4100e+03 8.2857e+02 3.1679e+00 -1.4100e+03 8.5714e+02 3.2618e+00 -1.4100e+03 8.8571e+02 3.3554e+00 -1.4100e+03 9.1429e+02 3.4486e+00 -1.4100e+03 9.4286e+02 3.5412e+00 -1.4100e+03 9.7143e+02 3.6331e+00 -1.4100e+03 1.0000e+03 3.7242e+00 -1.4100e+03 1.0286e+03 3.8144e+00 -1.4100e+03 1.0571e+03 3.9037e+00 -1.4100e+03 1.0857e+03 3.9920e+00 -1.4100e+03 1.1143e+03 4.0791e+00 -1.4100e+03 1.1429e+03 4.1651e+00 -1.4100e+03 1.1714e+03 4.2499e+00 -1.4100e+03 1.2000e+03 4.3335e+00 -1.4100e+03 1.2286e+03 4.4158e+00 -1.4100e+03 1.2571e+03 4.4968e+00 -1.4100e+03 1.2857e+03 4.5765e+00 -1.4100e+03 1.3143e+03 4.6549e+00 -1.4100e+03 1.3429e+03 4.7319e+00 -1.4100e+03 1.3714e+03 4.8076e+00 -1.4100e+03 1.4000e+03 4.8819e+00 -1.4100e+03 1.4286e+03 4.9548e+00 -1.4100e+03 1.4571e+03 5.0264e+00 -1.4100e+03 1.4857e+03 5.0967e+00 -1.4100e+03 1.5143e+03 5.1656e+00 -1.4100e+03 1.5429e+03 5.2332e+00 -1.4100e+03 1.5714e+03 5.2995e+00 -1.4100e+03 1.6000e+03 5.3646e+00 -1.4100e+03 1.6286e+03 5.4283e+00 -1.4100e+03 1.6571e+03 5.4908e+00 -1.4100e+03 1.6857e+03 5.5520e+00 -1.4100e+03 1.7143e+03 5.6120e+00 -1.4100e+03 1.7429e+03 5.6708e+00 -1.4100e+03 1.7714e+03 5.7284e+00 -1.4100e+03 1.8000e+03 5.7849e+00 -1.4100e+03 1.8286e+03 5.8402e+00 -1.4100e+03 1.8571e+03 5.8944e+00 -1.4100e+03 1.8857e+03 5.9475e+00 -1.4100e+03 1.9143e+03 5.9995e+00 -1.4100e+03 1.9429e+03 6.0505e+00 -1.4100e+03 1.9714e+03 6.1004e+00 -1.4100e+03 2.0000e+03 6.1493e+00 -1.4400e+03 -2.0000e+03 6.1851e+00 -1.4400e+03 -1.9714e+03 6.1374e+00 -1.4400e+03 -1.9429e+03 6.0888e+00 -1.4400e+03 -1.9143e+03 6.0392e+00 -1.4400e+03 -1.8857e+03 5.9886e+00 -1.4400e+03 -1.8571e+03 5.9370e+00 -1.4400e+03 -1.8286e+03 5.8843e+00 -1.4400e+03 -1.8000e+03 5.8306e+00 -1.4400e+03 -1.7714e+03 5.7758e+00 -1.4400e+03 -1.7429e+03 5.7200e+00 -1.4400e+03 -1.7143e+03 5.6630e+00 -1.4400e+03 -1.6857e+03 5.6049e+00 -1.4400e+03 -1.6571e+03 5.5457e+00 -1.4400e+03 -1.6286e+03 5.4853e+00 -1.4400e+03 -1.6000e+03 5.4237e+00 -1.4400e+03 -1.5714e+03 5.3610e+00 -1.4400e+03 -1.5429e+03 5.2970e+00 -1.4400e+03 -1.5143e+03 5.2318e+00 -1.4400e+03 -1.4857e+03 5.1655e+00 -1.4400e+03 -1.4571e+03 5.0979e+00 -1.4400e+03 -1.4286e+03 5.0290e+00 -1.4400e+03 -1.4000e+03 4.9590e+00 -1.4400e+03 -1.3714e+03 4.8877e+00 -1.4400e+03 -1.3429e+03 4.8151e+00 -1.4400e+03 -1.3143e+03 4.7414e+00 -1.4400e+03 -1.2857e+03 4.6664e+00 -1.4400e+03 -1.2571e+03 4.5903e+00 -1.4400e+03 -1.2286e+03 4.5130e+00 -1.4400e+03 -1.2000e+03 4.4345e+00 -1.4400e+03 -1.1714e+03 4.3549e+00 -1.4400e+03 -1.1429e+03 4.2742e+00 -1.4400e+03 -1.1143e+03 4.1924e+00 -1.4400e+03 -1.0857e+03 4.1097e+00 -1.4400e+03 -1.0571e+03 4.0260e+00 -1.4400e+03 -1.0286e+03 3.9415e+00 -1.4400e+03 -1.0000e+03 3.8561e+00 -1.4400e+03 -9.7143e+02 3.7701e+00 -1.4400e+03 -9.4286e+02 3.6833e+00 -1.4400e+03 -9.1429e+02 3.5961e+00 -1.4400e+03 -8.8571e+02 3.5084e+00 -1.4400e+03 -8.5714e+02 3.4205e+00 -1.4400e+03 -8.2857e+02 3.3323e+00 -1.4400e+03 -8.0000e+02 3.2441e+00 -1.4400e+03 -7.7143e+02 3.1561e+00 -1.4400e+03 -7.4286e+02 3.0683e+00 -1.4400e+03 -7.1429e+02 2.9810e+00 -1.4400e+03 -6.8571e+02 2.8944e+00 -1.4400e+03 -6.5714e+02 2.8086e+00 -1.4400e+03 -6.2857e+02 2.7239e+00 -1.4400e+03 -6.0000e+02 2.6404e+00 -1.4400e+03 -5.7143e+02 2.5585e+00 -1.4400e+03 -5.4286e+02 2.4783e+00 -1.4400e+03 -5.1429e+02 2.4001e+00 -1.4400e+03 -4.8571e+02 2.3242e+00 -1.4400e+03 -4.5714e+02 2.2508e+00 -1.4400e+03 -4.2857e+02 2.1802e+00 -1.4400e+03 -4.0000e+02 2.1125e+00 -1.4400e+03 -3.7143e+02 2.0482e+00 -1.4400e+03 -3.4286e+02 1.9874e+00 -1.4400e+03 -3.1429e+02 1.9304e+00 -1.4400e+03 -2.8571e+02 1.8774e+00 -1.4400e+03 -2.5714e+02 1.8287e+00 -1.4400e+03 -2.2857e+02 1.7844e+00 -1.4400e+03 -2.0000e+02 1.7449e+00 -1.4400e+03 -1.7143e+02 1.7102e+00 -1.4400e+03 -1.4286e+02 1.6806e+00 -1.4400e+03 -1.1429e+02 1.6561e+00 -1.4400e+03 -8.5714e+01 1.6370e+00 -1.4400e+03 -5.7143e+01 1.6232e+00 -1.4400e+03 -2.8571e+01 1.6150e+00 -1.4400e+03 0.0000e+00 1.6122e+00 -1.4400e+03 2.8571e+01 1.6150e+00 -1.4400e+03 5.7143e+01 1.6232e+00 -1.4400e+03 8.5714e+01 1.6370e+00 -1.4400e+03 1.1429e+02 1.6561e+00 -1.4400e+03 1.4286e+02 1.6806e+00 -1.4400e+03 1.7143e+02 1.7102e+00 -1.4400e+03 2.0000e+02 1.7449e+00 -1.4400e+03 2.2857e+02 1.7844e+00 -1.4400e+03 2.5714e+02 1.8287e+00 -1.4400e+03 2.8571e+02 1.8774e+00 -1.4400e+03 3.1429e+02 1.9304e+00 -1.4400e+03 3.4286e+02 1.9874e+00 -1.4400e+03 3.7143e+02 2.0482e+00 -1.4400e+03 4.0000e+02 2.1125e+00 -1.4400e+03 4.2857e+02 2.1802e+00 -1.4400e+03 4.5714e+02 2.2508e+00 -1.4400e+03 4.8571e+02 2.3242e+00 -1.4400e+03 5.1429e+02 2.4001e+00 -1.4400e+03 5.4286e+02 2.4783e+00 -1.4400e+03 5.7143e+02 2.5585e+00 -1.4400e+03 6.0000e+02 2.6404e+00 -1.4400e+03 6.2857e+02 2.7239e+00 -1.4400e+03 6.5714e+02 2.8086e+00 -1.4400e+03 6.8571e+02 2.8944e+00 -1.4400e+03 7.1429e+02 2.9810e+00 -1.4400e+03 7.4286e+02 3.0683e+00 -1.4400e+03 7.7143e+02 3.1561e+00 -1.4400e+03 8.0000e+02 3.2441e+00 -1.4400e+03 8.2857e+02 3.3323e+00 -1.4400e+03 8.5714e+02 3.4205e+00 -1.4400e+03 8.8571e+02 3.5084e+00 -1.4400e+03 9.1429e+02 3.5961e+00 -1.4400e+03 9.4286e+02 3.6833e+00 -1.4400e+03 9.7143e+02 3.7701e+00 -1.4400e+03 1.0000e+03 3.8561e+00 -1.4400e+03 1.0286e+03 3.9415e+00 -1.4400e+03 1.0571e+03 4.0260e+00 -1.4400e+03 1.0857e+03 4.1097e+00 -1.4400e+03 1.1143e+03 4.1924e+00 -1.4400e+03 1.1429e+03 4.2742e+00 -1.4400e+03 1.1714e+03 4.3549e+00 -1.4400e+03 1.2000e+03 4.4345e+00 -1.4400e+03 1.2286e+03 4.5130e+00 -1.4400e+03 1.2571e+03 4.5903e+00 -1.4400e+03 1.2857e+03 4.6664e+00 -1.4400e+03 1.3143e+03 4.7414e+00 -1.4400e+03 1.3429e+03 4.8151e+00 -1.4400e+03 1.3714e+03 4.8877e+00 -1.4400e+03 1.4000e+03 4.9590e+00 -1.4400e+03 1.4286e+03 5.0290e+00 -1.4400e+03 1.4571e+03 5.0979e+00 -1.4400e+03 1.4857e+03 5.1655e+00 -1.4400e+03 1.5143e+03 5.2318e+00 -1.4400e+03 1.5429e+03 5.2970e+00 -1.4400e+03 1.5714e+03 5.3610e+00 -1.4400e+03 1.6000e+03 5.4237e+00 -1.4400e+03 1.6286e+03 5.4853e+00 -1.4400e+03 1.6571e+03 5.5457e+00 -1.4400e+03 1.6857e+03 5.6049e+00 -1.4400e+03 1.7143e+03 5.6630e+00 -1.4400e+03 1.7429e+03 5.7200e+00 -1.4400e+03 1.7714e+03 5.7758e+00 -1.4400e+03 1.8000e+03 5.8306e+00 -1.4400e+03 1.8286e+03 5.8843e+00 -1.4400e+03 1.8571e+03 5.9370e+00 -1.4400e+03 1.8857e+03 5.9886e+00 -1.4400e+03 1.9143e+03 6.0392e+00 -1.4400e+03 1.9429e+03 6.0888e+00 -1.4400e+03 1.9714e+03 6.1374e+00 -1.4400e+03 2.0000e+03 6.1851e+00 -1.4700e+03 -2.0000e+03 6.2207e+00 -1.4700e+03 -1.9714e+03 6.1743e+00 -1.4700e+03 -1.9429e+03 6.1269e+00 -1.4700e+03 -1.9143e+03 6.0786e+00 -1.4700e+03 -1.8857e+03 6.0294e+00 -1.4700e+03 -1.8571e+03 5.9792e+00 -1.4700e+03 -1.8286e+03 5.9281e+00 -1.4700e+03 -1.8000e+03 5.8760e+00 -1.4700e+03 -1.7714e+03 5.8228e+00 -1.4700e+03 -1.7429e+03 5.7687e+00 -1.4700e+03 -1.7143e+03 5.7135e+00 -1.4700e+03 -1.6857e+03 5.6572e+00 -1.4700e+03 -1.6571e+03 5.5999e+00 -1.4700e+03 -1.6286e+03 5.5415e+00 -1.4700e+03 -1.6000e+03 5.4820e+00 -1.4700e+03 -1.5714e+03 5.4215e+00 -1.4700e+03 -1.5429e+03 5.3598e+00 -1.4700e+03 -1.5143e+03 5.2970e+00 -1.4700e+03 -1.4857e+03 5.2330e+00 -1.4700e+03 -1.4571e+03 5.1680e+00 -1.4700e+03 -1.4286e+03 5.1018e+00 -1.4700e+03 -1.4000e+03 5.0345e+00 -1.4700e+03 -1.3714e+03 4.9661e+00 -1.4700e+03 -1.3429e+03 4.8966e+00 -1.4700e+03 -1.3143e+03 4.8260e+00 -1.4700e+03 -1.2857e+03 4.7542e+00 -1.4700e+03 -1.2571e+03 4.6814e+00 -1.4700e+03 -1.2286e+03 4.6076e+00 -1.4700e+03 -1.2000e+03 4.5327e+00 -1.4700e+03 -1.1714e+03 4.4569e+00 -1.4700e+03 -1.1429e+03 4.3801e+00 -1.4700e+03 -1.1143e+03 4.3024e+00 -1.4700e+03 -1.0857e+03 4.2238e+00 -1.4700e+03 -1.0571e+03 4.1444e+00 -1.4700e+03 -1.0286e+03 4.0643e+00 -1.4700e+03 -1.0000e+03 3.9835e+00 -1.4700e+03 -9.7143e+02 3.9021e+00 -1.4700e+03 -9.4286e+02 3.8203e+00 -1.4700e+03 -9.1429e+02 3.7380e+00 -1.4700e+03 -8.8571e+02 3.6554e+00 -1.4700e+03 -8.5714e+02 3.5727e+00 -1.4700e+03 -8.2857e+02 3.4899e+00 -1.4700e+03 -8.0000e+02 3.4071e+00 -1.4700e+03 -7.7143e+02 3.3246e+00 -1.4700e+03 -7.4286e+02 3.2425e+00 -1.4700e+03 -7.1429e+02 3.1609e+00 -1.4700e+03 -6.8571e+02 3.0800e+00 -1.4700e+03 -6.5714e+02 3.0000e+00 -1.4700e+03 -6.2857e+02 2.9211e+00 -1.4700e+03 -6.0000e+02 2.8435e+00 -1.4700e+03 -5.7143e+02 2.7674e+00 -1.4700e+03 -5.4286e+02 2.6930e+00 -1.4700e+03 -5.1429e+02 2.6205e+00 -1.4700e+03 -4.8571e+02 2.5503e+00 -1.4700e+03 -4.5714e+02 2.4824e+00 -1.4700e+03 -4.2857e+02 2.4171e+00 -1.4700e+03 -4.0000e+02 2.3547e+00 -1.4700e+03 -3.7143e+02 2.2954e+00 -1.4700e+03 -3.4286e+02 2.2394e+00 -1.4700e+03 -3.1429e+02 2.1869e+00 -1.4700e+03 -2.8571e+02 2.1382e+00 -1.4700e+03 -2.5714e+02 2.0935e+00 -1.4700e+03 -2.2857e+02 2.0529e+00 -1.4700e+03 -2.0000e+02 2.0166e+00 -1.4700e+03 -1.7143e+02 1.9848e+00 -1.4700e+03 -1.4286e+02 1.9576e+00 -1.4700e+03 -1.1429e+02 1.9352e+00 -1.4700e+03 -8.5714e+01 1.9177e+00 -1.4700e+03 -5.7143e+01 1.9051e+00 -1.4700e+03 -2.8571e+01 1.8975e+00 -1.4700e+03 0.0000e+00 1.8950e+00 -1.4700e+03 2.8571e+01 1.8975e+00 -1.4700e+03 5.7143e+01 1.9051e+00 -1.4700e+03 8.5714e+01 1.9177e+00 -1.4700e+03 1.1429e+02 1.9352e+00 -1.4700e+03 1.4286e+02 1.9576e+00 -1.4700e+03 1.7143e+02 1.9848e+00 -1.4700e+03 2.0000e+02 2.0166e+00 -1.4700e+03 2.2857e+02 2.0529e+00 -1.4700e+03 2.5714e+02 2.0935e+00 -1.4700e+03 2.8571e+02 2.1382e+00 -1.4700e+03 3.1429e+02 2.1869e+00 -1.4700e+03 3.4286e+02 2.2394e+00 -1.4700e+03 3.7143e+02 2.2954e+00 -1.4700e+03 4.0000e+02 2.3547e+00 -1.4700e+03 4.2857e+02 2.4171e+00 -1.4700e+03 4.5714e+02 2.4824e+00 -1.4700e+03 4.8571e+02 2.5503e+00 -1.4700e+03 5.1429e+02 2.6205e+00 -1.4700e+03 5.4286e+02 2.6930e+00 -1.4700e+03 5.7143e+02 2.7674e+00 -1.4700e+03 6.0000e+02 2.8435e+00 -1.4700e+03 6.2857e+02 2.9211e+00 -1.4700e+03 6.5714e+02 3.0000e+00 -1.4700e+03 6.8571e+02 3.0800e+00 -1.4700e+03 7.1429e+02 3.1609e+00 -1.4700e+03 7.4286e+02 3.2425e+00 -1.4700e+03 7.7143e+02 3.3246e+00 -1.4700e+03 8.0000e+02 3.4071e+00 -1.4700e+03 8.2857e+02 3.4899e+00 -1.4700e+03 8.5714e+02 3.5727e+00 -1.4700e+03 8.8571e+02 3.6554e+00 -1.4700e+03 9.1429e+02 3.7380e+00 -1.4700e+03 9.4286e+02 3.8203e+00 -1.4700e+03 9.7143e+02 3.9021e+00 -1.4700e+03 1.0000e+03 3.9835e+00 -1.4700e+03 1.0286e+03 4.0643e+00 -1.4700e+03 1.0571e+03 4.1444e+00 -1.4700e+03 1.0857e+03 4.2238e+00 -1.4700e+03 1.1143e+03 4.3024e+00 -1.4700e+03 1.1429e+03 4.3801e+00 -1.4700e+03 1.1714e+03 4.4569e+00 -1.4700e+03 1.2000e+03 4.5327e+00 -1.4700e+03 1.2286e+03 4.6076e+00 -1.4700e+03 1.2571e+03 4.6814e+00 -1.4700e+03 1.2857e+03 4.7542e+00 -1.4700e+03 1.3143e+03 4.8260e+00 -1.4700e+03 1.3429e+03 4.8966e+00 -1.4700e+03 1.3714e+03 4.9661e+00 -1.4700e+03 1.4000e+03 5.0345e+00 -1.4700e+03 1.4286e+03 5.1018e+00 -1.4700e+03 1.4571e+03 5.1680e+00 -1.4700e+03 1.4857e+03 5.2330e+00 -1.4700e+03 1.5143e+03 5.2970e+00 -1.4700e+03 1.5429e+03 5.3598e+00 -1.4700e+03 1.5714e+03 5.4215e+00 -1.4700e+03 1.6000e+03 5.4820e+00 -1.4700e+03 1.6286e+03 5.5415e+00 -1.4700e+03 1.6571e+03 5.5999e+00 -1.4700e+03 1.6857e+03 5.6572e+00 -1.4700e+03 1.7143e+03 5.7135e+00 -1.4700e+03 1.7429e+03 5.7687e+00 -1.4700e+03 1.7714e+03 5.8228e+00 -1.4700e+03 1.8000e+03 5.8760e+00 -1.4700e+03 1.8286e+03 5.9281e+00 -1.4700e+03 1.8571e+03 5.9792e+00 -1.4700e+03 1.8857e+03 6.0294e+00 -1.4700e+03 1.9143e+03 6.0786e+00 -1.4700e+03 1.9429e+03 6.1269e+00 -1.4700e+03 1.9714e+03 6.1743e+00 -1.4700e+03 2.0000e+03 6.2207e+00 -1.5000e+03 -2.0000e+03 6.2561e+00 -1.5000e+03 -1.9714e+03 6.2109e+00 -1.5000e+03 -1.9429e+03 6.1648e+00 -1.5000e+03 -1.9143e+03 6.1178e+00 -1.5000e+03 -1.8857e+03 6.0699e+00 -1.5000e+03 -1.8571e+03 6.0212e+00 -1.5000e+03 -1.8286e+03 5.9715e+00 -1.5000e+03 -1.8000e+03 5.9209e+00 -1.5000e+03 -1.7714e+03 5.8693e+00 -1.5000e+03 -1.7429e+03 5.8168e+00 -1.5000e+03 -1.7143e+03 5.7633e+00 -1.5000e+03 -1.6857e+03 5.7089e+00 -1.5000e+03 -1.6571e+03 5.6534e+00 -1.5000e+03 -1.6286e+03 5.5970e+00 -1.5000e+03 -1.6000e+03 5.5395e+00 -1.5000e+03 -1.5714e+03 5.4810e+00 -1.5000e+03 -1.5429e+03 5.4215e+00 -1.5000e+03 -1.5143e+03 5.3610e+00 -1.5000e+03 -1.4857e+03 5.2994e+00 -1.5000e+03 -1.4571e+03 5.2369e+00 -1.5000e+03 -1.4286e+03 5.1732e+00 -1.5000e+03 -1.4000e+03 5.1086e+00 -1.5000e+03 -1.3714e+03 5.0429e+00 -1.5000e+03 -1.3429e+03 4.9763e+00 -1.5000e+03 -1.3143e+03 4.9086e+00 -1.5000e+03 -1.2857e+03 4.8400e+00 -1.5000e+03 -1.2571e+03 4.7704e+00 -1.5000e+03 -1.2286e+03 4.6998e+00 -1.5000e+03 -1.2000e+03 4.6284e+00 -1.5000e+03 -1.1714e+03 4.5561e+00 -1.5000e+03 -1.1429e+03 4.4829e+00 -1.5000e+03 -1.1143e+03 4.4090e+00 -1.5000e+03 -1.0857e+03 4.3343e+00 -1.5000e+03 -1.0571e+03 4.2590e+00 -1.5000e+03 -1.0286e+03 4.1830e+00 -1.5000e+03 -1.0000e+03 4.1065e+00 -1.5000e+03 -9.7143e+02 4.0296e+00 -1.5000e+03 -9.4286e+02 3.9522e+00 -1.5000e+03 -9.1429e+02 3.8745e+00 -1.5000e+03 -8.8571e+02 3.7967e+00 -1.5000e+03 -8.5714e+02 3.7188e+00 -1.5000e+03 -8.2857e+02 3.6409e+00 -1.5000e+03 -8.0000e+02 3.5632e+00 -1.5000e+03 -7.7143e+02 3.4858e+00 -1.5000e+03 -7.4286e+02 3.4088e+00 -1.5000e+03 -7.1429e+02 3.3324e+00 -1.5000e+03 -6.8571e+02 3.2568e+00 -1.5000e+03 -6.5714e+02 3.1821e+00 -1.5000e+03 -6.2857e+02 3.1086e+00 -1.5000e+03 -6.0000e+02 3.0363e+00 -1.5000e+03 -5.7143e+02 2.9655e+00 -1.5000e+03 -5.4286e+02 2.8963e+00 -1.5000e+03 -5.1429e+02 2.8291e+00 -1.5000e+03 -4.8571e+02 2.7639e+00 -1.5000e+03 -4.5714e+02 2.7010e+00 -1.5000e+03 -4.2857e+02 2.6406e+00 -1.5000e+03 -4.0000e+02 2.5829e+00 -1.5000e+03 -3.7143e+02 2.5281e+00 -1.5000e+03 -3.4286e+02 2.4764e+00 -1.5000e+03 -3.1429e+02 2.4280e+00 -1.5000e+03 -2.8571e+02 2.3832e+00 -1.5000e+03 -2.5714e+02 2.3419e+00 -1.5000e+03 -2.2857e+02 2.3046e+00 -1.5000e+03 -2.0000e+02 2.2712e+00 -1.5000e+03 -1.7143e+02 2.2420e+00 -1.5000e+03 -1.4286e+02 2.2170e+00 -1.5000e+03 -1.1429e+02 2.1965e+00 -1.5000e+03 -8.5714e+01 2.1804e+00 -1.5000e+03 -5.7143e+01 2.1688e+00 -1.5000e+03 -2.8571e+01 2.1619e+00 -1.5000e+03 0.0000e+00 2.1595e+00 -1.5000e+03 2.8571e+01 2.1619e+00 -1.5000e+03 5.7143e+01 2.1688e+00 -1.5000e+03 8.5714e+01 2.1804e+00 -1.5000e+03 1.1429e+02 2.1965e+00 -1.5000e+03 1.4286e+02 2.2170e+00 -1.5000e+03 1.7143e+02 2.2420e+00 -1.5000e+03 2.0000e+02 2.2712e+00 -1.5000e+03 2.2857e+02 2.3046e+00 -1.5000e+03 2.5714e+02 2.3419e+00 -1.5000e+03 2.8571e+02 2.3832e+00 -1.5000e+03 3.1429e+02 2.4280e+00 -1.5000e+03 3.4286e+02 2.4764e+00 -1.5000e+03 3.7143e+02 2.5281e+00 -1.5000e+03 4.0000e+02 2.5829e+00 -1.5000e+03 4.2857e+02 2.6406e+00 -1.5000e+03 4.5714e+02 2.7010e+00 -1.5000e+03 4.8571e+02 2.7639e+00 -1.5000e+03 5.1429e+02 2.8291e+00 -1.5000e+03 5.4286e+02 2.8963e+00 -1.5000e+03 5.7143e+02 2.9655e+00 -1.5000e+03 6.0000e+02 3.0363e+00 -1.5000e+03 6.2857e+02 3.1086e+00 -1.5000e+03 6.5714e+02 3.1821e+00 -1.5000e+03 6.8571e+02 3.2568e+00 -1.5000e+03 7.1429e+02 3.3324e+00 -1.5000e+03 7.4286e+02 3.4088e+00 -1.5000e+03 7.7143e+02 3.4858e+00 -1.5000e+03 8.0000e+02 3.5632e+00 -1.5000e+03 8.2857e+02 3.6409e+00 -1.5000e+03 8.5714e+02 3.7188e+00 -1.5000e+03 8.8571e+02 3.7967e+00 -1.5000e+03 9.1429e+02 3.8745e+00 -1.5000e+03 9.4286e+02 3.9522e+00 -1.5000e+03 9.7143e+02 4.0296e+00 -1.5000e+03 1.0000e+03 4.1065e+00 -1.5000e+03 1.0286e+03 4.1830e+00 -1.5000e+03 1.0571e+03 4.2590e+00 -1.5000e+03 1.0857e+03 4.3343e+00 -1.5000e+03 1.1143e+03 4.4090e+00 -1.5000e+03 1.1429e+03 4.4829e+00 -1.5000e+03 1.1714e+03 4.5561e+00 -1.5000e+03 1.2000e+03 4.6284e+00 -1.5000e+03 1.2286e+03 4.6998e+00 -1.5000e+03 1.2571e+03 4.7704e+00 -1.5000e+03 1.2857e+03 4.8400e+00 -1.5000e+03 1.3143e+03 4.9086e+00 -1.5000e+03 1.3429e+03 4.9763e+00 -1.5000e+03 1.3714e+03 5.0429e+00 -1.5000e+03 1.4000e+03 5.1086e+00 -1.5000e+03 1.4286e+03 5.1732e+00 -1.5000e+03 1.4571e+03 5.2369e+00 -1.5000e+03 1.4857e+03 5.2994e+00 -1.5000e+03 1.5143e+03 5.3610e+00 -1.5000e+03 1.5429e+03 5.4215e+00 -1.5000e+03 1.5714e+03 5.4810e+00 -1.5000e+03 1.6000e+03 5.5395e+00 -1.5000e+03 1.6286e+03 5.5970e+00 -1.5000e+03 1.6571e+03 5.6534e+00 -1.5000e+03 1.6857e+03 5.7089e+00 -1.5000e+03 1.7143e+03 5.7633e+00 -1.5000e+03 1.7429e+03 5.8168e+00 -1.5000e+03 1.7714e+03 5.8693e+00 -1.5000e+03 1.8000e+03 5.9209e+00 -1.5000e+03 1.8286e+03 5.9715e+00 -1.5000e+03 1.8571e+03 6.0212e+00 -1.5000e+03 1.8857e+03 6.0699e+00 -1.5000e+03 1.9143e+03 6.1178e+00 -1.5000e+03 1.9429e+03 6.1648e+00 -1.5000e+03 1.9714e+03 6.2109e+00 -1.5000e+03 2.0000e+03 6.2561e+00 -1.5300e+03 -2.0000e+03 6.2913e+00 -1.5300e+03 -1.9714e+03 6.2472e+00 -1.5300e+03 -1.9429e+03 6.2024e+00 -1.5300e+03 -1.9143e+03 6.1567e+00 -1.5300e+03 -1.8857e+03 6.1101e+00 -1.5300e+03 -1.8571e+03 6.0627e+00 -1.5300e+03 -1.8286e+03 6.0145e+00 -1.5300e+03 -1.8000e+03 5.9653e+00 -1.5300e+03 -1.7714e+03 5.9153e+00 -1.5300e+03 -1.7429e+03 5.8644e+00 -1.5300e+03 -1.7143e+03 5.8126e+00 -1.5300e+03 -1.6857e+03 5.7599e+00 -1.5300e+03 -1.6571e+03 5.7062e+00 -1.5300e+03 -1.6286e+03 5.6517e+00 -1.5300e+03 -1.6000e+03 5.5962e+00 -1.5300e+03 -1.5714e+03 5.5397e+00 -1.5300e+03 -1.5429e+03 5.4823e+00 -1.5300e+03 -1.5143e+03 5.4240e+00 -1.5300e+03 -1.4857e+03 5.3647e+00 -1.5300e+03 -1.4571e+03 5.3044e+00 -1.5300e+03 -1.4286e+03 5.2432e+00 -1.5300e+03 -1.4000e+03 5.1811e+00 -1.5300e+03 -1.3714e+03 5.1181e+00 -1.5300e+03 -1.3429e+03 5.0542e+00 -1.5300e+03 -1.3143e+03 4.9893e+00 -1.5300e+03 -1.2857e+03 4.9236e+00 -1.5300e+03 -1.2571e+03 4.8570e+00 -1.5300e+03 -1.2286e+03 4.7896e+00 -1.5300e+03 -1.2000e+03 4.7215e+00 -1.5300e+03 -1.1714e+03 4.6525e+00 -1.5300e+03 -1.1429e+03 4.5828e+00 -1.5300e+03 -1.1143e+03 4.5125e+00 -1.5300e+03 -1.0857e+03 4.4415e+00 -1.5300e+03 -1.0571e+03 4.3699e+00 -1.5300e+03 -1.0286e+03 4.2979e+00 -1.5300e+03 -1.0000e+03 4.2254e+00 -1.5300e+03 -9.7143e+02 4.1525e+00 -1.5300e+03 -9.4286e+02 4.0793e+00 -1.5300e+03 -9.1429e+02 4.0060e+00 -1.5300e+03 -8.8571e+02 3.9326e+00 -1.5300e+03 -8.5714e+02 3.8591e+00 -1.5300e+03 -8.2857e+02 3.7858e+00 -1.5300e+03 -8.0000e+02 3.7127e+00 -1.5300e+03 -7.7143e+02 3.6400e+00 -1.5300e+03 -7.4286e+02 3.5678e+00 -1.5300e+03 -7.1429e+02 3.4963e+00 -1.5300e+03 -6.8571e+02 3.4255e+00 -1.5300e+03 -6.5714e+02 3.3557e+00 -1.5300e+03 -6.2857e+02 3.2870e+00 -1.5300e+03 -6.0000e+02 3.2195e+00 -1.5300e+03 -5.7143e+02 3.1535e+00 -1.5300e+03 -5.4286e+02 3.0892e+00 -1.5300e+03 -5.1429e+02 3.0266e+00 -1.5300e+03 -4.8571e+02 2.9661e+00 -1.5300e+03 -4.5714e+02 2.9077e+00 -1.5300e+03 -4.2857e+02 2.8517e+00 -1.5300e+03 -4.0000e+02 2.7983e+00 -1.5300e+03 -3.7143e+02 2.7475e+00 -1.5300e+03 -3.4286e+02 2.6997e+00 -1.5300e+03 -3.1429e+02 2.6550e+00 -1.5300e+03 -2.8571e+02 2.6136e+00 -1.5300e+03 -2.5714e+02 2.5756e+00 -1.5300e+03 -2.2857e+02 2.5411e+00 -1.5300e+03 -2.0000e+02 2.5103e+00 -1.5300e+03 -1.7143e+02 2.4834e+00 -1.5300e+03 -1.4286e+02 2.4604e+00 -1.5300e+03 -1.1429e+02 2.4415e+00 -1.5300e+03 -8.5714e+01 2.4267e+00 -1.5300e+03 -5.7143e+01 2.4160e+00 -1.5300e+03 -2.8571e+01 2.4096e+00 -1.5300e+03 0.0000e+00 2.4075e+00 -1.5300e+03 2.8571e+01 2.4096e+00 -1.5300e+03 5.7143e+01 2.4160e+00 -1.5300e+03 8.5714e+01 2.4267e+00 -1.5300e+03 1.1429e+02 2.4415e+00 -1.5300e+03 1.4286e+02 2.4604e+00 -1.5300e+03 1.7143e+02 2.4834e+00 -1.5300e+03 2.0000e+02 2.5103e+00 -1.5300e+03 2.2857e+02 2.5411e+00 -1.5300e+03 2.5714e+02 2.5756e+00 -1.5300e+03 2.8571e+02 2.6136e+00 -1.5300e+03 3.1429e+02 2.6550e+00 -1.5300e+03 3.4286e+02 2.6997e+00 -1.5300e+03 3.7143e+02 2.7475e+00 -1.5300e+03 4.0000e+02 2.7983e+00 -1.5300e+03 4.2857e+02 2.8517e+00 -1.5300e+03 4.5714e+02 2.9077e+00 -1.5300e+03 4.8571e+02 2.9661e+00 -1.5300e+03 5.1429e+02 3.0266e+00 -1.5300e+03 5.4286e+02 3.0892e+00 -1.5300e+03 5.7143e+02 3.1535e+00 -1.5300e+03 6.0000e+02 3.2195e+00 -1.5300e+03 6.2857e+02 3.2870e+00 -1.5300e+03 6.5714e+02 3.3557e+00 -1.5300e+03 6.8571e+02 3.4255e+00 -1.5300e+03 7.1429e+02 3.4963e+00 -1.5300e+03 7.4286e+02 3.5678e+00 -1.5300e+03 7.7143e+02 3.6400e+00 -1.5300e+03 8.0000e+02 3.7127e+00 -1.5300e+03 8.2857e+02 3.7858e+00 -1.5300e+03 8.5714e+02 3.8591e+00 -1.5300e+03 8.8571e+02 3.9326e+00 -1.5300e+03 9.1429e+02 4.0060e+00 -1.5300e+03 9.4286e+02 4.0793e+00 -1.5300e+03 9.7143e+02 4.1525e+00 -1.5300e+03 1.0000e+03 4.2254e+00 -1.5300e+03 1.0286e+03 4.2979e+00 -1.5300e+03 1.0571e+03 4.3699e+00 -1.5300e+03 1.0857e+03 4.4415e+00 -1.5300e+03 1.1143e+03 4.5125e+00 -1.5300e+03 1.1429e+03 4.5828e+00 -1.5300e+03 1.1714e+03 4.6525e+00 -1.5300e+03 1.2000e+03 4.7215e+00 -1.5300e+03 1.2286e+03 4.7896e+00 -1.5300e+03 1.2571e+03 4.8570e+00 -1.5300e+03 1.2857e+03 4.9236e+00 -1.5300e+03 1.3143e+03 4.9893e+00 -1.5300e+03 1.3429e+03 5.0542e+00 -1.5300e+03 1.3714e+03 5.1181e+00 -1.5300e+03 1.4000e+03 5.1811e+00 -1.5300e+03 1.4286e+03 5.2432e+00 -1.5300e+03 1.4571e+03 5.3044e+00 -1.5300e+03 1.4857e+03 5.3647e+00 -1.5300e+03 1.5143e+03 5.4240e+00 -1.5300e+03 1.5429e+03 5.4823e+00 -1.5300e+03 1.5714e+03 5.5397e+00 -1.5300e+03 1.6000e+03 5.5962e+00 -1.5300e+03 1.6286e+03 5.6517e+00 -1.5300e+03 1.6571e+03 5.7062e+00 -1.5300e+03 1.6857e+03 5.7599e+00 -1.5300e+03 1.7143e+03 5.8126e+00 -1.5300e+03 1.7429e+03 5.8644e+00 -1.5300e+03 1.7714e+03 5.9153e+00 -1.5300e+03 1.8000e+03 5.9653e+00 -1.5300e+03 1.8286e+03 6.0145e+00 -1.5300e+03 1.8571e+03 6.0627e+00 -1.5300e+03 1.8857e+03 6.1101e+00 -1.5300e+03 1.9143e+03 6.1567e+00 -1.5300e+03 1.9429e+03 6.2024e+00 -1.5300e+03 1.9714e+03 6.2472e+00 -1.5300e+03 2.0000e+03 6.2913e+00 -1.5600e+03 -2.0000e+03 6.3263e+00 -1.5600e+03 -1.9714e+03 6.2834e+00 -1.5600e+03 -1.9429e+03 6.2397e+00 -1.5600e+03 -1.9143e+03 6.1952e+00 -1.5600e+03 -1.8857e+03 6.1499e+00 -1.5600e+03 -1.8571e+03 6.1039e+00 -1.5600e+03 -1.8286e+03 6.0570e+00 -1.5600e+03 -1.8000e+03 6.0093e+00 -1.5600e+03 -1.7714e+03 5.9608e+00 -1.5600e+03 -1.7429e+03 5.9115e+00 -1.5600e+03 -1.7143e+03 5.8613e+00 -1.5600e+03 -1.6857e+03 5.8102e+00 -1.5600e+03 -1.6571e+03 5.7583e+00 -1.5600e+03 -1.6286e+03 5.7056e+00 -1.5600e+03 -1.6000e+03 5.6519e+00 -1.5600e+03 -1.5714e+03 5.5974e+00 -1.5600e+03 -1.5429e+03 5.5421e+00 -1.5600e+03 -1.5143e+03 5.4858e+00 -1.5600e+03 -1.4857e+03 5.4287e+00 -1.5600e+03 -1.4571e+03 5.3707e+00 -1.5600e+03 -1.4286e+03 5.3119e+00 -1.5600e+03 -1.4000e+03 5.2522e+00 -1.5600e+03 -1.3714e+03 5.1917e+00 -1.5600e+03 -1.3429e+03 5.1303e+00 -1.5600e+03 -1.3143e+03 5.0682e+00 -1.5600e+03 -1.2857e+03 5.0053e+00 -1.5600e+03 -1.2571e+03 4.9416e+00 -1.5600e+03 -1.2286e+03 4.8772e+00 -1.5600e+03 -1.2000e+03 4.8120e+00 -1.5600e+03 -1.1714e+03 4.7462e+00 -1.5600e+03 -1.1429e+03 4.6798e+00 -1.5600e+03 -1.1143e+03 4.6128e+00 -1.5600e+03 -1.0857e+03 4.5453e+00 -1.5600e+03 -1.0571e+03 4.4773e+00 -1.5600e+03 -1.0286e+03 4.4089e+00 -1.5600e+03 -1.0000e+03 4.3402e+00 -1.5600e+03 -9.7143e+02 4.2711e+00 -1.5600e+03 -9.4286e+02 4.2019e+00 -1.5600e+03 -9.1429e+02 4.1326e+00 -1.5600e+03 -8.8571e+02 4.0632e+00 -1.5600e+03 -8.5714e+02 3.9940e+00 -1.5600e+03 -8.2857e+02 3.9249e+00 -1.5600e+03 -8.0000e+02 3.8561e+00 -1.5600e+03 -7.7143e+02 3.7878e+00 -1.5600e+03 -7.4286e+02 3.7200e+00 -1.5600e+03 -7.1429e+02 3.6528e+00 -1.5600e+03 -6.8571e+02 3.5865e+00 -1.5600e+03 -6.5714e+02 3.5211e+00 -1.5600e+03 -6.2857e+02 3.4569e+00 -1.5600e+03 -6.0000e+02 3.3939e+00 -1.5600e+03 -5.7143e+02 3.3323e+00 -1.5600e+03 -5.4286e+02 3.2723e+00 -1.5600e+03 -5.1429e+02 3.2140e+00 -1.5600e+03 -4.8571e+02 3.1577e+00 -1.5600e+03 -4.5714e+02 3.1034e+00 -1.5600e+03 -4.2857e+02 3.0514e+00 -1.5600e+03 -4.0000e+02 3.0018e+00 -1.5600e+03 -3.7143e+02 2.9548e+00 -1.5600e+03 -3.4286e+02 2.9105e+00 -1.5600e+03 -3.1429e+02 2.8691e+00 -1.5600e+03 -2.8571e+02 2.8307e+00 -1.5600e+03 -2.5714e+02 2.7956e+00 -1.5600e+03 -2.2857e+02 2.7637e+00 -1.5600e+03 -2.0000e+02 2.7353e+00 -1.5600e+03 -1.7143e+02 2.7104e+00 -1.5600e+03 -1.4286e+02 2.6892e+00 -1.5600e+03 -1.1429e+02 2.6718e+00 -1.5600e+03 -8.5714e+01 2.6581e+00 -1.5600e+03 -5.7143e+01 2.6483e+00 -1.5600e+03 -2.8571e+01 2.6424e+00 -1.5600e+03 0.0000e+00 2.6404e+00 -1.5600e+03 2.8571e+01 2.6424e+00 -1.5600e+03 5.7143e+01 2.6483e+00 -1.5600e+03 8.5714e+01 2.6581e+00 -1.5600e+03 1.1429e+02 2.6718e+00 -1.5600e+03 1.4286e+02 2.6892e+00 -1.5600e+03 1.7143e+02 2.7104e+00 -1.5600e+03 2.0000e+02 2.7353e+00 -1.5600e+03 2.2857e+02 2.7637e+00 -1.5600e+03 2.5714e+02 2.7956e+00 -1.5600e+03 2.8571e+02 2.8307e+00 -1.5600e+03 3.1429e+02 2.8691e+00 -1.5600e+03 3.4286e+02 2.9105e+00 -1.5600e+03 3.7143e+02 2.9548e+00 -1.5600e+03 4.0000e+02 3.0018e+00 -1.5600e+03 4.2857e+02 3.0514e+00 -1.5600e+03 4.5714e+02 3.1034e+00 -1.5600e+03 4.8571e+02 3.1577e+00 -1.5600e+03 5.1429e+02 3.2140e+00 -1.5600e+03 5.4286e+02 3.2723e+00 -1.5600e+03 5.7143e+02 3.3323e+00 -1.5600e+03 6.0000e+02 3.3939e+00 -1.5600e+03 6.2857e+02 3.4569e+00 -1.5600e+03 6.5714e+02 3.5211e+00 -1.5600e+03 6.8571e+02 3.5865e+00 -1.5600e+03 7.1429e+02 3.6528e+00 -1.5600e+03 7.4286e+02 3.7200e+00 -1.5600e+03 7.7143e+02 3.7878e+00 -1.5600e+03 8.0000e+02 3.8561e+00 -1.5600e+03 8.2857e+02 3.9249e+00 -1.5600e+03 8.5714e+02 3.9940e+00 -1.5600e+03 8.8571e+02 4.0632e+00 -1.5600e+03 9.1429e+02 4.1326e+00 -1.5600e+03 9.4286e+02 4.2019e+00 -1.5600e+03 9.7143e+02 4.2711e+00 -1.5600e+03 1.0000e+03 4.3402e+00 -1.5600e+03 1.0286e+03 4.4089e+00 -1.5600e+03 1.0571e+03 4.4773e+00 -1.5600e+03 1.0857e+03 4.5453e+00 -1.5600e+03 1.1143e+03 4.6128e+00 -1.5600e+03 1.1429e+03 4.6798e+00 -1.5600e+03 1.1714e+03 4.7462e+00 -1.5600e+03 1.2000e+03 4.8120e+00 -1.5600e+03 1.2286e+03 4.8772e+00 -1.5600e+03 1.2571e+03 4.9416e+00 -1.5600e+03 1.2857e+03 5.0053e+00 -1.5600e+03 1.3143e+03 5.0682e+00 -1.5600e+03 1.3429e+03 5.1303e+00 -1.5600e+03 1.3714e+03 5.1917e+00 -1.5600e+03 1.4000e+03 5.2522e+00 -1.5600e+03 1.4286e+03 5.3119e+00 -1.5600e+03 1.4571e+03 5.3707e+00 -1.5600e+03 1.4857e+03 5.4287e+00 -1.5600e+03 1.5143e+03 5.4858e+00 -1.5600e+03 1.5429e+03 5.5421e+00 -1.5600e+03 1.5714e+03 5.5974e+00 -1.5600e+03 1.6000e+03 5.6519e+00 -1.5600e+03 1.6286e+03 5.7056e+00 -1.5600e+03 1.6571e+03 5.7583e+00 -1.5600e+03 1.6857e+03 5.8102e+00 -1.5600e+03 1.7143e+03 5.8613e+00 -1.5600e+03 1.7429e+03 5.9115e+00 -1.5600e+03 1.7714e+03 5.9608e+00 -1.5600e+03 1.8000e+03 6.0093e+00 -1.5600e+03 1.8286e+03 6.0570e+00 -1.5600e+03 1.8571e+03 6.1039e+00 -1.5600e+03 1.8857e+03 6.1499e+00 -1.5600e+03 1.9143e+03 6.1952e+00 -1.5600e+03 1.9429e+03 6.2397e+00 -1.5600e+03 1.9714e+03 6.2834e+00 -1.5600e+03 2.0000e+03 6.3263e+00 -1.5900e+03 -2.0000e+03 6.3610e+00 -1.5900e+03 -1.9714e+03 6.3192e+00 -1.5900e+03 -1.9429e+03 6.2767e+00 -1.5900e+03 -1.9143e+03 6.2334e+00 -1.5900e+03 -1.8857e+03 6.1894e+00 -1.5900e+03 -1.8571e+03 6.1447e+00 -1.5900e+03 -1.8286e+03 6.0992e+00 -1.5900e+03 -1.8000e+03 6.0529e+00 -1.5900e+03 -1.7714e+03 6.0058e+00 -1.5900e+03 -1.7429e+03 5.9580e+00 -1.5900e+03 -1.7143e+03 5.9093e+00 -1.5900e+03 -1.6857e+03 5.8599e+00 -1.5900e+03 -1.6571e+03 5.8097e+00 -1.5900e+03 -1.6286e+03 5.7587e+00 -1.5900e+03 -1.6000e+03 5.7069e+00 -1.5900e+03 -1.5714e+03 5.6542e+00 -1.5900e+03 -1.5429e+03 5.6008e+00 -1.5900e+03 -1.5143e+03 5.5466e+00 -1.5900e+03 -1.4857e+03 5.4916e+00 -1.5900e+03 -1.4571e+03 5.4357e+00 -1.5900e+03 -1.4286e+03 5.3791e+00 -1.5900e+03 -1.4000e+03 5.3218e+00 -1.5900e+03 -1.3714e+03 5.2637e+00 -1.5900e+03 -1.3429e+03 5.2048e+00 -1.5900e+03 -1.3143e+03 5.1452e+00 -1.5900e+03 -1.2857e+03 5.0850e+00 -1.5900e+03 -1.2571e+03 5.0240e+00 -1.5900e+03 -1.2286e+03 4.9624e+00 -1.5900e+03 -1.2000e+03 4.9002e+00 -1.5900e+03 -1.1714e+03 4.8374e+00 -1.5900e+03 -1.1429e+03 4.7741e+00 -1.5900e+03 -1.1143e+03 4.7103e+00 -1.5900e+03 -1.0857e+03 4.6460e+00 -1.5900e+03 -1.0571e+03 4.5813e+00 -1.5900e+03 -1.0286e+03 4.5164e+00 -1.5900e+03 -1.0000e+03 4.4511e+00 -1.5900e+03 -9.7143e+02 4.3857e+00 -1.5900e+03 -9.4286e+02 4.3201e+00 -1.5900e+03 -9.1429e+02 4.2546e+00 -1.5900e+03 -8.8571e+02 4.1890e+00 -1.5900e+03 -8.5714e+02 4.1236e+00 -1.5900e+03 -8.2857e+02 4.0585e+00 -1.5900e+03 -8.0000e+02 3.9937e+00 -1.5900e+03 -7.7143e+02 3.9294e+00 -1.5900e+03 -7.4286e+02 3.8656e+00 -1.5900e+03 -7.1429e+02 3.8026e+00 -1.5900e+03 -6.8571e+02 3.7403e+00 -1.5900e+03 -6.5714e+02 3.6791e+00 -1.5900e+03 -6.2857e+02 3.6189e+00 -1.5900e+03 -6.0000e+02 3.5600e+00 -1.5900e+03 -5.7143e+02 3.5024e+00 -1.5900e+03 -5.4286e+02 3.4464e+00 -1.5900e+03 -5.1429e+02 3.3920e+00 -1.5900e+03 -4.8571e+02 3.3395e+00 -1.5900e+03 -4.5714e+02 3.2890e+00 -1.5900e+03 -4.2857e+02 3.2406e+00 -1.5900e+03 -4.0000e+02 3.1945e+00 -1.5900e+03 -3.7143e+02 3.1508e+00 -1.5900e+03 -3.4286e+02 3.1097e+00 -1.5900e+03 -3.1429e+02 3.0713e+00 -1.5900e+03 -2.8571e+02 3.0357e+00 -1.5900e+03 -2.5714e+02 3.0031e+00 -1.5900e+03 -2.2857e+02 2.9736e+00 -1.5900e+03 -2.0000e+02 2.9473e+00 -1.5900e+03 -1.7143e+02 2.9243e+00 -1.5900e+03 -1.4286e+02 2.9047e+00 -1.5900e+03 -1.1429e+02 2.8885e+00 -1.5900e+03 -8.5714e+01 2.8759e+00 -1.5900e+03 -5.7143e+01 2.8668e+00 -1.5900e+03 -2.8571e+01 2.8614e+00 -1.5900e+03 0.0000e+00 2.8596e+00 -1.5900e+03 2.8571e+01 2.8614e+00 -1.5900e+03 5.7143e+01 2.8668e+00 -1.5900e+03 8.5714e+01 2.8759e+00 -1.5900e+03 1.1429e+02 2.8885e+00 -1.5900e+03 1.4286e+02 2.9047e+00 -1.5900e+03 1.7143e+02 2.9243e+00 -1.5900e+03 2.0000e+02 2.9473e+00 -1.5900e+03 2.2857e+02 2.9736e+00 -1.5900e+03 2.5714e+02 3.0031e+00 -1.5900e+03 2.8571e+02 3.0357e+00 -1.5900e+03 3.1429e+02 3.0713e+00 -1.5900e+03 3.4286e+02 3.1097e+00 -1.5900e+03 3.7143e+02 3.1508e+00 -1.5900e+03 4.0000e+02 3.1945e+00 -1.5900e+03 4.2857e+02 3.2406e+00 -1.5900e+03 4.5714e+02 3.2890e+00 -1.5900e+03 4.8571e+02 3.3395e+00 -1.5900e+03 5.1429e+02 3.3920e+00 -1.5900e+03 5.4286e+02 3.4464e+00 -1.5900e+03 5.7143e+02 3.5024e+00 -1.5900e+03 6.0000e+02 3.5600e+00 -1.5900e+03 6.2857e+02 3.6189e+00 -1.5900e+03 6.5714e+02 3.6791e+00 -1.5900e+03 6.8571e+02 3.7403e+00 -1.5900e+03 7.1429e+02 3.8026e+00 -1.5900e+03 7.4286e+02 3.8656e+00 -1.5900e+03 7.7143e+02 3.9294e+00 -1.5900e+03 8.0000e+02 3.9937e+00 -1.5900e+03 8.2857e+02 4.0585e+00 -1.5900e+03 8.5714e+02 4.1236e+00 -1.5900e+03 8.8571e+02 4.1890e+00 -1.5900e+03 9.1429e+02 4.2546e+00 -1.5900e+03 9.4286e+02 4.3201e+00 -1.5900e+03 9.7143e+02 4.3857e+00 -1.5900e+03 1.0000e+03 4.4511e+00 -1.5900e+03 1.0286e+03 4.5164e+00 -1.5900e+03 1.0571e+03 4.5813e+00 -1.5900e+03 1.0857e+03 4.6460e+00 -1.5900e+03 1.1143e+03 4.7103e+00 -1.5900e+03 1.1429e+03 4.7741e+00 -1.5900e+03 1.1714e+03 4.8374e+00 -1.5900e+03 1.2000e+03 4.9002e+00 -1.5900e+03 1.2286e+03 4.9624e+00 -1.5900e+03 1.2571e+03 5.0240e+00 -1.5900e+03 1.2857e+03 5.0850e+00 -1.5900e+03 1.3143e+03 5.1452e+00 -1.5900e+03 1.3429e+03 5.2048e+00 -1.5900e+03 1.3714e+03 5.2637e+00 -1.5900e+03 1.4000e+03 5.3218e+00 -1.5900e+03 1.4286e+03 5.3791e+00 -1.5900e+03 1.4571e+03 5.4357e+00 -1.5900e+03 1.4857e+03 5.4916e+00 -1.5900e+03 1.5143e+03 5.5466e+00 -1.5900e+03 1.5429e+03 5.6008e+00 -1.5900e+03 1.5714e+03 5.6542e+00 -1.5900e+03 1.6000e+03 5.7069e+00 -1.5900e+03 1.6286e+03 5.7587e+00 -1.5900e+03 1.6571e+03 5.8097e+00 -1.5900e+03 1.6857e+03 5.8599e+00 -1.5900e+03 1.7143e+03 5.9093e+00 -1.5900e+03 1.7429e+03 5.9580e+00 -1.5900e+03 1.7714e+03 6.0058e+00 -1.5900e+03 1.8000e+03 6.0529e+00 -1.5900e+03 1.8286e+03 6.0992e+00 -1.5900e+03 1.8571e+03 6.1447e+00 -1.5900e+03 1.8857e+03 6.1894e+00 -1.5900e+03 1.9143e+03 6.2334e+00 -1.5900e+03 1.9429e+03 6.2767e+00 -1.5900e+03 1.9714e+03 6.3192e+00 -1.5900e+03 2.0000e+03 6.3610e+00 -1.6200e+03 -2.0000e+03 6.3955e+00 -1.6200e+03 -1.9714e+03 6.3548e+00 -1.6200e+03 -1.9429e+03 6.3134e+00 -1.6200e+03 -1.9143e+03 6.2713e+00 -1.6200e+03 -1.8857e+03 6.2285e+00 -1.6200e+03 -1.8571e+03 6.1850e+00 -1.6200e+03 -1.8286e+03 6.1408e+00 -1.6200e+03 -1.8000e+03 6.0959e+00 -1.6200e+03 -1.7714e+03 6.0503e+00 -1.6200e+03 -1.7429e+03 6.0039e+00 -1.6200e+03 -1.7143e+03 5.9568e+00 -1.6200e+03 -1.6857e+03 5.9089e+00 -1.6200e+03 -1.6571e+03 5.8603e+00 -1.6200e+03 -1.6286e+03 5.8110e+00 -1.6200e+03 -1.6000e+03 5.7609e+00 -1.6200e+03 -1.5714e+03 5.7101e+00 -1.6200e+03 -1.5429e+03 5.6586e+00 -1.6200e+03 -1.5143e+03 5.6063e+00 -1.6200e+03 -1.4857e+03 5.5532e+00 -1.6200e+03 -1.4571e+03 5.4995e+00 -1.6200e+03 -1.4286e+03 5.4451e+00 -1.6200e+03 -1.4000e+03 5.3899e+00 -1.6200e+03 -1.3714e+03 5.3341e+00 -1.6200e+03 -1.3429e+03 5.2776e+00 -1.6200e+03 -1.3143e+03 5.2205e+00 -1.6200e+03 -1.2857e+03 5.1627e+00 -1.6200e+03 -1.2571e+03 5.1044e+00 -1.6200e+03 -1.2286e+03 5.0454e+00 -1.6200e+03 -1.2000e+03 4.9860e+00 -1.6200e+03 -1.1714e+03 4.9260e+00 -1.6200e+03 -1.1429e+03 4.8656e+00 -1.6200e+03 -1.1143e+03 4.8048e+00 -1.6200e+03 -1.0857e+03 4.7436e+00 -1.6200e+03 -1.0571e+03 4.6821e+00 -1.6200e+03 -1.0286e+03 4.6204e+00 -1.6200e+03 -1.0000e+03 4.5584e+00 -1.6200e+03 -9.7143e+02 4.4963e+00 -1.6200e+03 -9.4286e+02 4.4342e+00 -1.6200e+03 -9.1429e+02 4.3721e+00 -1.6200e+03 -8.8571e+02 4.3102e+00 -1.6200e+03 -8.5714e+02 4.2484e+00 -1.6200e+03 -8.2857e+02 4.1869e+00 -1.6200e+03 -8.0000e+02 4.1258e+00 -1.6200e+03 -7.7143e+02 4.0652e+00 -1.6200e+03 -7.4286e+02 4.0052e+00 -1.6200e+03 -7.1429e+02 3.9459e+00 -1.6200e+03 -6.8571e+02 3.8874e+00 -1.6200e+03 -6.5714e+02 3.8299e+00 -1.6200e+03 -6.2857e+02 3.7735e+00 -1.6200e+03 -6.0000e+02 3.7183e+00 -1.6200e+03 -5.7143e+02 3.6645e+00 -1.6200e+03 -5.4286e+02 3.6121e+00 -1.6200e+03 -5.1429e+02 3.5613e+00 -1.6200e+03 -4.8571e+02 3.5123e+00 -1.6200e+03 -4.5714e+02 3.4652e+00 -1.6200e+03 -4.2857e+02 3.4201e+00 -1.6200e+03 -4.0000e+02 3.3771e+00 -1.6200e+03 -3.7143e+02 3.3365e+00 -1.6200e+03 -3.4286e+02 3.2982e+00 -1.6200e+03 -3.1429e+02 3.2625e+00 -1.6200e+03 -2.8571e+02 3.2295e+00 -1.6200e+03 -2.5714e+02 3.1992e+00 -1.6200e+03 -2.2857e+02 3.1719e+00 -1.6200e+03 -2.0000e+02 3.1475e+00 -1.6200e+03 -1.7143e+02 3.1261e+00 -1.6200e+03 -1.4286e+02 3.1080e+00 -1.6200e+03 -1.1429e+02 3.0930e+00 -1.6200e+03 -8.5714e+01 3.0813e+00 -1.6200e+03 -5.7143e+01 3.0729e+00 -1.6200e+03 -2.8571e+01 3.0679e+00 -1.6200e+03 0.0000e+00 3.0662e+00 -1.6200e+03 2.8571e+01 3.0679e+00 -1.6200e+03 5.7143e+01 3.0729e+00 -1.6200e+03 8.5714e+01 3.0813e+00 -1.6200e+03 1.1429e+02 3.0930e+00 -1.6200e+03 1.4286e+02 3.1080e+00 -1.6200e+03 1.7143e+02 3.1261e+00 -1.6200e+03 2.0000e+02 3.1475e+00 -1.6200e+03 2.2857e+02 3.1719e+00 -1.6200e+03 2.5714e+02 3.1992e+00 -1.6200e+03 2.8571e+02 3.2295e+00 -1.6200e+03 3.1429e+02 3.2625e+00 -1.6200e+03 3.4286e+02 3.2982e+00 -1.6200e+03 3.7143e+02 3.3365e+00 -1.6200e+03 4.0000e+02 3.3771e+00 -1.6200e+03 4.2857e+02 3.4201e+00 -1.6200e+03 4.5714e+02 3.4652e+00 -1.6200e+03 4.8571e+02 3.5123e+00 -1.6200e+03 5.1429e+02 3.5613e+00 -1.6200e+03 5.4286e+02 3.6121e+00 -1.6200e+03 5.7143e+02 3.6645e+00 -1.6200e+03 6.0000e+02 3.7183e+00 -1.6200e+03 6.2857e+02 3.7735e+00 -1.6200e+03 6.5714e+02 3.8299e+00 -1.6200e+03 6.8571e+02 3.8874e+00 -1.6200e+03 7.1429e+02 3.9459e+00 -1.6200e+03 7.4286e+02 4.0052e+00 -1.6200e+03 7.7143e+02 4.0652e+00 -1.6200e+03 8.0000e+02 4.1258e+00 -1.6200e+03 8.2857e+02 4.1869e+00 -1.6200e+03 8.5714e+02 4.2484e+00 -1.6200e+03 8.8571e+02 4.3102e+00 -1.6200e+03 9.1429e+02 4.3721e+00 -1.6200e+03 9.4286e+02 4.4342e+00 -1.6200e+03 9.7143e+02 4.4963e+00 -1.6200e+03 1.0000e+03 4.5584e+00 -1.6200e+03 1.0286e+03 4.6204e+00 -1.6200e+03 1.0571e+03 4.6821e+00 -1.6200e+03 1.0857e+03 4.7436e+00 -1.6200e+03 1.1143e+03 4.8048e+00 -1.6200e+03 1.1429e+03 4.8656e+00 -1.6200e+03 1.1714e+03 4.9260e+00 -1.6200e+03 1.2000e+03 4.9860e+00 -1.6200e+03 1.2286e+03 5.0454e+00 -1.6200e+03 1.2571e+03 5.1044e+00 -1.6200e+03 1.2857e+03 5.1627e+00 -1.6200e+03 1.3143e+03 5.2205e+00 -1.6200e+03 1.3429e+03 5.2776e+00 -1.6200e+03 1.3714e+03 5.3341e+00 -1.6200e+03 1.4000e+03 5.3899e+00 -1.6200e+03 1.4286e+03 5.4451e+00 -1.6200e+03 1.4571e+03 5.4995e+00 -1.6200e+03 1.4857e+03 5.5532e+00 -1.6200e+03 1.5143e+03 5.6063e+00 -1.6200e+03 1.5429e+03 5.6586e+00 -1.6200e+03 1.5714e+03 5.7101e+00 -1.6200e+03 1.6000e+03 5.7609e+00 -1.6200e+03 1.6286e+03 5.8110e+00 -1.6200e+03 1.6571e+03 5.8603e+00 -1.6200e+03 1.6857e+03 5.9089e+00 -1.6200e+03 1.7143e+03 5.9568e+00 -1.6200e+03 1.7429e+03 6.0039e+00 -1.6200e+03 1.7714e+03 6.0503e+00 -1.6200e+03 1.8000e+03 6.0959e+00 -1.6200e+03 1.8286e+03 6.1408e+00 -1.6200e+03 1.8571e+03 6.1850e+00 -1.6200e+03 1.8857e+03 6.2285e+00 -1.6200e+03 1.9143e+03 6.2713e+00 -1.6200e+03 1.9429e+03 6.3134e+00 -1.6200e+03 1.9714e+03 6.3548e+00 -1.6200e+03 2.0000e+03 6.3955e+00 -1.6500e+03 -2.0000e+03 6.4297e+00 -1.6500e+03 -1.9714e+03 6.3900e+00 -1.6500e+03 -1.9429e+03 6.3498e+00 -1.6500e+03 -1.9143e+03 6.3088e+00 -1.6500e+03 -1.8857e+03 6.2672e+00 -1.6500e+03 -1.8571e+03 6.2250e+00 -1.6500e+03 -1.8286e+03 6.1821e+00 -1.6500e+03 -1.8000e+03 6.1385e+00 -1.6500e+03 -1.7714e+03 6.0942e+00 -1.6500e+03 -1.7429e+03 6.0492e+00 -1.6500e+03 -1.7143e+03 6.0036e+00 -1.6500e+03 -1.6857e+03 5.9572e+00 -1.6500e+03 -1.6571e+03 5.9102e+00 -1.6500e+03 -1.6286e+03 5.8625e+00 -1.6500e+03 -1.6000e+03 5.8141e+00 -1.6500e+03 -1.5714e+03 5.7651e+00 -1.6500e+03 -1.5429e+03 5.7153e+00 -1.6500e+03 -1.5143e+03 5.6649e+00 -1.6500e+03 -1.4857e+03 5.6138e+00 -1.6500e+03 -1.4571e+03 5.5620e+00 -1.6500e+03 -1.4286e+03 5.5096e+00 -1.6500e+03 -1.4000e+03 5.4566e+00 -1.6500e+03 -1.3714e+03 5.4030e+00 -1.6500e+03 -1.3429e+03 5.3488e+00 -1.6500e+03 -1.3143e+03 5.2940e+00 -1.6500e+03 -1.2857e+03 5.2386e+00 -1.6500e+03 -1.2571e+03 5.1827e+00 -1.6500e+03 -1.2286e+03 5.1263e+00 -1.6500e+03 -1.2000e+03 5.0695e+00 -1.6500e+03 -1.1714e+03 5.0122e+00 -1.6500e+03 -1.1429e+03 4.9546e+00 -1.6500e+03 -1.1143e+03 4.8966e+00 -1.6500e+03 -1.0857e+03 4.8383e+00 -1.6500e+03 -1.0571e+03 4.7797e+00 -1.6500e+03 -1.0286e+03 4.7210e+00 -1.6500e+03 -1.0000e+03 4.6622e+00 -1.6500e+03 -9.7143e+02 4.6032e+00 -1.6500e+03 -9.4286e+02 4.5443e+00 -1.6500e+03 -9.1429e+02 4.4855e+00 -1.6500e+03 -8.8571e+02 4.4268e+00 -1.6500e+03 -8.5714e+02 4.3684e+00 -1.6500e+03 -8.2857e+02 4.3103e+00 -1.6500e+03 -8.0000e+02 4.2527e+00 -1.6500e+03 -7.7143e+02 4.1955e+00 -1.6500e+03 -7.4286e+02 4.1390e+00 -1.6500e+03 -7.1429e+02 4.0832e+00 -1.6500e+03 -6.8571e+02 4.0282e+00 -1.6500e+03 -6.5714e+02 3.9742e+00 -1.6500e+03 -6.2857e+02 3.9212e+00 -1.6500e+03 -6.0000e+02 3.8695e+00 -1.6500e+03 -5.7143e+02 3.8190e+00 -1.6500e+03 -5.4286e+02 3.7699e+00 -1.6500e+03 -5.1429e+02 3.7224e+00 -1.6500e+03 -4.8571e+02 3.6766e+00 -1.6500e+03 -4.5714e+02 3.6326e+00 -1.6500e+03 -4.2857e+02 3.5905e+00 -1.6500e+03 -4.0000e+02 3.5504e+00 -1.6500e+03 -3.7143e+02 3.5125e+00 -1.6500e+03 -3.4286e+02 3.4769e+00 -1.6500e+03 -3.1429e+02 3.4437e+00 -1.6500e+03 -2.8571e+02 3.4130e+00 -1.6500e+03 -2.5714e+02 3.3848e+00 -1.6500e+03 -2.2857e+02 3.3594e+00 -1.6500e+03 -2.0000e+02 3.3367e+00 -1.6500e+03 -1.7143e+02 3.3169e+00 -1.6500e+03 -1.4286e+02 3.3000e+00 -1.6500e+03 -1.1429e+02 3.2861e+00 -1.6500e+03 -8.5714e+01 3.2753e+00 -1.6500e+03 -5.7143e+01 3.2675e+00 -1.6500e+03 -2.8571e+01 3.2628e+00 -1.6500e+03 0.0000e+00 3.2612e+00 -1.6500e+03 2.8571e+01 3.2628e+00 -1.6500e+03 5.7143e+01 3.2675e+00 -1.6500e+03 8.5714e+01 3.2753e+00 -1.6500e+03 1.1429e+02 3.2861e+00 -1.6500e+03 1.4286e+02 3.3000e+00 -1.6500e+03 1.7143e+02 3.3169e+00 -1.6500e+03 2.0000e+02 3.3367e+00 -1.6500e+03 2.2857e+02 3.3594e+00 -1.6500e+03 2.5714e+02 3.3848e+00 -1.6500e+03 2.8571e+02 3.4130e+00 -1.6500e+03 3.1429e+02 3.4437e+00 -1.6500e+03 3.4286e+02 3.4769e+00 -1.6500e+03 3.7143e+02 3.5125e+00 -1.6500e+03 4.0000e+02 3.5504e+00 -1.6500e+03 4.2857e+02 3.5905e+00 -1.6500e+03 4.5714e+02 3.6326e+00 -1.6500e+03 4.8571e+02 3.6766e+00 -1.6500e+03 5.1429e+02 3.7224e+00 -1.6500e+03 5.4286e+02 3.7699e+00 -1.6500e+03 5.7143e+02 3.8190e+00 -1.6500e+03 6.0000e+02 3.8695e+00 -1.6500e+03 6.2857e+02 3.9212e+00 -1.6500e+03 6.5714e+02 3.9742e+00 -1.6500e+03 6.8571e+02 4.0282e+00 -1.6500e+03 7.1429e+02 4.0832e+00 -1.6500e+03 7.4286e+02 4.1390e+00 -1.6500e+03 7.7143e+02 4.1955e+00 -1.6500e+03 8.0000e+02 4.2527e+00 -1.6500e+03 8.2857e+02 4.3103e+00 -1.6500e+03 8.5714e+02 4.3684e+00 -1.6500e+03 8.8571e+02 4.4268e+00 -1.6500e+03 9.1429e+02 4.4855e+00 -1.6500e+03 9.4286e+02 4.5443e+00 -1.6500e+03 9.7143e+02 4.6032e+00 -1.6500e+03 1.0000e+03 4.6622e+00 -1.6500e+03 1.0286e+03 4.7210e+00 -1.6500e+03 1.0571e+03 4.7797e+00 -1.6500e+03 1.0857e+03 4.8383e+00 -1.6500e+03 1.1143e+03 4.8966e+00 -1.6500e+03 1.1429e+03 4.9546e+00 -1.6500e+03 1.1714e+03 5.0122e+00 -1.6500e+03 1.2000e+03 5.0695e+00 -1.6500e+03 1.2286e+03 5.1263e+00 -1.6500e+03 1.2571e+03 5.1827e+00 -1.6500e+03 1.2857e+03 5.2386e+00 -1.6500e+03 1.3143e+03 5.2940e+00 -1.6500e+03 1.3429e+03 5.3488e+00 -1.6500e+03 1.3714e+03 5.4030e+00 -1.6500e+03 1.4000e+03 5.4566e+00 -1.6500e+03 1.4286e+03 5.5096e+00 -1.6500e+03 1.4571e+03 5.5620e+00 -1.6500e+03 1.4857e+03 5.6138e+00 -1.6500e+03 1.5143e+03 5.6649e+00 -1.6500e+03 1.5429e+03 5.7153e+00 -1.6500e+03 1.5714e+03 5.7651e+00 -1.6500e+03 1.6000e+03 5.8141e+00 -1.6500e+03 1.6286e+03 5.8625e+00 -1.6500e+03 1.6571e+03 5.9102e+00 -1.6500e+03 1.6857e+03 5.9572e+00 -1.6500e+03 1.7143e+03 6.0036e+00 -1.6500e+03 1.7429e+03 6.0492e+00 -1.6500e+03 1.7714e+03 6.0942e+00 -1.6500e+03 1.8000e+03 6.1385e+00 -1.6500e+03 1.8286e+03 6.1821e+00 -1.6500e+03 1.8571e+03 6.2250e+00 -1.6500e+03 1.8857e+03 6.2672e+00 -1.6500e+03 1.9143e+03 6.3088e+00 -1.6500e+03 1.9429e+03 6.3498e+00 -1.6500e+03 1.9714e+03 6.3900e+00 -1.6500e+03 2.0000e+03 6.4297e+00 -1.6800e+03 -2.0000e+03 6.4636e+00 -1.6800e+03 -1.9714e+03 6.4250e+00 -1.6800e+03 -1.9429e+03 6.3858e+00 -1.6800e+03 -1.9143e+03 6.3460e+00 -1.6800e+03 -1.8857e+03 6.3055e+00 -1.6800e+03 -1.8571e+03 6.2645e+00 -1.6800e+03 -1.8286e+03 6.2228e+00 -1.6800e+03 -1.8000e+03 6.1805e+00 -1.6800e+03 -1.7714e+03 6.1375e+00 -1.6800e+03 -1.7429e+03 6.0940e+00 -1.6800e+03 -1.7143e+03 6.0497e+00 -1.6800e+03 -1.6857e+03 6.0049e+00 -1.6800e+03 -1.6571e+03 5.9594e+00 -1.6800e+03 -1.6286e+03 5.9132e+00 -1.6800e+03 -1.6000e+03 5.8665e+00 -1.6800e+03 -1.5714e+03 5.8191e+00 -1.6800e+03 -1.5429e+03 5.7710e+00 -1.6800e+03 -1.5143e+03 5.7224e+00 -1.6800e+03 -1.4857e+03 5.6732e+00 -1.6800e+03 -1.4571e+03 5.6233e+00 -1.6800e+03 -1.4286e+03 5.5729e+00 -1.6800e+03 -1.4000e+03 5.5219e+00 -1.6800e+03 -1.3714e+03 5.4704e+00 -1.6800e+03 -1.3429e+03 5.4183e+00 -1.6800e+03 -1.3143e+03 5.3657e+00 -1.6800e+03 -1.2857e+03 5.3126e+00 -1.6800e+03 -1.2571e+03 5.2591e+00 -1.6800e+03 -1.2286e+03 5.2051e+00 -1.6800e+03 -1.2000e+03 5.1508e+00 -1.6800e+03 -1.1714e+03 5.0960e+00 -1.6800e+03 -1.1429e+03 5.0410e+00 -1.6800e+03 -1.1143e+03 4.9857e+00 -1.6800e+03 -1.0857e+03 4.9301e+00 -1.6800e+03 -1.0571e+03 4.8744e+00 -1.6800e+03 -1.0286e+03 4.8185e+00 -1.6800e+03 -1.0000e+03 4.7625e+00 -1.6800e+03 -9.7143e+02 4.7066e+00 -1.6800e+03 -9.4286e+02 4.6507e+00 -1.6800e+03 -9.1429e+02 4.5949e+00 -1.6800e+03 -8.8571e+02 4.5393e+00 -1.6800e+03 -8.5714e+02 4.4840e+00 -1.6800e+03 -8.2857e+02 4.4291e+00 -1.6800e+03 -8.0000e+02 4.3746e+00 -1.6800e+03 -7.7143e+02 4.3207e+00 -1.6800e+03 -7.4286e+02 4.2674e+00 -1.6800e+03 -7.1429e+02 4.2148e+00 -1.6800e+03 -6.8571e+02 4.1630e+00 -1.6800e+03 -6.5714e+02 4.1122e+00 -1.6800e+03 -6.2857e+02 4.0624e+00 -1.6800e+03 -6.0000e+02 4.0138e+00 -1.6800e+03 -5.7143e+02 3.9665e+00 -1.6800e+03 -5.4286e+02 3.9205e+00 -1.6800e+03 -5.1429e+02 3.8760e+00 -1.6800e+03 -4.8571e+02 3.8331e+00 -1.6800e+03 -4.5714e+02 3.7919e+00 -1.6800e+03 -4.2857e+02 3.7526e+00 -1.6800e+03 -4.0000e+02 3.7151e+00 -1.6800e+03 -3.7143e+02 3.6798e+00 -1.6800e+03 -3.4286e+02 3.6465e+00 -1.6800e+03 -3.1429e+02 3.6155e+00 -1.6800e+03 -2.8571e+02 3.5869e+00 -1.6800e+03 -2.5714e+02 3.5607e+00 -1.6800e+03 -2.2857e+02 3.5370e+00 -1.6800e+03 -2.0000e+02 3.5159e+00 -1.6800e+03 -1.7143e+02 3.4975e+00 -1.6800e+03 -1.4286e+02 3.4818e+00 -1.6800e+03 -1.1429e+02 3.4688e+00 -1.6800e+03 -8.5714e+01 3.4587e+00 -1.6800e+03 -5.7143e+01 3.4515e+00 -1.6800e+03 -2.8571e+01 3.4471e+00 -1.6800e+03 0.0000e+00 3.4457e+00 -1.6800e+03 2.8571e+01 3.4471e+00 -1.6800e+03 5.7143e+01 3.4515e+00 -1.6800e+03 8.5714e+01 3.4587e+00 -1.6800e+03 1.1429e+02 3.4688e+00 -1.6800e+03 1.4286e+02 3.4818e+00 -1.6800e+03 1.7143e+02 3.4975e+00 -1.6800e+03 2.0000e+02 3.5159e+00 -1.6800e+03 2.2857e+02 3.5370e+00 -1.6800e+03 2.5714e+02 3.5607e+00 -1.6800e+03 2.8571e+02 3.5869e+00 -1.6800e+03 3.1429e+02 3.6155e+00 -1.6800e+03 3.4286e+02 3.6465e+00 -1.6800e+03 3.7143e+02 3.6798e+00 -1.6800e+03 4.0000e+02 3.7151e+00 -1.6800e+03 4.2857e+02 3.7526e+00 -1.6800e+03 4.5714e+02 3.7919e+00 -1.6800e+03 4.8571e+02 3.8331e+00 -1.6800e+03 5.1429e+02 3.8760e+00 -1.6800e+03 5.4286e+02 3.9205e+00 -1.6800e+03 5.7143e+02 3.9665e+00 -1.6800e+03 6.0000e+02 4.0138e+00 -1.6800e+03 6.2857e+02 4.0624e+00 -1.6800e+03 6.5714e+02 4.1122e+00 -1.6800e+03 6.8571e+02 4.1630e+00 -1.6800e+03 7.1429e+02 4.2148e+00 -1.6800e+03 7.4286e+02 4.2674e+00 -1.6800e+03 7.7143e+02 4.3207e+00 -1.6800e+03 8.0000e+02 4.3746e+00 -1.6800e+03 8.2857e+02 4.4291e+00 -1.6800e+03 8.5714e+02 4.4840e+00 -1.6800e+03 8.8571e+02 4.5393e+00 -1.6800e+03 9.1429e+02 4.5949e+00 -1.6800e+03 9.4286e+02 4.6507e+00 -1.6800e+03 9.7143e+02 4.7066e+00 -1.6800e+03 1.0000e+03 4.7625e+00 -1.6800e+03 1.0286e+03 4.8185e+00 -1.6800e+03 1.0571e+03 4.8744e+00 -1.6800e+03 1.0857e+03 4.9301e+00 -1.6800e+03 1.1143e+03 4.9857e+00 -1.6800e+03 1.1429e+03 5.0410e+00 -1.6800e+03 1.1714e+03 5.0960e+00 -1.6800e+03 1.2000e+03 5.1508e+00 -1.6800e+03 1.2286e+03 5.2051e+00 -1.6800e+03 1.2571e+03 5.2591e+00 -1.6800e+03 1.2857e+03 5.3126e+00 -1.6800e+03 1.3143e+03 5.3657e+00 -1.6800e+03 1.3429e+03 5.4183e+00 -1.6800e+03 1.3714e+03 5.4704e+00 -1.6800e+03 1.4000e+03 5.5219e+00 -1.6800e+03 1.4286e+03 5.5729e+00 -1.6800e+03 1.4571e+03 5.6233e+00 -1.6800e+03 1.4857e+03 5.6732e+00 -1.6800e+03 1.5143e+03 5.7224e+00 -1.6800e+03 1.5429e+03 5.7710e+00 -1.6800e+03 1.5714e+03 5.8191e+00 -1.6800e+03 1.6000e+03 5.8665e+00 -1.6800e+03 1.6286e+03 5.9132e+00 -1.6800e+03 1.6571e+03 5.9594e+00 -1.6800e+03 1.6857e+03 6.0049e+00 -1.6800e+03 1.7143e+03 6.0497e+00 -1.6800e+03 1.7429e+03 6.0940e+00 -1.6800e+03 1.7714e+03 6.1375e+00 -1.6800e+03 1.8000e+03 6.1805e+00 -1.6800e+03 1.8286e+03 6.2228e+00 -1.6800e+03 1.8571e+03 6.2645e+00 -1.6800e+03 1.8857e+03 6.3055e+00 -1.6800e+03 1.9143e+03 6.3460e+00 -1.6800e+03 1.9429e+03 6.3858e+00 -1.6800e+03 1.9714e+03 6.4250e+00 -1.6800e+03 2.0000e+03 6.4636e+00 -1.7100e+03 -2.0000e+03 6.4972e+00 -1.7100e+03 -1.9714e+03 6.4596e+00 -1.7100e+03 -1.9429e+03 6.4215e+00 -1.7100e+03 -1.9143e+03 6.3828e+00 -1.7100e+03 -1.8857e+03 6.3435e+00 -1.7100e+03 -1.8571e+03 6.3036e+00 -1.7100e+03 -1.8286e+03 6.2631e+00 -1.7100e+03 -1.8000e+03 6.2220e+00 -1.7100e+03 -1.7714e+03 6.1803e+00 -1.7100e+03 -1.7429e+03 6.1381e+00 -1.7100e+03 -1.7143e+03 6.0952e+00 -1.7100e+03 -1.6857e+03 6.0518e+00 -1.7100e+03 -1.6571e+03 6.0078e+00 -1.7100e+03 -1.6286e+03 5.9631e+00 -1.7100e+03 -1.6000e+03 5.9179e+00 -1.7100e+03 -1.5714e+03 5.8722e+00 -1.7100e+03 -1.5429e+03 5.8258e+00 -1.7100e+03 -1.5143e+03 5.7789e+00 -1.7100e+03 -1.4857e+03 5.7314e+00 -1.7100e+03 -1.4571e+03 5.6834e+00 -1.7100e+03 -1.4286e+03 5.6349e+00 -1.7100e+03 -1.4000e+03 5.5858e+00 -1.7100e+03 -1.3714e+03 5.5363e+00 -1.7100e+03 -1.3429e+03 5.4863e+00 -1.7100e+03 -1.3143e+03 5.4358e+00 -1.7100e+03 -1.2857e+03 5.3849e+00 -1.7100e+03 -1.2571e+03 5.3336e+00 -1.7100e+03 -1.2286e+03 5.2819e+00 -1.7100e+03 -1.2000e+03 5.2299e+00 -1.7100e+03 -1.1714e+03 5.1776e+00 -1.7100e+03 -1.1429e+03 5.1250e+00 -1.7100e+03 -1.1143e+03 5.0722e+00 -1.7100e+03 -1.0857e+03 5.0192e+00 -1.7100e+03 -1.0571e+03 4.9661e+00 -1.7100e+03 -1.0286e+03 4.9129e+00 -1.7100e+03 -1.0000e+03 4.8596e+00 -1.7100e+03 -9.7143e+02 4.8065e+00 -1.7100e+03 -9.4286e+02 4.7534e+00 -1.7100e+03 -9.1429e+02 4.7005e+00 -1.7100e+03 -8.8571e+02 4.6478e+00 -1.7100e+03 -8.5714e+02 4.5954e+00 -1.7100e+03 -8.2857e+02 4.5434e+00 -1.7100e+03 -8.0000e+02 4.4919e+00 -1.7100e+03 -7.7143e+02 4.4409e+00 -1.7100e+03 -7.4286e+02 4.3906e+00 -1.7100e+03 -7.1429e+02 4.3410e+00 -1.7100e+03 -6.8571e+02 4.2922e+00 -1.7100e+03 -6.5714e+02 4.2444e+00 -1.7100e+03 -6.2857e+02 4.1976e+00 -1.7100e+03 -6.0000e+02 4.1519e+00 -1.7100e+03 -5.7143e+02 4.1074e+00 -1.7100e+03 -5.4286e+02 4.0642e+00 -1.7100e+03 -5.1429e+02 4.0225e+00 -1.7100e+03 -4.8571e+02 3.9823e+00 -1.7100e+03 -4.5714e+02 3.9437e+00 -1.7100e+03 -4.2857e+02 3.9068e+00 -1.7100e+03 -4.0000e+02 3.8718e+00 -1.7100e+03 -3.7143e+02 3.8388e+00 -1.7100e+03 -3.4286e+02 3.8077e+00 -1.7100e+03 -3.1429e+02 3.7788e+00 -1.7100e+03 -2.8571e+02 3.7520e+00 -1.7100e+03 -2.5714e+02 3.7276e+00 -1.7100e+03 -2.2857e+02 3.7055e+00 -1.7100e+03 -2.0000e+02 3.6858e+00 -1.7100e+03 -1.7143e+02 3.6686e+00 -1.7100e+03 -1.4286e+02 3.6540e+00 -1.7100e+03 -1.1429e+02 3.6419e+00 -1.7100e+03 -8.5714e+01 3.6325e+00 -1.7100e+03 -5.7143e+01 3.6258e+00 -1.7100e+03 -2.8571e+01 3.6217e+00 -1.7100e+03 0.0000e+00 3.6204e+00 -1.7100e+03 2.8571e+01 3.6217e+00 -1.7100e+03 5.7143e+01 3.6258e+00 -1.7100e+03 8.5714e+01 3.6325e+00 -1.7100e+03 1.1429e+02 3.6419e+00 -1.7100e+03 1.4286e+02 3.6540e+00 -1.7100e+03 1.7143e+02 3.6686e+00 -1.7100e+03 2.0000e+02 3.6858e+00 -1.7100e+03 2.2857e+02 3.7055e+00 -1.7100e+03 2.5714e+02 3.7276e+00 -1.7100e+03 2.8571e+02 3.7520e+00 -1.7100e+03 3.1429e+02 3.7788e+00 -1.7100e+03 3.4286e+02 3.8077e+00 -1.7100e+03 3.7143e+02 3.8388e+00 -1.7100e+03 4.0000e+02 3.8718e+00 -1.7100e+03 4.2857e+02 3.9068e+00 -1.7100e+03 4.5714e+02 3.9437e+00 -1.7100e+03 4.8571e+02 3.9823e+00 -1.7100e+03 5.1429e+02 4.0225e+00 -1.7100e+03 5.4286e+02 4.0642e+00 -1.7100e+03 5.7143e+02 4.1074e+00 -1.7100e+03 6.0000e+02 4.1519e+00 -1.7100e+03 6.2857e+02 4.1976e+00 -1.7100e+03 6.5714e+02 4.2444e+00 -1.7100e+03 6.8571e+02 4.2922e+00 -1.7100e+03 7.1429e+02 4.3410e+00 -1.7100e+03 7.4286e+02 4.3906e+00 -1.7100e+03 7.7143e+02 4.4409e+00 -1.7100e+03 8.0000e+02 4.4919e+00 -1.7100e+03 8.2857e+02 4.5434e+00 -1.7100e+03 8.5714e+02 4.5954e+00 -1.7100e+03 8.8571e+02 4.6478e+00 -1.7100e+03 9.1429e+02 4.7005e+00 -1.7100e+03 9.4286e+02 4.7534e+00 -1.7100e+03 9.7143e+02 4.8065e+00 -1.7100e+03 1.0000e+03 4.8596e+00 -1.7100e+03 1.0286e+03 4.9129e+00 -1.7100e+03 1.0571e+03 4.9661e+00 -1.7100e+03 1.0857e+03 5.0192e+00 -1.7100e+03 1.1143e+03 5.0722e+00 -1.7100e+03 1.1429e+03 5.1250e+00 -1.7100e+03 1.1714e+03 5.1776e+00 -1.7100e+03 1.2000e+03 5.2299e+00 -1.7100e+03 1.2286e+03 5.2819e+00 -1.7100e+03 1.2571e+03 5.3336e+00 -1.7100e+03 1.2857e+03 5.3849e+00 -1.7100e+03 1.3143e+03 5.4358e+00 -1.7100e+03 1.3429e+03 5.4863e+00 -1.7100e+03 1.3714e+03 5.5363e+00 -1.7100e+03 1.4000e+03 5.5858e+00 -1.7100e+03 1.4286e+03 5.6349e+00 -1.7100e+03 1.4571e+03 5.6834e+00 -1.7100e+03 1.4857e+03 5.7314e+00 -1.7100e+03 1.5143e+03 5.7789e+00 -1.7100e+03 1.5429e+03 5.8258e+00 -1.7100e+03 1.5714e+03 5.8722e+00 -1.7100e+03 1.6000e+03 5.9179e+00 -1.7100e+03 1.6286e+03 5.9631e+00 -1.7100e+03 1.6571e+03 6.0078e+00 -1.7100e+03 1.6857e+03 6.0518e+00 -1.7100e+03 1.7143e+03 6.0952e+00 -1.7100e+03 1.7429e+03 6.1381e+00 -1.7100e+03 1.7714e+03 6.1803e+00 -1.7100e+03 1.8000e+03 6.2220e+00 -1.7100e+03 1.8286e+03 6.2631e+00 -1.7100e+03 1.8571e+03 6.3036e+00 -1.7100e+03 1.8857e+03 6.3435e+00 -1.7100e+03 1.9143e+03 6.3828e+00 -1.7100e+03 1.9429e+03 6.4215e+00 -1.7100e+03 1.9714e+03 6.4596e+00 -1.7100e+03 2.0000e+03 6.4972e+00 -1.7400e+03 -2.0000e+03 6.5305e+00 -1.7400e+03 -1.9714e+03 6.4940e+00 -1.7400e+03 -1.9429e+03 6.4568e+00 -1.7400e+03 -1.9143e+03 6.4192e+00 -1.7400e+03 -1.8857e+03 6.3810e+00 -1.7400e+03 -1.8571e+03 6.3422e+00 -1.7400e+03 -1.8286e+03 6.3029e+00 -1.7400e+03 -1.8000e+03 6.2630e+00 -1.7400e+03 -1.7714e+03 6.2226e+00 -1.7400e+03 -1.7429e+03 6.1816e+00 -1.7400e+03 -1.7143e+03 6.1401e+00 -1.7400e+03 -1.6857e+03 6.0980e+00 -1.7400e+03 -1.6571e+03 6.0554e+00 -1.7400e+03 -1.6286e+03 6.0123e+00 -1.7400e+03 -1.6000e+03 5.9686e+00 -1.7400e+03 -1.5714e+03 5.9243e+00 -1.7400e+03 -1.5429e+03 5.8796e+00 -1.7400e+03 -1.5143e+03 5.8343e+00 -1.7400e+03 -1.4857e+03 5.7886e+00 -1.7400e+03 -1.4571e+03 5.7423e+00 -1.7400e+03 -1.4286e+03 5.6956e+00 -1.7400e+03 -1.4000e+03 5.6484e+00 -1.7400e+03 -1.3714e+03 5.6008e+00 -1.7400e+03 -1.3429e+03 5.5527e+00 -1.7400e+03 -1.3143e+03 5.5042e+00 -1.7400e+03 -1.2857e+03 5.4554e+00 -1.7400e+03 -1.2571e+03 5.4062e+00 -1.7400e+03 -1.2286e+03 5.3567e+00 -1.7400e+03 -1.2000e+03 5.3069e+00 -1.7400e+03 -1.1714e+03 5.2569e+00 -1.7400e+03 -1.1429e+03 5.2066e+00 -1.7400e+03 -1.1143e+03 5.1562e+00 -1.7400e+03 -1.0857e+03 5.1056e+00 -1.7400e+03 -1.0571e+03 5.0550e+00 -1.7400e+03 -1.0286e+03 5.0043e+00 -1.7400e+03 -1.0000e+03 4.9537e+00 -1.7400e+03 -9.7143e+02 4.9031e+00 -1.7400e+03 -9.4286e+02 4.8526e+00 -1.7400e+03 -9.1429e+02 4.8024e+00 -1.7400e+03 -8.8571e+02 4.7524e+00 -1.7400e+03 -8.5714e+02 4.7028e+00 -1.7400e+03 -8.2857e+02 4.6535e+00 -1.7400e+03 -8.0000e+02 4.6048e+00 -1.7400e+03 -7.7143e+02 4.5566e+00 -1.7400e+03 -7.4286e+02 4.5090e+00 -1.7400e+03 -7.1429e+02 4.4622e+00 -1.7400e+03 -6.8571e+02 4.4162e+00 -1.7400e+03 -6.5714e+02 4.3711e+00 -1.7400e+03 -6.2857e+02 4.3270e+00 -1.7400e+03 -6.0000e+02 4.2840e+00 -1.7400e+03 -5.7143e+02 4.2421e+00 -1.7400e+03 -5.4286e+02 4.2016e+00 -1.7400e+03 -5.1429e+02 4.1624e+00 -1.7400e+03 -4.8571e+02 4.1246e+00 -1.7400e+03 -4.5714e+02 4.0884e+00 -1.7400e+03 -4.2857e+02 4.0539e+00 -1.7400e+03 -4.0000e+02 4.0211e+00 -1.7400e+03 -3.7143e+02 3.9901e+00 -1.7400e+03 -3.4286e+02 3.9611e+00 -1.7400e+03 -3.1429e+02 3.9340e+00 -1.7400e+03 -2.8571e+02 3.9090e+00 -1.7400e+03 -2.5714e+02 3.8861e+00 -1.7400e+03 -2.2857e+02 3.8655e+00 -1.7400e+03 -2.0000e+02 3.8471e+00 -1.7400e+03 -1.7143e+02 3.8311e+00 -1.7400e+03 -1.4286e+02 3.8174e+00 -1.7400e+03 -1.1429e+02 3.8062e+00 -1.7400e+03 -8.5714e+01 3.7974e+00 -1.7400e+03 -5.7143e+01 3.7911e+00 -1.7400e+03 -2.8571e+01 3.7873e+00 -1.7400e+03 0.0000e+00 3.7861e+00 -1.7400e+03 2.8571e+01 3.7873e+00 -1.7400e+03 5.7143e+01 3.7911e+00 -1.7400e+03 8.5714e+01 3.7974e+00 -1.7400e+03 1.1429e+02 3.8062e+00 -1.7400e+03 1.4286e+02 3.8174e+00 -1.7400e+03 1.7143e+02 3.8311e+00 -1.7400e+03 2.0000e+02 3.8471e+00 -1.7400e+03 2.2857e+02 3.8655e+00 -1.7400e+03 2.5714e+02 3.8861e+00 -1.7400e+03 2.8571e+02 3.9090e+00 -1.7400e+03 3.1429e+02 3.9340e+00 -1.7400e+03 3.4286e+02 3.9611e+00 -1.7400e+03 3.7143e+02 3.9901e+00 -1.7400e+03 4.0000e+02 4.0211e+00 -1.7400e+03 4.2857e+02 4.0539e+00 -1.7400e+03 4.5714e+02 4.0884e+00 -1.7400e+03 4.8571e+02 4.1246e+00 -1.7400e+03 5.1429e+02 4.1624e+00 -1.7400e+03 5.4286e+02 4.2016e+00 -1.7400e+03 5.7143e+02 4.2421e+00 -1.7400e+03 6.0000e+02 4.2840e+00 -1.7400e+03 6.2857e+02 4.3270e+00 -1.7400e+03 6.5714e+02 4.3711e+00 -1.7400e+03 6.8571e+02 4.4162e+00 -1.7400e+03 7.1429e+02 4.4622e+00 -1.7400e+03 7.4286e+02 4.5090e+00 -1.7400e+03 7.7143e+02 4.5566e+00 -1.7400e+03 8.0000e+02 4.6048e+00 -1.7400e+03 8.2857e+02 4.6535e+00 -1.7400e+03 8.5714e+02 4.7028e+00 -1.7400e+03 8.8571e+02 4.7524e+00 -1.7400e+03 9.1429e+02 4.8024e+00 -1.7400e+03 9.4286e+02 4.8526e+00 -1.7400e+03 9.7143e+02 4.9031e+00 -1.7400e+03 1.0000e+03 4.9537e+00 -1.7400e+03 1.0286e+03 5.0043e+00 -1.7400e+03 1.0571e+03 5.0550e+00 -1.7400e+03 1.0857e+03 5.1056e+00 -1.7400e+03 1.1143e+03 5.1562e+00 -1.7400e+03 1.1429e+03 5.2066e+00 -1.7400e+03 1.1714e+03 5.2569e+00 -1.7400e+03 1.2000e+03 5.3069e+00 -1.7400e+03 1.2286e+03 5.3567e+00 -1.7400e+03 1.2571e+03 5.4062e+00 -1.7400e+03 1.2857e+03 5.4554e+00 -1.7400e+03 1.3143e+03 5.5042e+00 -1.7400e+03 1.3429e+03 5.5527e+00 -1.7400e+03 1.3714e+03 5.6008e+00 -1.7400e+03 1.4000e+03 5.6484e+00 -1.7400e+03 1.4286e+03 5.6956e+00 -1.7400e+03 1.4571e+03 5.7423e+00 -1.7400e+03 1.4857e+03 5.7886e+00 -1.7400e+03 1.5143e+03 5.8343e+00 -1.7400e+03 1.5429e+03 5.8796e+00 -1.7400e+03 1.5714e+03 5.9243e+00 -1.7400e+03 1.6000e+03 5.9686e+00 -1.7400e+03 1.6286e+03 6.0123e+00 -1.7400e+03 1.6571e+03 6.0554e+00 -1.7400e+03 1.6857e+03 6.0980e+00 -1.7400e+03 1.7143e+03 6.1401e+00 -1.7400e+03 1.7429e+03 6.1816e+00 -1.7400e+03 1.7714e+03 6.2226e+00 -1.7400e+03 1.8000e+03 6.2630e+00 -1.7400e+03 1.8286e+03 6.3029e+00 -1.7400e+03 1.8571e+03 6.3422e+00 -1.7400e+03 1.8857e+03 6.3810e+00 -1.7400e+03 1.9143e+03 6.4192e+00 -1.7400e+03 1.9429e+03 6.4568e+00 -1.7400e+03 1.9714e+03 6.4940e+00 -1.7400e+03 2.0000e+03 6.5305e+00 -1.7700e+03 -2.0000e+03 6.5636e+00 -1.7700e+03 -1.9714e+03 6.5279e+00 -1.7700e+03 -1.9429e+03 6.4918e+00 -1.7700e+03 -1.9143e+03 6.4552e+00 -1.7700e+03 -1.8857e+03 6.4180e+00 -1.7700e+03 -1.8571e+03 6.3804e+00 -1.7700e+03 -1.8286e+03 6.3422e+00 -1.7700e+03 -1.8000e+03 6.3035e+00 -1.7700e+03 -1.7714e+03 6.2643e+00 -1.7700e+03 -1.7429e+03 6.2246e+00 -1.7700e+03 -1.7143e+03 6.1843e+00 -1.7700e+03 -1.6857e+03 6.1436e+00 -1.7700e+03 -1.6571e+03 6.1023e+00 -1.7700e+03 -1.6286e+03 6.0606e+00 -1.7700e+03 -1.6000e+03 6.0183e+00 -1.7700e+03 -1.5714e+03 5.9756e+00 -1.7700e+03 -1.5429e+03 5.9324e+00 -1.7700e+03 -1.5143e+03 5.8887e+00 -1.7700e+03 -1.4857e+03 5.8446e+00 -1.7700e+03 -1.4571e+03 5.8000e+00 -1.7700e+03 -1.4286e+03 5.7550e+00 -1.7700e+03 -1.4000e+03 5.7096e+00 -1.7700e+03 -1.3714e+03 5.6638e+00 -1.7700e+03 -1.3429e+03 5.6176e+00 -1.7700e+03 -1.3143e+03 5.5711e+00 -1.7700e+03 -1.2857e+03 5.5242e+00 -1.7700e+03 -1.2571e+03 5.4770e+00 -1.7700e+03 -1.2286e+03 5.4296e+00 -1.7700e+03 -1.2000e+03 5.3819e+00 -1.7700e+03 -1.1714e+03 5.3340e+00 -1.7700e+03 -1.1429e+03 5.2860e+00 -1.7700e+03 -1.1143e+03 5.2378e+00 -1.7700e+03 -1.0857e+03 5.1895e+00 -1.7700e+03 -1.0571e+03 5.1412e+00 -1.7700e+03 -1.0286e+03 5.0929e+00 -1.7700e+03 -1.0000e+03 5.0447e+00 -1.7700e+03 -9.7143e+02 4.9966e+00 -1.7700e+03 -9.4286e+02 4.9486e+00 -1.7700e+03 -9.1429e+02 4.9008e+00 -1.7700e+03 -8.8571e+02 4.8534e+00 -1.7700e+03 -8.5714e+02 4.8063e+00 -1.7700e+03 -8.2857e+02 4.7596e+00 -1.7700e+03 -8.0000e+02 4.7134e+00 -1.7700e+03 -7.7143e+02 4.6678e+00 -1.7700e+03 -7.4286e+02 4.6229e+00 -1.7700e+03 -7.1429e+02 4.5786e+00 -1.7700e+03 -6.8571e+02 4.5352e+00 -1.7700e+03 -6.5714e+02 4.4926e+00 -1.7700e+03 -6.2857e+02 4.4510e+00 -1.7700e+03 -6.0000e+02 4.4105e+00 -1.7700e+03 -5.7143e+02 4.3711e+00 -1.7700e+03 -5.4286e+02 4.3329e+00 -1.7700e+03 -5.1429e+02 4.2960e+00 -1.7700e+03 -4.8571e+02 4.2606e+00 -1.7700e+03 -4.5714e+02 4.2266e+00 -1.7700e+03 -4.2857e+02 4.1942e+00 -1.7700e+03 -4.0000e+02 4.1634e+00 -1.7700e+03 -3.7143e+02 4.1344e+00 -1.7700e+03 -3.4286e+02 4.1071e+00 -1.7700e+03 -3.1429e+02 4.0818e+00 -1.7700e+03 -2.8571e+02 4.0584e+00 -1.7700e+03 -2.5714e+02 4.0370e+00 -1.7700e+03 -2.2857e+02 4.0177e+00 -1.7700e+03 -2.0000e+02 4.0005e+00 -1.7700e+03 -1.7143e+02 3.9855e+00 -1.7700e+03 -1.4286e+02 3.9727e+00 -1.7700e+03 -1.1429e+02 3.9622e+00 -1.7700e+03 -8.5714e+01 3.9540e+00 -1.7700e+03 -5.7143e+01 3.9481e+00 -1.7700e+03 -2.8571e+01 3.9446e+00 -1.7700e+03 0.0000e+00 3.9434e+00 -1.7700e+03 2.8571e+01 3.9446e+00 -1.7700e+03 5.7143e+01 3.9481e+00 -1.7700e+03 8.5714e+01 3.9540e+00 -1.7700e+03 1.1429e+02 3.9622e+00 -1.7700e+03 1.4286e+02 3.9727e+00 -1.7700e+03 1.7143e+02 3.9855e+00 -1.7700e+03 2.0000e+02 4.0005e+00 -1.7700e+03 2.2857e+02 4.0177e+00 -1.7700e+03 2.5714e+02 4.0370e+00 -1.7700e+03 2.8571e+02 4.0584e+00 -1.7700e+03 3.1429e+02 4.0818e+00 -1.7700e+03 3.4286e+02 4.1071e+00 -1.7700e+03 3.7143e+02 4.1344e+00 -1.7700e+03 4.0000e+02 4.1634e+00 -1.7700e+03 4.2857e+02 4.1942e+00 -1.7700e+03 4.5714e+02 4.2266e+00 -1.7700e+03 4.8571e+02 4.2606e+00 -1.7700e+03 5.1429e+02 4.2960e+00 -1.7700e+03 5.4286e+02 4.3329e+00 -1.7700e+03 5.7143e+02 4.3711e+00 -1.7700e+03 6.0000e+02 4.4105e+00 -1.7700e+03 6.2857e+02 4.4510e+00 -1.7700e+03 6.5714e+02 4.4926e+00 -1.7700e+03 6.8571e+02 4.5352e+00 -1.7700e+03 7.1429e+02 4.5786e+00 -1.7700e+03 7.4286e+02 4.6229e+00 -1.7700e+03 7.7143e+02 4.6678e+00 -1.7700e+03 8.0000e+02 4.7134e+00 -1.7700e+03 8.2857e+02 4.7596e+00 -1.7700e+03 8.5714e+02 4.8063e+00 -1.7700e+03 8.8571e+02 4.8534e+00 -1.7700e+03 9.1429e+02 4.9008e+00 -1.7700e+03 9.4286e+02 4.9486e+00 -1.7700e+03 9.7143e+02 4.9966e+00 -1.7700e+03 1.0000e+03 5.0447e+00 -1.7700e+03 1.0286e+03 5.0929e+00 -1.7700e+03 1.0571e+03 5.1412e+00 -1.7700e+03 1.0857e+03 5.1895e+00 -1.7700e+03 1.1143e+03 5.2378e+00 -1.7700e+03 1.1429e+03 5.2860e+00 -1.7700e+03 1.1714e+03 5.3340e+00 -1.7700e+03 1.2000e+03 5.3819e+00 -1.7700e+03 1.2286e+03 5.4296e+00 -1.7700e+03 1.2571e+03 5.4770e+00 -1.7700e+03 1.2857e+03 5.5242e+00 -1.7700e+03 1.3143e+03 5.5711e+00 -1.7700e+03 1.3429e+03 5.6176e+00 -1.7700e+03 1.3714e+03 5.6638e+00 -1.7700e+03 1.4000e+03 5.7096e+00 -1.7700e+03 1.4286e+03 5.7550e+00 -1.7700e+03 1.4571e+03 5.8000e+00 -1.7700e+03 1.4857e+03 5.8446e+00 -1.7700e+03 1.5143e+03 5.8887e+00 -1.7700e+03 1.5429e+03 5.9324e+00 -1.7700e+03 1.5714e+03 5.9756e+00 -1.7700e+03 1.6000e+03 6.0183e+00 -1.7700e+03 1.6286e+03 6.0606e+00 -1.7700e+03 1.6571e+03 6.1023e+00 -1.7700e+03 1.6857e+03 6.1436e+00 -1.7700e+03 1.7143e+03 6.1843e+00 -1.7700e+03 1.7429e+03 6.2246e+00 -1.7700e+03 1.7714e+03 6.2643e+00 -1.7700e+03 1.8000e+03 6.3035e+00 -1.7700e+03 1.8286e+03 6.3422e+00 -1.7700e+03 1.8571e+03 6.3804e+00 -1.7700e+03 1.8857e+03 6.4180e+00 -1.7700e+03 1.9143e+03 6.4552e+00 -1.7700e+03 1.9429e+03 6.4918e+00 -1.7700e+03 1.9714e+03 6.5279e+00 -1.7700e+03 2.0000e+03 6.5636e+00 -1.8000e+03 -2.0000e+03 6.5963e+00 -1.8000e+03 -1.9714e+03 6.5616e+00 -1.8000e+03 -1.9429e+03 6.5264e+00 -1.8000e+03 -1.9143e+03 6.4908e+00 -1.8000e+03 -1.8857e+03 6.4547e+00 -1.8000e+03 -1.8571e+03 6.4181e+00 -1.8000e+03 -1.8286e+03 6.3810e+00 -1.8000e+03 -1.8000e+03 6.3434e+00 -1.8000e+03 -1.7714e+03 6.3054e+00 -1.8000e+03 -1.7429e+03 6.2669e+00 -1.8000e+03 -1.7143e+03 6.2279e+00 -1.8000e+03 -1.6857e+03 6.1884e+00 -1.8000e+03 -1.6571e+03 6.1485e+00 -1.8000e+03 -1.6286e+03 6.1081e+00 -1.8000e+03 -1.6000e+03 6.0673e+00 -1.8000e+03 -1.5714e+03 6.0260e+00 -1.8000e+03 -1.5429e+03 5.9843e+00 -1.8000e+03 -1.5143e+03 5.9421e+00 -1.8000e+03 -1.4857e+03 5.8995e+00 -1.8000e+03 -1.4571e+03 5.8566e+00 -1.8000e+03 -1.4286e+03 5.8132e+00 -1.8000e+03 -1.4000e+03 5.7695e+00 -1.8000e+03 -1.3714e+03 5.7254e+00 -1.8000e+03 -1.3429e+03 5.6810e+00 -1.8000e+03 -1.3143e+03 5.6363e+00 -1.8000e+03 -1.2857e+03 5.5913e+00 -1.8000e+03 -1.2571e+03 5.5461e+00 -1.8000e+03 -1.2286e+03 5.5006e+00 -1.8000e+03 -1.2000e+03 5.4550e+00 -1.8000e+03 -1.1714e+03 5.4091e+00 -1.8000e+03 -1.1429e+03 5.3632e+00 -1.8000e+03 -1.1143e+03 5.3171e+00 -1.8000e+03 -1.0857e+03 5.2710e+00 -1.8000e+03 -1.0571e+03 5.2249e+00 -1.8000e+03 -1.0286e+03 5.1788e+00 -1.8000e+03 -1.0000e+03 5.1329e+00 -1.8000e+03 -9.7143e+02 5.0870e+00 -1.8000e+03 -9.4286e+02 5.0414e+00 -1.8000e+03 -9.1429e+02 4.9960e+00 -1.8000e+03 -8.8571e+02 4.9509e+00 -1.8000e+03 -8.5714e+02 4.9062e+00 -1.8000e+03 -8.2857e+02 4.8619e+00 -1.8000e+03 -8.0000e+02 4.8181e+00 -1.8000e+03 -7.7143e+02 4.7749e+00 -1.8000e+03 -7.4286e+02 4.7324e+00 -1.8000e+03 -7.1429e+02 4.6905e+00 -1.8000e+03 -6.8571e+02 4.6494e+00 -1.8000e+03 -6.5714e+02 4.6092e+00 -1.8000e+03 -6.2857e+02 4.5700e+00 -1.8000e+03 -6.0000e+02 4.5318e+00 -1.8000e+03 -5.7143e+02 4.4946e+00 -1.8000e+03 -5.4286e+02 4.4587e+00 -1.8000e+03 -5.1429e+02 4.4240e+00 -1.8000e+03 -4.8571e+02 4.3906e+00 -1.8000e+03 -4.5714e+02 4.3586e+00 -1.8000e+03 -4.2857e+02 4.3282e+00 -1.8000e+03 -4.0000e+02 4.2993e+00 -1.8000e+03 -3.7143e+02 4.2720e+00 -1.8000e+03 -3.4286e+02 4.2464e+00 -1.8000e+03 -3.1429e+02 4.2226e+00 -1.8000e+03 -2.8571e+02 4.2007e+00 -1.8000e+03 -2.5714e+02 4.1807e+00 -1.8000e+03 -2.2857e+02 4.1626e+00 -1.8000e+03 -2.0000e+02 4.1465e+00 -1.8000e+03 -1.7143e+02 4.1324e+00 -1.8000e+03 -1.4286e+02 4.1205e+00 -1.8000e+03 -1.1429e+02 4.1106e+00 -1.8000e+03 -8.5714e+01 4.1030e+00 -1.8000e+03 -5.7143e+01 4.0975e+00 -1.8000e+03 -2.8571e+01 4.0942e+00 -1.8000e+03 0.0000e+00 4.0931e+00 -1.8000e+03 2.8571e+01 4.0942e+00 -1.8000e+03 5.7143e+01 4.0975e+00 -1.8000e+03 8.5714e+01 4.1030e+00 -1.8000e+03 1.1429e+02 4.1106e+00 -1.8000e+03 1.4286e+02 4.1205e+00 -1.8000e+03 1.7143e+02 4.1324e+00 -1.8000e+03 2.0000e+02 4.1465e+00 -1.8000e+03 2.2857e+02 4.1626e+00 -1.8000e+03 2.5714e+02 4.1807e+00 -1.8000e+03 2.8571e+02 4.2007e+00 -1.8000e+03 3.1429e+02 4.2226e+00 -1.8000e+03 3.4286e+02 4.2464e+00 -1.8000e+03 3.7143e+02 4.2720e+00 -1.8000e+03 4.0000e+02 4.2993e+00 -1.8000e+03 4.2857e+02 4.3282e+00 -1.8000e+03 4.5714e+02 4.3586e+00 -1.8000e+03 4.8571e+02 4.3906e+00 -1.8000e+03 5.1429e+02 4.4240e+00 -1.8000e+03 5.4286e+02 4.4587e+00 -1.8000e+03 5.7143e+02 4.4946e+00 -1.8000e+03 6.0000e+02 4.5318e+00 -1.8000e+03 6.2857e+02 4.5700e+00 -1.8000e+03 6.5714e+02 4.6092e+00 -1.8000e+03 6.8571e+02 4.6494e+00 -1.8000e+03 7.1429e+02 4.6905e+00 -1.8000e+03 7.4286e+02 4.7324e+00 -1.8000e+03 7.7143e+02 4.7749e+00 -1.8000e+03 8.0000e+02 4.8181e+00 -1.8000e+03 8.2857e+02 4.8619e+00 -1.8000e+03 8.5714e+02 4.9062e+00 -1.8000e+03 8.8571e+02 4.9509e+00 -1.8000e+03 9.1429e+02 4.9960e+00 -1.8000e+03 9.4286e+02 5.0414e+00 -1.8000e+03 9.7143e+02 5.0870e+00 -1.8000e+03 1.0000e+03 5.1329e+00 -1.8000e+03 1.0286e+03 5.1788e+00 -1.8000e+03 1.0571e+03 5.2249e+00 -1.8000e+03 1.0857e+03 5.2710e+00 -1.8000e+03 1.1143e+03 5.3171e+00 -1.8000e+03 1.1429e+03 5.3632e+00 -1.8000e+03 1.1714e+03 5.4091e+00 -1.8000e+03 1.2000e+03 5.4550e+00 -1.8000e+03 1.2286e+03 5.5006e+00 -1.8000e+03 1.2571e+03 5.5461e+00 -1.8000e+03 1.2857e+03 5.5913e+00 -1.8000e+03 1.3143e+03 5.6363e+00 -1.8000e+03 1.3429e+03 5.6810e+00 -1.8000e+03 1.3714e+03 5.7254e+00 -1.8000e+03 1.4000e+03 5.7695e+00 -1.8000e+03 1.4286e+03 5.8132e+00 -1.8000e+03 1.4571e+03 5.8566e+00 -1.8000e+03 1.4857e+03 5.8995e+00 -1.8000e+03 1.5143e+03 5.9421e+00 -1.8000e+03 1.5429e+03 5.9843e+00 -1.8000e+03 1.5714e+03 6.0260e+00 -1.8000e+03 1.6000e+03 6.0673e+00 -1.8000e+03 1.6286e+03 6.1081e+00 -1.8000e+03 1.6571e+03 6.1485e+00 -1.8000e+03 1.6857e+03 6.1884e+00 -1.8000e+03 1.7143e+03 6.2279e+00 -1.8000e+03 1.7429e+03 6.2669e+00 -1.8000e+03 1.7714e+03 6.3054e+00 -1.8000e+03 1.8000e+03 6.3434e+00 -1.8000e+03 1.8286e+03 6.3810e+00 -1.8000e+03 1.8571e+03 6.4181e+00 -1.8000e+03 1.8857e+03 6.4547e+00 -1.8000e+03 1.9143e+03 6.4908e+00 -1.8000e+03 1.9429e+03 6.5264e+00 -1.8000e+03 1.9714e+03 6.5616e+00 -1.8000e+03 2.0000e+03 6.5963e+00 -1.8300e+03 -2.0000e+03 6.6287e+00 -1.8300e+03 -1.9714e+03 6.5949e+00 -1.8300e+03 -1.9429e+03 6.5607e+00 -1.8300e+03 -1.9143e+03 6.5260e+00 -1.8300e+03 -1.8857e+03 6.4909e+00 -1.8300e+03 -1.8571e+03 6.4553e+00 -1.8300e+03 -1.8286e+03 6.4193e+00 -1.8300e+03 -1.8000e+03 6.3829e+00 -1.8300e+03 -1.7714e+03 6.3460e+00 -1.8300e+03 -1.7429e+03 6.3086e+00 -1.8300e+03 -1.7143e+03 6.2708e+00 -1.8300e+03 -1.6857e+03 6.2326e+00 -1.8300e+03 -1.6571e+03 6.1939e+00 -1.8300e+03 -1.6286e+03 6.1549e+00 -1.8300e+03 -1.6000e+03 6.1154e+00 -1.8300e+03 -1.5714e+03 6.0755e+00 -1.8300e+03 -1.5429e+03 6.0352e+00 -1.8300e+03 -1.5143e+03 5.9945e+00 -1.8300e+03 -1.4857e+03 5.9534e+00 -1.8300e+03 -1.4571e+03 5.9120e+00 -1.8300e+03 -1.4286e+03 5.8702e+00 -1.8300e+03 -1.4000e+03 5.8281e+00 -1.8300e+03 -1.3714e+03 5.7857e+00 -1.8300e+03 -1.3429e+03 5.7430e+00 -1.8300e+03 -1.3143e+03 5.7001e+00 -1.8300e+03 -1.2857e+03 5.6569e+00 -1.8300e+03 -1.2571e+03 5.6135e+00 -1.8300e+03 -1.2286e+03 5.5698e+00 -1.8300e+03 -1.2000e+03 5.5261e+00 -1.8300e+03 -1.1714e+03 5.4822e+00 -1.8300e+03 -1.1429e+03 5.4382e+00 -1.8300e+03 -1.1143e+03 5.3942e+00 -1.8300e+03 -1.0857e+03 5.3501e+00 -1.8300e+03 -1.0571e+03 5.3061e+00 -1.8300e+03 -1.0286e+03 5.2621e+00 -1.8300e+03 -1.0000e+03 5.2183e+00 -1.8300e+03 -9.7143e+02 5.1746e+00 -1.8300e+03 -9.4286e+02 5.1312e+00 -1.8300e+03 -9.1429e+02 5.0880e+00 -1.8300e+03 -8.8571e+02 5.0451e+00 -1.8300e+03 -8.5714e+02 5.0026e+00 -1.8300e+03 -8.2857e+02 4.9606e+00 -1.8300e+03 -8.0000e+02 4.9191e+00 -1.8300e+03 -7.7143e+02 4.8781e+00 -1.8300e+03 -7.4286e+02 4.8378e+00 -1.8300e+03 -7.1429e+02 4.7981e+00 -1.8300e+03 -6.8571e+02 4.7593e+00 -1.8300e+03 -6.5714e+02 4.7213e+00 -1.8300e+03 -6.2857e+02 4.6842e+00 -1.8300e+03 -6.0000e+02 4.6481e+00 -1.8300e+03 -5.7143e+02 4.6130e+00 -1.8300e+03 -5.4286e+02 4.5791e+00 -1.8300e+03 -5.1429e+02 4.5464e+00 -1.8300e+03 -4.8571e+02 4.5150e+00 -1.8300e+03 -4.5714e+02 4.4849e+00 -1.8300e+03 -4.2857e+02 4.4562e+00 -1.8300e+03 -4.0000e+02 4.4291e+00 -1.8300e+03 -3.7143e+02 4.4034e+00 -1.8300e+03 -3.4286e+02 4.3794e+00 -1.8300e+03 -3.1429e+02 4.3571e+00 -1.8300e+03 -2.8571e+02 4.3365e+00 -1.8300e+03 -2.5714e+02 4.3176e+00 -1.8300e+03 -2.2857e+02 4.3007e+00 -1.8300e+03 -2.0000e+02 4.2856e+00 -1.8300e+03 -1.7143e+02 4.2724e+00 -1.8300e+03 -1.4286e+02 4.2612e+00 -1.8300e+03 -1.1429e+02 4.2520e+00 -1.8300e+03 -8.5714e+01 4.2448e+00 -1.8300e+03 -5.7143e+01 4.2396e+00 -1.8300e+03 -2.8571e+01 4.2365e+00 -1.8300e+03 0.0000e+00 4.2355e+00 -1.8300e+03 2.8571e+01 4.2365e+00 -1.8300e+03 5.7143e+01 4.2396e+00 -1.8300e+03 8.5714e+01 4.2448e+00 -1.8300e+03 1.1429e+02 4.2520e+00 -1.8300e+03 1.4286e+02 4.2612e+00 -1.8300e+03 1.7143e+02 4.2724e+00 -1.8300e+03 2.0000e+02 4.2856e+00 -1.8300e+03 2.2857e+02 4.3007e+00 -1.8300e+03 2.5714e+02 4.3176e+00 -1.8300e+03 2.8571e+02 4.3365e+00 -1.8300e+03 3.1429e+02 4.3571e+00 -1.8300e+03 3.4286e+02 4.3794e+00 -1.8300e+03 3.7143e+02 4.4034e+00 -1.8300e+03 4.0000e+02 4.4291e+00 -1.8300e+03 4.2857e+02 4.4562e+00 -1.8300e+03 4.5714e+02 4.4849e+00 -1.8300e+03 4.8571e+02 4.5150e+00 -1.8300e+03 5.1429e+02 4.5464e+00 -1.8300e+03 5.4286e+02 4.5791e+00 -1.8300e+03 5.7143e+02 4.6130e+00 -1.8300e+03 6.0000e+02 4.6481e+00 -1.8300e+03 6.2857e+02 4.6842e+00 -1.8300e+03 6.5714e+02 4.7213e+00 -1.8300e+03 6.8571e+02 4.7593e+00 -1.8300e+03 7.1429e+02 4.7981e+00 -1.8300e+03 7.4286e+02 4.8378e+00 -1.8300e+03 7.7143e+02 4.8781e+00 -1.8300e+03 8.0000e+02 4.9191e+00 -1.8300e+03 8.2857e+02 4.9606e+00 -1.8300e+03 8.5714e+02 5.0026e+00 -1.8300e+03 8.8571e+02 5.0451e+00 -1.8300e+03 9.1429e+02 5.0880e+00 -1.8300e+03 9.4286e+02 5.1312e+00 -1.8300e+03 9.7143e+02 5.1746e+00 -1.8300e+03 1.0000e+03 5.2183e+00 -1.8300e+03 1.0286e+03 5.2621e+00 -1.8300e+03 1.0571e+03 5.3061e+00 -1.8300e+03 1.0857e+03 5.3501e+00 -1.8300e+03 1.1143e+03 5.3942e+00 -1.8300e+03 1.1429e+03 5.4382e+00 -1.8300e+03 1.1714e+03 5.4822e+00 -1.8300e+03 1.2000e+03 5.5261e+00 -1.8300e+03 1.2286e+03 5.5698e+00 -1.8300e+03 1.2571e+03 5.6135e+00 -1.8300e+03 1.2857e+03 5.6569e+00 -1.8300e+03 1.3143e+03 5.7001e+00 -1.8300e+03 1.3429e+03 5.7430e+00 -1.8300e+03 1.3714e+03 5.7857e+00 -1.8300e+03 1.4000e+03 5.8281e+00 -1.8300e+03 1.4286e+03 5.8702e+00 -1.8300e+03 1.4571e+03 5.9120e+00 -1.8300e+03 1.4857e+03 5.9534e+00 -1.8300e+03 1.5143e+03 5.9945e+00 -1.8300e+03 1.5429e+03 6.0352e+00 -1.8300e+03 1.5714e+03 6.0755e+00 -1.8300e+03 1.6000e+03 6.1154e+00 -1.8300e+03 1.6286e+03 6.1549e+00 -1.8300e+03 1.6571e+03 6.1939e+00 -1.8300e+03 1.6857e+03 6.2326e+00 -1.8300e+03 1.7143e+03 6.2708e+00 -1.8300e+03 1.7429e+03 6.3086e+00 -1.8300e+03 1.7714e+03 6.3460e+00 -1.8300e+03 1.8000e+03 6.3829e+00 -1.8300e+03 1.8286e+03 6.4193e+00 -1.8300e+03 1.8571e+03 6.4553e+00 -1.8300e+03 1.8857e+03 6.4909e+00 -1.8300e+03 1.9143e+03 6.5260e+00 -1.8300e+03 1.9429e+03 6.5607e+00 -1.8300e+03 1.9714e+03 6.5949e+00 -1.8300e+03 2.0000e+03 6.6287e+00 -1.8600e+03 -2.0000e+03 6.6607e+00 -1.8600e+03 -1.9714e+03 6.6278e+00 -1.8600e+03 -1.9429e+03 6.5945e+00 -1.8600e+03 -1.9143e+03 6.5608e+00 -1.8600e+03 -1.8857e+03 6.5267e+00 -1.8600e+03 -1.8571e+03 6.4921e+00 -1.8600e+03 -1.8286e+03 6.4572e+00 -1.8600e+03 -1.8000e+03 6.4218e+00 -1.8600e+03 -1.7714e+03 6.3860e+00 -1.8600e+03 -1.7429e+03 6.3497e+00 -1.8600e+03 -1.7143e+03 6.3131e+00 -1.8600e+03 -1.6857e+03 6.2761e+00 -1.8600e+03 -1.6571e+03 6.2386e+00 -1.8600e+03 -1.6286e+03 6.2008e+00 -1.8600e+03 -1.6000e+03 6.1626e+00 -1.8600e+03 -1.5714e+03 6.1241e+00 -1.8600e+03 -1.5429e+03 6.0851e+00 -1.8600e+03 -1.5143e+03 6.0459e+00 -1.8600e+03 -1.4857e+03 6.0062e+00 -1.8600e+03 -1.4571e+03 5.9663e+00 -1.8600e+03 -1.4286e+03 5.9260e+00 -1.8600e+03 -1.4000e+03 5.8855e+00 -1.8600e+03 -1.3714e+03 5.8447e+00 -1.8600e+03 -1.3429e+03 5.8036e+00 -1.8600e+03 -1.3143e+03 5.7623e+00 -1.8600e+03 -1.2857e+03 5.7208e+00 -1.8600e+03 -1.2571e+03 5.6791e+00 -1.8600e+03 -1.2286e+03 5.6373e+00 -1.8600e+03 -1.2000e+03 5.5954e+00 -1.8600e+03 -1.1714e+03 5.5533e+00 -1.8600e+03 -1.1429e+03 5.5112e+00 -1.8600e+03 -1.1143e+03 5.4691e+00 -1.8600e+03 -1.0857e+03 5.4270e+00 -1.8600e+03 -1.0571e+03 5.3849e+00 -1.8600e+03 -1.0286e+03 5.3429e+00 -1.8600e+03 -1.0000e+03 5.3011e+00 -1.8600e+03 -9.7143e+02 5.2595e+00 -1.8600e+03 -9.4286e+02 5.2180e+00 -1.8600e+03 -9.1429e+02 5.1769e+00 -1.8600e+03 -8.8571e+02 5.1361e+00 -1.8600e+03 -8.5714e+02 5.0958e+00 -1.8600e+03 -8.2857e+02 5.0558e+00 -1.8600e+03 -8.0000e+02 5.0164e+00 -1.8600e+03 -7.7143e+02 4.9775e+00 -1.8600e+03 -7.4286e+02 4.9393e+00 -1.8600e+03 -7.1429e+02 4.9017e+00 -1.8600e+03 -6.8571e+02 4.8650e+00 -1.8600e+03 -6.5714e+02 4.8290e+00 -1.8600e+03 -6.2857e+02 4.7939e+00 -1.8600e+03 -6.0000e+02 4.7598e+00 -1.8600e+03 -5.7143e+02 4.7267e+00 -1.8600e+03 -5.4286e+02 4.6947e+00 -1.8600e+03 -5.1429e+02 4.6638e+00 -1.8600e+03 -4.8571e+02 4.6342e+00 -1.8600e+03 -4.5714e+02 4.6058e+00 -1.8600e+03 -4.2857e+02 4.5788e+00 -1.8600e+03 -4.0000e+02 4.5532e+00 -1.8600e+03 -3.7143e+02 4.5291e+00 -1.8600e+03 -3.4286e+02 4.5065e+00 -1.8600e+03 -3.1429e+02 4.4855e+00 -1.8600e+03 -2.8571e+02 4.4661e+00 -1.8600e+03 -2.5714e+02 4.4484e+00 -1.8600e+03 -2.2857e+02 4.4325e+00 -1.8600e+03 -2.0000e+02 4.4183e+00 -1.8600e+03 -1.7143e+02 4.4059e+00 -1.8600e+03 -1.4286e+02 4.3954e+00 -1.8600e+03 -1.1429e+02 4.3867e+00 -1.8600e+03 -8.5714e+01 4.3800e+00 -1.8600e+03 -5.7143e+01 4.3752e+00 -1.8600e+03 -2.8571e+01 4.3723e+00 -1.8600e+03 0.0000e+00 4.3713e+00 -1.8600e+03 2.8571e+01 4.3723e+00 -1.8600e+03 5.7143e+01 4.3752e+00 -1.8600e+03 8.5714e+01 4.3800e+00 -1.8600e+03 1.1429e+02 4.3867e+00 -1.8600e+03 1.4286e+02 4.3954e+00 -1.8600e+03 1.7143e+02 4.4059e+00 -1.8600e+03 2.0000e+02 4.4183e+00 -1.8600e+03 2.2857e+02 4.4325e+00 -1.8600e+03 2.5714e+02 4.4484e+00 -1.8600e+03 2.8571e+02 4.4661e+00 -1.8600e+03 3.1429e+02 4.4855e+00 -1.8600e+03 3.4286e+02 4.5065e+00 -1.8600e+03 3.7143e+02 4.5291e+00 -1.8600e+03 4.0000e+02 4.5532e+00 -1.8600e+03 4.2857e+02 4.5788e+00 -1.8600e+03 4.5714e+02 4.6058e+00 -1.8600e+03 4.8571e+02 4.6342e+00 -1.8600e+03 5.1429e+02 4.6638e+00 -1.8600e+03 5.4286e+02 4.6947e+00 -1.8600e+03 5.7143e+02 4.7267e+00 -1.8600e+03 6.0000e+02 4.7598e+00 -1.8600e+03 6.2857e+02 4.7939e+00 -1.8600e+03 6.5714e+02 4.8290e+00 -1.8600e+03 6.8571e+02 4.8650e+00 -1.8600e+03 7.1429e+02 4.9017e+00 -1.8600e+03 7.4286e+02 4.9393e+00 -1.8600e+03 7.7143e+02 4.9775e+00 -1.8600e+03 8.0000e+02 5.0164e+00 -1.8600e+03 8.2857e+02 5.0558e+00 -1.8600e+03 8.5714e+02 5.0958e+00 -1.8600e+03 8.8571e+02 5.1361e+00 -1.8600e+03 9.1429e+02 5.1769e+00 -1.8600e+03 9.4286e+02 5.2180e+00 -1.8600e+03 9.7143e+02 5.2595e+00 -1.8600e+03 1.0000e+03 5.3011e+00 -1.8600e+03 1.0286e+03 5.3429e+00 -1.8600e+03 1.0571e+03 5.3849e+00 -1.8600e+03 1.0857e+03 5.4270e+00 -1.8600e+03 1.1143e+03 5.4691e+00 -1.8600e+03 1.1429e+03 5.5112e+00 -1.8600e+03 1.1714e+03 5.5533e+00 -1.8600e+03 1.2000e+03 5.5954e+00 -1.8600e+03 1.2286e+03 5.6373e+00 -1.8600e+03 1.2571e+03 5.6791e+00 -1.8600e+03 1.2857e+03 5.7208e+00 -1.8600e+03 1.3143e+03 5.7623e+00 -1.8600e+03 1.3429e+03 5.8036e+00 -1.8600e+03 1.3714e+03 5.8447e+00 -1.8600e+03 1.4000e+03 5.8855e+00 -1.8600e+03 1.4286e+03 5.9260e+00 -1.8600e+03 1.4571e+03 5.9663e+00 -1.8600e+03 1.4857e+03 6.0062e+00 -1.8600e+03 1.5143e+03 6.0459e+00 -1.8600e+03 1.5429e+03 6.0851e+00 -1.8600e+03 1.5714e+03 6.1241e+00 -1.8600e+03 1.6000e+03 6.1626e+00 -1.8600e+03 1.6286e+03 6.2008e+00 -1.8600e+03 1.6571e+03 6.2386e+00 -1.8600e+03 1.6857e+03 6.2761e+00 -1.8600e+03 1.7143e+03 6.3131e+00 -1.8600e+03 1.7429e+03 6.3497e+00 -1.8600e+03 1.7714e+03 6.3860e+00 -1.8600e+03 1.8000e+03 6.4218e+00 -1.8600e+03 1.8286e+03 6.4572e+00 -1.8600e+03 1.8571e+03 6.4921e+00 -1.8600e+03 1.8857e+03 6.5267e+00 -1.8600e+03 1.9143e+03 6.5608e+00 -1.8600e+03 1.9429e+03 6.5945e+00 -1.8600e+03 1.9714e+03 6.6278e+00 -1.8600e+03 2.0000e+03 6.6607e+00 -1.8900e+03 -2.0000e+03 6.6924e+00 -1.8900e+03 -1.9714e+03 6.6604e+00 -1.8900e+03 -1.9429e+03 6.6280e+00 -1.8900e+03 -1.9143e+03 6.5952e+00 -1.8900e+03 -1.8857e+03 6.5620e+00 -1.8900e+03 -1.8571e+03 6.5285e+00 -1.8900e+03 -1.8286e+03 6.4945e+00 -1.8900e+03 -1.8000e+03 6.4601e+00 -1.8900e+03 -1.7714e+03 6.4254e+00 -1.8900e+03 -1.7429e+03 6.3903e+00 -1.8900e+03 -1.7143e+03 6.3547e+00 -1.8900e+03 -1.6857e+03 6.3189e+00 -1.8900e+03 -1.6571e+03 6.2826e+00 -1.8900e+03 -1.6286e+03 6.2460e+00 -1.8900e+03 -1.6000e+03 6.2091e+00 -1.8900e+03 -1.5714e+03 6.1718e+00 -1.8900e+03 -1.5429e+03 6.1342e+00 -1.8900e+03 -1.5143e+03 6.0962e+00 -1.8900e+03 -1.4857e+03 6.0580e+00 -1.8900e+03 -1.4571e+03 6.0195e+00 -1.8900e+03 -1.4286e+03 5.9807e+00 -1.8900e+03 -1.4000e+03 5.9416e+00 -1.8900e+03 -1.3714e+03 5.9023e+00 -1.8900e+03 -1.3429e+03 5.8628e+00 -1.8900e+03 -1.3143e+03 5.8231e+00 -1.8900e+03 -1.2857e+03 5.7833e+00 -1.8900e+03 -1.2571e+03 5.7432e+00 -1.8900e+03 -1.2286e+03 5.7031e+00 -1.8900e+03 -1.2000e+03 5.6628e+00 -1.8900e+03 -1.1714e+03 5.6225e+00 -1.8900e+03 -1.1429e+03 5.5822e+00 -1.8900e+03 -1.1143e+03 5.5419e+00 -1.8900e+03 -1.0857e+03 5.5016e+00 -1.8900e+03 -1.0571e+03 5.4614e+00 -1.8900e+03 -1.0286e+03 5.4213e+00 -1.8900e+03 -1.0000e+03 5.3814e+00 -1.8900e+03 -9.7143e+02 5.3416e+00 -1.8900e+03 -9.4286e+02 5.3022e+00 -1.8900e+03 -9.1429e+02 5.2630e+00 -1.8900e+03 -8.8571e+02 5.2242e+00 -1.8900e+03 -8.5714e+02 5.1857e+00 -1.8900e+03 -8.2857e+02 5.1478e+00 -1.8900e+03 -8.0000e+02 5.1103e+00 -1.8900e+03 -7.7143e+02 5.0734e+00 -1.8900e+03 -7.4286e+02 5.0371e+00 -1.8900e+03 -7.1429e+02 5.0015e+00 -1.8900e+03 -6.8571e+02 4.9666e+00 -1.8900e+03 -6.5714e+02 4.9326e+00 -1.8900e+03 -6.2857e+02 4.8994e+00 -1.8900e+03 -6.0000e+02 4.8671e+00 -1.8900e+03 -5.7143e+02 4.8358e+00 -1.8900e+03 -5.4286e+02 4.8055e+00 -1.8900e+03 -5.1429e+02 4.7764e+00 -1.8900e+03 -4.8571e+02 4.7484e+00 -1.8900e+03 -4.5714e+02 4.7216e+00 -1.8900e+03 -4.2857e+02 4.6962e+00 -1.8900e+03 -4.0000e+02 4.6720e+00 -1.8900e+03 -3.7143e+02 4.6493e+00 -1.8900e+03 -3.4286e+02 4.6280e+00 -1.8900e+03 -3.1429e+02 4.6082e+00 -1.8900e+03 -2.8571e+02 4.5900e+00 -1.8900e+03 -2.5714e+02 4.5734e+00 -1.8900e+03 -2.2857e+02 4.5583e+00 -1.8900e+03 -2.0000e+02 4.5450e+00 -1.8900e+03 -1.7143e+02 4.5334e+00 -1.8900e+03 -1.4286e+02 4.5235e+00 -1.8900e+03 -1.1429e+02 4.5154e+00 -1.8900e+03 -8.5714e+01 4.5090e+00 -1.8900e+03 -5.7143e+01 4.5045e+00 -1.8900e+03 -2.8571e+01 4.5018e+00 -1.8900e+03 0.0000e+00 4.5008e+00 -1.8900e+03 2.8571e+01 4.5018e+00 -1.8900e+03 5.7143e+01 4.5045e+00 -1.8900e+03 8.5714e+01 4.5090e+00 -1.8900e+03 1.1429e+02 4.5154e+00 -1.8900e+03 1.4286e+02 4.5235e+00 -1.8900e+03 1.7143e+02 4.5334e+00 -1.8900e+03 2.0000e+02 4.5450e+00 -1.8900e+03 2.2857e+02 4.5583e+00 -1.8900e+03 2.5714e+02 4.5734e+00 -1.8900e+03 2.8571e+02 4.5900e+00 -1.8900e+03 3.1429e+02 4.6082e+00 -1.8900e+03 3.4286e+02 4.6280e+00 -1.8900e+03 3.7143e+02 4.6493e+00 -1.8900e+03 4.0000e+02 4.6720e+00 -1.8900e+03 4.2857e+02 4.6962e+00 -1.8900e+03 4.5714e+02 4.7216e+00 -1.8900e+03 4.8571e+02 4.7484e+00 -1.8900e+03 5.1429e+02 4.7764e+00 -1.8900e+03 5.4286e+02 4.8055e+00 -1.8900e+03 5.7143e+02 4.8358e+00 -1.8900e+03 6.0000e+02 4.8671e+00 -1.8900e+03 6.2857e+02 4.8994e+00 -1.8900e+03 6.5714e+02 4.9326e+00 -1.8900e+03 6.8571e+02 4.9666e+00 -1.8900e+03 7.1429e+02 5.0015e+00 -1.8900e+03 7.4286e+02 5.0371e+00 -1.8900e+03 7.7143e+02 5.0734e+00 -1.8900e+03 8.0000e+02 5.1103e+00 -1.8900e+03 8.2857e+02 5.1478e+00 -1.8900e+03 8.5714e+02 5.1857e+00 -1.8900e+03 8.8571e+02 5.2242e+00 -1.8900e+03 9.1429e+02 5.2630e+00 -1.8900e+03 9.4286e+02 5.3022e+00 -1.8900e+03 9.7143e+02 5.3416e+00 -1.8900e+03 1.0000e+03 5.3814e+00 -1.8900e+03 1.0286e+03 5.4213e+00 -1.8900e+03 1.0571e+03 5.4614e+00 -1.8900e+03 1.0857e+03 5.5016e+00 -1.8900e+03 1.1143e+03 5.5419e+00 -1.8900e+03 1.1429e+03 5.5822e+00 -1.8900e+03 1.1714e+03 5.6225e+00 -1.8900e+03 1.2000e+03 5.6628e+00 -1.8900e+03 1.2286e+03 5.7031e+00 -1.8900e+03 1.2571e+03 5.7432e+00 -1.8900e+03 1.2857e+03 5.7833e+00 -1.8900e+03 1.3143e+03 5.8231e+00 -1.8900e+03 1.3429e+03 5.8628e+00 -1.8900e+03 1.3714e+03 5.9023e+00 -1.8900e+03 1.4000e+03 5.9416e+00 -1.8900e+03 1.4286e+03 5.9807e+00 -1.8900e+03 1.4571e+03 6.0195e+00 -1.8900e+03 1.4857e+03 6.0580e+00 -1.8900e+03 1.5143e+03 6.0962e+00 -1.8900e+03 1.5429e+03 6.1342e+00 -1.8900e+03 1.5714e+03 6.1718e+00 -1.8900e+03 1.6000e+03 6.2091e+00 -1.8900e+03 1.6286e+03 6.2460e+00 -1.8900e+03 1.6571e+03 6.2826e+00 -1.8900e+03 1.6857e+03 6.3189e+00 -1.8900e+03 1.7143e+03 6.3547e+00 -1.8900e+03 1.7429e+03 6.3903e+00 -1.8900e+03 1.7714e+03 6.4254e+00 -1.8900e+03 1.8000e+03 6.4601e+00 -1.8900e+03 1.8286e+03 6.4945e+00 -1.8900e+03 1.8571e+03 6.5285e+00 -1.8900e+03 1.8857e+03 6.5620e+00 -1.8900e+03 1.9143e+03 6.5952e+00 -1.8900e+03 1.9429e+03 6.6280e+00 -1.8900e+03 1.9714e+03 6.6604e+00 -1.8900e+03 2.0000e+03 6.6924e+00 -1.9200e+03 -2.0000e+03 6.7238e+00 -1.9200e+03 -1.9714e+03 6.6927e+00 -1.9200e+03 -1.9429e+03 6.6611e+00 -1.9200e+03 -1.9143e+03 6.6292e+00 -1.9200e+03 -1.8857e+03 6.5970e+00 -1.9200e+03 -1.8571e+03 6.5643e+00 -1.9200e+03 -1.8286e+03 6.5313e+00 -1.9200e+03 -1.8000e+03 6.4980e+00 -1.9200e+03 -1.7714e+03 6.4642e+00 -1.9200e+03 -1.7429e+03 6.4302e+00 -1.9200e+03 -1.7143e+03 6.3957e+00 -1.9200e+03 -1.6857e+03 6.3610e+00 -1.9200e+03 -1.6571e+03 6.3259e+00 -1.9200e+03 -1.6286e+03 6.2905e+00 -1.9200e+03 -1.6000e+03 6.2547e+00 -1.9200e+03 -1.5714e+03 6.2187e+00 -1.9200e+03 -1.5429e+03 6.1823e+00 -1.9200e+03 -1.5143e+03 6.1457e+00 -1.9200e+03 -1.4857e+03 6.1088e+00 -1.9200e+03 -1.4571e+03 6.0716e+00 -1.9200e+03 -1.4286e+03 6.0342e+00 -1.9200e+03 -1.4000e+03 5.9966e+00 -1.9200e+03 -1.3714e+03 5.9587e+00 -1.9200e+03 -1.3429e+03 5.9207e+00 -1.9200e+03 -1.3143e+03 5.8825e+00 -1.9200e+03 -1.2857e+03 5.8442e+00 -1.9200e+03 -1.2571e+03 5.8057e+00 -1.9200e+03 -1.2286e+03 5.7672e+00 -1.9200e+03 -1.2000e+03 5.7286e+00 -1.9200e+03 -1.1714e+03 5.6899e+00 -1.9200e+03 -1.1429e+03 5.6513e+00 -1.9200e+03 -1.1143e+03 5.6127e+00 -1.9200e+03 -1.0857e+03 5.5741e+00 -1.9200e+03 -1.0571e+03 5.5357e+00 -1.9200e+03 -1.0286e+03 5.4974e+00 -1.9200e+03 -1.0000e+03 5.4592e+00 -1.9200e+03 -9.7143e+02 5.4213e+00 -1.9200e+03 -9.4286e+02 5.3837e+00 -1.9200e+03 -9.1429e+02 5.3463e+00 -1.9200e+03 -8.8571e+02 5.3093e+00 -1.9200e+03 -8.5714e+02 5.2727e+00 -1.9200e+03 -8.2857e+02 5.2366e+00 -1.9200e+03 -8.0000e+02 5.2010e+00 -1.9200e+03 -7.7143e+02 5.1659e+00 -1.9200e+03 -7.4286e+02 5.1314e+00 -1.9200e+03 -7.1429e+02 5.0976e+00 -1.9200e+03 -6.8571e+02 5.0646e+00 -1.9200e+03 -6.5714e+02 5.0323e+00 -1.9200e+03 -6.2857e+02 5.0008e+00 -1.9200e+03 -6.0000e+02 4.9702e+00 -1.9200e+03 -5.7143e+02 4.9406e+00 -1.9200e+03 -5.4286e+02 4.9120e+00 -1.9200e+03 -5.1429e+02 4.8844e+00 -1.9200e+03 -4.8571e+02 4.8580e+00 -1.9200e+03 -4.5714e+02 4.8327e+00 -1.9200e+03 -4.2857e+02 4.8087e+00 -1.9200e+03 -4.0000e+02 4.7859e+00 -1.9200e+03 -3.7143e+02 4.7644e+00 -1.9200e+03 -3.4286e+02 4.7444e+00 -1.9200e+03 -3.1429e+02 4.7257e+00 -1.9200e+03 -2.8571e+02 4.7085e+00 -1.9200e+03 -2.5714e+02 4.6929e+00 -1.9200e+03 -2.2857e+02 4.6787e+00 -1.9200e+03 -2.0000e+02 4.6662e+00 -1.9200e+03 -1.7143e+02 4.6552e+00 -1.9200e+03 -1.4286e+02 4.6459e+00 -1.9200e+03 -1.1429e+02 4.6383e+00 -1.9200e+03 -8.5714e+01 4.6323e+00 -1.9200e+03 -5.7143e+01 4.6280e+00 -1.9200e+03 -2.8571e+01 4.6255e+00 -1.9200e+03 0.0000e+00 4.6246e+00 -1.9200e+03 2.8571e+01 4.6255e+00 -1.9200e+03 5.7143e+01 4.6280e+00 -1.9200e+03 8.5714e+01 4.6323e+00 -1.9200e+03 1.1429e+02 4.6383e+00 -1.9200e+03 1.4286e+02 4.6459e+00 -1.9200e+03 1.7143e+02 4.6552e+00 -1.9200e+03 2.0000e+02 4.6662e+00 -1.9200e+03 2.2857e+02 4.6787e+00 -1.9200e+03 2.5714e+02 4.6929e+00 -1.9200e+03 2.8571e+02 4.7085e+00 -1.9200e+03 3.1429e+02 4.7257e+00 -1.9200e+03 3.4286e+02 4.7444e+00 -1.9200e+03 3.7143e+02 4.7644e+00 -1.9200e+03 4.0000e+02 4.7859e+00 -1.9200e+03 4.2857e+02 4.8087e+00 -1.9200e+03 4.5714e+02 4.8327e+00 -1.9200e+03 4.8571e+02 4.8580e+00 -1.9200e+03 5.1429e+02 4.8844e+00 -1.9200e+03 5.4286e+02 4.9120e+00 -1.9200e+03 5.7143e+02 4.9406e+00 -1.9200e+03 6.0000e+02 4.9702e+00 -1.9200e+03 6.2857e+02 5.0008e+00 -1.9200e+03 6.5714e+02 5.0323e+00 -1.9200e+03 6.8571e+02 5.0646e+00 -1.9200e+03 7.1429e+02 5.0976e+00 -1.9200e+03 7.4286e+02 5.1314e+00 -1.9200e+03 7.7143e+02 5.1659e+00 -1.9200e+03 8.0000e+02 5.2010e+00 -1.9200e+03 8.2857e+02 5.2366e+00 -1.9200e+03 8.5714e+02 5.2727e+00 -1.9200e+03 8.8571e+02 5.3093e+00 -1.9200e+03 9.1429e+02 5.3463e+00 -1.9200e+03 9.4286e+02 5.3837e+00 -1.9200e+03 9.7143e+02 5.4213e+00 -1.9200e+03 1.0000e+03 5.4592e+00 -1.9200e+03 1.0286e+03 5.4974e+00 -1.9200e+03 1.0571e+03 5.5357e+00 -1.9200e+03 1.0857e+03 5.5741e+00 -1.9200e+03 1.1143e+03 5.6127e+00 -1.9200e+03 1.1429e+03 5.6513e+00 -1.9200e+03 1.1714e+03 5.6899e+00 -1.9200e+03 1.2000e+03 5.7286e+00 -1.9200e+03 1.2286e+03 5.7672e+00 -1.9200e+03 1.2571e+03 5.8057e+00 -1.9200e+03 1.2857e+03 5.8442e+00 -1.9200e+03 1.3143e+03 5.8825e+00 -1.9200e+03 1.3429e+03 5.9207e+00 -1.9200e+03 1.3714e+03 5.9587e+00 -1.9200e+03 1.4000e+03 5.9966e+00 -1.9200e+03 1.4286e+03 6.0342e+00 -1.9200e+03 1.4571e+03 6.0716e+00 -1.9200e+03 1.4857e+03 6.1088e+00 -1.9200e+03 1.5143e+03 6.1457e+00 -1.9200e+03 1.5429e+03 6.1823e+00 -1.9200e+03 1.5714e+03 6.2187e+00 -1.9200e+03 1.6000e+03 6.2547e+00 -1.9200e+03 1.6286e+03 6.2905e+00 -1.9200e+03 1.6571e+03 6.3259e+00 -1.9200e+03 1.6857e+03 6.3610e+00 -1.9200e+03 1.7143e+03 6.3957e+00 -1.9200e+03 1.7429e+03 6.4302e+00 -1.9200e+03 1.7714e+03 6.4642e+00 -1.9200e+03 1.8000e+03 6.4980e+00 -1.9200e+03 1.8286e+03 6.5313e+00 -1.9200e+03 1.8571e+03 6.5643e+00 -1.9200e+03 1.8857e+03 6.5970e+00 -1.9200e+03 1.9143e+03 6.6292e+00 -1.9200e+03 1.9429e+03 6.6611e+00 -1.9200e+03 1.9714e+03 6.6927e+00 -1.9200e+03 2.0000e+03 6.7238e+00 -1.9500e+03 -2.0000e+03 6.7549e+00 -1.9500e+03 -1.9714e+03 6.7245e+00 -1.9500e+03 -1.9429e+03 6.6938e+00 -1.9500e+03 -1.9143e+03 6.6628e+00 -1.9500e+03 -1.8857e+03 6.6314e+00 -1.9500e+03 -1.8571e+03 6.5997e+00 -1.9500e+03 -1.8286e+03 6.5677e+00 -1.9500e+03 -1.8000e+03 6.5353e+00 -1.9500e+03 -1.7714e+03 6.5025e+00 -1.9500e+03 -1.7429e+03 6.4695e+00 -1.9500e+03 -1.7143e+03 6.4361e+00 -1.9500e+03 -1.6857e+03 6.4024e+00 -1.9500e+03 -1.6571e+03 6.3684e+00 -1.9500e+03 -1.6286e+03 6.3341e+00 -1.9500e+03 -1.6000e+03 6.2995e+00 -1.9500e+03 -1.5714e+03 6.2647e+00 -1.9500e+03 -1.5429e+03 6.2296e+00 -1.9500e+03 -1.5143e+03 6.1942e+00 -1.9500e+03 -1.4857e+03 6.1585e+00 -1.9500e+03 -1.4571e+03 6.1227e+00 -1.9500e+03 -1.4286e+03 6.0866e+00 -1.9500e+03 -1.4000e+03 6.0503e+00 -1.9500e+03 -1.3714e+03 6.0139e+00 -1.9500e+03 -1.3429e+03 5.9773e+00 -1.9500e+03 -1.3143e+03 5.9405e+00 -1.9500e+03 -1.2857e+03 5.9037e+00 -1.9500e+03 -1.2571e+03 5.8667e+00 -1.9500e+03 -1.2286e+03 5.8297e+00 -1.9500e+03 -1.2000e+03 5.7927e+00 -1.9500e+03 -1.1714e+03 5.7556e+00 -1.9500e+03 -1.1429e+03 5.7185e+00 -1.9500e+03 -1.1143e+03 5.6815e+00 -1.9500e+03 -1.0857e+03 5.6446e+00 -1.9500e+03 -1.0571e+03 5.6078e+00 -1.9500e+03 -1.0286e+03 5.5712e+00 -1.9500e+03 -1.0000e+03 5.5348e+00 -1.9500e+03 -9.7143e+02 5.4985e+00 -1.9500e+03 -9.4286e+02 5.4626e+00 -1.9500e+03 -9.1429e+02 5.4270e+00 -1.9500e+03 -8.8571e+02 5.3917e+00 -1.9500e+03 -8.5714e+02 5.3569e+00 -1.9500e+03 -8.2857e+02 5.3225e+00 -1.9500e+03 -8.0000e+02 5.2886e+00 -1.9500e+03 -7.7143e+02 5.2552e+00 -1.9500e+03 -7.4286e+02 5.2225e+00 -1.9500e+03 -7.1429e+02 5.1903e+00 -1.9500e+03 -6.8571e+02 5.1590e+00 -1.9500e+03 -6.5714e+02 5.1283e+00 -1.9500e+03 -6.2857e+02 5.0985e+00 -1.9500e+03 -6.0000e+02 5.0695e+00 -1.9500e+03 -5.7143e+02 5.0414e+00 -1.9500e+03 -5.4286e+02 5.0143e+00 -1.9500e+03 -5.1429e+02 4.9882e+00 -1.9500e+03 -4.8571e+02 4.9632e+00 -1.9500e+03 -4.5714e+02 4.9393e+00 -1.9500e+03 -4.2857e+02 4.9166e+00 -1.9500e+03 -4.0000e+02 4.8951e+00 -1.9500e+03 -3.7143e+02 4.8748e+00 -1.9500e+03 -3.4286e+02 4.8559e+00 -1.9500e+03 -3.1429e+02 4.8383e+00 -1.9500e+03 -2.8571e+02 4.8221e+00 -1.9500e+03 -2.5714e+02 4.8073e+00 -1.9500e+03 -2.2857e+02 4.7939e+00 -1.9500e+03 -2.0000e+02 4.7821e+00 -1.9500e+03 -1.7143e+02 4.7718e+00 -1.9500e+03 -1.4286e+02 4.7630e+00 -1.9500e+03 -1.1429e+02 4.7558e+00 -1.9500e+03 -8.5714e+01 4.7502e+00 -1.9500e+03 -5.7143e+01 4.7462e+00 -1.9500e+03 -2.8571e+01 4.7437e+00 -1.9500e+03 0.0000e+00 4.7429e+00 -1.9500e+03 2.8571e+01 4.7437e+00 -1.9500e+03 5.7143e+01 4.7462e+00 -1.9500e+03 8.5714e+01 4.7502e+00 -1.9500e+03 1.1429e+02 4.7558e+00 -1.9500e+03 1.4286e+02 4.7630e+00 -1.9500e+03 1.7143e+02 4.7718e+00 -1.9500e+03 2.0000e+02 4.7821e+00 -1.9500e+03 2.2857e+02 4.7939e+00 -1.9500e+03 2.5714e+02 4.8073e+00 -1.9500e+03 2.8571e+02 4.8221e+00 -1.9500e+03 3.1429e+02 4.8383e+00 -1.9500e+03 3.4286e+02 4.8559e+00 -1.9500e+03 3.7143e+02 4.8748e+00 -1.9500e+03 4.0000e+02 4.8951e+00 -1.9500e+03 4.2857e+02 4.9166e+00 -1.9500e+03 4.5714e+02 4.9393e+00 -1.9500e+03 4.8571e+02 4.9632e+00 -1.9500e+03 5.1429e+02 4.9882e+00 -1.9500e+03 5.4286e+02 5.0143e+00 -1.9500e+03 5.7143e+02 5.0414e+00 -1.9500e+03 6.0000e+02 5.0695e+00 -1.9500e+03 6.2857e+02 5.0985e+00 -1.9500e+03 6.5714e+02 5.1283e+00 -1.9500e+03 6.8571e+02 5.1590e+00 -1.9500e+03 7.1429e+02 5.1903e+00 -1.9500e+03 7.4286e+02 5.2225e+00 -1.9500e+03 7.7143e+02 5.2552e+00 -1.9500e+03 8.0000e+02 5.2886e+00 -1.9500e+03 8.2857e+02 5.3225e+00 -1.9500e+03 8.5714e+02 5.3569e+00 -1.9500e+03 8.8571e+02 5.3917e+00 -1.9500e+03 9.1429e+02 5.4270e+00 -1.9500e+03 9.4286e+02 5.4626e+00 -1.9500e+03 9.7143e+02 5.4985e+00 -1.9500e+03 1.0000e+03 5.5348e+00 -1.9500e+03 1.0286e+03 5.5712e+00 -1.9500e+03 1.0571e+03 5.6078e+00 -1.9500e+03 1.0857e+03 5.6446e+00 -1.9500e+03 1.1143e+03 5.6815e+00 -1.9500e+03 1.1429e+03 5.7185e+00 -1.9500e+03 1.1714e+03 5.7556e+00 -1.9500e+03 1.2000e+03 5.7927e+00 -1.9500e+03 1.2286e+03 5.8297e+00 -1.9500e+03 1.2571e+03 5.8667e+00 -1.9500e+03 1.2857e+03 5.9037e+00 -1.9500e+03 1.3143e+03 5.9405e+00 -1.9500e+03 1.3429e+03 5.9773e+00 -1.9500e+03 1.3714e+03 6.0139e+00 -1.9500e+03 1.4000e+03 6.0503e+00 -1.9500e+03 1.4286e+03 6.0866e+00 -1.9500e+03 1.4571e+03 6.1227e+00 -1.9500e+03 1.4857e+03 6.1585e+00 -1.9500e+03 1.5143e+03 6.1942e+00 -1.9500e+03 1.5429e+03 6.2296e+00 -1.9500e+03 1.5714e+03 6.2647e+00 -1.9500e+03 1.6000e+03 6.2995e+00 -1.9500e+03 1.6286e+03 6.3341e+00 -1.9500e+03 1.6571e+03 6.3684e+00 -1.9500e+03 1.6857e+03 6.4024e+00 -1.9500e+03 1.7143e+03 6.4361e+00 -1.9500e+03 1.7429e+03 6.4695e+00 -1.9500e+03 1.7714e+03 6.5025e+00 -1.9500e+03 1.8000e+03 6.5353e+00 -1.9500e+03 1.8286e+03 6.5677e+00 -1.9500e+03 1.8571e+03 6.5997e+00 -1.9500e+03 1.8857e+03 6.6314e+00 -1.9500e+03 1.9143e+03 6.6628e+00 -1.9500e+03 1.9429e+03 6.6938e+00 -1.9500e+03 1.9714e+03 6.7245e+00 -1.9500e+03 2.0000e+03 6.7549e+00 -1.9800e+03 -2.0000e+03 6.7856e+00 -1.9800e+03 -1.9714e+03 6.7560e+00 -1.9800e+03 -1.9429e+03 6.7262e+00 -1.9800e+03 -1.9143e+03 6.6960e+00 -1.9800e+03 -1.8857e+03 6.6655e+00 -1.9800e+03 -1.8571e+03 6.6346e+00 -1.9800e+03 -1.8286e+03 6.6035e+00 -1.9800e+03 -1.8000e+03 6.5720e+00 -1.9800e+03 -1.7714e+03 6.5403e+00 -1.9800e+03 -1.7429e+03 6.5082e+00 -1.9800e+03 -1.7143e+03 6.4758e+00 -1.9800e+03 -1.6857e+03 6.4432e+00 -1.9800e+03 -1.6571e+03 6.4103e+00 -1.9800e+03 -1.6286e+03 6.3771e+00 -1.9800e+03 -1.6000e+03 6.3436e+00 -1.9800e+03 -1.5714e+03 6.3099e+00 -1.9800e+03 -1.5429e+03 6.2759e+00 -1.9800e+03 -1.5143e+03 6.2417e+00 -1.9800e+03 -1.4857e+03 6.2073e+00 -1.9800e+03 -1.4571e+03 6.1727e+00 -1.9800e+03 -1.4286e+03 6.1379e+00 -1.9800e+03 -1.4000e+03 6.1029e+00 -1.9800e+03 -1.3714e+03 6.0678e+00 -1.9800e+03 -1.3429e+03 6.0326e+00 -1.9800e+03 -1.3143e+03 5.9972e+00 -1.9800e+03 -1.2857e+03 5.9618e+00 -1.9800e+03 -1.2571e+03 5.9262e+00 -1.9800e+03 -1.2286e+03 5.8907e+00 -1.9800e+03 -1.2000e+03 5.8551e+00 -1.9800e+03 -1.1714e+03 5.8195e+00 -1.9800e+03 -1.1429e+03 5.7840e+00 -1.9800e+03 -1.1143e+03 5.7485e+00 -1.9800e+03 -1.0857e+03 5.7132e+00 -1.9800e+03 -1.0571e+03 5.6780e+00 -1.9800e+03 -1.0286e+03 5.6429e+00 -1.9800e+03 -1.0000e+03 5.6081e+00 -1.9800e+03 -9.7143e+02 5.5734e+00 -1.9800e+03 -9.4286e+02 5.5391e+00 -1.9800e+03 -9.1429e+02 5.5051e+00 -1.9800e+03 -8.8571e+02 5.4715e+00 -1.9800e+03 -8.5714e+02 5.4383e+00 -1.9800e+03 -8.2857e+02 5.4055e+00 -1.9800e+03 -8.0000e+02 5.3732e+00 -1.9800e+03 -7.7143e+02 5.3415e+00 -1.9800e+03 -7.4286e+02 5.3103e+00 -1.9800e+03 -7.1429e+02 5.2798e+00 -1.9800e+03 -6.8571e+02 5.2499e+00 -1.9800e+03 -6.5714e+02 5.2208e+00 -1.9800e+03 -6.2857e+02 5.1925e+00 -1.9800e+03 -6.0000e+02 5.1650e+00 -1.9800e+03 -5.7143e+02 5.1384e+00 -1.9800e+03 -5.4286e+02 5.1127e+00 -1.9800e+03 -5.1429e+02 5.0880e+00 -1.9800e+03 -4.8571e+02 5.0643e+00 -1.9800e+03 -4.5714e+02 5.0417e+00 -1.9800e+03 -4.2857e+02 5.0202e+00 -1.9800e+03 -4.0000e+02 4.9999e+00 -1.9800e+03 -3.7143e+02 4.9807e+00 -1.9800e+03 -3.4286e+02 4.9628e+00 -1.9800e+03 -3.1429e+02 4.9462e+00 -1.9800e+03 -2.8571e+02 4.9309e+00 -1.9800e+03 -2.5714e+02 4.9169e+00 -1.9800e+03 -2.2857e+02 4.9043e+00 -1.9800e+03 -2.0000e+02 4.8932e+00 -1.9800e+03 -1.7143e+02 4.8834e+00 -1.9800e+03 -1.4286e+02 4.8751e+00 -1.9800e+03 -1.1429e+02 4.8683e+00 -1.9800e+03 -8.5714e+01 4.8630e+00 -1.9800e+03 -5.7143e+01 4.8592e+00 -1.9800e+03 -2.8571e+01 4.8570e+00 -1.9800e+03 0.0000e+00 4.8562e+00 -1.9800e+03 2.8571e+01 4.8570e+00 -1.9800e+03 5.7143e+01 4.8592e+00 -1.9800e+03 8.5714e+01 4.8630e+00 -1.9800e+03 1.1429e+02 4.8683e+00 -1.9800e+03 1.4286e+02 4.8751e+00 -1.9800e+03 1.7143e+02 4.8834e+00 -1.9800e+03 2.0000e+02 4.8932e+00 -1.9800e+03 2.2857e+02 4.9043e+00 -1.9800e+03 2.5714e+02 4.9169e+00 -1.9800e+03 2.8571e+02 4.9309e+00 -1.9800e+03 3.1429e+02 4.9462e+00 -1.9800e+03 3.4286e+02 4.9628e+00 -1.9800e+03 3.7143e+02 4.9807e+00 -1.9800e+03 4.0000e+02 4.9999e+00 -1.9800e+03 4.2857e+02 5.0202e+00 -1.9800e+03 4.5714e+02 5.0417e+00 -1.9800e+03 4.8571e+02 5.0643e+00 -1.9800e+03 5.1429e+02 5.0880e+00 -1.9800e+03 5.4286e+02 5.1127e+00 -1.9800e+03 5.7143e+02 5.1384e+00 -1.9800e+03 6.0000e+02 5.1650e+00 -1.9800e+03 6.2857e+02 5.1925e+00 -1.9800e+03 6.5714e+02 5.2208e+00 -1.9800e+03 6.8571e+02 5.2499e+00 -1.9800e+03 7.1429e+02 5.2798e+00 -1.9800e+03 7.4286e+02 5.3103e+00 -1.9800e+03 7.7143e+02 5.3415e+00 -1.9800e+03 8.0000e+02 5.3732e+00 -1.9800e+03 8.2857e+02 5.4055e+00 -1.9800e+03 8.5714e+02 5.4383e+00 -1.9800e+03 8.8571e+02 5.4715e+00 -1.9800e+03 9.1429e+02 5.5051e+00 -1.9800e+03 9.4286e+02 5.5391e+00 -1.9800e+03 9.7143e+02 5.5734e+00 -1.9800e+03 1.0000e+03 5.6081e+00 -1.9800e+03 1.0286e+03 5.6429e+00 -1.9800e+03 1.0571e+03 5.6780e+00 -1.9800e+03 1.0857e+03 5.7132e+00 -1.9800e+03 1.1143e+03 5.7485e+00 -1.9800e+03 1.1429e+03 5.7840e+00 -1.9800e+03 1.1714e+03 5.8195e+00 -1.9800e+03 1.2000e+03 5.8551e+00 -1.9800e+03 1.2286e+03 5.8907e+00 -1.9800e+03 1.2571e+03 5.9262e+00 -1.9800e+03 1.2857e+03 5.9618e+00 -1.9800e+03 1.3143e+03 5.9972e+00 -1.9800e+03 1.3429e+03 6.0326e+00 -1.9800e+03 1.3714e+03 6.0678e+00 -1.9800e+03 1.4000e+03 6.1029e+00 -1.9800e+03 1.4286e+03 6.1379e+00 -1.9800e+03 1.4571e+03 6.1727e+00 -1.9800e+03 1.4857e+03 6.2073e+00 -1.9800e+03 1.5143e+03 6.2417e+00 -1.9800e+03 1.5429e+03 6.2759e+00 -1.9800e+03 1.5714e+03 6.3099e+00 -1.9800e+03 1.6000e+03 6.3436e+00 -1.9800e+03 1.6286e+03 6.3771e+00 -1.9800e+03 1.6571e+03 6.4103e+00 -1.9800e+03 1.6857e+03 6.4432e+00 -1.9800e+03 1.7143e+03 6.4758e+00 -1.9800e+03 1.7429e+03 6.5082e+00 -1.9800e+03 1.7714e+03 6.5403e+00 -1.9800e+03 1.8000e+03 6.5720e+00 -1.9800e+03 1.8286e+03 6.6035e+00 -1.9800e+03 1.8571e+03 6.6346e+00 -1.9800e+03 1.8857e+03 6.6655e+00 -1.9800e+03 1.9143e+03 6.6960e+00 -1.9800e+03 1.9429e+03 6.7262e+00 -1.9800e+03 1.9714e+03 6.7560e+00 -1.9800e+03 2.0000e+03 6.7856e+00 -2.0100e+03 -2.0000e+03 6.8159e+00 -2.0100e+03 -1.9714e+03 6.7872e+00 -2.0100e+03 -1.9429e+03 6.7581e+00 -2.0100e+03 -1.9143e+03 6.7287e+00 -2.0100e+03 -1.8857e+03 6.6991e+00 -2.0100e+03 -1.8571e+03 6.6691e+00 -2.0100e+03 -1.8286e+03 6.6388e+00 -2.0100e+03 -1.8000e+03 6.6083e+00 -2.0100e+03 -1.7714e+03 6.5775e+00 -2.0100e+03 -1.7429e+03 6.5463e+00 -2.0100e+03 -1.7143e+03 6.5150e+00 -2.0100e+03 -1.6857e+03 6.4833e+00 -2.0100e+03 -1.6571e+03 6.4514e+00 -2.0100e+03 -1.6286e+03 6.4193e+00 -2.0100e+03 -1.6000e+03 6.3869e+00 -2.0100e+03 -1.5714e+03 6.3542e+00 -2.0100e+03 -1.5429e+03 6.3214e+00 -2.0100e+03 -1.5143e+03 6.2884e+00 -2.0100e+03 -1.4857e+03 6.2551e+00 -2.0100e+03 -1.4571e+03 6.2217e+00 -2.0100e+03 -1.4286e+03 6.1881e+00 -2.0100e+03 -1.4000e+03 6.1544e+00 -2.0100e+03 -1.3714e+03 6.1206e+00 -2.0100e+03 -1.3429e+03 6.0866e+00 -2.0100e+03 -1.3143e+03 6.0526e+00 -2.0100e+03 -1.2857e+03 6.0185e+00 -2.0100e+03 -1.2571e+03 5.9843e+00 -2.0100e+03 -1.2286e+03 5.9501e+00 -2.0100e+03 -1.2000e+03 5.9160e+00 -2.0100e+03 -1.1714e+03 5.8818e+00 -2.0100e+03 -1.1429e+03 5.8477e+00 -2.0100e+03 -1.1143e+03 5.8137e+00 -2.0100e+03 -1.0857e+03 5.7798e+00 -2.0100e+03 -1.0571e+03 5.7461e+00 -2.0100e+03 -1.0286e+03 5.7125e+00 -2.0100e+03 -1.0000e+03 5.6792e+00 -2.0100e+03 -9.7143e+02 5.6461e+00 -2.0100e+03 -9.4286e+02 5.6133e+00 -2.0100e+03 -9.1429e+02 5.5808e+00 -2.0100e+03 -8.8571e+02 5.5487e+00 -2.0100e+03 -8.5714e+02 5.5170e+00 -2.0100e+03 -8.2857e+02 5.4858e+00 -2.0100e+03 -8.0000e+02 5.4550e+00 -2.0100e+03 -7.7143e+02 5.4248e+00 -2.0100e+03 -7.4286e+02 5.3951e+00 -2.0100e+03 -7.1429e+02 5.3661e+00 -2.0100e+03 -6.8571e+02 5.3377e+00 -2.0100e+03 -6.5714e+02 5.3101e+00 -2.0100e+03 -6.2857e+02 5.2832e+00 -2.0100e+03 -6.0000e+02 5.2571e+00 -2.0100e+03 -5.7143e+02 5.2318e+00 -2.0100e+03 -5.4286e+02 5.2075e+00 -2.0100e+03 -5.1429e+02 5.1840e+00 -2.0100e+03 -4.8571e+02 5.1616e+00 -2.0100e+03 -4.5714e+02 5.1401e+00 -2.0100e+03 -4.2857e+02 5.1198e+00 -2.0100e+03 -4.0000e+02 5.1005e+00 -2.0100e+03 -3.7143e+02 5.0824e+00 -2.0100e+03 -3.4286e+02 5.0654e+00 -2.0100e+03 -3.1429e+02 5.0497e+00 -2.0100e+03 -2.8571e+02 5.0352e+00 -2.0100e+03 -2.5714e+02 5.0220e+00 -2.0100e+03 -2.2857e+02 5.0102e+00 -2.0100e+03 -2.0000e+02 4.9996e+00 -2.0100e+03 -1.7143e+02 4.9904e+00 -2.0100e+03 -1.4286e+02 4.9826e+00 -2.0100e+03 -1.1429e+02 4.9762e+00 -2.0100e+03 -8.5714e+01 4.9712e+00 -2.0100e+03 -5.7143e+01 4.9676e+00 -2.0100e+03 -2.8571e+01 4.9654e+00 -2.0100e+03 0.0000e+00 4.9647e+00 -2.0100e+03 2.8571e+01 4.9654e+00 -2.0100e+03 5.7143e+01 4.9676e+00 -2.0100e+03 8.5714e+01 4.9712e+00 -2.0100e+03 1.1429e+02 4.9762e+00 -2.0100e+03 1.4286e+02 4.9826e+00 -2.0100e+03 1.7143e+02 4.9904e+00 -2.0100e+03 2.0000e+02 4.9996e+00 -2.0100e+03 2.2857e+02 5.0102e+00 -2.0100e+03 2.5714e+02 5.0220e+00 -2.0100e+03 2.8571e+02 5.0352e+00 -2.0100e+03 3.1429e+02 5.0497e+00 -2.0100e+03 3.4286e+02 5.0654e+00 -2.0100e+03 3.7143e+02 5.0824e+00 -2.0100e+03 4.0000e+02 5.1005e+00 -2.0100e+03 4.2857e+02 5.1198e+00 -2.0100e+03 4.5714e+02 5.1401e+00 -2.0100e+03 4.8571e+02 5.1616e+00 -2.0100e+03 5.1429e+02 5.1840e+00 -2.0100e+03 5.4286e+02 5.2075e+00 -2.0100e+03 5.7143e+02 5.2318e+00 -2.0100e+03 6.0000e+02 5.2571e+00 -2.0100e+03 6.2857e+02 5.2832e+00 -2.0100e+03 6.5714e+02 5.3101e+00 -2.0100e+03 6.8571e+02 5.3377e+00 -2.0100e+03 7.1429e+02 5.3661e+00 -2.0100e+03 7.4286e+02 5.3951e+00 -2.0100e+03 7.7143e+02 5.4248e+00 -2.0100e+03 8.0000e+02 5.4550e+00 -2.0100e+03 8.2857e+02 5.4858e+00 -2.0100e+03 8.5714e+02 5.5170e+00 -2.0100e+03 8.8571e+02 5.5487e+00 -2.0100e+03 9.1429e+02 5.5808e+00 -2.0100e+03 9.4286e+02 5.6133e+00 -2.0100e+03 9.7143e+02 5.6461e+00 -2.0100e+03 1.0000e+03 5.6792e+00 -2.0100e+03 1.0286e+03 5.7125e+00 -2.0100e+03 1.0571e+03 5.7461e+00 -2.0100e+03 1.0857e+03 5.7798e+00 -2.0100e+03 1.1143e+03 5.8137e+00 -2.0100e+03 1.1429e+03 5.8477e+00 -2.0100e+03 1.1714e+03 5.8818e+00 -2.0100e+03 1.2000e+03 5.9160e+00 -2.0100e+03 1.2286e+03 5.9501e+00 -2.0100e+03 1.2571e+03 5.9843e+00 -2.0100e+03 1.2857e+03 6.0185e+00 -2.0100e+03 1.3143e+03 6.0526e+00 -2.0100e+03 1.3429e+03 6.0866e+00 -2.0100e+03 1.3714e+03 6.1206e+00 -2.0100e+03 1.4000e+03 6.1544e+00 -2.0100e+03 1.4286e+03 6.1881e+00 -2.0100e+03 1.4571e+03 6.2217e+00 -2.0100e+03 1.4857e+03 6.2551e+00 -2.0100e+03 1.5143e+03 6.2884e+00 -2.0100e+03 1.5429e+03 6.3214e+00 -2.0100e+03 1.5714e+03 6.3542e+00 -2.0100e+03 1.6000e+03 6.3869e+00 -2.0100e+03 1.6286e+03 6.4193e+00 -2.0100e+03 1.6571e+03 6.4514e+00 -2.0100e+03 1.6857e+03 6.4833e+00 -2.0100e+03 1.7143e+03 6.5150e+00 -2.0100e+03 1.7429e+03 6.5463e+00 -2.0100e+03 1.7714e+03 6.5775e+00 -2.0100e+03 1.8000e+03 6.6083e+00 -2.0100e+03 1.8286e+03 6.6388e+00 -2.0100e+03 1.8571e+03 6.6691e+00 -2.0100e+03 1.8857e+03 6.6991e+00 -2.0100e+03 1.9143e+03 6.7287e+00 -2.0100e+03 1.9429e+03 6.7581e+00 -2.0100e+03 1.9714e+03 6.7872e+00 -2.0100e+03 2.0000e+03 6.8159e+00 -2.0400e+03 -2.0000e+03 6.8459e+00 -2.0400e+03 -1.9714e+03 6.8179e+00 -2.0400e+03 -1.9429e+03 6.7897e+00 -2.0400e+03 -1.9143e+03 6.7611e+00 -2.0400e+03 -1.8857e+03 6.7322e+00 -2.0400e+03 -1.8571e+03 6.7031e+00 -2.0400e+03 -1.8286e+03 6.6737e+00 -2.0400e+03 -1.8000e+03 6.6440e+00 -2.0400e+03 -1.7714e+03 6.6141e+00 -2.0400e+03 -1.7429e+03 6.5839e+00 -2.0400e+03 -1.7143e+03 6.5535e+00 -2.0400e+03 -1.6857e+03 6.5228e+00 -2.0400e+03 -1.6571e+03 6.4919e+00 -2.0400e+03 -1.6286e+03 6.4607e+00 -2.0400e+03 -1.6000e+03 6.4294e+00 -2.0400e+03 -1.5714e+03 6.3978e+00 -2.0400e+03 -1.5429e+03 6.3660e+00 -2.0400e+03 -1.5143e+03 6.3341e+00 -2.0400e+03 -1.4857e+03 6.3020e+00 -2.0400e+03 -1.4571e+03 6.2697e+00 -2.0400e+03 -1.4286e+03 6.2373e+00 -2.0400e+03 -1.4000e+03 6.2048e+00 -2.0400e+03 -1.3714e+03 6.1722e+00 -2.0400e+03 -1.3429e+03 6.1395e+00 -2.0400e+03 -1.3143e+03 6.1067e+00 -2.0400e+03 -1.2857e+03 6.0738e+00 -2.0400e+03 -1.2571e+03 6.0410e+00 -2.0400e+03 -1.2286e+03 6.0081e+00 -2.0400e+03 -1.2000e+03 5.9753e+00 -2.0400e+03 -1.1714e+03 5.9425e+00 -2.0400e+03 -1.1429e+03 5.9098e+00 -2.0400e+03 -1.1143e+03 5.8772e+00 -2.0400e+03 -1.0857e+03 5.8447e+00 -2.0400e+03 -1.0571e+03 5.8123e+00 -2.0400e+03 -1.0286e+03 5.7802e+00 -2.0400e+03 -1.0000e+03 5.7483e+00 -2.0400e+03 -9.7143e+02 5.7166e+00 -2.0400e+03 -9.4286e+02 5.6853e+00 -2.0400e+03 -9.1429e+02 5.6543e+00 -2.0400e+03 -8.8571e+02 5.6236e+00 -2.0400e+03 -8.5714e+02 5.5933e+00 -2.0400e+03 -8.2857e+02 5.5635e+00 -2.0400e+03 -8.0000e+02 5.5342e+00 -2.0400e+03 -7.7143e+02 5.5054e+00 -2.0400e+03 -7.4286e+02 5.4771e+00 -2.0400e+03 -7.1429e+02 5.4495e+00 -2.0400e+03 -6.8571e+02 5.4225e+00 -2.0400e+03 -6.5714e+02 5.3962e+00 -2.0400e+03 -6.2857e+02 5.3706e+00 -2.0400e+03 -6.0000e+02 5.3458e+00 -2.0400e+03 -5.7143e+02 5.3218e+00 -2.0400e+03 -5.4286e+02 5.2987e+00 -2.0400e+03 -5.1429e+02 5.2764e+00 -2.0400e+03 -4.8571e+02 5.2551e+00 -2.0400e+03 -4.5714e+02 5.2348e+00 -2.0400e+03 -4.2857e+02 5.2155e+00 -2.0400e+03 -4.0000e+02 5.1973e+00 -2.0400e+03 -3.7143e+02 5.1801e+00 -2.0400e+03 -3.4286e+02 5.1640e+00 -2.0400e+03 -3.1429e+02 5.1492e+00 -2.0400e+03 -2.8571e+02 5.1355e+00 -2.0400e+03 -2.5714e+02 5.1230e+00 -2.0400e+03 -2.2857e+02 5.1117e+00 -2.0400e+03 -2.0000e+02 5.1017e+00 -2.0400e+03 -1.7143e+02 5.0930e+00 -2.0400e+03 -1.4286e+02 5.0857e+00 -2.0400e+03 -1.1429e+02 5.0796e+00 -2.0400e+03 -8.5714e+01 5.0748e+00 -2.0400e+03 -5.7143e+01 5.0715e+00 -2.0400e+03 -2.8571e+01 5.0694e+00 -2.0400e+03 0.0000e+00 5.0687e+00 -2.0400e+03 2.8571e+01 5.0694e+00 -2.0400e+03 5.7143e+01 5.0715e+00 -2.0400e+03 8.5714e+01 5.0748e+00 -2.0400e+03 1.1429e+02 5.0796e+00 -2.0400e+03 1.4286e+02 5.0857e+00 -2.0400e+03 1.7143e+02 5.0930e+00 -2.0400e+03 2.0000e+02 5.1017e+00 -2.0400e+03 2.2857e+02 5.1117e+00 -2.0400e+03 2.5714e+02 5.1230e+00 -2.0400e+03 2.8571e+02 5.1355e+00 -2.0400e+03 3.1429e+02 5.1492e+00 -2.0400e+03 3.4286e+02 5.1640e+00 -2.0400e+03 3.7143e+02 5.1801e+00 -2.0400e+03 4.0000e+02 5.1973e+00 -2.0400e+03 4.2857e+02 5.2155e+00 -2.0400e+03 4.5714e+02 5.2348e+00 -2.0400e+03 4.8571e+02 5.2551e+00 -2.0400e+03 5.1429e+02 5.2764e+00 -2.0400e+03 5.4286e+02 5.2987e+00 -2.0400e+03 5.7143e+02 5.3218e+00 -2.0400e+03 6.0000e+02 5.3458e+00 -2.0400e+03 6.2857e+02 5.3706e+00 -2.0400e+03 6.5714e+02 5.3962e+00 -2.0400e+03 6.8571e+02 5.4225e+00 -2.0400e+03 7.1429e+02 5.4495e+00 -2.0400e+03 7.4286e+02 5.4771e+00 -2.0400e+03 7.7143e+02 5.5054e+00 -2.0400e+03 8.0000e+02 5.5342e+00 -2.0400e+03 8.2857e+02 5.5635e+00 -2.0400e+03 8.5714e+02 5.5933e+00 -2.0400e+03 8.8571e+02 5.6236e+00 -2.0400e+03 9.1429e+02 5.6543e+00 -2.0400e+03 9.4286e+02 5.6853e+00 -2.0400e+03 9.7143e+02 5.7166e+00 -2.0400e+03 1.0000e+03 5.7483e+00 -2.0400e+03 1.0286e+03 5.7802e+00 -2.0400e+03 1.0571e+03 5.8123e+00 -2.0400e+03 1.0857e+03 5.8447e+00 -2.0400e+03 1.1143e+03 5.8772e+00 -2.0400e+03 1.1429e+03 5.9098e+00 -2.0400e+03 1.1714e+03 5.9425e+00 -2.0400e+03 1.2000e+03 5.9753e+00 -2.0400e+03 1.2286e+03 6.0081e+00 -2.0400e+03 1.2571e+03 6.0410e+00 -2.0400e+03 1.2857e+03 6.0738e+00 -2.0400e+03 1.3143e+03 6.1067e+00 -2.0400e+03 1.3429e+03 6.1395e+00 -2.0400e+03 1.3714e+03 6.1722e+00 -2.0400e+03 1.4000e+03 6.2048e+00 -2.0400e+03 1.4286e+03 6.2373e+00 -2.0400e+03 1.4571e+03 6.2697e+00 -2.0400e+03 1.4857e+03 6.3020e+00 -2.0400e+03 1.5143e+03 6.3341e+00 -2.0400e+03 1.5429e+03 6.3660e+00 -2.0400e+03 1.5714e+03 6.3978e+00 -2.0400e+03 1.6000e+03 6.4294e+00 -2.0400e+03 1.6286e+03 6.4607e+00 -2.0400e+03 1.6571e+03 6.4919e+00 -2.0400e+03 1.6857e+03 6.5228e+00 -2.0400e+03 1.7143e+03 6.5535e+00 -2.0400e+03 1.7429e+03 6.5839e+00 -2.0400e+03 1.7714e+03 6.6141e+00 -2.0400e+03 1.8000e+03 6.6440e+00 -2.0400e+03 1.8286e+03 6.6737e+00 -2.0400e+03 1.8571e+03 6.7031e+00 -2.0400e+03 1.8857e+03 6.7322e+00 -2.0400e+03 1.9143e+03 6.7611e+00 -2.0400e+03 1.9429e+03 6.7897e+00 -2.0400e+03 1.9714e+03 6.8179e+00 -2.0400e+03 2.0000e+03 6.8459e+00 -2.0700e+03 -2.0000e+03 6.8756e+00 -2.0700e+03 -1.9714e+03 6.8483e+00 -2.0700e+03 -1.9429e+03 6.8208e+00 -2.0700e+03 -1.9143e+03 6.7930e+00 -2.0700e+03 -1.8857e+03 6.7649e+00 -2.0700e+03 -1.8571e+03 6.7366e+00 -2.0700e+03 -1.8286e+03 6.7080e+00 -2.0700e+03 -1.8000e+03 6.6792e+00 -2.0700e+03 -1.7714e+03 6.6502e+00 -2.0700e+03 -1.7429e+03 6.6209e+00 -2.0700e+03 -1.7143e+03 6.5913e+00 -2.0700e+03 -1.6857e+03 6.5616e+00 -2.0700e+03 -1.6571e+03 6.5316e+00 -2.0700e+03 -1.6286e+03 6.5015e+00 -2.0700e+03 -1.6000e+03 6.4711e+00 -2.0700e+03 -1.5714e+03 6.4406e+00 -2.0700e+03 -1.5429e+03 6.4098e+00 -2.0700e+03 -1.5143e+03 6.3790e+00 -2.0700e+03 -1.4857e+03 6.3479e+00 -2.0700e+03 -1.4571e+03 6.3168e+00 -2.0700e+03 -1.4286e+03 6.2855e+00 -2.0700e+03 -1.4000e+03 6.2541e+00 -2.0700e+03 -1.3714e+03 6.2227e+00 -2.0700e+03 -1.3429e+03 6.1911e+00 -2.0700e+03 -1.3143e+03 6.1595e+00 -2.0700e+03 -1.2857e+03 6.1279e+00 -2.0700e+03 -1.2571e+03 6.0963e+00 -2.0700e+03 -1.2286e+03 6.0647e+00 -2.0700e+03 -1.2000e+03 6.0331e+00 -2.0700e+03 -1.1714e+03 6.0016e+00 -2.0700e+03 -1.1429e+03 5.9702e+00 -2.0700e+03 -1.1143e+03 5.9389e+00 -2.0700e+03 -1.0857e+03 5.9077e+00 -2.0700e+03 -1.0571e+03 5.8768e+00 -2.0700e+03 -1.0286e+03 5.8460e+00 -2.0700e+03 -1.0000e+03 5.8154e+00 -2.0700e+03 -9.7143e+02 5.7851e+00 -2.0700e+03 -9.4286e+02 5.7551e+00 -2.0700e+03 -9.1429e+02 5.7254e+00 -2.0700e+03 -8.8571e+02 5.6961e+00 -2.0700e+03 -8.5714e+02 5.6672e+00 -2.0700e+03 -8.2857e+02 5.6388e+00 -2.0700e+03 -8.0000e+02 5.6108e+00 -2.0700e+03 -7.7143e+02 5.5833e+00 -2.0700e+03 -7.4286e+02 5.5564e+00 -2.0700e+03 -7.1429e+02 5.5300e+00 -2.0700e+03 -6.8571e+02 5.5043e+00 -2.0700e+03 -6.5714e+02 5.4793e+00 -2.0700e+03 -6.2857e+02 5.4550e+00 -2.0700e+03 -6.0000e+02 5.4314e+00 -2.0700e+03 -5.7143e+02 5.4086e+00 -2.0700e+03 -5.4286e+02 5.3866e+00 -2.0700e+03 -5.1429e+02 5.3655e+00 -2.0700e+03 -4.8571e+02 5.3452e+00 -2.0700e+03 -4.5714e+02 5.3260e+00 -2.0700e+03 -4.2857e+02 5.3076e+00 -2.0700e+03 -4.0000e+02 5.2903e+00 -2.0700e+03 -3.7143e+02 5.2740e+00 -2.0700e+03 -3.4286e+02 5.2588e+00 -2.0700e+03 -3.1429e+02 5.2447e+00 -2.0700e+03 -2.8571e+02 5.2317e+00 -2.0700e+03 -2.5714e+02 5.2199e+00 -2.0700e+03 -2.2857e+02 5.2093e+00 -2.0700e+03 -2.0000e+02 5.1998e+00 -2.0700e+03 -1.7143e+02 5.1916e+00 -2.0700e+03 -1.4286e+02 5.1846e+00 -2.0700e+03 -1.1429e+02 5.1788e+00 -2.0700e+03 -8.5714e+01 5.1744e+00 -2.0700e+03 -5.7143e+01 5.1712e+00 -2.0700e+03 -2.8571e+01 5.1692e+00 -2.0700e+03 0.0000e+00 5.1686e+00 -2.0700e+03 2.8571e+01 5.1692e+00 -2.0700e+03 5.7143e+01 5.1712e+00 -2.0700e+03 8.5714e+01 5.1744e+00 -2.0700e+03 1.1429e+02 5.1788e+00 -2.0700e+03 1.4286e+02 5.1846e+00 -2.0700e+03 1.7143e+02 5.1916e+00 -2.0700e+03 2.0000e+02 5.1998e+00 -2.0700e+03 2.2857e+02 5.2093e+00 -2.0700e+03 2.5714e+02 5.2199e+00 -2.0700e+03 2.8571e+02 5.2317e+00 -2.0700e+03 3.1429e+02 5.2447e+00 -2.0700e+03 3.4286e+02 5.2588e+00 -2.0700e+03 3.7143e+02 5.2740e+00 -2.0700e+03 4.0000e+02 5.2903e+00 -2.0700e+03 4.2857e+02 5.3076e+00 -2.0700e+03 4.5714e+02 5.3260e+00 -2.0700e+03 4.8571e+02 5.3452e+00 -2.0700e+03 5.1429e+02 5.3655e+00 -2.0700e+03 5.4286e+02 5.3866e+00 -2.0700e+03 5.7143e+02 5.4086e+00 -2.0700e+03 6.0000e+02 5.4314e+00 -2.0700e+03 6.2857e+02 5.4550e+00 -2.0700e+03 6.5714e+02 5.4793e+00 -2.0700e+03 6.8571e+02 5.5043e+00 -2.0700e+03 7.1429e+02 5.5300e+00 -2.0700e+03 7.4286e+02 5.5564e+00 -2.0700e+03 7.7143e+02 5.5833e+00 -2.0700e+03 8.0000e+02 5.6108e+00 -2.0700e+03 8.2857e+02 5.6388e+00 -2.0700e+03 8.5714e+02 5.6672e+00 -2.0700e+03 8.8571e+02 5.6961e+00 -2.0700e+03 9.1429e+02 5.7254e+00 -2.0700e+03 9.4286e+02 5.7551e+00 -2.0700e+03 9.7143e+02 5.7851e+00 -2.0700e+03 1.0000e+03 5.8154e+00 -2.0700e+03 1.0286e+03 5.8460e+00 -2.0700e+03 1.0571e+03 5.8768e+00 -2.0700e+03 1.0857e+03 5.9077e+00 -2.0700e+03 1.1143e+03 5.9389e+00 -2.0700e+03 1.1429e+03 5.9702e+00 -2.0700e+03 1.1714e+03 6.0016e+00 -2.0700e+03 1.2000e+03 6.0331e+00 -2.0700e+03 1.2286e+03 6.0647e+00 -2.0700e+03 1.2571e+03 6.0963e+00 -2.0700e+03 1.2857e+03 6.1279e+00 -2.0700e+03 1.3143e+03 6.1595e+00 -2.0700e+03 1.3429e+03 6.1911e+00 -2.0700e+03 1.3714e+03 6.2227e+00 -2.0700e+03 1.4000e+03 6.2541e+00 -2.0700e+03 1.4286e+03 6.2855e+00 -2.0700e+03 1.4571e+03 6.3168e+00 -2.0700e+03 1.4857e+03 6.3479e+00 -2.0700e+03 1.5143e+03 6.3790e+00 -2.0700e+03 1.5429e+03 6.4098e+00 -2.0700e+03 1.5714e+03 6.4406e+00 -2.0700e+03 1.6000e+03 6.4711e+00 -2.0700e+03 1.6286e+03 6.5015e+00 -2.0700e+03 1.6571e+03 6.5316e+00 -2.0700e+03 1.6857e+03 6.5616e+00 -2.0700e+03 1.7143e+03 6.5913e+00 -2.0700e+03 1.7429e+03 6.6209e+00 -2.0700e+03 1.7714e+03 6.6502e+00 -2.0700e+03 1.8000e+03 6.6792e+00 -2.0700e+03 1.8286e+03 6.7080e+00 -2.0700e+03 1.8571e+03 6.7366e+00 -2.0700e+03 1.8857e+03 6.7649e+00 -2.0700e+03 1.9143e+03 6.7930e+00 -2.0700e+03 1.9429e+03 6.8208e+00 -2.0700e+03 1.9714e+03 6.8483e+00 -2.0700e+03 2.0000e+03 6.8756e+00 -2.1000e+03 -2.0000e+03 6.9049e+00 -2.1000e+03 -1.9714e+03 6.8784e+00 -2.1000e+03 -1.9429e+03 6.8516e+00 -2.1000e+03 -1.9143e+03 6.8245e+00 -2.1000e+03 -1.8857e+03 6.7972e+00 -2.1000e+03 -1.8571e+03 6.7697e+00 -2.1000e+03 -1.8286e+03 6.7419e+00 -2.1000e+03 -1.8000e+03 6.7139e+00 -2.1000e+03 -1.7714e+03 6.6857e+00 -2.1000e+03 -1.7429e+03 6.6573e+00 -2.1000e+03 -1.7143e+03 6.6286e+00 -2.1000e+03 -1.6857e+03 6.5998e+00 -2.1000e+03 -1.6571e+03 6.5707e+00 -2.1000e+03 -1.6286e+03 6.5415e+00 -2.1000e+03 -1.6000e+03 6.5121e+00 -2.1000e+03 -1.5714e+03 6.4825e+00 -2.1000e+03 -1.5429e+03 6.4528e+00 -2.1000e+03 -1.5143e+03 6.4230e+00 -2.1000e+03 -1.4857e+03 6.3930e+00 -2.1000e+03 -1.4571e+03 6.3629e+00 -2.1000e+03 -1.4286e+03 6.3327e+00 -2.1000e+03 -1.4000e+03 6.3024e+00 -2.1000e+03 -1.3714e+03 6.2720e+00 -2.1000e+03 -1.3429e+03 6.2416e+00 -2.1000e+03 -1.3143e+03 6.2112e+00 -2.1000e+03 -1.2857e+03 6.1808e+00 -2.1000e+03 -1.2571e+03 6.1503e+00 -2.1000e+03 -1.2286e+03 6.1199e+00 -2.1000e+03 -1.2000e+03 6.0896e+00 -2.1000e+03 -1.1714e+03 6.0593e+00 -2.1000e+03 -1.1429e+03 6.0291e+00 -2.1000e+03 -1.1143e+03 5.9990e+00 -2.1000e+03 -1.0857e+03 5.9691e+00 -2.1000e+03 -1.0571e+03 5.9394e+00 -2.1000e+03 -1.0286e+03 5.9099e+00 -2.1000e+03 -1.0000e+03 5.8806e+00 -2.1000e+03 -9.7143e+02 5.8516e+00 -2.1000e+03 -9.4286e+02 5.8229e+00 -2.1000e+03 -9.1429e+02 5.7945e+00 -2.1000e+03 -8.8571e+02 5.7665e+00 -2.1000e+03 -8.5714e+02 5.7389e+00 -2.1000e+03 -8.2857e+02 5.7117e+00 -2.1000e+03 -8.0000e+02 5.6849e+00 -2.1000e+03 -7.7143e+02 5.6587e+00 -2.1000e+03 -7.4286e+02 5.6330e+00 -2.1000e+03 -7.1429e+02 5.6079e+00 -2.1000e+03 -6.8571e+02 5.5834e+00 -2.1000e+03 -6.5714e+02 5.5596e+00 -2.1000e+03 -6.2857e+02 5.5364e+00 -2.1000e+03 -6.0000e+02 5.5140e+00 -2.1000e+03 -5.7143e+02 5.4923e+00 -2.1000e+03 -5.4286e+02 5.4714e+00 -2.1000e+03 -5.1429e+02 5.4513e+00 -2.1000e+03 -4.8571e+02 5.4321e+00 -2.1000e+03 -4.5714e+02 5.4137e+00 -2.1000e+03 -4.2857e+02 5.3963e+00 -2.1000e+03 -4.0000e+02 5.3799e+00 -2.1000e+03 -3.7143e+02 5.3645e+00 -2.1000e+03 -3.4286e+02 5.3500e+00 -2.1000e+03 -3.1429e+02 5.3366e+00 -2.1000e+03 -2.8571e+02 5.3243e+00 -2.1000e+03 -2.5714e+02 5.3131e+00 -2.1000e+03 -2.2857e+02 5.3030e+00 -2.1000e+03 -2.0000e+02 5.2941e+00 -2.1000e+03 -1.7143e+02 5.2863e+00 -2.1000e+03 -1.4286e+02 5.2796e+00 -2.1000e+03 -1.1429e+02 5.2742e+00 -2.1000e+03 -8.5714e+01 5.2700e+00 -2.1000e+03 -5.7143e+01 5.2669e+00 -2.1000e+03 -2.8571e+01 5.2651e+00 -2.1000e+03 0.0000e+00 5.2645e+00 -2.1000e+03 2.8571e+01 5.2651e+00 -2.1000e+03 5.7143e+01 5.2669e+00 -2.1000e+03 8.5714e+01 5.2700e+00 -2.1000e+03 1.1429e+02 5.2742e+00 -2.1000e+03 1.4286e+02 5.2796e+00 -2.1000e+03 1.7143e+02 5.2863e+00 -2.1000e+03 2.0000e+02 5.2941e+00 -2.1000e+03 2.2857e+02 5.3030e+00 -2.1000e+03 2.5714e+02 5.3131e+00 -2.1000e+03 2.8571e+02 5.3243e+00 -2.1000e+03 3.1429e+02 5.3366e+00 -2.1000e+03 3.4286e+02 5.3500e+00 -2.1000e+03 3.7143e+02 5.3645e+00 -2.1000e+03 4.0000e+02 5.3799e+00 -2.1000e+03 4.2857e+02 5.3963e+00 -2.1000e+03 4.5714e+02 5.4137e+00 -2.1000e+03 4.8571e+02 5.4321e+00 -2.1000e+03 5.1429e+02 5.4513e+00 -2.1000e+03 5.4286e+02 5.4714e+00 -2.1000e+03 5.7143e+02 5.4923e+00 -2.1000e+03 6.0000e+02 5.5140e+00 -2.1000e+03 6.2857e+02 5.5364e+00 -2.1000e+03 6.5714e+02 5.5596e+00 -2.1000e+03 6.8571e+02 5.5834e+00 -2.1000e+03 7.1429e+02 5.6079e+00 -2.1000e+03 7.4286e+02 5.6330e+00 -2.1000e+03 7.7143e+02 5.6587e+00 -2.1000e+03 8.0000e+02 5.6849e+00 -2.1000e+03 8.2857e+02 5.7117e+00 -2.1000e+03 8.5714e+02 5.7389e+00 -2.1000e+03 8.8571e+02 5.7665e+00 -2.1000e+03 9.1429e+02 5.7945e+00 -2.1000e+03 9.4286e+02 5.8229e+00 -2.1000e+03 9.7143e+02 5.8516e+00 -2.1000e+03 1.0000e+03 5.8806e+00 -2.1000e+03 1.0286e+03 5.9099e+00 -2.1000e+03 1.0571e+03 5.9394e+00 -2.1000e+03 1.0857e+03 5.9691e+00 -2.1000e+03 1.1143e+03 5.9990e+00 -2.1000e+03 1.1429e+03 6.0291e+00 -2.1000e+03 1.1714e+03 6.0593e+00 -2.1000e+03 1.2000e+03 6.0896e+00 -2.1000e+03 1.2286e+03 6.1199e+00 -2.1000e+03 1.2571e+03 6.1503e+00 -2.1000e+03 1.2857e+03 6.1808e+00 -2.1000e+03 1.3143e+03 6.2112e+00 -2.1000e+03 1.3429e+03 6.2416e+00 -2.1000e+03 1.3714e+03 6.2720e+00 -2.1000e+03 1.4000e+03 6.3024e+00 -2.1000e+03 1.4286e+03 6.3327e+00 -2.1000e+03 1.4571e+03 6.3629e+00 -2.1000e+03 1.4857e+03 6.3930e+00 -2.1000e+03 1.5143e+03 6.4230e+00 -2.1000e+03 1.5429e+03 6.4528e+00 -2.1000e+03 1.5714e+03 6.4825e+00 -2.1000e+03 1.6000e+03 6.5121e+00 -2.1000e+03 1.6286e+03 6.5415e+00 -2.1000e+03 1.6571e+03 6.5707e+00 -2.1000e+03 1.6857e+03 6.5998e+00 -2.1000e+03 1.7143e+03 6.6286e+00 -2.1000e+03 1.7429e+03 6.6573e+00 -2.1000e+03 1.7714e+03 6.6857e+00 -2.1000e+03 1.8000e+03 6.7139e+00 -2.1000e+03 1.8286e+03 6.7419e+00 -2.1000e+03 1.8571e+03 6.7697e+00 -2.1000e+03 1.8857e+03 6.7972e+00 -2.1000e+03 1.9143e+03 6.8245e+00 -2.1000e+03 1.9429e+03 6.8516e+00 -2.1000e+03 1.9714e+03 6.8784e+00 -2.1000e+03 2.0000e+03 6.9049e+00 diff --git a/examples/04-dcip/plot_dc_analytic.py b/examples/04-dcip/plot_dc_analytic.py index b47ba4ed2a..b926c247a3 100644 --- a/examples/04-dcip/plot_dc_analytic.py +++ b/examples/04-dcip/plot_dc_analytic.py @@ -12,11 +12,6 @@ import matplotlib.pyplot as plt from simpeg.electromagnetics.static import resistivity as DC -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - cs = 25.0 hx = [(cs, 7, -1.3), (cs, 21), (cs, 7, 1.3)] @@ -35,9 +30,7 @@ rx = DC.Rx.Dipole(xyz_rxP, xyz_rxN) src = DC.Src.Dipole([rx], np.r_[-200, 0, -12.5], np.r_[+200, 0, -12.5]) survey = DC.Survey([src]) -sim = DC.Simulation3DCellCentered( - mesh, survey=survey, solver=Solver, sigma=sigma, bc_type="Neumann" -) +sim = DC.Simulation3DCellCentered(mesh, survey=survey, sigma=sigma, bc_type="Neumann") data = sim.dpred() diff --git a/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.py b/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.py index 7f1ba7dfd2..b198c330d8 100644 --- a/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.py +++ b/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.py @@ -31,11 +31,6 @@ import numpy as np import matplotlib.pyplot as plt -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - np.random.seed(12345) # 3D Mesh @@ -153,10 +148,10 @@ def getCylinderPoints(xc, zc, r): # Setup Problem with exponential mapping and Active cells only in the core mesh expmap = maps.ExpMap(mesh) -mapactive = maps.InjectActiveCells(mesh=mesh, indActive=actind, valInactive=-5.0) +mapactive = maps.InjectActiveCells(mesh=mesh, active_cells=actind, value_inactive=-5.0) mapping = expmap * mapactive problem = DC.Simulation3DCellCentered( - mesh, survey=survey, sigmaMap=mapping, solver=Solver, bc_type="Neumann" + mesh, survey=survey, sigmaMap=mapping, bc_type="Neumann" ) data = problem.make_synthetic_data(mtrue[actind], relative_error=0.05, add_noise=True) diff --git a/examples/04-dcip/plot_inv_dcip_dipoledipole_parametric_inversion.py b/examples/04-dcip/plot_inv_dcip_dipoledipole_parametric_inversion.py index 8304fce3d6..fec53a57d6 100644 --- a/examples/04-dcip/plot_inv_dcip_dipoledipole_parametric_inversion.py +++ b/examples/04-dcip/plot_inv_dcip_dipoledipole_parametric_inversion.py @@ -34,11 +34,6 @@ import numpy as np from pylab import hist -try: - from pymatsolver import PardisoSolver as Solver -except ImportError: - from simpeg import SolverLU as Solver - def run( plotIt=True, @@ -149,7 +144,10 @@ def run( # Generate 2.5D DC problem # "N" means potential is defined at nodes prb = DC.Simulation2DNodal( - mesh, survey=survey, rhoMap=mapping, storeJ=True, solver=Solver + mesh, + survey=survey, + rhoMap=mapping, + storeJ=True, ) # Make synthetic DC data with 5% Gaussian noise diff --git a/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py b/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py index 43699d6743..99bb11c07b 100644 --- a/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py +++ b/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py @@ -16,10 +16,6 @@ import matplotlib.pyplot as plt import time -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver import discretize from simpeg import ( @@ -208,9 +204,7 @@ def interface(x): # create the survey and problem objects for running the forward simulation survey = FDEM.Survey(source_list) -prob = FDEM.Simulation3DMagneticFluxDensity( - mesh, survey=survey, sigmaMap=mapping, solver=Solver -) +prob = FDEM.Simulation3DMagneticFluxDensity(mesh, survey=survey, sigmaMap=mapping) ############################################################################### # Set up data for inversion @@ -285,10 +279,10 @@ def plot_data(data, ax=None, color="C0", label=""): dmisfit = data_misfit.L2DataMisfit(simulation=prob, data=data) reg = regularization.WeightedLeastSquares(inversion_mesh) -opt = optimization.InexactGaussNewton(maxIterCG=10, remember="xc") +opt = optimization.InexactGaussNewton(cg_maxiter=10, remember="xc") invProb = inverse_problem.BaseInvProblem(dmisfit, reg, opt) -betaest = directives.BetaEstimate_ByEig(beta0_ratio=0.05, n_pw_iter=1, seed=1) +betaest = directives.BetaEstimate_ByEig(beta0_ratio=0.05, n_pw_iter=1, random_seed=1) target = directives.TargetMisfit() directiveList = [betaest, target] diff --git a/examples/06-tdem/plot_fwd_tdem_3d_model.py b/examples/06-tdem/plot_fwd_tdem_3d_model.py index def2b65a74..dbe0d774e4 100644 --- a/examples/06-tdem/plot_fwd_tdem_3d_model.py +++ b/examples/06-tdem/plot_fwd_tdem_3d_model.py @@ -6,10 +6,6 @@ import empymod import discretize -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver import numpy as np from simpeg import maps @@ -316,7 +312,6 @@ mesh, survey=survey, rhoMap=maps.IdentityMap(mesh), - solver=Solver, time_steps=time_steps, ) diff --git a/examples/06-tdem/plot_fwd_tdem_inductive_src_permeable_target.py b/examples/06-tdem/plot_fwd_tdem_inductive_src_permeable_target.py index 68f0b668b0..5c18bf377f 100644 --- a/examples/06-tdem/plot_fwd_tdem_inductive_src_permeable_target.py +++ b/examples/06-tdem/plot_fwd_tdem_inductive_src_permeable_target.py @@ -17,10 +17,6 @@ from matplotlib.colors import LogNorm from scipy.constants import mu_0 -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver import time from simpeg.electromagnetics import time_domain as TDEM @@ -192,14 +188,12 @@ survey=survey_magnetostatic, sigmaMap=maps.IdentityMap(mesh), time_steps=ramp, - solver=Solver, ) prob_ramp_on = TDEM.Simulation3DMagneticFluxDensity( mesh=mesh, survey=survey_ramp_on, sigmaMap=maps.IdentityMap(mesh), time_steps=ramp, - solver=Solver, ) ############################################################################### diff --git a/examples/06-tdem/plot_inv_tdem_1D.py b/examples/06-tdem/plot_inv_tdem_1D.py index 992dbb13fb..8a581ba273 100644 --- a/examples/06-tdem/plot_inv_tdem_1D.py +++ b/examples/06-tdem/plot_inv_tdem_1D.py @@ -56,7 +56,7 @@ def run(plotIt=True): data = simulation.make_synthetic_data(mtrue, relative_error=rel_err) dmisfit = data_misfit.L2DataMisfit(simulation=simulation, data=data) - regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) + regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].active_cells]]) reg = regularization.WeightedLeastSquares(regMesh, alpha_s=1e-2, alpha_x=1.0) opt = optimization.InexactGaussNewton(maxIter=5, LSshorten=0.5) invProb = inverse_problem.BaseInvProblem(dmisfit, reg, opt) diff --git a/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py b/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py index 06b793bb51..2ad881b0fc 100644 --- a/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py +++ b/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py @@ -23,11 +23,6 @@ import matplotlib.pyplot as plt from scipy.interpolate import interp1d -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - def run(plotIt=True): cs, ncx, ncz, npad = 5.0, 25, 24, 15 @@ -50,7 +45,7 @@ def run(plotIt=True): x = np.r_[30, 50, 70, 90] rxloc = np.c_[x, x * 0.0, np.zeros_like(x)] - prb = TDEM.Simulation3DMagneticFluxDensity(mesh, sigmaMap=mapping, solver=Solver) + prb = TDEM.Simulation3DMagneticFluxDensity(mesh, sigmaMap=mapping) prb.time_steps = [ (1e-3, 5), (1e-4, 5), @@ -80,7 +75,7 @@ def run(plotIt=True): data = prb.make_synthetic_data(mtrue, relative_error=0.02, noise_floor=1e-11) dmisfit = data_misfit.L2DataMisfit(simulation=prb, data=data) - regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) + regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].active_cells]]) reg = regularization.WeightedLeastSquares(regMesh) opt = optimization.InexactGaussNewton(maxIter=5, LSshorten=0.5) invProb = inverse_problem.BaseInvProblem(dmisfit, reg, opt) diff --git a/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py b/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py index c5627008e7..014b112a6b 100644 --- a/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py +++ b/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py @@ -14,11 +14,6 @@ import numpy as np import matplotlib.pyplot as plt -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import Solver - def run(plotIt=True): """ @@ -61,11 +56,19 @@ def run(plotIt=True): # Make a receiver list receiver_list = [] for rx_orientation in ["xx", "xy", "yx", "yy"]: - receiver_list.append(NSEM.Rx.PointNaturalSource(rx_loc, rx_orientation, "real")) - receiver_list.append(NSEM.Rx.PointNaturalSource(rx_loc, rx_orientation, "imag")) + receiver_list.append( + NSEM.Rx.Impedance(rx_loc, orientation=rx_orientation, component="real") + ) + receiver_list.append( + NSEM.Rx.Impedance(rx_loc, orientation=rx_orientation, component="imag") + ) for rx_orientation in ["zx", "zy"]: - receiver_list.append(NSEM.Rx.Point3DTipper(rx_loc, rx_orientation, "real")) - receiver_list.append(NSEM.Rx.Point3DTipper(rx_loc, rx_orientation, "imag")) + receiver_list.append( + NSEM.Rx.Tipper(rx_loc, orientation=rx_orientation, component="real") + ) + receiver_list.append( + NSEM.Rx.Tipper(rx_loc, orientation=rx_orientation, component="imag") + ) # Source list source_list = [ @@ -79,7 +82,6 @@ def run(plotIt=True): problem = NSEM.Simulation3DPrimarySecondary( M, survey=survey, - solver=Solver, sigma=sig, sigmaPrimary=sigBG, forward_only=True, diff --git a/examples/08-vrm/plot_fwd_vrm.py b/examples/08-vrm/plot_fwd_vrm.py index fba26e608f..9b87566329 100644 --- a/examples/08-vrm/plot_fwd_vrm.py +++ b/examples/08-vrm/plot_fwd_vrm.py @@ -110,7 +110,7 @@ problem_vrm = VRM.Simulation3DLinear( mesh, survey=survey_vrm, - indActive=topoCells, + active_cells=topoCells, refinement_factor=3, refinement_distance=[1.25, 2.5, 3.75], ) diff --git a/examples/08-vrm/plot_inv_vrm_eq.py b/examples/08-vrm/plot_inv_vrm_eq.py index a8558023c8..60e9176edb 100644 --- a/examples/08-vrm/plot_inv_vrm_eq.py +++ b/examples/08-vrm/plot_inv_vrm_eq.py @@ -124,7 +124,7 @@ problem_vrm = VRM.Simulation3DLinear( mesh, survey=survey_vrm, - indActive=topoCells, + active_cells=topoCells, refinement_factor=3, refinement_distance=[1.25, 2.5, 3.75], ) @@ -176,7 +176,7 @@ problem_inv = VRM.Simulation3DLinear( mesh, survey=survey_vrm, - indActive=actCells, + active_cells=actCells, refinement_factor=3, refinement_distance=[1.25, 2.5, 3.75], ) @@ -201,7 +201,7 @@ ) opt = optimization.ProjectedGNCG( - maxIter=20, lower=0.0, upper=1e-2, maxIterLS=20, tolCG=1e-4 + maxIter=20, lower=0.0, upper=1e-2, maxIterLS=20, cg_rtol=1e-4 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) directives = [ diff --git a/examples/09-flow/plot_fwd_flow_richards_1D.py b/examples/09-flow/plot_fwd_flow_richards_1D.py index 1ce540fa12..9cbdd370f3 100644 --- a/examples/09-flow/plot_fwd_flow_richards_1D.py +++ b/examples/09-flow/plot_fwd_flow_richards_1D.py @@ -77,7 +77,6 @@ def run(plotIt=True): initial_conditions=h, do_newton=False, method="mixed", - debug=False, ) prob.time_steps = [(5, 25, 1.1), (60, 40)] diff --git a/examples/09-flow/plot_inv_flow_richards_1D.py b/examples/09-flow/plot_inv_flow_richards_1D.py index 567e41f112..9d83f53153 100644 --- a/examples/09-flow/plot_inv_flow_richards_1D.py +++ b/examples/09-flow/plot_inv_flow_richards_1D.py @@ -71,7 +71,6 @@ def run(plotIt=True): initial_conditions=h, do_newton=False, method="mixed", - debug=False, ) prob.time_steps = [(5, 25, 1.1), (60, 40)] @@ -100,7 +99,7 @@ def run(plotIt=True): # Setup a pretty standard inversion reg = regularization.WeightedLeastSquares(M, alpha_s=1e-1) dmis = data_misfit.L2DataMisfit(simulation=prob, data=data) - opt = optimization.InexactGaussNewton(maxIter=20, maxIterCG=10) + opt = optimization.InexactGaussNewton(maxIter=20, cg_maxiter=10) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) beta = directives.BetaSchedule(coolingFactor=4) betaest = directives.BetaEstimate_ByEig(beta0_ratio=1e2) diff --git a/examples/10-pgi/plot_inv_0_PGI_Linear_1D.py b/examples/10-pgi/plot_inv_0_PGI_Linear_1D.py index 86ba4c2572..0f69e45d27 100644 --- a/examples/10-pgi/plot_inv_0_PGI_Linear_1D.py +++ b/examples/10-pgi/plot_inv_0_PGI_Linear_1D.py @@ -73,7 +73,7 @@ def g(k): # Setup the inverse problem reg = regularization.WeightedLeastSquares(mesh, alpha_s=1.0, alpha_x=1.0) dmis = data_misfit.L2DataMisfit(data=survey, simulation=prob) -opt = optimization.ProjectedGNCG(maxIter=10, maxIterCG=50, tolCG=1e-4) +opt = optimization.ProjectedGNCG(maxIter=10, cg_maxiter=50, cg_rtol=1e-3) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) directiveslist = [ directives.BetaEstimate_ByEig(beta0_ratio=1e-5), @@ -114,7 +114,7 @@ def g(k): ) # Optimization -opt = optimization.ProjectedGNCG(maxIter=20, maxIterCG=50, tolCG=1e-4) +opt = optimization.ProjectedGNCG(maxIter=20, cg_maxiter=50, cg_rtol=1e-3) opt.remember("xc") # Setup new inverse problem diff --git a/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py b/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py index 693139ab38..b46d68f494 100644 --- a/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py +++ b/examples/10-pgi/plot_inv_1_PGI_Linear_1D_joint_WithRelationships.py @@ -11,6 +11,7 @@ import discretize as Mesh import matplotlib.pyplot as plt +import matplotlib.lines as mlines import numpy as np from simpeg import ( data_misfit, @@ -146,8 +147,8 @@ def g(k): opt = optimization.ProjectedGNCG( maxIter=50, tolX=1e-6, - maxIterCG=100, - tolCG=1e-3, + cg_maxiter=100, + cg_rtol=1e-3, lower=-10, upper=10, ) @@ -194,8 +195,8 @@ def g(k): opt = optimization.ProjectedGNCG( maxIter=50, tolX=1e-6, - maxIterCG=100, - tolCG=1e-3, + cg_maxiter=100, + cg_rtol=1e-3, lower=-10, upper=10, ) @@ -245,8 +246,8 @@ def g(k): opt = optimization.ProjectedGNCG( maxIter=50, tolX=1e-6, - maxIterCG=100, - tolCG=1e-3, + cg_maxiter=100, + cg_rtol=1e-3, lower=-10, upper=10, ) @@ -324,10 +325,16 @@ def g(k): alpha=0.25, cmap="viridis", ) -axes[3].scatter(wires.m1 * mcluster_map, wires.m2 * mcluster_map, marker="v") +cs_proxy = mlines.Line2D([], [], label="True Petrophysical Distribution") + +ps = axes[3].scatter( + wires.m1 * mcluster_map, + wires.m2 * mcluster_map, + marker="v", + label="Recovered model crossplot", +) axes[3].set_title("Petrophysical Distribution") -CS.collections[0].set_label("") -axes[3].legend(["True Petrophysical Distribution", "Recovered model crossplot"]) +axes[3].legend(handles=[cs_proxy, ps]) axes[3].set_xlabel("Property 1") axes[3].set_ylabel("Property 2") @@ -372,7 +379,6 @@ def g(k): 500, cmap="viridis", linestyles="--", - label="Modeled Petro. Distribution", ) axes[7].scatter( wires.m1 * mcluster_no_map, @@ -380,8 +386,12 @@ def g(k): marker="v", label="Recovered model crossplot", ) +cs_modeled_proxy = mlines.Line2D( + [], [], linestyle="--", label="Modeled Petro. Distribution" +) + axes[7].set_title("Petrophysical Distribution") -axes[7].legend() +axes[7].legend(handles=[cs_proxy, cs_modeled_proxy, ps]) axes[7].set_xlabel("Property 1") axes[7].set_ylabel("Property 2") @@ -423,8 +433,7 @@ def g(k): ) axes[11].scatter(wires.m1 * mtik, wires.m2 * mtik, marker="v") axes[11].set_title("Petro Distribution") -CS.collections[0].set_label("") -axes[11].legend(["True Petro Distribution", "Recovered model crossplot"]) +axes[11].legend(handles=[cs_proxy, ps]) axes[11].set_xlabel("Property 1") axes[11].set_ylabel("Property 2") plt.subplots_adjust(wspace=0.3, hspace=0.3, top=0.85) diff --git a/examples/20-published/plot_booky_1D_time_freq_inv.py b/examples/20-published/plot_booky_1D_time_freq_inv.py index dd42c3938d..bbc0fb480d 100644 --- a/examples/20-published/plot_booky_1D_time_freq_inv.py +++ b/examples/20-published/plot_booky_1D_time_freq_inv.py @@ -33,7 +33,6 @@ import matplotlib import matplotlib.pyplot as plt from scipy.constants import mu_0 -from pymatsolver import Pardiso as Solver import discretize from simpeg import ( @@ -214,7 +213,7 @@ def run(plotIt=True, saveFig=False, cleanup=True): # Set FDEM survey (In-phase and Quadrature) survey = FDEM.Survey(source_list) - prb = FDEM.Simulation3DMagneticFluxDensity(mesh, sigmaMap=mapping, solver=Solver) + prb = FDEM.Simulation3DMagneticFluxDensity(mesh, sigmaMap=mapping) prb.survey = survey # ------------------ RESOLVE Inversion ------------------ # @@ -244,7 +243,7 @@ def run(plotIt=True, saveFig=False, cleanup=True): dmisfit = data_misfit.L2DataMisfit(simulation=prb, data=data_resolve) # Regularization - regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) + regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].active_cells]]) reg = regularization.WeightedLeastSquares( regMesh, mapping=maps.IdentityMap(regMesh) ) @@ -317,9 +316,7 @@ def run(plotIt=True, saveFig=False, cleanup=True): (1e-4, 10), (5e-4, 15), ] - prob = TDEM.Simulation3DElectricField( - mesh, time_steps=timeSteps, sigmaMap=mapping, solver=Solver - ) + prob = TDEM.Simulation3DElectricField(mesh, time_steps=timeSteps, sigmaMap=mapping) survey = TDEM.Survey(source_list) prob.survey = survey @@ -360,7 +357,7 @@ def run(plotIt=True, saveFig=False, cleanup=True): dmisfit = data_misfit.L2DataMisfit(simulation=prob, data=data_sky) # Regularization - regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) + regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].active_cells]]) reg = regularization.WeightedLeastSquares( regMesh, mapping=maps.IdentityMap(regMesh) ) diff --git a/examples/20-published/plot_booky_1Dstitched_resolve_inv.py b/examples/20-published/plot_booky_1Dstitched_resolve_inv.py index ec900f6da8..a9d093c96e 100644 --- a/examples/20-published/plot_booky_1Dstitched_resolve_inv.py +++ b/examples/20-published/plot_booky_1Dstitched_resolve_inv.py @@ -27,7 +27,6 @@ import numpy as np import matplotlib.pyplot as plt -from pymatsolver import PardisoSolver from scipy.constants import mu_0 from scipy.spatial import cKDTree @@ -113,9 +112,7 @@ def resolve_1Dinversions( # construct a forward simulation survey = FDEM.Survey(source_list) - prb = FDEM.Simulation3DMagneticFluxDensity( - mesh, sigmaMap=mapping, Solver=PardisoSolver - ) + prb = FDEM.Simulation3DMagneticFluxDensity(mesh, sigmaMap=mapping) prb.survey = survey # ------------------- Inversion ------------------- # @@ -125,7 +122,7 @@ def resolve_1Dinversions( dmisfit = data_misfit.L2DataMisfit(simulation=prb, data=dat) # regularization - regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) + regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].active_cells]]) reg = regularization.WeightedLeastSquares(regMesh) reg.reference_model = mref diff --git a/examples/20-published/plot_heagyetal2017_casing.py b/examples/20-published/plot_heagyetal2017_casing.py index e9f817c04d..4caa786f89 100644 --- a/examples/20-published/plot_heagyetal2017_casing.py +++ b/examples/20-published/plot_heagyetal2017_casing.py @@ -36,15 +36,6 @@ from simpeg.electromagnetics import frequency_domain as FDEM, mu_0 from simpeg.utils.io_utils import download -# try: -# from pymatsolver import MumpsSolver as Solver -# print('using MumpsSolver') -# except ImportError: -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - import numpy as np import scipy.sparse as sp import time @@ -244,13 +235,13 @@ def primaryMapping(self): # inject casing parameters so they are included in the construction # of the layered background + casing injectCasingParams = maps.InjectActiveCells( - None, indActive=np.r_[0, 1, 4, 5], valInactive=valInactive, nC=10 + None, active_cells=np.r_[0, 1, 4, 5], value_inactive=valInactive, nC=10 ) # maps a list of casing parameters to the cyl mesh (below the # subsurface) paramMapPrimary = maps.ParametricCasingAndLayer( - self.meshp, indActive=self.indActivePrimary, slopeFact=1e4 + self.meshp, active_cells=self.indActivePrimary, slopeFact=1e4 ) # inject air cells @@ -349,7 +340,6 @@ def primaryProblem(self): ) primaryProblem.mu = self.muModel - primaryProblem.solver = Solver self._primaryProblem = primaryProblem print("... done building primary problem") @@ -538,7 +528,9 @@ def mapping(self): # model on our mesh if getattr(self, "_mapping", None) is None: print("building secondary mapping") - paramMap = maps.ParametricBlockInLayer(self.meshs, indActive=self.indActive) + paramMap = maps.ParametricBlockInLayer( + self.meshs, active_cells=self.indActive + ) self._mapping = ( self.expMap * self.injActMap # log sigma --> sigma @@ -555,7 +547,7 @@ def primaryMap2meshs(self): # block) print("Building primaryMap2meshs") paramMapPrimaryMeshs = maps.ParametricLayer( - self.meshs, indActive=self.indActive + self.meshs, active_cells=self.indActive ) self._primaryMap2mesh = ( @@ -573,7 +565,6 @@ def setupSecondaryProblem(self, mapping=None): if mapping is None: mapping = [("sigma", maps.IdentityMap(self.meshs))] sec_problem = FDEM.Simulation3DElectricField(self.meshs, sigmaMap=mapping) - sec_problem.solver = Solver print("... done setting up secondary problem") return sec_problem @@ -673,8 +664,6 @@ def plotPrimaryFields(self, primaryFields, saveFig=False): def plotSecondarySource(self, primaryFields, saveFig=False): # get source term secondaryProblem = self.setupSecondaryProblem(mapping=self.mapping) - secondaryProblem.solver = Solver - self.primaryProblem.solver = Solver secondaryProblem.model = self.mtrue secondarySurvey = self.setupSecondarySurvey( self.primaryProblem, self.primarySurvey, self.primaryMap2meshs @@ -935,8 +924,8 @@ def plotJ( from matplotlib.colors import LogNorm f = ax.contourf( - rx_x, - rx_y, + self.rx_x, + self.rx_y, np.absolute(Jv), num, cmap=plt.get_cmap("viridis"), @@ -950,7 +939,7 @@ def plotJ( if plotGrid: self.meshs.plot_slice( - np.nan * np.ones(mesh.nC), normal="Z", grid=True, ax=ax + np.nan * np.ones(self.meshs.nC), normal="Z", grid=True, ax=ax ) if xlim is not None: diff --git a/examples/20-published/plot_heagyetal2017_cyl_inversions.py b/examples/20-published/plot_heagyetal2017_cyl_inversions.py index 53f328aeaf..8187ae3382 100644 --- a/examples/20-published/plot_heagyetal2017_cyl_inversions.py +++ b/examples/20-published/plot_heagyetal2017_cyl_inversions.py @@ -35,11 +35,6 @@ import matplotlib.pyplot as plt import matplotlib -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - def run(plotIt=True, saveFig=False): # Set up cylindrically symmeric mesh @@ -93,7 +88,7 @@ def run(plotIt=True, saveFig=False): surveyFD = FDEM.Survey(source_list) prbFD = FDEM.Simulation3DMagneticFluxDensity( - mesh, survey=surveyFD, sigmaMap=mapping, solver=Solver + mesh, survey=surveyFD, sigmaMap=mapping ) rel_err = 0.03 dataFD = prbFD.make_synthetic_data(mtrue, relative_error=rel_err, add_noise=True) @@ -102,14 +97,14 @@ def run(plotIt=True, saveFig=False): # FDEM inversion np.random.seed(1) dmisfit = data_misfit.L2DataMisfit(simulation=prbFD, data=dataFD) - regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) + regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].active_cells]]) reg = regularization.WeightedLeastSquares(regMesh) - opt = optimization.InexactGaussNewton(maxIterCG=10) + opt = optimization.InexactGaussNewton(cg_maxiter=10) invProb = inverse_problem.BaseInvProblem(dmisfit, reg, opt) # Inversion Directives beta = directives.BetaSchedule(coolingFactor=4, coolingRate=3) - betaest = directives.BetaEstimate_ByEig(beta0_ratio=1.0, seed=518936) + betaest = directives.BetaEstimate_ByEig(beta0_ratio=1.0, random_seed=518936) target = directives.TargetMisfit() directiveList = [beta, betaest, target] @@ -138,7 +133,7 @@ def run(plotIt=True, saveFig=False): surveyTD = TDEM.Survey([src]) prbTD = TDEM.Simulation3DMagneticFluxDensity( - mesh, survey=surveyTD, sigmaMap=mapping, solver=Solver + mesh, survey=surveyTD, sigmaMap=mapping ) prbTD.time_steps = [(5e-5, 10), (1e-4, 10), (5e-4, 10)] @@ -148,14 +143,14 @@ def run(plotIt=True, saveFig=False): # TDEM inversion dmisfit = data_misfit.L2DataMisfit(simulation=prbTD, data=dataTD) - regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) + regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].active_cells]]) reg = regularization.WeightedLeastSquares(regMesh) - opt = optimization.InexactGaussNewton(maxIterCG=10) + opt = optimization.InexactGaussNewton(cg_maxiter=10) invProb = inverse_problem.BaseInvProblem(dmisfit, reg, opt) # directives beta = directives.BetaSchedule(coolingFactor=4, coolingRate=3) - betaest = directives.BetaEstimate_ByEig(beta0_ratio=1.0, seed=518936) + betaest = directives.BetaEstimate_ByEig(beta0_ratio=1.0, random_seed=518936) target = directives.TargetMisfit() directiveList = [beta, betaest, target] diff --git a/examples/20-published/plot_laguna_del_maule_inversion.py b/examples/20-published/plot_laguna_del_maule_inversion.py index d4dd2ffca5..90bdefc85f 100644 --- a/examples/20-published/plot_laguna_del_maule_inversion.py +++ b/examples/20-published/plot_laguna_del_maule_inversion.py @@ -92,7 +92,7 @@ def run(plotIt=True, cleanAfterRun=True): # Now that we have a model and a survey we can build the linear system ... # Create the forward model operator simulation = gravity.simulation.Simulation3DIntegral( - survey=survey, mesh=mesh, rhoMap=staticCells, ind_active=active + survey=survey, mesh=mesh, rhoMap=staticCells, active_cells=active ) # %% Create inversion objects @@ -110,8 +110,8 @@ def run(plotIt=True, cleanAfterRun=True): lower=driver.bounds[0], upper=driver.bounds[1], maxIterLS=10, - maxIterCG=20, - tolCG=1e-4, + cg_maxiter=20, + cg_rtol=1e-4, ) # Define misfit function (obs-calc) @@ -121,13 +121,16 @@ def run(plotIt=True, cleanAfterRun=True): invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) # Specify how the initial beta is found - betaest = directives.BetaEstimate_ByEig(beta0_ratio=0.5, seed=518936) + betaest = directives.BetaEstimate_ByEig(beta0_ratio=0.5, random_seed=518936) # IRLS sets up the Lp inversion problem # Set the eps parameter parameter in Line 11 of the # input file based on the distribution of model (DEFAULT = 95th %ile) - IRLS = directives.Update_IRLS( - f_min_change=1e-4, max_irls_iterations=40, coolEpsFact=1.5, beta_tol=5e-1 + IRLS = directives.UpdateIRLS( + f_min_change=1e-4, + max_irls_iterations=40, + irls_cooling_factor=1.5, + misfit_tolerance=5e-1, ) # Preconditioning refreshing for each IRLS iteration diff --git a/examples/20-published/plot_schenkel_morrison_casing.py b/examples/20-published/plot_schenkel_morrison_casing.py index 6654e0ad08..5348f0fd99 100644 --- a/examples/20-published/plot_schenkel_morrison_casing.py +++ b/examples/20-published/plot_schenkel_morrison_casing.py @@ -52,11 +52,6 @@ from simpeg.electromagnetics import frequency_domain as FDEM import time -try: - from pymatsolver import Pardiso as Solver -except Exception: - from simpeg import SolverLU as Solver - def run(plotIt=True): # ------------------ MODEL ------------------ @@ -229,7 +224,9 @@ def run(plotIt=True): # ------------ Problem and Survey --------------- survey = FDEM.Survey(sg_p + dg_p) problem = FDEM.Simulation3DMagneticField( - mesh, survey=survey, sigmaMap=maps.IdentityMap(mesh), solver=Solver + mesh, + survey=survey, + sigmaMap=maps.IdentityMap(mesh), ) # ------------- Solve --------------------------- diff --git a/examples/_archived/plot_inv_dcip_2_5Dinversion.py b/examples/_archived/plot_inv_dcip_2_5Dinversion.py index 717cf0cb5c..5d28430207 100644 --- a/examples/_archived/plot_inv_dcip_2_5Dinversion.py +++ b/examples/_archived/plot_inv_dcip_2_5Dinversion.py @@ -27,11 +27,6 @@ import numpy as np from pylab import hist -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - def run(plotIt=True, survey_type="dipole-dipole"): np.random.seed(1) @@ -130,9 +125,7 @@ def run(plotIt=True, survey_type="dipole-dipole"): # Generate 2.5D DC problem # "N" means potential is defined at nodes - prb = DC.Simulation2DNodal( - mesh, survey=survey_dc, rhoMap=mapping, storeJ=True, solver=Solver - ) + prb = DC.Simulation2DNodal(mesh, survey=survey_dc, rhoMap=mapping, storeJ=True) # Make synthetic DC data with 5% Gaussian noise data_dc = prb.make_synthetic_data(mtrue_dc, relative_error=0.05, add_noise=True) @@ -144,7 +137,7 @@ def run(plotIt=True, survey_type="dipole-dipole"): # "N" means potential is defined at nodes survey_ip = IP.from_dc_to_ip_survey(survey_dc, dim="2.5D") prb_ip = IP.Simulation2DNodal( - mesh, survey=survey_ip, etaMap=actmap, storeJ=True, rho=rho, solver=Solver + mesh, survey=survey_ip, etaMap=actmap, storeJ=True, rho=rho ) data_ip = prb_ip.make_synthetic_data(mtrue_ip, relative_error=0.05, add_noise=True) diff --git a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py index dafcfee6a9..26c60fd3c6 100644 --- a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py +++ b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py @@ -30,11 +30,6 @@ import numpy as np from pylab import hist -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - def run(plotIt=True, survey_type="dipole-dipole"): np.random.seed(1) @@ -112,7 +107,7 @@ def run(plotIt=True, survey_type="dipole-dipole"): # Generate 2.5D DC problem # "N" means potential is defined at nodes prb = DC.Simulation2DNodal( - mesh, survey=survey, rhoMap=mapping, storeJ=True, Solver=Solver, verbose=True + mesh, survey=survey, rhoMap=mapping, storeJ=True, verbose=True ) # Make synthetic DC data with 5% Gaussian noise diff --git a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py index a2c67c696a..356fe58804 100644 --- a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py +++ b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py @@ -38,11 +38,6 @@ import numpy as np from pylab import hist -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - def run(plotIt=True, survey_type="dipole-dipole", p=0.0, qx=2.0, qz=2.0): np.random.seed(1) @@ -111,7 +106,9 @@ def run(plotIt=True, survey_type="dipole-dipole", p=0.0, qx=2.0, qz=2.0): plt.show() # Use Exponential Map: m = log(rho) - actmap = maps.InjectActiveCells(mesh, indActive=actind, valInactive=np.log(1e8)) + actmap = maps.InjectActiveCells( + mesh, active_cells=actind, value_inactive=np.log(1e8) + ) mapping = maps.ExpMap(mesh) * actmap # Generate mtrue @@ -120,7 +117,7 @@ def run(plotIt=True, survey_type="dipole-dipole", p=0.0, qx=2.0, qz=2.0): # Generate 2.5D DC problem # "N" means potential is defined at nodes prb = DC.Simulation2DNodal( - mesh, survey=survey, rhoMap=mapping, storeJ=True, Solver=Solver, verbose=True + mesh, survey=survey, rhoMap=mapping, storeJ=True, verbose=True ) # Make synthetic DC data with 5% Gaussian noise @@ -158,14 +155,13 @@ def run(plotIt=True, survey_type="dipole-dipole", p=0.0, qx=2.0, qz=2.0): mesh, active_cells=actind, mapping=regmap, gradient_type="components" ) reg.norms = [p, qx, qz, 0.0] - IRLS = directives.Update_IRLS( - max_irls_iterations=20, minGNiter=1, beta_search=False, fix_Jmatrix=True + irls = directives.UpdateIRLS( + max_irls_iterations=20, ) - opt = optimization.InexactGaussNewton(maxIter=40) invProb = inverse_problem.BaseInvProblem(dmisfit, reg, opt) betaest = directives.BetaEstimate_ByEig(beta0_ratio=1e0) - inv = inversion.BaseInversion(invProb, directiveList=[betaest, IRLS]) + inv = inversion.BaseInversion(invProb, directiveList=[betaest, irls]) prb.counter = opt.counter = utils.Counter() opt.LSshorten = 0.5 opt.remember("xc") diff --git a/examples/_archived/plot_inv_grav_linear.py b/examples/_archived/plot_inv_grav_linear.py index 7d6e07ab05..68752be2ee 100644 --- a/examples/_archived/plot_inv_grav_linear.py +++ b/examples/_archived/plot_inv_grav_linear.py @@ -84,7 +84,7 @@ def run(plotIt=True): # Create the forward simulation simulation = gravity.simulation.Simulation3DIntegral( - survey=survey, mesh=mesh, rhoMap=idenMap, ind_active=actv + survey=survey, mesh=mesh, rhoMap=idenMap, active_cells=actv ) # Compute linear forward operator and compute some data @@ -112,7 +112,7 @@ def run(plotIt=True): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 + maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, cg_maxiter=10, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) betaest = directives.BetaEstimate_ByEig(beta0_ratio=1e-1) @@ -120,11 +120,11 @@ def run(plotIt=True): # Here is where the norms are applied # Use pick a threshold parameter empirically based on the distribution of # model parameters - update_IRLS = directives.Update_IRLS( + update_IRLS = directives.UpdateIRLS( f_min_change=1e-4, max_irls_iterations=30, - coolEpsFact=1.5, - beta_tol=1e-2, + irls_cooling_factor=1.5, + misfit_tolerance=1e-2, ) saveDict = directives.SaveOutputEveryIteration(save_txt=False) update_Jacobi = directives.UpdatePreconditioner() @@ -261,7 +261,9 @@ def run(plotIt=True): axs = plt.subplot() axs.plot(saveDict.phi_d, "k", lw=2) axs.plot( - np.r_[update_IRLS.iterStart, update_IRLS.iterStart], + np.r_[ + update_IRLS.metrics.start_irls_iter, update_IRLS.metrics.start_irls_iter + ], np.r_[0, np.max(saveDict.phi_d)], "k:", ) @@ -269,7 +271,7 @@ def run(plotIt=True): twin = axs.twinx() twin.plot(saveDict.phi_m, "k--", lw=2) axs.text( - update_IRLS.iterStart, + update_IRLS.metrics.start_irls_iter, np.max(saveDict.phi_d) / 2.0, "IRLS Steps", va="bottom", diff --git a/examples/_archived/plot_inv_mag_linear.py b/examples/_archived/plot_inv_mag_linear.py index 8656f1dce1..438ab3dea2 100644 --- a/examples/_archived/plot_inv_mag_linear.py +++ b/examples/_archived/plot_inv_mag_linear.py @@ -91,7 +91,7 @@ def run(plotIt=True): survey=survey, mesh=mesh, chiMap=idenMap, - ind_active=actv, + active_cells=actv, ) # Compute linear forward operator and compute some data @@ -120,7 +120,7 @@ def run(plotIt=True): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=20, lower=0.0, upper=1.0, maxIterLS=20, maxIterCG=20, tolCG=1e-3 + maxIter=20, lower=0.0, upper=1.0, maxIterLS=20, cg_maxiter=20, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) betaest = directives.BetaEstimate_ByEig(beta0_ratio=1e-1) @@ -128,7 +128,7 @@ def run(plotIt=True): # Here is where the norms are applied # Use pick a threshold parameter empirically based on the distribution of # model parameters - IRLS = directives.Update_IRLS(f_min_change=1e-3, max_irls_iterations=40) + IRLS = directives.UpdateIRLS(f_min_change=1e-3, max_irls_iterations=40) saveDict = directives.SaveOutputEveryIteration(save_txt=False) update_Jacobi = directives.UpdatePreconditioner() # Add sensitivity weights @@ -291,7 +291,7 @@ def run(plotIt=True): axs = plt.subplot() axs.plot(saveDict.phi_d, "k", lw=2) axs.plot( - np.r_[IRLS.iterStart, IRLS.iterStart], + np.r_[IRLS.metrics.start_irls_iter, IRLS.metrics.start_irls_iter], np.r_[0, np.max(saveDict.phi_d)], "k:", ) @@ -299,7 +299,7 @@ def run(plotIt=True): twin = axs.twinx() twin.plot(saveDict.phi_m, "k--", lw=2) axs.text( - IRLS.iterStart, + IRLS.metrics.start_irls_iter, 0, "IRLS Steps", va="bottom", diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..0496ef5fb8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,267 @@ +[build-system] +requires = ["setuptools>=64", "setuptools_scm>=8"] +build-backend = "setuptools.build_meta" + + +[project] +name = 'simpeg' +description = "SimPEG: Simulation and Parameter Estimation in Geophysics" +readme = 'README.rst' +requires-python = '>=3.11' +authors = [ + {name = 'SimPEG developers', email = 'rowanc1@gmail.com'}, +] +keywords = [ + 'geophysics', 'inverse problem' +] +dependencies = [ + "numpy>=1.22", + "scipy>=1.12", + "pymatsolver>=0.3", + "matplotlib", + "discretize>=0.12", + "geoana>=0.7", + "libdlf", + "typing_extensions; python_version<'3.13'", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Mathematics", + "Topic :: Scientific/Engineering :: Physics", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Operating System :: Unix", + "Operating System :: MacOS", + "Natural Language :: English", +] +dynamic = ["version"] + +[project.license] +file = 'LICENSE' + +[project.urls] +Homepage = 'https://simpeg.xyz' +Documentation = 'https://docs.simpeg.xyz' +Repository = 'http://github.com/simpeg/simpeg.git' + +[project.optional-dependencies] +dask = ["dask", "zarr", "fsspec>=0.3.3"] +choclo = ["choclo>=0.3.0"] +reporting = ["scooby"] +plotting = ["plotly"] +sklearn = ["scikit-learn>=1.2"] +pandas = ["pandas"] +all = [ + "simpeg[dask,choclo,plotting,reporting,sklearn,pandas]" +] # all optional *runtime* dependencies (not related to development) +style = [ + "black==24.3.0", + "flake8==7.0.0", + "flake8-bugbear==23.12.2", + "flake8-builtins==2.2.0", + "flake8-mutable==1.2.0", + "flake8-rst-docstrings==0.3.0", + "flake8-docstrings==1.7.0", + "flake8-pyproject==1.2.3", +] +docs = [ + "sphinx", + "sphinx-gallery>=0.1.13", + "sphinxcontrib-apidoc", + "sphinx-reredirects", + "sphinx-design", + "pydata-sphinx-theme", + "nbsphinx", + "empymod>=2.0.0", + "numpydoc", + "pillow", + "sympy", + "memory_profiler", + "python-kaleido", +] +tests = [ + "simpeg[all,docs]", + "pytest", + "pytest-cov", +] +dev = [ + "simpeg[all,style,docs,tests]", +] # the whole kit and caboodle + +[tool.setuptools] +py-modules = ['SimPEG'] + +[tool.setuptools.packages.find] +include = ["simpeg*"] + +[tool.setuptools_scm] +version_file = "simpeg/version.py" +local_scheme = "node-and-date" + +[tool.coverage.run] +branch = true +source = ["simpeg", "tests", "examples", "tutorials"] + +[tool.coverage.report] +ignore_errors = false +show_missing = true +# Regexes for lines to exclude from consideration +exclude_also = [ + # Don't complain about missing debug-only code: + "def __repr__", + "if self\\.debug", + + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", + "AbstractMethodError", + + # Don't complain if non-runnable code isn't run: + "if 0:", + "if __name__ == .__main__.:", + + # Don't complain about default solver choices: + 'if AvailableSolvers["Pardiso"]:', + + # Don't complain about abstract methods, they aren't run: + "@(abc\\.)?abstractmethod", +] + +[tool.coverage.html] +directory = "coverage_html_report" + +[tool.black] +required-version = '24.3.0' +target-version = ['py310', 'py311', 'py312'] + +[tool.flake8] +extend-ignore = [ + # Default ignores by flake (added here for when ignore gets overwritten) + 'E121','E123','E126','E226','E24','E704','W503','W504', + # Too many leading '#' for block comment + 'E266', + # Line too long (82 > 79 characters) + 'E501', + # Do not use variables named 'I', 'O', or 'l' + 'E741', + # Line break before binary operator (conflicts with black) + 'W503', + # Ignore spaces before a colon (Black handles it) + 'E203', +] +exclude = [ + '.git', + '__pycache__', + '.ipynb_checkpoints', + 'docs/conf.py', + 'docs/_build/', +] +per-file-ignores = [ + # disable unused-imports errors on __init__.py + '__init__.py:F401', +] +exclude-from-doctest = [ + # Don't check style in docstring of test functions + 'tests', +] +ignore = [ + # assertRaises(Exception): should be considered evil + 'B017', + # Missing docstring in public module + 'D100', + # Missing docstring in public class + 'D101', + # Missing docstring in public method + 'D102', + # Missing docstring in public function + 'D103', + # Missing docstring in public package + 'D104', + # Missing docstring in magic method + 'D105', + # Missing docstring in __init__ + 'D107', + # One-line docstring should fit on one line with quotes + 'D200', + # No blank lines allowed before function docstring + 'D201', + # No blank lines allowed after function docstring + 'D202', + # 1 blank line required between summary line and description + 'D205', + # Docstring is over-indented + 'D208', + # Multi-line docstring closing quotes should be on a separate line + 'D209', + # No whitespaces allowed surrounding docstring text + 'D210', + # No blank lines allowed before class docstring + 'D211', + # Use """triple double quotes""" + 'D300', + # First line should end with a period + 'D400', + # First line should be in imperative mood; try rephrasing + 'D401', + # First line should not be the function's "signature" + 'D402', + # First word of the first line should be properly capitalized + 'D403', + # No blank lines allowed between a section header and its content + 'D412', + # Section has no content + 'D414', + # Docstring is empty + 'D419', + # module level import not at top of file + 'E402', + # Block quote ends without a blank line; unexpected unindent. + 'RST201', + # Definition list ends without a blank line; unexpected unindent. + 'RST203', + # Field list ends without a blank line; unexpected unindent. + 'RST206', + # Inline strong start-string without end-string. + 'RST210', + # Title underline too short. + 'RST212', + # Inline emphasis start-string without end-string. + 'RST213', + # Inline interpreted text or phrase reference start-string without end-string. + 'RST215', + # Inline substitution_reference start-string without end-string. + 'RST219', + # Unexpected indentation. + 'RST301', + # Unknown directive type "*". + 'RST303', + # Unknown interpreted text role "*". + 'RST304', + # Error in "*" directive: + 'RST307', + # Previously unseen severe error, not yet assigned a unique code. + 'RST499', +] + +rst-roles = [ + 'class', + 'func', + 'mod', + 'meth', + 'ref', +] + +# pyproject.toml +[tool.pytest.ini_options] +filterwarnings = [ + "error:You are running a pytest without setting a random seed.*:UserWarning", + 'error:The `simpeg\.directives\.[a-z_]+` submodule has been deprecated', + 'error:Casting complex values to real discards the imaginary part', + 'ignore::simpeg.utils.BreakingChangeWarning', +] +xfail_strict = true diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d6e1198b1a..0000000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ --e . diff --git a/requirements_dask.txt b/requirements_dask.txt deleted file mode 100644 index 557aece885..0000000000 --- a/requirements_dask.txt +++ /dev/null @@ -1,3 +0,0 @@ -dask -zarr -fsspec>=0.3.3 diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index 5bab4a0c4b..0000000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,27 +0,0 @@ -sphinx -sphinx_rtd_theme -sphinx-gallery>=0.1.13 -sphinx-toolbox -sphinxcontrib-apidoc -pydata-sphinx-theme -nbsphinx -numpydoc -pillow -pylint -numpy>=1.20 -scipy>=1.8 -scikit-learn>=1.2 -sympy -wheel -pytest -pytest-cov -jupyter -toolz -empymod>=2.0.0 -scooby -black==24.3.0 -pre-commit -twine -memory_profiler -plotly -setuptools_scm diff --git a/requirements_style.txt b/requirements_style.txt deleted file mode 100644 index a4fd699571..0000000000 --- a/requirements_style.txt +++ /dev/null @@ -1,7 +0,0 @@ -black==24.3.0 -flake8==7.0.0 -flake8-bugbear==23.12.2 -flake8-builtins==2.2.0 -flake8-mutable==1.2.0 -flake8-rst-docstrings==0.3.0 -flake8-docstrings==1.7.0 diff --git a/setup.py b/setup.py deleted file mode 100644 index 10411d654e..0000000000 --- a/setup.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python -"""SimPEG: Simulation and Parameter Estimation in Geophysics - -SimPEG is a python package for simulation and gradient based -parameter estimation in the context of geophysical applications. -""" - -from setuptools import setup, find_packages -import os - -CLASSIFIERS = [ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python", - "Topic :: Scientific/Engineering", - "Topic :: Scientific/Engineering :: Mathematics", - "Topic :: Scientific/Engineering :: Physics", - "Operating System :: Microsoft :: Windows", - "Operating System :: POSIX", - "Operating System :: Unix", - "Operating System :: MacOS", - "Natural Language :: English", -] - -with open("README.rst") as f: - LONG_DESCRIPTION = "".join(f.readlines()) - -setup( - name="simpeg", - packages=find_packages(exclude=["tests*", "examples*", "tutorials*"]), - py_modules=["SimPEG"], - python_requires=">=3.8", - setup_requires=[ - "setuptools_scm", - ], - install_requires=[ - "numpy>=1.20", - "scipy>=1.8", - "scikit-learn>=1.2", - "pymatsolver>=0.2", - "matplotlib", - "discretize>=0.10", - "geoana>=0.5.0", - "empymod>=2.0.0", - "pandas", - ], - author="Rowan Cockett", - author_email="rowanc1@gmail.com", - description="SimPEG: Simulation and Parameter Estimation in Geophysics", - long_description=LONG_DESCRIPTION, - license="MIT", - keywords="geophysics inverse problem", - url="https://simpeg.xyz/", - download_url="https://github.com/simpeg/simpeg", - classifiers=CLASSIFIERS, - platforms=["Windows", "Linux", "Solaris", "Mac OS-X", "Unix"], - use_2to3=False, - use_scm_version={ - "write_to": os.path.join("simpeg", "version.py"), - }, -) diff --git a/simpeg/__init__.py b/simpeg/__init__.py index f46c2b0e0b..baf84190d3 100644 --- a/simpeg/__init__.py +++ b/simpeg/__init__.py @@ -73,24 +73,32 @@ maps.ComboMap maps.ComplexMap maps.ExpMap - maps.LinearMap maps.IdentityMap maps.InjectActiveCells - maps.MuRelative - maps.LogMap + maps.LinearMap maps.LogisticSigmoidMap + maps.LogMap + maps.Mesh2Mesh + maps.MuRelative maps.ParametricBlock + maps.ParametricBlockInLayer + maps.ParametricCasingAndLayer maps.ParametricCircleMap maps.ParametricEllipsoid maps.ParametricLayer maps.ParametricPolyMap + maps.ParametricSplineMap + maps.PolynomialPetroClusterMap maps.Projection maps.ReciprocalMap + maps.SelfConsistentEffectiveMedium maps.SphericalSystem + maps.SumMap maps.Surject2Dto3D maps.SurjectFull maps.SurjectUnits maps.SurjectVertical1D + maps.TileMap maps.Weighting maps.Wires @@ -149,12 +157,12 @@ from . import regularization from . import survey from . import simulation +from . import typing from . import utils from .utils import mkvc from .utils import Report from .utils.solver_utils import ( - _checkAccuracy, SolverWrapD, SolverWrapI, Solver, diff --git a/simpeg/base/pde_simulation.py b/simpeg/base/pde_simulation.py index bf000b4c5c..7a999e4fdd 100644 --- a/simpeg/base/pde_simulation.py +++ b/simpeg/base/pde_simulation.py @@ -1,12 +1,18 @@ +import inspect +import warnings import numpy as np +import pymatsolver import scipy.sparse as sp from discretize.utils import Zero, TensorType +import discretize.base from ..simulation import BaseSimulation from .. import props from scipy.constants import mu_0 +from ..utils import validate_type, get_default_solver, get_logger, PerformanceWarning -def __inner_mat_mul_op(M, u, v=None, adjoint=False): + +def _inner_mat_mul_op(M, u, v=None, adjoint=False): u = np.squeeze(u) if sp.issparse(M): if v is not None: @@ -233,9 +239,7 @@ def MccDeriv_prop(self, u, v=None, adjoint=False): self, f"{arg.lower()}Deriv" ) setattr(self, stash_name, M_prop_deriv) - return __inner_mat_mul_op( - getattr(self, stash_name), u, v=v, adjoint=adjoint - ) + return _inner_mat_mul_op(getattr(self, stash_name), u, v=v, adjoint=adjoint) setattr(cls, f"Mcc{arg}Deriv", MccDeriv_prop) @@ -255,9 +259,7 @@ def MnDeriv_prop(self, u, v=None, adjoint=False): * getattr(self, f"{arg.lower()}Deriv") ) setattr(self, stash_name, M_prop_deriv) - return __inner_mat_mul_op( - getattr(self, stash_name), u, v=v, adjoint=adjoint - ) + return _inner_mat_mul_op(getattr(self, stash_name), u, v=v, adjoint=adjoint) setattr(cls, f"Mn{arg}Deriv", MnDeriv_prop) @@ -287,9 +289,7 @@ def MfDeriv_prop(self, u, v=None, adjoint=False): else: setattr(self, stash_name, (M_deriv_func, prop_deriv)) - return __inner_mat_mul_op( - getattr(self, stash_name), u, v=v, adjoint=adjoint - ) + return _inner_mat_mul_op(getattr(self, stash_name), u, v=v, adjoint=adjoint) setattr(cls, f"Mf{arg}Deriv", MfDeriv_prop) @@ -318,9 +318,7 @@ def MeDeriv_prop(self, u, v=None, adjoint=False): setattr(self, stash_name, M_prop_deriv) else: setattr(self, stash_name, (M_deriv_func, prop_deriv)) - return __inner_mat_mul_op( - getattr(self, stash_name), u, v=v, adjoint=adjoint - ) + return _inner_mat_mul_op(getattr(self, stash_name), u, v=v, adjoint=adjoint) setattr(cls, f"Me{arg}Deriv", MeDeriv_prop) @@ -413,6 +411,126 @@ def _clear_on_prop_update(self): class BasePDESimulation(BaseSimulation): + """Base simulation for PDE solutions. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + Mesh on which the forward problem is discretized. + solver : type[pymatsolver.base.Base], optional + Numerical solver used to solve the forward problem. If ``None``, + an appropriate solver specific to the simulation class is set by default. + solver_opts : dict, optional + Solver-specific parameters. If ``None``, default parameters are used for + the solver set by ``solver``. Otherwise, the ``dict`` must contain appropriate + pairs of keyword arguments and parameter values for the solver. Please visit + `pymatsolver `__ to learn more + about solvers and their parameters. + """ + + def __init__(self, mesh, solver=None, solver_opts=None, **kwargs): + self.mesh = mesh + super().__init__(**kwargs) + if solver is None: + solver = get_default_solver() + get_logger().info( + f"Setting the default solver '{solver.__name__}' for the " + f"'{type(self).__name__}'.\n" + "To avoid receiving this message, pass a solver to the simulation. " + "For example:" + "\n\n" + " from simpeg.utils import get_default_solver\n" + "\n" + " solver = get_default_solver()\n" + f" simulation = {type(self).__name__}(solver=solver, ...)" + ) + self.solver = solver + if solver_opts is None: + solver_opts = {} + self.solver_opts = solver_opts + + @property + def mesh(self): + """Mesh for the simulation. + + For more on meshes, visit :py:class:`discretize.base.BaseMesh`. + + Returns + ------- + discretize.base.BaseMesh + Mesh on which the forward problem is discretized. + """ + return self._mesh + + @mesh.setter + def mesh(self, value): + self._mesh = validate_type("mesh", value, discretize.base.BaseMesh, cast=False) + + @property + def solver(self): + r"""Numerical solver used in the forward simulation. + + Many forward simulations in SimPEG require solutions to discrete linear + systems of the form: + + .. math:: + \mathbf{A}(\mathbf{m}) \, \mathbf{u} = \mathbf{q} + + where :math:`\mathbf{A}` is an invertible matrix that depends on the + model :math:`\mathbf{m}`. The numerical solver can be set using the + ``solver`` property. In SimPEG, the + `pymatsolver `__ package + is used to create solver objects. Parameters specific to each solver + can be set manually using the ``solver_opts`` property. + + Returns + ------- + type[pymatsolver.solvers.Base] + Numerical solver used to solve the forward problem. + """ + return self._solver + + @solver.setter + def solver(self, cls): + if cls is not None: + if not inspect.isclass(cls): + raise TypeError(f"{type(self).__qualname__}.solver must be a class") + if not issubclass(cls, pymatsolver.solvers.Base): + raise TypeError( + f"{cls.__qualname__} is not a subclass of pymatsolver.base.BaseSolver" + ) + if cls in (pymatsolver.SolverLU, pymatsolver.Solver): + warnings.warn( + f"The 'pymatsolver.{cls.__name__}' solver might lead to high " + "computation times. " + "We recommend using a faster alternative such as 'pymatsolver.Pardiso' " + "or 'pymatsolver.Mumps'.", + PerformanceWarning, + stacklevel=2, + ) + self._solver = cls + + @property + def solver_opts(self): + """Solver-specific parameters. + + The parameters specific to the solver set with the ``solver`` property are set + upon instantiation. The ``solver_opts`` property is used to set solver-specific properties. + This is done by providing a ``dict`` that contains appropriate pairs of keyword arguments + and parameter values. Please visit `pymatsolver `__ + to learn more about solvers and their parameters. + + Returns + ------- + dict + keyword arguments and parameters passed to the solver. + """ + return self._solver_opts + + @solver_opts.setter + def solver_opts(self, value): + self._solver_opts = validate_type("solver_opts", value, dict, cast=False) + @property def Vol(self): return self.Mcc @@ -506,11 +624,11 @@ def __init__( self.rhoMap = rhoMap @property - def deleteTheseOnModelUpdate(self): + def _delete_on_model_update(self): """ matrices to be deleted if the model for conductivity/resistivity is updated """ - toDelete = super().deleteTheseOnModelUpdate + toDelete = super()._delete_on_model_update if self.sigmaMap is not None or self.rhoMap is not None: toDelete = ( toDelete + self._clear_on_sigma_update + self._clear_on_rho_update @@ -549,11 +667,11 @@ def __setattr__(self, name, value): delattr(self, mat) @property - def deleteTheseOnModelUpdate(self): + def _delete_on_model_update(self): """ items to be deleted if the model for Magnetic Permeability is updated """ - toDelete = super().deleteTheseOnModelUpdate + toDelete = super()._delete_on_model_update if self.muMap is not None or self.muiMap is not None: toDelete = toDelete + self._clear_on_mu_update + self._clear_on_mui_update return toDelete diff --git a/simpeg/dask/simulation.py b/simpeg/dask/simulation.py index 3f64f35ac0..1d5deecb24 100644 --- a/simpeg/dask/simulation.py +++ b/simpeg/dask/simulation.py @@ -51,10 +51,7 @@ def max_chunk_size(self, other): def __init__( self, - mesh=None, survey=None, - solver=None, - solver_opts=None, sensitivity_path="./sensitivity/", counter=None, verbose=False, @@ -65,10 +62,7 @@ def __init__( ): _old_init( self, - mesh=mesh, survey=survey, - solver=solver, - solver_opts=solver_opts, sensitivity_path=sensitivity_path, counter=counter, verbose=verbose, diff --git a/simpeg/data.py b/simpeg/data.py index 1d53972caa..b461e455ba 100644 --- a/simpeg/data.py +++ b/simpeg/data.py @@ -4,6 +4,7 @@ from .survey import BaseSurvey from .utils import mkvc, validate_ndarray_with_shape, validate_float, validate_type + __all__ = ["Data", "SyntheticData"] @@ -58,7 +59,7 @@ def __init__( relative_error=None, noise_floor=None, standard_deviation=None, - **kwargs + **kwargs, ): super().__init__(**kwargs) self.survey = survey @@ -283,57 +284,17 @@ def shape(self): """ return self.dobs.shape - @property - def index_dictionary(self): - """Dictionary for indexing data by sources and receiver. - - FULL DESCRIPTION REQUIRED - - Returns - ------- - dict - Dictionary for indexing data by source and receiver - - Examples - -------- - NEED EXAMPLE (1D TEM WOULD BE GOOD) - - """ - if getattr(self, "_index_dictionary", None) is None: - if self.survey is None: - raise Exception( - "To set or get values by source-receiver pairs, a survey must " - "first be set. `data.survey = survey`" - ) - - # create an empty dict - self._index_dictionary = {} - - # create an empty dict associated with each source - for src in self.survey.source_list: - self._index_dictionary[src] = {} - - # loop over sources and find the associated data indices - indBot, indTop = 0, 0 - for src in self.survey.source_list: - for rx in src.receiver_list: - indTop += rx.nD - self._index_dictionary[src][rx] = np.arange(indBot, indTop) - indBot += rx.nD - - return self._index_dictionary - ########################## # Methods ########################## def __setitem__(self, key, value): - index = self.index_dictionary[key[0]][key[1]] - self.dobs[index] = mkvc(value) + slice_obj = self.survey.get_slice(*key) + self.dobs[slice_obj] = mkvc(value) def __getitem__(self, key): - index = self.index_dictionary[key[0]][key[1]] - return self.dobs[index] + slice_obj = self.survey.get_slice(*key) + return self.dobs[slice_obj] def tovec(self): """Convert observed data to a vector @@ -413,12 +374,11 @@ def dclean(self): Notes -------- This array should be indexing the data object - using the a tuple of the survey's sources and receivers. + using a tuple of the survey's sources and receivers. >>> data = Data(survey) >>> for src in survey.source_list: ... for rx in src.receiver_list: - ... index = data.index_dictionary(src, rx) ... data.dclean[src, rx] = datum """ diff --git a/simpeg/data_misfit.py b/simpeg/data_misfit.py index 6b489b425a..740e8035fc 100644 --- a/simpeg/data_misfit.py +++ b/simpeg/data_misfit.py @@ -225,9 +225,14 @@ def residual(self, m, f=None): (n_data, ) numpy.ndarray The data residual vector. """ - if self.data is None: - raise Exception("data must be set before a residual can be calculated.") - return self.simulation.residual(m, self.data.dobs, f=f) + dpred = self.simulation.dpred(m, f=f) + if np.isnan(dpred).any() or np.isinf(dpred).any(): + msg = ( + f"The `{type(self.simulation).__name__}.dpred()` method " + "returned an array that contains `nan`s and/or `inf`s." + ) + raise ValueError(msg) + return dpred - self.data.dobs class L2DataMisfit(BaseDataMisfit): @@ -266,7 +271,10 @@ def __call__(self, m, f=None): """Evaluate the residual for a given model.""" R = self.W * self.residual(m, f=f) - return np.vdot(R, R) + # Imaginary part is always zero, even for complex data, as it takes the + # complex-conjugate dot-product. Ensure it returns a float + # (``np.vdot(R, R).real`` is the same as ``np.linalg.norm(R)**2``). + return np.vdot(R, R).real @timeIt def deriv(self, m, f=None): diff --git a/simpeg/directives/__init__.py b/simpeg/directives/__init__.py index 69081767d1..4d542245cc 100644 --- a/simpeg/directives/__init__.py +++ b/simpeg/directives/__init__.py @@ -55,6 +55,8 @@ .. autosummary:: :toctree: generated/ + UpdateIRLS + SphericalUnitsWeights Update_IRLS @@ -97,7 +99,7 @@ """ -from .directives import ( +from ._directives import ( InversionDirective, DirectiveList, BetaEstimateMaxDerivative, @@ -108,7 +110,6 @@ SaveModelEveryIteration, SaveOutputEveryIteration, SaveOutputDictEveryIteration, - Update_IRLS, UpdatePreconditioner, Update_Wj, AlphasSmoothEstimate_ByEig, @@ -119,16 +120,21 @@ ProjectSphericalBounds, ) -from .pgi_directives import ( +from ._pgi_directives import ( PGI_UpdateParameters, PGI_BetaAlphaSchedule, PGI_AddMrefInSmooth, ) -from .sim_directives import ( +from ._regularization import UpdateIRLS, SphericalUnitsWeights + +from ._sim_directives import ( SimilarityMeasureInversionDirective, SimilarityMeasureSaveOutputEveryIteration, PairedBetaEstimate_ByEig, PairedBetaSchedule, MovingAndMultiTargetStopping, ) + +### Deprecated class +from ._regularization import Update_IRLS diff --git a/simpeg/directives/_directives.py b/simpeg/directives/_directives.py new file mode 100644 index 0000000000..5cbaa81ffd --- /dev/null +++ b/simpeg/directives/_directives.py @@ -0,0 +1,2685 @@ +from abc import ABCMeta, abstractmethod +from typing import TYPE_CHECKING + +from datetime import datetime +import pathlib +import numpy as np +import matplotlib.pyplot as plt +import warnings +import scipy.sparse as sp +from ..typing import RandomSeed +from ..data_misfit import BaseDataMisfit +from ..objective_function import BaseObjectiveFunction, ComboObjectiveFunction +from ..maps import IdentityMap, Wires +from ..regularization import ( + WeightedLeastSquares, + BaseRegularization, + Smallness, + Sparse, + SparseSmallness, + PGIsmallness, + SmoothnessFirstOrder, + SparseSmoothness, + BaseSimilarityMeasure, +) +from ..utils import ( + mkvc, + set_kwargs, + sdiag, + estimate_diagonal, + spherical2cartesian, + cartesian2spherical, + Zero, + eigenvalue_by_power_iteration, + validate_string, + get_logger, +) +from ..utils.code_utils import ( + deprecate_property, + validate_type, + validate_integer, + validate_float, + validate_ndarray_with_shape, +) + +if TYPE_CHECKING: + from ..simulation import BaseSimulation + from ..survey import BaseSurvey + + +class InversionDirective: + """Base inversion directive class. + + SimPEG directives initialize and update parameters used by the inversion algorithm; + e.g. setting the initial beta or updating the regularization. ``InversionDirective`` + is a parent class responsible for connecting directives to the data misfit, regularization + and optimization defining the inverse problem. + + Parameters + ---------- + inversion : simpeg.inversion.BaseInversion, None + An SimPEG inversion object; i.e. an instance of :class:`simpeg.inversion.BaseInversion`. + dmisfit : simpeg.data_misfit.BaseDataMisfit, None + A data data misfit; i.e. an instance of :class:`simpeg.data_misfit.BaseDataMisfit`. + reg : simpeg.regularization.BaseRegularization, None + The regularization, or model objective function; i.e. an instance of :class:`simpeg.regularization.BaseRegularization`. + verbose : bool + Whether or not to print debugging information. + """ + + _REGISTRY = {} + + _regPair = [WeightedLeastSquares, BaseRegularization, ComboObjectiveFunction] + _dmisfitPair = [BaseDataMisfit, ComboObjectiveFunction] + + def __init__(self, inversion=None, dmisfit=None, reg=None, verbose=False, **kwargs): + self.inversion = inversion + self.dmisfit = dmisfit + self.reg = reg + self.verbose = verbose + set_kwargs(self, **kwargs) + + @property + def verbose(self): + """Whether or not to print debugging information. + + Returns + ------- + bool + """ + return self._verbose + + @verbose.setter + def verbose(self, value): + self._verbose = validate_type("verbose", value, bool) + + @property + def inversion(self): + """Inversion object associated with the directive. + + Returns + ------- + simpeg.inversion.BaseInversion + The inversion associated with the directive. + """ + if not hasattr(self, "_inversion"): + return None + return self._inversion + + @inversion.setter + def inversion(self, i): + if getattr(self, "_inversion", None) is not None: + warnings.warn( + "InversionDirective {0!s} has switched to a new inversion.".format( + self.__class__.__name__ + ), + stacklevel=2, + ) + self._inversion = i + + @property + def invProb(self): + """Inverse problem associated with the directive. + + Returns + ------- + simpeg.inverse_problem.BaseInvProblem + The inverse problem associated with the directive. + """ + return self.inversion.invProb + + @property + def opt(self): + """Optimization algorithm associated with the directive. + + Returns + ------- + simpeg.optimization.Minimize + Optimization algorithm associated with the directive. + """ + return self.invProb.opt + + @property + def reg(self) -> BaseObjectiveFunction: + """Regularization associated with the directive. + + Returns + ------- + simpeg.regularization.BaseRegularization + The regularization associated with the directive. + """ + if getattr(self, "_reg", None) is None: + self.reg = self.invProb.reg # go through the setter + return self._reg + + @reg.setter + def reg(self, value): + if value is not None: + assert any( + [isinstance(value, regtype) for regtype in self._regPair] + ), "Regularization must be in {}, not {}".format(self._regPair, type(value)) + + if isinstance(value, WeightedLeastSquares): + value = 1 * value # turn it into a combo objective function + self._reg = value + + @property + def dmisfit(self) -> BaseObjectiveFunction: + """Data misfit associated with the directive. + + Returns + ------- + simpeg.data_misfit.BaseDataMisfit + The data misfit associated with the directive. + """ + if getattr(self, "_dmisfit", None) is None: + self.dmisfit = self.invProb.dmisfit # go through the setter + return self._dmisfit + + @dmisfit.setter + def dmisfit(self, value): + if value is not None: + assert any( + [isinstance(value, dmisfittype) for dmisfittype in self._dmisfitPair] + ), "Misfit must be in {}, not {}".format(self._dmisfitPair, type(value)) + + if not isinstance(value, ComboObjectiveFunction): + value = 1 * value # turn it into a combo objective function + self._dmisfit = value + + @property + def survey(self) -> list["BaseSurvey"]: + """Return survey for all data misfits + + Assuming that ``dmisfit`` is always a ``ComboObjectiveFunction``, + return a list containing the survey for each data misfit; i.e. + [survey1, survey2, ...] + + Returns + ------- + list of simpeg.survey.Survey + Survey for all data misfits. + """ + return [objfcts.simulation.survey for objfcts in self.dmisfit.objfcts] + + @property + def simulation(self) -> list["BaseSimulation"]: + """Return simulation for all data misfits. + + Assuming that ``dmisfit`` is always a ``ComboObjectiveFunction``, + return a list containing the simulation for each data misfit; i.e. + [sim1, sim2, ...]. + + Returns + ------- + list of simpeg.simulation.BaseSimulation + Simulation for all data misfits. + """ + return [objfcts.simulation for objfcts in self.dmisfit.objfcts] + + def initialize(self): + """Initialize inversion parameter(s) according to directive.""" + pass + + def endIter(self): + """Update inversion parameter(s) according to directive at end of iteration.""" + pass + + def finish(self): + """Update inversion parameter(s) according to directive at end of inversion.""" + pass + + def validate(self, directiveList=None): + """Validate directive. + + The `validate` method returns ``True`` if the directive and its location within + the directives list does not encounter conflicts. Otherwise, an appropriate error + message is returned describing the conflict. + + Parameters + ---------- + directive_list : simpeg.directives.DirectiveList + List of directives used in the inversion. + + Returns + ------- + bool + Returns ``True`` if validated, otherwise an approriate error is returned. + """ + return True + + +class DirectiveList(object): + """Directives list + + SimPEG directives initialize and update parameters used by the inversion algorithm; + e.g. setting the initial beta or updating the regularization. ``DirectiveList`` stores + the set of directives used in the inversion algorithm. + + Parameters + ---------- + *directives : simpeg.directives.InversionDirective + Directives for the inversion. + inversion : simpeg.inversion.BaseInversion + The inversion associated with the directives list. + debug : bool + Whether to print debugging information. + + """ + + def __init__(self, *directives, inversion=None, debug=False, **kwargs): + super().__init__(**kwargs) + self.dList = [] + for d in directives: + assert isinstance( + d, InversionDirective + ), "All directives must be InversionDirectives not {}".format(type(d)) + self.dList.append(d) + self.inversion = inversion + self.verbose = debug + + @property + def debug(self): + """Whether or not to print debugging information + + Returns + ------- + bool + """ + return getattr(self, "_debug", False) + + @debug.setter + def debug(self, value): + for d in self.dList: + d.debug = value + self._debug = value + + @property + def inversion(self): + """Inversion object associated with the directives list. + + Returns + ------- + simpeg.inversion.BaseInversion + The inversion associated with the directives list. + """ + return getattr(self, "_inversion", None) + + @inversion.setter + def inversion(self, i): + if self.inversion is i: + return + if getattr(self, "_inversion", None) is not None: + warnings.warn( + "{0!s} has switched to a new inversion.".format( + self.__class__.__name__ + ), + stacklevel=2, + ) + for d in self.dList: + d.inversion = i + self._inversion = i + + def call(self, ruleType): + if self.dList is None: + if self.verbose: + print("DirectiveList is None, no directives to call!") + return + + directives = ["initialize", "endIter", "finish"] + assert ruleType in directives, 'Directive type must be in ["{0!s}"]'.format( + '", "'.join(directives) + ) + for r in self.dList: + getattr(r, ruleType)() + + def validate(self): + [directive.validate(self) for directive in self] + return True + + def __iter__(self): + return iter(self.dList) + + +class BaseBetaEstimator(InversionDirective): + """Base class for estimating initial trade-off parameter (beta). + + This class has properties and methods inherited by directive classes which estimate + the initial trade-off parameter (beta). This class is not used directly to create + directives for the inversion. + + Parameters + ---------- + beta0_ratio : float + Desired ratio between data misfit and model objective function at initial beta iteration. + random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed used for random sampling. It can either be an int, + a predefined Numpy random number generator, or any valid input to + ``numpy.random.default_rng``. + + """ + + def __init__( + self, + beta0_ratio=1.0, + random_seed: RandomSeed | None = None, + **kwargs, + ): + # Deprecate seed argument + if kwargs.pop("seed", None) is not None: + raise TypeError( + "'seed' has been removed in " + " SimPEG v0.24.0, please use 'random_seed' instead.", + ) + super().__init__(**kwargs) + self.beta0_ratio = beta0_ratio + self.random_seed = random_seed + + @property + def beta0_ratio(self): + """The estimated ratio is multiplied by this to obtain beta. + + Returns + ------- + float + """ + return self._beta0_ratio + + @beta0_ratio.setter + def beta0_ratio(self, value): + self._beta0_ratio = validate_float( + "beta0_ratio", value, min_val=0.0, inclusive_min=False + ) + + @property + def random_seed(self): + """Random seed to initialize with. + + Returns + ------- + int, numpy.random.Generator or None + """ + return self._random_seed + + @random_seed.setter + def random_seed(self, value): + try: + np.random.default_rng(value) + except TypeError as err: + msg = ( + "Unable to initialize the random number generator with " + f"a {type(value).__name__}" + ) + raise TypeError(msg) from err + self._random_seed = value + + def validate(self, directive_list): + ind = [isinstance(d, BaseBetaEstimator) for d in directive_list.dList] + assert np.sum(ind) == 1, ( + "Multiple directives for computing initial beta detected in directives list. " + "Only one directive can be used to set the initial beta." + ) + + return True + + seed = deprecate_property( + random_seed, + "seed", + "random_seed", + removal_version="0.24.0", + error=True, + ) + + +class BetaEstimateMaxDerivative(BaseBetaEstimator): + r"""Estimate initial trade-off parameter (beta) using largest derivatives. + + The initial trade-off parameter (beta) is estimated by scaling the ratio + between the largest derivatives in the gradient of the data misfit and + model objective function. The estimated trade-off parameter is used to + update the **beta** property in the associated :class:`simpeg.inverse_problem.BaseInvProblem` + object prior to running the inversion. A separate directive is used for updating the + trade-off parameter at successive beta iterations; see :class:`BetaSchedule`. + + Parameters + ---------- + beta0_ratio: float + Desired ratio between data misfit and model objective function at initial beta iteration. + random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed used for random sampling. It can either be an int, + a predefined Numpy random number generator, or any valid input to + ``numpy.random.default_rng``. + + Notes + ----- + Let :math:`\phi_d` represent the data misfit, :math:`\phi_m` represent the model + objective function and :math:`\mathbf{m_0}` represent the starting model. The first + model update is obtained by minimizing the a global objective function of the form: + + .. math:: + \phi (\mathbf{m_0}) = \phi_d (\mathbf{m_0}) + \beta_0 \phi_m (\mathbf{m_0}) + + where :math:`\beta_0` represents the initial trade-off parameter (beta). + + We define :math:`\gamma` as the desired ratio between the data misfit and model objective + functions at the initial beta iteration (defined by the 'beta0_ratio' input argument). + Here, the initial trade-off parameter is computed according to: + + .. math:: + \beta_0 = \gamma \frac{| \nabla_m \phi_d (\mathbf{m_0}) |_{max}}{| \nabla_m \phi_m (\mathbf{m_0 + \delta m}) |_{max}} + + where + + .. math:: + \delta \mathbf{m} = \frac{m_{max}}{\mu_{max}} \boldsymbol{\mu} + + and :math:`\boldsymbol{\mu}` is a set of independent samples from the + continuous uniform distribution between 0 and 1. + + """ + + def __init__( + self, beta0_ratio=1.0, random_seed: RandomSeed | None = None, **kwargs + ): + super().__init__(beta0_ratio=beta0_ratio, random_seed=random_seed, **kwargs) + + def initialize(self): + rng = np.random.default_rng(seed=self.random_seed) + + if self.verbose: + print("Calculating the beta0 parameter.") + + m = self.invProb.model + + x0 = rng.random(size=m.shape) + phi_d_deriv = np.abs(self.dmisfit.deriv(m)).max() + dm = x0 / x0.max() * m.max() + phi_m_deriv = np.abs(self.reg.deriv(m + dm)).max() + + self.ratio = np.asarray(phi_d_deriv / phi_m_deriv) + self.beta0 = self.beta0_ratio * self.ratio + self.invProb.beta = self.beta0 + + +class BetaEstimate_ByEig(BaseBetaEstimator): + r"""Estimate initial trade-off parameter (beta) by power iteration. + + The initial trade-off parameter (beta) is estimated by scaling the ratio + between the largest eigenvalue in the second derivative of the data + misfit and the model objective function. The largest eigenvalues are estimated + using the power iteration method; see :func:`simpeg.utils.eigenvalue_by_power_iteration`. + The estimated trade-off parameter is used to update the **beta** property in the + associated :class:`simpeg.inverse_problem.BaseInvProblem` object prior to running the inversion. + Note that a separate directive is used for updating the trade-off parameter at successive + beta iterations; see :class:`BetaSchedule`. + + Parameters + ---------- + beta0_ratio: float + Desired ratio between data misfit and model objective function at initial beta iteration. + n_pw_iter : int + Number of power iterations used to estimate largest eigenvalues. + random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed used for random sampling. It can either be an int, + a predefined Numpy random number generator, or any valid input to + ``numpy.random.default_rng``. + + Notes + ----- + Let :math:`\phi_d` represent the data misfit, :math:`\phi_m` represent the model + objective function and :math:`\mathbf{m_0}` represent the starting model. The first + model update is obtained by minimizing the a global objective function of the form: + + .. math:: + \phi (\mathbf{m_0}) = \phi_d (\mathbf{m_0}) + \beta_0 \phi_m (\mathbf{m_0}) + + where :math:`\beta_0` represents the initial trade-off parameter (beta). + Let :math:`\gamma` define the desired ratio between the data misfit and model + objective functions at the initial beta iteration (defined by the 'beta0_ratio' input argument). + Using the power iteration approach, our initial trade-off parameter is given by: + + .. math:: + \beta_0 = \gamma \frac{\lambda_d}{\lambda_m} + + where :math:`\lambda_d` as the largest eigenvalue of the Hessian of the data misfit, and + :math:`\lambda_m` as the largest eigenvalue of the Hessian of the model objective function. + For each Hessian, the largest eigenvalue is computed using power iteration. The input + parameter 'n_pw_iter' sets the number of power iterations used in the estimate. + + For a description of the power iteration approach for estimating the larges eigenvalue, + see :func:`simpeg.utils.eigenvalue_by_power_iteration`. + + """ + + def __init__( + self, + beta0_ratio=1.0, + n_pw_iter=4, + random_seed: RandomSeed | None = None, + **kwargs, + ): + super().__init__(beta0_ratio=beta0_ratio, random_seed=random_seed, **kwargs) + self.n_pw_iter = n_pw_iter + + @property + def n_pw_iter(self): + """Number of power iterations for estimating largest eigenvalues. + + Returns + ------- + int + Number of power iterations for estimating largest eigenvalues. + """ + return self._n_pw_iter + + @n_pw_iter.setter + def n_pw_iter(self, value): + self._n_pw_iter = validate_integer("n_pw_iter", value, min_val=1) + + def initialize(self): + rng = np.random.default_rng(seed=self.random_seed) + + if self.verbose: + print("Calculating the beta0 parameter.") + + m = self.invProb.model + + dm_eigenvalue = eigenvalue_by_power_iteration( + self.dmisfit, + m, + n_pw_iter=self.n_pw_iter, + random_seed=rng, + ) + reg_eigenvalue = eigenvalue_by_power_iteration( + self.reg, + m, + n_pw_iter=self.n_pw_iter, + random_seed=rng, + ) + + self.ratio = np.asarray(dm_eigenvalue / reg_eigenvalue) + self.beta0 = self.beta0_ratio * self.ratio + self.invProb.beta = self.beta0 + + +class BetaSchedule(InversionDirective): + """Reduce trade-off parameter (beta) at successive iterations using a cooling schedule. + + Updates the **beta** property in the associated :class:`simpeg.inverse_problem.BaseInvProblem` + while the inversion is running. + For linear least-squares problems, the optimization problem can be solved in a + single step and the cooling rate can be set to *1*. For non-linear optimization + problems, multiple steps are required obtain the minimizer for a fixed trade-off + parameter. In this case, the cooling rate should be larger than 1. + + Parameters + ---------- + coolingFactor : float + The factor by which the trade-off parameter is decreased when updated. + The preexisting value of the trade-off parameter is divided by the cooling factor. + coolingRate : int + Sets the number of successive iterations before the trade-off parameter is reduced. + Use *1* for linear least-squares optimization problems. Use *2* for weakly non-linear + optimization problems. Use *3* for general non-linear optimization problems. + + """ + + def __init__(self, coolingFactor=8.0, coolingRate=3, **kwargs): + super().__init__(**kwargs) + self.coolingFactor = coolingFactor + self.coolingRate = coolingRate + + @property + def coolingFactor(self): + """Beta is divided by this value every `coolingRate` iterations. + + Returns + ------- + float + """ + return self._coolingFactor + + @coolingFactor.setter + def coolingFactor(self, value): + self._coolingFactor = validate_float( + "coolingFactor", value, min_val=0.0, inclusive_min=False + ) + + @property + def coolingRate(self): + """Cool after this number of iterations. + + Returns + ------- + int + """ + return self._coolingRate + + @coolingRate.setter + def coolingRate(self, value): + self._coolingRate = validate_integer("coolingRate", value, min_val=1) + + def endIter(self): + it = self.opt.iter + if 0 < it < self.opt.maxIter and it % self.coolingRate == 0: + if self.verbose: + print( + "BetaSchedule is cooling Beta. Iteration: {0:d}".format( + self.opt.iter + ) + ) + self.invProb.beta /= self.coolingFactor + + +class AlphasSmoothEstimate_ByEig(InversionDirective): + """ + Estimate the alphas multipliers for the smoothness terms of the regularization + as a multiple of the ratio between the highest eigenvalue of the + smallness term and the highest eigenvalue of each smoothness term of the regularization. + The highest eigenvalue are estimated through power iterations and Rayleigh quotient. + """ + + def __init__( + self, + alpha0_ratio=1.0, + n_pw_iter=4, + random_seed: RandomSeed | None = None, + **kwargs, + ): + # Deprecate seed argument + if kwargs.pop("seed", None) is not None: + raise TypeError( + "'seed' has been removed in " + " SimPEG v0.24.0, please use 'random_seed' instead.", + ) + super().__init__(**kwargs) + self.alpha0_ratio = alpha0_ratio + self.n_pw_iter = n_pw_iter + self.random_seed = random_seed + + @property + def alpha0_ratio(self): + """the estimated Alpha_smooth is multiplied by this ratio (int or array). + + Returns + ------- + numpy.ndarray + """ + return self._alpha0_ratio + + @alpha0_ratio.setter + def alpha0_ratio(self, value): + self._alpha0_ratio = validate_ndarray_with_shape( + "alpha0_ratio", value, shape=("*",) + ) + + @property + def n_pw_iter(self): + """Number of power iterations for estimation. + + Returns + ------- + int + """ + return self._n_pw_iter + + @n_pw_iter.setter + def n_pw_iter(self, value): + self._n_pw_iter = validate_integer("n_pw_iter", value, min_val=1) + + @property + def random_seed(self): + """Random seed to initialize with. + + Returns + ------- + int, numpy.random.Generator or None + """ + return self._random_seed + + @random_seed.setter + def random_seed(self, value): + try: + np.random.default_rng(value) + except TypeError as err: + msg = ( + "Unable to initialize the random number generator with " + f"a {type(value).__name__}" + ) + raise TypeError(msg) from err + self._random_seed = value + + seed = deprecate_property( + random_seed, + "seed", + "random_seed", + removal_version="0.24.0", + error=True, + ) + + def initialize(self): + """""" + rng = np.random.default_rng(seed=self.random_seed) + + smoothness = [] + smallness = [] + parents = {} + for regobjcts in self.reg.objfcts: + if isinstance(regobjcts, ComboObjectiveFunction): + objfcts = regobjcts.objfcts + else: + objfcts = [regobjcts] + + for obj in objfcts: + if isinstance( + obj, + ( + Smallness, + SparseSmallness, + PGIsmallness, + ), + ): + smallness += [obj] + + elif isinstance(obj, (SmoothnessFirstOrder, SparseSmoothness)): + parents[obj] = regobjcts + smoothness += [obj] + + if len(smallness) == 0: + raise UserWarning( + "Directive 'AlphasSmoothEstimate_ByEig' requires a regularization with at least one Small instance." + ) + + smallness_eigenvalue = eigenvalue_by_power_iteration( + smallness[0], + self.invProb.model, + n_pw_iter=self.n_pw_iter, + random_seed=rng, + ) + + self.alpha0_ratio = self.alpha0_ratio * np.ones(len(smoothness)) + + if len(self.alpha0_ratio) != len(smoothness): + raise ValueError( + f"Input values for 'alpha0_ratio' should be of len({len(smoothness)}). Provided {self.alpha0_ratio}" + ) + + alphas = [] + for user_alpha, obj in zip(self.alpha0_ratio, smoothness): + smooth_i_eigenvalue = eigenvalue_by_power_iteration( + obj, + self.invProb.model, + n_pw_iter=self.n_pw_iter, + random_seed=rng, + ) + ratio = smallness_eigenvalue / smooth_i_eigenvalue + + mtype = obj._multiplier_pair + + new_alpha = getattr(parents[obj], mtype) * user_alpha * ratio + setattr(parents[obj], mtype, new_alpha) + alphas += [new_alpha] + + if self.verbose: + print(f"Alpha scales: {alphas}") + + +class ScalingMultipleDataMisfits_ByEig(InversionDirective): + """ + For multiple data misfits only: multiply each data misfit term + by the inverse of its highest eigenvalue and then + normalize the sum of the data misfit multipliers to one. + The highest eigenvalue are estimated through power iterations and Rayleigh quotient. + """ + + def __init__( + self, + chi0_ratio=None, + n_pw_iter=4, + random_seed: RandomSeed | None = None, + **kwargs, + ): + # Deprecate seed argument + if kwargs.pop("seed", None) is not None: + raise TypeError( + "'seed' has been removed in " + " SimPEG v0.24.0, please use 'random_seed' instead.", + ) + super().__init__(**kwargs) + self.chi0_ratio = chi0_ratio + self.n_pw_iter = n_pw_iter + self.random_seed = random_seed + + @property + def chi0_ratio(self): + """the estimated Alpha_smooth is multiplied by this ratio (int or array) + + Returns + ------- + numpy.ndarray + """ + return self._chi0_ratio + + @chi0_ratio.setter + def chi0_ratio(self, value): + if value is not None: + value = validate_ndarray_with_shape("chi0_ratio", value, shape=("*",)) + self._chi0_ratio = value + + @property + def n_pw_iter(self): + """Number of power iterations for estimation. + + Returns + ------- + int + """ + return self._n_pw_iter + + @n_pw_iter.setter + def n_pw_iter(self, value): + self._n_pw_iter = validate_integer("n_pw_iter", value, min_val=1) + + @property + def random_seed(self): + """Random seed to initialize with + + Returns + ------- + int, numpy.random.Generator or None + """ + return self._random_seed + + @random_seed.setter + def random_seed(self, value): + try: + np.random.default_rng(value) + except TypeError as err: + msg = ( + "Unable to initialize the random number generator with " + f"a {type(value).__name__}" + ) + raise TypeError(msg) from err + self._random_seed = value + + seed = deprecate_property( + random_seed, + "seed", + "random_seed", + removal_version="0.24.0", + error=True, + ) + + def initialize(self): + """""" + rng = np.random.default_rng(seed=self.random_seed) + + if self.verbose: + print("Calculating the scaling parameter.") + + if ( + getattr(self.dmisfit, "objfcts", None) is None + or len(self.dmisfit.objfcts) == 1 + ): + raise TypeError( + "ScalingMultipleDataMisfits_ByEig only applies to joint inversion" + ) + + ndm = len(self.dmisfit.objfcts) + if self.chi0_ratio is not None: + self.chi0_ratio = self.chi0_ratio * np.ones(ndm) + else: + self.chi0_ratio = self.dmisfit.multipliers + + m = self.invProb.model + + dm_eigenvalue_list = [] + for dm in self.dmisfit.objfcts: + dm_eigenvalue_list += [ + eigenvalue_by_power_iteration(dm, m, random_seed=rng) + ] + + self.chi0 = self.chi0_ratio / np.r_[dm_eigenvalue_list] + self.chi0 = self.chi0 / np.sum(self.chi0) + self.dmisfit.multipliers = self.chi0 + + if self.verbose: + print("Scale Multipliers: ", self.dmisfit.multipliers) + + +class JointScalingSchedule(InversionDirective): + """ + For multiple data misfits only: rebalance each data misfit term + during the inversion when some datasets are fit, and others not + using the ratios of current misfits and their respective target. + It implements the strategy described in https://doi.org/10.1093/gji/ggaa378. + """ + + def __init__( + self, warmingFactor=1.0, chimax=1e10, chimin=1e-10, update_rate=1, **kwargs + ): + super().__init__(**kwargs) + self.mode = 1 + self.warmingFactor = warmingFactor + self.chimax = chimax + self.chimin = chimin + self.update_rate = update_rate + + @property + def mode(self): + """The type of update to perform. + + Returns + ------- + {1, 2} + """ + return self._mode + + @mode.setter + def mode(self, value): + self._mode = validate_integer("mode", value, min_val=1, max_val=2) + + @property + def warmingFactor(self): + """Factor to adjust scaling of the data misfits by. + + Returns + ------- + float + """ + return self._warmingFactor + + @warmingFactor.setter + def warmingFactor(self, value): + self._warmingFactor = validate_float( + "warmingFactor", value, min_val=0.0, inclusive_min=False + ) + + @property + def chimax(self): + """Maximum chi factor. + + Returns + ------- + float + """ + return self._chimax + + @chimax.setter + def chimax(self, value): + self._chimax = validate_float("chimax", value, min_val=0.0, inclusive_min=False) + + @property + def chimin(self): + """Minimum chi factor. + + Returns + ------- + float + """ + return self._chimin + + @chimin.setter + def chimin(self, value): + self._chimin = validate_float("chimin", value, min_val=0.0, inclusive_min=False) + + @property + def update_rate(self): + """Will update the data misfit scalings after this many iterations. + + Returns + ------- + int + """ + return self._update_rate + + @update_rate.setter + def update_rate(self, value): + self._update_rate = validate_integer("update_rate", value, min_val=1) + + def initialize(self): + if ( + getattr(self.dmisfit, "objfcts", None) is None + or len(self.dmisfit.objfcts) == 1 + ): + raise TypeError("JointScalingSchedule only applies to joint inversion") + + targetclass = np.r_[ + [ + isinstance(dirpart, MultiTargetMisfits) + for dirpart in self.inversion.directiveList.dList + ] + ] + if ~np.any(targetclass): + self.DMtarget = None + else: + self.targetclass = np.where(targetclass)[0][-1] + self.DMtarget = self.inversion.directiveList.dList[ + self.targetclass + ].DMtarget + + if self.verbose: + print("Initial data misfit scales: ", self.dmisfit.multipliers) + + def endIter(self): + self.dmlist = self.inversion.directiveList.dList[self.targetclass].dmlist + + if np.any(self.dmlist < self.DMtarget): + self.mode = 2 + else: + self.mode = 1 + + if self.opt.iter > 0 and self.opt.iter % self.update_rate == 0: + if self.mode == 2: + if np.all(np.r_[self.dmisfit.multipliers] > self.chimin) and np.all( + np.r_[self.dmisfit.multipliers] < self.chimax + ): + indx = self.dmlist > self.DMtarget + if np.any(indx): + multipliers = self.warmingFactor * np.median( + self.DMtarget[~indx] / self.dmlist[~indx] + ) + if np.sum(indx) == 1: + indx = np.where(indx)[0][0] + self.dmisfit.multipliers[indx] *= multipliers + self.dmisfit.multipliers /= np.sum(self.dmisfit.multipliers) + + if self.verbose: + print("Updating scaling for data misfits by ", multipliers) + print("New scales:", self.dmisfit.multipliers) + + +class TargetMisfit(InversionDirective): + """ + ... note:: Currently this target misfit is not set up for joint inversion. + Check out MultiTargetMisfits + """ + + def __init__(self, target=None, phi_d_star=None, chifact=1.0, **kwargs): + super().__init__(**kwargs) + self.chifact = chifact + self.phi_d_star = phi_d_star + if phi_d_star is not None and target is not None: + raise AttributeError("Attempted to set both target and phi_d_star.") + if target is not None: + self.target = target + + @property + def target(self): + """The target value for the data misfit + + Returns + ------- + float + """ + if getattr(self, "_target", None) is None: + self._target = self.chifact * self.phi_d_star + return self._target + + @target.setter + def target(self, val): + self._target = validate_float("target", val, min_val=0.0, inclusive_min=False) + + @property + def chifact(self): + """The a multiplier for the target data misfit value. + + The target value is `chifact` times `phi_d_star` + + Returns + ------- + float + """ + return self._chifact + + @chifact.setter + def chifact(self, value): + self._chifact = validate_float( + "chifact", value, min_val=0.0, inclusive_min=False + ) + self._target = None + + @property + def phi_d_star(self): + """The target phi_d value for the data misfit. + + The target value is `chifact` times `phi_d_star` + + Returns + ------- + float + """ + # phid = ||dpred - dobs||^2 + if self._phi_d_star is None: + nD = 0 + for survey in self.survey: + nD += survey.nD + self._phi_d_star = nD + return self._phi_d_star + + @phi_d_star.setter + def phi_d_star(self, value): + # phid = ||dpred - dobs||^2 + if value is not None: + value = validate_float( + "phi_d_star", value, min_val=0.0, inclusive_min=False + ) + self._phi_d_star = value + self._target = None + + def initialize(self): + logger = get_logger() + logger.info( + f"Directive {self.__class__.__name__}: Target data misfit is {self.target}" + ) + + def endIter(self): + if self.invProb.phi_d < self.target: + self.opt.stopNextIteration = True + self.print_final_misfit() + + def print_final_misfit(self): + if self.opt.print_type == "ubc": + self.opt.print_target = ( + ">> Target misfit: %.1f (# of data) is achieved" + ) % (self.target) + + +class MultiTargetMisfits(InversionDirective): + def __init__( + self, + WeightsInTarget=False, + chifact=1.0, + phi_d_star=None, + TriggerSmall=True, + chiSmall=1.0, + phi_ms_star=None, + TriggerTheta=False, + ToleranceTheta=1.0, + distance_norm=np.inf, + **kwargs, + ): + super().__init__(**kwargs) + + self.WeightsInTarget = WeightsInTarget + # Chi factor for Geophsyical Data Misfit + self.chifact = chifact + self.phi_d_star = phi_d_star + + # Chifact for Clustering/Smallness + self.TriggerSmall = TriggerSmall + self.chiSmall = chiSmall + self.phi_ms_star = phi_ms_star + + # Tolerance for parameters difference with their priors + self.TriggerTheta = TriggerTheta # deactivated by default + self.ToleranceTheta = ToleranceTheta + self.distance_norm = distance_norm + + self._DM = False + self._CL = False + self._DP = False + + @property + def WeightsInTarget(self): + """Whether to account for weights in the petrophysical misfit. + + Returns + ------- + bool + """ + return self._WeightsInTarget + + @WeightsInTarget.setter + def WeightsInTarget(self, value): + self._WeightsInTarget = validate_type("WeightsInTarget", value, bool) + + @property + def chifact(self): + """The a multiplier for the target Geophysical data misfit value. + + The target value is `chifact` times `phi_d_star` + + Returns + ------- + numpy.ndarray + """ + return self._chifact + + @chifact.setter + def chifact(self, value): + self._chifact = validate_ndarray_with_shape("chifact", value, shape=("*",)) + self._DMtarget = None + + @property + def phi_d_star(self): + """The target phi_d value for the Geophysical data misfit. + + The target value is `chifact` times `phi_d_star` + + Returns + ------- + float + """ + # phid = || dpred - dobs||^2 + if getattr(self, "_phi_d_star", None) is None: + # Check if it is a ComboObjective + if isinstance(self.dmisfit, ComboObjectiveFunction): + value = np.r_[[survey.nD for survey in self.survey]] + else: + value = np.r_[[self.survey.nD]] + self._phi_d_star = value + self._DMtarget = None + + return self._phi_d_star + + @phi_d_star.setter + def phi_d_star(self, value): + # phid =|| dpred - dobs||^2 + if value is not None: + value = validate_ndarray_with_shape("phi_d_star", value, shape=("*",)) + self._phi_d_star = value + self._DMtarget = None + + @property + def chiSmall(self): + """The a multiplier for the target petrophysical misfit value. + + The target value is `chiSmall` times `phi_ms_star` + + Returns + ------- + float + """ + return self._chiSmall + + @chiSmall.setter + def chiSmall(self, value): + self._chiSmall = validate_float("chiSmall", value) + self._CLtarget = None + + @property + def phi_ms_star(self): + """The target value for the petrophysical data misfit. + + The target value is `chiSmall` times `phi_ms_star` + + Returns + ------- + float + """ + return self._phi_ms_star + + @phi_ms_star.setter + def phi_ms_star(self, value): + if value is not None: + value = validate_float("phi_ms_star", value) + self._phi_ms_star = value + self._CLtarget = None + + @property + def TriggerSmall(self): + """Whether to trigger the smallness misfit test. + + Returns + ------- + bool + """ + return self._TriggerSmall + + @TriggerSmall.setter + def TriggerSmall(self, value): + self._TriggerSmall = validate_type("TriggerSmall", value, bool) + + @property + def TriggerTheta(self): + """Whether to trigger the GMM misfit test. + + Returns + ------- + bool + """ + return self._TriggerTheta + + @TriggerTheta.setter + def TriggerTheta(self, value): + self._TriggerTheta = validate_type("TriggerTheta", value, bool) + + @property + def ToleranceTheta(self): + """Target value for the GMM misfit. + + Returns + ------- + float + """ + return self._ToleranceTheta + + @ToleranceTheta.setter + def ToleranceTheta(self, value): + self._ToleranceTheta = validate_float("ToleranceTheta", value, min_val=0.0) + + @property + def distance_norm(self): + """Distance norm to use for GMM misfit measure. + + Returns + ------- + float + """ + return self._distance_norm + + @distance_norm.setter + def distance_norm(self, value): + self._distance_norm = validate_float("distance_norm", value, min_val=0.0) + + def initialize(self): + self.dmlist = np.r_[[dmis(self.invProb.model) for dmis in self.dmisfit.objfcts]] + + if getattr(self.invProb.reg.objfcts[0], "objfcts", None) is not None: + smallness = np.r_[ + [ + ( + np.r_[ + i, + j, + isinstance(regpart, PGIsmallness), + ] + ) + for i, regobjcts in enumerate(self.invProb.reg.objfcts) + for j, regpart in enumerate(regobjcts.objfcts) + ] + ] + if smallness[smallness[:, 2] == 1][:, :2].size == 0: + warnings.warn( + "There is no PGI regularization. Smallness target is turned off (TriggerSmall flag)", + stacklevel=2, + ) + self.smallness = -1 + self.pgi_smallness = None + + else: + self.smallness = smallness[smallness[:, 2] == 1][:, :2][0] + self.pgi_smallness = self.invProb.reg.objfcts[ + self.smallness[0] + ].objfcts[self.smallness[1]] + + if self.verbose: + print( + type( + self.invProb.reg.objfcts[self.smallness[0]].objfcts[ + self.smallness[1] + ] + ) + ) + + self._regmode = 1 + + else: + smallness = np.r_[ + [ + ( + np.r_[ + j, + isinstance(regpart, PGIsmallness), + ] + ) + for j, regpart in enumerate(self.invProb.reg.objfcts) + ] + ] + if smallness[smallness[:, 1] == 1][:, :1].size == 0: + if self.TriggerSmall: + warnings.warn( + "There is no PGI regularization. Smallness target is turned off (TriggerSmall flag).", + stacklevel=2, + ) + self.TriggerSmall = False + self.smallness = -1 + else: + self.smallness = smallness[smallness[:, 1] == 1][:, :1][0] + self.pgi_smallness = self.invProb.reg.objfcts[self.smallness[0]] + + if self.verbose: + print(type(self.invProb.reg.objfcts[self.smallness[0]])) + + self._regmode = 2 + + @property + def DM(self): + """Whether the geophysical data misfit target was satisfied. + + Returns + ------- + bool + """ + return self._DM + + @property + def CL(self): + """Whether the petrophysical misfit target was satisified. + + Returns + ------- + bool + """ + return self._CL + + @property + def DP(self): + """Whether the GMM misfit was below the threshold. + + Returns + ------- + bool + """ + return self._DP + + @property + def AllStop(self): + """Whether all target misfit values have been met. + + Returns + ------- + bool + """ + + return self.DM and self.CL and self.DP + + @property + def DMtarget(self): + if getattr(self, "_DMtarget", None) is None: + self._DMtarget = self.chifact * self.phi_d_star + return self._DMtarget + + @DMtarget.setter + def DMtarget(self, val): + self._DMtarget = val + + @property + def CLtarget(self): + if not getattr(self.pgi_smallness, "approx_eval", True): + # if nonlinear prior, compute targer numerically at each GMM update + samples, _ = self.pgi_smallness.gmm.sample( + len(self.pgi_smallness.gmm.cell_volumes) + ) + self.phi_ms_star = self.pgi_smallness( + mkvc(samples), externalW=self.WeightsInTarget + ) + + self._CLtarget = self.chiSmall * self.phi_ms_star + + elif getattr(self, "_CLtarget", None) is None: + # phid = ||dpred - dobs||^2 + if self.phi_ms_star is None: + # Expected value is number of active cells * number of physical + # properties + self.phi_ms_star = len(self.invProb.model) + + self._CLtarget = self.chiSmall * self.phi_ms_star + + return self._CLtarget + + @property + def CLnormalizedConstant(self): + if ~self.WeightsInTarget: + return 1.0 + elif np.any(self.smallness == -1): + return np.sum( + sp.csr_matrix.diagonal(self.invProb.reg.objfcts[0].W) ** 2.0 + ) / len(self.invProb.model) + else: + return np.sum(sp.csr_matrix.diagonal(self.pgi_smallness.W) ** 2.0) / len( + self.invProb.model + ) + + @CLtarget.setter + def CLtarget(self, val): + self._CLtarget = val + + def phims(self): + if np.any(self.smallness == -1): + return self.invProb.reg.objfcts[0](self.invProb.model) + else: + return ( + self.pgi_smallness( + self.invProb.model, external_weights=self.WeightsInTarget + ) + / self.CLnormalizedConstant + ) + + def ThetaTarget(self): + maxdiff = 0.0 + + for i in range(self.invProb.reg.gmm.n_components): + meandiff = np.linalg.norm( + (self.invProb.reg.gmm.means_[i] - self.invProb.reg.gmmref.means_[i]) + / self.invProb.reg.gmmref.means_[i], + ord=self.distance_norm, + ) + maxdiff = np.maximum(maxdiff, meandiff) + + if ( + self.invProb.reg.gmm.covariance_type == "full" + or self.invProb.reg.gmm.covariance_type == "spherical" + ): + covdiff = np.linalg.norm( + ( + self.invProb.reg.gmm.covariances_[i] + - self.invProb.reg.gmmref.covariances_[i] + ) + / self.invProb.reg.gmmref.covariances_[i], + ord=self.distance_norm, + ) + else: + covdiff = np.linalg.norm( + ( + self.invProb.reg.gmm.covariances_ + - self.invProb.reg.gmmref.covariances_ + ) + / self.invProb.reg.gmmref.covariances_, + ord=self.distance_norm, + ) + maxdiff = np.maximum(maxdiff, covdiff) + + pidiff = np.linalg.norm( + [ + ( + self.invProb.reg.gmm.weights_[i] + - self.invProb.reg.gmmref.weights_[i] + ) + / self.invProb.reg.gmmref.weights_[i] + ], + ord=self.distance_norm, + ) + maxdiff = np.maximum(maxdiff, pidiff) + + return maxdiff + + def endIter(self): + self._DM = False + self._CL = True + self._DP = True + self.dmlist = np.r_[[dmis(self.invProb.model) for dmis in self.dmisfit.objfcts]] + self.targetlist = np.r_[ + [dm < tgt for dm, tgt in zip(self.dmlist, self.DMtarget)] + ] + + if np.all(self.targetlist): + self._DM = True + + if self.TriggerSmall and np.any(self.smallness != -1): + if self.phims() > self.CLtarget: + self._CL = False + + if self.TriggerTheta: + if self.ThetaTarget() > self.ToleranceTheta: + self._DP = False + + if self.verbose: + message = "geophys. misfits: " + "; ".join( + map( + str, + [ + "{0} (target {1} [{2}])".format(val, tgt, cond) + for val, tgt, cond in zip( + np.round(self.dmlist, 1), + np.round(self.DMtarget, 1), + self.targetlist, + ) + ], + ) + ) + if self.TriggerSmall: + message += ( + " | smallness misfit: {0:.1f} (target: {1:.1f} [{2}])".format( + self.phims(), self.CLtarget, self.CL + ) + ) + if self.TriggerTheta: + message += " | GMM parameters within tolerance: {}".format(self.DP) + print(message) + + if self.AllStop: + self.opt.stopNextIteration = True + if self.verbose: + print("All targets have been reached") + + +class SaveEveryIteration(InversionDirective, metaclass=ABCMeta): + """SaveEveryIteration + + This directive saves information at each iteration. + + Parameters + ---------- + directory : pathlib.Path or str, optional + The directory to store output information to, defaults to current directory. + name : str, optional + Root of the filename to be saved, commonly this will get iteration specific + details appended to it. + on_disk : bool, optional + Whether this directive will save a log file to disk. + """ + + def __init__(self, directory=".", name="InversionModel", on_disk=True, **kwargs): + self._on_disk = validate_type("on_disk", on_disk, bool) + + super().__init__(**kwargs) + if self.on_disk: + self.directory = directory + else: + self.directory = None + self.name = name + self._time_string_format = "%Y-%m-%d-%H-%M" + self._iter_format = "03d" + self._iter_string = "###" + self._start_time = self._time_string_format + + def initialize(self): + self._start_time = datetime.now().strftime(self._time_string_format) + if opt := getattr(self, "opt", None): + max_digit = len(str(opt.maxIter)) + self._iter_format = f"0{max_digit}d" + + @property + def on_disk(self) -> bool: + """Whether this object stores information to `file_abs_path`.""" + return self._on_disk + + @on_disk.setter + def on_disk(self, value): + self._on_disk = validate_type("on_disk", value, bool) + + @property + def directory(self) -> pathlib.Path: + """Directory to save results in. + + Returns + ------- + pathlib.Path + """ + if not self.on_disk: + raise AttributeError( + f"'{type(self).__qualname__}.directory' is only available if saving to disk." + ) + return self._directory + + @directory.setter + def directory(self, value): + if value is None and self.on_disk: + raise ValueError("Directory is not optional if 'on_disk==True'.") + if value is not None: + value = validate_type("directory", value, pathlib.Path).resolve() + self._directory = value + + @property + def name(self) -> str: + """Root of the filename to be saved. + + Returns + ------- + str + """ + return self._name + + @name.setter + def name(self, value): + self._name = validate_string("name", value) + + @property + def _time_iter_file_name(self) -> pathlib.Path: + time_string = self._start_time + if not getattr(self, "opt", None): + iter_string = "###" + else: + itr = getattr(self.opt, "iter", 0) + iter_string = f"{itr:{self._iter_format}}" + + return pathlib.Path(f"{self.name}_{time_string}_{iter_string}") + + @property + def _time_file_name(self) -> pathlib.Path: + return pathlib.Path(f"{self.name}_{self._start_time}") + + def _mkdir_and_check_output_file(self, should_exist=False): + """ + Use this to ensure a directory exists, and to check if file_abs_path exists. + Issues a warning if the output file exists but should not, + or if it doesn't exist but does. + + Parameters + ---------- + should_exist : bool, optional + Whether file_abs_path should exist. + """ + self.directory.mkdir(exist_ok=True) + fp = self.file_abs_path + exists = fp.exists() + if exists and not should_exist: + warnings.warn(f"Overwriting file {fp}", UserWarning, stacklevel=2) + if not exists and should_exist: + warnings.warn( + f"File {fp} was not found, creating a new one.", + UserWarning, + stacklevel=2, + ) + + @property + def fileName(self): + warnings.warn( + "'fileName' has been deprecated and will be removed in SimPEG 0.26.0 use 'file_abs_path'", + FutureWarning, + stacklevel=2, + ) + return self.file_abs_path.stem + + @property + @abstractmethod + def file_abs_path(self) -> pathlib.Path: + """The absolute path to the saved output file. + + Returns + ------- + pathlib.Path + """ + + +class SaveModelEveryIteration(SaveEveryIteration): + """Saves the inversion model at the end of every iteration to a directory + + Parameters + ---------- + directory : pathlib.Path or str, optional + The directory to store output information to, defaults to current directory. + name : str, optional + Root of the filename to be saved, defaults to ``'InversionModel'`` + + Notes + ----- + + This directive saves the model as a numpy array at each iteration. The + default directory is the current directory and the models are saved as + `name` + ``'_YYYY-MM-DD-HH-MM_iter.npy'`` + """ + + def __init__(self, **kwargs): + if "on_disk" in kwargs: + msg = ( + f"The 'on_disk' argument is ignored by the '{type(self).__name__}' " + "directive, it's always True." + ) + warnings.warn(msg, UserWarning, stacklevel=2) + kwargs.pop("on_disk") + super().__init__(on_disk=True, **kwargs) + + def initialize(self): + super().initialize() + print( + f"{type(self).__qualname__} will save your models as: " + f"'{self.file_abs_path}'" + ) + + @property + def on_disk(self) -> bool: + """This class always saves to disk. + + Returns + ------- + bool + """ + return True + + @on_disk.setter + def on_disk(self, value): # noqa: F811 + """This class always saves to disk.""" + msg = ( + f"Cannot modify value of 'on_disk' for {type(self).__name__}' directive. " + "It's always True." + ) + raise AttributeError(msg) + + @property + def file_abs_path(self) -> pathlib.Path: + return self.directory / self._time_iter_file_name.with_suffix(".npy") + + def endIter(self): + self._mkdir_and_check_output_file(should_exist=False) + np.save(self.file_abs_path, self.opt.xc) + + +class SaveOutputEveryIteration(SaveEveryIteration): + """Keeps track of the objective function values. + + Parameters + ---------- + on_disk : bool, optional + Whether this directive additionally stores the log to a text file. + directory : pathlib.Path, optional + The directory to store output information to if `on_disk`, defaults to current directory. + name : str, optional + The root name of the file to save to, will append the inversion start time to this value. + """ + + def __init__(self, on_disk=True, **kwargs): + if (save_txt := kwargs.pop("save_txt", None)) is not None: + self.save_txt = save_txt + on_disk = self.save_txt + super().__init__(on_disk=on_disk, **kwargs) + + def initialize(self): + super().initialize() + if self.on_disk: + fp = self.file_abs_path + print( + f"'{type(self).__qualname__}' will save your inversion " + f"progress to: '{fp}'" + ) + self._mkdir_and_check_output_file(should_exist=False) + with open(fp, "w") as f: + f.write(f"{self._header}\n") + self._initialize_lists() + + @property + def _header(self): + return " # beta phi_d phi_m phi_m_small phi_m_smoomth_x phi_m_smoomth_y phi_m_smoomth_z phi" + + def _initialize_lists(self): + # Create a list of each + self.beta = [] + self.phi_d = [] + self.phi_m = [] + self.phi_m_small = [] + self.phi_m_smooth_x = [] + self.phi_m_smooth_y = [] + self.phi_m_smooth_z = [] + self.phi = [] + + @property + def file_abs_path(self) -> pathlib.Path | None: + """The absolute path to the saved log file.""" + if self.on_disk: + return self.directory / self._time_file_name.with_suffix(".txt") + + save_txt = deprecate_property( + SaveEveryIteration.on_disk, + "save_txt", + removal_version="0.26.0", + future_warn=True, + ) + + def endIter(self): + phi_s, phi_x, phi_y, phi_z = 0, 0, 0, 0 + + for reg in self.reg.objfcts: + if isinstance(reg, Sparse): + i_s, i_x, i_y, i_z = 0, 1, 2, 3 + else: + i_s, i_x, i_y, i_z = 0, 1, 3, 5 + if getattr(reg, "alpha_s", None): + phi_s += reg.objfcts[i_s](self.invProb.model) * reg.alpha_s + if getattr(reg, "alpha_x", None): + phi_x += reg.objfcts[i_x](self.invProb.model) * reg.alpha_x + + if reg.regularization_mesh.dim > 1 and getattr(reg, "alpha_y", None): + phi_y += reg.objfcts[i_y](self.invProb.model) * reg.alpha_y + if reg.regularization_mesh.dim > 2 and getattr(reg, "alpha_z", None): + phi_z += reg.objfcts[i_z](self.invProb.model) * reg.alpha_z + + self.beta.append(self.invProb.beta) + self.phi_d.append(self.invProb.phi_d) + self.phi_m.append(self.invProb.phi_m) + self.phi_m_small.append(phi_s) + self.phi_m_smooth_x.append(phi_x) + self.phi_m_smooth_y.append(phi_y) + self.phi_m_smooth_z.append(phi_z) + self.phi.append(self.opt.f) + + if self.on_disk: + self._mkdir_and_check_output_file(should_exist=True) + with open(self.file_abs_path, "a") as f: + f.write( + " {0:3d} {1:1.4e} {2:1.4e} {3:1.4e} {4:1.4e} {5:1.4e} " + "{6:1.4e} {7:1.4e} {8:1.4e}\n".format( + self.opt.iter, + self.beta[-1], + self.phi_d[-1], + self.phi_m[-1], + self.phi_m_small[-1], + self.phi_m_smooth_x[-1], + self.phi_m_smooth_y[-1], + self.phi_m_smooth_z[-1], + self.phi[-1], + ) + ) + + def load_results(self, file_name=None): + if file_name is None: + if not self.on_disk: + raise TypeError( + f"'file_name' is a required argument if '{type(self).__qualname__}.on_disk' is `False`" + ) + file_name = self.file_abs_path + results = np.loadtxt(file_name, comments="#") + if results.shape[1] != 9: + raise ValueError(f"{file_name} does not have valid results") + + self.beta = results[:, 1] + self.phi_d = results[:, 2] + self.phi_m = results[:, 3] + self.phi_m_small = results[:, 4] + self.phi_m_smooth_x = results[:, 5] + self.phi_m_smooth_y = results[:, 6] + self.phi_m_smooth_z = results[:, 7] + self.f = results[:, 8] + + self.phi_m_smooth = ( + self.phi_m_smooth_x + self.phi_m_smooth_y + self.phi_m_smooth_z + ) + + self.target_misfit = self.invProb.dmisfit.simulation.survey.nD + self.i_target = None + + if self.invProb.phi_d < self.target_misfit: + i_target = 0 + while self.phi_d[i_target] > self.target_misfit: + i_target += 1 + self.i_target = i_target + + def plot_misfit_curves( + self, + fname=None, + dpi=300, + plot_small_smooth=False, + plot_phi_m=True, + plot_small=False, + plot_smooth=False, + ): + self.target_misfit = np.sum([dmis.nD for dmis in self.invProb.dmisfit.objfcts]) + self.i_target = None + + if self.invProb.phi_d < self.target_misfit: + i_target = 0 + while self.phi_d[i_target] > self.target_misfit: + i_target += 1 + self.i_target = i_target + + fig = plt.figure(figsize=(5, 2)) + ax = plt.subplot(111) + ax_1 = ax.twinx() + ax.semilogy( + np.arange(len(self.phi_d)), self.phi_d, "k-", lw=2, label=r"$\phi_d$" + ) + + if plot_phi_m: + ax_1.semilogy( + np.arange(len(self.phi_d)), self.phi_m, "r", lw=2, label=r"$\phi_m$" + ) + + if plot_small_smooth or plot_small: + ax_1.semilogy( + np.arange(len(self.phi_d)), self.phi_m_small, "ro", label="small" + ) + if plot_small_smooth or plot_smooth: + ax_1.semilogy( + np.arange(len(self.phi_d)), self.phi_m_smooth_x, "rx", label="smooth_x" + ) + ax_1.semilogy( + np.arange(len(self.phi_d)), self.phi_m_smooth_y, "rx", label="smooth_y" + ) + ax_1.semilogy( + np.arange(len(self.phi_d)), self.phi_m_smooth_z, "rx", label="smooth_z" + ) + + ax.legend(loc=1) + ax_1.legend(loc=2) + + ax.plot( + np.r_[ax.get_xlim()[0], ax.get_xlim()[1]], + np.ones(2) * self.target_misfit, + "k:", + ) + ax.set_xlabel("Iteration") + ax.set_ylabel(r"$\phi_d$") + ax_1.set_ylabel(r"$\phi_m$", color="r") + ax_1.tick_params(axis="y", which="both", colors="red") + + plt.show() + if fname is not None: + fig.savefig(fname, dpi=dpi) + + def plot_tikhonov_curves(self, fname=None, dpi=200): + self.target_misfit = self.invProb.dmisfit.simulation.survey.nD + self.i_target = None + + if self.invProb.phi_d < self.target_misfit: + i_target = 0 + while self.phi_d[i_target] > self.target_misfit: + i_target += 1 + self.i_target = i_target + + fig = plt.figure(figsize=(5, 8)) + ax1 = plt.subplot(311) + ax2 = plt.subplot(312) + ax3 = plt.subplot(313) + + ax1.plot(self.beta, self.phi_d, "k-", lw=2, ms=4) + ax1.set_xlim(np.hstack(self.beta).min(), np.hstack(self.beta).max()) + ax1.set_xlabel(r"$\beta$", fontsize=14) + ax1.set_ylabel(r"$\phi_d$", fontsize=14) + + ax2.plot(self.beta, self.phi_m, "k-", lw=2) + ax2.set_xlim(np.hstack(self.beta).min(), np.hstack(self.beta).max()) + ax2.set_xlabel(r"$\beta$", fontsize=14) + ax2.set_ylabel(r"$\phi_m$", fontsize=14) + + ax3.plot(self.phi_m, self.phi_d, "k-", lw=2) + ax3.set_xlim(np.hstack(self.phi_m).min(), np.hstack(self.phi_m).max()) + ax3.set_xlabel(r"$\phi_m$", fontsize=14) + ax3.set_ylabel(r"$\phi_d$", fontsize=14) + + if self.i_target is not None: + ax1.plot(self.beta[self.i_target], self.phi_d[self.i_target], "k*", ms=10) + ax2.plot(self.beta[self.i_target], self.phi_m[self.i_target], "k*", ms=10) + ax3.plot(self.phi_m[self.i_target], self.phi_d[self.i_target], "k*", ms=10) + + for ax in [ax1, ax2, ax3]: + ax.set_xscale("linear") + ax.set_yscale("linear") + plt.tight_layout() + plt.show() + if fname is not None: + fig.savefig(fname, dpi=dpi) + + +class SaveOutputDictEveryIteration(SaveEveryIteration): + """Saves inversion parameters to a dictionary at every iteration. + + At the end of every iteration, information about the current iteration is + saved to the `outDict` property of this object. + + Parameters + ---------- + on_disk : bool, optional + Whether to also save the parameters to an `npz` file at the end of each iteration. + directory : pathlib.Path or str, optional + Directory to save inversion parameters to if `on_disk`, defaults to current directory. + name : str, optional + Root name of the output file. The inversion start time and the iteration are appended to this. + """ + + # Initialize the output dict + def __init__(self, on_disk=False, **kwargs): + if (save_on_disk := kwargs.pop("saveOnDisk", None)) is not None: + self.saveOnDisk = save_on_disk + on_disk = self.saveOnDisk + super().__init__(on_disk=on_disk, **kwargs) + + saveOnDisk = deprecate_property( + SaveEveryIteration.on_disk, + "saveOnDisk", + removal_version="0.26.0", + future_warn=True, + ) + + @property + def file_abs_path(self) -> pathlib.Path | None: + if self.on_disk: + return self.directory / self._time_iter_file_name.with_suffix(".npz") + + def initialize(self): + super().initialize() + self.outDict = {} + if self.on_disk: + print( + f"'{type(self).__qualname__}' will save your inversion progress as a dictionary to: " + f"'{self.file_abs_path}'" + ) + + def endIter(self): + # regCombo = ["phi_ms", "phi_msx"] + + # if self.simulation[0].mesh.dim >= 2: + # regCombo += ["phi_msy"] + + # if self.simulation[0].mesh.dim == 3: + # regCombo += ["phi_msz"] + + # Initialize the output dict + iterDict = {} + + # Save the data. + iterDict["iter"] = self.opt.iter + iterDict["beta"] = self.invProb.beta + iterDict["phi_d"] = self.invProb.phi_d + iterDict["phi_m"] = self.invProb.phi_m + + # for label, fcts in zip(regCombo, self.reg.objfcts[0].objfcts): + # iterDict[label] = fcts(self.invProb.model) + + iterDict["f"] = self.opt.f + iterDict["m"] = self.invProb.model + iterDict["dpred"] = self.invProb.dpred + + for reg in self.reg.objfcts: + if isinstance(reg, Sparse): + for reg_part, norm in zip(reg.objfcts, reg.norms): + reg_name = f"{type(reg_part).__name__}" + if hasattr(reg_part, "orientation"): + reg_name = reg_part.orientation + " " + reg_name + iterDict[reg_name + ".irls_threshold"] = reg_part.irls_threshold + iterDict[reg_name + ".norm"] = norm + + # Save the file as a npz + if self.on_disk: + self._mkdir_and_check_output_file(should_exist=False) + np.savez(self.file_abs_path, iterDict) + + self.outDict[self.opt.iter] = iterDict + + +class UpdatePreconditioner(InversionDirective): + """ + Create a Jacobi preconditioner for the linear problem + """ + + def __init__(self, update_every_iteration=True, **kwargs): + super().__init__(**kwargs) + self.update_every_iteration = update_every_iteration + + @property + def update_every_iteration(self): + """Whether to update the preconditioner at every iteration. + + Returns + ------- + bool + """ + return self._update_every_iteration + + @update_every_iteration.setter + def update_every_iteration(self, value): + self._update_every_iteration = validate_type( + "update_every_iteration", value, bool + ) + + def initialize(self): + # Create the pre-conditioner + regDiag = np.zeros_like(self.invProb.model) + m = self.invProb.model + + for reg in self.reg.objfcts: + # Check if regularization has a projection + rdg = reg.deriv2(m) + if not isinstance(rdg, Zero): + regDiag += rdg.diagonal() + + JtJdiag = np.zeros_like(self.invProb.model) + for sim, dmisfit in zip(self.simulation, self.dmisfit.objfcts): + if getattr(sim, "getJtJdiag", None) is None: + assert getattr(sim, "getJ", None) is not None, ( + "Simulation does not have a getJ attribute." + + "Cannot form the sensitivity explicitly" + ) + JtJdiag += np.sum(np.power((dmisfit.W * sim.getJ(m)), 2), axis=0) + else: + JtJdiag += sim.getJtJdiag(m, W=dmisfit.W) + + diagA = JtJdiag + self.invProb.beta * regDiag + diagA[diagA != 0] = diagA[diagA != 0] ** -1.0 + PC = sdiag((diagA)) + + self.opt.approxHinv = PC + + def endIter(self): + # Cool the threshold parameter + if self.update_every_iteration is False: + return + + # Create the pre-conditioner + regDiag = np.zeros_like(self.invProb.model) + m = self.invProb.model + + for reg in self.reg.objfcts: + # Check if he has wire + regDiag += reg.deriv2(m).diagonal() + + JtJdiag = np.zeros_like(self.invProb.model) + for sim, dmisfit in zip(self.simulation, self.dmisfit.objfcts): + if getattr(sim, "getJtJdiag", None) is None: + assert getattr(sim, "getJ", None) is not None, ( + "Simulation does not have a getJ attribute." + + "Cannot form the sensitivity explicitly" + ) + JtJdiag += np.sum(np.power((dmisfit.W * sim.getJ(m)), 2), axis=0) + else: + JtJdiag += sim.getJtJdiag(m, W=dmisfit.W) + + diagA = JtJdiag + self.invProb.beta * regDiag + diagA[diagA != 0] = diagA[diagA != 0] ** -1.0 + PC = sdiag((diagA)) + self.opt.approxHinv = PC + + +class Update_Wj(InversionDirective): + """ + Create approx-sensitivity base weighting using the probing method + """ + + def __init__(self, k=None, itr=None, **kwargs): + self.k = k + self.itr = itr + super().__init__(**kwargs) + + @property + def k(self): + """Number of probing cycles for the estimator. + + Returns + ------- + int + """ + return self._k + + @k.setter + def k(self, value): + if value is not None: + value = validate_integer("k", value, min_val=1) + self._k = value + + @property + def itr(self): + """Which iteration to update the sensitivity. + + Will always update if `None`. + + Returns + ------- + int or None + """ + return self._itr + + @itr.setter + def itr(self, value): + if value is not None: + value = validate_integer("itr", value, min_val=1) + self._itr = value + + def endIter(self): + if self.itr is None or self.itr == self.opt.iter: + m = self.invProb.model + if self.k is None: + self.k = int(self.survey.nD / 10) + + def JtJv(v): + Jv = self.simulation.Jvec(m, v) + + return self.simulation.Jtvec(m, Jv) + + JtJdiag = estimate_diagonal(JtJv, len(m), k=self.k) + JtJdiag = JtJdiag / max(JtJdiag) + + self.reg.wght = JtJdiag + + +class UpdateSensitivityWeights(InversionDirective): + r""" + Sensitivity weighting for linear and non-linear least-squares inverse problems. + + This directive computes the root-mean squared sensitivities for the forward + simulation(s) attached to the inverse problem, then truncates and scales the result + to create cell weights which are applied in the regularization. + + .. important:: + + This directive **requires** that the map for the regularization function is + either :class:`simpeg.maps.Wires` or :class:`simpeg.maps.IdentityMap`. In other + words, the sensitivity weighting cannot be applied for parametric inversion. In + addition, the simulation(s) connected to the inverse problem **must** have + a ``getJ`` or ``getJtJdiag`` method. + + .. important:: + + This directive **must** be placed before any directives which update the + preconditioner for the inverse problem (i.e. :class:`UpdatePreconditioner`), and + **must** be before any directives that estimate the starting trade-off parameter + (i.e. :class:`BetaEstimate_ByEig` and :class:`BetaEstimateMaxDerivative`). + + Parameters + ---------- + every_iteration : bool + When ``True``, update sensitivity weighting at every model update; non-linear problems. + When ``False``, create sensitivity weights for starting model only; linear problems. + threshold_value : float + Threshold value for smallest weighting value. + threshold_method : {'amplitude', 'global', 'percentile'} + Threshold method for how `threshold_value` is applied: + + - amplitude: + The smallest root-mean squared sensitivity is a fractional percent of the + largest value; must be between 0 and 1. + - global: + The ``threshold_value`` is added to the cell weights prior to normalization; + must be greater than 0. + - percentile: + The smallest root-mean squared sensitivity is set using percentile + threshold; must be between 0 and 100. + + normalization_method : {'maximum', 'min_value', None} + Normalization method applied to sensitivity weights. + + Options are: + + - maximum: + Sensitivity weights are normalized by the largest value such that the + largest weight is equal to 1. + - minimum: + Sensitivity weights are normalized by the smallest value, after + thresholding, such that the smallest weights are equal to 1. + - ``None``: + Normalization is not applied. + + Notes + ----- + Let :math:`\mathbf{J}` represent the Jacobian. To create sensitivity weights, root-mean squared (RMS) sensitivities + :math:`\mathbf{s}` are computed by summing the squares of the rows of the Jacobian: + + .. math:: + \mathbf{s} = \Bigg [ \sum_i \, \mathbf{J_{i, \centerdot }}^2 \, \Bigg ]^{1/2} + + The dynamic range of RMS sensitivities can span many orders of magnitude. When computing sensitivity + weights, thresholding is generally applied to set a minimum value. + + **Thresholding:** + + If **global** thresholding is applied, we add a constant :math:`\tau` to the RMS sensitivities: + + .. math:: + \mathbf{\tilde{s}} = \mathbf{s} + \tau + + In the case of **percentile** thresholding, we let :math:`s_{\%}` represent a given percentile. + Thresholding to set a minimum value is applied as follows: + + .. math:: + \tilde{s}_j = \begin{cases} + s_j \;\; for \;\; s_j \geq s_{\%} \\ + s_{\%} \;\; for \;\; s_j < s_{\%} + \end{cases} + + If **absolute** thresholding is applied, we define :math:`\eta` as a fractional percent. + In this case, thresholding is applied as follows: + + .. math:: + \tilde{s}_j = \begin{cases} + s_j \;\; for \;\; s_j \geq \eta s_{max} \\ + \eta s_{max} \;\; for \;\; s_j < \eta s_{max} + \end{cases} + """ + + def __init__( + self, + every_iteration=False, + threshold_value=1e-12, + threshold_method="amplitude", + normalization_method="maximum", + **kwargs, + ): + + super().__init__(**kwargs) + + self.every_iteration = every_iteration + self.threshold_value = threshold_value + self.threshold_method = threshold_method + self.normalization_method = normalization_method + + @property + def every_iteration(self): + """Update sensitivity weights when model is updated. + + When ``True``, update sensitivity weighting at every model update; non-linear problems. + When ``False``, create sensitivity weights for starting model only; linear problems. + + Returns + ------- + bool + """ + return self._every_iteration + + @every_iteration.setter + def every_iteration(self, value): + self._every_iteration = validate_type("every_iteration", value, bool) + + @property + def threshold_value(self): + """Threshold value used to set minimum weighting value. + + The way thresholding is applied to the weighting model depends on the + `threshold_method` property. The choices for `threshold_method` are: + + - global: + `threshold_value` is added to the cell weights prior to normalization; must be greater than 0. + - percentile: + `threshold_value` is a percentile cutoff; must be between 0 and 100 + - amplitude: + `threshold_value` is the fractional percent of the largest value; must be between 0 and 1 + + + Returns + ------- + float + """ + return self._threshold_value + + @threshold_value.setter + def threshold_value(self, value): + self._threshold_value = validate_float("threshold_value", value, min_val=0.0) + + @property + def threshold_method(self): + """Threshold method for how `threshold_value` is applied: + + - global: + `threshold_value` is added to the cell weights prior to normalization; must be greater than 0. + - percentile: + the smallest root-mean squared sensitivity is set using percentile threshold; must be between 0 and 100 + - amplitude: + the smallest root-mean squared sensitivity is a fractional percent of the largest value; must be between 0 and 1 + + + Returns + ------- + str + """ + return self._threshold_method + + @threshold_method.setter + def threshold_method(self, value): + self._threshold_method = validate_string( + "threshold_method", value, string_list=["global", "percentile", "amplitude"] + ) + + @property + def normalization_method(self): + """Normalization method applied to sensitivity weights. + + Options are: + + - ``None`` + normalization is not applied + - maximum: + sensitivity weights are normalized by the largest value such that the largest weight is equal to 1. + - minimum: + sensitivity weights are normalized by the smallest value, after thresholding, such that the smallest weights are equal to 1. + + Returns + ------- + None, str + """ + return self._normalization_method + + @normalization_method.setter + def normalization_method(self, value): + if value is None: + self._normalization_method = value + else: + self._normalization_method = validate_string( + "normalization_method", value, string_list=["minimum", "maximum"] + ) + + def initialize(self): + """Compute sensitivity weights upon starting the inversion.""" + for reg in self.reg.objfcts: + if not isinstance(reg.mapping, (IdentityMap, Wires)): + raise TypeError( + f"Mapping for the regularization must be of type {IdentityMap} or {Wires}. " + + f"Input mapping of type {type(reg.mapping)}." + ) + + self.update() + + def endIter(self): + """Execute end of iteration.""" + + if self.every_iteration: + self.update() + + def update(self): + """Update sensitivity weights""" + + jtj_diag = np.zeros_like(self.invProb.model) + m = self.invProb.model + + for sim, dmisfit in zip(self.simulation, self.dmisfit.objfcts): + if getattr(sim, "getJtJdiag", None) is None: + if getattr(sim, "getJ", None) is None: + raise AttributeError( + "Simulation does not have a getJ attribute." + + "Cannot form the sensitivity explicitly" + ) + jtj_diag += mkvc(np.sum((dmisfit.W * sim.getJ(m)) ** 2.0, axis=0)) + else: + jtj_diag += sim.getJtJdiag(m, W=dmisfit.W) + + # Compute and sum root-mean squared sensitivities for all objective functions + wr = np.zeros_like(self.invProb.model) + for reg in self.reg.objfcts: + if isinstance(reg, BaseSimilarityMeasure): + continue + + mesh = reg.regularization_mesh + n_cells = mesh.nC + mapped_jtj_diag = reg.mapping * jtj_diag + # reshape the mapped, so you can divide by volume + # (let's say it was a vector or anisotropic model) + mapped_jtj_diag = mapped_jtj_diag.reshape((n_cells, -1), order="F") + wr_temp = mapped_jtj_diag / reg.regularization_mesh.vol[:, None] ** 2.0 + wr_temp = wr_temp.reshape(-1, order="F") + + wr += reg.mapping.deriv(self.invProb.model).T * wr_temp + + wr **= 0.5 + + # Apply thresholding + if self.threshold_method == "global": + wr += self.threshold_value + elif self.threshold_method == "percentile": + wr = np.clip( + wr, a_min=np.percentile(wr, self.threshold_value), a_max=np.inf + ) + else: + wr = np.clip(wr, a_min=self.threshold_value * wr.max(), a_max=np.inf) + + # Apply normalization + if self.normalization_method == "maximum": + wr /= wr.max() + elif self.normalization_method == "minimum": + wr /= wr.min() + + # Add sensitivity weighting to all model objective functions + for reg in self.reg.objfcts: + if not isinstance(reg, BaseSimilarityMeasure): + sub_regs = getattr(reg, "objfcts", [reg]) + for sub_reg in sub_regs: + sub_reg.set_weights(sensitivity=sub_reg.mapping * wr) + + def validate(self, directiveList): + """Validate directive against directives list. + + The ``UpdateSensitivityWeights`` directive impacts the regularization by applying + cell weights. As a result, its place in the :class:`DirectivesList` must be + before any directives which update the preconditioner for the inverse problem + (i.e. :class:`UpdatePreconditioner`), and must be before any directives that + estimate the starting trade-off parameter (i.e. :class:`EstimateBeta_ByEig` + and :class:`EstimateBetaMaxDerivative`). + + + Returns + ------- + bool + Returns ``True`` if validation passes. Otherwise, an error is thrown. + """ + # check if a beta estimator is in the list after setting the weights + dList = directiveList.dList + self_ind = dList.index(self) + + beta_estimator_ind = [isinstance(d, BaseBetaEstimator) for d in dList] + lin_precond_ind = [isinstance(d, UpdatePreconditioner) for d in dList] + + if any(beta_estimator_ind): + assert beta_estimator_ind.index(True) > self_ind, ( + "The directive for setting intial beta must be after UpdateSensitivityWeights " + "in the directiveList" + ) + + if any(lin_precond_ind): + assert lin_precond_ind.index(True) > self_ind, ( + "The directive 'UpdatePreconditioner' must be after UpdateSensitivityWeights " + "in the directiveList" + ) + + return True + + +class ProjectSphericalBounds(InversionDirective): + r""" + Trick for spherical coordinate system. + Project :math:`\theta` and :math:`\phi` angles back to :math:`[-\pi,\pi]` + using back and forth conversion. + spherical->cartesian->spherical + """ + + def initialize(self): + x = self.invProb.model + # Convert to cartesian than back to avoid over rotation + nC = int(len(x) / 3) + + xyz = spherical2cartesian(x.reshape((nC, 3), order="F")) + m = cartesian2spherical(xyz.reshape((nC, 3), order="F")) + + self.invProb.model = m + + for sim in self.simulation: + sim.model = m + + self.opt.xc = m + + def endIter(self): + x = self.invProb.model + nC = int(len(x) / 3) + + # Convert to cartesian than back to avoid over rotation + xyz = spherical2cartesian(x.reshape((nC, 3), order="F")) + m = cartesian2spherical(xyz.reshape((nC, 3), order="F")) + + self.invProb.model = m + + phi_m_last = [] + for reg in self.reg.objfcts: + reg.model = self.invProb.model + phi_m_last += [reg(self.invProb.model)] + + self.invProb.phi_m_last = phi_m_last + + for sim in self.simulation: + sim.model = m + + self.opt.xc = m diff --git a/simpeg/directives/_pgi_directives.py b/simpeg/directives/_pgi_directives.py new file mode 100644 index 0000000000..60f4488b90 --- /dev/null +++ b/simpeg/directives/_pgi_directives.py @@ -0,0 +1,474 @@ +############################################################################### +# # +# Directives for PGI: Petrophysically guided Regularization # +# # +############################################################################### + +import copy + +import numpy as np + +from ..directives import InversionDirective, MultiTargetMisfits +from ..regularization import ( + PGI, + PGIsmallness, + SmoothnessFirstOrder, + SparseSmoothness, +) +from ..utils import ( + GaussianMixtureWithNonlinearRelationships, + GaussianMixtureWithNonlinearRelationshipsWithPrior, + GaussianMixtureWithPrior, + WeightedGaussianMixture, + mkvc, +) + + +class PGI_UpdateParameters(InversionDirective): + """ + This directive is to be used with regularization from regularization.pgi. + It updates: + - the reference model and weights in the smallness (L2-approximation of PGI) + - the GMM as a MAP estimate between the prior and the current model + For more details, please consult: + - https://doi.org/10.1093/gji/ggz389 + """ + + verbose = False # print info. about the GMM at each iteration + update_rate = 1 # updates at each `update_rate` iterations + update_gmm = False # update the GMM + zeta = ( + 1e10 # confidence in the prior proportions; default: high value, keep GMM fixed + ) + nu = ( + 1e10 # confidence in the prior covariances; default: high value, keep GMM fixed + ) + kappa = 1e10 # confidence in the prior means;default: high value, keep GMM fixed + update_covariances = ( + True # Average the covariances, If false: average the precisions + ) + fixed_membership = None # keep the membership of specific cells fixed + keep_ref_fixed_in_Smooth = True # keep mref fixed in the Smoothness + + def initialize(self): + pgi_reg = self.reg.get_functions_of_type(PGIsmallness) + if len(pgi_reg) != 1: + raise UserWarning( + "'PGI_UpdateParameters' requires one 'PGIsmallness' regularization " + "in the objective function." + ) + self.pgi_reg = pgi_reg[0] + + def endIter(self): + if self.opt.iter > 0 and self.opt.iter % self.update_rate == 0: + m = self.invProb.model + modellist = self.pgi_reg.wiresmap * m + model = np.c_[[a * b for a, b in zip(self.pgi_reg.maplist, modellist)]].T + + if self.update_gmm and isinstance( + self.pgi_reg.gmmref, GaussianMixtureWithNonlinearRelationships + ): + clfupdate = GaussianMixtureWithNonlinearRelationshipsWithPrior( + gmmref=self.pgi_reg.gmmref, + zeta=self.zeta, + kappa=self.kappa, + nu=self.nu, + verbose=self.verbose, + prior_type="semi", + update_covariances=self.update_covariances, + max_iter=self.pgi_reg.gmm.max_iter, + n_init=self.pgi_reg.gmm.n_init, + reg_covar=self.pgi_reg.gmm.reg_covar, + weights_init=self.pgi_reg.gmm.weights_, + means_init=self.pgi_reg.gmm.means_, + precisions_init=self.pgi_reg.gmm.precisions_, + random_state=self.pgi_reg.gmm.random_state, + tol=self.pgi_reg.gmm.tol, + verbose_interval=self.pgi_reg.gmm.verbose_interval, + warm_start=self.pgi_reg.gmm.warm_start, + fixed_membership=self.fixed_membership, + ) + clfupdate = clfupdate.fit(model) + + elif self.update_gmm and isinstance( + self.pgi_reg.gmmref, WeightedGaussianMixture + ): + clfupdate = GaussianMixtureWithPrior( + gmmref=self.pgi_reg.gmmref, + zeta=self.zeta, + kappa=self.kappa, + nu=self.nu, + verbose=self.verbose, + prior_type="semi", + update_covariances=self.update_covariances, + max_iter=self.pgi_reg.gmm.max_iter, + n_init=self.pgi_reg.gmm.n_init, + reg_covar=self.pgi_reg.gmm.reg_covar, + weights_init=self.pgi_reg.gmm.weights_, + means_init=self.pgi_reg.gmm.means_, + precisions_init=self.pgi_reg.gmm.precisions_, + random_state=self.pgi_reg.gmm.random_state, + tol=self.pgi_reg.gmm.tol, + verbose_interval=self.pgi_reg.gmm.verbose_interval, + warm_start=self.pgi_reg.gmm.warm_start, + fixed_membership=self.fixed_membership, + ) + clfupdate = clfupdate.fit(model) + + else: + clfupdate = copy.deepcopy(self.pgi_reg.gmmref) + + self.pgi_reg.gmm = clfupdate + membership = self.pgi_reg.gmm.predict(model) + + if self.fixed_membership is not None: + membership[self.fixed_membership[:, 0]] = self.fixed_membership[:, 1] + + mref = mkvc(self.pgi_reg.gmm.means_[membership]) + self.pgi_reg.reference_model = mref + if getattr(self.fixed_membership, "shape", [0, 0])[0] < len(membership): + self.pgi_reg._r_second_deriv = None + + +class PGI_BetaAlphaSchedule(InversionDirective): + """ + This directive is to be used with regularizations from regularization.pgi. + It implements the strategy described in https://doi.org/10.1093/gji/ggz389 + for iteratively updating beta and alpha_s for fitting the + geophysical and smallness targets. + """ + + verbose = False # print information (progress, updates made) + tolerance = 0.0 # tolerance on the geophysical target misfit for cooling + progress = 0.1 # minimum percentage progress (default 10%) before cooling beta + coolingFactor = 2.0 # when cooled, beta is divided by it + warmingFactor = 1.0 # when warmed, alpha_s is multiplied by the ratio of the + # geophysical target with their current misfit, times this factor + mode = 1 # mode 1: start with nothing fitted. Mode 2: warmstart with fitted geophysical data + alphasmax = 1e10 # max alpha_s + betamin = 1e-10 # minimum beta + update_rate = 1 # update every `update_rate` iterations + pgi_reg = None + ratio_in_cooling = ( + False # add the ratio of geophysical misfit with their target in cooling + ) + + def initialize(self): + """Initialize the directive.""" + self.update_previous_score() + self.update_previous_dmlist() + + def endIter(self): + """Run after the end of each iteration in the inversion.""" + # Get some variables from the MultiTargetMisfits directive + data_misfits_achieved = self.multi_target_misfits_directive.DM + data_misfits_target = self.multi_target_misfits_directive.DMtarget + dmlist = self.multi_target_misfits_directive.dmlist + targetlist = self.multi_target_misfits_directive.targetlist + + # Change mode if data misfit targets have been achieved + if data_misfits_achieved: + self.mode = 2 + + # Don't cool beta of warm alpha if we are in the first iteration or if + # the current iteration doesn't match the update rate + if self.opt.iter == 0 or self.opt.iter % self.update_rate != 0: + self.update_previous_score() + self.update_previous_dmlist() + return None + + if self.verbose: + targets = np.round( + np.maximum( + (1.0 - self.progress) * self.previous_dmlist, + (1.0 + self.tolerance) * data_misfits_target, + ), + decimals=1, + ) + dmlist_rounded = np.round(dmlist, decimals=1) + print( + f"Beta cooling evaluation: progress: {dmlist_rounded}; " + f"minimum progress targets: {targets}" + ) + + # Decide if we should cool beta + threshold = np.maximum( + (1.0 - self.progress) * self.previous_dmlist[~targetlist], + data_misfits_target[~targetlist], + ) + if ( + (dmlist[~targetlist] > threshold).all() + and not data_misfits_achieved + and self.mode == 1 + and self.invProb.beta > self.betamin + ): + self.cool_beta() + if self.verbose: + print("Decreasing beta to counter data misfit decrase plateau.") + + # Decide if we should warm alpha instead + elif ( + data_misfits_achieved + and self.mode == 2 + and np.all(self.pgi_regularization.alpha_pgi < self.alphasmax) + ): + self.warm_alpha() + if self.verbose: + print( + "Warming alpha_pgi to favor clustering: ", + self.pgi_regularization.alpha_pgi, + ) + + # Decide if we should cool beta (to counter data misfit increase) + elif ( + np.any(dmlist > (1.0 + self.tolerance) * data_misfits_target) + and self.mode == 2 + and self.invProb.beta > self.betamin + ): + self.cool_beta() + if self.verbose: + print("Decreasing beta to counter data misfit increase.") + + # Update previous score and dmlist + self.update_previous_score() + self.update_previous_dmlist() + + def cool_beta(self): + """Cool beta according to schedule.""" + data_misfits_target = self.multi_target_misfits_directive.DMtarget + dmlist = self.multi_target_misfits_directive.dmlist + ratio = 1.0 + indx = dmlist > (1.0 + self.tolerance) * data_misfits_target + if np.any(indx) and self.ratio_in_cooling: + ratio = np.median([dmlist[indx] / data_misfits_target[indx]]) + self.invProb.beta /= self.coolingFactor * ratio + + def warm_alpha(self): + """Warm alpha according to schedule.""" + data_misfits_target = self.multi_target_misfits_directive.DMtarget + dmlist = self.multi_target_misfits_directive.dmlist + ratio = np.median(data_misfits_target / dmlist) + self.pgi_regularization.alpha_pgi *= self.warmingFactor * ratio + + def update_previous_score(self): + """ + Update the value of the ``previous_score`` attribute. + + Update it with the current value of the petrophysical misfit, obtained + from the :meth:`MultiTargetMisfit.phims()` method. + """ + self.previous_score = copy.deepcopy(self.multi_target_misfits_directive.phims()) + + def update_previous_dmlist(self): + """ + Update the value of the ``previous_dmlist`` attribute. + + Update it with the current value of the data misfits, obtained + from the :meth:`MultiTargetMisfit.dmlist` attribute. + """ + self.previous_dmlist = copy.deepcopy(self.multi_target_misfits_directive.dmlist) + + @property + def directives(self): + """List of all the directives in the :class:`simpeg.inverison.BaseInversion``.""" + return self.inversion.directiveList.dList + + @property + def multi_target_misfits_directive(self): + """``MultiTargetMisfit`` directive in the :class:`simpeg.inverison.BaseInversion``.""" + if not hasattr(self, "_mtm_directive"): + # Obtain multi target misfits directive from the directive list + multi_target_misfits_directive = [ + directive + for directive in self.directives + if isinstance(directive, MultiTargetMisfits) + ] + if not multi_target_misfits_directive: + raise UserWarning( + "No MultiTargetMisfits directive found in the current inversion. " + "A MultiTargetMisfits directive is needed by the " + "PGI_BetaAlphaSchedule directive." + ) + (self._mtm_directive,) = multi_target_misfits_directive + return self._mtm_directive + + @property + def pgi_update_params_directive(self): + """``PGI_UpdateParam``s directive in the :class:`simpeg.inverison.BaseInversion``.""" + if not hasattr(self, "_pgi_update_params"): + # Obtain PGI_UpdateParams directive from the directive list + pgi_update_params_directive = [ + directive + for directive in self.directives + if isinstance(directive, PGI_UpdateParameters) + ] + if pgi_update_params_directive: + (self._pgi_update_params,) = pgi_update_params_directive + else: + self._pgi_update_params = None + return self._pgi_update_params + + @property + def pgi_regularization(self): + """PGI regularization in the :class:`simpeg.inverse_problem.BaseInvProblem``.""" + if not hasattr(self, "_pgi_regularization"): + pgi_regularization = self.reg.get_functions_of_type(PGI) + if len(pgi_regularization) != 1: + raise UserWarning( + "'PGI_UpdateParameters' requires one 'PGI' regularization " + "in the objective function." + ) + self._pgi_regularization = pgi_regularization[0] + return self._pgi_regularization + + +class PGI_AddMrefInSmooth(InversionDirective): + """ + This directive is to be used with regularizations from regularization.pgi. + It implements the strategy described in https://doi.org/10.1093/gji/ggz389 + for including the learned reference model, once stable, in the smoothness terms. + """ + + # Chi factor for Data Misfit + chifact = 1.0 + tolerance_phid = 0.0 + phi_d_target = None + wait_till_stable = True + tolerance = 0.0 + verbose = False + + def initialize(self): + targetclass = np.r_[ + [ + isinstance(dirpart, MultiTargetMisfits) + for dirpart in self.inversion.directiveList.dList + ] + ] + if ~np.any(targetclass): + self.DMtarget = None + else: + self.targetclass = np.where(targetclass)[0][-1] + self._DMtarget = self.inversion.directiveList.dList[ + self.targetclass + ].DMtarget + + self.pgi_updategmm_class = np.r_[ + [ + isinstance(dirpart, PGI_UpdateParameters) + for dirpart in self.inversion.directiveList.dList + ] + ] + + if getattr(self.reg.objfcts[0], "objfcts", None) is not None: + # Find the petrosmallness terms in a two-levels combo-regularization. + petrosmallness = np.where( + np.r_[[isinstance(regpart, PGI) for regpart in self.reg.objfcts]] + )[0][0] + self.petrosmallness = petrosmallness + + # Find the smoothness terms in a two-levels combo-regularization. + Smooth = [] + for i, regobjcts in enumerate(self.reg.objfcts): + for j, regpart in enumerate(regobjcts.objfcts): + Smooth += [ + [ + i, + j, + isinstance( + regpart, (SmoothnessFirstOrder, SparseSmoothness) + ), + ] + ] + self.Smooth = np.r_[Smooth] + + self.nbr = np.sum( + [len(self.reg.objfcts[i].objfcts) for i in range(len(self.reg.objfcts))] + ) + self._regmode = 1 + self.pgi_reg = self.reg.objfcts[self.petrosmallness] + + else: + self._regmode = 2 + self.pgi_reg = self.reg + self.nbr = len(self.reg.objfcts) + self.Smooth = np.r_[ + [ + isinstance(regpart, (SmoothnessFirstOrder, SparseSmoothness)) + for regpart in self.reg.objfcts + ] + ] + self._regmode = 2 + + if ~np.any(self.pgi_updategmm_class): + self.previous_membership = self.pgi_reg.membership(self.invProb.model) + else: + self.previous_membership = self.pgi_reg.compute_quasi_geology_model() + + @property + def DMtarget(self): + if getattr(self, "_DMtarget", None) is None: + self.phi_d_target = self.invProb.dmisfit.survey.nD + self._DMtarget = self.chifact * self.phi_d_target + return self._DMtarget + + @DMtarget.setter + def DMtarget(self, val): + self._DMtarget = val + + def endIter(self): + self.DM = self.inversion.directiveList.dList[self.targetclass].DM + self.dmlist = self.inversion.directiveList.dList[self.targetclass].dmlist + + if ~np.any(self.pgi_updategmm_class): + self.membership = self.pgi_reg.membership(self.invProb.model) + else: + self.membership = self.pgi_reg.compute_quasi_geology_model() + + same_mref = np.all(self.membership == self.previous_membership) + percent_diff = ( + len(self.membership) + - np.count_nonzero(self.previous_membership == self.membership) + ) / len(self.membership) + if self.verbose: + print( + "mref changed in ", + len(self.membership) + - np.count_nonzero(self.previous_membership == self.membership), + " places", + ) + if ( + self.DM or np.all(self.dmlist < (1 + self.tolerance_phid) * self.DMtarget) + ) and ( + same_mref or not self.wait_till_stable or percent_diff <= self.tolerance + ): + self.reg.reference_model_in_smooth = True + self.pgi_reg.reference_model_in_smooth = True + + if self._regmode == 2: + for i in range(self.nbr): + if self.Smooth[i]: + self.reg.objfcts[i].reference_model = mkvc( + self.pgi_reg.gmm.means_[self.membership] + ) + if self.verbose: + print( + "Add mref to Smoothness. Changes in mref happened in {} % of the cells".format( + percent_diff + ) + ) + + elif self._regmode == 1: + for i in range(self.nbr): + if self.Smooth[i, 2]: + idx = self.Smooth[i, :2] + self.reg.objfcts[idx[0]].objfcts[idx[1]].reference_model = mkvc( + self.pgi_reg.gmm.means_[self.membership] + ) + if self.verbose: + print( + "Add mref to Smoothness. Changes in mref happened in {} % of the cells".format( + percent_diff + ) + ) + + self.previous_membership = copy.deepcopy(self.membership) diff --git a/simpeg/directives/_regularization.py b/simpeg/directives/_regularization.py new file mode 100644 index 0000000000..92f9dac962 --- /dev/null +++ b/simpeg/directives/_regularization.py @@ -0,0 +1,500 @@ +from __future__ import annotations + +import warnings + +import numpy as np +from dataclasses import dataclass + +from ..maps import Projection +from ._directives import InversionDirective, UpdatePreconditioner, BetaSchedule +from ..regularization import ( + Sparse, + BaseSparse, + SmoothnessFirstOrder, + WeightedLeastSquares, +) +from ..utils import validate_integer, validate_float, deprecate_class + + +@dataclass +class IRLSMetrics: + """ + Data class to store metrics used by the IRLS algorithm. + + Parameters + ---------- + input_norms : list of floats or None + List of norms temporarily stored during the initialization. + irls_iteration_count : int + Number of IRLS iterations. + start_irls_iter : int or None + Iteration number when the IRLS process started. + f_old : float + Previous value of the regularization function. + """ + + input_norms: list[float] | None = None + irls_iteration_count: int = 0 + start_irls_iter: int | None = None + f_old: float = 0.0 + + +class UpdateIRLS(InversionDirective): + """ + Directive to control the IRLS iterations for :class:`~simpeg.regularization.Sparse`. + + Parameters + ---------- + cooling_rate: int + Number of iterations to cool beta. + cooling_factor: float + Factor to cool beta. + chifact_start: float + Starting chi factor for the IRLS iterations. + chifact_target: float + Target chi factor for the IRLS iterations. + irls_cooling_factor: float + Factor to cool the IRLS threshold epsilon. + f_min_change: float + Minimum change in the regularization function to continue the IRLS iterations. + max_irls_iterations: int + Maximum number of IRLS iterations. + misfit_tolerance: float + Tolerance for the target misfit. + percentile: float + Percentile of the function values used to determine the initial IRLS threshold. + verbose: bool + Print information to the screen. + """ + + def __init__( + self, + cooling_rate: int = 1, + cooling_factor: float = 2.0, + chifact_start: float = 1.0, + chifact_target: float = 1.0, + irls_cooling_factor: float = 1.2, + f_min_change: float = 1e-2, + max_irls_iterations: int = 20, + misfit_tolerance: float = 1e-1, + percentile: float = 100.0, + verbose: bool = True, + **kwargs, + ): + self._metrics: IRLSMetrics | None = None + self.cooling_rate = cooling_rate + self.cooling_factor = cooling_factor + self.chifact_start: float = chifact_start + self.chifact_target: float = chifact_target + self.irls_cooling_factor: float = irls_cooling_factor + self.f_min_change: float = f_min_change + self.max_irls_iterations: int = max_irls_iterations + self.misfit_tolerance: float = misfit_tolerance + self.percentile: float = percentile + + super().__init__( + verbose=verbose, + **kwargs, + ) + + @property + def metrics(self) -> IRLSMetrics: + """Various metrics used by the IRLS algorithm.""" + if self._metrics is None: + self._metrics = IRLSMetrics() + return self._metrics + + @property + def max_irls_iterations(self) -> int: + """Maximum irls iterations.""" + return self._max_irls_iterations + + @max_irls_iterations.setter + def max_irls_iterations(self, value): + self._max_irls_iterations = validate_integer( + "max_irls_iterations", value, min_val=0 + ) + + @property + def misfit_tolerance(self) -> float: + """Tolerance on deviation from the target chi factor, as a fractional percent.""" + return self._misfit_tolerance + + @misfit_tolerance.setter + def misfit_tolerance(self, value): + self._misfit_tolerance = validate_float("misfit_tolerance", value, min_val=0) + + @property + def percentile(self) -> float: + """Tolerance on deviation from the target chi factor, as a fractional percent.""" + return self._percentile + + @percentile.setter + def percentile(self, value): + self._percentile = validate_float( + "percentile", value, min_val=0.0, max_val=100.0 + ) + + @property + def chifact_start(self) -> float: + """Target chi factor to start the IRLS process.""" + return self._chifact_start + + @chifact_start.setter + def chifact_start(self, value): + self._chifact_start = validate_float( + "chifact_start", value, min_val=0, inclusive_min=False + ) + + @property + def chifact_target(self) -> float: + """Targer chi factor to maintain during the IRLS process.""" + return self._chifact_target + + @chifact_target.setter + def chifact_target(self, value): + self._chifact_target = validate_float( + "chifact_target", value, min_val=0, inclusive_min=False + ) + + @property + def cooling_factor(self): + """Beta is divided by this value every :attr:`cooling_rate` iterations. + + Returns + ------- + float + """ + return self._cooling_factor + + @cooling_factor.setter + def cooling_factor(self, value): + self._cooling_factor = validate_float( + "cooling_factor", value, min_val=0.0, inclusive_min=False + ) + + @property + def cooling_rate(self): + """Cool beta after this number of iterations. + + Returns + ------- + int + """ + return self._cooling_rate + + @cooling_rate.setter + def cooling_rate(self, value): + self._cooling_rate = validate_integer("cooling_rate", value, min_val=1) + + @property + def irls_cooling_factor(self) -> float: + """IRLS threshold parameter (epsilon) is divided by this value every iteration.""" + return self._irls_cooling_factor + + @irls_cooling_factor.setter + def irls_cooling_factor(self, value): + self._irls_cooling_factor = validate_float( + "irls_cooling_factor", value, min_val=0.0, inclusive_min=False + ) + + @property + def f_min_change(self) -> float: + """Target chi factor to start the IRLS process.""" + return self._f_min_change + + @f_min_change.setter + def f_min_change(self, value): + self._f_min_change = validate_float( + "f_min_change", value, min_val=0, inclusive_min=False + ) + + def misfit_from_chi_factor(self, chi_factor: float) -> float: + """ + Compute the target misfit from the chi factor. + + Parameters + ---------- + chi_factor : float + Chi factor to compute the target misfit from. + """ + value = 0 + + for survey in self.survey: + value += survey.nD * chi_factor + + return value + + def adjust_cooling_schedule(self): + """ + Adjust the cooling schedule based on the misfit. + """ + if self.metrics.start_irls_iter is not None: + ratio = self.invProb.phi_d / self.misfit_from_chi_factor( + self.chifact_target + ) + if np.abs(1.0 - ratio) > self.misfit_tolerance: + + if ratio > 1: + update_ratio = 1 / np.mean([0.75, 1 / ratio]) + else: + update_ratio = 1 / np.mean([2.0, 1 / ratio]) + + self.cooling_factor = update_ratio + else: + self.cooling_factor = 1.0 + + def initialize(self): + """ + Initialize the IRLS iterations with l2-norm regularization (mode:1). + """ + + input_norms = [] + for reg in self.reg.objfcts: + if not isinstance(reg, Sparse): + input_norms += [None] + else: + input_norms += [reg.norms] + reg.norms = [2.0 for _ in reg.objfcts] + + self._metrics = IRLSMetrics(input_norms=input_norms) + + def endIter(self): + """ + Check on progress of the inversion and start/update the IRLS process. + """ + # Update the cooling factor (only after IRLS has started) + self.adjust_cooling_schedule() + + # After reaching target misfit with l2-norm, switch to IRLS (mode:2) + if ( + self.metrics.start_irls_iter is None + and self.invProb.phi_d < self.misfit_from_chi_factor(self.chifact_start) + ): + self.start_irls() + + # Perform IRLS (only after `self.cooling_rate` iterations) + if ( + self.metrics.start_irls_iter is not None + and (self.opt.iter - self.metrics.start_irls_iter) % self.cooling_rate == 0 + ): + if self.stopping_criteria(): + self.opt.stopNextIteration = True + return + else: + self.opt.stopNextIteration = False + + # Cool irls thresholds + for reg in self.reg.objfcts: + if not isinstance(reg, Sparse): + continue + + for obj in reg.objfcts: + obj.irls_threshold /= self.irls_cooling_factor + + self.metrics.irls_iteration_count += 1 + + # Reset the regularization matrices so that it is + # recalculated for current model. Do it to all levels of comboObj + for reg in self.reg.objfcts: + if not isinstance(reg, Sparse): + continue + + reg.update_weights(reg.model) + + self.invProb.phi_m_last = self.reg(self.invProb.model) + + # Apply beta cooling schedule mechanism + if self.opt.iter > 0 and self.opt.iter % self.cooling_rate == 0: + self.invProb.beta /= self.cooling_factor + + def start_irls(self): + """ + Start the IRLS iterations by computing the initial threshold values. + """ + if self.verbose: + print( + "Reached starting chifact with l2-norm regularization:" + + " Start IRLS steps..." + ) + + self.metrics.start_irls_iter = getattr(self.opt, "iter", 0) + self.invProb.phi_m_last = self.reg(self.invProb.model) + + # Either use the supplied irls_threshold, or fix base on distribution of + # model values + for reg, norms in zip(self.reg.objfcts, self.metrics.input_norms): + if not isinstance(reg, Sparse): + continue + + for obj in reg.objfcts: + threshold = np.percentile( + np.abs(obj.mapping * obj._delta_m(self.invProb.model)), + self.percentile, + ) + if isinstance(obj, SmoothnessFirstOrder): + threshold /= reg.regularization_mesh.base_length + + obj.irls_threshold = threshold + + reg.norms = norms + + if self.verbose: + print("irls_threshold " + str(reg.objfcts[0].irls_threshold)) + + # Save l2-model + self.invProb.l2model = self.invProb.model.copy() + + self.cooling_factor = 1.0 + + def validate(self, directiveList=None): + directive_list = directiveList.dList + self_ind = directive_list.index(self) + lin_precond_ind = [isinstance(d, UpdatePreconditioner) for d in directive_list] + + if any(lin_precond_ind): + if lin_precond_ind.index(True) < self_ind: + raise AssertionError( + "The directive 'UpdatePreconditioner' must be after UpdateIRLS " + "in the directiveList" + ) + else: + warnings.warn( + "Without a Linear preconditioner, convergence may be slow. " + "Consider adding `directives.UpdatePreconditioner` to your " + "directives list", + stacklevel=2, + ) + + beta_schedule = [ + d for d in directive_list if isinstance(d, BetaSchedule) and d is not self + ] + + if beta_schedule: + raise AssertionError( + "Beta scheduling is handled by the `UpdateIRLS` directive." + "Remove the redundant `BetaSchedule` from your list of directives.", + ) + + spherical_scale = [isinstance(d, SphericalUnitsWeights) for d in directive_list] + if any(spherical_scale): + assert spherical_scale.index(True) < self_ind, ( + "The directive 'SphericalUnitsWeights' must be before UpdateIRLS " + "in the directiveList" + ) + + return True + + def stopping_criteria(self): + """ + Check for stopping criteria of max_irls_iteration or minimum change. + """ + phim_new = 0 + for reg in self.reg.objfcts: + if isinstance(reg, (Sparse, BaseSparse)): + reg.model = self.invProb.model + phim_new += reg(reg.model) + + # Check for maximum number of IRLS cycles + if self.metrics.irls_iteration_count == self.max_irls_iterations: + if self.verbose: + print( + "Reach maximum number of IRLS cycles:" + + f" {self.max_irls_iterations:d}" + ) + return True + + # Check if the function has changed enough + f_change = np.abs(self.metrics.f_old - phim_new) / (self.metrics.f_old + 1e-12) + + if ( + f_change < self.f_min_change + and self.metrics.irls_iteration_count > 1 + and np.abs( + 1.0 + - self.invProb.phi_d / self.misfit_from_chi_factor(self.chifact_target) + ) + < self.misfit_tolerance + ): + if self.verbose: + print("Minimum decrease in regularization. End of IRLS") + return True + + self.metrics.f_old = phim_new + + return False + + +class SphericalUnitsWeights(InversionDirective): + """ + Directive to update the regularization weights to account for spherical + parameters in radian and SI. + + The scaling applied to the regularization weights is based on the ratio + between the maximum value of the model and the maximum value of angles (pi). + + Parameters + ---------- + amplitude: Projection + Map to the model parameters for the amplitude of the vector + angles: list[WeightedLeastSquares] + List of WeightedLeastSquares for the angles. + verbose: bool + Print information to the screen. + """ + + def __init__( + self, + amplitude: Projection, + angles: list[WeightedLeastSquares], + verbose: bool = True, + **kwargs, + ): + + if not isinstance(amplitude, Projection): + raise TypeError( + "Attribute 'amplitude' must be of type " "'wires.Projection'" + ) + + self._amplitude = amplitude + + if not isinstance(angles, (list, tuple)) or not all( + [isinstance(fun, WeightedLeastSquares) for fun in angles] + ): + raise TypeError( + "Attribute 'angles' must be a list of " + "'regularization.WeightedLeastSquares'." + ) + + self._angles = angles + + super().__init__( + verbose=verbose, + **kwargs, + ) + + def initialize(self): + self.update_scaling() + + def endIter(self): + self.update_scaling() + + def update_scaling(self): + """ + Add an 'angle_scale' to the list of weights on the angle regularization for the + different block of models to account for units of radian and SI. + """ + amplitude = self._amplitude * self.invProb.model + max_p = max(amplitude) + + for reg in self._angles: + for obj in reg.objfcts: + if obj.units != "radian": + continue + + obj.set_weights(angle_scale=np.ones_like(amplitude) * max_p / np.pi) + + +@deprecate_class(removal_version="0.24.0", error=True) +class Update_IRLS(UpdateIRLS): + pass diff --git a/simpeg/directives/_sim_directives.py b/simpeg/directives/_sim_directives.py new file mode 100644 index 0000000000..7a5f13a5ef --- /dev/null +++ b/simpeg/directives/_sim_directives.py @@ -0,0 +1,379 @@ +import numpy as np +from ..regularization import BaseSimilarityMeasure +from ..utils import eigenvalue_by_power_iteration +from ..optimization import IterationPrinters, StoppingCriteria +from ._directives import InversionDirective, SaveOutputEveryIteration + + +############################################################################### +# # +# Directives of joint inversion # +# # +############################################################################### +class SimilarityMeasureInversionPrinters: + beta = { + "title": "betas", + "value": lambda M: [f"{elem:1.2e}" for elem in M.parent.betas], + "width": 26, + "format": lambda v: f"{v!s}", + } + lambd = { + "title": "lambda", + "value": lambda M: M.parent.lambd, + "width": 10, + "format": lambda v: f"{v:1.2e}", + } + phi_d = { + "title": "phi_d", + "value": lambda M: [f"{elem:1.2e}" for elem in M.parent.dmisfit._last_obj_vals], + "width": 26, + "format": lambda v: f"{v!s}", + } + phi_m = { + "title": "phi_m", + "value": lambda M: [ + f"{elem:1.2e}" for elem in M.parent.reg._last_obj_vals[:-1] + ], + "width": 26, + "format": lambda v: f"{v!s}", + } + phi_sim = { + "title": "phi_sim", + "value": lambda M: M.parent.reg._last_obj_vals[-1], + "width": 10, + "format": lambda v: f"{v:1.2e}", + } + + +class SimilarityMeasureInversionDirective(InversionDirective): + """ + Directive for two model similiraty measure joint inversions. Sets Printers and + StoppingCriteria. + + Notes + ----- + Methods assume we are working with two models, and a single similarity measure. + Also, the SimilarityMeasure objective function must be the last regularization. + """ + + printers = [ + IterationPrinters.iteration, + SimilarityMeasureInversionPrinters.beta, + SimilarityMeasureInversionPrinters.lambd, + IterationPrinters.f, + SimilarityMeasureInversionPrinters.phi_d, + SimilarityMeasureInversionPrinters.phi_m, + SimilarityMeasureInversionPrinters.phi_sim, + IterationPrinters.iterationCG, + IterationPrinters.iteration_CG_rel_residual, + IterationPrinters.iteration_CG_abs_residual, + ] + + def initialize(self): + if not isinstance(self.reg.objfcts[-1], BaseSimilarityMeasure): + raise TypeError( + f"The last regularization function must be an instance of " + f"BaseSimilarityMeasure, got {type(self.reg.objfcts[-1])}." + ) + + # define relevant attributes + self.betas = self.reg.multipliers[:-1] + self.lambd = self.reg.multipliers[-1] + self.phi_d_list = [] + self.phi_m_list = [] + self.phi_sim = 0.0 + + # pass attributes to invProb + self.invProb.betas = self.betas + self.invProb.num_models = len(self.betas) + self.invProb.lambd = self.lambd + self.invProb.phi_d_list = self.phi_d_list + self.invProb.phi_m_list = self.phi_m_list + self.invProb.phi_sim = self.phi_sim + + self.opt.printers = self.printers + self.opt.stoppers = [StoppingCriteria.iteration] + + def validate(self, directiveList): + # check that this directive is first in the DirectiveList + dList = directiveList.dList + self_ind = dList.index(self) + if self_ind != 0: + raise IndexError( + "The CrossGradientInversionDirective must be first in directive list." + ) + return True + + def endIter(self): + # compute attribute values + phi_d = self.dmisfit._last_obj_vals + + phi_m = self.reg._last_obj_vals + + # pass attributes values to invProb + self.invProb.phi_d_list = phi_d + self.invProb.phi_m_list = phi_m[:-1] + self.invProb.phi_sim = phi_m[-1] + self.invProb.betas = self.reg.multipliers[:-1] + # Assume last reg.objfct is the coupling + self.invProb.lambd = self.reg.multipliers[-1] + + +class SimilarityMeasureSaveOutputEveryIteration(SaveOutputEveryIteration): + """ + SaveOutputEveryIteration for Joint Inversions. + Saves information on the tradeoff parameters, data misfits, regularizations, + coupling term, number of CG iterations, and value of cost function. + """ + + @property + def _header(self): + return " # betas lambda joint_phi_d joint_phi_m phi_sim iterCG phi " + + def _initialize_lists(self): + # Create a list of each + self.betas = [] + self.lambd = [] + self.phi_d = [] + self.phi_m = [] + self.phi = [] + self.phi_sim = [] + + def endIter(self): + self.betas.append(self.invProb.betas) + self.phi_d.append(self.invProb.phi_d_list) + self.phi_m.append(self.invProb.phi_m_list) + self.lambd.append(self.invProb.lambd) + self.phi_sim.append(self.invProb.phi_sim) + self.phi.append(self.opt.f) + + if self.on_disk: + self._mkdir_and_check_output_file(should_exist=True) + with open(self.file_abs_path, "a") as f: + f.write( + " {0:2d} {1} {2:.2e} {3} {4} {5:1.4e} {6:d} {7:1.4e}\n".format( + self.opt.iter, + [f"{el:.2e}" for el in self.betas[-1]], + self.lambd[-1], + [f"{el:.3e}" for el in self.phi_d[-1]], + [f"{el:.3e}" for el in self.phi_m[-1]], + self.phi_sim[-1], + self.opt.cg_count, + self.phi[-1], + ) + ) + + def load_results(self, file_name=None): + if file_name is None: + if not self.on_disk: + raise TypeError( + f"'file_name' is a required argument if '{type(self).__qualname__}.on_disk' is `False`" + ) + file_name = self.file_abs_path + results = np.loadtxt(file_name, comments="#") + + if results.shape[1] != 8: + raise ValueError(f"{file_name} does not have valid results") + + self.betas = results[:, 1] + self.lambd = results[:, 2] + self.phi_d = results[:, 3] + self.phi_m = results[:, 4] + self.phi_sim = results[:, 5] + self.phi = results[:, 7] + + +class PairedBetaEstimate_ByEig(InversionDirective): + """ + Estimate the trade-off parameter, beta, between pairs of data misfit(s) and the + regularization(s) as a multiple of the ratio between the highest eigenvalue of the + data misfit term and the highest eigenvalue of the regularization. + The highest eigenvalues are estimated through power iterations and Rayleigh + quotient. + + Notes + ----- + This class assumes the order of the data misfits for each model parameter match + the order for the respective regularizations, i.e. + + >>> data_misfits = [phi_d_m1, phi_d_m2, phi_d_m3] + >>> regs = [phi_m_m1, phi_m_m2, phi_m_m3] + + In which case it will estimate regularization parameters for each respective pair. + """ + + beta0_ratio = 1.0 #: the estimated ratio is multiplied by this to obtain beta + n_pw_iter = 4 #: number of power iterations for estimation. + seed = None #: Random seed for the directive + + def initialize(self): + r""" + The initial beta is calculated by comparing the estimated + eigenvalues of :math:`J^T J` and :math:`W^T W`. + To estimate the eigenvector of **A**, we will use one iteration + of the *Power Method*: + + .. math:: + + \mathbf{x_1 = A x_0} + + Given this (very course) approximation of the eigenvector, we can + use the *Rayleigh quotient* to approximate the largest eigenvalue. + + .. math:: + + \lambda_0 = \frac{\mathbf{x^\top A x}}{\mathbf{x^\top x}} + + We will approximate the largest eigenvalue for both JtJ and WtW, + and use some ratio of the quotient to estimate beta0. + + .. math:: + + \beta_0 = \gamma \frac{\mathbf{x^\top J^\top J x}}{\mathbf{x^\top W^\top W x}} + + :rtype: float + :return: beta0 + """ + rng = np.random.default_rng(seed=self.seed) + + if self.verbose: + print("Calculating the beta0 parameter.") + + m = self.invProb.model + dmis_eigenvalues = [] + reg_eigenvalues = [] + dmis_objs = self.dmisfit.objfcts + reg_objs = [ + obj + for obj in self.reg.objfcts + if not isinstance(obj, BaseSimilarityMeasure) + ] + if len(dmis_objs) != len(reg_objs): + raise ValueError( + f"There must be the same number of data misfit and regularizations." + f"Got {len(dmis_objs)} and {len(reg_objs)} respectively." + ) + for dmis, reg in zip(dmis_objs, reg_objs): + dmis_eigenvalues.append( + eigenvalue_by_power_iteration( + dmis, + m, + n_pw_iter=self.n_pw_iter, + random_seed=rng, + ) + ) + + reg_eigenvalues.append( + eigenvalue_by_power_iteration( + reg, + m, + n_pw_iter=self.n_pw_iter, + random_seed=rng, + ) + ) + + self.ratios = np.array(dmis_eigenvalues) / np.array(reg_eigenvalues) + self.invProb.betas = self.beta0_ratio * self.ratios + self.reg.multipliers[:-1] = self.invProb.betas + + +class PairedBetaSchedule(InversionDirective): + """ + Directive for beta cooling schedule to determine the tradeoff + parameters when using paired data misfits and regularizations for a joint inversion. + """ + + chifact_target = 1.0 + beta_tol = 1e-1 + update_beta = True + cooling_rate = 1 + cooling_factor = 2 + dmis_met = False + + @property + def target(self): + if getattr(self, "_target", None) is None: + nD = np.array([survey.nD for survey in self.survey]) + + self._target = nD * self.chifact_target + + return self._target + + @target.setter + def target(self, val): + self._target = val + + def initialize(self): + self.dmis_met = np.zeros_like(self.invProb.betas, dtype=bool) + + def endIter(self): + # Check if target misfit has been reached, if so, set dmis_met to True + for i, phi_d in enumerate(self.invProb.phi_d_list): + self.dmis_met[i] = phi_d < self.target[i] + + # check separately if misfits are within the tolerance, + # otherwise, scale beta individually + for i, phi_d in enumerate(self.invProb.phi_d_list): + if self.opt.iter > 0 and self.opt.iter % self.cooling_rate == 0: + target = self.target[i] + ratio = phi_d / target + if self.update_beta and ratio <= (1.0 + self.beta_tol): + if ratio <= 1: + ratio = np.maximum(0.75, ratio) + else: + ratio = np.minimum(1.5, ratio) + + self.invProb.betas[i] /= ratio + elif ratio > 1.0: + self.invProb.betas[i] /= self.cooling_factor + + self.reg.multipliers[:-1] = self.invProb.betas + + +class MovingAndMultiTargetStopping(InversionDirective): + r""" + Directive for setting stopping criteria for a joint inversion. + Ensures both that all target misfits are met and there is a small change in the + model. Computes the percentage change of the current model from the previous model. + + ..math:: + \frac {\| \mathbf{m_i} - \mathbf{m_{i-1}} \|} {\| \mathbf{m_{i-1}} \|} + """ + + tol = 1e-5 + beta_tol = 1e-1 + chifact_target = 1.0 + + @property + def target(self): + if getattr(self, "_target", None) is None: + nD = [] + for survey in self.survey: + nD += [survey.nD] + nD = np.array(nD) + + self._target = nD * self.chifact_target + + return self._target + + @target.setter + def target(self, val): + self._target = val + + def endIter(self): + for phi_d, target in zip(self.invProb.phi_d_list, self.target): + if np.abs(1.0 - phi_d / target) >= self.beta_tol: + return + if ( + np.linalg.norm(self.opt.xc - self.opt.x_last) + / np.linalg.norm(self.opt.x_last) + > self.tol + ): + return + + print( + "stopping criteria met: ", + np.linalg.norm(self.opt.xc - self.opt.x_last) + / np.linalg.norm(self.opt.x_last), + ) + self.opt.stopNextIteration = True diff --git a/simpeg/directives/directives.py b/simpeg/directives/directives.py index 156f1acb33..c40e514918 100644 --- a/simpeg/directives/directives.py +++ b/simpeg/directives/directives.py @@ -1,2820 +1,18 @@ -import numpy as np -import matplotlib.pyplot as plt -import warnings -import os -import scipy.sparse as sp -from ..data_misfit import BaseDataMisfit -from ..objective_function import ComboObjectiveFunction -from ..maps import IdentityMap, Wires -from ..regularization import ( - WeightedLeastSquares, - BaseRegularization, - BaseSparse, - Smallness, - Sparse, - SparseSmallness, - PGIsmallness, - SmoothnessFirstOrder, - SparseSmoothness, - BaseSimilarityMeasure, -) -from ..utils import ( - mkvc, - set_kwargs, - sdiag, - estimate_diagonal, - spherical2cartesian, - cartesian2spherical, - Zero, - eigenvalue_by_power_iteration, - validate_string, -) -from ..utils.code_utils import ( - deprecate_property, - validate_type, - validate_integer, - validate_float, - validate_ndarray_with_shape, -) - - -class InversionDirective: - """Base inversion directive class. - - SimPEG directives initialize and update parameters used by the inversion algorithm; - e.g. setting the initial beta or updating the regularization. ``InversionDirective`` - is a parent class responsible for connecting directives to the data misfit, regularization - and optimization defining the inverse problem. - - Parameters - ---------- - inversion : simpeg.inversion.BaseInversion, None - An SimPEG inversion object; i.e. an instance of :class:`simpeg.inversion.BaseInversion`. - dmisfit : simpeg.data_misfit.BaseDataMisfit, None - A data data misfit; i.e. an instance of :class:`simpeg.data_misfit.BaseDataMisfit`. - reg : simpeg.regularization.BaseRegularization, None - The regularization, or model objective function; i.e. an instance of :class:`simpeg.regularization.BaseRegularization`. - verbose : bool - Whether or not to print debugging information. - """ - - _REGISTRY = {} - - _regPair = [WeightedLeastSquares, BaseRegularization, ComboObjectiveFunction] - _dmisfitPair = [BaseDataMisfit, ComboObjectiveFunction] - - def __init__(self, inversion=None, dmisfit=None, reg=None, verbose=False, **kwargs): - # Raise error on deprecated arguments - if (key := "debug") in kwargs.keys(): - raise TypeError(f"'{key}' property has been removed. Please use 'verbose'.") - self.inversion = inversion - self.dmisfit = dmisfit - self.reg = reg - self.verbose = verbose - set_kwargs(self, **kwargs) - - @property - def verbose(self): - """Whether or not to print debugging information. - - Returns - ------- - bool - """ - return self._verbose - - @verbose.setter - def verbose(self, value): - self._verbose = validate_type("verbose", value, bool) - - debug = deprecate_property( - verbose, "debug", "verbose", removal_version="0.19.0", error=True - ) - - @property - def inversion(self): - """Inversion object associated with the directive. - - Returns - ------- - simpeg.inversion.BaseInversion - The inversion associated with the directive. - """ - if not hasattr(self, "_inversion"): - return None - return self._inversion - - @inversion.setter - def inversion(self, i): - if getattr(self, "_inversion", None) is not None: - warnings.warn( - "InversionDirective {0!s} has switched to a new inversion.".format( - self.__class__.__name__ - ), - stacklevel=2, - ) - self._inversion = i - - @property - def invProb(self): - """Inverse problem associated with the directive. - - Returns - ------- - simpeg.inverse_problem.BaseInvProblem - The inverse problem associated with the directive. - """ - return self.inversion.invProb - - @property - def opt(self): - """Optimization algorithm associated with the directive. - - Returns - ------- - simpeg.optimization.Minimize - Optimization algorithm associated with the directive. - """ - return self.invProb.opt - - @property - def reg(self): - """Regularization associated with the directive. - - Returns - ------- - simpeg.regularization.BaseRegularization - The regularization associated with the directive. - """ - if getattr(self, "_reg", None) is None: - self.reg = self.invProb.reg # go through the setter - return self._reg - - @reg.setter - def reg(self, value): - if value is not None: - assert any( - [isinstance(value, regtype) for regtype in self._regPair] - ), "Regularization must be in {}, not {}".format(self._regPair, type(value)) - - if isinstance(value, WeightedLeastSquares): - value = 1 * value # turn it into a combo objective function - self._reg = value - - @property - def dmisfit(self): - """Data misfit associated with the directive. - - Returns - ------- - simpeg.data_misfit.BaseDataMisfit - The data misfit associated with the directive. - """ - if getattr(self, "_dmisfit", None) is None: - self.dmisfit = self.invProb.dmisfit # go through the setter - return self._dmisfit - - @dmisfit.setter - def dmisfit(self, value): - if value is not None: - assert any( - [isinstance(value, dmisfittype) for dmisfittype in self._dmisfitPair] - ), "Misfit must be in {}, not {}".format(self._dmisfitPair, type(value)) - - if not isinstance(value, ComboObjectiveFunction): - value = 1 * value # turn it into a combo objective function - self._dmisfit = value - - @property - def survey(self): - """Return survey for all data misfits - - Assuming that ``dmisfit`` is always a ``ComboObjectiveFunction``, - return a list containing the survey for each data misfit; i.e. - [survey1, survey2, ...] - - Returns - ------- - list of simpeg.survey.Survey - Survey for all data misfits. - """ - return [objfcts.simulation.survey for objfcts in self.dmisfit.objfcts] - - @property - def simulation(self): - """Return simulation for all data misfits. - - Assuming that ``dmisfit`` is always a ``ComboObjectiveFunction``, - return a list containing the simulation for each data misfit; i.e. - [sim1, sim2, ...]. - - Returns - ------- - list of simpeg.simulation.BaseSimulation - Simulation for all data misfits. - """ - return [objfcts.simulation for objfcts in self.dmisfit.objfcts] - - def initialize(self): - """Initialize inversion parameter(s) according to directive.""" - pass - - def endIter(self): - """Update inversion parameter(s) according to directive at end of iteration.""" - pass - - def finish(self): - """Update inversion parameter(s) according to directive at end of inversion.""" - pass - - def validate(self, directiveList=None): - """Validate directive. - - The `validate` method returns ``True`` if the directive and its location within - the directives list does not encounter conflicts. Otherwise, an appropriate error - message is returned describing the conflict. - - Parameters - ---------- - directive_list : simpeg.directives.DirectiveList - List of directives used in the inversion. - - Returns - ------- - bool - Returns ``True`` if validated, otherwise an approriate error is returned. - """ - return True - - -class DirectiveList(object): - """Directives list - - SimPEG directives initialize and update parameters used by the inversion algorithm; - e.g. setting the initial beta or updating the regularization. ``DirectiveList`` stores - the set of directives used in the inversion algorithm. - - Parameters - ---------- - directives : list of simpeg.directives.InversionDirective - List of directives. - inversion : simpeg.inversion.BaseInversion - The inversion associated with the directives list. - debug : bool - Whether or not to print debugging information. - - """ - - def __init__(self, *directives, inversion=None, debug=False, **kwargs): - super().__init__(**kwargs) - self.dList = [] - for d in directives: - assert isinstance( - d, InversionDirective - ), "All directives must be InversionDirectives not {}".format(type(d)) - self.dList.append(d) - self.inversion = inversion - self.verbose = debug - - @property - def debug(self): - """Whether or not to print debugging information - - Returns - ------- - bool - """ - return getattr(self, "_debug", False) - - @debug.setter - def debug(self, value): - for d in self.dList: - d.debug = value - self._debug = value - - @property - def inversion(self): - """Inversion object associated with the directives list. - - Returns - ------- - simpeg.inversion.BaseInversion - The inversion associated with the directives list. - """ - return getattr(self, "_inversion", None) - - @inversion.setter - def inversion(self, i): - if self.inversion is i: - return - if getattr(self, "_inversion", None) is not None: - warnings.warn( - "{0!s} has switched to a new inversion.".format( - self.__class__.__name__ - ), - stacklevel=2, - ) - for d in self.dList: - d.inversion = i - self._inversion = i - - def call(self, ruleType): - if self.dList is None: - if self.verbose: - print("DirectiveList is None, no directives to call!") - return - - directives = ["initialize", "endIter", "finish"] - assert ruleType in directives, 'Directive type must be in ["{0!s}"]'.format( - '", "'.join(directives) - ) - for r in self.dList: - getattr(r, ruleType)() - - def validate(self): - [directive.validate(self) for directive in self.dList] - return True - - -class BaseBetaEstimator(InversionDirective): - """Base class for estimating initial trade-off parameter (beta). - - This class has properties and methods inherited by directive classes which estimate - the initial trade-off parameter (beta). This class is not used directly to create - directives for the inversion. - - Parameters - ---------- - beta0_ratio : float - Desired ratio between data misfit and model objective function at initial beta iteration. - seed : int, None - Seed used for random sampling. - - """ - - def __init__( - self, - beta0_ratio=1.0, - n_pw_iter=4, - seed=None, - method="power_iteration", - **kwargs, - ): - super().__init__(**kwargs) - self.beta0_ratio = beta0_ratio - self.seed = seed - - @property - def beta0_ratio(self): - """The estimated ratio is multiplied by this to obtain beta. - - Returns - ------- - float - """ - return self._beta0_ratio - - @beta0_ratio.setter - def beta0_ratio(self, value): - self._beta0_ratio = validate_float( - "beta0_ratio", value, min_val=0.0, inclusive_min=False - ) - - @property - def seed(self): - """Random seed to initialize with. - - Returns - ------- - int - """ - return self._seed - - @seed.setter - def seed(self, value): - if value is not None: - value = validate_integer("seed", value, min_val=1) - self._seed = value - - def validate(self, directive_list): - ind = [isinstance(d, BaseBetaEstimator) for d in directive_list.dList] - assert np.sum(ind) == 1, ( - "Multiple directives for computing initial beta detected in directives list. " - "Only one directive can be used to set the initial beta." - ) - - return True - - -class BetaEstimateMaxDerivative(BaseBetaEstimator): - r"""Estimate initial trade-off parameter (beta) using largest derivatives. - - The initial trade-off parameter (beta) is estimated by scaling the ratio - between the largest derivatives in the gradient of the data misfit and - model objective function. The estimated trade-off parameter is used to - update the **beta** property in the associated :class:`simpeg.inverse_problem.BaseInvProblem` - object prior to running the inversion. A separate directive is used for updating the - trade-off parameter at successive beta iterations; see :class:`BetaSchedule`. - - Parameters - ---------- - beta0_ratio: float - Desired ratio between data misfit and model objective function at initial beta iteration. - seed : int, None - Seed used for random sampling. - - Notes - ----- - Let :math:`\phi_d` represent the data misfit, :math:`\phi_m` represent the model - objective function and :math:`\mathbf{m_0}` represent the starting model. The first - model update is obtained by minimizing the a global objective function of the form: - - .. math:: - \phi (\mathbf{m_0}) = \phi_d (\mathbf{m_0}) + \beta_0 \phi_m (\mathbf{m_0}) - - where :math:`\beta_0` represents the initial trade-off parameter (beta). - - We define :math:`\gamma` as the desired ratio between the data misfit and model objective - functions at the initial beta iteration (defined by the 'beta0_ratio' input argument). - Here, the initial trade-off parameter is computed according to: - - .. math:: - \beta_0 = \gamma \frac{| \nabla_m \phi_d (\mathbf{m_0}) |_{max}}{| \nabla_m \phi_m (\mathbf{m_0 + \delta m}) |_{max}} - - where - - .. math:: - \delta \mathbf{m} = \frac{m_{max}}{\mu_{max}} \boldsymbol{\mu} - - and :math:`\boldsymbol{\mu}` is a set of independent samples from the - continuous uniform distribution between 0 and 1. - - """ - - def __init__(self, beta0_ratio=1.0, seed=None, **kwargs): - super().__init__(beta0_ratio, seed, **kwargs) - - def initialize(self): - if self.seed is not None: - np.random.seed(self.seed) - - if self.verbose: - print("Calculating the beta0 parameter.") - - m = self.invProb.model - - x0 = np.random.rand(*m.shape) - phi_d_deriv = np.abs(self.dmisfit.deriv(m)).max() - dm = x0 / x0.max() * m.max() - phi_m_deriv = np.abs(self.reg.deriv(m + dm)).max() - - self.ratio = np.asarray(phi_d_deriv / phi_m_deriv) - self.beta0 = self.beta0_ratio * self.ratio - self.invProb.beta = self.beta0 - - -class BetaEstimate_ByEig(BaseBetaEstimator): - r"""Estimate initial trade-off parameter (beta) by power iteration. - - The initial trade-off parameter (beta) is estimated by scaling the ratio - between the largest eigenvalue in the second derivative of the data - misfit and the model objective function. The largest eigenvalues are estimated - using the power iteration method; see :func:`simpeg.utils.eigenvalue_by_power_iteration`. - The estimated trade-off parameter is used to update the **beta** property in the - associated :class:`simpeg.inverse_problem.BaseInvProblem` object prior to running the inversion. - Note that a separate directive is used for updating the trade-off parameter at successive - beta iterations; see :class:`BetaSchedule`. - - Parameters - ---------- - beta0_ratio: float - Desired ratio between data misfit and model objective function at initial beta iteration. - n_pw_iter : int - Number of power iterations used to estimate largest eigenvalues. - seed : int, None - Seed used for random sampling. - - Notes - ----- - Let :math:`\phi_d` represent the data misfit, :math:`\phi_m` represent the model - objective function and :math:`\mathbf{m_0}` represent the starting model. The first - model update is obtained by minimizing the a global objective function of the form: - - .. math:: - \phi (\mathbf{m_0}) = \phi_d (\mathbf{m_0}) + \beta_0 \phi_m (\mathbf{m_0}) - - where :math:`\beta_0` represents the initial trade-off parameter (beta). - Let :math:`\gamma` define the desired ratio between the data misfit and model - objective functions at the initial beta iteration (defined by the 'beta0_ratio' input argument). - Using the power iteration approach, our initial trade-off parameter is given by: - - .. math:: - \beta_0 = \gamma \frac{\lambda_d}{\lambda_m} - - where :math:`\lambda_d` as the largest eigenvalue of the Hessian of the data misfit, and - :math:`\lambda_m` as the largest eigenvalue of the Hessian of the model objective function. - For each Hessian, the largest eigenvalue is computed using power iteration. The input - parameter 'n_pw_iter' sets the number of power iterations used in the estimate. - - For a description of the power iteration approach for estimating the larges eigenvalue, - see :func:`simpeg.utils.eigenvalue_by_power_iteration`. - - """ - - def __init__(self, beta0_ratio=1.0, n_pw_iter=4, seed=None, **kwargs): - super().__init__(beta0_ratio, seed, **kwargs) - self.n_pw_iter = n_pw_iter - - @property - def n_pw_iter(self): - """Number of power iterations for estimating largest eigenvalues. - - Returns - ------- - int - Number of power iterations for estimating largest eigenvalues. - """ - return self._n_pw_iter - - @n_pw_iter.setter - def n_pw_iter(self, value): - self._n_pw_iter = validate_integer("n_pw_iter", value, min_val=1) - - def initialize(self): - if self.seed is not None: - np.random.seed(self.seed) - - if self.verbose: - print("Calculating the beta0 parameter.") - - m = self.invProb.model - - dm_eigenvalue = eigenvalue_by_power_iteration( - self.dmisfit, - m, - n_pw_iter=self.n_pw_iter, - ) - reg_eigenvalue = eigenvalue_by_power_iteration( - self.reg, - m, - n_pw_iter=self.n_pw_iter, - ) - - self.ratio = np.asarray(dm_eigenvalue / reg_eigenvalue) - self.beta0 = self.beta0_ratio * self.ratio - self.invProb.beta = self.beta0 - - -class BetaSchedule(InversionDirective): - """Reduce trade-off parameter (beta) at successive iterations using a cooling schedule. - - Updates the **beta** property in the associated :class:`simpeg.inverse_problem.BaseInvProblem` - while the inversion is running. - For linear least-squares problems, the optimization problem can be solved in a - single step and the cooling rate can be set to *1*. For non-linear optimization - problems, multiple steps are required obtain the minimizer for a fixed trade-off - parameter. In this case, the cooling rate should be larger than 1. - - Parameters - ---------- - coolingFactor : float - The factor by which the trade-off parameter is decreased when updated. - The preexisting value of the trade-off parameter is divided by the cooling factor. - coolingRate : int - Sets the number of successive iterations before the trade-off parameter is reduced. - Use *1* for linear least-squares optimization problems. Use *2* for weakly non-linear - optimization problems. Use *3* for general non-linear optimization problems. - - """ - - def __init__(self, coolingFactor=8.0, coolingRate=3, **kwargs): - super().__init__(**kwargs) - self.coolingFactor = coolingFactor - self.coolingRate = coolingRate - - @property - def coolingFactor(self): - """Beta is divided by this value every `coolingRate` iterations. - - Returns - ------- - float - """ - return self._coolingFactor - - @coolingFactor.setter - def coolingFactor(self, value): - self._coolingFactor = validate_float( - "coolingFactor", value, min_val=0.0, inclusive_min=False - ) - - @property - def coolingRate(self): - """Cool after this number of iterations. - - Returns - ------- - int - """ - return self._coolingRate - - @coolingRate.setter - def coolingRate(self, value): - self._coolingRate = validate_integer("coolingRate", value, min_val=1) - - def endIter(self): - if self.opt.iter > 0 and self.opt.iter % self.coolingRate == 0: - if self.verbose: - print( - "BetaSchedule is cooling Beta. Iteration: {0:d}".format( - self.opt.iter - ) - ) - self.invProb.beta /= self.coolingFactor - - -class AlphasSmoothEstimate_ByEig(InversionDirective): - """ - Estimate the alphas multipliers for the smoothness terms of the regularization - as a multiple of the ratio between the highest eigenvalue of the - smallness term and the highest eigenvalue of each smoothness term of the regularization. - The highest eigenvalue are estimated through power iterations and Rayleigh quotient. - """ - - def __init__(self, alpha0_ratio=1.0, n_pw_iter=4, seed=None, **kwargs): - super().__init__(**kwargs) - self.alpha0_ratio = alpha0_ratio - self.n_pw_iter = n_pw_iter - self.seed = seed - - @property - def alpha0_ratio(self): - """the estimated Alpha_smooth is multiplied by this ratio (int or array). - - Returns - ------- - numpy.ndarray - """ - return self._alpha0_ratio - - @alpha0_ratio.setter - def alpha0_ratio(self, value): - self._alpha0_ratio = validate_ndarray_with_shape( - "alpha0_ratio", value, shape=("*",) - ) - - @property - def n_pw_iter(self): - """Number of power iterations for estimation. - - Returns - ------- - int - """ - return self._n_pw_iter - - @n_pw_iter.setter - def n_pw_iter(self, value): - self._n_pw_iter = validate_integer("n_pw_iter", value, min_val=1) - - @property - def seed(self): - """Random seed to initialize with. - - Returns - ------- - int - """ - return self._seed - - @seed.setter - def seed(self, value): - if value is not None: - value = validate_integer("seed", value, min_val=1) - self._seed = value - - def initialize(self): - """""" - if self.seed is not None: - np.random.seed(self.seed) - - smoothness = [] - smallness = [] - parents = {} - for regobjcts in self.reg.objfcts: - if isinstance(regobjcts, ComboObjectiveFunction): - objfcts = regobjcts.objfcts - else: - objfcts = [regobjcts] - - for obj in objfcts: - if isinstance( - obj, - ( - Smallness, - SparseSmallness, - PGIsmallness, - ), - ): - smallness += [obj] - - elif isinstance(obj, (SmoothnessFirstOrder, SparseSmoothness)): - parents[obj] = regobjcts - smoothness += [obj] - - if len(smallness) == 0: - raise UserWarning( - "Directive 'AlphasSmoothEstimate_ByEig' requires a regularization with at least one Small instance." - ) - - smallness_eigenvalue = eigenvalue_by_power_iteration( - smallness[0], - self.invProb.model, - n_pw_iter=self.n_pw_iter, - ) - - self.alpha0_ratio = self.alpha0_ratio * np.ones(len(smoothness)) - - if len(self.alpha0_ratio) != len(smoothness): - raise ValueError( - f"Input values for 'alpha0_ratio' should be of len({len(smoothness)}). Provided {self.alpha0_ratio}" - ) - - alphas = [] - for user_alpha, obj in zip(self.alpha0_ratio, smoothness): - smooth_i_eigenvalue = eigenvalue_by_power_iteration( - obj, - self.invProb.model, - n_pw_iter=self.n_pw_iter, - ) - ratio = smallness_eigenvalue / smooth_i_eigenvalue - - mtype = obj._multiplier_pair - - new_alpha = getattr(parents[obj], mtype) * user_alpha * ratio - setattr(parents[obj], mtype, new_alpha) - alphas += [new_alpha] - - if self.verbose: - print(f"Alpha scales: {alphas}") - - -class ScalingMultipleDataMisfits_ByEig(InversionDirective): - """ - For multiple data misfits only: multiply each data misfit term - by the inverse of its highest eigenvalue and then - normalize the sum of the data misfit multipliers to one. - The highest eigenvalue are estimated through power iterations and Rayleigh quotient. - """ - - def __init__(self, chi0_ratio=None, n_pw_iter=4, seed=None, **kwargs): - super().__init__(**kwargs) - self.chi0_ratio = chi0_ratio - self.n_pw_iter = n_pw_iter - self.seed = seed - - @property - def chi0_ratio(self): - """the estimated Alpha_smooth is multiplied by this ratio (int or array) - - Returns - ------- - numpy.ndarray - """ - return self._chi0_ratio - - @chi0_ratio.setter - def chi0_ratio(self, value): - if value is not None: - value = validate_ndarray_with_shape("chi0_ratio", value, shape=("*",)) - self._chi0_ratio = value - - @property - def n_pw_iter(self): - """Number of power iterations for estimation. - - Returns - ------- - int - """ - return self._n_pw_iter - - @n_pw_iter.setter - def n_pw_iter(self, value): - self._n_pw_iter = validate_integer("n_pw_iter", value, min_val=1) - - @property - def seed(self): - """Random seed to initialize with - - Returns - ------- - int - """ - return self._seed - - @seed.setter - def seed(self, value): - if value is not None: - value = validate_integer("seed", value, min_val=1) - self._seed = value - - def initialize(self): - """""" - if self.seed is not None: - np.random.seed(self.seed) - - if self.verbose: - print("Calculating the scaling parameter.") - - if ( - getattr(self.dmisfit, "objfcts", None) is None - or len(self.dmisfit.objfcts) == 1 - ): - raise TypeError( - "ScalingMultipleDataMisfits_ByEig only applies to joint inversion" - ) - - ndm = len(self.dmisfit.objfcts) - if self.chi0_ratio is not None: - self.chi0_ratio = self.chi0_ratio * np.ones(ndm) - else: - self.chi0_ratio = self.dmisfit.multipliers - - m = self.invProb.model - - dm_eigenvalue_list = [] - for dm in self.dmisfit.objfcts: - dm_eigenvalue_list += [eigenvalue_by_power_iteration(dm, m)] - - self.chi0 = self.chi0_ratio / np.r_[dm_eigenvalue_list] - self.chi0 = self.chi0 / np.sum(self.chi0) - self.dmisfit.multipliers = self.chi0 - - if self.verbose: - print("Scale Multipliers: ", self.dmisfit.multipliers) - - -class JointScalingSchedule(InversionDirective): - """ - For multiple data misfits only: rebalance each data misfit term - during the inversion when some datasets are fit, and others not - using the ratios of current misfits and their respective target. - It implements the strategy described in https://doi.org/10.1093/gji/ggaa378. - """ - - def __init__( - self, warmingFactor=1.0, chimax=1e10, chimin=1e-10, update_rate=1, **kwargs - ): - super().__init__(**kwargs) - self.mode = 1 - self.warmingFactor = warmingFactor - self.chimax = chimax - self.chimin = chimin - self.update_rate = update_rate - - @property - def mode(self): - """The type of update to perform. - - Returns - ------- - {1, 2} - """ - return self._mode - - @mode.setter - def mode(self, value): - self._mode = validate_integer("mode", value, min_val=1, max_val=2) - - @property - def warmingFactor(self): - """Factor to adjust scaling of the data misfits by. - - Returns - ------- - float - """ - return self._warmingFactor - - @warmingFactor.setter - def warmingFactor(self, value): - self._warmingFactor = validate_float( - "warmingFactor", value, min_val=0.0, inclusive_min=False - ) - - @property - def chimax(self): - """Maximum chi factor. - - Returns - ------- - float - """ - return self._chimax - - @chimax.setter - def chimax(self, value): - self._chimax = validate_float("chimax", value, min_val=0.0, inclusive_min=False) - - @property - def chimin(self): - """Minimum chi factor. - - Returns - ------- - float - """ - return self._chimin - - @chimin.setter - def chimin(self, value): - self._chimin = validate_float("chimin", value, min_val=0.0, inclusive_min=False) - - @property - def update_rate(self): - """Will update the data misfit scalings after this many iterations. - - Returns - ------- - int - """ - return self._update_rate - - @update_rate.setter - def update_rate(self, value): - self._update_rate = validate_integer("update_rate", value, min_val=1) - - def initialize(self): - if ( - getattr(self.dmisfit, "objfcts", None) is None - or len(self.dmisfit.objfcts) == 1 - ): - raise TypeError("JointScalingSchedule only applies to joint inversion") - - targetclass = np.r_[ - [ - isinstance(dirpart, MultiTargetMisfits) - for dirpart in self.inversion.directiveList.dList - ] - ] - if ~np.any(targetclass): - self.DMtarget = None - else: - self.targetclass = np.where(targetclass)[0][-1] - self.DMtarget = self.inversion.directiveList.dList[ - self.targetclass - ].DMtarget - - if self.verbose: - print("Initial data misfit scales: ", self.dmisfit.multipliers) - - def endIter(self): - self.dmlist = self.inversion.directiveList.dList[self.targetclass].dmlist - - if np.any(self.dmlist < self.DMtarget): - self.mode = 2 - else: - self.mode = 1 - - if self.opt.iter > 0 and self.opt.iter % self.update_rate == 0: - if self.mode == 2: - if np.all(np.r_[self.dmisfit.multipliers] > self.chimin) and np.all( - np.r_[self.dmisfit.multipliers] < self.chimax - ): - indx = self.dmlist > self.DMtarget - if np.any(indx): - multipliers = self.warmingFactor * np.median( - self.DMtarget[~indx] / self.dmlist[~indx] - ) - if np.sum(indx) == 1: - indx = np.where(indx)[0][0] - self.dmisfit.multipliers[indx] *= multipliers - self.dmisfit.multipliers /= np.sum(self.dmisfit.multipliers) - - if self.verbose: - print("Updating scaling for data misfits by ", multipliers) - print("New scales:", self.dmisfit.multipliers) - - -class TargetMisfit(InversionDirective): - """ - ... note:: Currently this target misfit is not set up for joint inversion. - Check out MultiTargetMisfits - """ - - def __init__(self, target=None, phi_d_star=None, chifact=1.0, **kwargs): - super().__init__(**kwargs) - self.chifact = chifact - self.phi_d_star = phi_d_star - if phi_d_star is not None and target is not None: - raise AttributeError("Attempted to set both target and phi_d_star.") - if target is not None: - self.target = target - - @property - def target(self): - """The target value for the data misfit - - Returns - ------- - float - """ - if getattr(self, "_target", None) is None: - self._target = self.chifact * self.phi_d_star - return self._target - - @target.setter - def target(self, val): - self._target = validate_float("target", val, min_val=0.0, inclusive_min=False) - - @property - def chifact(self): - """The a multiplier for the target data misfit value. - - The target value is `chifact` times `phi_d_star` - - Returns - ------- - float - """ - return self._chifact - - @chifact.setter - def chifact(self, value): - self._chifact = validate_float( - "chifact", value, min_val=0.0, inclusive_min=False - ) - self._target = None - - @property - def phi_d_star(self): - """The target phi_d value for the data misfit. - - The target value is `chifact` times `phi_d_star` - - Returns - ------- - float - """ - # phid = ||dpred - dobs||^2 - if self._phi_d_star is None: - nD = 0 - for survey in self.survey: - nD += survey.nD - self._phi_d_star = nD - return self._phi_d_star - - @phi_d_star.setter - def phi_d_star(self, value): - # phid = ||dpred - dobs||^2 - if value is not None: - value = validate_float( - "phi_d_star", value, min_val=0.0, inclusive_min=False - ) - self._phi_d_star = value - self._target = None - - def endIter(self): - if self.invProb.phi_d < self.target: - self.opt.stopNextIteration = True - self.print_final_misfit() - - def print_final_misfit(self): - if self.opt.print_type == "ubc": - self.opt.print_target = ( - ">> Target misfit: %.1f (# of data) is achieved" - ) % (self.target * self.invProb.opt.factor) - - -class MultiTargetMisfits(InversionDirective): - def __init__( - self, - WeightsInTarget=False, - chifact=1.0, - phi_d_star=None, - TriggerSmall=True, - chiSmall=1.0, - phi_ms_star=None, - TriggerTheta=False, - ToleranceTheta=1.0, - distance_norm=np.inf, - **kwargs, - ): - super().__init__(**kwargs) - - self.WeightsInTarget = WeightsInTarget - # Chi factor for Geophsyical Data Misfit - self.chifact = chifact - self.phi_d_star = phi_d_star - - # Chifact for Clustering/Smallness - self.TriggerSmall = TriggerSmall - self.chiSmall = chiSmall - self.phi_ms_star = phi_ms_star - - # Tolerance for parameters difference with their priors - self.TriggerTheta = TriggerTheta # deactivated by default - self.ToleranceTheta = ToleranceTheta - self.distance_norm = distance_norm - - self._DM = False - self._CL = False - self._DP = False - - @property - def WeightsInTarget(self): - """Whether to account for weights in the petrophysical misfit. - - Returns - ------- - bool - """ - return self._WeightsInTarget - - @WeightsInTarget.setter - def WeightsInTarget(self, value): - self._WeightsInTarget = validate_type("WeightsInTarget", value, bool) - - @property - def chifact(self): - """The a multiplier for the target Geophysical data misfit value. - - The target value is `chifact` times `phi_d_star` - - Returns - ------- - numpy.ndarray - """ - return self._chifact - - @chifact.setter - def chifact(self, value): - self._chifact = validate_ndarray_with_shape("chifact", value, shape=("*",)) - self._DMtarget = None - - @property - def phi_d_star(self): - """The target phi_d value for the Geophysical data misfit. - - The target value is `chifact` times `phi_d_star` - - Returns - ------- - float - """ - # phid = || dpred - dobs||^2 - if getattr(self, "_phi_d_star", None) is None: - # Check if it is a ComboObjective - if isinstance(self.dmisfit, ComboObjectiveFunction): - value = np.r_[[survey.nD for survey in self.survey]] - else: - value = np.r_[[self.survey.nD]] - self._phi_d_star = value - self._DMtarget = None - - return self._phi_d_star - - @phi_d_star.setter - def phi_d_star(self, value): - # phid =|| dpred - dobs||^2 - if value is not None: - value = validate_ndarray_with_shape("phi_d_star", value, shape=("*",)) - self._phi_d_star = value - self._DMtarget = None - - @property - def chiSmall(self): - """The a multiplier for the target petrophysical misfit value. - - The target value is `chiSmall` times `phi_ms_star` - - Returns - ------- - float - """ - return self._chiSmall - - @chiSmall.setter - def chiSmall(self, value): - self._chiSmall = validate_float("chiSmall", value) - self._CLtarget = None - - @property - def phi_ms_star(self): - """The target value for the petrophysical data misfit. - - The target value is `chiSmall` times `phi_ms_star` - - Returns - ------- - float - """ - return self._phi_ms_star - - @phi_ms_star.setter - def phi_ms_star(self, value): - if value is not None: - value = validate_float("phi_ms_star", value) - self._phi_ms_star = value - self._CLtarget = None - - @property - def TriggerSmall(self): - """Whether to trigger the smallness misfit test. - - Returns - ------- - bool - """ - return self._TriggerSmall - - @TriggerSmall.setter - def TriggerSmall(self, value): - self._TriggerSmall = validate_type("TriggerSmall", value, bool) - - @property - def TriggerTheta(self): - """Whether to trigger the GMM misfit test. - - Returns - ------- - bool - """ - return self._TriggerTheta - - @TriggerTheta.setter - def TriggerTheta(self, value): - self._TriggerTheta = validate_type("TriggerTheta", value, bool) - - @property - def ToleranceTheta(self): - """Target value for the GMM misfit. - - Returns - ------- - float - """ - return self._ToleranceTheta - - @ToleranceTheta.setter - def ToleranceTheta(self, value): - self._ToleranceTheta = validate_float("ToleranceTheta", value, min_val=0.0) - - @property - def distance_norm(self): - """Distance norm to use for GMM misfit measure. - - Returns - ------- - float - """ - return self._distance_norm - - @distance_norm.setter - def distance_norm(self, value): - self._distance_norm = validate_float("distance_norm", value, min_val=0.0) - - def initialize(self): - self.dmlist = np.r_[[dmis(self.invProb.model) for dmis in self.dmisfit.objfcts]] - - if getattr(self.invProb.reg.objfcts[0], "objfcts", None) is not None: - smallness = np.r_[ - [ - ( - np.r_[ - i, - j, - isinstance(regpart, PGIsmallness), - ] - ) - for i, regobjcts in enumerate(self.invProb.reg.objfcts) - for j, regpart in enumerate(regobjcts.objfcts) - ] - ] - if smallness[smallness[:, 2] == 1][:, :2].size == 0: - warnings.warn( - "There is no PGI regularization. Smallness target is turned off (TriggerSmall flag)", - stacklevel=2, - ) - self.smallness = -1 - self.pgi_smallness = None - - else: - self.smallness = smallness[smallness[:, 2] == 1][:, :2][0] - self.pgi_smallness = self.invProb.reg.objfcts[ - self.smallness[0] - ].objfcts[self.smallness[1]] - - if self.verbose: - print( - type( - self.invProb.reg.objfcts[self.smallness[0]].objfcts[ - self.smallness[1] - ] - ) - ) - - self._regmode = 1 - - else: - smallness = np.r_[ - [ - ( - np.r_[ - j, - isinstance(regpart, PGIsmallness), - ] - ) - for j, regpart in enumerate(self.invProb.reg.objfcts) - ] - ] - if smallness[smallness[:, 1] == 1][:, :1].size == 0: - if self.TriggerSmall: - warnings.warn( - "There is no PGI regularization. Smallness target is turned off (TriggerSmall flag).", - stacklevel=2, - ) - self.TriggerSmall = False - self.smallness = -1 - else: - self.smallness = smallness[smallness[:, 1] == 1][:, :1][0] - self.pgi_smallness = self.invProb.reg.objfcts[self.smallness[0]] - - if self.verbose: - print(type(self.invProb.reg.objfcts[self.smallness[0]])) - - self._regmode = 2 - - @property - def DM(self): - """Whether the geophysical data misfit target was satisfied. - - Returns - ------- - bool - """ - return self._DM - - @property - def CL(self): - """Whether the petrophysical misfit target was satisified. - - Returns - ------- - bool - """ - return self._CL - - @property - def DP(self): - """Whether the GMM misfit was below the threshold. - - Returns - ------- - bool - """ - return self._DP - - @property - def AllStop(self): - """Whether all target misfit values have been met. - - Returns - ------- - bool - """ - - return self.DM and self.CL and self.DP - - @property - def DMtarget(self): - if getattr(self, "_DMtarget", None) is None: - self._DMtarget = self.chifact * self.phi_d_star - return self._DMtarget - - @DMtarget.setter - def DMtarget(self, val): - self._DMtarget = val - - @property - def CLtarget(self): - if not getattr(self.pgi_smallness, "approx_eval", True): - # if nonlinear prior, compute targer numerically at each GMM update - samples, _ = self.pgi_smallness.gmm.sample( - len(self.pgi_smallness.gmm.cell_volumes) - ) - self.phi_ms_star = self.pgi_smallness( - mkvc(samples), externalW=self.WeightsInTarget - ) - - self._CLtarget = self.chiSmall * self.phi_ms_star - - elif getattr(self, "_CLtarget", None) is None: - # phid = ||dpred - dobs||^2 - if self.phi_ms_star is None: - # Expected value is number of active cells * number of physical - # properties - self.phi_ms_star = len(self.invProb.model) - - self._CLtarget = self.chiSmall * self.phi_ms_star - - return self._CLtarget - - @property - def CLnormalizedConstant(self): - if ~self.WeightsInTarget: - return 1.0 - elif np.any(self.smallness == -1): - return np.sum( - sp.csr_matrix.diagonal(self.invProb.reg.objfcts[0].W) ** 2.0 - ) / len(self.invProb.model) - else: - return np.sum(sp.csr_matrix.diagonal(self.pgi_smallness.W) ** 2.0) / len( - self.invProb.model - ) - - @CLtarget.setter - def CLtarget(self, val): - self._CLtarget = val - - def phims(self): - if np.any(self.smallness == -1): - return self.invProb.reg.objfcts[0](self.invProb.model) - else: - return ( - self.pgi_smallness( - self.invProb.model, external_weights=self.WeightsInTarget - ) - / self.CLnormalizedConstant - ) - - def ThetaTarget(self): - maxdiff = 0.0 - - for i in range(self.invProb.reg.gmm.n_components): - meandiff = np.linalg.norm( - (self.invProb.reg.gmm.means_[i] - self.invProb.reg.gmmref.means_[i]) - / self.invProb.reg.gmmref.means_[i], - ord=self.distance_norm, - ) - maxdiff = np.maximum(maxdiff, meandiff) - - if ( - self.invProb.reg.gmm.covariance_type == "full" - or self.invProb.reg.gmm.covariance_type == "spherical" - ): - covdiff = np.linalg.norm( - ( - self.invProb.reg.gmm.covariances_[i] - - self.invProb.reg.gmmref.covariances_[i] - ) - / self.invProb.reg.gmmref.covariances_[i], - ord=self.distance_norm, - ) - else: - covdiff = np.linalg.norm( - ( - self.invProb.reg.gmm.covariances_ - - self.invProb.reg.gmmref.covariances_ - ) - / self.invProb.reg.gmmref.covariances_, - ord=self.distance_norm, - ) - maxdiff = np.maximum(maxdiff, covdiff) - - pidiff = np.linalg.norm( - [ - ( - self.invProb.reg.gmm.weights_[i] - - self.invProb.reg.gmmref.weights_[i] - ) - / self.invProb.reg.gmmref.weights_[i] - ], - ord=self.distance_norm, - ) - maxdiff = np.maximum(maxdiff, pidiff) - - return maxdiff +""" +Backward compatibility with the ``simpeg.directives.directives`` submodule. - def endIter(self): - self._DM = False - self._CL = True - self._DP = True - self.dmlist = np.r_[[dmis(self.invProb.model) for dmis in self.dmisfit.objfcts]] - self.targetlist = np.r_[ - [dm < tgt for dm, tgt in zip(self.dmlist, self.DMtarget)] - ] +This file will be deleted when the ``simpeg.directives.directives`` submodule is +removed. +""" - if np.all(self.targetlist): - self._DM = True - - if self.TriggerSmall and np.any(self.smallness != -1): - if self.phims() > self.CLtarget: - self._CL = False - - if self.TriggerTheta: - if self.ThetaTarget() > self.ToleranceTheta: - self._DP = False - - if self.verbose: - message = "geophys. misfits: " + "; ".join( - map( - str, - [ - "{0} (target {1} [{2}])".format(val, tgt, cond) - for val, tgt, cond in zip( - np.round(self.dmlist, 1), - np.round(self.DMtarget, 1), - self.targetlist, - ) - ], - ) - ) - if self.TriggerSmall: - message += ( - " | smallness misfit: {0:.1f} (target: {1:.1f} [{2}])".format( - self.phims(), self.CLtarget, self.CL - ) - ) - if self.TriggerTheta: - message += " | GMM parameters within tolerance: {}".format(self.DP) - print(message) - - if self.AllStop: - self.opt.stopNextIteration = True - if self.verbose: - print("All targets have been reached") - - -class SaveEveryIteration(InversionDirective): - """SaveEveryIteration - - This directive saves an array at each iteration. The default - directory is the current directory and the models are saved as - ``InversionModel-YYYY-MM-DD-HH-MM-iter.npy`` - """ - - def __init__(self, directory=".", name="InversionModel", **kwargs): - super().__init__(**kwargs) - self.directory = directory - self.name = name - - @property - def directory(self): - """Directory to save results in. - - Returns - ------- - str - """ - return self._directory - - @directory.setter - def directory(self, value): - value = validate_string("directory", value) - fullpath = os.path.abspath(os.path.expanduser(value)) - - if not os.path.isdir(fullpath): - os.mkdir(fullpath) - self._directory = value - - @property - def name(self): - """Root of the filename to be saved. - - Returns - ------- - str - """ - return self._name - - @name.setter - def name(self, value): - self._name = validate_string("name", value) - - @property - def fileName(self): - if getattr(self, "_fileName", None) is None: - from datetime import datetime - - self._fileName = "{0!s}-{1!s}".format( - self.name, datetime.now().strftime("%Y-%m-%d-%H-%M") - ) - return self._fileName - - -class SaveModelEveryIteration(SaveEveryIteration): - """SaveModelEveryIteration - - This directive saves the model as a numpy array at each iteration. The - default directory is the current directoy and the models are saved as - ``InversionModel-YYYY-MM-DD-HH-MM-iter.npy`` - """ - - def initialize(self): - print( - "simpeg.SaveModelEveryIteration will save your models as: " - "'{0!s}###-{1!s}.npy'".format(self.directory + os.path.sep, self.fileName) - ) - - def endIter(self): - np.save( - "{0!s}{1:03d}-{2!s}".format( - self.directory + os.path.sep, self.opt.iter, self.fileName - ), - self.opt.xc, - ) - - -class SaveOutputEveryIteration(SaveEveryIteration): - """SaveOutputEveryIteration""" - - def __init__(self, save_txt=True, **kwargs): - super().__init__(**kwargs) - - self.save_txt = save_txt - - @property - def save_txt(self): - """Whether to save the output as a text file. - - Returns - ------- - bool - """ - return self._save_txt - - @save_txt.setter - def save_txt(self, value): - self._save_txt = validate_type("save_txt", value, bool) - - def initialize(self): - if self.save_txt is True: - print( - "simpeg.SaveOutputEveryIteration will save your inversion " - "progress as: '###-{0!s}.txt'".format(self.fileName) - ) - f = open(self.fileName + ".txt", "w") - header = " # beta phi_d phi_m phi_m_small phi_m_smoomth_x phi_m_smoomth_y phi_m_smoomth_z phi\n" - f.write(header) - f.close() - - # Create a list of each - - self.beta = [] - self.phi_d = [] - self.phi_m = [] - self.phi_m_small = [] - self.phi_m_smooth_x = [] - self.phi_m_smooth_y = [] - self.phi_m_smooth_z = [] - self.phi = [] - - def endIter(self): - phi_s, phi_x, phi_y, phi_z = 0, 0, 0, 0 - - for reg in self.reg.objfcts: - if isinstance(reg, Sparse): - i_s, i_x, i_y, i_z = 0, 1, 2, 3 - else: - i_s, i_x, i_y, i_z = 0, 1, 3, 5 - if getattr(reg, "alpha_s", None): - phi_s += reg.objfcts[i_s](self.invProb.model) * reg.alpha_s - if getattr(reg, "alpha_x", None): - phi_x += reg.objfcts[i_x](self.invProb.model) * reg.alpha_x - - if reg.regularization_mesh.dim > 1 and getattr(reg, "alpha_y", None): - phi_y += reg.objfcts[i_y](self.invProb.model) * reg.alpha_y - if reg.regularization_mesh.dim > 2 and getattr(reg, "alpha_z", None): - phi_z += reg.objfcts[i_z](self.invProb.model) * reg.alpha_z - - self.beta.append(self.invProb.beta) - self.phi_d.append(self.invProb.phi_d) - self.phi_m.append(self.invProb.phi_m) - self.phi_m_small.append(phi_s) - self.phi_m_smooth_x.append(phi_x) - self.phi_m_smooth_y.append(phi_y) - self.phi_m_smooth_z.append(phi_z) - self.phi.append(self.opt.f) - - if self.save_txt: - f = open(self.fileName + ".txt", "a") - f.write( - " {0:3d} {1:1.4e} {2:1.4e} {3:1.4e} {4:1.4e} {5:1.4e} " - "{6:1.4e} {7:1.4e} {8:1.4e}\n".format( - self.opt.iter, - self.beta[self.opt.iter - 1], - self.phi_d[self.opt.iter - 1], - self.phi_m[self.opt.iter - 1], - self.phi_m_small[self.opt.iter - 1], - self.phi_m_smooth_x[self.opt.iter - 1], - self.phi_m_smooth_y[self.opt.iter - 1], - self.phi_m_smooth_z[self.opt.iter - 1], - self.phi[self.opt.iter - 1], - ) - ) - f.close() - - def load_results(self): - results = np.loadtxt(self.fileName + str(".txt"), comments="#") - self.beta = results[:, 1] - self.phi_d = results[:, 2] - self.phi_m = results[:, 3] - self.phi_m_small = results[:, 4] - self.phi_m_smooth_x = results[:, 5] - self.phi_m_smooth_y = results[:, 6] - self.phi_m_smooth_z = results[:, 7] - - self.phi_m_smooth = ( - self.phi_m_smooth_x + self.phi_m_smooth_y + self.phi_m_smooth_z - ) - - self.f = results[:, 7] - - self.target_misfit = self.invProb.dmisfit.simulation.survey.nD - self.i_target = None - - if self.invProb.phi_d < self.target_misfit: - i_target = 0 - while self.phi_d[i_target] > self.target_misfit: - i_target += 1 - self.i_target = i_target - - def plot_misfit_curves( - self, - fname=None, - dpi=300, - plot_small_smooth=False, - plot_phi_m=True, - plot_small=False, - plot_smooth=False, - ): - self.target_misfit = np.sum([dmis.nD for dmis in self.invProb.dmisfit.objfcts]) - self.i_target = None - - if self.invProb.phi_d < self.target_misfit: - i_target = 0 - while self.phi_d[i_target] > self.target_misfit: - i_target += 1 - self.i_target = i_target - - fig = plt.figure(figsize=(5, 2)) - ax = plt.subplot(111) - ax_1 = ax.twinx() - ax.semilogy( - np.arange(len(self.phi_d)), self.phi_d, "k-", lw=2, label=r"$\phi_d$" - ) - - if plot_phi_m: - ax_1.semilogy( - np.arange(len(self.phi_d)), self.phi_m, "r", lw=2, label=r"$\phi_m$" - ) - - if plot_small_smooth or plot_small: - ax_1.semilogy( - np.arange(len(self.phi_d)), self.phi_m_small, "ro", label="small" - ) - if plot_small_smooth or plot_smooth: - ax_1.semilogy( - np.arange(len(self.phi_d)), self.phi_m_smooth_x, "rx", label="smooth_x" - ) - ax_1.semilogy( - np.arange(len(self.phi_d)), self.phi_m_smooth_y, "rx", label="smooth_y" - ) - ax_1.semilogy( - np.arange(len(self.phi_d)), self.phi_m_smooth_z, "rx", label="smooth_z" - ) - - ax.legend(loc=1) - ax_1.legend(loc=2) - - ax.plot( - np.r_[ax.get_xlim()[0], ax.get_xlim()[1]], - np.ones(2) * self.target_misfit, - "k:", - ) - ax.set_xlabel("Iteration") - ax.set_ylabel(r"$\phi_d$") - ax_1.set_ylabel(r"$\phi_m$", color="r") - ax_1.tick_params(axis="y", which="both", colors="red") - - plt.show() - if fname is not None: - fig.savefig(fname, dpi=dpi) - - def plot_tikhonov_curves(self, fname=None, dpi=200): - self.target_misfit = self.invProb.dmisfit.simulation.survey.nD - self.i_target = None - - if self.invProb.phi_d < self.target_misfit: - i_target = 0 - while self.phi_d[i_target] > self.target_misfit: - i_target += 1 - self.i_target = i_target - - fig = plt.figure(figsize=(5, 8)) - ax1 = plt.subplot(311) - ax2 = plt.subplot(312) - ax3 = plt.subplot(313) - - ax1.plot(self.beta, self.phi_d, "k-", lw=2, ms=4) - ax1.set_xlim(np.hstack(self.beta).min(), np.hstack(self.beta).max()) - ax1.set_xlabel(r"$\beta$", fontsize=14) - ax1.set_ylabel(r"$\phi_d$", fontsize=14) - - ax2.plot(self.beta, self.phi_m, "k-", lw=2) - ax2.set_xlim(np.hstack(self.beta).min(), np.hstack(self.beta).max()) - ax2.set_xlabel(r"$\beta$", fontsize=14) - ax2.set_ylabel(r"$\phi_m$", fontsize=14) - - ax3.plot(self.phi_m, self.phi_d, "k-", lw=2) - ax3.set_xlim(np.hstack(self.phi_m).min(), np.hstack(self.phi_m).max()) - ax3.set_xlabel(r"$\phi_m$", fontsize=14) - ax3.set_ylabel(r"$\phi_d$", fontsize=14) - - if self.i_target is not None: - ax1.plot(self.beta[self.i_target], self.phi_d[self.i_target], "k*", ms=10) - ax2.plot(self.beta[self.i_target], self.phi_m[self.i_target], "k*", ms=10) - ax3.plot(self.phi_m[self.i_target], self.phi_d[self.i_target], "k*", ms=10) - - for ax in [ax1, ax2, ax3]: - ax.set_xscale("linear") - ax.set_yscale("linear") - plt.tight_layout() - plt.show() - if fname is not None: - fig.savefig(fname, dpi=dpi) - - -class SaveOutputDictEveryIteration(SaveEveryIteration): - """ - Saves inversion parameters at every iteration. - """ - - # Initialize the output dict - def __init__(self, saveOnDisk=False, **kwargs): - super().__init__(**kwargs) - self.saveOnDisk = saveOnDisk - - @property - def saveOnDisk(self): - """Whether to save the output dict to disk. - - Returns - ------- - bool - """ - return self._saveOnDisk - - @saveOnDisk.setter - def saveOnDisk(self, value): - self._saveOnDisk = validate_type("saveOnDisk", value, bool) - - def initialize(self): - self.outDict = {} - if self.saveOnDisk: - print( - "simpeg.SaveOutputDictEveryIteration will save your inversion progress as dictionary: '###-{0!s}.npz'".format( - self.fileName - ) - ) - - def endIter(self): - # regCombo = ["phi_ms", "phi_msx"] - - # if self.simulation[0].mesh.dim >= 2: - # regCombo += ["phi_msy"] - - # if self.simulation[0].mesh.dim == 3: - # regCombo += ["phi_msz"] - - # Initialize the output dict - iterDict = {} - - # Save the data. - iterDict["iter"] = self.opt.iter - iterDict["beta"] = self.invProb.beta - iterDict["phi_d"] = self.invProb.phi_d - iterDict["phi_m"] = self.invProb.phi_m - - # for label, fcts in zip(regCombo, self.reg.objfcts[0].objfcts): - # iterDict[label] = fcts(self.invProb.model) - - iterDict["f"] = self.opt.f - iterDict["m"] = self.invProb.model - iterDict["dpred"] = self.invProb.dpred - - for reg in self.reg.objfcts: - if isinstance(reg, Sparse): - for reg_part, norm in zip(reg.objfcts, reg.norms): - reg_name = f"{type(reg_part).__name__}" - if hasattr(reg_part, "orientation"): - reg_name = reg_part.orientation + " " + reg_name - iterDict[reg_name + ".irls_threshold"] = reg_part.irls_threshold - iterDict[reg_name + ".norm"] = norm - - # Save the file as a npz - if self.saveOnDisk: - np.savez("{:03d}-{:s}".format(self.opt.iter, self.fileName), iterDict) - - self.outDict[self.opt.iter] = iterDict - - -class Update_IRLS(InversionDirective): - f_old = 0 - f_min_change = 1e-2 - beta_tol = 1e-1 - beta_ratio_l2 = None - prctile = 100 - chifact_start = 1.0 - chifact_target = 1.0 - - # Solving parameter for IRLS (mode:2) - irls_iteration = 0 - minGNiter = 1 - iterStart = 0 - sphericalDomain = False - - # Beta schedule - ComboObjFun = False - mode = 1 - coolEpsOptimized = True - coolEps_p = True - coolEps_q = True - floorEps_p = 1e-8 - floorEps_q = 1e-8 - coolEpsFact = 1.2 - silent = False - fix_Jmatrix = False - - def __init__( - self, - max_irls_iterations=20, - update_beta=True, - beta_search=False, - coolingFactor=2.0, - coolingRate=1, - **kwargs, - ): - super().__init__(**kwargs) - self.max_irls_iterations = max_irls_iterations - self.update_beta = update_beta - self.beta_search = beta_search - self.coolingFactor = coolingFactor - self.coolingRate = coolingRate - - @property - def max_irls_iterations(self): - """Maximum irls iterations. - - Returns - ------- - int - """ - return self._max_irls_iterations - - @max_irls_iterations.setter - def max_irls_iterations(self, value): - self._max_irls_iterations = validate_integer( - "max_irls_iterations", value, min_val=0 - ) - - @property - def coolingFactor(self): - """Beta is divided by this value every `coolingRate` iterations. - - Returns - ------- - float - """ - return self._coolingFactor - - @coolingFactor.setter - def coolingFactor(self, value): - self._coolingFactor = validate_float( - "coolingFactor", value, min_val=0.0, inclusive_min=False - ) - - @property - def coolingRate(self): - """Cool after this number of iterations. - - Returns - ------- - int - """ - return self._coolingRate - - @coolingRate.setter - def coolingRate(self, value): - self._coolingRate = validate_integer("coolingRate", value, min_val=1) - - @property - def update_beta(self): - """Whether to update beta. - - Returns - ------- - bool - """ - return self._update_beta - - @update_beta.setter - def update_beta(self, value): - self._update_beta = validate_type("update_beta", value, bool) - - @property - def beta_search(self): - """Whether to do a beta search. - - Returns - ------- - bool - """ - return self._beta_search - - @beta_search.setter - def beta_search(self, value): - self._beta_search = validate_type("beta_search", value, bool) - - @property - def target(self): - if getattr(self, "_target", None) is None: - nD = 0 - for survey in self.survey: - nD += survey.nD - - self._target = nD * self.chifact_target - - return self._target - - @target.setter - def target(self, val): - self._target = val - - @property - def start(self): - if getattr(self, "_start", None) is None: - if isinstance(self.survey, list): - self._start = 0 - for survey in self.survey: - self._start += survey.nD * self.chifact_start - - else: - self._start = self.survey.nD * self.chifact_start - return self._start - - @start.setter - def start(self, val): - self._start = val - - def initialize(self): - if self.mode == 1: - self.norms = [] - for reg in self.reg.objfcts: - if not isinstance(reg, Sparse): - continue - self.norms.append(reg.norms) - reg.norms = [2.0 for obj in reg.objfcts] - reg.model = self.invProb.model - - # Update the model used by the regularization - for reg in self.reg.objfcts: - if not isinstance(reg, Sparse): - continue - - reg.model = self.invProb.model - - if self.sphericalDomain: - self.angleScale() - - def endIter(self): - if self.sphericalDomain: - self.angleScale() - - # Check if misfit is within the tolerance, otherwise scale beta - if np.all( - [ - np.abs(1.0 - self.invProb.phi_d / self.target) > self.beta_tol, - self.update_beta, - self.mode != 1, - ] - ): - ratio = self.target / self.invProb.phi_d - - if ratio > 1: - ratio = np.mean([2.0, ratio]) - else: - ratio = np.mean([0.75, ratio]) - - self.invProb.beta = self.invProb.beta * ratio - - if np.all([self.mode != 1, self.beta_search]): - print("Beta search step") - # self.update_beta = False - # Re-use previous model and continue with new beta - self.invProb.model = self.reg.objfcts[0].model - self.opt.xc = self.reg.objfcts[0].model - self.opt.iter -= 1 - return - - elif np.all([self.mode == 1, self.opt.iter % self.coolingRate == 0]): - self.invProb.beta = self.invProb.beta / self.coolingFactor - - # After reaching target misfit with l2-norm, switch to IRLS (mode:2) - if np.all([self.invProb.phi_d < self.start, self.mode == 1]): - self.start_irls() - - # Only update after GN iterations - if np.all( - [(self.opt.iter - self.iterStart) % self.minGNiter == 0, self.mode != 1] - ): - if self.stopping_criteria(): - self.opt.stopNextIteration = True - return - - # Print to screen - for reg in self.reg.objfcts: - if not isinstance(reg, Sparse): - continue - - for obj in reg.objfcts: - if isinstance(reg, (Sparse, BaseSparse)): - obj.irls_threshold = obj.irls_threshold / self.coolEpsFact - - self.irls_iteration += 1 - - # Reset the regularization matrices so that it is - # recalculated for current model. Do it to all levels of comboObj - for reg in self.reg.objfcts: - if not isinstance(reg, Sparse): - continue - - reg.update_weights(reg.model) - - self.update_beta = True - self.invProb.phi_m_last = self.reg(self.invProb.model) - - def start_irls(self): - if not self.silent: - print( - "Reached starting chifact with l2-norm regularization:" - + " Start IRLS steps..." - ) - - self.mode = 2 - - if getattr(self.opt, "iter", None) is None: - self.iterStart = 0 - else: - self.iterStart = self.opt.iter - - self.invProb.phi_m_last = self.reg(self.invProb.model) - - # Either use the supplied irls_threshold, or fix base on distribution of - # model values - for reg in self.reg.objfcts: - if not isinstance(reg, Sparse): - continue - - for obj in reg.objfcts: - threshold = np.percentile( - np.abs(obj.mapping * obj._delta_m(self.invProb.model)), self.prctile - ) - if isinstance(obj, SmoothnessFirstOrder): - threshold /= reg.regularization_mesh.base_length - - obj.irls_threshold = threshold - - # Re-assign the norms supplied by user l2 -> lp - for reg, norms in zip(self.reg.objfcts, self.norms): - if not isinstance(reg, Sparse): - continue - reg.norms = norms - - # Save l2-model - self.invProb.l2model = self.invProb.model.copy() - - # Print to screen - for reg in self.reg.objfcts: - if not isinstance(reg, Sparse): - continue - if not self.silent: - print("irls_threshold " + str(reg.objfcts[0].irls_threshold)) - - def angleScale(self): - """ - Update the scales used by regularization for the - different block of models - """ - # Currently implemented for MVI-S only - max_p = [] - for reg in self.reg.objfcts[0].objfcts: - f_m = abs(reg.f_m(reg.model)) - max_p += [np.max(f_m)] - - max_p = np.asarray(max_p).max() - - max_s = [np.pi, np.pi] - - for reg, var in zip(self.reg.objfcts[1:], max_s): - for obj in reg.objfcts: - # TODO Need to make weights_shapes a public method - obj.set_weights( - angle_scale=np.ones(obj._weights_shapes[0]) * max_p / var - ) - - def validate(self, directiveList): - dList = directiveList.dList - self_ind = dList.index(self) - lin_precond_ind = [isinstance(d, UpdatePreconditioner) for d in dList] - - if any(lin_precond_ind): - assert lin_precond_ind.index(True) > self_ind, ( - "The directive 'UpdatePreconditioner' must be after Update_IRLS " - "in the directiveList" - ) - else: - warnings.warn( - "Without a Linear preconditioner, convergence may be slow. " - "Consider adding `Directives.UpdatePreconditioner` to your " - "directives list", - stacklevel=2, - ) - return True - - def stopping_criteria(self): - """ - Check for stopping criteria of max_irls_iteration or minimum change. - """ - phim_new = 0 - for reg in self.reg.objfcts: - if isinstance(reg, (Sparse, BaseSparse)): - reg.model = self.invProb.model - phim_new += reg(reg.model) - - # Check for maximum number of IRLS cycles1 - if self.irls_iteration == self.max_irls_iterations: - if not self.silent: - print( - "Reach maximum number of IRLS cycles:" - + " {0:d}".format(self.max_irls_iterations) - ) - return True - - # Check if the function has changed enough - f_change = np.abs(self.f_old - phim_new) / (self.f_old + 1e-12) - if np.all( - [ - f_change < self.f_min_change, - self.irls_iteration > 1, - np.abs(1.0 - self.invProb.phi_d / self.target) < self.beta_tol, - ] - ): - print("Minimum decrease in regularization." + "End of IRLS") - return True - - self.f_old = phim_new - - return False - - -class UpdatePreconditioner(InversionDirective): - """ - Create a Jacobi preconditioner for the linear problem - """ - - def __init__(self, update_every_iteration=True, **kwargs): - super().__init__(**kwargs) - self.update_every_iteration = update_every_iteration - - @property - def update_every_iteration(self): - """Whether to update the preconditioner at every iteration. - - Returns - ------- - bool - """ - return self._update_every_iteration - - @update_every_iteration.setter - def update_every_iteration(self, value): - self._update_every_iteration = validate_type( - "update_every_iteration", value, bool - ) - - def initialize(self): - # Create the pre-conditioner - regDiag = np.zeros_like(self.invProb.model) - m = self.invProb.model - - for reg in self.reg.objfcts: - # Check if regularization has a projection - rdg = reg.deriv2(m) - if not isinstance(rdg, Zero): - regDiag += rdg.diagonal() - - JtJdiag = np.zeros_like(self.invProb.model) - for sim, dmisfit in zip(self.simulation, self.dmisfit.objfcts): - if getattr(sim, "getJtJdiag", None) is None: - assert getattr(sim, "getJ", None) is not None, ( - "Simulation does not have a getJ attribute." - + "Cannot form the sensitivity explicitly" - ) - JtJdiag += np.sum(np.power((dmisfit.W * sim.getJ(m)), 2), axis=0) - else: - JtJdiag += sim.getJtJdiag(m, W=dmisfit.W) - - diagA = JtJdiag + self.invProb.beta * regDiag - diagA[diagA != 0] = diagA[diagA != 0] ** -1.0 - PC = sdiag((diagA)) - - self.opt.approxHinv = PC - - def endIter(self): - # Cool the threshold parameter - if self.update_every_iteration is False: - return - - # Create the pre-conditioner - regDiag = np.zeros_like(self.invProb.model) - m = self.invProb.model - - for reg in self.reg.objfcts: - # Check if he has wire - regDiag += reg.deriv2(m).diagonal() - - JtJdiag = np.zeros_like(self.invProb.model) - for sim, dmisfit in zip(self.simulation, self.dmisfit.objfcts): - if getattr(sim, "getJtJdiag", None) is None: - assert getattr(sim, "getJ", None) is not None, ( - "Simulation does not have a getJ attribute." - + "Cannot form the sensitivity explicitly" - ) - JtJdiag += np.sum(np.power((dmisfit.W * sim.getJ(m)), 2), axis=0) - else: - JtJdiag += sim.getJtJdiag(m, W=dmisfit.W) - - diagA = JtJdiag + self.invProb.beta * regDiag - diagA[diagA != 0] = diagA[diagA != 0] ** -1.0 - PC = sdiag((diagA)) - self.opt.approxHinv = PC - - -class Update_Wj(InversionDirective): - """ - Create approx-sensitivity base weighting using the probing method - """ - - def __init__(self, k=None, itr=None, **kwargs): - self.k = k - self.itr = itr - super().__init__(**kwargs) - - @property - def k(self): - """Number of probing cycles for the estimator. - - Returns - ------- - int - """ - return self._k - - @k.setter - def k(self, value): - if value is not None: - value = validate_integer("k", value, min_val=1) - self._k = value - - @property - def itr(self): - """Which iteration to update the sensitivity. - - Will always update if `None`. - - Returns - ------- - int or None - """ - return self._itr - - @itr.setter - def itr(self, value): - if value is not None: - value = validate_integer("itr", value, min_val=1) - self._itr = value - - def endIter(self): - if self.itr is None or self.itr == self.opt.iter: - m = self.invProb.model - if self.k is None: - self.k = int(self.survey.nD / 10) - - def JtJv(v): - Jv = self.simulation.Jvec(m, v) - - return self.simulation.Jtvec(m, Jv) - - JtJdiag = estimate_diagonal(JtJv, len(m), k=self.k) - JtJdiag = JtJdiag / max(JtJdiag) - - self.reg.wght = JtJdiag - - -class UpdateSensitivityWeights(InversionDirective): - r""" - Sensitivity weighting for linear and non-linear least-squares inverse problems. - - This directive computes the root-mean squared sensitivities for the - forward simulation(s) attached to the inverse problem, then truncates - and scales the result to create cell weights which are applied in the regularization. - The underlying theory is provided below in the `Notes` section. - - This directive **requires** that the map for the regularization function is either - class:`simpeg.maps.Wires` or class:`simpeg.maps.Identity`. In other words, the - sensitivity weighting cannot be applied for parametric inversion. In addition, - the simulation(s) connected to the inverse problem **must** have a ``getJ`` or - ``getJtJdiag`` method. - - This directive's place in the :class:`DirectivesList` **must** be - before any directives which update the preconditioner for the inverse problem - (i.e. :class:`UpdatePreconditioner`), and **must** be before any directives that - estimate the starting trade-off parameter (i.e. :class:`EstimateBeta_ByEig` - and :class:`EstimateBetaMaxDerivative`). - - Parameters - ---------- - every_iteration : bool - When ``True``, update sensitivity weighting at every model update; non-linear problems. - When ``False``, create sensitivity weights for starting model only; linear problems. - threshold : float - Threshold value for smallest weighting value. - threshold_method : {'amplitude', 'global', 'percentile'} - Threshold method for how `threshold_value` is applied: - - - amplitude: - the smallest root-mean squared sensitivity is a fractional percent of the largest value; must be between 0 and 1. - - global: - `threshold_value` is added to the cell weights prior to normalization; must be greater than 0. - - percentile: - the smallest root-mean squared sensitivity is set using percentile threshold; must be between 0 and 100. - - normalization_method : {'maximum', 'min_value', None} - Normalization method applied to sensitivity weights. - - Options are: - - - maximum: - sensitivity weights are normalized by the largest value such that the largest weight is equal to 1. - - minimum: - sensitivity weights are normalized by the smallest value, after thresholding, such that the smallest weights are equal to 1. - - ``None``: - normalization is not applied. - - Notes - ----- - Let :math:`\mathbf{J}` represent the Jacobian. To create sensitivity weights, root-mean squared (RMS) sensitivities - :math:`\mathbf{s}` are computed by summing the squares of the rows of the Jacobian: - - .. math:: - \mathbf{s} = \Bigg [ \sum_i \, \mathbf{J_{i, \centerdot }}^2 \, \Bigg ]^{1/2} - - The dynamic range of RMS sensitivities can span many orders of magnitude. When computing sensitivity - weights, thresholding is generally applied to set a minimum value. - - Thresholding - ^^^^^^^^^^^^ - - If **global** thresholding is applied, we add a constant :math:`\tau` to the RMS sensitivities: - - .. math:: - \mathbf{\tilde{s}} = \mathbf{s} + \tau - - In the case of **percentile** thresholding, we let :math:`s_{\%}` represent a given percentile. - Thresholding to set a minimum value is applied as follows: - - .. math:: - \tilde{s}_j = \begin{cases} - s_j \;\; for \;\; s_j \geq s_{\%} \\ - s_{\%} \;\; for \;\; s_j < s_{\%} - \end{cases} - - If **absolute** thresholding is applied, we define :math:`\eta` as a fractional percent. - In this case, thresholding is applied as follows: - - .. math:: - \tilde{s}_j = \begin{cases} - s_j \;\; for \;\; s_j \geq \eta s_{max} \\ - \eta s_{max} \;\; for \;\; s_j < \eta s_{max} - \end{cases} - """ - - def __init__( - self, - every_iteration=False, - threshold_value=1e-12, - threshold_method="amplitude", - normalization_method="maximum", - **kwargs, - ): - # Raise errors on deprecated arguments - if (key := "everyIter") in kwargs.keys(): - raise TypeError( - f"'{key}' property has been removed. Please use 'every_iteration'.", - ) - if (key := "threshold") in kwargs.keys(): - raise TypeError( - f"'{key}' property has been removed. Please use 'threshold_value'.", - ) - if (key := "normalization") in kwargs.keys(): - raise TypeError( - f"'{key}' property has been removed. " - "Please define normalization using 'normalization_method'.", - ) - - super().__init__(**kwargs) - - self.every_iteration = every_iteration - self.threshold_value = threshold_value - self.threshold_method = threshold_method - self.normalization_method = normalization_method - - @property - def every_iteration(self): - """Update sensitivity weights when model is updated. - - When ``True``, update sensitivity weighting at every model update; non-linear problems. - When ``False``, create sensitivity weights for starting model only; linear problems. - - Returns - ------- - bool - """ - return self._every_iteration - - @every_iteration.setter - def every_iteration(self, value): - self._every_iteration = validate_type("every_iteration", value, bool) - - everyIter = deprecate_property( - every_iteration, - "everyIter", - "every_iteration", - removal_version="0.20.0", - error=True, - ) - - @property - def threshold_value(self): - """Threshold value used to set minimum weighting value. - - The way thresholding is applied to the weighting model depends on the - `threshold_method` property. The choices for `threshold_method` are: - - - global: - `threshold_value` is added to the cell weights prior to normalization; must be greater than 0. - - percentile: - `threshold_value` is a percentile cutoff; must be between 0 and 100 - - amplitude: - `threshold_value` is the fractional percent of the largest value; must be between 0 and 1 - - - Returns - ------- - float - """ - return self._threshold_value - - @threshold_value.setter - def threshold_value(self, value): - self._threshold_value = validate_float("threshold_value", value, min_val=0.0) - - threshold = deprecate_property( - threshold_value, - "threshold", - "threshold_value", - removal_version="0.20.0", - error=True, - ) - - @property - def threshold_method(self): - """Threshold method for how `threshold_value` is applied: - - - global: - `threshold_value` is added to the cell weights prior to normalization; must be greater than 0. - - percentile: - the smallest root-mean squared sensitivity is set using percentile threshold; must be between 0 and 100 - - amplitude: - the smallest root-mean squared sensitivity is a fractional percent of the largest value; must be between 0 and 1 - - - Returns - ------- - str - """ - return self._threshold_method - - @threshold_method.setter - def threshold_method(self, value): - self._threshold_method = validate_string( - "threshold_method", value, string_list=["global", "percentile", "amplitude"] - ) - - @property - def normalization_method(self): - """Normalization method applied to sensitivity weights. - - Options are: - - - ``None`` - normalization is not applied - - maximum: - sensitivity weights are normalized by the largest value such that the largest weight is equal to 1. - - minimum: - sensitivity weights are normalized by the smallest value, after thresholding, such that the smallest weights are equal to 1. - - Returns - ------- - None, str - """ - return self._normalization_method - - @normalization_method.setter - def normalization_method(self, value): - if value is None: - self._normalization_method = value - else: - self._normalization_method = validate_string( - "normalization_method", value, string_list=["minimum", "maximum"] - ) - - normalization = deprecate_property( - normalization_method, - "normalization", - "normalization_method", - removal_version="0.20.0", - error=True, - ) - - def initialize(self): - """Compute sensitivity weights upon starting the inversion.""" - for reg in self.reg.objfcts: - if not isinstance(reg.mapping, (IdentityMap, Wires)): - raise TypeError( - f"Mapping for the regularization must be of type {IdentityMap} or {Wires}. " - + f"Input mapping of type {type(reg.mapping)}." - ) - - self.update() - - def endIter(self): - """Execute end of iteration.""" - - if self.every_iteration: - self.update() - - def update(self): - """Update sensitivity weights""" - - jtj_diag = np.zeros_like(self.invProb.model) - m = self.invProb.model - - for sim, dmisfit in zip(self.simulation, self.dmisfit.objfcts): - if getattr(sim, "getJtJdiag", None) is None: - if getattr(sim, "getJ", None) is None: - raise AttributeError( - "Simulation does not have a getJ attribute." - + "Cannot form the sensitivity explicitly" - ) - jtj_diag += mkvc(np.sum((dmisfit.W * sim.getJ(m)) ** 2.0, axis=0)) - else: - jtj_diag += sim.getJtJdiag(m, W=dmisfit.W) - - # Compute and sum root-mean squared sensitivities for all objective functions - wr = np.zeros_like(self.invProb.model) - for reg in self.reg.objfcts: - if isinstance(reg, BaseSimilarityMeasure): - continue - - mesh = reg.regularization_mesh - n_cells = mesh.nC - mapped_jtj_diag = reg.mapping * jtj_diag - # reshape the mapped, so you can divide by volume - # (let's say it was a vector or anisotropic model) - mapped_jtj_diag = mapped_jtj_diag.reshape((n_cells, -1), order="F") - wr_temp = mapped_jtj_diag / reg.regularization_mesh.vol[:, None] ** 2.0 - wr_temp = wr_temp.reshape(-1, order="F") - - wr += reg.mapping.deriv(self.invProb.model).T * wr_temp - - wr **= 0.5 - - # Apply thresholding - if self.threshold_method == "global": - wr += self.threshold_value - elif self.threshold_method == "percentile": - wr = np.clip( - wr, a_min=np.percentile(wr, self.threshold_value), a_max=np.inf - ) - else: - wr = np.clip(wr, a_min=self.threshold_value * wr.max(), a_max=np.inf) - - # Apply normalization - if self.normalization_method == "maximum": - wr /= wr.max() - elif self.normalization_method == "minimum": - wr /= wr.min() - - # Add sensitivity weighting to all model objective functions - for reg in self.reg.objfcts: - if not isinstance(reg, BaseSimilarityMeasure): - sub_regs = getattr(reg, "objfcts", [reg]) - for sub_reg in sub_regs: - sub_reg.set_weights(sensitivity=sub_reg.mapping * wr) - - def validate(self, directiveList): - """Validate directive against directives list. - - The ``UpdateSensitivityWeights`` directive impacts the regularization by applying - cell weights. As a result, its place in the :class:`DirectivesList` must be - before any directives which update the preconditioner for the inverse problem - (i.e. :class:`UpdatePreconditioner`), and must be before any directives that - estimate the starting trade-off parameter (i.e. :class:`EstimateBeta_ByEig` - and :class:`EstimateBetaMaxDerivative`). - - - Returns - ------- - bool - Returns ``True`` if validation passes. Otherwise, an error is thrown. - """ - # check if a beta estimator is in the list after setting the weights - dList = directiveList.dList - self_ind = dList.index(self) - - beta_estimator_ind = [isinstance(d, BaseBetaEstimator) for d in dList] - lin_precond_ind = [isinstance(d, UpdatePreconditioner) for d in dList] - - if any(beta_estimator_ind): - assert beta_estimator_ind.index(True) > self_ind, ( - "The directive for setting intial beta must be after UpdateSensitivityWeights " - "in the directiveList" - ) - - if any(lin_precond_ind): - assert lin_precond_ind.index(True) > self_ind, ( - "The directive 'UpdatePreconditioner' must be after UpdateSensitivityWeights " - "in the directiveList" - ) - - return True - - -class ProjectSphericalBounds(InversionDirective): - r""" - Trick for spherical coordinate system. - Project :math:`\theta` and :math:`\phi` angles back to :math:`[-\pi,\pi]` - using back and forth conversion. - spherical->cartesian->spherical - """ - - def initialize(self): - x = self.invProb.model - # Convert to cartesian than back to avoid over rotation - nC = int(len(x) / 3) - - xyz = spherical2cartesian(x.reshape((nC, 3), order="F")) - m = cartesian2spherical(xyz.reshape((nC, 3), order="F")) - - self.invProb.model = m - - for sim in self.simulation: - sim.model = m - - self.opt.xc = m - - def endIter(self): - x = self.invProb.model - nC = int(len(x) / 3) - - # Convert to cartesian than back to avoid over rotation - xyz = spherical2cartesian(x.reshape((nC, 3), order="F")) - m = cartesian2spherical(xyz.reshape((nC, 3), order="F")) - - self.invProb.model = m - - phi_m_last = [] - for reg in self.reg.objfcts: - reg.model = self.invProb.model - phi_m_last += [reg(self.invProb.model)] - - self.invProb.phi_m_last = phi_m_last - - for sim in self.simulation: - sim.model = m - - self.opt.xc = m +import warnings +from ._directives import * # noqa: F403,F401 + +warnings.warn( + "The `simpeg.directives.directives` submodule has been deprecated, " + "and will be removed in SimPEG v0.26.0." + "Import any directive class directly from the `simpeg.directives` module. " + "E.g.: `from simpeg.directives import BetaSchedule`", + FutureWarning, + stacklevel=2, +) diff --git a/simpeg/directives/pgi_directives.py b/simpeg/directives/pgi_directives.py index 60f4488b90..ec0108210c 100644 --- a/simpeg/directives/pgi_directives.py +++ b/simpeg/directives/pgi_directives.py @@ -1,474 +1,18 @@ -############################################################################### -# # -# Directives for PGI: Petrophysically guided Regularization # -# # -############################################################################### - -import copy - -import numpy as np - -from ..directives import InversionDirective, MultiTargetMisfits -from ..regularization import ( - PGI, - PGIsmallness, - SmoothnessFirstOrder, - SparseSmoothness, -) -from ..utils import ( - GaussianMixtureWithNonlinearRelationships, - GaussianMixtureWithNonlinearRelationshipsWithPrior, - GaussianMixtureWithPrior, - WeightedGaussianMixture, - mkvc, +""" +Backward compatibility with the ``simpeg.directives.pgi_directives`` submodule. + +This file will be deleted when the ``simpeg.directives.pgi_directives`` submodule is +removed. +""" + +import warnings +from ._pgi_directives import * # noqa: F403,F401 + +warnings.warn( + "The `simpeg.directives.pgi_directives` submodule has been deprecated, " + "and will be removed in SimPEG v0.26.0." + "Import any directive class directly from the `simpeg.directives` module. " + "E.g.: `from simpeg.directives import PGI_UpdateParameters`. ", + FutureWarning, + stacklevel=2, ) - - -class PGI_UpdateParameters(InversionDirective): - """ - This directive is to be used with regularization from regularization.pgi. - It updates: - - the reference model and weights in the smallness (L2-approximation of PGI) - - the GMM as a MAP estimate between the prior and the current model - For more details, please consult: - - https://doi.org/10.1093/gji/ggz389 - """ - - verbose = False # print info. about the GMM at each iteration - update_rate = 1 # updates at each `update_rate` iterations - update_gmm = False # update the GMM - zeta = ( - 1e10 # confidence in the prior proportions; default: high value, keep GMM fixed - ) - nu = ( - 1e10 # confidence in the prior covariances; default: high value, keep GMM fixed - ) - kappa = 1e10 # confidence in the prior means;default: high value, keep GMM fixed - update_covariances = ( - True # Average the covariances, If false: average the precisions - ) - fixed_membership = None # keep the membership of specific cells fixed - keep_ref_fixed_in_Smooth = True # keep mref fixed in the Smoothness - - def initialize(self): - pgi_reg = self.reg.get_functions_of_type(PGIsmallness) - if len(pgi_reg) != 1: - raise UserWarning( - "'PGI_UpdateParameters' requires one 'PGIsmallness' regularization " - "in the objective function." - ) - self.pgi_reg = pgi_reg[0] - - def endIter(self): - if self.opt.iter > 0 and self.opt.iter % self.update_rate == 0: - m = self.invProb.model - modellist = self.pgi_reg.wiresmap * m - model = np.c_[[a * b for a, b in zip(self.pgi_reg.maplist, modellist)]].T - - if self.update_gmm and isinstance( - self.pgi_reg.gmmref, GaussianMixtureWithNonlinearRelationships - ): - clfupdate = GaussianMixtureWithNonlinearRelationshipsWithPrior( - gmmref=self.pgi_reg.gmmref, - zeta=self.zeta, - kappa=self.kappa, - nu=self.nu, - verbose=self.verbose, - prior_type="semi", - update_covariances=self.update_covariances, - max_iter=self.pgi_reg.gmm.max_iter, - n_init=self.pgi_reg.gmm.n_init, - reg_covar=self.pgi_reg.gmm.reg_covar, - weights_init=self.pgi_reg.gmm.weights_, - means_init=self.pgi_reg.gmm.means_, - precisions_init=self.pgi_reg.gmm.precisions_, - random_state=self.pgi_reg.gmm.random_state, - tol=self.pgi_reg.gmm.tol, - verbose_interval=self.pgi_reg.gmm.verbose_interval, - warm_start=self.pgi_reg.gmm.warm_start, - fixed_membership=self.fixed_membership, - ) - clfupdate = clfupdate.fit(model) - - elif self.update_gmm and isinstance( - self.pgi_reg.gmmref, WeightedGaussianMixture - ): - clfupdate = GaussianMixtureWithPrior( - gmmref=self.pgi_reg.gmmref, - zeta=self.zeta, - kappa=self.kappa, - nu=self.nu, - verbose=self.verbose, - prior_type="semi", - update_covariances=self.update_covariances, - max_iter=self.pgi_reg.gmm.max_iter, - n_init=self.pgi_reg.gmm.n_init, - reg_covar=self.pgi_reg.gmm.reg_covar, - weights_init=self.pgi_reg.gmm.weights_, - means_init=self.pgi_reg.gmm.means_, - precisions_init=self.pgi_reg.gmm.precisions_, - random_state=self.pgi_reg.gmm.random_state, - tol=self.pgi_reg.gmm.tol, - verbose_interval=self.pgi_reg.gmm.verbose_interval, - warm_start=self.pgi_reg.gmm.warm_start, - fixed_membership=self.fixed_membership, - ) - clfupdate = clfupdate.fit(model) - - else: - clfupdate = copy.deepcopy(self.pgi_reg.gmmref) - - self.pgi_reg.gmm = clfupdate - membership = self.pgi_reg.gmm.predict(model) - - if self.fixed_membership is not None: - membership[self.fixed_membership[:, 0]] = self.fixed_membership[:, 1] - - mref = mkvc(self.pgi_reg.gmm.means_[membership]) - self.pgi_reg.reference_model = mref - if getattr(self.fixed_membership, "shape", [0, 0])[0] < len(membership): - self.pgi_reg._r_second_deriv = None - - -class PGI_BetaAlphaSchedule(InversionDirective): - """ - This directive is to be used with regularizations from regularization.pgi. - It implements the strategy described in https://doi.org/10.1093/gji/ggz389 - for iteratively updating beta and alpha_s for fitting the - geophysical and smallness targets. - """ - - verbose = False # print information (progress, updates made) - tolerance = 0.0 # tolerance on the geophysical target misfit for cooling - progress = 0.1 # minimum percentage progress (default 10%) before cooling beta - coolingFactor = 2.0 # when cooled, beta is divided by it - warmingFactor = 1.0 # when warmed, alpha_s is multiplied by the ratio of the - # geophysical target with their current misfit, times this factor - mode = 1 # mode 1: start with nothing fitted. Mode 2: warmstart with fitted geophysical data - alphasmax = 1e10 # max alpha_s - betamin = 1e-10 # minimum beta - update_rate = 1 # update every `update_rate` iterations - pgi_reg = None - ratio_in_cooling = ( - False # add the ratio of geophysical misfit with their target in cooling - ) - - def initialize(self): - """Initialize the directive.""" - self.update_previous_score() - self.update_previous_dmlist() - - def endIter(self): - """Run after the end of each iteration in the inversion.""" - # Get some variables from the MultiTargetMisfits directive - data_misfits_achieved = self.multi_target_misfits_directive.DM - data_misfits_target = self.multi_target_misfits_directive.DMtarget - dmlist = self.multi_target_misfits_directive.dmlist - targetlist = self.multi_target_misfits_directive.targetlist - - # Change mode if data misfit targets have been achieved - if data_misfits_achieved: - self.mode = 2 - - # Don't cool beta of warm alpha if we are in the first iteration or if - # the current iteration doesn't match the update rate - if self.opt.iter == 0 or self.opt.iter % self.update_rate != 0: - self.update_previous_score() - self.update_previous_dmlist() - return None - - if self.verbose: - targets = np.round( - np.maximum( - (1.0 - self.progress) * self.previous_dmlist, - (1.0 + self.tolerance) * data_misfits_target, - ), - decimals=1, - ) - dmlist_rounded = np.round(dmlist, decimals=1) - print( - f"Beta cooling evaluation: progress: {dmlist_rounded}; " - f"minimum progress targets: {targets}" - ) - - # Decide if we should cool beta - threshold = np.maximum( - (1.0 - self.progress) * self.previous_dmlist[~targetlist], - data_misfits_target[~targetlist], - ) - if ( - (dmlist[~targetlist] > threshold).all() - and not data_misfits_achieved - and self.mode == 1 - and self.invProb.beta > self.betamin - ): - self.cool_beta() - if self.verbose: - print("Decreasing beta to counter data misfit decrase plateau.") - - # Decide if we should warm alpha instead - elif ( - data_misfits_achieved - and self.mode == 2 - and np.all(self.pgi_regularization.alpha_pgi < self.alphasmax) - ): - self.warm_alpha() - if self.verbose: - print( - "Warming alpha_pgi to favor clustering: ", - self.pgi_regularization.alpha_pgi, - ) - - # Decide if we should cool beta (to counter data misfit increase) - elif ( - np.any(dmlist > (1.0 + self.tolerance) * data_misfits_target) - and self.mode == 2 - and self.invProb.beta > self.betamin - ): - self.cool_beta() - if self.verbose: - print("Decreasing beta to counter data misfit increase.") - - # Update previous score and dmlist - self.update_previous_score() - self.update_previous_dmlist() - - def cool_beta(self): - """Cool beta according to schedule.""" - data_misfits_target = self.multi_target_misfits_directive.DMtarget - dmlist = self.multi_target_misfits_directive.dmlist - ratio = 1.0 - indx = dmlist > (1.0 + self.tolerance) * data_misfits_target - if np.any(indx) and self.ratio_in_cooling: - ratio = np.median([dmlist[indx] / data_misfits_target[indx]]) - self.invProb.beta /= self.coolingFactor * ratio - - def warm_alpha(self): - """Warm alpha according to schedule.""" - data_misfits_target = self.multi_target_misfits_directive.DMtarget - dmlist = self.multi_target_misfits_directive.dmlist - ratio = np.median(data_misfits_target / dmlist) - self.pgi_regularization.alpha_pgi *= self.warmingFactor * ratio - - def update_previous_score(self): - """ - Update the value of the ``previous_score`` attribute. - - Update it with the current value of the petrophysical misfit, obtained - from the :meth:`MultiTargetMisfit.phims()` method. - """ - self.previous_score = copy.deepcopy(self.multi_target_misfits_directive.phims()) - - def update_previous_dmlist(self): - """ - Update the value of the ``previous_dmlist`` attribute. - - Update it with the current value of the data misfits, obtained - from the :meth:`MultiTargetMisfit.dmlist` attribute. - """ - self.previous_dmlist = copy.deepcopy(self.multi_target_misfits_directive.dmlist) - - @property - def directives(self): - """List of all the directives in the :class:`simpeg.inverison.BaseInversion``.""" - return self.inversion.directiveList.dList - - @property - def multi_target_misfits_directive(self): - """``MultiTargetMisfit`` directive in the :class:`simpeg.inverison.BaseInversion``.""" - if not hasattr(self, "_mtm_directive"): - # Obtain multi target misfits directive from the directive list - multi_target_misfits_directive = [ - directive - for directive in self.directives - if isinstance(directive, MultiTargetMisfits) - ] - if not multi_target_misfits_directive: - raise UserWarning( - "No MultiTargetMisfits directive found in the current inversion. " - "A MultiTargetMisfits directive is needed by the " - "PGI_BetaAlphaSchedule directive." - ) - (self._mtm_directive,) = multi_target_misfits_directive - return self._mtm_directive - - @property - def pgi_update_params_directive(self): - """``PGI_UpdateParam``s directive in the :class:`simpeg.inverison.BaseInversion``.""" - if not hasattr(self, "_pgi_update_params"): - # Obtain PGI_UpdateParams directive from the directive list - pgi_update_params_directive = [ - directive - for directive in self.directives - if isinstance(directive, PGI_UpdateParameters) - ] - if pgi_update_params_directive: - (self._pgi_update_params,) = pgi_update_params_directive - else: - self._pgi_update_params = None - return self._pgi_update_params - - @property - def pgi_regularization(self): - """PGI regularization in the :class:`simpeg.inverse_problem.BaseInvProblem``.""" - if not hasattr(self, "_pgi_regularization"): - pgi_regularization = self.reg.get_functions_of_type(PGI) - if len(pgi_regularization) != 1: - raise UserWarning( - "'PGI_UpdateParameters' requires one 'PGI' regularization " - "in the objective function." - ) - self._pgi_regularization = pgi_regularization[0] - return self._pgi_regularization - - -class PGI_AddMrefInSmooth(InversionDirective): - """ - This directive is to be used with regularizations from regularization.pgi. - It implements the strategy described in https://doi.org/10.1093/gji/ggz389 - for including the learned reference model, once stable, in the smoothness terms. - """ - - # Chi factor for Data Misfit - chifact = 1.0 - tolerance_phid = 0.0 - phi_d_target = None - wait_till_stable = True - tolerance = 0.0 - verbose = False - - def initialize(self): - targetclass = np.r_[ - [ - isinstance(dirpart, MultiTargetMisfits) - for dirpart in self.inversion.directiveList.dList - ] - ] - if ~np.any(targetclass): - self.DMtarget = None - else: - self.targetclass = np.where(targetclass)[0][-1] - self._DMtarget = self.inversion.directiveList.dList[ - self.targetclass - ].DMtarget - - self.pgi_updategmm_class = np.r_[ - [ - isinstance(dirpart, PGI_UpdateParameters) - for dirpart in self.inversion.directiveList.dList - ] - ] - - if getattr(self.reg.objfcts[0], "objfcts", None) is not None: - # Find the petrosmallness terms in a two-levels combo-regularization. - petrosmallness = np.where( - np.r_[[isinstance(regpart, PGI) for regpart in self.reg.objfcts]] - )[0][0] - self.petrosmallness = petrosmallness - - # Find the smoothness terms in a two-levels combo-regularization. - Smooth = [] - for i, regobjcts in enumerate(self.reg.objfcts): - for j, regpart in enumerate(regobjcts.objfcts): - Smooth += [ - [ - i, - j, - isinstance( - regpart, (SmoothnessFirstOrder, SparseSmoothness) - ), - ] - ] - self.Smooth = np.r_[Smooth] - - self.nbr = np.sum( - [len(self.reg.objfcts[i].objfcts) for i in range(len(self.reg.objfcts))] - ) - self._regmode = 1 - self.pgi_reg = self.reg.objfcts[self.petrosmallness] - - else: - self._regmode = 2 - self.pgi_reg = self.reg - self.nbr = len(self.reg.objfcts) - self.Smooth = np.r_[ - [ - isinstance(regpart, (SmoothnessFirstOrder, SparseSmoothness)) - for regpart in self.reg.objfcts - ] - ] - self._regmode = 2 - - if ~np.any(self.pgi_updategmm_class): - self.previous_membership = self.pgi_reg.membership(self.invProb.model) - else: - self.previous_membership = self.pgi_reg.compute_quasi_geology_model() - - @property - def DMtarget(self): - if getattr(self, "_DMtarget", None) is None: - self.phi_d_target = self.invProb.dmisfit.survey.nD - self._DMtarget = self.chifact * self.phi_d_target - return self._DMtarget - - @DMtarget.setter - def DMtarget(self, val): - self._DMtarget = val - - def endIter(self): - self.DM = self.inversion.directiveList.dList[self.targetclass].DM - self.dmlist = self.inversion.directiveList.dList[self.targetclass].dmlist - - if ~np.any(self.pgi_updategmm_class): - self.membership = self.pgi_reg.membership(self.invProb.model) - else: - self.membership = self.pgi_reg.compute_quasi_geology_model() - - same_mref = np.all(self.membership == self.previous_membership) - percent_diff = ( - len(self.membership) - - np.count_nonzero(self.previous_membership == self.membership) - ) / len(self.membership) - if self.verbose: - print( - "mref changed in ", - len(self.membership) - - np.count_nonzero(self.previous_membership == self.membership), - " places", - ) - if ( - self.DM or np.all(self.dmlist < (1 + self.tolerance_phid) * self.DMtarget) - ) and ( - same_mref or not self.wait_till_stable or percent_diff <= self.tolerance - ): - self.reg.reference_model_in_smooth = True - self.pgi_reg.reference_model_in_smooth = True - - if self._regmode == 2: - for i in range(self.nbr): - if self.Smooth[i]: - self.reg.objfcts[i].reference_model = mkvc( - self.pgi_reg.gmm.means_[self.membership] - ) - if self.verbose: - print( - "Add mref to Smoothness. Changes in mref happened in {} % of the cells".format( - percent_diff - ) - ) - - elif self._regmode == 1: - for i in range(self.nbr): - if self.Smooth[i, 2]: - idx = self.Smooth[i, :2] - self.reg.objfcts[idx[0]].objfcts[idx[1]].reference_model = mkvc( - self.pgi_reg.gmm.means_[self.membership] - ) - if self.verbose: - print( - "Add mref to Smoothness. Changes in mref happened in {} % of the cells".format( - percent_diff - ) - ) - - self.previous_membership = copy.deepcopy(self.membership) diff --git a/simpeg/directives/sim_directives.py b/simpeg/directives/sim_directives.py index 0a3464717d..e2c3f8a5f3 100644 --- a/simpeg/directives/sim_directives.py +++ b/simpeg/directives/sim_directives.py @@ -1,389 +1,18 @@ -import numpy as np -from ..regularization import BaseSimilarityMeasure -from ..utils import eigenvalue_by_power_iteration -from ..optimization import IterationPrinters, StoppingCriteria -from .directives import InversionDirective, SaveEveryIteration - - -############################################################################### -# # -# Directives of joint inversion # -# # -############################################################################### -class SimilarityMeasureInversionPrinters: - betas = { - "title": "betas", - "value": lambda M: ["{:.2e}".format(elem) for elem in M.parent.betas], - "width": 26, - "format": "%s", - } - lambd = { - "title": "lambda", - "value": lambda M: M.parent.lambd, - "width": 10, - "format": "%1.2e", - } - phi_d_list = { - "title": "phi_d", - "value": lambda M: ["{:.2e}".format(elem) for elem in M.parent.phi_d_list], - "width": 26, - "format": "%s", - } - phi_m_list = { - "title": "phi_m", - "value": lambda M: ["{:.2e}".format(elem) for elem in M.parent.phi_m_list], - "width": 26, - "format": "%s", - } - phi_sim = { - "title": "phi_sim", - "value": lambda M: M.parent.phi_sim, - "width": 10, - "format": "%1.2e", - } - iterationCG = { - "title": "iterCG", - "value": lambda M: M.cg_count, - "width": 10, - "format": "%3d", - } - - -class SimilarityMeasureInversionDirective(InversionDirective): - """ - Directive for two model similiraty measure joint inversions. Sets Printers and - StoppingCriteria. - - Notes - ----- - Methods assume we are working with two models, and a single similarity measure. - Also, the SimilarityMeasure objective function must be the last regularization. - """ - - printers = [ - IterationPrinters.iteration, - SimilarityMeasureInversionPrinters.betas, - SimilarityMeasureInversionPrinters.lambd, - IterationPrinters.f, - SimilarityMeasureInversionPrinters.phi_d_list, - SimilarityMeasureInversionPrinters.phi_m_list, - SimilarityMeasureInversionPrinters.phi_sim, - SimilarityMeasureInversionPrinters.iterationCG, - ] - - def initialize(self): - if not isinstance(self.reg.objfcts[-1], BaseSimilarityMeasure): - raise TypeError( - f"The last regularization function must be an instance of " - f"BaseSimilarityMeasure, got {type(self.reg.objfcts[-1])}." - ) - - # define relevant attributes - self.betas = self.reg.multipliers[:-1] - self.lambd = self.reg.multipliers[-1] - self.phi_d_list = [] - self.phi_m_list = [] - self.phi_sim = 0.0 - - # pass attributes to invProb - self.invProb.betas = self.betas - self.invProb.num_models = len(self.betas) - self.invProb.lambd = self.lambd - self.invProb.phi_d_list = self.phi_d_list - self.invProb.phi_m_list = self.phi_m_list - self.invProb.phi_sim = self.phi_sim - - self.opt.printers = self.printers - self.opt.stoppers = [StoppingCriteria.iteration] - - def validate(self, directiveList): - # check that this directive is first in the DirectiveList - dList = directiveList.dList - self_ind = dList.index(self) - if self_ind != 0: - raise IndexError( - "The CrossGradientInversionDirective must be first in directive list." - ) - return True - - def endIter(self): - # compute attribute values - phi_d = [] - for dmis in self.dmisfit.objfcts: - phi_d.append(dmis(self.opt.xc)) - - phi_m = [] - for reg in self.reg.objfcts: - phi_m.append(reg(self.opt.xc)) - - # pass attributes values to invProb - self.invProb.phi_d_list = phi_d - self.invProb.phi_m_list = phi_m[:-1] - self.invProb.phi_sim = phi_m[-1] - self.invProb.betas = self.reg.multipliers[:-1] - # Assume last reg.objfct is the coupling - self.invProb.lambd = self.reg.multipliers[-1] - - -class SimilarityMeasureSaveOutputEveryIteration(SaveEveryIteration): - """ - SaveOutputEveryIteration for Joint Inversions. - Saves information on the tradeoff parameters, data misfits, regularizations, - coupling term, number of CG iterations, and value of cost function. - """ - - header = None - save_txt = True - betas = None - phi_d = None - phi_m = None - phi_sim = None - phi = None - - def initialize(self): - if self.save_txt is True: - print( - "CrossGradientSaveOutputEveryIteration will save your inversion " - "progress as: '###-{0!s}.txt'".format(self.fileName) - ) - f = open(self.fileName + ".txt", "w") - self.header = " # betas lambda joint_phi_d joint_phi_m phi_sim iterCG phi \n" - f.write(self.header) - f.close() - - # Create a list of each - self.betas = [] - self.lambd = [] - self.phi_d = [] - self.phi_m = [] - self.phi = [] - self.phi_sim = [] - - def endIter(self): - self.betas.append(["{:.2e}".format(elem) for elem in self.invProb.betas]) - self.phi_d.append(["{:.3e}".format(elem) for elem in self.invProb.phi_d_list]) - self.phi_m.append(["{:.3e}".format(elem) for elem in self.invProb.phi_m_list]) - self.lambd.append("{:.2e}".format(self.invProb.lambd)) - self.phi_sim.append(self.invProb.phi_sim) - self.phi.append(self.opt.f) - - if self.save_txt: - f = open(self.fileName + ".txt", "a") - i = self.opt.iter - f.write( - " {0:2d} {1} {2} {3} {4} {5:1.4e} {6:d} {7:1.4e}\n".format( - i, - self.betas[i - 1], - self.lambd[i - 1], - self.phi_d[i - 1], - self.phi_m[i - 1], - self.phi_sim[i - 1], - self.opt.cg_count, - self.phi[i - 1], - ) - ) - f.close() - - def load_results(self): - results = np.loadtxt(self.fileName + str(".txt"), comments="#") - self.betas = results[:, 1] - self.lambd = results[:, 2] - self.phi_d = results[:, 3] - self.phi_m = results[:, 4] - self.phi_sim = results[:, 5] - self.f = results[:, 7] - - -class PairedBetaEstimate_ByEig(InversionDirective): - """ - Estimate the trade-off parameter, beta, between pairs of data misfit(s) and the - regularization(s) as a multiple of the ratio between the highest eigenvalue of the - data misfit term and the highest eigenvalue of the regularization. - The highest eigenvalues are estimated through power iterations and Rayleigh - quotient. - - Notes - ----- - This class assumes the order of the data misfits for each model parameter match - the order for the respective regularizations, i.e. - - >>> data_misfits = [phi_d_m1, phi_d_m2, phi_d_m3] - >>> regs = [phi_m_m1, phi_m_m2, phi_m_m3] - - In which case it will estimate regularization parameters for each respective pair. - """ - - beta0_ratio = 1.0 #: the estimated ratio is multiplied by this to obtain beta - n_pw_iter = 4 #: number of power iterations for estimation. - seed = None #: Random seed for the directive - - def initialize(self): - r""" - The initial beta is calculated by comparing the estimated - eigenvalues of :math:`J^T J` and :math:`W^T W`. - To estimate the eigenvector of **A**, we will use one iteration - of the *Power Method*: - - .. math:: - - \mathbf{x_1 = A x_0} - - Given this (very course) approximation of the eigenvector, we can - use the *Rayleigh quotient* to approximate the largest eigenvalue. - - .. math:: - - \lambda_0 = \frac{\mathbf{x^\top A x}}{\mathbf{x^\top x}} - - We will approximate the largest eigenvalue for both JtJ and WtW, - and use some ratio of the quotient to estimate beta0. - - .. math:: - - \beta_0 = \gamma \frac{\mathbf{x^\top J^\top J x}}{\mathbf{x^\top W^\top W x}} - - :rtype: float - :return: beta0 - """ - if self.seed is not None: - np.random.seed(self.seed) - - if self.verbose: - print("Calculating the beta0 parameter.") - - m = self.invProb.model - dmis_eigenvalues = [] - reg_eigenvalues = [] - dmis_objs = self.dmisfit.objfcts - reg_objs = [ - obj - for obj in self.reg.objfcts - if not isinstance(obj, BaseSimilarityMeasure) - ] - if len(dmis_objs) != len(reg_objs): - raise ValueError( - f"There must be the same number of data misfit and regularizations." - f"Got {len(dmis_objs)} and {len(reg_objs)} respectively." - ) - for dmis, reg in zip(dmis_objs, reg_objs): - dmis_eigenvalues.append( - eigenvalue_by_power_iteration( - dmis, - m, - n_pw_iter=self.n_pw_iter, - ) - ) - - reg_eigenvalues.append( - eigenvalue_by_power_iteration( - reg, - m, - n_pw_iter=self.n_pw_iter, - ) - ) - - self.ratios = np.array(dmis_eigenvalues) / np.array(reg_eigenvalues) - self.invProb.betas = self.beta0_ratio * self.ratios - self.reg.multipliers[:-1] = self.invProb.betas - - -class PairedBetaSchedule(InversionDirective): - """ - Directive for beta cooling schedule to determine the tradeoff - parameters when using paired data misfits and regularizations for a joint inversion. - """ - - chifact_target = 1.0 - beta_tol = 1e-1 - update_beta = True - cooling_rate = 1 - cooling_factor = 2 - dmis_met = False - - @property - def target(self): - if getattr(self, "_target", None) is None: - nD = np.array([survey.nD for survey in self.survey]) - - self._target = nD * self.chifact_target - - return self._target - - @target.setter - def target(self, val): - self._target = val - - def initialize(self): - self.dmis_met = np.zeros_like(self.invProb.betas, dtype=bool) - - def endIter(self): - # Check if target misfit has been reached, if so, set dmis_met to True - for i, phi_d in enumerate(self.invProb.phi_d_list): - self.dmis_met[i] = phi_d < self.target[i] - - # check separately if misfits are within the tolerance, - # otherwise, scale beta individually - for i, phi_d in enumerate(self.invProb.phi_d_list): - if self.opt.iter > 0 and self.opt.iter % self.cooling_rate == 0: - target = self.target[i] - ratio = phi_d / target - if self.update_beta and ratio <= (1.0 + self.beta_tol): - if ratio <= 1: - ratio = np.maximum(0.75, ratio) - else: - ratio = np.minimum(1.5, ratio) - - self.invProb.betas[i] /= ratio - elif ratio > 1.0: - self.invProb.betas[i] /= self.cooling_factor - - self.reg.multipliers[:-1] = self.invProb.betas - - -class MovingAndMultiTargetStopping(InversionDirective): - r""" - Directive for setting stopping criteria for a joint inversion. - Ensures both that all target misfits are met and there is a small change in the - model. Computes the percentage change of the current model from the previous model. - - ..math:: - \frac {\| \mathbf{m_i} - \mathbf{m_{i-1}} \|} {\| \mathbf{m_{i-1}} \|} - """ - - tol = 1e-5 - beta_tol = 1e-1 - chifact_target = 1.0 - - @property - def target(self): - if getattr(self, "_target", None) is None: - nD = [] - for survey in self.survey: - nD += [survey.nD] - nD = np.array(nD) - - self._target = nD * self.chifact_target - - return self._target - - @target.setter - def target(self, val): - self._target = val - - def endIter(self): - for phi_d, target in zip(self.invProb.phi_d_list, self.target): - if np.abs(1.0 - phi_d / target) >= self.beta_tol: - return - if ( - np.linalg.norm(self.opt.xc - self.opt.x_last) - / np.linalg.norm(self.opt.x_last) - > self.tol - ): - return - - print( - "stopping criteria met: ", - np.linalg.norm(self.opt.xc - self.opt.x_last) - / np.linalg.norm(self.opt.x_last), - ) - self.opt.stopNextIteration = True +""" +Backward compatibility with the ``simpeg.directives.sim_directives`` submodule. + +This file will be deleted when the ``simpeg.directives.sim_directives`` submodule +is removed. +""" + +import warnings +from ._sim_directives import * # noqa: F403,F401 + +warnings.warn( + "The `simpeg.directives.sim_directives` submodule has been deprecated, " + "and will be removed in SimPEG v0.26.0." + "Import any directive class directly from the `simpeg.directives` module. " + "E.g.: `from simpeg.directives import PairedBetaEstimate_ByEig`", + FutureWarning, + stacklevel=2, +) diff --git a/simpeg/electromagnetics/analytics/FDEMDipolarfields.py b/simpeg/electromagnetics/analytics/FDEMDipolarfields.py index bfdfee7748..62f6e8d1eb 100644 --- a/simpeg/electromagnetics/analytics/FDEMDipolarfields.py +++ b/simpeg/electromagnetics/analytics/FDEMDipolarfields.py @@ -199,7 +199,7 @@ def J_galvanic_from_ElectricDipoleWholeSpace( Add description of parameters """ - Ex_galvanic, Ey_galvanic, Ez_galvanic = E_galvanic_from_ElectricDipoleWholeSpaced( + Ex_galvanic, Ey_galvanic, Ez_galvanic = E_galvanic_from_ElectricDipoleWholeSpace( XYZ, srcLoc, sig, @@ -229,7 +229,7 @@ def J_inductive_from_ElectricDipoleWholeSpace( Ex_inductive, Ey_inductive, Ez_inductive, - ) = E_inductive_from_ElectricDipoleWholeSpaced( + ) = E_inductive_from_ElectricDipoleWholeSpace( XYZ, srcLoc, sig, @@ -318,6 +318,7 @@ def B_from_ElectricDipoleWholeSpace( kappa=kappa, epsr=epsr, ) + mu = mu_0 * (1 + kappa) Bx = mu * Hx By = mu * Hy Bz = mu * Hz diff --git a/simpeg/electromagnetics/analytics/FDEMcasing.py b/simpeg/electromagnetics/analytics/FDEMcasing.py index 0196d1277b..1510a6a2e9 100644 --- a/simpeg/electromagnetics/analytics/FDEMcasing.py +++ b/simpeg/electromagnetics/analytics/FDEMcasing.py @@ -120,9 +120,10 @@ def getCasingEphiMagDipole( srcloc, obsloc, freq, sigma, a, b, mu=(mu_0, mu_0, mu_0), eps=epsilon_0, moment=1.0 ): mu = np.asarray(mu) + omega = 2 * np.pi * freq return ( 1j - * omega(freq) + * omega * mu * _getCasingHertzMagDipoleDeriv_r( srcloc, obsloc, freq, sigma, a, b, mu, eps, moment diff --git a/simpeg/electromagnetics/base_1d.py b/simpeg/electromagnetics/base_1d.py index f1c85f44e1..22609872e1 100644 --- a/simpeg/electromagnetics/base_1d.py +++ b/simpeg/electromagnetics/base_1d.py @@ -1,8 +1,9 @@ +from collections import namedtuple + from scipy.constants import mu_0 import numpy as np from scipy import sparse as sp from scipy.special import roots_legendre -from empymod.transform import get_dlf_points from ..simulation import BaseSimulation @@ -17,10 +18,16 @@ validate_integer, ) from .. import props -from empymod.utils import check_hankel +import libdlf __all__ = ["BaseEM1DSimulation"] +HANKEL_FILTERS = {} +for filter_name in libdlf.hankel.__all__: + hankel_filter = getattr(libdlf.hankel, filter_name) + if "j0" in hankel_filter.values and "j1" in hankel_filter.values: + HANKEL_FILTERS[filter_name] = hankel_filter + ############################################################################### # # # Base EM1D Simulation # @@ -95,7 +102,7 @@ def __init__( n_points_per_path=3, **kwargs, ): - super().__init__(mesh=None, **kwargs) + super().__init__(**kwargs) self.sigma = sigma self.rho = rho self.sigmaMap = sigmaMap @@ -137,30 +144,33 @@ def __init__( self.hankel_filter = hankel_filter self.fix_Jmatrix = fix_Jmatrix - # Check input arguments. If self.hankel_filter is not a valid filter, - # it will set it to the default (key_201_2009). - ht, htarg = check_hankel( - "dlf", {"dlf": self.hankel_filter, "pts_per_dec": 0}, 1 - ) - - self._fhtfilt = htarg["dlf"] # Store filter - # self.hankel_pts_per_dec = htarg["pts_per_dec"] # Store pts_per_dec if self.verbose: print(">> Use " + self.hankel_filter + " filter for Hankel Transform") @property def hankel_filter(self): - """The hankely filter to use. + """The hankel filter used. Returns ------- str + + See Also + -------- + libdlf.hankel + The package housing the filter values. """ return self._hankel_filter @hankel_filter.setter def hankel_filter(self, value): - self._hankel_filter = validate_string("hankel_filter", value) + self._hankel_filter = validate_string( + "hankel_filter", value, list(HANKEL_FILTERS.keys()) + ) + base, j0, j1 = HANKEL_FILTERS[self._hankel_filter]() + hank = namedtuple("HankelFilter", "base j0 j1") + self._fhtfilt = hank(base, j0, j1) + self._coefficients_set = False _hankel_pts_per_dec = 0 # Default: Standard DLF @@ -324,7 +334,7 @@ def compute_complex_mu(self, frequencies): return mu_complex def Jvec(self, m, v, f=None): - Js = self.getJ(m, f=f) + Js = self._getJ(m, f=f) out = 0.0 if self.hMap is not None: out = out + Js["dh"] @ (self.hDeriv @ v) @@ -337,7 +347,7 @@ def Jvec(self, m, v, f=None): return out def Jtvec(self, m, v, f=None): - Js = self.getJ(m, f=f) + Js = self._getJ(m, f=f) out = 0.0 if self.hMap is not None: out = out + self.hDeriv.T @ (Js["dh"].T @ v) @@ -430,9 +440,8 @@ def _compute_hankel_coefficients(self): offsets = src.radius * np.ones(rx.locations.shape[0]) # computations for hankel transform... - lambd, _ = get_dlf_points( - self._fhtfilt, offsets, self._hankel_pts_per_dec - ) + lambd = self._fhtfilt.base / offsets[:, None] + # calculate the source-rx coefficients for the hankel transform C0 = 0.0 C1 = 0.0 @@ -549,8 +558,9 @@ def _compute_hankel_coefficients(self): C1s.append(np.exp(-lambd * (z + h)[:, None]) * C1 / offsets[:, None]) lambs.append(lambd) n_w_past += n_w - Is.append(np.ones(n_w, dtype=int) * i_count) - i_count += 1 + for _ in range(rx.locations.shape[0]): + Is.append(np.ones(n_w, dtype=int) * i_count) + i_count += 1 # Store these on the simulation for faster future executions self._lambs = np.vstack(lambs) @@ -567,8 +577,8 @@ def _compute_hankel_coefficients(self): self._W = self._W.tocsr() @property - def deleteTheseOnModelUpdate(self): - toDelete = super().deleteTheseOnModelUpdate + def _delete_on_model_update(self): + toDelete = super()._delete_on_model_update if self.fix_Jmatrix is False: toDelete += ["_J", "_gtgdiag"] return toDelete @@ -591,7 +601,7 @@ def get_threshold(self, uncert): def getJtJdiag(self, m, W=None, f=None): if getattr(self, "_gtgdiag", None) is None: - Js = self.getJ(m, f=f) + Js = self._getJ(m, f=f) if W is None: W = np.ones(self.survey.nD) else: diff --git a/simpeg/electromagnetics/frequency_domain/__init__.py b/simpeg/electromagnetics/frequency_domain/__init__.py index 5ec6ac0a13..e2f9422515 100644 --- a/simpeg/electromagnetics/frequency_domain/__init__.py +++ b/simpeg/electromagnetics/frequency_domain/__init__.py @@ -1,10 +1,28 @@ -""" +r""" ============================================================================== Frequency-Domain EM (:mod:`simpeg.electromagnetics.frequency_domain`) ============================================================================== .. currentmodule:: simpeg.electromagnetics.frequency_domain -About ``frequency_domain`` +The ``frequency_domain`` module contains functionality for solving Maxwell's equations +in the frequency-domain for controlled sources. Where a :math:`+i\omega t` +Fourier convention is used, this module is used to solve problems of the form: + +.. math:: + \begin{align} + \nabla \times \vec{E} + i\omega \vec{B} &= - i \omega \vec{S}_m \\ + \nabla \times \vec{H} - \vec{J} &= \vec{S}_e + \end{align} + +where the constitutive relations between fields and fluxes are given by: + +* :math:`\vec{J} = (\sigma + i \omega \varepsilon) \vec{E}` +* :math:`\vec{B} = \mu \vec{H}` + +and: + +* :math:`\vec{S}_m` represents a magnetic source term +* :math:`\vec{S}_e` represents a current source term Simulations =========== diff --git a/simpeg/electromagnetics/frequency_domain/fields.py b/simpeg/electromagnetics/frequency_domain/fields.py index ecf0023c55..d23392e0ad 100644 --- a/simpeg/electromagnetics/frequency_domain/fields.py +++ b/simpeg/electromagnetics/frequency_domain/fields.py @@ -6,34 +6,60 @@ class FieldsFDEM(Fields): - r""" - Fancy Field Storage for a FDEM survey. Only one field type is stored for - each problem, the rest are computed. The fields object acts like an array - and is indexed by + r"""Base class for storing FDEM fields. - .. code-block:: python + FDEM fields classes are used to store the discrete solution of the fields for a + corresponding FDEM simulation; see :class:`.BaseFDEMSimulation`. + Only one field type (e.g. ``'e'``, ``'j'``, ``'h'``, or ``'b'``) is stored, but certain field types + can be rapidly computed and returned on the fly. The field type that is stored and the + field types that can be returned depend on the formulation used by the associated simulation class. + Once a field object has been created, the individual fields can be accessed; see the example below. - f = problem.fields(m) - e = f[source_list,'e'] - b = f[source_list,'b'] + Parameters + ---------- + simulation : .BaseFDEMSimulation + The FDEM simulation object used to compute the discrete field solution. - If accessing all sources for a given field, use the :code:`:` + Example + ------- + We want to access the fields for a discrete solution with :math:`\mathbf{e}` discretized + to edges and :math:`\mathbf{b}` discretized to faces. To extract the fields for all sources: .. code-block:: python - f = problem.fields(m) + f = simulation.fields(m) e = f[:,'e'] b = f[:,'b'] - The array returned will be size (``nE`` or ``nF``, ``nSrcs`` :math:`\times` - ``nFrequencies``) + The array ``e`` returned will have shape (`n_edges`, `n_sources`). And the array ``b`` + returned will have shape (`n_faces`, `n_sources`). We can also extract the fields for + a subset of the source list used for the simulation as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list,'e'] + b = f[source_list,'b'] + """ - knownFields = {} - _dtype = complex + def __init__(self, simulation): + dtype = complex + super().__init__(simulation=simulation, dtype=dtype) def _GLoc(self, fieldType): - """Grid location of the fieldType""" + """Return grid locations of the fieldType. + + Parameters + ---------- + fieldType : str + The field type. + + Returns + ------- + str + The grid locations. One of {``'CC'``, ``'N'``, ``'E'``, ``'F'``}. + """ return self.aliasFields[fieldType][1] def _e(self, solution, source_list): @@ -295,28 +321,66 @@ def _jDeriv(self, src, du_dm_v, v, adjoint=False): class Fields3DElectricField(FieldsFDEM): - """ - Fields object for Simulation3DElectricField. + r"""Fields class for storing 3D total electric field solutions. + + This class stores the total electric field solution computed using a + :class:`.frequency_domain.Simulation3DElectricField` + simulation object. This class can be used to extract the following quantities: + + * ``'e'``, ``'ePrimary'``, ``'eSecondary'`` and ``'j'`` on mesh edges. + * ``'h'``, ``'b'``, ``'bPrimary'`` and ``'bSecondary'`` on mesh faces. + * ``'charge'`` on mesh nodes. + * ``'charge_density'`` at cell centers. + + See the example below to learn how fields can be extracted from a + ``Fields3DElectricField`` object. + + Parameters + ---------- + simulation : .frequency_domain.Simulation3DElectricField + The FDEM simulation object associated with the fields. - :param discretize.base.BaseMesh mesh: mesh - :param simpeg.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey + Example + ------- + The ``Fields3DElectricField`` object stores the total electric field solution + on mesh edges. To extract the discrete electric fields and magnetic flux + densities for all sources: + + .. code-block:: python + + f = simulation.fields(m) + e = f[:, 'e'] + b = f[:, 'b'] + + The array ``e`` returned will have shape (`n_edges`, `n_sources`). And the array ``b`` + returned will have shape (`n_faces`, `n_sources`). We can also extract the fields for + a subset of the source list used for the simulation as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list,'e'] + b = f[source_list,'b'] """ - knownFields = {"eSolution": "E"} - aliasFields = { - "e": ["eSolution", "E", "_e"], - "ePrimary": ["eSolution", "E", "_ePrimary"], - "eSecondary": ["eSolution", "E", "_eSecondary"], - "b": ["eSolution", "F", "_b"], - "bPrimary": ["eSolution", "F", "_bPrimary"], - "bSecondary": ["eSolution", "F", "_bSecondary"], - "j": ["eSolution", "E", "_j"], - "h": ["eSolution", "F", "_h"], - "charge": ["eSolution", "N", "_charge"], - "charge_density": ["eSolution", "CC", "_charge_density"], - } + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"eSolution": "E"} + self._aliasFields = { + "e": ["eSolution", "E", "_e"], + "ePrimary": ["eSolution", "E", "_ePrimary"], + "eSecondary": ["eSolution", "E", "_eSecondary"], + "b": ["eSolution", "F", "_b"], + "bPrimary": ["eSolution", "F", "_bPrimary"], + "bSecondary": ["eSolution", "F", "_bSecondary"], + "j": ["eSolution", "E", "_j"], + "h": ["eSolution", "F", "_h"], + "charge": ["eSolution", "N", "_charge"], + "charge_density": ["eSolution", "CC", "_charge_density"], + } def startup(self): + # Docstring inherited from parent. self._edgeCurl = self.simulation.mesh.edge_curl self._aveE2CCV = self.simulation.mesh.aveE2CCV self._aveF2CCV = self.simulation.mesh.aveF2CCV @@ -623,28 +687,67 @@ def _charge_density(self, eSolution, source_list): class Fields3DMagneticFluxDensity(FieldsFDEM): - """ - Fields object for Simulation3DMagneticFluxDensity. + r"""Fields class for storing 3D total magnetic flux density solutions. + + This class stores the total magnetic flux density solution computed using a + :class:`.frequency_domain.Simulation3DMagneticFluxDensity` + simulation object. This class can be used to extract the following quantities: + + * ``'b'``, ``'bPrimary'``, ``'bSecondary'`` and ``'h'`` on mesh faces. + * ``'e'``, ``'ePrimary'``, ``'eSecondary'`` and ``'j'`` on mesh edges. + * ``'charge'`` on mesh nodes. + * ``'charge_density'`` at cell centers. + + See the example below to learn how fields can be extracted from a + ``Fields3DMagneticFluxDensity`` object. + + Parameters + ---------- + simulation : .frequency_domain.Simulation3DMagneticFluxDensity + The FDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DMagneticFluxDensity`` object stores the total magnetic flux density solution + on mesh faces. To extract the discrete electric fields and magnetic flux + densities for all sources: + + .. code-block:: python + + f = simulation.fields(m) + e = f[:, 'e'] + b = f[:, 'b'] + + The array ``e`` returned will have shape (`n_edges`, `n_sources`). And the array ``b`` + returned will have shape (`n_faces`, `n_sources`). We can also extract the fields for + a subset of the source list used for the simulation as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list, 'e'] + b = f[source_list, 'b'] - :param discretize.base.BaseMesh mesh: mesh - :param simpeg.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey """ - knownFields = {"bSolution": "F"} - aliasFields = { - "b": ["bSolution", "F", "_b"], - "bPrimary": ["bSolution", "F", "_bPrimary"], - "bSecondary": ["bSolution", "F", "_bSecondary"], - "e": ["bSolution", "E", "_e"], - "ePrimary": ["bSolution", "E", "_ePrimary"], - "eSecondary": ["bSolution", "E", "_eSecondary"], - "j": ["bSolution", "E", "_j"], - "h": ["bSolution", "F", "_h"], - "charge": ["bSolution", "N", "_charge"], - "charge_density": ["bSolution", "CC", "_charge_density"], - } + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"bSolution": "F"} + self._aliasFields = { + "b": ["bSolution", "F", "_b"], + "bPrimary": ["bSolution", "F", "_bPrimary"], + "bSecondary": ["bSolution", "F", "_bSecondary"], + "e": ["bSolution", "E", "_e"], + "ePrimary": ["bSolution", "E", "_ePrimary"], + "eSecondary": ["bSolution", "E", "_eSecondary"], + "j": ["bSolution", "E", "_j"], + "h": ["bSolution", "F", "_h"], + "charge": ["bSolution", "N", "_charge"], + "charge_density": ["bSolution", "CC", "_charge_density"], + } def startup(self): + # Docstring inherited from parent. self._edgeCurl = self.simulation.mesh.edge_curl self._MeSigma = self.simulation.MeSigma self._MeSigmaI = self.simulation.MeSigmaI @@ -953,28 +1056,65 @@ def _charge_density(self, bSolution, source_list): class Fields3DCurrentDensity(FieldsFDEM): - """ - Fields object for Simulation3DCurrentDensity. + r"""Fields class for storing 3D current density solutions. + + This class stores the total current density solution computed using a + :class:`.frequency_domain.Simulation3DCurrentDensity` + simulation object. This class can be used to extract the following quantities: + + * ``'j'``, ``'jPrimary'``, ``'jSecondary'`` and ``'e'`` on mesh faces. + * ``'h'``, ``'hPrimary'``, ``'hSecondary'`` and ``'b'`` on mesh edges. + * ``'charge'`` and ``'charge_density'`` at cell centers. + + See the example below to learn how fields can be extracted from a + ``Fields3DCurrentDensity`` object. + + Parameters + ---------- + simulation : .frequency_domain.Simulation3DCurrentDensity + The FDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DCurrentDensity`` object stores the total current density solution + on mesh faces. To extract the discrete current density and magnetic field: + + .. code-block:: python + + f = simulation.fields(m) + j = f[:, 'j'] + h = f[:, 'h'] + + The array ``j`` returned will have shape (`n_faces`, `n_sources`). And the array ``h`` + returned will have shape (`n_edges`, `n_sources`). We can also extract the fields for + a subset of the source list used for the simulation as follows: + + .. code-block:: python + + f = simulation.fields(m) + j = f[source_list, 'j'] + h = f[source_list, 'h'] - :param discretize.base.BaseMesh mesh: mesh - :param simpeg.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey """ - knownFields = {"jSolution": "F"} - aliasFields = { - "j": ["jSolution", "F", "_j"], - "jPrimary": ["jSolution", "F", "_jPrimary"], - "jSecondary": ["jSolution", "F", "_jSecondary"], - "h": ["jSolution", "E", "_h"], - "hPrimary": ["jSolution", "E", "_hPrimary"], - "hSecondary": ["jSolution", "E", "_hSecondary"], - "e": ["jSolution", "F", "_e"], - "b": ["jSolution", "E", "_b"], - "charge": ["bSolution", "CC", "_charge"], - "charge_density": ["bSolution", "CC", "_charge_density"], - } + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"jSolution": "F"} + self._aliasFields = { + "j": ["jSolution", "F", "_j"], + "jPrimary": ["jSolution", "F", "_jPrimary"], + "jSecondary": ["jSolution", "F", "_jSecondary"], + "h": ["jSolution", "E", "_h"], + "hPrimary": ["jSolution", "E", "_hPrimary"], + "hSecondary": ["jSolution", "E", "_hSecondary"], + "e": ["jSolution", "F", "_e"], + "b": ["jSolution", "E", "_b"], + "charge": ["jSolution", "CC", "_charge"], + "charge_density": ["jSolution", "CC", "_charge_density"], + } def startup(self): + # Docstring inherited from parent. self._edgeCurl = self.simulation.mesh.edge_curl self._MeMu = self.simulation.MeMu self._MeMuI = self.simulation.MeMuI @@ -988,6 +1128,7 @@ def startup(self): self._nC = self.simulation.mesh.nC self._MeI = self.simulation.MeI self._MfI = self.simulation.MfI + self._faceDiv = self.simulation.mesh.face_divergence def _GLoc(self, fieldType): if fieldType in ["h", "hSecondary", "hPrimary", "b"]: @@ -1344,28 +1485,65 @@ def _charge_density(self, jSolution, source_list): class Fields3DMagneticField(FieldsFDEM): - """ - Fields object for Simulation3DMagneticField. + r"""Fields class for storing 3D magnetic field solutions. + + This class stores the total magnetic field solution computed using a + :class:`.frequency_domain.Simulation3DMagneticField` + simulation object. This class can be used to extract the following quantities: + + * ``'h'``, ``'hPrimary'``, ``'hSecondary'`` and ``'b'`` on mesh edges. + * ``'j'``, ``'jPrimary'``, ``'jSecondary'`` and ``'e'`` on mesh faces. + * ``'charge'`` and ``'charge_density'`` at cell centers. + + See the example below to learn how fields can be extracted from a + ``Fields3DMagneticField`` object. + + Parameters + ---------- + simulation : .frequency_domain.Simulation3DMagneticField + The FDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DMagneticField`` object stores the total magnetic field solution + on mesh edges. To extract the discrete current density and magnetic field: + + .. code-block:: python + + f = simulation.fields(m) + j = f[:, 'j'] + h = f[:, 'h'] + + The array ``j`` returned will have shape (`n_faces`, `n_sources`). And the array ``h`` + returned will have shape (`n_edges`, `n_sources`). We can also extract the fields for + a subset of the source list used for the simulation as follows: + + .. code-block:: python + + f = simulation.fields(m) + j = f[source_list, 'j'] + h = f[source_list, 'h'] - :param discretize.base.BaseMesh mesh: mesh - :param simpeg.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey """ - knownFields = {"hSolution": "E"} - aliasFields = { - "h": ["hSolution", "E", "_h"], - "hPrimary": ["hSolution", "E", "_hPrimary"], - "hSecondary": ["hSolution", "E", "_hSecondary"], - "j": ["hSolution", "F", "_j"], - "jPrimary": ["hSolution", "F", "_jPrimary"], - "jSecondary": ["hSolution", "F", "_jSecondary"], - "e": ["hSolution", "CCV", "_e"], - "b": ["hSolution", "CCV", "_b"], - "charge": ["hSolution", "CC", "_charge"], - "charge_density": ["hSolution", "CC", "_charge_density"], - } + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"hSolution": "E"} + self._aliasFields = { + "h": ["hSolution", "E", "_h"], + "hPrimary": ["hSolution", "E", "_hPrimary"], + "hSecondary": ["hSolution", "E", "_hSecondary"], + "j": ["hSolution", "F", "_j"], + "jPrimary": ["hSolution", "F", "_jPrimary"], + "jSecondary": ["hSolution", "F", "_jSecondary"], + "e": ["hSolution", "CCV", "_e"], + "b": ["hSolution", "CCV", "_b"], + "charge": ["hSolution", "CC", "_charge"], + "charge_density": ["hSolution", "CC", "_charge_density"], + } def startup(self): + # Docstring inherited from parent. self._edgeCurl = self.simulation.mesh.edge_curl self._MeMu = self.simulation.MeMu self._MeMuDeriv = self.simulation.MeMuDeriv @@ -1379,6 +1557,7 @@ def startup(self): self._nC = self.simulation.mesh.nC self._MfI = self.simulation.MfI self._MeI = self.simulation.MeI + self._faceDiv = self.simulation.mesh.face_divergence def _GLoc(self, fieldType): if fieldType in ["h", "hSecondary", "hPrimary", "b"]: diff --git a/simpeg/electromagnetics/frequency_domain/simulation.py b/simpeg/electromagnetics/frequency_domain/simulation.py index 19619a8f89..726052f30b 100644 --- a/simpeg/electromagnetics/frequency_domain/simulation.py +++ b/simpeg/electromagnetics/frequency_domain/simulation.py @@ -3,7 +3,6 @@ from discretize.utils import Zero from ... import props -from ...data import Data from ...utils import mkvc, validate_type from ..base import BaseEMSimulation from ..utils import omega @@ -20,43 +19,51 @@ class BaseFDEMSimulation(BaseEMSimulation): - r""" - We start by looking at Maxwell's equations in the electric - field (:math:`\mathbf{e}`) and the magnetic flux - density (:math:`\mathbf{b}`) - - .. math :: - - \mathbf{C} \mathbf{e} + i \omega \mathbf{b} = \mathbf{s_m} - {\mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f} \mathbf{b} - - \mathbf{M_{\sigma}^e} \mathbf{e} = \mathbf{s_e}} - - if using the E-B formulation (:code:`Simulation3DElectricField` - or :code:`Simulation3DMagneticFluxDensity`). Note that in this case, - :math:`\mathbf{s_e}` is an integrated quantity. - - If we write Maxwell's equations in terms of - :math:`\mathbf{h}` and current density :math:`\mathbf{j}`. - - .. math :: - - \mathbf{C}^{\top} \mathbf{M_{\rho}^f} \mathbf{j} + - i \omega \mathbf{M_{\mu}^e} \mathbf{h} = \mathbf{s_m} - \mathbf{C} \mathbf{h} - \mathbf{j} = \mathbf{s_e} - - if using the H-J formulation (:code:`Simulation3DCurrentDensity` or - :code:`Simulation3DMagneticField`). Note that here, :math:`\mathbf{s_m}` is an - integrated quantity. - - The problem performs the elimination so that we are solving the system - for :math:`mathbf{e}`, :math:`mathbf{b}`, :math:`mathbf{j}` or - :math:`mathbf{h}`. - + r"""Base finite volume FDEM simulation class. + + This class is used to define properties and methods necessary for solving + 3D frequency-domain EM problems. For a :math:`+i\omega t` Fourier convention, + Maxwell's equations are expressed as: + + .. math:: + \begin{align} + \nabla \times \vec{E} + i\omega \vec{B} &= - i \omega \vec{S}_m \\ + \nabla \times \vec{H} - \vec{J} &= \vec{S}_e + \end{align} + + where the constitutive relations between fields and fluxes are given by: + + * :math:`\vec{J} = \sigma \vec{E}` + * :math:`\vec{B} = \mu \vec{H}` + + and: + + * :math:`\vec{S}_m` represents a magnetic source term + * :math:`\vec{S}_e` represents a current source term + + Child classes of ``BaseFDEMSimulation`` solve the above expression numerically + for various cases using mimetic finite volume. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .frequency_domain.survey.Survey + The frequency-domain EM survey. + forward_only : bool, optional + If ``True``, the factorization for the inverse of the system matrix at each + frequency is discarded after the fields are computed at that frequency. + If ``False``, the factorizations of the system matrices for all frequencies are stored. + permittivity : (n_cells,) numpy.ndarray, optional + Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement + is ignored. Please note that `permittivity` is not an invertible property, and that future + development will result in the deprecation of this propery. + storeJ : bool, optional + Whether to compute and store the sensitivity matrix. """ fieldsPair = FieldsFDEM permittivity = props.PhysicalProperty("Dielectric permittivity (F/m)") - # permittivity, permittivityMap, permittivityDeriv = props.Invertible("Dielectric permittivity (F/m)") def __init__( self, @@ -65,7 +72,7 @@ def __init__( forward_only=False, permittivity=None, storeJ=False, - **kwargs + **kwargs, ): super().__init__(mesh=mesh, survey=survey, **kwargs) self.forward_only = forward_only @@ -79,11 +86,12 @@ def __init__( @property def survey(self): - """The simulations survey. + """The FDEM survey object. Returns ------- - simpeg.electromagnetics.frequency_domain.survey.Survey + .frequency_domain.survey.Survey + The FDEM survey object. """ if self._survey is None: raise AttributeError("Simulation must have a survey set") @@ -98,11 +106,12 @@ def survey(self, value): @property def storeJ(self): - """Whether to store the sensitivity matrix + """Whether to compute and store the sensitivity matrix. Returns ------- bool + Whether to compute and store the sensitivity matrix. """ return self._storeJ @@ -112,11 +121,16 @@ def storeJ(self, value): @property def forward_only(self): - """If True, A-inverse not stored at each frequency in forward simulation. + """Whether to store the factorizations of the inverses of the system matrices. + + If ``True``, the factorization for the inverse of the system matrix at each + frequency is discarded after the fields are computed at that frequency. + If ``False``, the factorizations of the system matrices for all frequencies are stored. Returns ------- bool + Whether to store the factorizations of the inverses of the system matrices. """ return self._forward_only @@ -154,12 +168,17 @@ def _get_edge_admittivity_property_matrix( # @profile def fields(self, m=None): - """ - Solve the forward problem for the fields. + """Compute and return the fields for the model provided. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model. - :param numpy.ndarray m: inversion model (nP,) - :rtype: numpy.ndarray - :return f: forward solution + Returns + ------- + .frequency_domain.fields.FieldsFDEM + The FDEM fields object. """ if m is not None: @@ -186,15 +205,34 @@ def fields(self, m=None): # @profile def Jvec(self, m, v, f=None): - """ - Sensitivity times a vector. - - :param numpy.ndarray m: inversion model (nP,) - :param numpy.ndarray v: vector which we take sensitivity product with - (nP,) - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object - :rtype: numpy.ndarray - :return: Jv (ndata,) + r"""Compute the sensitivity matrix times a vector. + + Where :math:`\mathbf{d}` are the data, :math:`\mathbf{m}` are the model parameters, + and the sensitivity matrix is defined as: + + .. math:: + \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} + + this method computes and returns the matrix-vector product: + + .. math:: + \mathbf{J v} + + for a given vector :math:`v`. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + v : (n_param,) numpy.ndarray + The vector. + f : .frequency_domain.fields.FieldsFDEM, optional + Fields solved for all sources. + + Returns + ------- + (n_data,) numpy.ndarray + The sensitivity matrix times a vector. """ if f is None: @@ -202,7 +240,8 @@ def Jvec(self, m, v, f=None): self.model = m - Jv = Data(self.survey) + survey_slices = self.survey.get_all_slices() + Jv = np.full(self.survey.nD, fill_value=np.nan) for nf, freq in enumerate(self.survey.frequencies): for src in self.survey.get_sources_by_frequency(freq): @@ -211,19 +250,42 @@ def Jvec(self, m, v, f=None): dRHS_dm_v = self.getRHSDeriv(freq, src, v) du_dm_v = self.Ainv[nf] * (-dA_dm_v + dRHS_dm_v) for rx in src.receiver_list: - Jv[src, rx] = rx.evalDeriv(src, self.mesh, f, du_dm_v=du_dm_v, v=v) + src_rx_slice = survey_slices[src, rx] + Jv[src_rx_slice] = mkvc( + rx.evalDeriv(src, self.mesh, f, du_dm_v=du_dm_v, v=v) + ) - return Jv.dobs + return Jv def Jtvec(self, m, v, f=None): - """ - Sensitivity transpose times a vector + r"""Compute the adjoint sensitivity matrix times a vector. + + Where :math:`\mathbf{d}` are the data, :math:`\mathbf{m}` are the model parameters, + and the sensitivity matrix is defined as: + + .. math:: + \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} - :param numpy.ndarray m: inversion model (nP,) - :param numpy.ndarray v: vector which we take adjoint product with (nP,) - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object - :rtype: numpy.ndarray - :return: Jv (ndata,) + this method computes and returns the matrix-vector product: + + .. math:: + \mathbf{J^T v} + + for a given vector :math:`v`. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + v : (n_data,) numpy.ndarray + The vector. + f : .frequency_domain.fields.FieldsFDEM, optional + Fields solved for all sources. + + Returns + ------- + (n_param,) numpy.ndarray + The adjoint sensitivity matrix times a vector. """ if f is None: @@ -231,9 +293,8 @@ def Jtvec(self, m, v, f=None): self.model = m - # Ensure v is a data object. - if not isinstance(v, Data): - v = Data(self.survey, v) + # Get dict of flat array slices for each source-receiver pair in the survey + survey_slices = self.survey.get_all_slices() Jtv = np.zeros(m.size) @@ -243,8 +304,9 @@ def Jtvec(self, m, v, f=None): df_duT_sum = 0 df_dmT_sum = 0 for rx in src.receiver_list: + src_rx_slice = survey_slices[src, rx] df_duT, df_dmT = rx.evalDeriv( - src, self.mesh, f, v=v[src, rx], adjoint=True + src, self.mesh, f, v=v[src_rx_slice], adjoint=True ) if not isinstance(df_duT, Zero): df_duT_sum += df_duT @@ -263,13 +325,27 @@ def Jtvec(self, m, v, f=None): return mkvc(Jtv) def getJ(self, m, f=None): - """ - Method to form full J given a model m + r"""Generate the full sensitivity matrix. + + This method generates and stores the full sensitivity matrix for the + model provided. I.e.: + + .. math:: + \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} + + where :math:`\mathbf{d}` are the data and :math:`\mathbf{m}` are the model parameters. - :param numpy.ndarray m: inversion model (nP,) - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object - :rtype: numpy.ndarray - :return: J (ndata, nP) + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + f : .static.resistivity.fields.FieldsDC, optional + Fields solved for all sources. + + Returns + ------- + (n_data, n_param) numpy.ndarray + The full sensitivity matrix. """ self.model = m @@ -282,7 +358,8 @@ def getJ(self, m, f=None): Jmatrix = np.zeros((self.survey.nD, m_size)) - data = Data(self.survey) + # Get dict of flat array slices for each source-receiver pair in the survey + survey_slices = self.survey.get_all_slices() for A_i, freq in zip(Ainv, self.survey.frequencies): for src in self.survey.get_sources_by_frequency(freq): @@ -309,22 +386,41 @@ def getJ(self, m, f=None): du_dmT += np.hstack(df_dmT) block = np.array(du_dmT, dtype=complex).real.T - data_inds = data.index_dictionary[src][rx] - Jmatrix[data_inds] = block + + src_rx_slice = survey_slices[src, rx] + Jmatrix[src_rx_slice] = block self._Jmatrix = Jmatrix return self._Jmatrix def getJtJdiag(self, m, W=None, f=None): - """ - Return the diagonal of JtJ + r"""Return the diagonal of :math:`\mathbf{J^T J}`. + + Where :math:`\mathbf{d}` are the data and :math:`\mathbf{m}` are the model parameters, + the sensitivity matrix :math:`\mathbf{J}` is defined as: - :param numpy.ndarray m: inversion model (nP,) - :param numpy.ndarray W: vector of weights (ndata,) - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM u: fields object - :rtype: numpy.ndarray - :return: JtJ (nP,) + .. math:: + \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} + + This method returns the diagonals of :math:`\mathbf{J^T J}`. When the + *W* input argument is used to include a diagonal weighting matrix + :math:`\mathbf{W}`, this method returns the diagonal of + :math:`\mathbf{W^T J^T J W}`. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + W : (n_param, n_param) scipy.sparse.csr_matrix + A diagonal weighting matrix. + f : .frequency_domain.fields.FieldsFDEM, optional + Fields solved for all sources. + + Returns + ------- + (n_param,) numpy.ndarray + The diagonals. """ self.model = m @@ -344,13 +440,33 @@ def getJtJdiag(self, m, W=None, f=None): # @profile def getSourceTerm(self, freq): - """ - Evaluates the sources for a given frequency and puts them in matrix - form + r"""Returns the discrete source terms for the frequency provided. + + This method computes and returns the discrete magnetic and electric source + terms for all soundings at the frequency provided. The exact shape and + implementation of the source terms when solving for the fields at each frequency + is formulation dependent. - :param float freq: Frequency - :rtype: tuple - :return: (s_m, s_e) (nE or nF, nSrc) + For definitions of the discrete magnetic (:math:`\mathbf{s_m}`) and electric + (:math:`\mathbf{s_e}`) source terms for each simulation, see the *Notes* sections + of the docstrings for: + + * :class:`.frequency_domain.Simulation3DElectricField` + * :class:`.frequency_domain.Simulation3DMagneticField` + * :class:`.frequency_domain.Simulation3DCurrentDensity` + * :class:`.frequency_domain.Simulation3DMagneticFluxDensity` + + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + s_m : numpy.ndarray + The magnetic sources terms. (n_faces, n_sources) for EB-formulations. (n_edges, n_sources) for HJ-formulations. + s_e : numpy.ndarray + The electric sources terms. (n_edges, n_sources) for EB-formulations. (n_faces, n_sources) for HJ-formulations. """ Srcs = self.survey.get_sources_by_frequency(freq) n_fields = sum(src._fields_per_source for src in Srcs) @@ -375,8 +491,18 @@ def getSourceTerm(self, freq): return s_m, s_e @property - def deleteTheseOnModelUpdate(self): - toDelete = super().deleteTheseOnModelUpdate + def _delete_on_model_update(self): + """List of model-dependent attributes to clean upon model update. + + Some of the FDEM simulation's attributes are model-dependent. This property specifies + the model-dependent attributes that much be cleared when the model is updated. + + Returns + ------- + list of str + List of the model-dependent attributes to clean upon model update. + """ + toDelete = super()._delete_on_model_update return toDelete + ["_Jmatrix", "_gtgdiag"] @@ -386,28 +512,99 @@ def deleteTheseOnModelUpdate(self): class Simulation3DElectricField(BaseFDEMSimulation): - r""" - By eliminating the magnetic flux density using - - .. math :: - - \mathbf{b} = \frac{1}{i \omega}\left(-\mathbf{C} \mathbf{e} + - \mathbf{s_m}\right) + r"""3D FDEM simulation in terms of the electric field. + + This simulation solves for the electric field at each frequency. + In this formulation, the electric fields are defined on mesh edges and the + magnetic flux density is defined on mesh faces; i.e. it is an EB formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .frequency_domain.survey.Survey + The frequency-domain EM survey. + forward_only : bool, optional + If ``True``, the factorization for the inverse of the system matrix at each + frequency is discarded after the fields are computed at that frequency. + If ``False``, the factorizations of the system matrices for all frequencies are stored. + permittivity : (n_cells,) numpy.ndarray, optional + Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement + is ignored. Please note that `permittivity` is not an invertible property, and that future + development will result in the deprecation of this propery. + storeJ : bool, optional + Whether to compute and store the sensitivity matrix. + + Notes + ----- + Here, we start with the Maxwell's equations in the frequency-domain where a + :math:`+i\omega t` Fourier convention is used: + + .. math:: + \begin{align} + &\nabla \times \vec{E} + i\omega \vec{B} = - i \omega \vec{S}_m \\ + &\nabla \times \vec{H} - \vec{J} = \vec{S}_e + \end{align} + + where :math:`\vec{S}_e` is an electric source term that defines a source current density, + and :math:`\vec{S}_m` magnetic source term that defines a source magnetic flux density. + We define the constitutive relations for the electrical conductivity :math:`\sigma` + and magnetic permeability :math:`\mu` as: + + .. math:: + \vec{J} &= \sigma \vec{E} \\ + \vec{H} &= \mu^{-1} \vec{B} + + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: + + .. math:: + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{E}) \, dv + + i \omega \int_\Omega \vec{u} \cdot \vec{B} \, dv + = - i \omega \int_\Omega \vec{u} \cdot \vec{S}_m \, dv \\ + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{H} \, dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{H} \times \hat{n}) \, da + - \int_\Omega \vec{u} \cdot \vec{J} \, dv + = \int_\Omega \vec{u} \cdot \vec{S}_j \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{J} \, dv = \int_\Omega \vec{u} \cdot \sigma \vec{E} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{H} \, dv = \int_\Omega \vec{u} \cdot \mu^{-1} \vec{B} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete electric fields :math:`\mathbf{e}` are defined on mesh edges, + and the discrete magnetic flux densities :math:`\mathbf{b}` are defined on mesh faces. + This implies :math:`\mathbf{j}` must be defined on mesh edges and :math:`\mathbf{h}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: + + .. math:: + &\mathbf{u_f^T M_f C e} + i \omega \mathbf{u_f^T M_f b} = - i \omega \mathbf{u_f^T M_f s_m} \\ + &\mathbf{u_e^T C^T M_f h} - \mathbf{u_e^T M_e j} = \mathbf{u_e^T s_e} \\ + &\mathbf{u_e^T M_e j} = \mathbf{u_e^T M_{e \sigma} e} \\ + &\mathbf{u_f^T M_f h} = \mathbf{u_f^T M_{f \mu} b} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + By cancelling like-terms and combining the discrete expressions to solve for the electric field, we obtain: + + .. math:: + \mathbf{A \, e} = \mathbf{q} + + where + + * :math:`\mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}}` + * :math:`\mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C^T M_{f\frac{1}{\mu}} s_m }` - - we can write Maxwell's equations as a second order system in - :math:`mathbf{e}` only: - - .. math :: - - \left(\mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f} \mathbf{C} + - i \omega \mathbf{M^e_{\sigma}} \right)\mathbf{e} = - \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f}\mathbf{s_m} - - i\omega\mathbf{M^e}\mathbf{s_e} - - which we solve for :math:`\mathbf{e}`. - - :param discretize.base.BaseMesh mesh: mesh """ _solutionType = "eSolution" @@ -415,17 +612,30 @@ class Simulation3DElectricField(BaseFDEMSimulation): fieldsPair = Fields3DElectricField def getA(self, freq): - r""" - System matrix + r"""System matrix for the frequency provided. + + This method returns the system matrix for the frequency provided: - .. math :: + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}} + + where + + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - \mathbf{A} = \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f} \mathbf{C} - + i \omega \mathbf{M^e_{\sigma}} + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. - :param float freq: Frequency - :rtype: scipy.sparse.csr_matrix - :return: A + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The system matrix. """ MfMui = self.MfMui @@ -441,38 +651,98 @@ def getA(self, freq): return A def getADeriv_sigma(self, freq, u, v, adjoint=False): - r""" - Product of the derivative of our system matrix with respect to the - conductivity model and a vector - - .. math :: - - \frac{\mathbf{A}(\mathbf{m}) \mathbf{v}}{d \mathbf{m}_{\sigma}} = - i \omega \frac{d \mathbf{M^e_{\sigma}}(\mathbf{u})\mathbf{v} }{d\mathbf{m}} - - :param float freq: frequency - :param numpy.ndarray u: solution vector (nE,) - :param numpy.ndarray v: vector to take prodct with (nP,) or (nD,) for - adjoint - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: derivative of the system matrix times a vector (nP,) or - adjoint (nD,) + r"""Conductivity derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}} + + where + + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\sigma}` are the set of model parameters defining the conductivity, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{e}` is the discrete electric field solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}_\boldsymbol{\sigma}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}_\boldsymbol{\sigma}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ dMe_dsig_v = self.MeSigmaDeriv(u, v, adjoint) return 1j * omega(freq) * dMe_dsig_v def getADeriv_mui(self, freq, u, v, adjoint=False): - r""" - Product of the derivative of the system matrix with respect to the - permeability model and a vector. + r"""Inverse permeability derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}} - .. math :: + where - \frac{\mathbf{A}(\mathbf{m}) \mathbf{v}}{d \mathbf{m}_{\mu^{-1}} = - \mathbf{C}^{\top} \frac{d \mathbf{M^f_{\mu^{-1}}}\mathbf{v}}{d\mathbf{m}} + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\mu}` are the set of model parameters defining the permeability, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{e}` is the discrete electric field solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}_\boldsymbol{\mu}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}_\boldsymbol{\mu}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ C = self.mesh.edge_curl @@ -483,6 +753,50 @@ def getADeriv_mui(self, freq, u, v, adjoint=False): return C.T * (self.MfMuiDeriv(C * u) * v) def getADeriv(self, freq, u, v, adjoint=False): + r"""Derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\frac{1}{\mu}} C} + i\omega \mathbf{M_{e\sigma}} + + where + + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters defining the electromagnetic properties + :math:`\mathbf{v}` is a vector and :math:`\mathbf{e}` is the discrete electric field solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, e})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ return ( self.getADeriv_sigma(freq, u, v, adjoint) + self.getADeriv_mui(freq, u, v, adjoint) @@ -490,18 +804,32 @@ def getADeriv(self, freq, u, v, adjoint=False): ) def getRHS(self, freq): - r""" - Right hand side for the system + r"""Right-hand sides for the given frequency. - .. math :: + This method returns the right-hand sides for the frequency provided. + The right-hand side for each source is constructed according to: - \mathbf{RHS} = \mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f}\mathbf{s_m} - - i\omega\mathbf{M_e}\mathbf{s_e} + .. math:: + \mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C^T M_{f\frac{1}{\mu}} s_m } + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrices for inverse permeabilities projected to faces - :param float freq: Frequency - :rtype: numpy.ndarray - :return: RHS (nE, nSrc) + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_edges, n_sources) numpy.ndarray + The right-hand sides. """ s_m, s_e = self.getSourceTerm(freq) @@ -511,9 +839,49 @@ def getRHS(self, freq): return C.T * (MfMui * s_m) - 1j * omega(freq) * s_e def getRHSDeriv(self, freq, src, v, adjoint=False): - """ - Derivative of the Right-hand side with respect to the model. This - includes calls to derivatives in the sources + r"""Derivative of the right-hand side times a vector for a given source and frequency. + + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q} = -i \omega \mathbf{s_e} - i \omega \mathbf{C^T M_{f\frac{1}{\mu}} s_m } + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrices for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : int + The frequency in Hz. + src : .frequency_domain.sources.BaseFDEMSrc + The FDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ C = self.mesh.edge_curl @@ -534,26 +902,99 @@ def getRHSDeriv(self, freq, src, v, adjoint=False): class Simulation3DMagneticFluxDensity(BaseFDEMSimulation): - r""" - We eliminate :math:`\mathbf{e}` using - - .. math :: + r"""3D FDEM simulation in terms of the magnetic flux field. + + This simulation solves for the magnetic flux density at each frequency. + In this formulation, the electric fields are defined on mesh edges and the + magnetic flux density is defined on mesh faces; i.e. it is an EB formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .frequency_domain.survey.Survey + The frequency-domain EM survey. + forward_only : bool, optional + If ``True``, the factorization for the inverse of the system matrix at each + frequency is discarded after the fields are computed at that frequency. + If ``False``, the factorizations of the system matrices for all frequencies are stored. + permittivity : (n_cells,) numpy.ndarray, optional + Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement + is ignored. Please note that `permittivity` is not an invertible property, and that future + development will result in the deprecation of this propery. + storeJ : bool, optional + Whether to compute and store the sensitivity matrix. + + Notes + ----- + Here, we start with the Maxwell's equations in the frequency-domain where a + :math:`+i\omega t` Fourier convention is used: + + .. math:: + \begin{align} + &\nabla \times \vec{E} + i\omega \vec{B} = - i \omega \vec{S}_m \\ + &\nabla \times \vec{H} - \vec{J} = \vec{S}_e + \end{align} + + where :math:`\vec{S}_e` is an electric source term that defines a source current density, + and :math:`\vec{S}_m` magnetic source term that defines a source magnetic flux density. + We define the constitutive relations for the electrical conductivity :math:`\sigma` + and magnetic permeability :math:`\mu` as: + + .. math:: + \vec{J} &= \sigma \vec{E} \\ + \vec{H} &= \mu^{-1} \vec{B} + + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: + + .. math:: + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{E}) \, dv + + i \omega \int_\Omega \vec{u} \cdot \vec{B} \, dv + = - i \omega \int_\Omega \vec{u} \cdot \vec{S}_m \, dv \\ + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{H} \, dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{H} \times \hat{n}) \, da + - \int_\Omega \vec{u} \cdot \vec{J} \, dv + = \int_\Omega \vec{u} \cdot \vec{S}_j \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{J} \, dv = \int_\Omega \vec{u} \cdot \sigma \vec{E} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{H} \, dv = \int_\Omega \vec{u} \cdot \mu^{-1} \vec{B} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete electric fields :math:`\mathbf{e}` are defined on mesh edges, + and the discrete magnetic flux densities :math:`\mathbf{b}` are defined on mesh faces. + This implies :math:`\mathbf{j}` must be defined on mesh edges and :math:`\mathbf{h}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: + + .. math:: + &\mathbf{u_f^T M_f C e} + i \omega \mathbf{u_f^T M_f b} = - i \omega \mathbf{u_f^T M_f s_m} \\ + &\mathbf{u_e^T C^T M_f h} - \mathbf{u_e^T M_e j} = \mathbf{u_e^T s_e} \\ + &\mathbf{u_e^T M_e j} = \mathbf{u_e^T M_{e\sigma} e} \\ + &\mathbf{u_f^T M_f h} = \mathbf{u_f^T M_{f \mu} b} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + By cancelling like-terms and combining the discrete expressions to solve for the magnetic flux density, we obtain: + + .. math:: + \mathbf{A \, b} = \mathbf{q} + + where + + * :math:`\mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I}` + * :math:`\mathbf{q} = \mathbf{C M_{e\sigma}^{-1} s_e} - i \omega \mathbf{s_m}` - \mathbf{e} = \mathbf{M^e_{\sigma}}^{-1} \left(\mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f} \mathbf{b} - \mathbf{s_e}\right) - - and solve for :math:`\mathbf{b}` using: - - .. math :: - - \left(\mathbf{C} \mathbf{M^e_{\sigma}}^{-1} \mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f} + i \omega \right)\mathbf{b} = \mathbf{s_m} + - \mathbf{M^e_{\sigma}}^{-1}\mathbf{M^e}\mathbf{s_e} - - .. note :: - The inverse problem will not work with full anisotropy - - :param discretize.base.BaseMesh mesh: mesh """ _solutionType = "bSolution" @@ -561,17 +1002,32 @@ class Simulation3DMagneticFluxDensity(BaseFDEMSimulation): fieldsPair = Fields3DMagneticFluxDensity def getA(self, freq): - r""" - System matrix + r"""System matrix for the frequency provided. + + This method returns the system matrix for the frequency provided: - .. math :: + .. math:: + \mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the curl operator + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - \mathbf{A} = \mathbf{C} \mathbf{M^e_{\sigma}}^{-1} - \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f} + i \omega + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. - :param float freq: Frequency - :rtype: scipy.sparse.csr_matrix - :return: A + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The system matrix. """ MfMui = self.MfMui @@ -592,23 +1048,51 @@ def getA(self, freq): return A def getADeriv_sigma(self, freq, u, v, adjoint=False): - r""" - Product of the derivative of our system matrix with respect to the - model and a vector - - .. math :: - - \frac{\mathbf{A}(\mathbf{m}) \mathbf{v}}{d \mathbf{m}} = - \mathbf{C} \frac{\mathbf{M^e_{\sigma}} \mathbf{v}}{d\mathbf{m}} - - :param float freq: frequency - :param numpy.ndarray u: solution vector (nF,) - :param numpy.ndarray v: vector to take prodct with (nP,) or (nD,) for - adjoint - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: derivative of the system matrix times a vector (nP,) or - adjoint (nD,) + r"""Conductivity derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the curl operator + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\sigma}` are the set of model parameters defining the conductivity, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{b}` is the discrete magnetic flux density solution, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}_\boldsymbol{\sigma}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}_\boldsymbol{\sigma}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. """ MfMui = self.MfMui @@ -625,6 +1109,52 @@ def getADeriv_sigma(self, freq, u, v, adjoint=False): # return C * (MeSigmaIDeriv * v) def getADeriv_mui(self, freq, u, v, adjoint=False): + r"""Inverse permeability derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the curl operator + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\mu}` are the set of model parameters defining the permeability, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{b}` is the discrete magnetic flux density solution, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}_\boldsymbol{\mu}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}_\boldsymbol{\mu}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ MfMuiDeriv = self.MfMuiDeriv(u) MeSigmaI = self.MeSigmaI C = self.mesh.edge_curl @@ -634,6 +1164,52 @@ def getADeriv_mui(self, freq, u, v, adjoint=False): return C * (MeSigmaI * (C.T * (MfMuiDeriv * v))) def getADeriv(self, freq, u, v, adjoint=False): + r"""Derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the curl operator + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters defining the electromagnetic properties, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{b}` is the discrete magnetic flux density solution, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, b})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ if adjoint is True and self._makeASymmetric: v = self.MfMui * v @@ -647,17 +1223,32 @@ def getADeriv(self, freq, u, v, adjoint=False): return ADeriv def getRHS(self, freq): - r""" - Right hand side for the system + r"""Right-hand sides for the given frequency. + + This method returns the right-hand sides for the frequency provided. + The right-hand side for each source is constructed according to: - .. math :: + .. math:: + \mathbf{q} = \mathbf{C M_{e\sigma}^{-1} s_e} - i \omega \mathbf{s_m } - \mathbf{RHS} = \mathbf{s_m} + - \mathbf{M^e_{\sigma}}^{-1}\mathbf{s_e} + where - :param float freq: Frequency - :rtype: numpy.ndarray - :return: RHS (nE, nSrc) + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_faces, n_sources) numpy.ndarray + The right-hand sides. """ s_m, s_e = self.getSourceTerm(freq) @@ -679,15 +1270,49 @@ def getRHS(self, freq): return RHS def getRHSDeriv(self, freq, src, v, adjoint=False): - """ - Derivative of the right hand side with respect to the model - - :param float freq: frequency - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM src: FDEM source - :param numpy.ndarray v: vector to take product with - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: product of rhs deriv with a vector + r"""Derivative of the right-hand side times a vector for a given source and frequency. + + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q} = \mathbf{C M_{e\sigma}^{-1} s_e} - i \omega \mathbf{s_m } + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : int + The frequency in Hz. + src : .frequency_domain.sources.BaseFDEMSrc + The FDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. """ C = self.mesh.edge_curl @@ -721,30 +1346,99 @@ def getRHSDeriv(self, freq, src, v, adjoint=False): class Simulation3DCurrentDensity(BaseFDEMSimulation): - r""" - We eliminate :math:`mathbf{h}` using - - .. math :: - - \mathbf{h} = \frac{1}{i \omega} \mathbf{M_{\mu}^e}^{-1} - \left(-\mathbf{C}^{\top} \mathbf{M_{\rho}^f} \mathbf{j} + - \mathbf{M^e} \mathbf{s_m} \right) - + r"""3D FDEM simulation in terms of the current density. + + This simulation solves for the current density at each frequency. + In this formulation, the magnetic fields are defined on mesh edges and the + current densities are defined on mesh faces; i.e. it is an HJ formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .frequency_domain.survey.Survey + The frequency-domain EM survey. + forward_only : bool, optional + If ``True``, the factorization for the inverse of the system matrix at each + frequency is discarded after the fields are computed at that frequency. + If ``False``, the factorizations of the system matrices for all frequencies are stored. + permittivity : (n_cells,) numpy.ndarray, optional + Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement + is ignored. Please note that `permittivity` is not an invertible property, and that future + development will result in the deprecation of this propery. + storeJ : bool, optional + Whether to compute and store the sensitivity matrix. + + Notes + ----- + Here, we start with the Maxwell's equations in the frequency-domain where a + :math:`+i\omega t` Fourier convention is used: + + .. math:: + \begin{align} + &\nabla \times \vec{E} + i\omega \vec{B} = - i \omega \vec{S}_m \\ + &\nabla \times \vec{H} - \vec{J} = \vec{S}_e + \end{align} + + where :math:`\vec{S}_e` is an electric source term that defines a source current density, + and :math:`\vec{S}_m` magnetic source term that defines a source magnetic flux density. + For now, we neglect displacement current (the `permittivity` attribute is ``None``). + We define the constitutive relations for the electrical resistivity :math:`\rho` + and magnetic permeability :math:`\mu` as: + + .. math:: + \vec{E} &= \rho \vec{J} \\ + \vec{B} &= \mu \vec{H} + + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: + + .. math:: + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{E} \; dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{E} \times \hat{n} ) \, da + + i \omega \int_\Omega \vec{u} \cdot \vec{B} \, dv + = - i \omega \int_\Omega \vec{u} \cdot \vec{S}_m dv \\ + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{H} ) \, dv + - \int_\Omega \vec{u} \cdot \vec{J} \, dv = \int_\Omega \vec{u} \cdot \vec{S}_j \, dv\\ + & \int_\Omega \vec{u} \cdot \vec{E} \, dv = \int_\Omega \vec{u} \cdot \rho \vec{J} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{B} \, dv = \int_\Omega \vec{u} \cdot \mu \vec{H} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete magnetic fields :math:`\mathbf{h}` are defined on mesh edges, + and the discrete current densities :math:`\mathbf{j}` are defined on mesh faces. + This implies :math:`\mathbf{b}` must be defined on mesh edges and :math:`\mathbf{e}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: + + .. math:: + &\mathbf{u_e^T C^T M_f \, e } + i \omega \mathbf{u_e^T M_e b} = - i\omega \mathbf{u_e^T s_m} \\ + &\mathbf{u_f^T C \, h} - \mathbf{u_f^T j} = \mathbf{u_f^T s_e} \\ + &\mathbf{u_f^T M_f e} = \mathbf{u_f^T M_{f\rho} j} \\ + &\mathbf{u_e^T M_e b} = \mathbf{u_e^T M_{e \mu} h} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + By cancelling like-terms and combining the discrete expressions to solve for the current density, we obtain: + + .. math:: + \mathbf{A \, j} = \mathbf{q} + + where + + * :math:`\mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho} + i\omega \mathbf{I}` + * :math:`\mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C M_{e\mu}^{-1} s_m}` - and solve for :math:`mathbf{j}` using - - .. math :: - - \left(\mathbf{C} \mathbf{M_{\mu}^e}^{-1} \mathbf{C}^{\top} - \mathbf{M_{\rho}^f} + i \omega\right)\mathbf{j} = - \mathbf{C} \mathbf{M_{\mu}^e}^{-1} \mathbf{M^e} \mathbf{s_m} - - i\omega\mathbf{s_e} - - .. note:: - - This implementation does not yet work with full anisotropy!! - - :param discretize.base.BaseMesh mesh: mesh """ _solutionType = "jSolution" @@ -760,17 +1454,31 @@ def __init__( self.permittivity = permittivity def getA(self, freq): - r""" - System matrix + r"""System matrix for the frequency provided. + + This method returns the system matrix for the frequency provided. + The system matrix at each frequency is given by: - .. math :: + .. math:: + \mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges - \mathbf{A} = \mathbf{C} \mathbf{M^e_{\mu^{-1}}} - \mathbf{C}^{\top} \mathbf{M^f_{\sigma^{-1}}} + i\omega + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. - :param float freq: Frequency - :rtype: scipy.sparse.csr_matrix - :return: A + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The system matrix. """ MeMuI = self.MeMuI @@ -791,28 +1499,49 @@ def getA(self, freq): return A def getADeriv_rho(self, freq, u, v, adjoint=False): - r""" - Product of the derivative of our system matrix with respect to the - model and a vector - - In this case, we assume that electrical conductivity, :math:`\sigma` - is the physical property of interest (i.e. :math:`\sigma` = - model.transform). Then we want - - .. math :: - - \frac{\mathbf{A(\sigma)} \mathbf{v}}{d \mathbf{m}} = - \mathbf{C} \mathbf{M^e_{mu^{-1}}} \mathbf{C^{\top}} - \frac{d \mathbf{M^f_{\sigma^{-1}}}\mathbf{v} }{d \mathbf{m}} - - :param float freq: frequency - :param numpy.ndarray u: solution vector (nF,) - :param numpy.ndarray v: vector to take prodct with (nP,) or (nD,) for - adjoint - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: derivative of the system matrix times a vector (nP,) or - adjoint (nD,) + r"""Resistivity derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\rho}` are the set of model parameters defining the resistivity, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{j}` is the discrete current density solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}_\boldsymbol{\sigma}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}_\boldsymbol{\sigma}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. """ MeMuI = self.MeMuI @@ -824,6 +1553,50 @@ def getADeriv_rho(self, freq, u, v, adjoint=False): return C * (MeMuI * (C.T * (self.MfRhoDeriv(u, v, adjoint)))) def getADeriv_mu(self, freq, u, v, adjoint=False): + r"""Permeability derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\mu}` are the set of model parameters defining the permeability, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{j}` is the discrete current density solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}_\boldsymbol{\mu}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}_\boldsymbol{\mu}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ C = self.mesh.edge_curl MfRho = self.MfRho @@ -840,6 +1613,50 @@ def getADeriv_mu(self, freq, u, v, adjoint=False): return Aderiv def getADeriv(self, freq, u, v, adjoint=False): + r"""Derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + i\omega \mathbf{I} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters defining the electromagnetic properties, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{j}` is the discrete current density solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, j})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ if adjoint and self._makeASymmetric: v = self.MfRho * v @@ -853,17 +1670,32 @@ def getADeriv(self, freq, u, v, adjoint=False): return ADeriv def getRHS(self, freq): - r""" - Right hand side for the system + r"""Right-hand sides for the given frequency. + + This method returns the right-hand sides for the frequency provided. + The right-hand side for each source is constructed according to: - .. math :: + .. math:: + \mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C M_{e\mu}^{-1} s_m} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrices for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. - \mathbf{RHS} = \mathbf{C} \mathbf{M_{\mu}^e}^{-1}\mathbf{s_m} - - i\omega \mathbf{s_e} + Parameters + ---------- + freq : float + The frequency in Hz. - :param float freq: Frequency - :rtype: numpy.ndarray - :return: RHS (nE, nSrc) + Returns + ------- + (n_faces, n_sources) numpy.ndarray + The right-hand sides. """ s_m, s_e = self.getSourceTerm(freq) @@ -878,15 +1710,49 @@ def getRHS(self, freq): return RHS def getRHSDeriv(self, freq, src, v, adjoint=False): - """ - Derivative of the right hand side with respect to the model - - :param float freq: frequency - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM src: FDEM source - :param numpy.ndarray v: vector to take product with - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: product of rhs deriv with a vector + r"""Derivative of the right-hand side times a vector for a given source and frequency. + + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q} = - i \omega \mathbf{s_e} - i \omega \mathbf{C M_{e\mu}^{-1} s_m} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrices for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : int + The frequency in Hz. + src : .frequency_domain.sources.BaseFDEMSrc + The FDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. """ # RHS = C * (MeMuI * s_m) - 1j * omega(freq) * s_e @@ -923,22 +1789,99 @@ def getRHSDeriv(self, freq, src, v, adjoint=False): class Simulation3DMagneticField(BaseFDEMSimulation): - r""" - We eliminate :math:`mathbf{j}` using - - .. math :: - - \mathbf{j} = \mathbf{C} \mathbf{h} - \mathbf{s_e} - - and solve for :math:`\mathbf{h}` using + r"""3D FDEM simulation in terms of the magnetic field. + + This simulation solves for the magnetic field at each frequency. + In this formulation, the magnetic fields are defined on mesh edges and the + current densities are defined on mesh faces; i.e. it is an HJ formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .frequency_domain.survey.Survey + The frequency-domain EM survey. + forward_only : bool, optional + If ``True``, the factorization for the inverse of the system matrix at each + frequency is discarded after the fields are computed at that frequency. + If ``False``, the factorizations of the system matrices for all frequencies are stored. + permittivity : (n_cells,) numpy.ndarray, optional + Dielectric permittivity (F/m) defined on the entire mesh. If ``None``, electric displacement + is ignored. Please note that `permittivity` is not an invertible property, and that future + development will result in the deprecation of this propery. + storeJ : bool, optional + Whether to compute and store the sensitivity matrix. + + Notes + ----- + Here, we start with the Maxwell's equations in the frequency-domain where a + :math:`+i\omega t` Fourier convention is used: + + .. math:: + \begin{align} + &\nabla \times \vec{E} + i\omega \vec{B} = - i \omega \vec{S}_m \\ + &\nabla \times \vec{H} - \vec{J} = \vec{S}_e + \end{align} + + where :math:`\vec{S}_e` is an electric source term that defines a source current density, + and :math:`\vec{S}_m` magnetic source term that defines a source magnetic flux density. + For now, we neglect displacement current (the `permittivity` attribute is ``None``). + We define the constitutive relations for the electrical resistivity :math:`\rho` + and magnetic permeability :math:`\mu` as: + + .. math:: + \vec{E} &= \rho \vec{J} \\ + \vec{B} &= \mu \vec{H} + + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: + + .. math:: + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{E} \; dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{E} \times \hat{n} ) \, da + + i \omega \int_\Omega \vec{u} \cdot \vec{B} \, dv + = - i \omega \int_\Omega \vec{u} \cdot \vec{S}_m dv \\ + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{H} ) \, dv + - \int_\Omega \vec{u} \cdot \vec{J} \, dv = \int_\Omega \vec{u} \cdot \vec{S}_j \, dv\\ + & \int_\Omega \vec{u} \cdot \vec{E} \, dv = \int_\Omega \vec{u} \cdot \rho \vec{J} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{B} \, dv = \int_\Omega \vec{u} \cdot \mu \vec{H} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete magnetic fields :math:`\mathbf{h}` are defined on mesh edges, + and the discrete current densities :math:`\mathbf{j}` are defined on mesh faces. + This implies :math:`\mathbf{b}` must be defined on mesh edges and :math:`\mathbf{e}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: + + .. math:: + &\mathbf{u_e^T C^T M_f \, e } + i \omega \mathbf{u_e^T M_e b} = - i\omega \mathbf{u_e^T s_m} \\ + &\mathbf{u_f^T C \, h} - \mathbf{u_f^T j} = \mathbf{u_f^T s_e} \\ + &\mathbf{u_f^T M_f e} = \mathbf{u_f^T M_{f\rho} j} \\ + &\mathbf{u_e^T M_e b} = \mathbf{u_e^T M_{e \mu} h} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + By cancelling like-terms and combining the discrete expressions to solve for the magnetic field, we obtain: + + .. math:: + \mathbf{A \, h} = \mathbf{q} + + where + + * :math:`\mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}}` + * :math:`\mathbf{q} = \mathbf{C^T M_{f\rho} s_e} - i\omega \mathbf{s_m}` - .. math :: - - \left(\mathbf{C}^{\top} \mathbf{M_{\rho}^f} \mathbf{C} + - i \omega \mathbf{M_{\mu}^e}\right) \mathbf{h} = \mathbf{M^e} - \mathbf{s_m} + \mathbf{C}^{\top} \mathbf{M_{\rho}^f} \mathbf{s_e} - - :param discretize.base.BaseMesh mesh: mesh """ _solutionType = "hSolution" @@ -946,19 +1889,31 @@ class Simulation3DMagneticField(BaseFDEMSimulation): fieldsPair = Fields3DMagneticField def getA(self, freq): - r""" - System matrix + r"""System matrix for the frequency provided. + + This method returns the system matrix for the frequency provided. + The system matrix at each frequency is given by: .. math:: + \mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}} - \mathbf{A} = \mathbf{C}^{\top} \mathbf{M_{\rho}^f} \mathbf{C} + - i \omega \mathbf{M_{\mu}^e} + where + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges - :param float freq: Frequency - :rtype: scipy.sparse.csr_matrix - :return: A + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The system matrix. """ MeMu = self.MeMu @@ -974,24 +1929,49 @@ def getA(self, freq): return C.T.tocsr() * (Mfyhati * C) + 1j * omega(freq) * MeMu def getADeriv_rho(self, freq, u, v, adjoint=False): - r""" - Product of the derivative of our system matrix with respect to the - model and a vector + r"""Resistivity derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\sigma}` are the set of model parameters defining the conductivity, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{h}` is the discrete magnetic field solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}_\boldsymbol{\sigma}} \, \mathbf{v} + + Or the adjoint operation .. math:: + \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}_\boldsymbol{\sigma}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. - \frac{\mathbf{A}(\mathbf{m}) \mathbf{v}}{d \mathbf{m}} = - \mathbf{C}^{\top}\frac{d \mathbf{M^f_{\rho}}\mathbf{v}} - {d\mathbf{m}} - - :param float freq: frequency - :param numpy.ndarray u: solution vector (nE,) - :param numpy.ndarray v: vector to take prodct with (nP,) or (nD,) for - adjoint - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: derivative of the system matrix times a vector (nP,) or - adjoint (nD,) + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ C = self.mesh.edge_curl if adjoint: @@ -999,6 +1979,50 @@ def getADeriv_rho(self, freq, u, v, adjoint=False): return C.T * self.MfRhoDeriv(C * u, v, adjoint) def getADeriv_mu(self, freq, u, v, adjoint=False): + r"""Permeability derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}_\boldsymbol{\mu}` are the set of model parameters defining the permeability, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{h}` is the discrete magnetic field solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}_\boldsymbol{\mu}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}_\boldsymbol{\mu}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ MeMuDeriv = self.MeMuDeriv(u) if adjoint is True: @@ -1007,23 +2031,82 @@ def getADeriv_mu(self, freq, u, v, adjoint=False): return 1j * omega(freq) * (MeMuDeriv * v) def getADeriv(self, freq, u, v, adjoint=False): + r"""Derivative operation for the system matrix times a vector. + + The system matrix at each frequency is given by: + + .. math:: + \mathbf{A} = \mathbf{C^T M_{f\rho} C} + i\omega \mathbf{M_{e\mu}} + + where + + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters defining the electromagnetic properties, + :math:`\mathbf{v}` is a vector and :math:`\mathbf{h}` is the discrete electric field solution, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A \, h})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : float + The frequency in Hz. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model at the specified frequency. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ return self.getADeriv_rho(freq, u, v, adjoint) + self.getADeriv_mu( freq, u, v, adjoint ) def getRHS(self, freq): - r""" - Right hand side for the system + r"""Right-hand sides for the given frequency. + + This method returns the right-hand sides for the frequency provided. + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q} = \mathbf{C^T M_{f\rho} s_e} - i\omega \mathbf{s_m} + + where - .. math :: + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrices for permeabilities projected to edges + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrices for resistivities projected to faces - \mathbf{RHS} = \mathbf{M^e} \mathbf{s_m} + \mathbf{C}^{\top} - \mathbf{M_{\rho}^f} \mathbf{s_e} + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. - :param float freq: Frequency - :rtype: numpy.ndarray - :return: RHS (nE, nSrc) + Parameters + ---------- + freq : float + The frequency in Hz. + Returns + ------- + (n_edges, n_sources) numpy.ndarray + The right-hand sides. """ s_m, s_e = self.getSourceTerm(freq) @@ -1039,15 +2122,50 @@ def getRHS(self, freq): return s_m + C.T * (Mfyhati * s_e) def getRHSDeriv(self, freq, src, v, adjoint=False): - """ - Derivative of the right hand side with respect to the model - - :param float freq: frequency - :param simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM src: FDEM source - :param numpy.ndarray v: vector to take product with - :param bool adjoint: adjoint? - :rtype: numpy.ndarray - :return: product of rhs deriv with a vector + r"""Derivative of the right-hand side times a vector for a given source and frequency. + + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q} = \mathbf{C^T M_{f\rho} s_e} - i\omega \mathbf{s_m} + + where + + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrices for permeabilities projected to edges + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrices for resistivities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + freq : int + The frequency in Hz. + src : .frequency_domain.sources.BaseFDEMSrc + The FDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ _, s_e = src.eval(self) diff --git a/simpeg/electromagnetics/frequency_domain/simulation_1d.py b/simpeg/electromagnetics/frequency_domain/simulation_1d.py index 7d880dd09c..c51e8e3d33 100644 --- a/simpeg/electromagnetics/frequency_domain/simulation_1d.py +++ b/simpeg/electromagnetics/frequency_domain/simulation_1d.py @@ -127,7 +127,27 @@ def fields(self, m): return self._project_to_data(v) - def getJ(self, m, f=None): + def _getJ(self, m, f=None): + """Build Jacobian matrix by blocks. + + This method builds the Jacobian matrix by blocks, each block for a particular + invertible property (receiver height, conductivity, permeability, layer + thickness). Each block of the Jacobian matrix is stored within a dictionary. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + dict + Dictionary containing the blocks of the Jacobian matrix for the invertible + properties. The keys of the dictionary can be `"dh"`, `"ds"`, `"dmu"`, and + `"dthick"`. + """ self.model = m if getattr(self, "_J", None) is None: self._J = {} @@ -174,9 +194,9 @@ def getJ(self, m, f=None): rTE = rTE_forward(frequencies, unique_lambs, sig, mu, self.thicknesses) rTE = rTE[i_freq] rTE = np.take_along_axis(rTE, inv_lambs, axis=1) - v_dh_temp = (C0s_dh * rTE) @ self._fhtfilt.j0 + ( - C1s_dh * rTE - ) @ self._fhtfilt.j1 + v_dh_temp = ((C0s_dh * rTE) @ self._fhtfilt.j0).real + ( + (C1s_dh * rTE) @ self._fhtfilt.j1 + ).real v_dh_temp += W @ v_dh_temp # need to re-arange v_dh as it's currently (n_data x 1) # however it already contains all the relevant information... @@ -244,6 +264,44 @@ def getJ(self, m, f=None): self._J["dthick"] = self._project_to_data(v_dthick) return self._J + def getJ(self, m, f=None): + r"""Get the Jacobian matrix. + + This method generates and stores the full Jacobian matrix for the + model provided. I.e.: + + .. math:: + \mathbf{J} = \dfrac{\partial f(\mu(\mathbf{m}))}{\partial \mathbf{m}} + + where :math:`f()` is the forward modelling function, :math:`\mu()` is the + mapping, and :math:`\mathbf{m}` is the model vector. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (n_data, n_param) numpy.ndarray + The full Jacobian matrix. + """ + Js = self._getJ(m, f=f) + # Map parameters with their corresponding derivatives + param_and_derivs = { + "dh": self.hDeriv, + "ds": self.sigmaDeriv, + "dmu": self.muDeriv, + "dthick": self.thicknessesDeriv, + } + + # Compute J matrix + J = sum(Js[param] @ param_and_derivs[param] for param in Js) + + return J + def _project_to_data(self, v): i_dat = 0 i_v = 0 @@ -288,6 +346,10 @@ def _project_to_data(self, v): out[i_dat:i_dat_p1] = v_slice.real elif rx.component == "imag": out[i_dat:i_dat_p1] = v_slice.imag + else: + raise NotImplementedError( + f"receiver component {rx.component} not implemented." + ) i_dat = i_dat_p1 i_v = i_v_p1 return out diff --git a/simpeg/electromagnetics/frequency_domain/sources.py b/simpeg/electromagnetics/frequency_domain/sources.py index c92ab26b3c..dc6624823b 100644 --- a/simpeg/electromagnetics/frequency_domain/sources.py +++ b/simpeg/electromagnetics/frequency_domain/sources.py @@ -14,7 +14,6 @@ validate_direction, validate_integer, ) -from ...utils.code_utils import deprecate_property from ..utils import omega from ..utils import segmented_line_current_source_term, line_through_faces @@ -360,6 +359,12 @@ class MagDipole(BaseFDEMSrc): \mathbf{M_{\sigma}^e} \mathbf{e^S} = -\mathbf{C}^T \mathbf{{M_{\mu^{-1}}^f}^S} \mathbf{b^P}} + To obtain $\mathbf{b^P}$, we compute it by taking the curl of the vector potential due to a point dipole. This is provided by :py:meth:`geoana.em.static.MagneticDipoleWholeSpace.vector_potential`. Specifically, + + .. math:: + + \vec{B}^P = \nabla \times \vec{A} + Parameters ---------- receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx @@ -473,7 +478,9 @@ def _dipole(self): return self.__dipole def _srcFct(self, obsLoc, coordinates="cartesian"): - return self._dipole.vector_potential(obsLoc, coordinates=coordinates) + out = self._dipole.vector_potential(obsLoc, coordinates=coordinates) + out[np.isnan(out)] = 0 + return out def bPrimary(self, simulation): """Compute primary magnetic flux density. @@ -491,40 +498,31 @@ def bPrimary(self, simulation): numpy.ndarray Primary magnetic flux density """ - formulation = simulation._formulation coordinates = "cartesian" - if formulation == "EB": - gridX = simulation.mesh.gridEx - gridY = simulation.mesh.gridEy - gridZ = simulation.mesh.gridEz + if simulation._formulation == "EB": C = simulation.mesh.edge_curl - elif formulation == "HJ": - gridX = simulation.mesh.gridFx - gridY = simulation.mesh.gridFy - gridZ = simulation.mesh.gridFz - C = simulation.mesh.edge_curl.T - - if simulation.mesh._meshType == "CYL": - coordinates = "cylindrical" - - if simulation.mesh.is_symmetric is True: - if not (np.linalg.norm(self.orientation - np.r_[0.0, 0.0, 1.0]) < 1e-6): - raise AssertionError( - "for cylindrical symmetry, the dipole must be oriented" - " in the Z direction" - ) - a = self._srcFct(gridY)[:, 1] + if simulation.mesh._meshType == "CYL": + coordinates = "cylindrical" - return C * a + if simulation.mesh.is_symmetric is True: + if not ( + np.linalg.norm(self.orientation - np.r_[0.0, 0.0, 1.0]) < 1e-6 + ): + raise AssertionError( + "for cylindrical symmetry, the dipole must be oriented" + " in the Z direction" + ) + a = self._srcFct(simulation.mesh.edges_y, coordinates)[:, 1] + return C * a - ax = self._srcFct(gridX, coordinates)[:, 0] - ay = self._srcFct(gridY, coordinates)[:, 1] - az = self._srcFct(gridZ, coordinates)[:, 2] - a = np.concatenate((ax, ay, az)) + avec = self._srcFct(simulation.mesh.edges, coordinates) + a = simulation.mesh.project_edge_vector(avec) + return C * a - return C * a + elif simulation._formulation == "HJ": + return self.mu * self.hPrimary(simulation) def hPrimary(self, simulation): """Compute primary magnetic field. @@ -555,8 +553,37 @@ def hPrimary(self, simulation): out.append(h_rx @ rx.orientation) self._1d_h = out return self._1d_h - b = self.bPrimary(simulation) - return 1.0 / self.mu * b + + if simulation._formulation == "EB": + b = self.bPrimary(simulation) + return ( + 1.0 / self.mu * b + ) # same as MfI * Mfmui * b (mu primary must be a scalar) + + elif simulation._formulation == "HJ": + coordinates = "cartesian" + if simulation.mesh._meshType == "CYL": + coordinates = "cylindrical" + if simulation.mesh.is_symmetric is True: + raise AssertionError( + "for cylindrical symmetry, you must use the EB formulation for the simulation" + ) + + avec = self._srcFct(simulation.mesh.faces, coordinates) + a = simulation.mesh.project_face_vector(avec) + + a_boundary = mkvc(self._srcFct(simulation.mesh.boundary_edges)) + a_bc = simulation.mesh.boundary_edge_vector_integral * a_boundary + + return ( + 1.0 + / self.mu + * simulation.MeI + * simulation.mesh.edge_curl.T + * simulation.Mf + * a + - 1 / self.mu * simulation.MeI * a_bc + ) def s_m(self, simulation): """Magnetic source term (s_m) @@ -572,10 +599,13 @@ def s_m(self, simulation): Magnetic source term on mesh. """ - b_p = self.bPrimary(simulation) - if simulation._formulation == "HJ": - b_p = simulation.Me * b_p - return -1j * omega(self.frequency) * b_p + if simulation._formulation == "EB": + b_p = self.bPrimary(simulation) + return -1j * omega(self.frequency) * b_p + elif simulation._formulation == "HJ": + h_p = self.hPrimary(simulation) + MeMu = simulation.MeMu + return -1j * omega(self.frequency) * MeMu * h_p def s_e(self, simulation): """Electric source term (s_e) @@ -683,7 +713,9 @@ def _srcFct(self, obsLoc, coordinates="cartesian"): location=self.location, moment=self.moment, ) - return self._dipole.magnetic_flux_density(obsLoc, coordinates=coordinates) + out = self._dipole.magnetic_flux_density(obsLoc, coordinates=coordinates) + out[np.isnan(out)] = 0 + return out def bPrimary(self, simulation): """ @@ -769,10 +801,6 @@ def __init__( **kwargs, ): kwargs.pop("moment", None) - - # Raise error on deprecated arguments - if (key := "N") in kwargs.keys(): - raise TypeError(f"'{key}' property has been removed. Please use 'n_turns'.") self.n_turns = n_turns super().__init__( @@ -869,11 +897,9 @@ def _srcFct(self, obsLoc, coordinates="cartesian"): radius=self.radius, current=self.current, ) - return self.n_turns * self._loop.vector_potential(obsLoc, coordinates) - - N = deprecate_property( - n_turns, "N", "n_turns", removal_version="0.19.0", error=True - ) + out = self._loop.vector_potential(obsLoc, coordinates) + out[np.isnan(out)] = 0 + return self.n_turns * out class PrimSecSigma(BaseFDEMSrc): @@ -1306,7 +1332,7 @@ def Mejs(self, simulation): if getattr(self, "_Mejs", None) is None: mesh = simulation.mesh locs = self.location - self._Mejs = self.current * segmented_line_current_source_term(mesh, locs) + self._Mejs = segmented_line_current_source_term(mesh, locs) return self.current * self._Mejs def Mfjs(self, simulation): diff --git a/simpeg/electromagnetics/natural_source/__init__.py b/simpeg/electromagnetics/natural_source/__init__.py index dca9d80cc5..337d6da777 100644 --- a/simpeg/electromagnetics/natural_source/__init__.py +++ b/simpeg/electromagnetics/natural_source/__init__.py @@ -23,8 +23,10 @@ .. autosummary:: :toctree: generated/ - receivers.PointNaturalSource - receivers.Point3DTipper + receivers.Impedance + receivers.Admittance + receivers.ApparentConductivity + receivers.Tipper Sources ======= diff --git a/simpeg/electromagnetics/natural_source/fields.py b/simpeg/electromagnetics/natural_source/fields.py index 2493c7f0bd..3ba8507807 100644 --- a/simpeg/electromagnetics/natural_source/fields.py +++ b/simpeg/electromagnetics/natural_source/fields.py @@ -681,7 +681,7 @@ def _b_pyDeriv(self, src, du_dm_v, adjoint=False): # Primary does not depend on u return np.array( self._b_pyDeriv_u(src, du_dm_v, adjoint) - + self._b_pyDeriv_m(src, v, adjoint), + + self._b_pyDeriv_m(src, du_dm_v, adjoint), complex, ) diff --git a/simpeg/electromagnetics/natural_source/receivers.py b/simpeg/electromagnetics/natural_source/receivers.py index 9e76a2fb4e..dcda2c43fd 100644 --- a/simpeg/electromagnetics/natural_source/receivers.py +++ b/simpeg/electromagnetics/natural_source/receivers.py @@ -1,8 +1,11 @@ -from ...utils.code_utils import validate_string - +from ...utils.code_utils import ( + validate_string, + validate_type, + validate_ndarray_with_shape, + deprecate_class, +) import numpy as np from scipy.constants import mu_0 - from ...survey import BaseRx @@ -10,67 +13,236 @@ def _alpha(src): return 1 / (2 * np.pi * mu_0 * src.frequency) -class PointNaturalSource(BaseRx): - """Point receiver class for magnetotelluric simulations. +class BaseNaturalSourceRx(BaseRx): + """ + Base class for natural source electromagnetic receivers. + + Parameters + ---------- + locations1, locations2 : (n_loc, n_dim) array_like + Locations where the two fields are measured. + **kwargs + Additional keyword arguments passed to `simpeg.BaseRx`. + """ + + _loc_names = ("First", "Second") + + def __init__(self, locations1, locations2, **kwargs): + super().__init__(locations=(locations1, locations2), **kwargs) + + @property + def locations(self): + """Locations of the two field measurements. + + Locations where the two fields are measured for the receiver. + The name of the field is dependant upon the MT receiver, but + for common MT receivers, these would be the electric field + and magnetic field measurement locations. + + Returns + ------- + locations1, locations2 : (n_loc, n_dim) numpy.ndarray + """ + return self._locations + + @locations.setter + def locations(self, locs): + locs = validate_type("locations", locs, tuple) + try: + loc0, loc1 = locs + except ValueError: + raise ValueError( + f"locations must have two values to unpack, got {len(locs)}" + ) + # check that they are both numpy arrays and have the same shape. + loc0 = validate_ndarray_with_shape( + f"{self._loc_names[0]} locations", loc0, shape=("*", "*") + ) + loc1 = validate_ndarray_with_shape( + f"{self._loc_names[1]} locations", loc1, shape=loc0.shape + ) + self._locations = (loc0, loc1) + # make sure projection matrices are cleared + self._Ps = {} + + @property + def nD(self): + """Number of data associated with the receiver object. + + Returns + ------- + int + Number of data associated with the receiver object. + """ + + return self._locations[0].shape[0] + + def getP(self, mesh, projected_grid, location_id=0): + """Get projection matrix from mesh to specified receiver locations. + + Natural source electromagnetic data may be computed from field measurements + at one or two locations. The `getP` method returns the projection matrix from + the mesh to the appropriate receiver locations. `location_id=0` is used to + project from the mesh to the set of roving receiver locations. `location_id=1` + is used when horizontal fields used to compute NSEM data are measured at a + base station. + + Parameters + ---------- + mesh : discretize.BaseMesh + A discretize mesh. + projected_grid : str + Define what part of the mesh (i.e. edges, faces, centers, nodes) to + project from. Must be one of:: + + 'Ex', 'edges_x' -> x-component of field defined on x edges + 'Ey', 'edges_y' -> y-component of field defined on y edges + 'Ez', 'edges_z' -> z-component of field defined on z edges + 'Fx', 'faces_x' -> x-component of field defined on x faces + 'Fy', 'faces_y' -> y-component of field defined on y faces + 'Fz', 'faces_z' -> z-component of field defined on z faces + 'N', 'nodes' -> scalar field defined on nodes + 'CC', 'cell_centers' -> scalar field defined on cell centers + 'CCVx', 'cell_centers_x' -> x-component of vector field defined on cell centers + 'CCVy', 'cell_centers_y' -> y-component of vector field defined on cell centers + 'CCVz', 'cell_centers_z' -> z-component of vector field defined on cell centers + + locations_id : int + Receiver locations ID. 0 used for roving locations. 1 used for base station locations. + + Returns + ------- + scipy.sparse.csr_matrix + P, the interpolation matrix. + """ + key = (mesh, projected_grid, location_id) + if key in self._Ps: + return self._Ps[key] + locs = self._locations[location_id] + P = mesh.get_interpolation_matrix(locs, projected_grid) + if self.storeProjections: + self._Ps[key] = P + return P + - Assumes that the data locations are standard xyz coordinates; - i.e. (x,y,z) is (Easting, Northing, up). +class _ElectricAndMagneticReceiver(BaseNaturalSourceRx): + """ + Intermediate class for MT receivers that measure an electric and magnetic field + """ + + _loc_names = ("Electric field", "Magnetic field") + + @property + def locations_e(self): + """Electric field measurement locations + + Returns + ------- + numpy.ndarray + Location where the electric field is measured for all receiver data + """ + return self._locations[0] + + @property + def locations_h(self): + """Magnetic field measurement locations + + Returns + ------- + numpy.ndarray + Location where the magnetic field is measured for all receiver data + """ + return self._locations[1] + + +class Impedance(_ElectricAndMagneticReceiver): + r"""Receiver class for 1D, 2D and 3D impedance data. + + This class is used to simulate data types that can be derived from the impedance tensor: + + .. math:: + \begin{bmatrix} Z_{xx} & Z_{xy} \\ Z_{yx} & Z_{yy} \end{bmatrix} = + \begin{bmatrix} E_x^{(x)} & E_x^{(y)} \\ E_y^{(x)} & E_y^{(y)} \end{bmatrix} \, + \begin{bmatrix} H_x^{(x)} & H_x^{(y)} \\ H_y^{(x)} & H_y^{(y)} \end{bmatrix}^{-1} + + where superscripts :math:`(x)` and :math:`(y)` denote signals corresponding to + incident planewaves whose electric fields are polarized along the x and y-directions + respectively. Electric and magnetic fields do not need to be simulated at the same + location, so this class can be used to simulate quasi-impedance data; i.e. where + the electric fields are measured at a base station. + + Note that in ``simpeg``, natural source EM data are defined according to + standard xyz coordinates; i.e. (x,y,z) is (Easting, Northing, Z +ve up). + + In addition to measuring the real or imaginary component of an impedance tensor + element :math:`Z_{ij}`, the receiver object can be set to measure the + the apparent resistivity: + + .. math:: + \rho_{ij} = \dfrac{| Z_{ij} \, |^2}{\mu_0 \omega} + + or the phase angle: + + .. math:: + \phi_{ij} = \frac{180}{\pi} \, + \tan^{-1} \Bigg ( \dfrac{Im[Z_{ij}]}{Re[Z_{ij}]} \Bigg ) + + where :math:`\mu_0` is the permeability of free-space and :math:`\omega` is the + angular frequency in rad/s. The phase angle is represented in degrees and + is computed by: Parameters ---------- - locations : (n_loc, n_dim) numpy.ndarray - Receiver locations. + locations_e : (n_loc, n_dim) array_like + Locations where the electric fields are measured. + locations_h : (n_loc, n_dim) array_like, optional + Locations where the magnetic fields are measured. Defaults to the same + locations as electric field measurements, `locations_e`. orientation : {'xx', 'xy', 'yx', 'yy'} - MT receiver orientation. - component : {'real', 'imag', 'apparent_resistivity', 'phase'} - MT data type. + Receiver orientation. Specifies whether the receiver's data correspond to + the :math:`Z_{xx}`, :math:`Z_{xy}`, :math:`Z_{yx}` or :math:`Z_{yy}` impedance. + The data type is specified by the `component` input argument. + component : {'real', 'imag', 'apparent_resistivity', 'phase', 'complex'} + Data type. For the impedance element :math:`Z_{ij}` specified by the `orientation` + input argument, the receiver can be set to compute the following: + - 'real': Real component of the impedance (V/A) + - 'imag': Imaginary component of the impedance (V/A) + - 'rho': Apparent resistivity (:math:`\Omega m`) + - 'phase': Phase angle (degrees) + - 'complex': The complex impedance is returned. Do not use for inversion! + storeProjections : bool + Whether to cache to internal projection matrices. """ def __init__( self, - locations=None, - orientation="xy", - component="real", - locations_e=None, + locations_e, locations_h=None, + orientation="xx", + component="real", + storeProjections=False, ): + if locations_h is None: + locations_h = locations_e + super().__init__( + locations1=locations_e, + locations2=locations_h, + storeProjections=storeProjections, + ) self.orientation = orientation self.component = component - # check if locations_e or h have been provided - if (locations_e is not None) and (locations_h is not None): - # check that locations are same size - if locations_e.size == locations_h.size: - self._locations_e = locations_e - self._locations_h = locations_h - else: - raise Exception("location h needs to be same size as location e") - - locations = np.hstack([locations_e, locations_h]) - elif locations is not None: - # check shape of locations - if isinstance(locations, list): - if len(locations) == 2: - self._locations_e = locations[0] - self._locations_h = locations[1] - elif len(locations) == 1: - self._locations_e = locations[0] - self._locations_h = locations[0] - else: - raise Exception("incorrect size of list, must be length of 1 or 2") - locations = locations[0] - elif isinstance(locations, np.ndarray): - self._locations_e = locations - self._locations_h = locations - else: - raise Exception("locations need to be either a list or numpy array") - else: - locations = np.array([[0.0]]) - super().__init__(locations) - @property def component(self): - """Data type; i.e. "real", "imag", "apparent_resistivity", "phase" + r"""Data type; i.e. "real", "imag", "apparent_resistivity", "phase" + + For the impedance element :math:`Z_{ij}`, the `component` property specifies + whether the data are: + - 'real': Real component of the impedance (V/A) + - 'imag': Imaginary component of the impedance (V/A) + - 'rho': Apparent resistivity (:math:`\Omega m`) + - 'phase': Phase angle (degrees) + - 'complex': Complex impedance (V/A) Returns ------- @@ -100,17 +272,22 @@ def component(self, var): "rhoa", ), ("phase", "phi"), + "complex", ], ) @property def orientation(self): - """Orientation of the receiver. + """Receiver orientation. + + Specifies whether the receiver's data correspond to + the :math:`Z_{xx}`, :math:`Z_{xy}`, :math:`Z_{yx}` or :math:`Z_{yy}` impedance. + The data type is specified by the `component` input argument. Returns ------- str - Orientation of the receiver. One of {'xx', 'xy', 'yx', 'yy'} + Receiver orientation. One of {'xx', 'xy', 'yx', 'yy'} """ return self._orientation @@ -120,85 +297,19 @@ def orientation(self, var): "orientation", var, string_list=("xx", "xy", "yx", "yy") ) - @property - def locations_e(self): - """Electric field measurement locations - - Returns - ------- - numpy.ndarray - Location where the electric field is measured for all receiver data - """ - return self._locations_e - - @property - def locations_h(self): - """Magnetic field measurement locations - - Returns - ------- - numpy.ndarray - Location where the magnetic field is measured for all receiver data - """ - return self._locations_h - - def getP(self, mesh, projected_grid, field="e"): - """Projection matrices for all components collected by the receivers - - Note projection matrices are stored as a dictionary listed by meshes. - - Parameters - ---------- - mesh : discretize.base.BaseMesh - The mesh on which the discrete set of equations is solved - projected_grid : str - Define what part of the mesh (i.e. edges, faces, centers, nodes) to - project from. Must be one of:: - - 'Ex', 'edges_x' -> x-component of field defined on x edges - 'Ey', 'edges_y' -> y-component of field defined on y edges - 'Ez', 'edges_z' -> z-component of field defined on z edges - 'Fx', 'faces_x' -> x-component of field defined on x faces - 'Fy', 'faces_y' -> y-component of field defined on y faces - 'Fz', 'faces_z' -> z-component of field defined on z faces - 'N', 'nodes' -> scalar field defined on nodes - 'CC', 'cell_centers' -> scalar field defined on cell centers - 'CCVx', 'cell_centers_x' -> x-component of vector field defined on cell centers - 'CCVy', 'cell_centers_y' -> y-component of vector field defined on cell centers - 'CCVz', 'cell_centers_z' -> z-component of vector field defined on cell centers - - field : str, default = "e" - Whether to project electric or magnetic fields from mesh. - Choose "e" or "h" - """ - if mesh.dim < 3: - return super().getP(mesh, projected_grid) - - if (mesh, projected_grid) in self._Ps: - return self._Ps[(mesh, projected_grid, field)] - - if field == "e": - locs = self.locations_e - else: - locs = self.locations_h - P = mesh.get_interpolation_matrix(locs, projected_grid) - if self.storeProjections: - self._Ps[(mesh, projected_grid, field)] = P - return P - def _eval_impedance(self, src, mesh, f): if mesh.dim < 3 and self.orientation in ["xx", "yy"]: - return 0.0 + return np.zeros((self.nD, 1), dtype=complex) e = f[src, "e"] h = f[src, "h"] if mesh.dim == 3: if self.orientation[0] == "x": - e = self.getP(mesh, "Ex", "e") @ e + e = self.getP(mesh, "Ex", 0) @ e else: - e = self.getP(mesh, "Ey", "e") @ e + e = self.getP(mesh, "Ey", 0) @ e - hx = self.getP(mesh, "Fx", "h") @ h - hy = self.getP(mesh, "Fy", "h") @ h + hx = self.getP(mesh, "Fx", 1) @ h + hy = self.getP(mesh, "Fy", 1) @ h if self.orientation[1] == "x": h = hy else: @@ -225,7 +336,7 @@ def _eval_impedance(self, src, mesh, f): # need to negate if 'yx' and fields are xy # and as well if 'xy' and fields are 'yx' if mesh.dim == 1 and self.orientation != f.field_directions: - bot = -bot + bot *= -1 return top / bot def _eval_impedance_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): @@ -238,14 +349,14 @@ def _eval_impedance_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=Fals h = f[src, "h"] if mesh.dim == 3: if self.orientation[0] == "x": - Pe = self.getP(mesh, "Ex", "e") + Pe = self.getP(mesh, "Ex", 0) e = Pe @ e else: - Pe = self.getP(mesh, "Ey", "e") + Pe = self.getP(mesh, "Ey", 0) e = Pe @ e - Phx = self.getP(mesh, "Fx", "h") - Phy = self.getP(mesh, "Fy", "h") + Phx = self.getP(mesh, "Fx", 1) + Phy = self.getP(mesh, "Fy", 1) hx = Phx @ h hy = Phy @ h if self.orientation[1] == "x": @@ -274,7 +385,7 @@ def _eval_impedance_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=Fals bot = PH @ h[:, 0] if mesh.dim == 1 and self.orientation != f.field_directions: - bot = -bot + bot *= -1 imp = top / bot @@ -355,7 +466,7 @@ def _eval_impedance_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=Fals dh_v = PH @ f._hDeriv(src, du_dm_v, v, adjoint=False) if mesh.dim == 1 and self.orientation != f.field_directions: - dh_v = -dh_v + dh_v *= -1 imp_deriv = (de_v - imp * dh_v) / bot @@ -375,29 +486,26 @@ def _eval_impedance_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=Fals rx_deriv = getattr(imp_deriv, self.component) return rx_deriv - def eval(self, src, mesh, f, return_complex=False): # noqa: A003 - """ - Project the fields to natural source data. + def eval(self, src, mesh, f): # noqa: A003 + """Compute receiver data from the discrete field solution. Parameters ---------- - src : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc - NSEM source - mesh : discretize.TensorMesh mesh - Mesh on which the discretize solution is obtained + src : .frequency_domain.sources.BaseFDEMSrc + NSEM source. + mesh : discretize.TensorMesh + Mesh on which the discretize solution is obtained. f : simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM - NSEM fields object of the source - return_complex : bool (optional) - Flag for return the complex evaluation + NSEM fields object of the source. Returns ------- numpy.ndarray - Evaluated data for the receiver + Evaluated data for the receiver. """ imp = self._eval_impedance(src, mesh, f) - if return_complex: + if self.component == "complex": return imp elif self.component == "apparent_resistivity": return _alpha(src) * (imp.real**2 + imp.imag**2) @@ -407,96 +515,206 @@ def eval(self, src, mesh, f, return_complex=False): # noqa: A003 return getattr(imp, self.component) def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): - """Derivative of projection with respect to the fields + r"""Derivative of data with respect to the fields. + + Let :math:`\mathbf{d}` represent the data corresponding the receiver object. + And let :math:`\mathbf{u}` represent the discrete numerical solution of the + fields on the mesh. Where :math:`\mathbf{P}` is a projection function that + maps from the fields to the data, i.e.: + + .. math:: + \mathbf{d} = \mathbf{P}(\mathbf{u}) + + this method computes and returns the derivative: + + .. math:: + \dfrac{\partial \mathbf{d}}{\partial \mathbf{u}} = + \dfrac{\partial [ \mathbf{P} (\mathbf{u}) ]}{\partial \mathbf{u}} Parameters ---------- - str : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc - NSEM source + str : .frequency_domain.sources.BaseFDEMSrc + The NSEM source. mesh : discretize.TensorMesh - Mesh on which the discretize solution is obtained + Mesh on which the discretize solution is obtained. f : simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM - NSEM fields object of the source - du_dm_v : None, + NSEM fields object for the source. + du_dm_v : None, optional Supply pre-computed derivative? - v : numpy.ndarray + v : numpy.ndarray, optional Vector of size - adjoint : bool, default = ``False`` - If ``True``, compute the adjoint operation + adjoint : bool, optional + Whether to compute the ajoint operation. Returns ------- numpy.ndarray - Calculated derivative (nD,) (adjoint=False) and (nP,2) (adjoint=True) for both polarizations + Calculated derivative (n_data,) if `adjoint` is ``False``, and (n_param, 2) if `adjoint` + is ``True``, for both polarizations. """ + if self.component == "complex": + raise NotImplementedError( + "complex valued data derivative is not implemented." + ) return self._eval_impedance_deriv( src, mesh, f, du_dm_v=du_dm_v, v=v, adjoint=adjoint ) -class Point3DTipper(PointNaturalSource): - """Point receiver class for Z-axis tipper simulations. +class Tipper(BaseNaturalSourceRx): + r"""Receiver class for tipper data (3D problems only). + + This class can be used to simulate AFMag tipper data, defined according to: + + .. math:: + \begin{bmatrix} T_{zx} & T_{zy} \end{bmatrix} = + \begin{bmatrix} H_x^{(x)} & H_y^{(x)} \\ H_x^{(y)} & H_y^{(y)} \end{bmatrix}^{-1} \, + \begin{bmatrix} H_z^{(x)} \\ H_z^{(y)} \end{bmatrix} + + where superscripts :math:`(x)` and :math:`(y)` denote signals corresponding to + incident planewaves whose electric fields are polarized along the x and y-directions + respectively. Note that in ``simpeg``, natural source EM data are defined according to + standard xyz coordinates; i.e. (x,y,z) is (Easting, Northing, Z +ve up). + + The receiver class can also be used to simulate a diverse set of Tipper-like data types + when horizontal magnetic fields are measured at a remote base station. These are defined + according to: - Assumes that the data locations are standard xyz coordinates; - i.e. (x,y,z) is (Easting, Northing, up). + .. math:: + \begin{bmatrix} T_{xx} & T_{yx} & T_{zx} \\ T_{xy} & T_{yy} & T_{zy} \end{bmatrix} = + \begin{bmatrix} H_x^{(x)} & H_y^{(x)} \\ H_x^{(y)} & H_y^{(y)} \end{bmatrix}_b^{-1} \, + \begin{bmatrix} H_x^{(x)} & H_y^{(x)} & H_z^{(x)} \\ H_x^{(y)} & H_y^{(y)} & H_z^{(y)} \end{bmatrix}_r + + where subscript :math:`b` denotes the base station location and subscript + :math:`r` denotes the mobile receiver location. Parameters ---------- - locations : (n_loc, n_dim) numpy.ndarray - Receiver locations. - orientation : str, default = 'zx' - NSEM receiver orientation. Must be one of {'zx', 'zy'} - component : str, default = 'real' - NSEM data type. Choose one of {'real', 'imag', 'apparent_resistivity', 'phase'} + locations_h : (n_loc, n_dim) array_like + Locations where the roving magnetic fields are measured. + locations_base : (n_loc, n_dim) array_like, optional + Locations where the base station magnetic fields are measured. Defaults to + the same locations as the roving magnetic fields measurements, + `locations_r`. + orientation : {'xx', 'yx', 'zx', 'zy', 'yy', 'zy'} + Specifies the tipper element :math:`T_{ij}` corresponding to the data. + component : {'real', 'imag', 'complex'} + Tipper data type. For the tipper element :math:`T_{ij}` specified by the `orientation` + input argument, the receiver can be set to compute the following: + - 'real': Real component of the tipper (unitless) + - 'imag': Imaginary component of the tipper (unitless) + - 'complex': The complex tipper is returned. Do not use for inversion! + storeProjections : bool + Whether to cache to internal projection matrices. """ + _loc_names = ("Roving magnetic field", "Base station magnetic field") + def __init__( self, - locations, - orientation="zx", + locations_h, + locations_base=None, + orientation="xx", component="real", - locations_e=None, - locations_h=None, + storeProjections=False, ): + if locations_base is None: + locations_base = locations_h super().__init__( - locations=locations, - orientation=orientation, - component=component, - locations_e=locations_e, - locations_h=locations_h, + locations1=locations_h, + locations2=locations_base, + storeProjections=storeProjections, + ) + self.orientation = orientation + self.component = component + + @property + def locations_h(self): + """Roving magnetic field measurement locations. + + Returns + ------- + numpy.ndarray + Roving locations where the magnetic field is measured for all receiver data. + """ + return self._locations[0] + + @property + def locations_base(self): + """Base station magnetic field measurement locations. + + Returns + ------- + numpy.ndarray + Base station locations where the horizontal magnetic fields are measured. + """ + return self._locations[1] + + @property + def component(self): + r"""Tipper data type; i.e. "real", "imag" + + For the tipper element :math:`T_{ij}`, the `component` property specifies + whether the data are: + - 'real': Real component of the tipper (unitless) + - 'imag': Imaginary component of the tipper (unitless) + - 'complex': Complex tipper (unitless) + + Returns + ------- + str + Tipper data type; i.e. "real", "imag", "complex" + """ + return self._component + + @component.setter + def component(self, var): + self._component = validate_string( + "component", + var, + [ + ("real", "re", "in-phase", "in phase"), + ("imag", "imaginary", "im", "out-of-phase", "out of phase"), + "complex", + ], ) @property def orientation(self): - """Orientation of the receiver. + """Specifies the tipper element :math:`T_{ij}` corresponding to the data. Returns ------- str - Orientation of the receiver. One of {'zx', 'zy'} + Specifies the tipper element :math:`T_{ij}` corresponding to the data. + One of {'xx', 'yx', 'zx', 'zy', 'yy', 'zy'}. """ return self._orientation @orientation.setter def orientation(self, var): self._orientation = validate_string( - "orientation", var, string_list=("zx", "zy") + "orientation", var, string_list=("zx", "zy", "xx", "xy", "yx", "yy") ) def _eval_tipper(self, src, mesh, f): # will grab both primary and secondary and sum them! h = f[src, "h"] - hx = self.getP(mesh, "Fx", "h") @ h - hy = self.getP(mesh, "Fy", "h") @ h - hz = self.getP(mesh, "Fz", "h") @ h + Phx = self.getP(mesh, "Fx", 1) + Phy = self.getP(mesh, "Fy", 1) + Pho = self.getP(mesh, "F" + self.orientation[0], 0) + + hx = Phx @ h + hy = Phy @ h + ho = Pho @ h if self.orientation[1] == "x": h = -hy else: h = hx - top = h[:, 0] * hz[:, 1] - h[:, 1] * hz[:, 0] + top = h[:, 0] * ho[:, 1] - h[:, 1] * ho[:, 0] bot = hx[:, 0] * hy[:, 1] - hx[:, 1] * hy[:, 0] return top / bot @@ -504,32 +722,33 @@ def _eval_tipper_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): # will grab both primary and secondary and sum them! h = f[src, "h"] - Phx = self.getP(mesh, "Fx", "h") - Phy = self.getP(mesh, "Fy", "h") - Phz = self.getP(mesh, "Fz", "h") + Phx = self.getP(mesh, "Fx", 1) + Phy = self.getP(mesh, "Fy", 1) + Pho = self.getP(mesh, "F" + self.orientation[0], 0) + hx = Phx @ h hy = Phy @ h - hz = Phz @ h + ho = Pho @ h if self.orientation[1] == "x": h = -hy else: h = hx - top = h[:, 0] * hz[:, 1] - h[:, 1] * hz[:, 0] + top = h[:, 0] * ho[:, 1] - h[:, 1] * ho[:, 0] bot = hx[:, 0] * hy[:, 1] - hx[:, 1] * hy[:, 0] - imp = top / bot + tip = top / bot if adjoint: # Work backwards! gtop_v = (v / bot)[..., None] - gbot_v = (-imp * v / bot)[..., None] + gbot_v = (-tip * v / bot)[..., None] n_d = self.nD ghx_v = np.c_[hy[:, 1], -hy[:, 0]] * gbot_v ghy_v = np.c_[-hx[:, 1], hx[:, 0]] * gbot_v - ghz_v = np.c_[-h[:, 1], h[:, 0]] * gtop_v - gh_v = np.c_[hz[:, 1], -hz[:, 0]] * gtop_v + gho_v = np.c_[-h[:, 1], h[:, 0]] * gtop_v + gh_v = np.c_[ho[:, 1], -ho[:, 0]] * gtop_v if self.orientation[1] == "x": ghy_v -= gh_v @@ -540,25 +759,25 @@ def _eval_tipper_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): # collapse into a long list of n_d vectors ghx_v = ghx_v.reshape((n_d, -1)) ghy_v = ghy_v.reshape((n_d, -1)) - ghz_v = ghz_v.reshape((n_d, -1)) + gho_v = gho_v.reshape((n_d, -1)) - gh_v = Phx.T @ ghx_v + Phy.T @ ghy_v + Phz.T @ ghz_v + gh_v = Phx.T @ ghx_v + Phy.T @ ghy_v + Pho.T @ gho_v return f._hDeriv(src, None, gh_v, adjoint=True) dh_v = f._hDeriv(src, du_dm_v, v, adjoint=False) dhx_v = Phx @ dh_v dhy_v = Phy @ dh_v - dhz_v = Phz @ dh_v + dho_v = Pho @ dh_v if self.orientation[1] == "x": dh_v = -dhy_v else: dh_v = dhx_v dtop_v = ( - h[:, 0] * dhz_v[:, 1] - + dh_v[:, 0] * hz[:, 1] - - h[:, 1] * dhz_v[:, 0] - - dh_v[:, 1] * hz[:, 0] + h[:, 0] * dho_v[:, 1] + + dh_v[:, 0] * ho[:, 1] + - h[:, 1] * dho_v[:, 0] + - dh_v[:, 1] * ho[:, 0] ) dbot_v = ( hx[:, 0] * dhy_v[:, 1] @@ -569,61 +788,459 @@ def _eval_tipper_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): return (bot * dtop_v - top * dbot_v) / (bot * bot) - def eval(self, src, mesh, f, return_complex=False): # noqa: A003 + def eval(self, src, mesh, f): # noqa: A003 + tip = self._eval_tipper(src, mesh, f) + if self.component == "complex": + return tip + else: + return getattr(tip, self.component) + + def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): + # Docstring inherited from parent class (Impedance). + if self.component == "complex": + raise NotImplementedError( + "complex valued data derivative is not implemented." + ) + if adjoint: + if self.component == "imag": + v = -1j * v + imp_deriv = self._eval_tipper_deriv( + src, mesh, f, du_dm_v=du_dm_v, v=v, adjoint=adjoint + ) + if adjoint: + return imp_deriv + return getattr(imp_deriv, self.component) + + +class Admittance(_ElectricAndMagneticReceiver): + r"""Receiver class for data types derived from the 3D admittance tensor. + + This class is used to simulate data types that can be derived from the admittance tensor: + + .. math:: + \begin{bmatrix} Y_{xx} & Y_{xy} \\ Y_{yx} & Y_{yy} \\ Y_{zx} & Y_{zy} \end{bmatrix} = + \begin{bmatrix} H_x^{(x)} & H_x^{(y)} \\ H_y^{(x)} & H_y^{(y)} \\ H_z^{(x)} & H_z^{(y)} \end{bmatrix}_{\, r} \; + \begin{bmatrix} E_x^{(x)} & E_x^{(y)} \\ E_y^{(x)} & E_y^{(y)} \end{bmatrix}_b^{-1} + + where superscripts :math:`(x)` and :math:`(y)` denote signals corresponding to + incident planewaves whose electric fields are polarized along the x and y-directions + respectively. Note that in simpeg, natural source EM data are defined according to + standard xyz coordinates; i.e. (x,y,z) is (Easting, Northing, Z +ve up). + + Parameters + ---------- + locations_e : (n_loc, n_dim) array_like + Locations where the electric fields are measured. + locations_h : (n_loc, n_dim) array_like, optional + Locations where the magnetic fields are measured. Defaults to the same + locations as electric field measurements, `locations_e`. + orientation : {'xx', 'xy', 'yx', 'yy', 'zx', 'zy'} + Admittance receiver orientation. Specifies the admittance tensor element + :math:`Y_{ij}` corresponding to the data. The data type is specified by + the `component` input argument. + component : {'real', 'imag', 'complex'} + Admittance data type. For the admittance element :math:`Y_{ij}` specified by the + `orientation` input argument, the receiver can be set to compute the following: + - 'real': Real component of the admittance (A/V) + - 'imag': Imaginary component of the admittance (A/V) + - 'complex': The complex admittance is returned. Do not use for inversion! + storeProjections : bool + Whether to cache to internal projection matrices. + """ + + def __init__( + self, + locations_e, + locations_h=None, + orientation="xx", + component="real", + storeProjections=False, + ): + if locations_h is None: + locations_h = locations_e + super().__init__( + locations1=locations_e, + locations2=locations_h, + storeProjections=storeProjections, + ) + self.orientation = orientation + self.component = component + + @property + def orientation(self): + """Receiver orientation. + + Specifies whether the receiver's data correspond to + the :math:`Y_{xx}`, :math:`Y_{xy}`, :math:`Y_{yx}`, :math:`Y_{yy}`, + :math:`Y_{zx}`, or :math:`Y_{zy}` admittance. + + Returns + ------- + str + Receiver orientation. One of {'xx', 'xy', 'yx', 'yy', 'zx', 'zy'} + """ + return self._orientation + + @orientation.setter + def orientation(self, var): + self._orientation = validate_string( + "orientation", var, string_list=("xx", "xy", "yx", "yy", "zx", "zy") + ) + + @property + def component(self): + r"""Admittance data type. + + For the admittance element :math:`Y_{ij}`, the `component` property specifies + whether the data are: + - 'real': Real component of the admittance (A/V) + - 'imag': Imaginary component of the admittance (A/V) + - 'complex': Complex admittance (A/V) + + Returns + ------- + str + Data type; i.e. "real", "imag". """ - Project the fields to natural source data. + return self._component + + @component.setter + def component(self, var): + self._component = validate_string( + "component", + var, + [ + ("real", "re", "in-phase", "in phase"), + ("imag", "imaginary", "im", "out-of-phase", "out of phase"), + "complex", + ], + ) + + def _eval_admittance(self, src, mesh, f): + if mesh.dim < 3: + raise NotImplementedError( + "Admittance receiver not implemented for dim < 3." + ) + + e = f[src, "e"] + h = f[src, "h"] + + ex = self.getP(mesh, "Ex", 0) @ e + ey = self.getP(mesh, "Ey", 0) @ e + + h = self.getP(mesh, "F" + self.orientation[0], 1) @ h + + if self.orientation[1] == "x": + top = h[:, 0] * ey[:, 1] - h[:, 1] * ex[:, 1] + else: + top = -h[:, 0] * ey[:, 0] + h[:, 1] * ex[:, 0] + + bot = ex[:, 0] * ey[:, 1] - ex[:, 1] * ey[:, 0] + + return top / bot + + def _eval_admittance_deriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): + if mesh.dim < 3: + raise NotImplementedError( + "Admittance receiver not implemented for dim < 3." + ) + + # Compute admittances + e = f[src, "e"] + h = f[src, "h"] + + Pex = self.getP(mesh, "Ex", 0) + Pey = self.getP(mesh, "Ey", 0) + Ph = self.getP(mesh, "F" + self.orientation[0], 1) + + ex = Pex @ e + ey = Pey @ e + h = Ph @ h + + if self.orientation[1] == "x": + p_ind = 1 + fact = 1.0 + else: + p_ind = 0 + fact = -1.0 + + top = fact * (h[:, 0] * ey[:, p_ind] - h[:, 1] * ex[:, p_ind]) + bot = ex[:, 0] * ey[:, 1] - ex[:, 1] * ey[:, 0] + adm = top / bot + + # ADJOINT + if adjoint: + if self.component == "imag": + v = -1j * v + + # J_T * v = d_top_T * a_v + d_bot_T * b + a_v = fact * v / bot # term 1 + b_v = -adm * v / bot # term 2 + + ex_v = np.c_[ey[:, 1], -ey[:, 0]] * b_v[:, None] # terms dex in bot + ey_v = np.c_[-ex[:, 1], ex[:, 0]] * b_v[:, None] # terms dey in bot + ex_v[:, p_ind] -= h[:, 1] * a_v # add terms dex in top + ey_v[:, p_ind] += h[:, 0] * a_v # add terms dey in top + e_v = Pex.T @ ex_v + Pey.T @ ey_v + + h_v = np.c_[ey[:, p_ind], -ex[:, p_ind]] * a_v[:, None] # h in top + h_v = Ph.T @ h_v + + fu_e_v, fm_e_v = f._eDeriv(src, None, e_v, adjoint=True) + fu_h_v, fm_h_v = f._hDeriv(src, None, h_v, adjoint=True) + + return fu_e_v + fu_h_v, fm_e_v + fm_h_v + + # JVEC + de_v = f._eDeriv(src, du_dm_v, v, adjoint=False) + dh_v = Ph @ f._hDeriv(src, du_dm_v, v, adjoint=False) + + dex_v = Pex @ de_v + dey_v = Pey @ de_v + + dtop_v = fact * ( + h[:, 0] * dey_v[:, p_ind] + + dh_v[:, 0] * ey[:, p_ind] + - h[:, 1] * dex_v[:, p_ind] + - dh_v[:, 1] * ex[:, p_ind] + ) + dbot_v = ( + ex[:, 0] * dey_v[:, 1] + + dex_v[:, 0] * ey[:, 1] + - ex[:, 1] * dey_v[:, 0] + - dex_v[:, 1] * ey[:, 0] + ) + adm_deriv = (bot * dtop_v - top * dbot_v) / (bot * bot) + + return getattr(adm_deriv, self.component) + + def eval(self, src, mesh, f): # noqa: A003 + # Docstring inherited from parent class (Impedance). + adm = self._eval_admittance(src, mesh, f) + if self.component == "complex": + return adm + return getattr(adm, self.component) + + def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): + # Docstring inherited from parent class (Impedance). + if self.component == "complex": + raise NotImplementedError( + "complex valued data derivative is not implemented." + ) + return self._eval_admittance_deriv( + src, mesh, f, du_dm_v=du_dm_v, v=v, adjoint=adjoint + ) + + +class ApparentConductivity(_ElectricAndMagneticReceiver): + r"""Receiver class for simulating apparent conductivity data (3D problems only). + + This class is used to simulate apparent conductivity data, in S/m, as defined by: + + .. math:: + \sigma_{app} = \mu_0 \omega \dfrac{\big | \vec{H} \big |^2}{\big | \vec{E} \big |^2} + + where :math:`\omega` is the angular frequency in rad/s, + + .. math:: + \big | \vec{H} \big | = \Big [ H_x^2 + H_y^2 + H_z^2 \Big ]^{1/2} + + and + + .. math:: + \big | \vec{E} \big | = \Big [ E_x^2 + E_y^2 \Big ]^{1/2} + + Parameters + ---------- + locations_e : (n_loc, n_dim) array_like + Locations where the electric fields are measured. + locations_h : (n_loc, n_dim) array_like, optional + Locations where the magnetic fields are measured. Defaults to the same + locations as electric field measurements, `locations_e`. + storeProjections : bool + Whether to cache to internal projection matrices. + """ + + def __init__(self, locations_e, locations_h=None, storeProjections=False): + if locations_h is None: + locations_h = locations_e + super().__init__( + locations1=locations_e, + locations2=locations_h, + storeProjections=storeProjections, + ) + + def _eval_apparent_conductivity(self, src, mesh, f): + if mesh.dim < 3: + raise NotImplementedError( + "ApparentConductivity receiver not implemented for dim < 3." + ) + + e = f[src, "e"] + h = f[src, "h"] + + Pex = self.getP(mesh, "Ex", 0) + Pey = self.getP(mesh, "Ey", 0) + Phx = self.getP(mesh, "Fx", 1) + Phy = self.getP(mesh, "Fy", 1) + Phz = self.getP(mesh, "Fz", 1) + + ex = np.sum(Pex @ e, axis=-1) + ey = np.sum(Pey @ e, axis=-1) + hx = np.sum(Phx @ h, axis=-1) + hy = np.sum(Phy @ h, axis=-1) + hz = np.sum(Phz @ h, axis=-1) + + top = np.abs(hx) ** 2 + np.abs(hy) ** 2 + np.abs(hz) ** 2 + bot = np.abs(ex) ** 2 + np.abs(ey) ** 2 + + return (2 * np.pi * src.frequency * mu_0) * top / bot + + def _eval_apparent_conductivity_deriv( + self, src, mesh, f, du_dm_v=None, v=None, adjoint=False + ): + if mesh.dim < 3: + raise NotImplementedError( + "Admittance receiver not implemented for dim < 3." + ) + + # Compute admittances + e = f[src, "e"] + h = f[src, "h"] + + Pex = self.getP(mesh, "Ex", 0) + Pey = self.getP(mesh, "Ey", 0) + Phx = self.getP(mesh, "Fx", 1) + Phy = self.getP(mesh, "Fy", 1) + Phz = self.getP(mesh, "Fz", 1) + + ex = np.sum(Pex @ e, axis=-1) + ey = np.sum(Pey @ e, axis=-1) + hx = np.sum(Phx @ h, axis=-1) + hy = np.sum(Phy @ h, axis=-1) + hz = np.sum(Phz @ h, axis=-1) + + fact = 2 * np.pi * src.frequency * mu_0 + top = np.abs(hx) ** 2 + np.abs(hy) ** 2 + np.abs(hz) ** 2 + bot = np.abs(ex) ** 2 + np.abs(ey) ** 2 + + # ADJOINT + if adjoint: + # Compute: J_T * v = d_top_T * a_v + d_bot_T * b + a_v = fact * v / bot # term 1 + b_v = -fact * top * v / bot**2 # term 2 + + hx *= a_v + hy *= a_v + hz *= a_v + ex *= b_v + ey *= b_v + + e_v = 2 * (Pex.T @ ex + Pey.T @ ey).conjugate() + h_v = 2 * (Phx.T @ hx + Phy.T @ hy + Phz.T @ hz).conjugate() + + fu_e_v, fm_e_v = f._eDeriv(src, None, e_v, adjoint=True) + fu_h_v, fm_h_v = f._hDeriv(src, None, h_v, adjoint=True) + + return fu_e_v + fu_h_v, fm_e_v + fm_h_v + + # JVEC + de_v = f._eDeriv(src, du_dm_v, v, adjoint=False) + dh_v = f._hDeriv(src, du_dm_v, v, adjoint=False) + + dex_v = np.sum(Pex @ de_v, axis=-1) + dey_v = np.sum(Pey @ de_v, axis=-1) + dhx_v = np.sum(Phx @ dh_v, axis=-1) + dhy_v = np.sum(Phy @ dh_v, axis=-1) + dhz_v = np.sum(Phz @ dh_v, axis=-1) + + # Imaginary components cancel and its 2x the real + dtop_v = ( + 2 + * ( + hx * dhx_v.conjugate() + hy * dhy_v.conjugate() + hz * dhz_v.conjugate() + ).real + ) + + dbot_v = 2 * (ex * dex_v.conjugate() + ey * dey_v.conjugate()).real + + return fact * (bot * dtop_v - top * dbot_v) / (bot * bot) + + def eval(self, src, mesh, f): # noqa: A003 + """Compute receiver data from the discrete field solution. Parameters ---------- - src : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc - NSEM source - mesh : discretize.TensorMesh mesh - Mesh on which the discretize solution is obtained + src : .frequency_domain.sources.BaseFDEMSrc + NSEM source. + mesh : discretize.TensorMesh + Mesh on which the discretize solution is obtained. f : simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM - NSEM fields object of the source - return_complex : bool (optional) - Flag for return the complex evaluation + NSEM fields object of the source. Returns ------- numpy.ndarray - Evaluated data for the receiver + Evaluated data for the receiver. """ + return self._eval_apparent_conductivity(src, mesh, f) - rx_eval_complex = self._eval_tipper(src, mesh, f) + def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): + r"""Derivative of data with respect to the fields. - return getattr(rx_eval_complex, self.component) + Let :math:`\mathbf{d}` represent the data corresponding the receiver object. + And let :math:`\mathbf{u}` represent the discrete numerical solution of the + fields on the mesh. Where :math:`\mathbf{P}` is a projection function that + maps from the fields to the data, i.e.: - def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): - """Derivative of projection with respect to the fields + .. math:: + \mathbf{d} = \mathbf{P}(\mathbf{u}) + + this method computes and returns the derivative: + + .. math:: + \dfrac{\partial \mathbf{d}}{\partial \mathbf{u}} = + \dfrac{\partial [ \mathbf{P} (\mathbf{u}) ]}{\partial \mathbf{u}} Parameters ---------- - str : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc - NSEM source + src : .frequency_domain.sources.BaseFDEMSrc + The NSEM source. mesh : discretize.TensorMesh - Mesh on which the discretize solution is obtained + Mesh on which the discretize solution is obtained. f : simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM - NSEM fields object of the source - du_dm_v : None, + NSEM fields object for the source. + du_dm_v : None, optional Supply pre-computed derivative? - v : numpy.ndarray + v : numpy.ndarray, optional Vector of size - adjoint : bool, default = ``False`` - If ``True``, compute the adjoint operation + adjoint : bool, optional + Whether to compute the ajoint operation. Returns ------- numpy.ndarray - Calculated derivative (nD,) (adjoint=False) and (nP,2) (adjoint=True) for both polarizations + Calculated derivative (n_data,) if `adjoint` is ``False``, and (n_param, 2) if `adjoint` + is ``True``, for both polarizations. """ - - if adjoint: - if self.component == "imag": - v = -1j * v - imp_deriv = self._eval_tipper_deriv( + return self._eval_apparent_conductivity_deriv( src, mesh, f, du_dm_v=du_dm_v, v=v, adjoint=adjoint ) - if adjoint: - return imp_deriv - return getattr(imp_deriv, self.component) + + +@deprecate_class(removal_version="0.24.0", error=True, replace_docstring=False) +class PointNaturalSource(Impedance): + """ + .. warning:: + This class was removed in SimPEG v0.24.0. + Please use :class:`.natural_source.receivers.Impedance`. + """ + + +@deprecate_class(removal_version="0.24.0", error=True, replace_docstring=False) +class Point3DTipper(Tipper): + """ + .. warning:: + This class was removed in SimPEG v0.24.0. + Please use :class:`.natural_source.receivers.Tipper`. + """ diff --git a/simpeg/electromagnetics/natural_source/simulation.py b/simpeg/electromagnetics/natural_source/simulation.py index cd531529a2..a7cf938a2b 100644 --- a/simpeg/electromagnetics/natural_source/simulation.py +++ b/simpeg/electromagnetics/natural_source/simulation.py @@ -110,6 +110,23 @@ def getADeriv(self, freq, u, v, adjoint=False): freq, u, v, adjoint ) + def getJ(self, m, f=None): + r"""Generate the full sensitivity matrix. + + .. important:: + + This method hasn't been implemented yet for this class. + + Raises + ------- + NotImplementedError + """ + msg = ( + "The getJ method hasn't been implemented for the " + f"{type(self).__name__} yet." + ) + raise NotImplementedError(msg) + class Simulation1DMagneticField(BaseFDEMSimulation): """ @@ -172,6 +189,23 @@ def getADeriv(self, freq, u, v, adjoint=False): freq, u, v, adjoint ) + def getJ(self, m, f=None): + r"""Generate the full sensitivity matrix. + + .. important:: + + This method hasn't been implemented yet for this class. + + Raises + ------- + NotImplementedError + """ + msg = ( + "The getJ method hasn't been implemented for the " + f"{type(self).__name__} yet." + ) + raise NotImplementedError(msg) + class Simulation1DPrimarySecondary(Simulation1DElectricField): r""" @@ -474,8 +508,8 @@ def boundary_fields(self, model=None): return self._boundary_fields @property - def deleteTheseOnModelUpdate(self): - items = super().deleteTheseOnModelUpdate + def _delete_on_model_update(self): + items = super()._delete_on_model_update items.append("_boundary_fields") return items @@ -696,8 +730,8 @@ def boundary_fields(self, model=None): return self._boundary_fields @property - def deleteTheseOnModelUpdate(self): - items = super().deleteTheseOnModelUpdate + def _delete_on_model_update(self): + items = super()._delete_on_model_update items.append("_boundary_fields") return items diff --git a/simpeg/electromagnetics/natural_source/simulation_1d.py b/simpeg/electromagnetics/natural_source/simulation_1d.py index 8ab8992ec5..ef0357718c 100644 --- a/simpeg/electromagnetics/natural_source/simulation_1d.py +++ b/simpeg/electromagnetics/natural_source/simulation_1d.py @@ -5,6 +5,7 @@ from ... import props from ...utils import validate_type from ..frequency_domain.survey import Survey +from .receivers import Impedance class Simulation1DRecursive(BaseSimulation): @@ -54,9 +55,9 @@ def __init__( thicknesses=None, thicknessesMap=None, fix_Jmatrix=False, - **kwargs + **kwargs, ): - super().__init__(mesh=None, survey=survey, **kwargs) + super().__init__(survey=survey, **kwargs) self.fix_Jmatrix = fix_Jmatrix self.sigma = sigma self.rho = rho @@ -81,6 +82,12 @@ def survey(self): def survey(self, value): if value is not None: value = validate_type("survey", value, Survey, cast=False) + for src in value.source_list: + for rx in src.receiver_list: + if not isinstance(rx, Impedance): + raise NotImplementedError( + f"{type(self).__name__} does not support {type(rx).__name__} receivers, only implemented for 'Impedance'." + ) self._survey = value @property @@ -241,8 +248,7 @@ def dpred(self, m, f=None): ) elif rx.component == "phase": d.append( - (180.0 / np.pi) - * np.arctan(np.imag(Z[i_freq]) / np.real(Z[i_freq])) + (180.0 / np.pi) * np.arctan2(Z[i_freq].imag, Z[i_freq].real) ) return np.array(d) @@ -345,8 +351,8 @@ def Jtvec(self, m, v, f=None): return JTvec @property - def deleteTheseOnModelUpdate(self): - toDelete = super().deleteTheseOnModelUpdate + def _delete_on_model_update(self): + toDelete = super()._delete_on_model_update if self.fix_Jmatrix: return toDelete else: diff --git a/simpeg/electromagnetics/natural_source/survey.py b/simpeg/electromagnetics/natural_source/survey.py index 935316a955..fc3751dc1d 100644 --- a/simpeg/electromagnetics/natural_source/survey.py +++ b/simpeg/electromagnetics/natural_source/survey.py @@ -5,7 +5,7 @@ from ...data import Data as BaseData from ...utils import mkvc from .sources import PlanewaveXYPrimary -from .receivers import PointNaturalSource, Point3DTipper +from .receivers import Impedance, Tipper from .utils.plot_utils import DataNSEMPlotMethods ######### @@ -80,12 +80,13 @@ def toRecArray(self, returnType="RealImag"): ("tzy", complex), ] + survey_slices = self.survey.get_all_slices() for src in self.survey.source_list: # Temp array for all the receivers of the source. # Note: needs to be written more generally, # using diffterent rxTypes and not all the data at the locations # Assume the same locs for all RX - locs = src.receiver_list[0].locations + locs = src.receiver_list[0].locations_e if locs.shape[1] == 1: locs = np.hstack((np.array([[0.0, 0.0]]), locs)) elif locs.shape[1] == 2: @@ -100,7 +101,7 @@ def toRecArray(self, returnType="RealImag"): ).view(dtRI) # Get the type and the value for the DataNSEM object as a list typeList = [ - [rx.orientation, rx.component, self[src, rx]] + [rx.orientation, rx.component, self.dobs[survey_slices[src, rx]]] for rx in src.receiver_list ] # Insert the values to the temp array @@ -141,9 +142,11 @@ def fromRecArray(cls, recArray, srcType="primary"): Parameters ---------- recArray : numpy.ndarray - Record array with the data. Has to have ('freq','x','y','z') columns and some ('zxx','zxy','zyx','zyy','tzx','tzy') - srcType : str, default: "primary" - The type of simpeg.EM.NSEM.SrcNSEM to be used. Either "primary" or "total" + Record array with the data. Has to have ('freq','x','y','z') + columns and some ('zxx','zxy','zyx','zyy','tzx','tzy'). + srcType : {"primary"} + The type of simpeg.EM.NSEM.SrcNSEM to be used. It currently accepts + only ``"primary"``. Returns ------- @@ -152,10 +155,13 @@ def fromRecArray(cls, recArray, srcType="primary"): """ if srcType == "primary": src = PlanewaveXYPrimary - elif srcType == "total": - src = Planewave_xy_1DhomotD + # TODO: implement total field formulation + # elif srcType == "total": + # src = Planewave_xy_1DhomotD else: - raise NotImplementedError("{:s} is not a valid source type for NSEMdata") + raise NotImplementedError( + f"Invalid srcType '{srcType}'. " "Only 'primary' is supported." + ) # Find all the frequencies in recArray uniFreq = np.unique(recArray["freq"].copy()) @@ -180,32 +186,40 @@ def fromRecArray(cls, recArray, srcType="primary"): if dFreq[rxType].dtype.name in "complex128": if "t" in rxType: receiver_list.append( - Point3DTipper(locs, rxType[1:3], "real") + Tipper(locs, orientation=rxType[1:3], component="real") ) dataList.append(dFreq[rxType][notNaNind].real.copy()) receiver_list.append( - Point3DTipper(locs, rxType[1:3], "imag") + Tipper(locs, orientation=rxType[1:3], component="imag") ) dataList.append(dFreq[rxType][notNaNind].imag.copy()) elif "z" in rxType: receiver_list.append( - PointNaturalSource(locs, rxType[1:3], "real") + Impedance( + locs, orientation=rxType[1:3], component="real" + ) ) dataList.append(dFreq[rxType][notNaNind].real.copy()) receiver_list.append( - PointNaturalSource(locs, rxType[1:3], "imag") + Impedance( + locs, orientation=rxType[1:3], component="imag" + ) ) dataList.append(dFreq[rxType][notNaNind].imag.copy()) else: component = "real" if "r" in rxType else "imag" if "z" in rxType: receiver_list.append( - PointNaturalSource(locs, rxType[1:3], component) + Impedance( + locs, orientation=rxType[1:3], component=component + ) ) dataList.append(dFreq[rxType][notNaNind].copy()) if "t" in rxType: receiver_list.append( - Point3DTipper(locs, rxType[1:3], component) + Tipper( + locs, orientation=rxType[1:3], component=component + ) ) dataList.append(dFreq[rxType][notNaNind].copy()) diff --git a/simpeg/electromagnetics/natural_source/utils/data_utils.py b/simpeg/electromagnetics/natural_source/utils/data_utils.py index eb1577be2b..1d226b0104 100644 --- a/simpeg/electromagnetics/natural_source/utils/data_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/data_utils.py @@ -6,8 +6,8 @@ import simpeg as simpeg from simpeg.electromagnetics.natural_source.survey import Survey, Data from simpeg.electromagnetics.natural_source.receivers import ( - PointNaturalSource, - Point3DTipper, + Impedance, + Tipper, ) from simpeg.electromagnetics.natural_source.sources import PlanewaveXYPrimary from simpeg.electromagnetics.natural_source.utils import ( @@ -61,13 +61,16 @@ def extract_data_info(NSEMdata): """ dL, freqL, rxTL = [], [], [] + survey_slices = NSEMdata.survey.get_all_slices() + for src in NSEMdata.survey.source_list: for rx in src.receiver_list: - dL.append(NSEMdata[src, rx]) + src_rx_slice = survey_slices[src, rx] + dL.append(NSEMdata.dobs[src_rx_slice]) freqL.append(np.ones(rx.nD) * src.frequency) - if isinstance(rx, PointNaturalSource): + if isinstance(rx, Impedance): rxTL.extend((("z" + rx.orientation + " ") * rx.nD).split()) - if isinstance(rx, Point3DTipper): + if isinstance(rx, Tipper): rxTL.extend((("t" + rx.orientation + " ") * rx.nD).split()) return np.concatenate(dL), np.concatenate(freqL), np.array(rxTL) @@ -121,9 +124,9 @@ def resample_data(NSEMdata, locs="All", freqs="All", rxs="All", verbose=False): rx_comp = [] for rxT in rxs: if "z" in rxT[0]: - rxtype = PointNaturalSource + rxtype = Impedance elif "t" in rxT[0]: - rxtype = Point3DTipper + rxtype = Tipper else: raise IOError("Unknown rx type string") orient = rxT[1:3] @@ -255,8 +258,8 @@ def convert3Dto1Dobject(NSEMdata, rxType3D="yx"): for loc in uniLocs: # Make the receiver list rx1DList = [] - rx1DList.append(PointNaturalSource(simpeg.mkvc(loc, 2).T, "real")) - rx1DList.append(PointNaturalSource(simpeg.mkvc(loc, 2).T, "imag")) + rx1DList.append(Impedance(simpeg.mkvc(loc, 2).T, component="real")) + rx1DList.append(Impedance(simpeg.mkvc(loc, 2).T, component="imag")) # Source list locrecData = recData[ np.sqrt( diff --git a/simpeg/electromagnetics/natural_source/utils/data_viewer.py b/simpeg/electromagnetics/natural_source/utils/data_viewer.py index 1e8270fcaa..6054d4406f 100644 --- a/simpeg/electromagnetics/natural_source/utils/data_viewer.py +++ b/simpeg/electromagnetics/natural_source/utils/data_viewer.py @@ -71,9 +71,9 @@ def __init__(self, data, data_dict=None, backend="qt"): ] ) ) - if rx.PointNaturalSource in unique_rx: + if rx.Impedance in unique_rx: self.station_figs.append(ApparentResPhsStationPlot()) - if rx.Point3DTipper in unique_rx: + if rx.Tipper in unique_rx: self.station_figs.append(TipperAmplitudeStationPlot()) self.freqency_figs = [] diff --git a/simpeg/electromagnetics/natural_source/utils/plot_data_types.py b/simpeg/electromagnetics/natural_source/utils/plot_data_types.py index aad9088c34..aea4821aec 100644 --- a/simpeg/electromagnetics/natural_source/utils/plot_data_types.py +++ b/simpeg/electromagnetics/natural_source/utils/plot_data_types.py @@ -1,4 +1,6 @@ -from matplotlib import pyplot as plt, colors, numpy as np +import numpy as np +from matplotlib import colors +from matplotlib import pyplot as plt def plotIsoFreqNSimpedance( diff --git a/simpeg/electromagnetics/natural_source/utils/plot_utils.py b/simpeg/electromagnetics/natural_source/utils/plot_utils.py index 81c6c6b140..c608940fec 100644 --- a/simpeg/electromagnetics/natural_source/utils/plot_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/plot_utils.py @@ -543,7 +543,7 @@ def map_data_locations(self, ax=None, **plot_kwargs): unique_locations = _unique_rows( np.concatenate( [ - rx.locations + rx.locations_e for src in self.survey.source_list for rx in src.receiver_list ] @@ -691,7 +691,7 @@ def _get_map_data(data, frequency, orientation, component, plot_error=False): else: if plot_error: freqs, plot_data, std_data, floor_data = _extract_frequency_data( - data, frequency, orientation, component, return_uncert=error + data, frequency, orientation, component, return_uncert=True ) attr_uncert = std_data * np.abs(plot_data) + floor_data errorbars = [attr_uncert, attr_uncert] @@ -813,7 +813,7 @@ def _extract_frequency_data( # Should be a more specifice Exeption raise Exception("To many Receivers of the same type, orientation and component") - loc_arr = rx.locations + loc_arr = rx.locations_e data_arr = data[src, rx] if return_uncert: std_arr = data.relative_error[src, rx] @@ -831,6 +831,10 @@ def _extract_location_data(data, location, orientation, component, return_uncert data_list = [] std_list = [] floor_list = [] + + # Get dict of flat array slices for each source-receiver pair in the survey + survey_slices = data.survey.get_all_slices() + for src in data.survey.source_list: rx_list = [ rx @@ -844,15 +848,15 @@ def _extract_location_data(data, location, orientation, component, return_uncert else: rx = rx_list[0] - ind_loc = np.sqrt(np.sum((rx.locations[:, :2] - location) ** 2, axis=1)) < 0.1 + ind_loc = np.sqrt(np.sum((rx.locations_e[:, :2] - location) ** 2, axis=1)) < 0.1 if np.any(ind_loc): freq_list.append(src.frequency) data_list.append(data[src, rx][ind_loc]) if return_uncert: - index = data.index_dictionary[src][rx] - std_list.append(data.relative_error[index][ind_loc]) - floor_list.append(data.noise_floor[index][ind_loc]) + src_rx_slice = survey_slices[src, rx] + std_list.append(data.relative_error[src_rx_slice][ind_loc]) + floor_list.append(data.noise_floor[src_rx_slice][ind_loc]) if return_uncert: return ( np.array(freq_list), diff --git a/simpeg/electromagnetics/natural_source/utils/solutions_1d.py b/simpeg/electromagnetics/natural_source/utils/solutions_1d.py index a69239a1ae..7c9b533e0f 100644 --- a/simpeg/electromagnetics/natural_source/utils/solutions_1d.py +++ b/simpeg/electromagnetics/natural_source/utils/solutions_1d.py @@ -1,7 +1,7 @@ import numpy as np from scipy.constants import mu_0 -from .... import Solver +from pymatsolver import Solver from ....utils import sdiag from .analytic_1d import getEHfields @@ -34,7 +34,7 @@ def get1DEfields(m1d, sigma, freq, sourceAmp=1.0): ## Note: The analytic solution is derived with e^iwt bc = np.r_[Etot[0], Etot[-1]] # The right hand side - rhs = Aio * bc + rhs = -Aio * bc # Solve the system Aii_inv = Solver(Aii) eii = Aii_inv * rhs diff --git a/simpeg/electromagnetics/natural_source/utils/source_utils.py b/simpeg/electromagnetics/natural_source/utils/source_utils.py index 7687ff5894..822c04d2c5 100644 --- a/simpeg/electromagnetics/natural_source/utils/source_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/source_utils.py @@ -43,7 +43,7 @@ def homo1DModelSource(mesh, freq, sigma_1d): for i in np.arange(mesh.vnEy[0]): ey_py[i, :] = e0_1d # ey_py[1:-1, 1:-1, 1:-1] = 0 - eBG_py = np.vstack((ex_py, mkvc(ey_py, 2), ez_py)) + eBG_py = np.vstack((ex_py, mkvc(ey_py, 2))) elif mesh.dim == 3: # us the z component of ex_grid as lookup for solution edges_u, inv_edges = np.unique(mesh.gridEx[:, -1], return_inverse=True) @@ -113,7 +113,7 @@ def analytic1DModelSource(mesh, freq, sigma_1d): for i in np.arange(mesh.vnEy[0]): ey_py[i, :] = e0_1d # ey_py[1:-1, 1:-1, 1:-1] = 0 - eBG_py = np.vstack((ex_py, mkvc(ey_py, 2), ez_py)) + eBG_py = np.vstack((ex_py, mkvc(ey_py, 2))) elif mesh.dim == 3: # Setup x (east) polarization (_x) ex_px = -np.array([E1dFieldDict[i] for i in mesh.gridEx[:, 2]]).reshape(-1, 1) diff --git a/simpeg/electromagnetics/natural_source/utils/test_utils.py b/simpeg/electromagnetics/natural_source/utils/test_utils.py index 9cde461e5a..88f8131db9 100644 --- a/simpeg/electromagnetics/natural_source/utils/test_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/test_utils.py @@ -1,27 +1,26 @@ import numpy as np import discretize -from simpeg import maps, mkvc, utils, Data +from simpeg import maps, mkvc, utils from ....utils import unpack_widths from ..receivers import ( - PointNaturalSource, - Point3DTipper, + Impedance, + Tipper, ) from ..survey import Survey from ..sources import PlanewaveXYPrimary, Planewave from ..simulation import Simulation3DPrimarySecondary from .data_utils import appResPhs -np.random.seed(1100) # Define the tolerances TOLr = 5e-2 TOLp = 5e-2 def getAppResPhs(NSEMdata, survey): - NSEMdata = Data(dobs=NSEMdata, survey=survey) # Make impedance zList = [] + survey_slices = survey.get_all_slices() for src in survey.source_list: zc = [src.frequency] for rx in src.receiver_list: @@ -29,14 +28,15 @@ def getAppResPhs(NSEMdata, survey): m = 1j else: m = 1 - zc.append(m * NSEMdata[src, rx]) + src_rx_slice = survey_slices[src, rx] + zc.append(m * NSEMdata[src_rx_slice]) zList.append(zc) return [ appResPhs(zList[i][0], np.sum(zList[i][1:3])) for i in np.arange(len(zList)) ] -def setup1DSurvey(sigmaHalf, tD=False, structure=False): +def setup1DSurvey(sigmaHalf, tD=False, structure=False, rx_orientation="xy"): # Frequency num_frequencies = 33 freqs = np.logspace(3, -3, num_frequencies) @@ -68,10 +68,14 @@ def setup1DSurvey(sigmaHalf, tD=False, structure=False): receiver_list = [] for _ in range(len(["z1d", "z1d"])): receiver_list.append( - PointNaturalSource(mkvc(np.array([0.0]), 2).T, component="real") + Impedance( + mkvc(np.array([0.0]), 2).T, component="real", orientation=rx_orientation + ) ) receiver_list.append( - PointNaturalSource(mkvc(np.array([0.0]), 2).T, component="imag") + Impedance( + mkvc(np.array([0.0]), 2).T, component="imag", orientation=rx_orientation + ) ) # Source list source_list = [] @@ -113,8 +117,8 @@ def setup1DSurveyElectricMagnetic(sigmaHalf, tD=False, structure=False): rxList = [] for _ in range(len(["z1d", "z1d"])): - rxList.append(PointNaturalSource(mkvc(np.array([0.0]), 2).T, component="real")) - rxList.append(PointNaturalSource(mkvc(np.array([0.0]), 2).T, component="imag")) + rxList.append(Impedance(mkvc(np.array([0.0]), 2).T, component="real")) + rxList.append(Impedance(mkvc(np.array([0.0]), 2).T, component="imag")) # Source list # srcList = [] src_list = [Planewave([], frequency=f) for f in frequencies] @@ -176,50 +180,44 @@ def setupSimpegNSEM_tests_location_assign_list( if comp == "Res": if singleList: rxList.append( - PointNaturalSource( - locations=[rx_loc], + Impedance( + rx_loc, orientation=rx_type, component="apparent_resistivity", ) ) rxList.append( - PointNaturalSource( - locations=[rx_loc], orientation=rx_type, component="phase" - ) + Impedance(rx_loc, orientation=rx_type, component="phase") ) else: rxList.append( - PointNaturalSource( - locations=[rx_loc, rx_loc], + Impedance( + locations_e=rx_loc, + locations_h=rx_loc, orientation=rx_type, component="apparent_resistivity", ) ) rxList.append( - PointNaturalSource( - locations=[rx_loc, rx_loc], + Impedance( + locations_e=rx_loc, + locations_h=rx_loc, orientation=rx_type, component="phase", ) ) else: + rxList.append(Impedance(rx_loc, orientation=rx_type, component="real")) rxList.append( - PointNaturalSource( - orientation=rx_type, component="real", locations=[rx_loc] - ) - ) - rxList.append( - PointNaturalSource( - orientation=rx_type, component="imag", locations=[rx_loc] + Impedance( + rx_loc, + orientation=rx_type, + component="imag", ) ) if rx_type in ["zx", "zy"]: - rxList.append( - Point3DTipper(orientation=rx_type, component="real", locations=[rx_loc]) - ) - rxList.append( - Point3DTipper(orientation=rx_type, component="imag", locations=[rx_loc]) - ) + rxList.append(Tipper(rx_loc, orientation=rx_type, component="real")) + rxList.append(Tipper(rx_loc, orientation=rx_type, component="imag")) srcList = [] if singleFreq: @@ -243,7 +241,7 @@ def setupSimpegNSEM_tests_location_assign_list( # Set the mapping actMap = maps.InjectActiveCells( - mesh=mesh, indActive=active, valInactive=np.log(1e-8) + mesh=mesh, active_cells=active, value_inactive=np.log(1e-8) ) mapping = maps.ExpMap(mesh) * actMap # print(survey_ns.source_list) @@ -328,23 +326,19 @@ def setupSimpegNSEM_PrimarySecondary(inputSetup, freqs, comp="Imp", singleFreq=F if rx_type in ["xx", "xy", "yx", "yy"]: if comp == "Res": rxList.append( - PointNaturalSource( - locations=rx_loc, + Impedance( + rx_loc, orientation=rx_type, component="apparent_resistivity", ) ) - rxList.append( - PointNaturalSource( - locations=rx_loc, orientation=rx_type, component="phase" - ) - ) + rxList.append(Impedance(rx_loc, orientation=rx_type, component="phase")) else: - rxList.append(PointNaturalSource(rx_loc, rx_type, "real")) - rxList.append(PointNaturalSource(rx_loc, rx_type, "imag")) + rxList.append(Impedance(rx_loc, orientation=rx_type, component="real")) + rxList.append(Impedance(rx_loc, orientation=rx_type, component="imag")) if rx_type in ["zx", "zy"]: - rxList.append(Point3DTipper(rx_loc, rx_type, "real")) - rxList.append(Point3DTipper(rx_loc, rx_type, "imag")) + rxList.append(Tipper(rx_loc, orientation=rx_type, component="real")) + rxList.append(Tipper(rx_loc, orientation=rx_type, component="imag")) srcList = [] if singleFreq: @@ -368,7 +362,7 @@ def setupSimpegNSEM_PrimarySecondary(inputSetup, freqs, comp="Imp", singleFreq=F # Set the mapping actMap = maps.InjectActiveCells( - mesh=mesh, indActive=active, valInactive=np.log(1e-8) + mesh=mesh, active_cells=active, value_inactive=np.log(1e-8) ) mapping = maps.ExpMap(mesh) * actMap # print(survey_ns.source_list) @@ -428,7 +422,7 @@ def setupSimpegNSEM_ePrimSec(inputSetup, comp="Imp", singleFreq=False, expMap=Tr if rx_type in ["xx", "xy", "yx", "yy"]: if comp == "Res": receiver_list.append( - PointNaturalSource( + Impedance( locations_e=rx_loc, locations_h=rx_loc, orientation=rx_type, @@ -436,7 +430,7 @@ def setupSimpegNSEM_ePrimSec(inputSetup, comp="Imp", singleFreq=False, expMap=Tr ) ) receiver_list.append( - PointNaturalSource( + Impedance( locations_e=rx_loc, locations_h=rx_loc, orientation=rx_type, @@ -444,11 +438,19 @@ def setupSimpegNSEM_ePrimSec(inputSetup, comp="Imp", singleFreq=False, expMap=Tr ) ) else: - receiver_list.append(PointNaturalSource(rx_loc, rx_type, "real")) - receiver_list.append(PointNaturalSource(rx_loc, rx_type, "imag")) + receiver_list.append( + Impedance(rx_loc, orientation=rx_type, component="real") + ) + receiver_list.append( + Impedance(rx_loc, orientation=rx_type, component="imag") + ) if rx_type in ["zx", "zy"]: - receiver_list.append(Point3DTipper(rx_loc, rx_type, "real")) - receiver_list.append(Point3DTipper(rx_loc, rx_type, "imag")) + receiver_list.append( + Impedance(rx_loc, orientation=rx_type, component="real") + ) + receiver_list.append( + Impedance(rx_loc, orientation=rx_type, component="imag") + ) # Source list source_list = [] @@ -473,12 +475,6 @@ def setupSimpegNSEM_ePrimSec(inputSetup, comp="Imp", singleFreq=False, expMap=Tr ) problem.model = sig problem.verbose = False - try: - from pymatsolver import Pardiso - - problem.solver = Pardiso - except ImportError: - pass return (survey, problem) @@ -510,14 +506,15 @@ def getInputs(): return M, freqs, rx_loc, elev -def random(conds): +def random(conds, seed=42): """Returns a random model based on the inputs""" + rng = np.random.default_rng(seed=seed) M, freqs, rx_loc, elev = getInputs() - # Backround + # Background sigBG = np.ones(M.nC) * conds # Add randomness to the model (10% of the value). - sig = np.exp(np.log(sigBG) + np.random.randn(M.nC) * (conds) * 1e-1) + sig = np.exp(np.log(sigBG) + rng.random(size=M.nC) * (conds) * 1e-1) return (M, freqs, sig, sigBG, rx_loc) diff --git a/simpeg/electromagnetics/static/induced_polarization/simulation.py b/simpeg/electromagnetics/static/induced_polarization/simulation.py index 027d6933bf..30c7dae47c 100644 --- a/simpeg/electromagnetics/static/induced_polarization/simulation.py +++ b/simpeg/electromagnetics/static/induced_polarization/simulation.py @@ -5,7 +5,7 @@ from .... import maps, props from ....base import BasePDESimulation -from ....data import Data +from ....utils import mkvc from ..resistivity import Simulation2DCellCentered as DC_2D_CC from ..resistivity import Simulation2DNodal as DC_2D_N from ..resistivity import Simulation3DCellCentered as DC_3D_CC @@ -43,7 +43,8 @@ def rhoDeriv(self): @cached_property def _scale(self): - scale = Data(self.survey, np.ones(self.survey.nD)) + survey_slices = self.survey.get_all_slices() + scale = np.ones(self.survey.nD) if self._f is None: # re-uses the DC simulation's fields method self._f = super().fields(None) @@ -55,14 +56,15 @@ def _scale(self): for src in self.survey.source_list: for rx in src.receiver_list: if rx.data_type == "apparent_chargeability": - scale[src, rx] = 1.0 / rx.eval(src, self.mesh, f) - return scale.dobs + src_rx_slice = survey_slices[src, rx] + scale[src_rx_slice] = mkvc(1.0 / rx.eval(src, self.mesh, f)) + return scale eta, etaMap, etaDeriv = props.Invertible("Electrical Chargeability (V/V)") def __init__( self, - mesh=None, + mesh, survey=None, sigma=None, rho=None, @@ -70,7 +72,7 @@ def __init__( etaMap=None, Ainv=None, # A DC's Ainv _f=None, # A pre-computed DC field - **kwargs + **kwargs, ): super().__init__(mesh=mesh, survey=survey, **kwargs) self.sigma = sigma @@ -132,7 +134,7 @@ def Jtvec(self, m, v, f=None): return super().Jtvec(m, v * self._scale, f) @property - def deleteTheseOnModelUpdate(self): + def _delete_on_model_update(self): toDelete = [] return toDelete diff --git a/simpeg/electromagnetics/static/resistivity/IODC.py b/simpeg/electromagnetics/static/resistivity/IODC.py index cc47403d7e..912bccc7a0 100644 --- a/simpeg/electromagnetics/static/resistivity/IODC.py +++ b/simpeg/electromagnetics/static/resistivity/IODC.py @@ -1,5 +1,4 @@ import numpy as np -import pandas as pd import matplotlib.pyplot as plt import matplotlib import warnings @@ -7,6 +6,12 @@ from discretize import TensorMesh, TreeMesh from discretize.base import BaseMesh from discretize.utils import refine_tree_xyz, unpack_widths, active_from_xyz +from discretize.utils import requires as module_requires + +try: + import pandas +except ImportError: + pandas = False from ....utils import ( sdiag, @@ -1283,6 +1288,7 @@ def read_ubc_dc2d_obs_file(self, filename, input_type="simple", toponame=None): survey.topo = topo return survey + @module_requires({"pandas": pandas}) def write_to_csv(self, fname, dobs, standard_deviation=None, **kwargs): uncert = kwargs.pop("uncertainty", None) if uncert is not None: @@ -1300,7 +1306,7 @@ def write_to_csv(self, fname, dobs, standard_deviation=None, **kwargs): dobs, standard_deviation, ] - df = pd.DataFrame( + df = pandas.DataFrame( data=data, columns=[ "Ax", @@ -1317,8 +1323,9 @@ def write_to_csv(self, fname, dobs, standard_deviation=None, **kwargs): ) df.to_csv(fname) + @module_requires({"pandas": pandas}) def read_dc_data_csv(self, fname, dim=2): - df = pd.read_csv(fname) + df = pandas.read_csv(fname) if dim == 2: a_locations = df[["Ax", "Az"]].values b_locations = df[["Bx", "Bz"]].values @@ -1352,8 +1359,9 @@ def read_dc_data_csv(self, fname, dim=2): raise NotImplementedError() return survey + @module_requires({"pandas": pandas}) def read_topo_csv(self, fname, dim=2): if dim == 2: - df = pd.read_csv(fname) + df = pandas.read_csv(fname) topo = df[["X", "Z"]].values return topo diff --git a/simpeg/electromagnetics/static/resistivity/receivers.py b/simpeg/electromagnetics/static/resistivity/receivers.py index 3e54d85d96..53c2614bb7 100644 --- a/simpeg/electromagnetics/static/resistivity/receivers.py +++ b/simpeg/electromagnetics/static/resistivity/receivers.py @@ -210,7 +210,7 @@ def evalDeriv(self, src, mesh, f, v=None, adjoint=False): v : numpy.ndarray The vector which being multiplied adjoint : bool, default = ``False`` - If ``True``, return the ajoint + If ``True``, return the adjoint Returns ------- diff --git a/simpeg/electromagnetics/static/resistivity/simulation.py b/simpeg/electromagnetics/static/resistivity/simulation.py index 2a78b6a17b..c84939afcf 100644 --- a/simpeg/electromagnetics/static/resistivity/simulation.py +++ b/simpeg/electromagnetics/static/resistivity/simulation.py @@ -10,6 +10,7 @@ ) from ....data import Data from ....base import BaseElectricalPDESimulation +from ....base.pde_simulation import _inner_mat_mul_op from .survey import Survey from .fields import Fields3DCellCentered, Fields3DNodal from .utils import _mini_pole_pole @@ -112,6 +113,7 @@ def fields(self, m=None, calcJ=True): return f def getJ(self, m, f=None): + self.model = m if getattr(self, "_Jmatrix", None) is None: if f is None: f = self.fields(m) @@ -215,7 +217,6 @@ def _Jtvec(self, m, v=None, f=None): if isinstance(v, Data): v = v.dobs v = self._mini_survey_dataT(v) - v = Data(survey, v) Jtv = np.zeros(m.size) else: # This is for forming full sensitivity matrix @@ -223,13 +224,17 @@ def _Jtvec(self, m, v=None, f=None): istrt = int(0) iend = int(0) + # Get dict of flat array slices for each source-receiver pair in the survey + survey_slices = survey.get_all_slices() + for source in survey.source_list: u_source = f[source, self._solutionType].copy() for rx in source.receiver_list: # wrt f, need possibility wrt m if v is not None: + src_rx_slice = survey_slices[source, rx] PTv = rx.evalDeriv( - source, self.mesh, f, v[source, rx], adjoint=True + source, self.mesh, f, v[src_rx_slice], adjoint=True ) else: PTv = rx.evalDeriv(source, self.mesh, f).toarray().T @@ -284,8 +289,8 @@ def getSourceTerm(self): return self._q @property - def deleteTheseOnModelUpdate(self): - toDelete = super().deleteTheseOnModelUpdate + def _delete_on_model_update(self): + toDelete = super()._delete_on_model_update return toDelete + ["_Jmatrix", "_gtgdiag"] def _mini_survey_data(self, d_mini): @@ -562,14 +567,7 @@ def getADeriv(self, u, v, adjoint=False): if self.bc_type != "Neumann" and self.sigmaMap is not None: if getattr(self, "_MBC_sigma", None) is None: self._MBC_sigma = self._AvgBC @ self.sigmaDeriv - if not isinstance(u, Zero): - u = u.flatten() - if v.ndim > 1: - u = u[:, None] - if not adjoint: - out += u * (self._MBC_sigma @ v) - else: - out += self._MBC_sigma.T @ (u * v) + out += _inner_mat_mul_op(self._MBC_sigma, u, v, adjoint) return out def setBC(self): diff --git a/simpeg/electromagnetics/static/resistivity/simulation_1d.py b/simpeg/electromagnetics/static/resistivity/simulation_1d.py index f2528d9a6f..85d72bd2fd 100644 --- a/simpeg/electromagnetics/static/resistivity/simulation_1d.py +++ b/simpeg/electromagnetics/static/resistivity/simulation_1d.py @@ -1,28 +1,22 @@ +from collections import namedtuple + +import libdlf import numpy as np +from ...utils.em1d_utils import get_splined_dlf_points from ....simulation import BaseSimulation from .... import props from .survey import Survey -from empymod.transform import get_dlf_points -from empymod import filters from ....utils import validate_type, validate_string from scipy.interpolate import InterpolatedUnivariateSpline as iuSpline - -HANKEL_FILTERS = [ - "kong_61_2007", - "kong_241_2007", - "key_101_2009", - "key_201_2009", - "key_401_2009", - "anderson_801_1982", - "key_51_2012", - "key_101_2012", - "key_201_2012", - "wer_201_2018", -] +HANKEL_FILTERS = {} +for filter_name in libdlf.hankel.__all__: + hankel_filter = getattr(libdlf.hankel, filter_name) + if "j0" in hankel_filter.values: + HANKEL_FILTERS[filter_name] = hankel_filter def _phi_tilde(rho, thicknesses, lambdas): @@ -200,11 +194,12 @@ def hankel_filter(self): @hankel_filter.setter def hankel_filter(self, value): self._hankel_filter = validate_string( - "hankel_filter", - value, - HANKEL_FILTERS, + "hankel_filter", value, list(HANKEL_FILTERS.keys()) ) - self._fhtfilt = getattr(filters, self._hankel_filter)() + filt = HANKEL_FILTERS[self._hankel_filter]() + hank = namedtuple("HankelFilter", "base j0") + self._fhtfilt = hank(filt[0], filt[1]) + self._coefficients_set = False @property def fix_Jmatrix(self): @@ -225,8 +220,8 @@ def _compute_hankel_coefficients(self): return survey = self.survey - r_min = np.infty - r_max = -np.infty + r_min = np.inf + r_max = -np.inf for src in survey.source_list: src_loc = src.location @@ -241,9 +236,8 @@ def _compute_hankel_coefficients(self): r_max = max(off.max(), r_max) self.survey.set_geometric_factor() - lambdas, r_spline_points = get_dlf_points( - self._fhtfilt, np.r_[r_min, r_max], -1 - ) + lambdas, r_spline_points = get_splined_dlf_points(self._fhtfilt, r_min, r_max) + lambdas = lambdas.reshape(-1) n_lambda = len(lambdas) n_r = len(r_spline_points) @@ -346,8 +340,8 @@ def Jtvec(self, m, v, f=None): return self.getJ(m, f=f).T @ v @property - def deleteTheseOnModelUpdate(self): - to_delete = super().deleteTheseOnModelUpdate + def _delete_on_model_update(self): + to_delete = super()._delete_on_model_update if not self.fix_Jmatrix: to_delete = to_delete + ["_Jmatrix"] return to_delete diff --git a/simpeg/electromagnetics/static/resistivity/simulation_2d.py b/simpeg/electromagnetics/static/resistivity/simulation_2d.py index 239d51536c..16b591f420 100644 --- a/simpeg/electromagnetics/static/resistivity/simulation_2d.py +++ b/simpeg/electromagnetics/static/resistivity/simulation_2d.py @@ -13,6 +13,7 @@ validate_active_indices, ) from ....base import BaseElectricalPDESimulation +from ....base.pde_simulation import _inner_mat_mul_op from ....data import Data from .survey import Survey @@ -21,6 +22,7 @@ from .utils import _mini_pole_pole from scipy.special import k0e, k1e, k0 from discretize.utils import make_boundary_bool +import discretize.base class BaseDCSimulation2D(BaseElectricalPDESimulation): @@ -128,6 +130,15 @@ def g(k): if miniaturize: self._dipoles, self._invs, self._mini_survey = _mini_pole_pole(self.survey) + @BaseElectricalPDESimulation.mesh.setter + def mesh(self, value): + value = validate_type("mesh", value, discretize.base.BaseMesh, cast=False) + if value.dim != 2: + raise ValueError( + f"{type(self).__name__} mesh must be 2D, received a {value.dim}D mesh." + ) + self._mesh = value + @property def survey(self): """The DC survey object. @@ -268,10 +279,10 @@ def getJ(self, m, f=None): """ Generate Full sensitivity matrix """ + self.model = m if getattr(self, "_Jmatrix", None) is None: if self.verbose: print("Calculating J and storing") - self.model = m if f is None: f = self.fields(m) self._Jmatrix = (self._Jtvec(m, v=None, f=f)).T @@ -357,16 +368,18 @@ def _Jtvec(self, m, v=None, f=None): v = self._mini_survey_dataT(v) Jtv = np.zeros(m.size, dtype=float) + # Get dict of flat array slices for each source-receiver pair in the survey + survey_slices = survey.get_all_slices() + for iky, ky in enumerate(kys): u_ky = f[:, self._solutionType, iky] - count = 0 for i_src, src in enumerate(survey.source_list): u_src = u_ky[:, i_src] df_duT_sum = 0 df_dmT_sum = 0 for rx in src.receiver_list: - my_v = v[count : count + rx.nD] - count += rx.nD + src_rx_slice = survey_slices[src, rx] + my_v = v[src_rx_slice] # wrt f, need possibility wrt m PTv = rx.evalDeriv(src, self.mesh, f, my_v, adjoint=True) df_duTFun = getattr(f, "_{0!s}Deriv".format(rx.projField), None) @@ -435,8 +448,8 @@ def getSourceTerm(self, ky): return q @property - def deleteTheseOnModelUpdate(self): - toDelete = super().deleteTheseOnModelUpdate + def _delete_on_model_update(self): + toDelete = super()._delete_on_model_update if self.fix_Jmatrix: return toDelete return toDelete + ["_Jmatrix"] @@ -562,23 +575,7 @@ def setBC(self, ky=None): else: mesh = self.mesh boundary_faces = mesh.boundary_faces - boundary_normals = mesh.boundary_face_outward_normals - n_bf = len(boundary_faces) - - # Top gets 0 Neumann - alpha = np.zeros(n_bf) - beta = np.ones(n_bf) - gamma = 0 - - # assume a source point at the middle of the top of the mesh - middle = np.median(mesh.nodes, axis=0) top_v = np.max(mesh.nodes[:, -1]) - source_point = np.r_[middle[:-1], top_v] - - r_vec = boundary_faces - source_point - r = np.linalg.norm(r_vec, axis=-1) - r_hat = r_vec / r[:, None] - r_dot_n = np.einsum("ij,ij->i", r_hat, boundary_normals) if self.surface_faces is None: # determine faces that are on the sides and bottom of the mesh... @@ -600,11 +597,29 @@ def setBC(self, ky=None): else: not_top = ~self.surface_faces + n_bf = len(boundary_faces) + + # Top gets 0 Neumann + alpha = np.zeros(n_bf) + beta = np.ones(n_bf) + gamma = 0 + + # assume a source point at the middle of the top of the mesh + middle = np.median(mesh.nodes, axis=0) + source_point = np.r_[middle[:-1], top_v] + + boundary_faces = boundary_faces[not_top] + boundary_normals = mesh.boundary_face_outward_normals[not_top] + r_vec = boundary_faces - source_point + r = np.linalg.norm(r_vec, axis=-1) + r_hat = r_vec / r[:, None] # small stabilizer to avoid divide by zero + r_dot_n = np.einsum("ij,ij->i", r_hat, boundary_normals) + # use the exponentialy scaled modified bessel function of second kind, # (the division will cancel out the scaling) # This is more stable for large values of ky * r # actual ratio is k1/k0... - alpha[not_top] = (ky * k1e(ky * r) / k0e(ky * r) * r_dot_n)[not_top] + alpha[not_top] = ky * k1e(ky * r) / k0e(ky * r) * r_dot_n B, bc = self.mesh.cell_gradient_weak_form_robin(alpha, beta, gamma) # bc should always be 0 because gamma was always 0 above @@ -693,14 +708,7 @@ def getADeriv(self, ky, u, v, adjoint=False): self._MBC_sigma = {} if ky not in self._MBC_sigma: self._MBC_sigma[ky] = self._AvgBC[ky] @ self.sigmaDeriv - if not isinstance(u, Zero): - u = u.flatten() - if v.ndim > 1: - u = u[:, None] - if not adjoint: - out += u * (self._MBC_sigma[ky] @ v) - else: - out += self._MBC_sigma[ky].T @ (u * v) + out += _inner_mat_mul_op(self._MBC_sigma[ky], u, v, adjoint) return out def getRHS(self, ky): @@ -741,20 +749,7 @@ def setBC(self, ky=None): mesh = self.mesh # calculate alpha, beta, gamma at the boundary faces boundary_faces = mesh.boundary_faces - boundary_normals = mesh.boundary_face_outward_normals - n_bf = len(boundary_faces) - - alpha = np.zeros(n_bf) - - # assume a source point at the middle of the top of the mesh - middle = np.median(mesh.nodes, axis=0) top_v = np.max(mesh.nodes[:, -1]) - source_point = np.r_[middle[:-1], top_v] - - r_vec = boundary_faces - source_point - r = np.linalg.norm(r_vec, axis=-1) - r_hat = r_vec / r[:, None] - r_dot_n = np.einsum("ij,ij->i", r_hat, boundary_normals) if self.surface_faces is None: # determine faces that are on the sides and bottom of the mesh... @@ -776,11 +771,27 @@ def setBC(self, ky=None): else: not_top = ~self.surface_faces + n_bf = len(boundary_faces) + + boundary_faces = boundary_faces[not_top] + boundary_normals = mesh.boundary_face_outward_normals[not_top] + + alpha = np.zeros(n_bf) + + # assume a source point at the middle of the top of the mesh + middle = np.median(mesh.nodes, axis=0) + source_point = np.r_[middle[:-1], top_v] + + r_vec = boundary_faces - source_point + r = np.linalg.norm(r_vec, axis=-1) + r_hat = r_vec / r[:, None] + r_dot_n = np.einsum("ij,ij->i", r_hat, boundary_normals) + # use the exponentiall scaled modified bessel function of second kind, # (the division will cancel out the scaling) # This is more stable for large values of ky * r # actual ratio is k1/k0... - alpha[not_top] = (ky * k1e(ky * r) / k0e(ky * r) * r_dot_n)[not_top] + alpha[not_top] = ky * k1e(ky * r) / k0e(ky * r) * r_dot_n P_bf = self.mesh.project_face_to_boundary_face diff --git a/simpeg/electromagnetics/static/resistivity/sources.py b/simpeg/electromagnetics/static/resistivity/sources.py index dd55089689..ee46681d3b 100644 --- a/simpeg/electromagnetics/static/resistivity/sources.py +++ b/simpeg/electromagnetics/static/resistivity/sources.py @@ -139,13 +139,18 @@ class Dipole(BaseSrc): ---------- receiver_list : list of simpeg.electromagnetics.static.resistivity.receivers.BaseRx A list of DC/IP receivers - location_a : (n_source, dim) numpy.array_like - A electrode locations; remember to set 'location_b' keyword argument to define N electrode locations. - location_b : (n_source, dim) numpy.array_like - B electrode locations; remember to set 'location_a' keyword argument to define M electrode locations. - location : list or tuple of length 2 of numpy.array_like - A and B electrode locations. In this case, do not set the 'location_a' and 'location_b' - keyword arguments. And we supply a list or tuple of the form [location_a, location_b]. + location_a : (dim) array_like + A electrode locations; remember to set ``location_b`` keyword argument + to define B electrode location. + location_b : (dim) array_like + B electrode locations; remember to set ``location_a`` keyword argument + to define A electrode location. + location : tuple of array_like, optional + A and B electrode locations. If ``location_a`` and ``location_b`` are + provided, don't pass values to this argument. Otherwise, provide + a tuple of the form ``(location_a, location_b)``. + current : float, optional + Current amplitude in :math:`A` that goes through each electrode. """ def __init__( @@ -154,41 +159,46 @@ def __init__( location_a=None, location_b=None, location=None, - **kwargs, + current=1.0, ): - if "current" in kwargs.keys(): - value = kwargs.pop("current") - current = [value, -value] - else: - current = [1.0, -1.0] - - # if location_a set, then use location_a, location_b - if location_a is not None: - if location_b is None: - raise ValueError( - "For a dipole source both location_a and location_b " "must be set" + if location is None and location_a is None and location_b is None: + raise TypeError( + "Found 'location', 'location_a' and 'location_b' as None. " + "Please specify 'location', or 'location_a' and 'location_b' " + "when defining a dipole source." + ) + if location is not None and (location_a is not None or location_b is not None): + raise TypeError( + "Found 'location_a' and/or 'location_b' as not None values. " + "When passing a not None value for 'location', 'location_a' and " + "'location_b' should be set to None." + ) + if location is None: + if location_a is None: + raise TypeError( + "Invalid 'location_a' set to None. When 'location' is None, " + "both 'location_a' and 'location_b' should be set to " + "a value different than None." ) - - if location is not None: - raise ValueError( - "Cannot set both location and location_a, location_b. " - "Please provide either location=(location_a, location_b) " - "or both location_a=location_a, location_b=location_b" + if location_b is None: + raise TypeError( + "Invalid 'location_b' set to None. When 'location' is None, " + "both 'location_a' and 'location_b' should be set to " + "a value different than None." ) - location = [location_a, location_b] - elif location is not None: - if len(location) != 2: - raise ValueError( - "location must be a list or tuple of length 2: " - "[location_a, location_b]. The input location has " - f"length {len(location)}" - ) + if len(location) != 2: + raise ValueError( + "location must be a list or tuple of length 2: " + "[location_a, location_b]. The input location has " + f"length {len(location)}" + ) - # instantiate super().__init__( - receiver_list=receiver_list, location=location, current=current, **kwargs + receiver_list=receiver_list, + location=location, + current=[current, -current], ) def __repr__(self): diff --git a/simpeg/electromagnetics/static/resistivity/survey.py b/simpeg/electromagnetics/static/resistivity/survey.py index 8f2e438664..0d607c86b6 100644 --- a/simpeg/electromagnetics/static/resistivity/survey.py +++ b/simpeg/electromagnetics/static/resistivity/survey.py @@ -1,17 +1,17 @@ +import warnings import numpy as np from ....utils.code_utils import validate_string from ....survey import BaseSurvey -from ..utils import drapeTopotoLoc +from ....utils import shift_to_discrete_topography from . import receivers as Rx from . import sources as Src from ..utils import static_utils -from simpeg import data class Survey(BaseSurvey): - """DC/IP survey class + """DC/IP survey class. Parameters ---------- @@ -19,26 +19,26 @@ class Survey(BaseSurvey): List of SimPEG DC/IP sources survey_geometry : {"surface", "borehole", "general"} Survey geometry. - survey_type : {"dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"} - Survey type. """ def __init__( self, source_list, survey_geometry="surface", - survey_type="dipole-dipole", **kwargs, ): + if kwargs.pop("survey_type", None) is not None: + raise TypeError( + "Argument 'survey_type' has been removed in SimPEG 0.24.0. Types of sources and" + "their corresponding receivers are obtained from their respective classes, without " + "the need to specify the survey type.", + ) super(Survey, self).__init__(source_list, **kwargs) self.survey_geometry = survey_geometry - self.survey_type = survey_type @property def survey_geometry(self): - """Survey geometry - - This property is deprecated. + """Survey geometry. Returns ------- @@ -56,34 +56,28 @@ def survey_geometry(self, var): @property def survey_type(self): - """Survey type; one of {"dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"} + """ + ``survey_type`` has been removed. - Returns - ------- - str - Survey type; one of {"dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"} + .. important: + + The `survey_type` property has been removed. Types of sources and + their corresponding receivers are obtained from their respective + classes, without the need to specify the survey type. """ - return self._survey_type + raise AttributeError("'survey_type' has been removed.") @survey_type.setter def survey_type(self, var): - var = validate_string( - "survey_type", - var, - ("dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"), - ) - self._survey_type = var + raise AttributeError("'survey_type' has been removed.") def __repr__(self): - return ( - f"{self.__class__.__name__}({self.survey_type}; " - f"#sources: {self.nSrc}; #data: {self.nD})" - ) + return f"{self.__class__.__name__}(#sources: {self.nSrc}; #data: {self.nD})" @property def locations_a(self): """ - Locations of the positive (+) current electrodes in the survey + Locations of the positive (+) current electrodes in the survey. Returns ------- @@ -97,7 +91,7 @@ def locations_a(self): @property def locations_b(self): """ - Locations of the negative (-) current electrodes in the survey + Locations of the negative (-) current electrodes in the survey. Returns ------- @@ -111,7 +105,7 @@ def locations_b(self): @property def locations_m(self): """ - Locations of the positive (+) potential electrodes in the survey + Locations of the positive (+) potential electrodes in the survey. Returns ------- @@ -125,7 +119,7 @@ def locations_m(self): @property def locations_n(self): """ - Locations of the negative (-) potential electrodes in the survey + Locations of the negative (-) potential electrodes in the survey. Returns ------- @@ -139,7 +133,7 @@ def locations_n(self): @property def unique_electrode_locations(self): """ - Unique locations of the A, B, M, N electrodes + Unique locations of the A, B, M, N electrodes. Returns ------- @@ -174,43 +168,29 @@ def source_locations(self): def set_geometric_factor( self, space_type="halfspace", - data_type=None, - survey_type=None, ): """ - Set and return the geometric factor for all data + Set and return the geometric factor for all data. Parameters ---------- space_type : {'halfspace', 'wholespace'} Calculate geometric factors using a half-space or whole-space formula. - data_type : str, default = ``None`` - This input argument is now deprecated - survey_type : str, default = ``None`` - This input argument is now deprecated Returns ------- (nD) numpy.ndarray The geometric factor for each datum """ - if data_type is not None: - raise TypeError( - "The data_type kwarg has been removed, please set the data_type on the " - "receiver object itself." - ) - if survey_type is not None: - raise TypeError("The survey_type parameter is no longer needed") - geometric_factor = static_utils.geometric_factor(self, space_type=space_type) - geometric_factor = data.Data(self, geometric_factor) + # geometric_factor = data.Data(self, geometric_factor) + survey_slices = self.get_all_slices() for source in self.source_list: for rx in source.receiver_list: - if data_type is not None: - rx.data_type = data_type if rx.data_type == "apparent_resistivity": - rx._geometric_factor[source] = geometric_factor[source, rx] + src_rx_slice = survey_slices[source, rx] + rx._geometric_factor[source] = geometric_factor[src_rx_slice] return geometric_factor def _set_abmn_locations(self): @@ -256,16 +236,14 @@ def _set_abmn_locations(self): self._locations_m = np.vstack(locations_m) self._locations_n = np.vstack(locations_n) - def getABMN_locations(self): - """The 'getABMN_locations' method has been removed.""" - raise TypeError( - "The getABMN_locations method has been Removed. Please instead " - "ask for the property of interest: survey.locations_a, " - "survey.locations_b, survey.locations_m, or survey.locations_n." - ) - def drape_electrodes_on_topography( - self, mesh, ind_active, option="top", topography=None, force=False + self, + mesh, + active_cells, + topo_cell_cutoff="top", + shift_horizontal=True, + option=None, + **kwargs, ): """Shift electrode locations to discrete surface topography. @@ -273,16 +251,66 @@ def drape_electrodes_on_topography( ---------- mesh : discretize.TensorMesh or discretize.TreeMesh The mesh on which the discretized fields are computed - ind_active : numpy.ndarray of int or bool + active_cells : numpy.ndarray of int or bool Active topography cells - option :{"top", "center"} + topo_cell_cutoff : {"top", "center"} Define topography at tops of cells or cell centers. topography : (n, dim) numpy.ndarray, default = ``None`` Surface topography + + .. deprecated:: v0.25.0 + + The ``topography`` argument is not used in this function. It will be + removed in SimPEG v0.27.0. + force : bool, default = ``False`` If ``True`` force electrodes to surface even if borehole + .. deprecated:: v0.25.0 + + The ``force`` argument is not used in this function. It will be removed + in SimPEG v0.27.0. + shift_horizontal : bool + When True, locations are shifted horizontally to lie vertically over cell + centers. When False, the original horizontal locations are preserved. + option : {"top", "center"} + Define topography at tops of cells or cell centers. + + .. deprecated:: 0.25.0 + + Argument ``option`` is deprecated in favor of ``topo_cell_cutoff`` + and will be removed in SimPEG v0.27.0. + """ + if option is not None: + msg = ( + "Argument ``option`` is deprecated in favor of ``topo_cell_cutoff`` " + "and will be removed in SimPEG v0.27.0." + ) + warnings.warn(msg, FutureWarning, stacklevel=2) + topo_cell_cutoff = option + + if (key := "topography") in kwargs: + msg = ( + "The `topography` argument is not used in the " + "`drape_electrodes_on_topography` and will be removed in " + "SimPEG v0.27.0." + ) + warnings.warn(msg, category=FutureWarning, stacklevel=2) + kwargs.pop(key) + + if (key := "force") in kwargs: + msg = ( + "The `force` argument is not used in the " + "`drape_electrodes_on_topography` and will be removed in " + "SimPEG v0.27.0." + ) + warnings.warn(msg, category=FutureWarning, stacklevel=2) + kwargs.pop(key) + + if kwargs: # TODO Remove this when removing kwargs argument. + raise TypeError("Unsupported keyword argument " + kwargs.popitem()[0]) + if self.survey_geometry == "surface": loc_a = self.locations_a[:, :2] loc_b = self.locations_b[:, :2] @@ -295,8 +323,12 @@ def drape_electrodes_on_topography( inv_b, inv = inv[: len(loc_b)], inv[len(loc_b) :] inv_m, inv_n = inv[: len(loc_m)], inv[len(loc_m) :] - electrodes_shifted = drapeTopotoLoc( - mesh, unique_electrodes, ind_active=ind_active, option=option + electrodes_shifted = shift_to_discrete_topography( + mesh, + unique_electrodes, + active_cells=active_cells, + topo_cell_cutoff=topo_cell_cutoff, + shift_horizontal=shift_horizontal, ) a_shifted = electrodes_shifted[inv_a] b_shifted = electrodes_shifted[inv_b] @@ -329,10 +361,3 @@ def drape_electrodes_on_topography( raise Exception( f"Input valid survey survey_geometry: {self.survey_geometry}" ) - - def drapeTopo(self, *args, **kwargs): - """This method is deprecated. See :meth:`drape_electrodes_on_topography`""" - raise TypeError( - "The drapeTopo method has been removed. Please instead " - "use the drape_electrodes_on_topography method." - ) diff --git a/simpeg/electromagnetics/static/self_potential/simulation.py b/simpeg/electromagnetics/static/self_potential/simulation.py index b97d6c21ad..05a657ad11 100644 --- a/simpeg/electromagnetics/static/self_potential/simulation.py +++ b/simpeg/electromagnetics/static/self_potential/simulation.py @@ -64,7 +64,7 @@ def __init__( rho=rho, sigmaMap=None, rhoMap=None, - **kwargs + **kwargs, ) self.q = q self.qMap = qMap @@ -78,10 +78,10 @@ def getRHSDeriv(self, source, v, adjoint=False): return self.Vol @ (self.qDeriv @ v) @property - def deleteTheseOnModelUpdate(self): + def _delete_on_model_update(self): # When enabling resistivity derivatives, uncomment these lines # if self.rhoMap is not None: - # return super().deleteTheseOnModelUpdate + # return super()._delete_on_model_update if self.storeJ and self.qMap is not None and not self.qMap.is_linear: return ["_Jmatrix", "_gtgdiag"] return [] @@ -166,7 +166,7 @@ class Survey(dc.Survey): Parameters ---------- - source_list : list of sources.StreamingCurrents + source_list : list of .sources.StreamingCurrents """ @property @@ -175,7 +175,7 @@ def source_list(self): Returns ------- - list of sources.StreamingCurrents + list of .sources.StreamingCurrents """ return self._source_list diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/data.py b/simpeg/electromagnetics/static/spectral_induced_polarization/data.py index 8535915679..f84e0185ee 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/data.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/data.py @@ -1,4 +1,5 @@ import numpy as np +from discretize.utils import mkvc from ....data import Data as BaseData diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/run.py b/simpeg/electromagnetics/static/spectral_induced_polarization/run.py index 93ee3cf5ed..34d6963be1 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/run.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/run.py @@ -12,13 +12,14 @@ def spectral_ip_mappings( mesh, - indActive=None, + active_cells=None, inactive_eta=1e-4, inactive_tau=1e-4, inactive_c=1e-4, is_log_eta=True, is_log_tau=True, is_log_c=True, + **kwargs, ): """ Generates Mappings for Spectral Induced Polarization Simulation. @@ -37,19 +38,31 @@ def spectral_ip_mappings( TODO: Illustrate input and output variables """ - if indActive is None: - indActive = np.ones(mesh.nC, dtype=bool) + # Deprecate indActive argument + if kwargs.pop("indActive", None) is not None: + raise TypeError( + "'indActive' was removed in SimPEG v0.24.0, please use 'active_cells' instead." + ) + if kwargs: # TODO Remove this when removing kwargs argument. + raise TypeError("Unsupported keyword argument " + kwargs.popitem()[0]) + + if active_cells is None: + active_cells = np.ones(mesh.nC, dtype=bool) actmap_eta = maps.InjectActiveCells( - mesh, indActive=indActive, valInactive=inactive_eta + mesh, active_cells=active_cells, value_inactive=inactive_eta ) actmap_tau = maps.InjectActiveCells( - mesh, indActive=indActive, valInactive=inactive_tau + mesh, active_cells=active_cells, value_inactive=inactive_tau + ) + actmap_c = maps.InjectActiveCells( + mesh, active_cells=active_cells, value_inactive=inactive_c ) - actmap_c = maps.InjectActiveCells(mesh, indActive=indActive, valInactive=inactive_c) wires = maps.Wires( - ("eta", indActive.sum()), ("tau", indActive.sum()), ("c", indActive.sum()) + ("eta", active_cells.sum()), + ("tau", active_cells.sum()), + ("c", active_cells.sum()), ) if is_log_eta: diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py b/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py index 87e5638875..b0b3344e93 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py @@ -45,7 +45,7 @@ def __init__( storeJ=False, actinds=None, storeInnerProduct=True, - **kwargs + **kwargs, ): super().__init__(mesh=mesh, survey=survey, **kwargs) self.tau = tau @@ -359,6 +359,7 @@ def getJ(self, m, f=None): """ Generate Full sensitivity matrix """ + self.model = m if self._Jmatrix is not None: return self._Jmatrix @@ -603,7 +604,7 @@ def Jtvec(self, m, v, f=None): return Jtv @property - def deleteTheseOnModelUpdate(self): + def _delete_on_model_update(self): toDelete = [ "_etaDeriv_store", "_tauiDeriv_store", diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/simulation_2d.py b/simpeg/electromagnetics/static/spectral_induced_polarization/simulation_2d.py index 2a0faf4906..7b1a3be006 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/simulation_2d.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/simulation_2d.py @@ -17,6 +17,8 @@ def getJ(self, m, f=None): Generate Full sensitivity matrix """ + self.model = m + if self.verbose: print(">> Compute Sensitivity matrix") diff --git a/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py b/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py index 05f5a0d460..0fdc0c54a4 100644 --- a/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py @@ -14,25 +14,20 @@ class Survey(BaseTimeSurvey): List of SimPEG spectral IP sources survey_geometry : {"surface", "borehole", "general"} Survey geometry. - survey_type : {"dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"} - Survey type. """ _n_pulse = 2 _T = 8.0 - def __init__( - self, - source_list=None, - survey_geometry="surface", - survey_type="dipole-dipole", - **kwargs - ): - if source_list is None: - raise AttributeError("Survey cannot be instantiated without sources") + def __init__(self, source_list, survey_geometry="surface", **kwargs): + if kwargs.pop("survey_type", None) is not None: + raise TypeError( + "Argument 'survey_type' has been removed in SimPEG 0.24.0. Types of sources and" + "their corresponding receivers are obtained from their respective classes, without " + "the need to specify the survey type.", + ) super(Survey, self).__init__(source_list, **kwargs) self.survey_geometry = survey_geometry - self.survey_type = survey_type @property def n_pulse(self): @@ -58,7 +53,7 @@ def T(self): @property def survey_geometry(self): - """Survey geometry; one of {"surface", "borehole", "general"} + """Survey geometry Returns ------- @@ -75,22 +70,20 @@ def survey_geometry(self, var): @property def survey_type(self): - """Survey type; one of {"dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"} + """ + ``survey_type`` has been removed. - Returns - ------- - str - Survey type; one of {"dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"} + .. important: + + The `survey_type` property has been removed. Types of sources and + their corresponding receivers are obtained from their respective + classes, without the need to specify the survey type. """ - return self._survey_type + raise AttributeError("'survey_type' has been removed.") @survey_type.setter def survey_type(self, var): - self._survey_type = validate_string( - "survey_type", - var, - ("dipole-dipole", "pole-dipole", "dipole-pole", "pole-pole"), - ) + raise AttributeError("'survey_type' has been removed.") @property def n_locations(self): diff --git a/simpeg/electromagnetics/static/spontaneous_potential/__init__.py b/simpeg/electromagnetics/static/spontaneous_potential/__init__.py index 86323379dd..758d141a51 100644 --- a/simpeg/electromagnetics/static/spontaneous_potential/__init__.py +++ b/simpeg/electromagnetics/static/spontaneous_potential/__init__.py @@ -1,69 +1,3 @@ -""" -============================================================================================ -Spontaneous Potential (:mod:`simpeg.electromagnetics.static.spontaneous_potential`) -============================================================================================ -.. currentmodule:: simpeg.electromagnetics.static.spontaneous_potential - -.. admonition:: important - - This module will be deprecated in favour of ``simpeg.electromagnetics.static.self_potential`` - - -Simulations -=========== -.. autosummary:: - :toctree: generated/ - - Simulation3DCellCentered - -Receivers -========= -This module makes use of the receivers in :mod:`simpeg.electromagnetics.static.resistivity` - -Sources -======= -.. autosummary:: - :toctree: generated/ - - sources.StreamingCurrents - -Surveys -======= -.. autosummary:: - :toctree: generated/ - - Survey - -Maps -==== -The spontaneous potential simulation provides two specialized maps to extend to inversions -with different types of model sources. - -.. autosummary:: - :toctree: generated/ - - CurrentDensityMap - HydraulicHeadMap - -""" - -import warnings - -warnings.warn( - ( - "The 'spontaneous_potential' module has been renamed to 'self_potential'. " - "Please use the 'self_potential' module instead. " - "The 'spontaneous_potential' module will be removed in SimPEG 0.23." - ), - FutureWarning, - stacklevel=2, +raise ImportError( + "The 'spontaneous_potential' module has been moved to 'self_potential'." ) - -from ..self_potential.simulation import ( - Simulation3DCellCentered, - Survey, - CurrentDensityMap, - HydraulicHeadMap, -) -from ..self_potential import sources -from ..self_potential import simulation diff --git a/simpeg/electromagnetics/static/utils/static_utils.py b/simpeg/electromagnetics/static/utils/static_utils.py index 30edce9209..43bb8e1b01 100644 --- a/simpeg/electromagnetics/static/utils/static_utils.py +++ b/simpeg/electromagnetics/static/utils/static_utils.py @@ -5,7 +5,6 @@ import matplotlib.pyplot as plt import matplotlib as mpl from matplotlib import ticker -import warnings from ..resistivity import sources, receivers from .. import resistivity as dc from ....utils import ( @@ -20,6 +19,16 @@ write_dcip3d_ubc, ) +import warnings + +try: + from warnings import deprecated +except ImportError: + # Use the deprecated decorator provided by typing_extensions (which + # supports older versions of Python) if it cannot be imported from + # warnings. + from typing_extensions import deprecated + from ....utils.plot_utils import plot_1d_layer_model # noqa: F401 @@ -76,7 +85,7 @@ def electrode_separations(survey_object, electrode_pair="all", **kwargs): Parameters ---------- survey_object : simpeg.electromagnetics.static.survey.Survey - A DC or IP survey object + A DC or IP survey object. electrode_pair : {'all', 'AB', 'MN', 'AM', 'AN', 'BM', 'BN} Which electrode separation pairs to compute. @@ -85,9 +94,7 @@ def electrode_separations(survey_object, electrode_pair="all", **kwargs): list of numpy.ndarray For each electrode pair specified, the electrode distance is returned in a list. - """ - if not isinstance(electrode_pair, list): if electrode_pair.lower() == "all": electrode_pair = ["AB", "MN", "AM", "AN", "BM", "BN"] @@ -179,11 +186,12 @@ def pseudo_locations(survey, wenner_tolerance=0.1, **kwargs): Parameters ---------- survey : simpeg.electromagnetics.static.resistivity.Survey - A DC or IP survey + A DC or IP survey. wenner_tolerance : float, default=0.1 - If the center location for a source and receiver pair are within wenner_tolerance, - we assume the datum was collected with a wenner configuration and the pseudo-location - is computed based on the AB electrode spacing. + If the center location for a source and receiver pair are within + wenner_tolerance, we assume the datum was collected with a wenner + configuration and the pseudo-location is computed based on the AB + electrode spacing. Returns ------- @@ -191,18 +199,14 @@ def pseudo_locations(survey, wenner_tolerance=0.1, **kwargs): For 2D surveys, *midxy* is a vector containing the along line position. For 3D surveys, *midxy* is an (n, 2) numpy array containing the (x,y) positions. In eithere case, *midz* is a vector containing the pseudo-depth locations. - """ - if not isinstance(survey, dc.Survey): raise TypeError(f"Input must be instance of {dc.Survey}, not {type(survey)}") - if len(kwargs) > 0: - warnings.warn( - "The keyword arguments of this function have been deprecated." + if kwargs: + raise TypeError( + "The keyword arguments of this function have been removed." " All of the necessary information is now in the DC survey class", - DeprecationWarning, - stacklevel=2, ) # Pre-allocate @@ -263,22 +267,22 @@ def geometric_factor(survey_object, space_type="half space", **kwargs): The geometric factor is given by: .. math:: - G = \frac{1}{C} \bigg [ \frac{1}{R_{AM}} - \frac{1}{R_{BM}} - \frac{1}{R_{AN}} + \frac{1}{R_{BN}} \bigg ] + G = \frac{1}{C} \bigg [ \frac{1}{R_{AM}} - \frac{1}{R_{BM}} + - \frac{1}{R_{AN}} + \frac{1}{R_{BN}} \bigg ] where :math:`C=2\pi` for a halfspace and :math:`C=4\pi` for a wholespace. Parameters ---------- survey_object : simpeg.electromagnetics.static.resistivity.Survey - A DC (or IP) survey object + A DC (or IP) survey object. space_type : {'half space', 'whole space'} Compute geometric factor for a halfspace or wholespace. Returns ------- (nD) numpy.ndarray - Geometric factor for each datum - + Geometric factor for each datum. """ # Set factor for whole-space or half-space assumption if space_type.lower() in SPACE_TYPES["whole space"]: @@ -313,20 +317,19 @@ def apparent_resistivity_from_voltage( Parameters ---------- survey : simpeg.electromagnetics.static.resistivity.Survey - A DC survey + A DC survey. volts : (nD) numpy.ndarray - Normalized voltage measurements [V/A] + Normalized voltage measurements [V/A]. space_type : {'half space', 'whole space'} Compute apparent resistivity assume a half space or whole space. eps : float, default=1e-10 - Stabilization constant in case of a null geometric factor + Stabilization constant in case of a null geometric factor. Returns ------- numpy.ndarray - Apparent resistivities for all data + Apparent resistivities for all data. """ - G = geometric_factor(survey, space_type=space_type) # Calculate apparent resistivity @@ -353,12 +356,12 @@ def convert_survey_3d_to_2d_lines( Parameters ---------- survey : simpeg.electromagnetics.static.resistivity.Survey - A DC (or IP) survey + A DC (or IP) survey. lineID : (n_data) numpy.ndarray - Defines the corresponding line ID for each datum - data_type : {'volt', 'apparent_resistivity', 'apparent_conductivity', 'apparent_chargeability'} + Defines the corresponding line ID for each datum. + data_type : {'volt', 'apparent_resistivity', 'apparent_conductivity', 'apparent_chargeability'} # E501 Data type for the survey. - output_indexing : bool, default=``False`` + output_indexing : bool, default=False, optional If ``True`` output a list of indexing arrays that map from the original 3D data to each 2D survey line. @@ -366,10 +369,26 @@ def convert_survey_3d_to_2d_lines( ------- survey_list : list of simpeg.electromagnetics.static.resistivity.Survey A list of 2D survey objects - out_indices_list : list of numpy.ndarray + out_indices_list : list of numpy.ndarray, optional A list of indexing arrays that map from the original 3D data to each 2D - survey line. + survey line. Will be returned only if ``output_indexing`` is set to + True. """ + # Check if the survey is 3D + if (ndims := survey.locations_a.shape[1]) != 3: + raise ValueError(f"Invalid {ndims}D 'survey'. It should be a 3D survey.") + # Checks on the passed lineID array + if (ndims := lineID.ndim) != 1: + raise ValueError( + f"Invalid 'lineID' array with '{ndims}' dimensions. " + "It should be a 1D array." + ) + if (size := lineID.size) != survey.nD: + raise ValueError( + f"Invalid 'lineID' array with '{size}' elements. " + "It should have the same number of elements as data " + f"in the survey ('{survey.nD}')." + ) # Find all unique line id unique_lineID = np.unique(lineID) @@ -513,33 +532,33 @@ def plot_pseudosection( plot_type : {"contourf", "scatter", "pcolor"} Which plot type to create. ax : mpl_toolkits.mplot3d.axes.Axes, optional - An axis for the plot + An axis for the plot. clim : (2) list, optional list containing the minimum and maximum value for the color range, - i.e. [vmin, vmax] + i.e. [vmin, vmax]. scale : {'linear', 'log'} Plot on linear or log base 10 scale. pcolor_opts : dict, optional - Dictionary defining kwargs for pcolor plot if `plot_type=='pcolor'` + Dictionary defining kwargs for pcolor plot if `plot_type=='pcolor'`. contourf_opts : dict, optional - Dictionary defining kwargs for filled contour plot if `plot_type=='contourf'` + Dictionary defining kwargs for filled contour plot if `plot_type=='contourf'`. scatter_opts : dict, optional - Dictionary defining kwargs for scatter plot if `plot_type=='scatter'` + Dictionary defining kwargs for scatter plot if `plot_type=='scatter'`. mask_topography : bool - This freature should be set to True when there is significant topography and the user - would like to mask interpolated locations in the filled contour plot that lie - above the surface topography. + This freature should be set to True when there is significant topography and + the user would like to mask interpolated locations in the filled contour plot + that lie above the surface topography. create_colorbar : bool If *True*, a colorbar is automatically generated. If *False*, it is not. If multiple planes are being plotted, only set the first scatter plot - to *True* + to *True*. cbar_opts : dict - Dictionary defining kwargs for the colorbar + Dictionary defining kwargs for the colorbar. cbar_label : str A string stating the color bar label for the - data; e.g. 'S/m', '$\Omega m$', '%' + data; e.g. 'S/m', '$\Omega m$', '%'. cax : mpl_toolkits.mplot3d.axes.Axes, optional - An axis object for the colorbar + An axis object for the colorbar. data_type : str, optional If dobs is ``None``, this will transform the data vector in the `survey` parameter when it is a simpeg.data.Data object from voltage to the requested `data_type`. @@ -553,13 +572,7 @@ def plot_pseudosection( ------- mpl_toolkits.mplot3d.axes3d.Axes3D The axis object that holds the plot - """ - - removed_kwargs = ["dim", "y_values", "sameratio", "survey_type"] - for kwarg in removed_kwargs: - if kwarg in kwargs: - raise TypeError(r"The {kwarg} keyword has been removed.") if len(kwargs) > 0: warnings.warn( f"plot_pseudosection unused kwargs: {list(kwargs.keys())}", stacklevel=2 @@ -767,15 +780,15 @@ def plot_3d_pseudosection( Parameters ---------- survey : simpeg.electromagnetics.static.survey.Survey - A DC or IP survey object + A DC or IP survey object. dvec : numpy.ndarray A data vector containing volts, integrated chargeabilities, apparent resistivities or apparent chargeabilities. marker_size : int - Sets the marker size for the points on the scatter plot + Sets the marker size for the points on the scatter plot. vlim : (2) list list containing the minimum and maximum value for the color range, - i.e. [vmin, vmax] + i.e. [vmin, vmax]. scale : {'linear', 'log'} Plot on linear or log base 10 scale. units : str @@ -790,18 +803,20 @@ def plot_3d_pseudosection( **plane_points**. A list is used if the *plane_distance* is different for each plane. cbar_opts: dict - Dictionary containing colorbar properties formatted according to plotly.graph_objects.scatter3d.cbar + Dictionary containing colorbar properties formatted according to + plotly.graph_objects.scatter3d.cbar. marker_opts : dict - Dictionary containing marker properties formatted according to plotly.graph_objects.scatter3d + Dictionary containing marker properties formatted according to + plotly.graph_objects.scatter3d. layout_opts : dict - Dictionary defining figure layout properties, formatted according to plotly.Layout + Dictionary defining figure layout properties, + formatted according to plotly.Layout. Returns ------- fig : A plotly figure """ - locations = pseudo_locations(survey) # Scaling @@ -957,7 +972,7 @@ def generate_survey_from_abmn_locations( locations_n : numpy.array An (n, dim) numpy array containing N electrode locations. If None, we assume all receivers are Pole receivers. - data_type : {'volt', 'apparent_conductivity', 'apparent_resistivity', 'apparent_chargeability'} + data_type : {'volt', 'apparent_conductivity', 'apparent_resistivity', 'apparent_chargeability'} # E501 Data type of the receivers. output_sorting : bool This option is used if the ABMN locations are sorted during the creation of the survey @@ -966,17 +981,14 @@ def generate_survey_from_abmn_locations( If True, the function will output a tuple containing the survey object and a numpy array (n,) that will sort the data vector to match the order of the electrodes in the survey. - Returns ------- survey - A simpeg.electromagnetic.static.survey.Survey object + A simpeg.electromagnetic.static.survey.Survey object. sort_index - A numpy array which defines any sorting that took place when creating the survey - + A numpy array which defines any sorting that took place when creating the survey. """ - if locations_a is None: raise TypeError("Locations for A electrodes must be provided.") if locations_m is None: @@ -987,7 +999,8 @@ def generate_survey_from_abmn_locations( "apparent_conductivity", "apparent_resistivity", "apparent_chargeability", - ], "data_type must be one of 'volt', 'apparent_conductivity', 'apparent_resistivity', 'apparent_chargeability'" + ], "data_type must be one of 'volt', 'apparent_conductivity', " + "'apparent_resistivity', 'apparent_chargeability'" if locations_b is None: locations_b = locations_a @@ -1082,22 +1095,22 @@ def generate_dcip_survey(endl, survey_type, a, b, n, dim=3, **kwargs): Parameters ---------- endl : numpy.ndarray - End points for survey line [x1, y1, z1, x2, y2, z2] + End points for survey line [x1, y1, z1, x2, y2, z2]. survey_type : {'dipole-dipole', 'pole-dipole', 'dipole-pole', 'pole-pole', 'gradient'} Survey type to generate. a : int - pole seperation + pole seperation. b : int - dipole separation + dipole separation. n : int - number of receiver dipoles per source + number of receiver dipoles per source. dim : int, default=3 - Create 2D or 3D survey + Create 2D or 3D survey. Returns ------- simpeg.electromagnetics.static.resistivity.Survey - A DC survey object + A DC survey object. """ def xy_2_r(x1, x2, y1, y2): @@ -1245,7 +1258,7 @@ def xy_2_r(x1, x2, y1, y2): srcClass = dc.Src.Dipole([rxClass], (endl[0, :]), (endl[1, :])) SrcList.append(srcClass) - survey = dc.Survey(SrcList, survey_type=survey_type) + survey = dc.Survey(SrcList) return survey @@ -1270,38 +1283,39 @@ def generate_dcip_sources_line( ---------- survey_type : {'dipole-dipole', 'pole-dipole', 'dipole-pole', 'pole-pole'} Survey type. - data_type : {'volt', 'apparent_conductivity', 'apparent_resistivity', 'apparent_chargeability'} + data_type : {'volt', 'apparent_conductivity', 'apparent_resistivity', 'apparent_chargeability'} # E501 Data type. dimension_type : {'2D', '3D'} Which dimension you are using. end_points : numpy.array Horizontal end points [x1, x2] or [x1, x2, y1, y2] topo : (n, dim) numpy.ndarray - Define survey topography + Define survey topography. num_rx_per_src : int - Maximum number of receivers per souces + Maximum number of receivers per souces. station_spacing : float - Distance between stations + Distance between stations. Returns ------- simpeg.electromagnetics.static.resistivity.Survey - A DC survey object + A DC survey object. """ - assert survey_type.lower() in [ "pole-pole", "pole-dipole", "dipole-pole", "dipole-dipole", - ], "survey_type must be one of 'pole-pole', 'pole-dipole', 'dipole-pole', 'dipole-dipole'" + ], "survey_type must be one of 'pole-pole', 'pole-dipole', " + "'dipole-pole', 'dipole-dipole'" assert data_type.lower() in [ "volt", "apparent_conductivity", "apparent_resistivity", "apparent_chargeability", - ], "data_type must be one of 'volt', 'apparent_conductivity', 'apparent_resistivity', 'apparent_chargeability'" + ], "data_type must be one of 'volt', 'apparent_conductivity', " + "'apparent_resistivity', 'apparent_chargeability'" assert dimension_type.upper() in [ "2D", @@ -1404,19 +1418,18 @@ def xy_2_lineID(dc_survey): Read DC survey class and append line ID. Assumes that the locations are listed in the order they were collected. May need to generalize for random - point locations, but will be more expensive + point locations, but will be more expensive. Parameters ---------- dc_survey : dict - Vectors of station location + Vectors of station location. Returns ------- numpy.ndarray - LineID Vector of integers + LineID Vector of integers. """ - # Compute unit vector between two points nstn = dc_survey.nSrc @@ -1479,21 +1492,20 @@ def xy_2_lineID(dc_survey): def r_unit(p1, p2): - """Compute unit vector between two points + """Compute unit vector between two points. Parameters ---------- p1 : (dim) numpy.array - Start point + Start point. p2 : (dim) numpy.array - End point + End point. Returns ------- (dim) numpy.array - Unit vector + Unit vector. """ - assert len(p1) == len(p2), "locs must be the same shape." dx = [] @@ -1512,10 +1524,24 @@ def r_unit(p1, p2): return vec, r +@deprecated( + "The `gettopoCC` function is deprecated, " + "and will be removed in SimPEG v0.27.0. " + "This functionality has been replaced by the " + "'get_discrete_topography' function, which can be imported from" + "simpeg.utils", + category=FutureWarning, +) def gettopoCC(mesh, ind_active, option="top"): """ Generate surface topography from active indices of mesh. + .. deprecated:: 0.25.0 + + ``gettopoCC`` will be removed in SimPEG v0.27.0. + Please, use the :func:`simpeg.utils.get_discrete_topography` function + instead. + Parameters ---------- mesh : discretize.TensorMesh or discretize.TreeMesh @@ -1581,31 +1607,55 @@ def gettopoCC(mesh, ind_active, option="top"): raise NotImplementedError(f"{type(mesh)} mesh is not supported.") -def drapeTopotoLoc(mesh, pts, ind_active=None, option="top", topo=None, **kwargs): - """Drape locations right below discretized surface topography +@deprecated( + "The `drapeTopotoLoc` function is deprecated, " + "and will be removed in SimPEG v0.27.0. " + "This functionality has been replaced by the " + "'shift_to_discrete_topography' function, which can be imported from" + "simpeg.utils", + category=FutureWarning, +) +def drapeTopotoLoc(mesh, pts, active_cells=None, option="top", topo=None, **kwargs): + """Drape locations right below discretized surface topography. This function projects the set of locations provided to the discrete surface topography. + .. deprecated:: 0.25.0 + + ``drapeTopotoLoc`` will be removed in SimPEG v0.27.0. + Please, use the :func:`simpeg.utils.shift_to_discrete_topography` function + instead. + Parameters ---------- mesh : discretize.TensorMesh or discretize.TreeMesh - A 2D tensor or tree mesh + A 2D tensor or tree mesh. pts : (n, dim) numpy.ndarray The set of points being projected to the discretize surface topography - ind_active : numpy.ndarray of int or bool, optional - Index array for all cells lying below the surface topography. Surface topography - can be specified using the 'ind_active' or 'topo' input parameters. + active_cells : numpy.ndarray of int or bool, optional + Index array for all cells lying below the surface topography. + Surface topography can be specified using the 'active_cells' or + 'topo' input parameters. option : {"top", "center"} - Define whether the cell center or entire cell of actice cells must be below the topography. - The topography is defined using the 'topo' input parameter. + Define whether the cell center or entire cell of actice cells must be below + the topography. The topography is defined using the 'topo' input parameter. topo : (n, dim) numpy.ndarray Surface topography. Can be used if an active indices array cannot be provided - for the input parameter 'ind_active' - """ + for the input parameter 'active_cells'. - if "actind" in kwargs: - ind_active = kwargs.pop("actind") + Returns + ------- + (n, dim) numpy.ndarray + The discrete topography locations. + """ + # Deprecate indActive argument + if kwargs.pop("indActive", None) is not None: + raise TypeError( + "'indActive' was removed in SimPEG v0.24.0, please use 'active_cells' instead." + ) + if kwargs: # TODO Remove this when removing kwargs argument. + raise TypeError("Unsupported keyword argument " + kwargs.popitem()[0]) if isinstance(mesh, discretize.CurvilinearMesh): raise ValueError("Curvilinear mesh is not supported.") @@ -1624,22 +1674,43 @@ def drapeTopotoLoc(mesh, pts, ind_active=None, option="top", topo=None, **kwargs else: raise ValueError("Unsupported mesh dimension") - if ind_active is None: - ind_active = discretize.utils.active_from_xyz(mesh, topo) + if active_cells is None: + active_cells = discretize.utils.active_from_xyz(mesh, topo) if mesh._meshType == "TENSOR": - meshtemp, topoCC = gettopoCC(mesh, ind_active, option=option) + # Ignore FutureWarning coming from gettopoCC's deprecation + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + message="The `gettopoCC` function is deprecated", + category=FutureWarning, + ) + meshtemp, topoCC = gettopoCC(mesh, active_cells, option=option) inds = meshtemp.closest_points_index(pts) topo = topoCC[inds] out = np.c_[pts, topo] elif mesh._meshType == "TREE": if mesh.dim == 3: - uniqXYlocs, topoCC = gettopoCC(mesh, ind_active, option=option) + # Ignore FutureWarning coming from gettopoCC's deprecation + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + message="The `gettopoCC` function is deprecated", + category=FutureWarning, + ) + uniqXYlocs, topoCC = gettopoCC(mesh, active_cells, option=option) inds = closestPointsGrid(uniqXYlocs, pts) out = np.c_[uniqXYlocs[inds, :], topoCC[inds]] else: - uniqXlocs, topoCC = gettopoCC(mesh, ind_active, option=option) + # Ignore FutureWarning coming from gettopoCC's deprecation + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + message="The `gettopoCC` function is deprecated", + category=FutureWarning, + ) + uniqXlocs, topoCC = gettopoCC(mesh, active_cells, option=option) inds = closestPointsGrid(uniqXlocs, pts, dim=1) out = np.c_[uniqXlocs[inds], topoCC[inds]] else: @@ -1649,24 +1720,24 @@ def drapeTopotoLoc(mesh, pts, ind_active=None, option="top", topo=None, **kwargs def genTopography(mesh, zmin, zmax, seed=None, its=100, anisotropy=None): - """Generate random topography + """Generate random topography. Parameters ---------- mesh : discretize.BaseMesh - A 2D or 3D mesh + A 2D or 3D mesh. zmin : float - Minimum topography [m] + Minimum topography [m]. zmax : float - Maximum topography [m] + Maximum topography [m]. seed : int, default=``None`` - Set the seed for the random generated model or leave as ``None`` + Set the seed for the random generated model or leave as ``None``. its : int, default=100 - Number of smoothing iterations after convolutions + Number of smoothing iterations after convolutions. anisotropy : (3, n) numpy.ndarray, default=``None`` - Apply a (3, n) blurring kernel that is used or leave as ``None`` in the case of isotropy. + Apply a (3, n) blurring kernel that is used or leave as ``None`` + in the case of isotropy. """ - if isinstance(mesh, discretize.CurvilinearMesh): raise ValueError("Curvilinear mesh is not supported.") @@ -1675,35 +1746,52 @@ def genTopography(mesh, zmin, zmax, seed=None, its=100, anisotropy=None): [mesh.h[0], mesh.h[1]], x0=[mesh.x0[0], mesh.x0[1]] ) out = model_builder.create_random_model( - mesh.vnC[:2], bounds=[zmin, zmax], its=its, seed=seed, anisotropy=anisotropy + mesh.vnC[:2], + bounds=[zmin, zmax], + its=its, + random_seed=seed, + anisotropy=anisotropy, ) return out, mesh2D elif mesh.dim == 2: mesh1D = discretize.TensorMesh([mesh.h[0]], x0=[mesh.x0[0]]) out = model_builder.create_random_model( - mesh.vnC[:1], bounds=[zmin, zmax], its=its, seed=seed, anisotropy=anisotropy + mesh.vnC[:1], + bounds=[zmin, zmax], + its=its, + random_seed=seed, + anisotropy=anisotropy, ) return out, mesh1D else: raise Exception("Only works for 2D and 3D models") +@deprecated( + "The `closestPointsGrid` function is now deprecated. " + "It will be removed in SimPEG v0.27.0.", + category=FutureWarning, +) def closestPointsGrid(grid, pts, dim=2): - """Move a list of points to the closest points on a grid. + """Return indices of closest gridded points for a set of input points. + + .. deprecated:: 0.25.0 + + ``closestPointsGrid`` will be removed in SimPEG v0.27.0. Parameters ---------- grid : (n, dim) numpy.ndarray - A gridded set of points + A gridded set of points. pts : (m, dim) numpy.ndarray - Points being projected to gridded locations + Points being projected to gridded locations. dim : int, default=2 - Dimension of the points + Dimension of the points. Returns ------- - (m) numpy.ndarray - indices for the closest gridded location for all *pts* supplied. + (n,) numpy.ndarray + Indices of the closest gridded points for all *pts* supplied. """ if dim == 1: nodeInds = np.asarray( @@ -1737,27 +1825,28 @@ def gen_3d_survey_from_2d_lines( Parameters ---------- survey_type : str - Survey type. Choose one of {'dipole-dipole', 'pole-dipole', 'dipole-pole', 'pole-pole', 'gradient'} + Survey type. Choose one of {'dipole-dipole', 'pole-dipole', + 'dipole-pole', 'pole-pole', 'gradient'}. a : int - pole seperation + pole seperation. b : int - dipole separation + dipole separation. n_spacing : int - number of receiver dipoles per source + number of receiver dipoles per source. n_lines : int, default=5 - Number of survey lines + Number of survey lines. line_length : float, default=200. Line length line_spacing : float, default=20. - Line spacing + Line spacing. x0, y0, z0 : float, default=0. - The origin for the 3D survey + The origin for the 3D survey. src_offset_y : float, default=0. - Source y offset + Source y offset. dim : int, default=3 - Define 2D or 3D survey + Define 2D or 3D survey. is_IO : bool, default=``True`` - If ``True``, is an IO class + If ``True``, is an IO class. Returns ------- diff --git a/simpeg/electromagnetics/time_domain/__init__.py b/simpeg/electromagnetics/time_domain/__init__.py index 3fb8db7522..5891b847ce 100644 --- a/simpeg/electromagnetics/time_domain/__init__.py +++ b/simpeg/electromagnetics/time_domain/__init__.py @@ -1,10 +1,28 @@ -""" +r""" ============================================================================== Time-Domain EM (:mod:`simpeg.electromagnetics.time_domain`) ============================================================================== .. currentmodule:: simpeg.electromagnetics.time_domain -About ``time_domain`` +The ``time_domain`` module contains functionality for solving Maxwell's equations +in the time-domain for controlled sources. Here, electric displacement is ignored, +and functionality is used to solve: + +.. math:: + \begin{align} + \nabla \times \vec{e} + \frac{\partial \vec{b}}{\partial t} &= -\frac{\partial \vec{s}_m}{\partial t} \\ + \nabla \times \vec{h} - \vec{j} &= \vec{s}_e + \end{align} + +where the constitutive relations between fields and fluxes are given by: + +* :math:`\vec{j} = \sigma \vec{e}` +* :math:`\vec{b} = \mu \vec{h}` + +and: + +* :math:`\vec{s}_m` represents a magnetic source term +* :math:`\vec{s}_e` represents a current source term Simulations =========== diff --git a/simpeg/electromagnetics/time_domain/fields.py b/simpeg/electromagnetics/time_domain/fields.py index 384432c736..927a35e3b1 100644 --- a/simpeg/electromagnetics/time_domain/fields.py +++ b/simpeg/electromagnetics/time_domain/fields.py @@ -6,31 +6,48 @@ class FieldsTDEM(TimeFields): - r""" - Fancy Field Storage for a TDEM simulation. Only one field type is stored for - each problem, the rest are computed. The fields obejct acts like an array - and is indexed by + r"""Base class for storing TDEM fields. + + TDEM fields classes are used to store the discrete solution of the fields for a + corresponding TDEM simulation; see :class:`.time_domain.BaseTDEMSimulation`. + Only one field type (e.g. ``'e'``, ``'j'``, ``'h'``, ``'b'``) is stored, but certain field types + can be rapidly computed and returned on the fly. The field type that is stored and the + field types that can be returned depend on the formulation used by the associated simulation class. + Once a field object has been created, the individual fields can be accessed; see the example below. + + Parameters + ---------- + simulation : .time_domain.BaseTDEMSimulation + The TDEM simulation object used to compute the discrete field solution. + + Example + ------- + We want to access the fields for a discrete solution with :math:`\mathbf{e}` discretized + to edges and :math:`\mathbf{b}` discretized to faces. To extract the fields for all sources + and all time steps: .. code-block:: python - f = problem.fields(m) - e = f[source_list,'e'] - b = f[source_list,'b'] + f = simulation.fields(m) + e = f[:, 'e', :] + b = f[:, 'b', :] - If accessing all sources for a given field, use the :code:`:` + The array ``e`` returned will have shape (`n_edges`, `n_sources`, `n_steps`). + And the array ``b`` returned will have shape (`n_faces`, `n_sources`, `n_steps`). + We can also extract the fields for + a subset of the source list used for the simulation and/or a subset of the time steps as follows: .. code-block:: python - f = problem.fields(m) - e = f[:,'e'] - b = f[:,'b'] + f = simulation.fields(m) + e = f[source_list, 'e', t_inds] + b = f[source_list, 'b', t_inds] - The array returned will be size (nE or nF, nSrcs :math:`\times` - nFrequencies) """ - knownFields = {} - dtype = float + def __init__(self, simulation): + dtype = float + super().__init__(simulation=simulation, dtype=dtype) def _GLoc(self, fieldType): """Grid location of the fieldType""" @@ -86,49 +103,105 @@ def _jDeriv(self, tInd, src, dun_dm_v, v, adjoint=False): class FieldsDerivativesEB(FieldsTDEM): - """ - A fields object for satshing derivs in the EB formulation + r"""Field class for stashing derivatives for EB formulations. + + Parameters + ---------- + simulation : .time_domain.BaseTDEMSimulation + The TDEM simulation object associated with the fields. + """ - knownFields = { - "bDeriv": "F", - "eDeriv": "E", - "hDeriv": "F", - "jDeriv": "E", - "dbdtDeriv": "F", - "dhdtDeriv": "F", - } + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = { + "bDeriv": "F", + "eDeriv": "E", + "hDeriv": "F", + "jDeriv": "E", + "dbdtDeriv": "F", + "dhdtDeriv": "F", + } class FieldsDerivativesHJ(FieldsTDEM): - """ - A fields object for satshing derivs in the HJ formulation + r"""Field class for stashing derivatives for HJ formulations. + + Parameters + ---------- + simulation : .time_domain.BaseTDEMSimulation + The TDEM simulation object associated with the fields. + """ - knownFields = { - "bDeriv": "E", - "eDeriv": "F", - "hDeriv": "E", - "jDeriv": "F", - "dbdtDeriv": "E", - "dhdtDeriv": "E", - } + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = { + "bDeriv": "E", + "eDeriv": "F", + "hDeriv": "E", + "jDeriv": "F", + "dbdtDeriv": "E", + "dhdtDeriv": "E", + } class Fields3DMagneticFluxDensity(FieldsTDEM): - """Field Storage for a TDEM simulation.""" - - knownFields = {"bSolution": "F"} - aliasFields = { - "b": ["bSolution", "F", "_b"], - "h": ["bSolution", "F", "_h"], - "e": ["bSolution", "E", "_e"], - "j": ["bSolution", "E", "_j"], - "dbdt": ["bSolution", "F", "_dbdt"], - "dhdt": ["bSolution", "F", "_dhdt"], - } + r"""Fields class for storing 3D total magnetic flux density solutions. + + This class stores the total magnetic flux density solution computed using a + :class:`.time_domain.Simulation3DMagneticFluxDensity` + simulation object. This class can be used to extract the following quantities: + + * ``'b'``, ``'h'``, ``'dbdt'`` and ``'dhdt'`` on mesh faces. + * ``'e'`` and ``'j'`` on mesh edges. + + See the example below to learn how fields can be extracted from a + ``Fields3DMagneticFluxDensity`` object. + + Parameters + ---------- + simulation : .time_domain.Simulation3DMagneticFluxDensity + The TDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DMagneticFluxDensity`` object stores the total magnetic flux density solution + on mesh faces. To extract the discrete electric fields and magnetic flux + densities for all sources and time-steps: + + .. code-block:: python + + f = simulation.fields(m) + e = f[:, 'e', :] + b = f[:, 'b', :] + + The array ``e`` returned will have shape (`n_edges`, `n_sources`, `n_steps`). + And the array ``b`` returned will have shape (`n_faces`, `n_sources`, `n_steps`). + We can also extract the fields for a subset of the sources and time-steps as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list, 'e', t_inds] + b = f[source_list, 'b', t_inds] + + """ + + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"bSolution": "F"} + self._aliasFields = { + "b": ["bSolution", "F", "_b"], + "h": ["bSolution", "F", "_h"], + "e": ["bSolution", "E", "_e"], + "j": ["bSolution", "E", "_j"], + "dbdt": ["bSolution", "F", "_dbdt"], + "dhdt": ["bSolution", "F", "_dhdt"], + } def startup(self): + # Docstring inherited from parent. self._times = self.simulation.times self._MeSigma = self.simulation.MeSigma self._MeSigmaI = self.simulation.MeSigmaI @@ -273,19 +346,61 @@ def _dhdtDeriv_m(self, tInd, src, v, adjoint=False): class Fields3DElectricField(FieldsTDEM): - """Fancy Field Storage for a TDEM simulation.""" - - knownFields = {"eSolution": "E"} - aliasFields = { - "e": ["eSolution", "E", "_e"], - "j": ["eSolution", "E", "_j"], - "b": ["eSolution", "F", "_b"], - # 'h': ['eSolution', 'F', '_h'], - "dbdt": ["eSolution", "F", "_dbdt"], - "dhdt": ["eSolution", "F", "_dhdt"], - } + r"""Fields class for storing 3D total electric field solutions. + + This class stores the total electric field solution computed using a + :class:`.time_domain.Simulation3DElectricField` + simulation object. This class can be used to extract the following quantities: + + * ``'e'`` and ``'j'`` on mesh edges. + * ``'b'``, ``'dbdt'`` and ``'dhdt'`` on mesh faces. + + See the example below to learn how fields can be extracted from a + ``Fields3DElectricField`` object. + + Parameters + ---------- + simulation : .time_domain.Simulation3DElectricField + The TDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DElectricField`` object stores the total electric field solution + on mesh edges. To extract the discrete electric fields and db/dt + for all sources and time-steps: + + .. code-block:: python + + f = simulation.fields(m) + e = f[:, 'e', :] + dbdt = f[:, 'dbdt', :] + + The array ``e`` returned will have shape (`n_edges`, `n_sources`, `n_steps`). + And the array ``dbdt`` returned will have shape (`n_faces`, `n_sources`, `n_steps`). + We can also extract the fields for a subset of the sources and time-steps as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list, 'e', t_inds] + dbdt = f[source_list, 'dbdt', t_inds] + + """ + + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"eSolution": "E"} + self._aliasFields = { + "e": ["eSolution", "E", "_e"], + "j": ["eSolution", "E", "_j"], + "b": ["eSolution", "F", "_b"], + # 'h': ['eSolution', 'F', '_h'], + "dbdt": ["eSolution", "F", "_dbdt"], + "dhdt": ["eSolution", "F", "_dhdt"], + } def startup(self): + # Docstring inherited from parent. self._times = self.simulation.times self._MeSigma = self.simulation.MeSigma self._MeSigmaI = self.simulation.MeSigmaI @@ -392,20 +507,63 @@ def _dhdtDeriv_m(self, tInd, src, v, adjoint=False): class Fields3DMagneticField(FieldsTDEM): - """Fancy Field Storage for a TDEM simulation.""" - - knownFields = {"hSolution": "E"} - aliasFields = { - "h": ["hSolution", "E", "_h"], - "b": ["hSolution", "E", "_b"], - "dhdt": ["hSolution", "E", "_dhdt"], - "dbdt": ["hSolution", "E", "_dbdt"], - "j": ["hSolution", "F", "_j"], - "e": ["hSolution", "F", "_e"], - "charge": ["hSolution", "CC", "_charge"], - } + r"""Fields class for storing 3D total magnetic field solutions. + + This class stores the total magnetic field solution computed using a + :class:`.time_domain.Simulation3DElectricField` + simulation object. This class can be used to extract the following quantities: + + * ``'h'``, ``'b'``, ``'dbdt'`` and ``'dbdt'`` on mesh edges. + * ``'j'`` and ``'e'`` on mesh faces. + * ``'charge'`` at cell centers. + + See the example below to learn how fields can be extracted from a + ``Fields3DMagneticField`` object. + + Parameters + ---------- + simulation : .time_domain.Simulation3DMagneticField + The TDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DMagneticField`` object stores the total magnetic field solution + on mesh edges. To extract the discrete magnetic fields and current density + for all sources and time-steps: + + .. code-block:: python + + f = simulation.fields(m) + h = f[:, 'h', :] + j = f[:, 'j', :] + + The array ``h`` returned will have shape (`n_edges`, `n_sources`, `n_steps`). + And the array ``j`` returned will have shape (`n_faces`, `n_sources`, `n_steps`). + We can also extract the fields for a subset of the sources and time-steps as follows: + + .. code-block:: python + + f = simulation.fields(m) + h = f[source_list, 'e', t_inds] + j = f[source_list, 'j', t_inds] + + """ + + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"hSolution": "E"} + self._aliasFields = { + "h": ["hSolution", "E", "_h"], + "b": ["hSolution", "E", "_b"], + "dhdt": ["hSolution", "E", "_dhdt"], + "dbdt": ["hSolution", "E", "_dbdt"], + "j": ["hSolution", "F", "_j"], + "e": ["hSolution", "F", "_e"], + "charge": ["hSolution", "CC", "_charge"], + } def startup(self): + # Docstring inherited from parent. self._times = self.simulation.times self._edgeCurl = self.simulation.mesh.edge_curl self._MeMuI = self.simulation.MeMuI @@ -559,19 +717,62 @@ def _charge(self, hSolution, source_list, tInd): class Fields3DCurrentDensity(FieldsTDEM): - """Fancy Field Storage for a TDEM simulation.""" - - knownFields = {"jSolution": "F"} - aliasFields = { - "dhdt": ["jSolution", "E", "_dhdt"], - "dbdt": ["jSolution", "E", "_dbdt"], - "j": ["jSolution", "F", "_j"], - "e": ["jSolution", "F", "_e"], - "charge": ["jSolution", "CC", "_charge"], - "charge_density": ["jSolution", "CC", "_charge_density"], - } + r"""Fields class for storing 3D current density solutions. + + This class stores the total current density solution computed using a + :class:`.time_domain.Simulation3DCurrentDensity` + simulation object. This class can be used to extract the following quantities: + + * ``'j'`` and ``'e'`` on mesh faces. + * ``'dbdt'`` and ``'dhdt'`` on mesh edges. + * ``'charge'`` and ``'charge_density'`` at cell centers. + + See the example below to learn how fields can be extracted from a + ``Fields3DCurrentDensity`` object. + + Parameters + ---------- + simulation : .time_domain.Simulation3DCurrentDensity + The TDEM simulation object associated with the fields. + + Example + ------- + The ``Fields3DCurrentDensity`` object stores the total current density solution + on mesh faces. To extract the discrete current densities and magnetic fields + for all sources and time-steps: + + .. code-block:: python + + f = simulation.fields(m) + j = f[:, 'j', :] + h = f[:, 'h', :] + + The array ``j`` returned will have shape (`n_faces`, `n_sources`, `n_steps`). + And the array ``h`` returned will have shape (`n_edges`, `n_sources`, `n_steps`). + We can also extract the fields for a subset of the sources and time-steps as follows: + + .. code-block:: python + + f = simulation.fields(m) + j = f[source_list, 'j', t_inds] + h = f[source_list, 'h', t_inds] + + """ + + def __init__(self, simulation): + super().__init__(simulation=simulation) + self._knownFields = {"jSolution": "F"} + self._aliasFields = { + "dhdt": ["jSolution", "E", "_dhdt"], + "dbdt": ["jSolution", "E", "_dbdt"], + "j": ["jSolution", "F", "_j"], + "e": ["jSolution", "F", "_e"], + "charge": ["jSolution", "CC", "_charge"], + "charge_density": ["jSolution", "CC", "_charge_density"], + } def startup(self): + # Docstring inherited from parent. self._times = self.simulation.times self._edgeCurl = self.simulation.mesh.edge_curl self._MeMuI = self.simulation.MeMuI diff --git a/simpeg/electromagnetics/time_domain/simulation.py b/simpeg/electromagnetics/time_domain/simulation.py index edb374cf06..94ad7a4b4f 100644 --- a/simpeg/electromagnetics/time_domain/simulation.py +++ b/simpeg/electromagnetics/time_domain/simulation.py @@ -1,7 +1,6 @@ import numpy as np import scipy.sparse as sp -from ...data import Data from ...simulation import BaseTimeSimulation from ...utils import mkvc, sdiag, speye, Zero, validate_type, validate_float from ..base import BaseEMSimulation @@ -17,10 +16,39 @@ class BaseTDEMSimulation(BaseTimeSimulation, BaseEMSimulation): - """ - We start with the first order form of Maxwell's equations, eliminate and - solve the second order form. For the time discretization, we use backward - Euler. + r"""Base class for quasi-static TDEM simulation with finite volume. + + This class is used to define properties and methods necessary for solving + 3D time-domain EM problems. In the quasi-static regime, we ignore electric + displacement, and Maxwell's equations are expressed as: + + .. math:: + \begin{align} + \nabla \times \vec{e} + \frac{\partial \vec{b}}{\partial t} &= -\frac{\partial \vec{s}_m}{\partial t} \\ + \nabla \times \vec{h} - \vec{j} &= \vec{s}_e + \end{align} + + where the constitutive relations between fields and fluxes are given by: + + * :math:`\vec{j} = \sigma \vec{e}` + * :math:`\vec{b} = \mu \vec{h}` + + and: + + * :math:`\vec{s}_m` represents a magnetic source term + * :math:`\vec{s}_e` represents a current source term + + Child classes of ``BaseTDEMSimulation`` solve the above expression numerically + for various cases using mimetic finite volume and backward Euler time discretization. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .time_domain.survey.Survey + The time-domain EM survey. + dt_threshold : float + Threshold used when determining the unique time-step lengths. """ def __init__(self, mesh, survey=None, dt_threshold=1e-8, **kwargs): @@ -34,10 +62,12 @@ def __init__(self, mesh, survey=None, dt_threshold=1e-8, **kwargs): @property def survey(self): - """The survey for the simulation + """The TDEM survey object. + Returns ------- - simpeg.electromagnetics.time_domain.survey.Survey + .time_domain.survey.Survey + The TDEM survey object. """ if self._survey is None: raise AttributeError("Simulation must have a survey set") @@ -51,14 +81,17 @@ def survey(self, value): @property def dt_threshold(self): - """The threshold used to determine if a previous matrix factor can be reused. + """Threshold used when determining the unique time-step lengths. - If the difference in time steps falls below this threshold, the factored matrix - is re-used. + The number of linear systems that must be factored to solve the forward + problem is equal to the number of unique time-step lengths. *dt_threshold* + effectively sets the round-off error when determining the unique time-step + lengths used by the simulation. Returns ------- float + Threshold used when determining the unique time-step lengths. """ return self._dt_threshold @@ -66,23 +99,18 @@ def dt_threshold(self): def dt_threshold(self, value): self._dt_threshold = validate_float("dt_threshold", value, min_val=0.0) - # def fields_nostore(self, m): - # """ - # Solve the forward problem without storing fields - - # :param numpy.ndarray m: inversion model (nP,) - # :rtype: numpy.ndarray - # :return numpy.ndarray: numpy.ndarray (nD,) - - # """ - def fields(self, m): - """ - Solve the forward problem for the fields. + """Compute and return the fields for the model provided. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model. - :param numpy.ndarray m: inversion model (nP,) - :rtype: simpeg.electromagnetics.time_domain.fields.FieldsTDEM - :return f: fields object + Returns + ------- + .time_domain.fields.FieldsTDEM + The TDEM fields object. """ self.model = m @@ -138,26 +166,34 @@ def fields(self, m): return f def Jvec(self, m, v, f=None): - r""" - Jvec computes the sensitivity times a vector + r"""Compute the sensitivity matrix times a vector. + + Where :math:`\mathbf{d}` are the data, :math:`\mathbf{m}` are the model parameters, + and the sensitivity matrix is defined as: .. math:: - \mathbf{J} \mathbf{v} = - \frac{d\mathbf{P}}{d\mathbf{F}} - \left( - \frac{d\mathbf{F}}{d\mathbf{u}} \frac{d\mathbf{u}}{d\mathbf{m}} - + \frac{\partial\mathbf{F}}{\partial\mathbf{m}} - \right) - \mathbf{v} + \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} - where + this method computes and returns the matrix-vector product: .. math:: - \mathbf{A} \frac{d\mathbf{u}}{d\mathbf{m}} - + \frac{\partial \mathbf{A} (\mathbf{u}, \mathbf{m})} - {\partial\mathbf{m}} = - \frac{d \mathbf{RHS}}{d \mathbf{m}} + \mathbf{J v} + for a given vector :math:`v`. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + v : (n_param,) numpy.ndarray + The vector. + f : .time_domain.fields.FieldsTDEM, optional + Fields solved for all sources. + + Returns + ------- + (n_data,) numpy.ndarray + The sensitivity matrix times a vector. """ if f is None: @@ -168,7 +204,7 @@ def Jvec(self, m, v, f=None): # mat to store previous time-step's solution deriv times a vector for # each source - # size: nu x nSrc + # size: nu x n_sources # this is a bit silly @@ -248,27 +284,34 @@ def Jvec(self, m, v, f=None): return np.hstack(Jv) def Jtvec(self, m, v, f=None): - r""" - Jvec computes the adjoint of the sensitivity times a vector + r"""Compute the adjoint sensitivity matrix times a vector. - .. math:: + Where :math:`\mathbf{d}` are the data, :math:`\mathbf{m}` are the model parameters, + and the sensitivity matrix is defined as: - \mathbf{J}^\top \mathbf{v} = - \left( - \frac{d\mathbf{u}}{d\mathbf{m}} ^ \top - \frac{d\mathbf{F}}{d\mathbf{u}} ^ \top - + \frac{\partial\mathbf{F}}{\partial\mathbf{m}} ^ \top - \right) - \frac{d\mathbf{P}}{d\mathbf{F}} ^ \top - \mathbf{v} + .. math:: + \mathbf{J} = \dfrac{\partial \mathbf{d}}{\partial \mathbf{m}} - where + this method computes and returns the matrix-vector product: .. math:: + \mathbf{J^T v} + + for a given vector :math:`v`. - \frac{d\mathbf{u}}{d\mathbf{m}} ^\top \mathbf{A}^\top + - \frac{d\mathbf{A}(\mathbf{u})}{d\mathbf{m}} ^ \top = - \frac{d \mathbf{RHS}}{d \mathbf{m}} ^ \top + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + v : (n_data,) numpy.ndarray + The vector. + f : .time_domain.fields.FieldsTDEM, optional + Fields solved for all sources. + + Returns + ------- + (n_param,) numpy.ndarray + The adjoint sensitivity matrix times a vector. """ if f is None: @@ -277,9 +320,8 @@ def Jtvec(self, m, v, f=None): self.model = m ftype = self._fieldType + "Solution" # the thing we solved for - # Ensure v is a data object. - if not isinstance(v, Data): - v = Data(self.survey, v) + # Get dict of flat array slices for each source-receiver pair in the survey + survey_slices = self.survey.get_all_slices() df_duT_v = self.Fields_Derivs(self) @@ -307,8 +349,9 @@ def Jtvec(self, m, v, f=None): ) for rx in src.receiver_list: + src_rx_slice = survey_slices[src, rx] PT_v[src, "{}Deriv".format(rx.projField), :] = rx.evalDeriv( - src, self.mesh, self.time_mesh, f, mkvc(v[src, rx]), adjoint=True + src, self.mesh, self.time_mesh, f, v[src_rx_slice], adjoint=True ) # this is += # PT_v = np.reshape(curPT_v,(len(curPT_v)/self.time_mesh.nN, @@ -360,11 +403,18 @@ def Jtvec(self, m, v, f=None): ATinv_df_duT_v[isrc, :] = ( AdiagTinv * df_duT_v[src, "{}Deriv".format(self._fieldType), tInd + 1] - ) + ).squeeze() elif tInd > -1: - ATinv_df_duT_v[isrc, :] = AdiagTinv * ( - mkvc(df_duT_v[src, "{}Deriv".format(self._fieldType), tInd + 1]) - - Asubdiag.T * mkvc(ATinv_df_duT_v[isrc, :]) + ATinv_df_duT_v[isrc, :] = ( + AdiagTinv + * ( + mkvc( + df_duT_v[ + src, "{}Deriv".format(self._fieldType), tInd + 1 + ] + ) + - Asubdiag.T * mkvc(ATinv_df_duT_v[isrc, :]) + ).squeeze() ) dAsubdiagT_dm_v = self.getAsubdiagDeriv( @@ -392,9 +442,32 @@ def Jtvec(self, m, v, f=None): return mkvc(JTv).astype(float) def getSourceTerm(self, tInd): - """ - Assemble the source term. This ensures that the RHS is a vector / array - of the correct size + r"""Return the discrete source terms for the time index provided. + + This method computes and returns the discrete magnetic and electric source terms for + all soundings at the time index provided. The exact shape and implementation of source + terms when solving for the fields at each time-step is formulation dependent. + + For definitions of the discrete magnetic (:math:`\mathbf{s_m}`) and electric + (:math:`\mathbf{s_e}`) source terms for each simulation, see the *Notes* sections + of the docstrings for: + + * :class:`.time_domain.Simulation3DElectricField` + * :class:`.time_domain.Simulation3DMagneticField` + * :class:`.time_domain.Simulation3DCurrentDensity` + * :class:`.time_domain.Simulation3DMagneticFluxDensity` + + Parameters + ---------- + tInd : int + The time index. Value between ``[0, n_steps]``. + + Returns + ------- + s_m : numpy.ndarray + The magnetic sources terms. (n_faces, n_sources) for EB-formulations. (n_edges, n_sources) for HJ-formulations. + s_e : numpy.ndarray + The electric sources terms. (n_edges, n_sources) for EB-formulations. (n_faces, n_sources) for HJ-formulations. """ Srcs = self.survey.source_list @@ -414,8 +487,12 @@ def getSourceTerm(self, tInd): return s_m, s_e def getInitialFields(self): - """ - Ask the sources for initial fields + """Returns the fields for all sources at the initial time. + + Returns + ------- + (n_edges or n_faces, n_sources) numpy.ndarray + The fields for all sources at the initial time. """ Srcs = self.survey.source_list @@ -436,6 +513,41 @@ def getInitialFields(self): return ifields def getInitialFieldsDeriv(self, src, v, adjoint=False, f=None): + r"""Derivative of the initial fields with respect to the model for a given source. + + For a given source object `src`, let :math:`\mathbf{u_0}` represent the initial + fields discretized to the mesh. Where :math:`\mathbf{m}` are the model parameters + and :math:`\mathbf{v}` is a vector, this method computes and returns: + + .. math:: + \dfrac{\partial \mathbf{u_0}}{\partial \mathbf{m}} \, \mathbf{v} + + or the adjoint operation: + + .. math:: + \dfrac{\partial \mathbf{u_0}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + src : .time_domain.sources.BaseTDEMSrc + A TDEM source. + v : numpy.ndarray + A vector of appropriate dimension. When `adjoint` is ``False``, `v` is a + (n_param,) numpy.ndarray. When `adjoint` is ``True``, `v` is a (n_edges or n_faces,) + numpy.ndarray. + adjoint : bool + Whether to perform the adjoint operation. + f : .time_domain.fields.BaseTDEMFields, optional + The TDEM fields object. + + Returns + ------- + numpy.ndarray + Derivatives of the initial fields with respect to the model for + a given source. + (n_edges or n_faces,) numpy.ndarray when ``adjoint`` is ``False``. + (n_param,) numpy.ndarray when ``adjoint`` is ``True``. + """ ifieldsDeriv = mkvc( getattr(src, "{}InitialDeriv".format(self._fieldType), None)( self, v, adjoint, f @@ -462,6 +574,33 @@ def getInitialFieldsDeriv(self, src, v, adjoint=False, f=None): # initial condition @property def Adcinv(self): + r"""Inverse of the factored system matrix for the DC resistivity problem. + + The solution to the DC resistivity problem is necessary at the initial time for + galvanic sources whose currents are non-zero at the initial time. + This property is used to compute and store the inverse of the factored linear system + matrix for the DC resistivity problem given by: + + .. math:: + \mathbf{A_{dc}} \, \boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the system matrix, :math:`\boldsymbol{\phi_0}` represents the + discrete solution for the electric potential and :math:`\mathbf{q_{dc}}` is the discrete + right-hand side. Electric fields are computed by applying a discrete gradient operator + to the discrete electric potential solution. + + Returns + ------- + pymatsolver.solvers.Base + Inver of the factored systems matrix for the DC resistivity problem. + + Notes + ----- + See the docstrings for :class:`.resistivity.BaseDCSimulation`, + :class:`.resistivity.Simulation3DCellCentered` and + :class:`.resistivity.Simulation3DNodal` to learn + more about how the DC resistivity problem is solved. + """ if not hasattr(self, "getAdc"): raise NotImplementedError( "Support for galvanic sources has not been implemented for " @@ -475,9 +614,22 @@ def Adcinv(self): return self._Adcinv @property - def clean_on_model_update(self): - items = super().clean_on_model_update - return items + ["_Adcinv"] #: clear DC matrix factors on any model updates + def _delete_on_model_update(self): + """List of model-dependent attributes to clean upon model update. + + Some of the TDEM simulation's attributes are model-dependent. This property specifies + the model-dependent attributes that much be cleared when the model is updated. + + Returns + ------- + list of str + List of the model-dependent attributes to clean upon model update. + """ + items = super()._delete_on_model_update + if self.sigmaMap is not None: + items = items + ["_Adcinv"] #: clear DC matrix factors on any model updates + # if there is a sigmaMap + return items ############################################################################### @@ -490,83 +642,160 @@ def clean_on_model_update(self): class Simulation3DMagneticFluxDensity(BaseTDEMSimulation): - r""" - Starting from the quasi-static E-B formulation of Maxwell's equations - (semi-discretized) + r"""3D TDEM simulation in terms of the magnetic flux density. + + This simulation solves for the magnetic flux density at each time-step. + In this formulation, the electric fields are defined on mesh edges and the + magnetic flux density is defined on mesh faces; i.e. it is an EB formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .time_domain.survey.Survey + The time-domain EM survey. + dt_threshold : float + Threshold used when determining the unique time-step lengths. + + Notes + ----- + Here, we start with the quasi-static approximation for Maxwell's equations by neglecting + electric displacement: .. math:: + &\nabla \times \vec{e} + \frac{\partial \vec{b}}{\partial t} = - \frac{\partial \vec{s}_m}{\partial t} \\ + &\nabla \times \vec{h} - \vec{j} = \vec{s}_e - \mathbf{C} \mathbf{e} + \frac{\partial \mathbf{b}}{\partial t} = - \mathbf{s_m} \\ - \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f} \mathbf{b} - - \mathbf{M_{\sigma}^e} \mathbf{e} = \mathbf{s_e} - - - where :math:`\mathbf{s_e}` is an integrated quantity, we eliminate - :math:`\mathbf{e}` using + where :math:`\vec{s}_e` is an electric source term that defines a source current density, + and :math:`\vec{s}_m` magnetic source term that defines a source magnetic flux density. + We define the constitutive relations for the electrical conductivity :math:`\sigma` + and magnetic permeability :math:`\mu` as: .. math:: + \vec{j} &= \sigma \vec{e} \\ + \vec{h} &= \mu^{-1} \vec{b} - \mathbf{e} = \mathbf{M_{\sigma}^e}^{-1} \mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f} \mathbf{b} - - \mathbf{M_{\sigma}^e}^{-1} \mathbf{s_e} + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: - - to obtain a second order semi-discretized system in :math:`\mathbf{b}` + .. math:: + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{e}) \, dv + + \int_\Omega \vec{u} \cdot \frac{\partial \vec{b}}{\partial t} \, dv + = - \int_\Omega \vec{u} \cdot \frac{\partial \vec{s}_m}{\partial t} \, dv \\ + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{h} \, dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{h} \times \hat{n}) \, da + - \int_\Omega \vec{u} \cdot \vec{j} \, dv = \int_\Omega \vec{u} \cdot \vec{s}_e \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{j} \, dv = \int_\Omega \vec{u} \cdot \sigma \vec{e} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{h} \, dv = \int_\Omega \vec{u} \cdot \mu^{-1} \vec{b} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete electric fields :math:`\mathbf{e}` are defined on mesh edges, + and the discrete magnetic flux densities :math:`\mathbf{b}` are defined on mesh faces. + This implies :math:`\mathbf{j}` must be defined on mesh edges and :math:`\mathbf{h}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: .. math:: + &\mathbf{u_f^T C e} + \mathbf{u_f^T } \, \frac{\partial \mathbf{b}}{\partial t} + = - \mathbf{u_f^T } \, \frac{\partial \mathbf{s_m}}{\partial t} \\ + &\mathbf{u_e^T C^T M_f h} - \mathbf{u_e^T M_e j} = \mathbf{u_e^T s_e} \\ + &\mathbf{u_e^T M_e j} = \mathbf{u_e^T M_{e\sigma} e} \\ + &\mathbf{u_f^T M_f h} = \mathbf{u_f^T M_{f \frac{1}{\mu}} b} - \mathbf{C} \mathbf{M_{\sigma}^e}^{-1} \mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f} \mathbf{b} + - \frac{\partial \mathbf{b}}{\partial t} = - \mathbf{C} \mathbf{M_{\sigma}^e}^{-1} \mathbf{s_e} + \mathbf{s_m} + where + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - and moving everything except the time derivative to the rhs gives + By cancelling like-terms and combining the discrete expressions in terms of the magnetic flux density, we obtain: .. math:: - \frac{\partial \mathbf{b}}{\partial t} = - -\mathbf{C} \mathbf{M_{\sigma}^e}^{-1} \mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f} \mathbf{b} + - \mathbf{C} \mathbf{M_{\sigma}^e}^{-1} \mathbf{s_e} + \mathbf{s_m} + \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}} b} + + \frac{\partial \mathbf{b}}{\partial t} + = \mathbf{C M_{e\sigma}^{-1}} \mathbf{s_e} + - \frac{\partial \mathbf{s_m}}{\partial t} - For the time discretization, we use backward euler. To solve for the - :math:`n+1` th time step, we have + Finally, we discretize in time according to backward Euler. The discrete magnetic flux density + on mesh faces at time :math:`t_k > t_0` is obtained by solving the following at each time-step: .. math:: + \mathbf{A}_k \mathbf{b}_k = \mathbf{q_k} - \mathbf{B}_k \mathbf{b}_{k-1} - \frac{\mathbf{b}^{n+1} - \mathbf{b}^{n}}{\mathbf{dt}} = - -\mathbf{C} \mathbf{M_{\sigma}^e}^{-1} \mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f} \mathbf{b}^{n+1} + - \mathbf{C} \mathbf{M_{\sigma}^e}^{-1} \mathbf{s_e}^{n+1} + - \mathbf{s_m}^{n+1} + where :math:`\Delta t_k = t_k - t_{k-1}` and + .. math:: + &\mathbf{A}_k = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + \frac{1}{\Delta t_k} \mathbf{I} \\ + &\mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{I}\\ + &\mathbf{q}_k = \mathbf{C M_{e\sigma}^{-1}} \mathbf{s}_{\mathbf{e}, k} \; + - \; \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{m}, k} - \mathbf{s}_{\mathbf{m}, k-1} \big ] - re-arranging to put :math:`\mathbf{b}^{n+1}` on the left hand side gives + Although the following system is never explicitly formed, we can represent + the solution at all time-steps as: .. math:: - - (\mathbf{I} + \mathbf{dt} \mathbf{C} \mathbf{M_{\sigma}^e}^{-1} - \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f}) \mathbf{b}^{n+1} = - \mathbf{b}^{n} + \mathbf{dt}(\mathbf{C} \mathbf{M_{\sigma}^e}^{-1} - \mathbf{s_e}^{n+1} + \mathbf{s_m}^{n+1}) + \begin{bmatrix} + \mathbf{A_1} & & & & \\ + \mathbf{B_2} & \mathbf{A_2} & & & \\ + & & \ddots & & \\ + & & & \mathbf{B_n} & \mathbf{A_n} + \end{bmatrix} + \begin{bmatrix} + \mathbf{b_1} \\ \mathbf{b_2} \\ \vdots \\ \mathbf{b_n} + \end{bmatrix} = + \begin{bmatrix} + \mathbf{q_1} \\ \mathbf{q_2} \\ \vdots \\ \mathbf{q_n} + \end{bmatrix} - + \begin{bmatrix} + \mathbf{B_1 b_0} \\ \mathbf{0} \\ \vdots \\ \mathbf{0} + \end{bmatrix} + + where the magnetic flux densities at the initial time :math:`\mathbf{b_0}` + are computed analytically or numerically depending on whether the source + carries non-zero current at the initial time. """ _fieldType = "b" _formulation = "EB" - fieldsPair = Fields3DMagneticFluxDensity #: A simpeg.EM.TDEM.Fields3DMagneticFluxDensity object + fieldsPair = Fields3DMagneticFluxDensity Fields_Derivs = FieldsDerivativesEB def getAdiag(self, tInd): - r""" - System matrix at a given time index + r"""Diagonal system matrix for the given time-step index. + + This method returns the diagonal system matrix for the time-step index provided: .. math:: + \mathbf{A}_k = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + \frac{1}{\Delta t_k} \mathbf{I} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{e \sigma}}` is the conductivity inner-product matrix on edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inverse permeability inner-product matrix on faces - (\mathbf{I} + \mathbf{dt} \mathbf{C} \mathbf{M_{\sigma}^e}^{-1} - \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f}) + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The diagonal system matrix. """ assert tInd >= 0 and tInd < self.nT @@ -583,8 +812,52 @@ def getAdiag(self, tInd): return A def getAdiagDeriv(self, tInd, u, v, adjoint=False): - """ - Derivative of ADiag + r"""Derivative operation for the diagonal system matrix times a vector. + + The diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{A}_k = \mathbf{C M_{e\sigma}^{-1} C^T M_{f\frac{1}{\mu}}} + \frac{1}{\Delta t_k} \mathbf{I} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{e \sigma}}` is the conductivity inner-product matrix on edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inverse permeability inner-product matrix on faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{b_k}` is the discrete solution for time-step *k*, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_k \, b_k})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_k \, b_k})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model; i.e. :math:`\mathbf{b_k}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. """ C = self.mesh.edge_curl @@ -605,8 +878,27 @@ def getAdiagDeriv(self, tInd, u, v, adjoint=False): return ADeriv def getAsubdiag(self, tInd): - """ - Matrix below the diagonal + r"""Sub-diagonal system matrix for the time-step index provided. + + This method returns the sub-diagonal system matrix for the time-step index provided: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{I} + + where :math:`\Delta t_k` is the step length and :math:`\mathbf{I}` is the identity matrix. + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The sub-diagonal system matrix. """ dt = self.time_steps[tInd] @@ -619,11 +911,82 @@ def getAsubdiag(self, tInd): return Asubdiag def getAsubdiagDeriv(self, tInd, u, v, adjoint=False): + r"""Derivative operation for the sub-diagonal system matrix times a vector. + + The sub-diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{I} + + where :math:`\Delta t_k` is the step length and :math:`\mathbf{I}` is the identity matrix. + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{b_{k-1}}` is the discrete solution for the previous time-step, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{B_k \, b_{k-1}})}{\partial \mathbf{m}} \, \mathbf{v} = \mathbf{0} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{B_k \, b_{k-1}})}{\partial \mathbf{m}}^T \, \mathbf{v} = \mathbf{0} + + The derivative operation returns a vector of zeros because the sub-diagonal system matrix + does not depend on the model!!! + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps-1]``. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model for the previous time-step; + i.e. :math:`\mathbf{b_{k-1}}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ return Zero() * v def getRHS(self, tInd): - """ - Assemble the RHS + r"""Right-hand sides for the given time index. + + This method returns the right-hand sides for the time index provided. + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q}_k = \mathbf{C M_{e\sigma}^{-1}} \mathbf{s}_{\mathbf{e}, k} \; + - \; \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{m}, k} - \mathbf{s}_{\mathbf{m}, k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + + Returns + ------- + (n_faces, n_sources) numpy.ndarray + The right-hand sides. """ C = self.mesh.edge_curl MeSigmaI = self.MeSigmaI @@ -637,8 +1000,51 @@ def getRHS(self, tInd): return rhs def getRHSDeriv(self, tInd, src, v, adjoint=False): - """ - Derivative of the RHS + r"""Derivative of the right-hand side times a vector for a given source and time index. + + The right-hand side for a given source at time index *k* is constructed according to: + + .. math:: + \mathbf{q}_k = \mathbf{C M_{e\sigma}^{-1}} \mathbf{s}_{\mathbf{e}, k} \; + - \; \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{m}, k} - \mathbf{s}_{\mathbf{m}, k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticFluxDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + src : .time_domain.sources.BaseTDEMSrc + The TDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. """ C = self.mesh.edge_curl @@ -673,38 +1079,124 @@ def getRHSDeriv(self, tInd, src, v, adjoint=False): # ------------------------------- Simulation3DElectricField ------------------------------- # class Simulation3DElectricField(BaseTDEMSimulation): - r""" - Solve the EB-formulation of Maxwell's equations for the electric field, e. - - Starting with + r"""3D TDEM simulation in terms of the electric field. + + This simulation solves for the electric field at each time-step. + In this formulation, the electric fields are defined on mesh edges and the + magnetic flux density is defined on mesh faces; i.e. it is an EB formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .time_domain.survey.Survey + The time-domain EM survey. + dt_threshold : float + Threshold used when determining the unique time-step lengths. + + Notes + ----- + Here, we start with the quasi-static approximation for Maxwell's equations by neglecting + electric displacement: .. math:: + &\nabla \times \vec{e} + \frac{\partial \vec{b}}{\partial t} = - \frac{\partial \vec{s}_m}{\partial t} \\ + &\nabla \times \vec{h} - \vec{j} = \vec{s}_e + + where :math:`\vec{s}_e` is an electric source term that defines a source current density, + and :math:`\vec{s}_m` magnetic source term that defines a source magnetic flux density. + We define the constitutive relations for the electrical conductivity :math:`\sigma` + and magnetic permeability :math:`\mu` as: - \nabla \times \mathbf{e} + \frac{\partial \mathbf{b}}{\partial t} = \mathbf{s_m} \ - \nabla \times \mu^{-1} \mathbf{b} - \sigma \mathbf{e} = \mathbf{s_e} + .. math:: + \vec{j} &= \sigma \vec{e} \\ + \vec{h} &= \mu^{-1} \vec{b} + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: - we eliminate :math:`\frac{\partial b}{\partial t}` using + .. math:: + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{e}) \, dv + + \int_\Omega \vec{u} \cdot \frac{\partial \vec{b}}{\partial t} \, dv + = - \int_\Omega \vec{u} \cdot \frac{\partial \vec{s}_m}{\partial t} \, dv \\ + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{h} \, dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{h} \times \hat{n}) \, da + - \int_\Omega \vec{u} \cdot \vec{j} \, dv = \int_\Omega \vec{u} \cdot \vec{s}_e \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{j} \, dv = \int_\Omega \vec{u} \cdot \sigma \vec{e} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{h} \, dv = \int_\Omega \vec{u} \cdot \mu^{-1} \vec{b} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete electric fields :math:`\mathbf{e}` are defined on mesh edges, + and the discrete magnetic flux densities :math:`\mathbf{b}` are defined on mesh faces. + This implies :math:`\mathbf{j}` must be defined on mesh edges and :math:`\mathbf{h}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: .. math:: + &\mathbf{u_f^T C e} + \mathbf{u_f^T } \, \frac{\partial \mathbf{b}}{\partial t} + = - \mathbf{u_f^T } \, \frac{\partial \mathbf{s_m}}{\partial t} \\ + &\mathbf{u_e^T C^T M_f h} - \mathbf{u_e^T M_e j} = \mathbf{u_e^T s_e} \\ + &\mathbf{u_e^T M_e j} = \mathbf{u_e^T M_{e\sigma} e} \\ + &\mathbf{u_f^T M_f h} = \mathbf{u_f^T M_{f \frac{1}{\mu}} b} - \frac{\partial \mathbf{b}}{\partial t} = - \nabla \times \mathbf{e} + \mathbf{s_m} + where + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{e\sigma}}` is the inner-product matrix for conductivities projected to edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrix for inverse permeabilities projected to faces - taking the time-derivative of Ampere's law, we see + By cancelling like-terms and combining the discrete expressions in terms of the electric field, we obtain: .. math:: + \mathbf{C^T M_{f\frac{1}{\mu}} C e} + \mathbf{M_{e\sigma}}\frac{\partial \mathbf{e}}{\partial t} + = \mathbf{C^T M_{f\frac{1}{\mu}}} \frac{\partial \mathbf{s_m}}{\partial t} + - \frac{\partial \mathbf{s_e}}{\partial t} - \frac{\partial}{\partial t}\left( \nabla \times \mu^{-1} \mathbf{b} - \sigma \mathbf{e} \right) = \frac{\partial \mathbf{s_e}}{\partial t} \ - \nabla \times \mu^{-1} \frac{\partial \mathbf{b}}{\partial t} - \sigma \frac{\partial\mathbf{e}}{\partial t} = \frac{\partial \mathbf{s_e}}{\partial t} + Finally, we discretize in time according to backward Euler. The discrete electric fields + on mesh edges at time :math:`t_k > t_0` is obtained by solving the following at each time-step: + .. math:: + \mathbf{A}_k \mathbf{b}_k = \mathbf{q}_k - \mathbf{B}_k \mathbf{b}_{k-1} - which gives us + where :math:`\Delta t_k = t_k - t_{k-1}` and .. math:: + &\mathbf{A}_k = \mathbf{C^T M_{f\frac{1}{\mu}} C} + \frac{1}{\Delta t_k} \mathbf{M_{e\sigma}} \\ + &\mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{M_{e\sigma}} \\ + &\mathbf{q}_k = \frac{1}{\Delta t_k} \mathbf{C^T M_{f\frac{1}{\mu}}} + \big [ \mathbf{s}_{\mathbf{m}, k} - \mathbf{s}_{\mathbf{m}, k-1} \big ] + -\frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{e}, k} - \mathbf{s}_{\mathbf{e}, k-1} \big ] - \nabla \times \mu^{-1} \nabla \times \mathbf{e} + \sigma \frac{\partial\mathbf{e}}{\partial t} = \nabla \times \mu^{-1} \mathbf{s_m} + \frac{\partial \mathbf{s_e}}{\partial t} + Although the following system is never explicitly formed, we can represent + the solution at all time-steps as: + .. math:: + \begin{bmatrix} + \mathbf{A_1} & & & & \\ + \mathbf{B_2} & \mathbf{A_2} & & & \\ + & & \ddots & & \\ + & & & \mathbf{B_n} & \mathbf{A_n} + \end{bmatrix} + \begin{bmatrix} + \mathbf{e_1} \\ \mathbf{e_2} \\ \vdots \\ \mathbf{e_n} + \end{bmatrix} = + \begin{bmatrix} + \mathbf{q_1} \\ \mathbf{q_2} \\ \vdots \\ \mathbf{q_n} + \end{bmatrix} - + \begin{bmatrix} + \mathbf{B_1 e_0} \\ \mathbf{0} \\ \vdots \\ \mathbf{0} + \end{bmatrix} + + where the electric fields at the initial time :math:`\mathbf{e_0}` + are computed analytically or numerically depending on whether the + source is galvanic and carries non-zero current at the initial time. """ @@ -715,19 +1207,15 @@ class Simulation3DElectricField(BaseTDEMSimulation): # @profile def Jtvec(self, m, v, f=None): - """ - Jvec computes the adjoint of the sensitivity times a vector - """ - + # Doctring inherited from parent class. if f is None: f = self.fields(m) self.model = m ftype = self._fieldType + "Solution" # the thing we solved for - # Ensure v is a data object. - if not isinstance(v, Data): - v = Data(self.survey, v) + # Get dict of flat array slices for each source-receiver pair in the survey + survey_slices = self.survey.get_all_slices() df_duT_v = self.Fields_Derivs(self) @@ -755,8 +1243,9 @@ def Jtvec(self, m, v, f=None): ) for rx in src.receiver_list: + src_rx_slice = survey_slices[src, rx] PT_v[src, "{}Deriv".format(rx.projField), :] = rx.evalDeriv( - src, self.mesh, self.time_mesh, f, mkvc(v[src, rx]), adjoint=True + src, self.mesh, self.time_mesh, f, v[src_rx_slice], adjoint=True ) # this is += @@ -810,11 +1299,18 @@ def Jtvec(self, m, v, f=None): ATinv_df_duT_v[isrc, :] = ( AdiagTinv * df_duT_v[src, "{}Deriv".format(self._fieldType), tInd + 1] - ) + ).squeeze() elif tInd > -1: - ATinv_df_duT_v[isrc, :] = AdiagTinv * ( - mkvc(df_duT_v[src, "{}Deriv".format(self._fieldType), tInd + 1]) - - Asubdiag.T * mkvc(ATinv_df_duT_v[isrc, :]) + ATinv_df_duT_v[isrc, :] = ( + AdiagTinv + * ( + mkvc( + df_duT_v[ + src, "{}Deriv".format(self._fieldType), tInd + 1 + ] + ) + - Asubdiag.T * mkvc(ATinv_df_duT_v[isrc, :]) + ).squeeze() ) dAsubdiagT_dm_v = self.getAsubdiagDeriv( @@ -851,7 +1347,7 @@ def Jtvec(self, m, v, f=None): ) - Asubdiag.T * mkvc(ATinv_df_duT_v[isrc, :]) ) - ) + ).squeeze() ) dRHST_dm_v = self.getRHSDeriv( @@ -873,8 +1369,32 @@ def Jtvec(self, m, v, f=None): return mkvc(JTv).astype(float) def getAdiag(self, tInd): - """ - Diagonal of the system matrix at a given time index + r"""Diagonal system matrix for the time-step index provided. + + This method returns the diagonal system matrix for the time-step index provided: + + .. math:: + \mathbf{A}_k = \mathbf{C^T M_{f\frac{1}{\mu}} C} + \frac{1}{\Delta t_k} \mathbf{M_{e\sigma}} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{e \sigma}}` is the conductivity inner-product matrix on edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inverse permeability inner-product matrix on faces + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The diagonal system matrix. """ assert tInd >= 0 and tInd < self.nT @@ -886,8 +1406,52 @@ def getAdiag(self, tInd): return C.T.tocsr() * (MfMui * C) + 1.0 / dt * MeSigma def getAdiagDeriv(self, tInd, u, v, adjoint=False): - """ - Deriv of ADiag with respect to electrical conductivity + r"""Derivative operation for the diagonal system matrix times a vector. + + The diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{A}_k = \mathbf{C^T M_{f\frac{1}{\mu}} C} + \frac{1}{\Delta t_k} \mathbf{M_{e\sigma}} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{e \sigma}}` is the conductivity inner-product matrix on edges + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inverse permeability inner-product matrix on faces + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{e_k}` is the discrete solution for time-step *k*, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_k \, e_k})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_k \, e_k})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model for the specified time-step; + i.e. :math:`\mathbf{e_k}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ assert tInd >= 0 and tInd < self.nT @@ -900,8 +1464,28 @@ def getAdiagDeriv(self, tInd, u, v, adjoint=False): return 1.0 / dt * self.MeSigmaDeriv(u, v, adjoint) def getAsubdiag(self, tInd): - """ - Matrix below the diagonal + r"""Sub-diagonal system matrix for the time-step index provided. + + This method returns the sub-diagonal system matrix for the time-step index provided: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{M_{e\sigma}} + + where :math:`\Delta t_k` is the step-length and :math:`\mathbf{M_{e \sigma}}` is the + conductivity inner-product matrix on edges. + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The sub-diagonal system matrix. """ assert tInd >= 0 and tInd < self.nT @@ -910,9 +1494,48 @@ def getAsubdiag(self, tInd): return -1.0 / dt * self.MeSigma def getAsubdiagDeriv(self, tInd, u, v, adjoint=False): - """ - Derivative of the matrix below the diagonal with respect to electrical - conductivity + r"""Derivative operation for the sub-diagonal system matrix times a vector. + + The sub-diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{M_{e\sigma}} + + where :math:`\Delta t_k` is the step-length and :math:`\mathbf{M_{e \sigma}}` is the + conductivity inner-product matrix on edges. + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{e_{k-1}}` is the discrete solution for the previous time-step, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{B_k \, e_{k-1}})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{B_k \, e_{k-1}})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model for the previous time-step; + i.e. :math:`\mathbf{e_{k-1}}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. """ dt = self.time_steps[tInd] @@ -922,8 +1545,36 @@ def getAsubdiagDeriv(self, tInd, u, v, adjoint=False): return -1.0 / dt * self.MeSigmaDeriv(u, v, adjoint) def getRHS(self, tInd): - """ - right hand side + r"""Right-hand sides for the given time index. + + This method returns the right-hand sides for the time index provided. + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q}_k = + -\frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{e}, k} - \mathbf{s}_{\mathbf{e}, k-1} \big ] + - \frac{1}{\Delta t_k} \mathbf{C^T M_{f\frac{1}{\mu}}} + \big [ \mathbf{s}_{\mathbf{m}, k} - \mathbf{s}_{\mathbf{m}, k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrices for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + + Returns + ------- + (n_edges, n_sources) numpy.ndarray + The right-hand sides. """ # Omit this: Note input was tInd+1 # if tInd == len(self.time_steps): @@ -936,31 +1587,141 @@ def getRHS(self, tInd): return -1.0 / dt * (s_e - s_en1) + self.mesh.edge_curl.T * self.MfMui * s_m def getRHSDeriv(self, tInd, src, v, adjoint=False): - # right now, we are assuming that s_e, s_m do not depend on the model. - return Zero() + r"""Derivative of the right-hand side times a vector for a given source and time index. - def getAdc(self): - MeSigma = self.MeSigma - Grad = self.mesh.nodal_gradient - Adc = Grad.T.tocsr() * MeSigma * Grad - # Handling Null space of A - Adc[0, 0] = Adc[0, 0] + 1.0 - return Adc + The right-hand side for a given source at time index *k* is constructed according to: - def getAdcDeriv(self, u, v, adjoint=False): + .. math:: + \mathbf{q}_k = + -\frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{e}, k} - \mathbf{s}_{\mathbf{e}, k-1} \big ] + - \frac{1}{\Delta t_k} \mathbf{C^T M_{f\frac{1}{\mu}} } + \big [ \mathbf{s}_{\mathbf{m}, k} - \mathbf{s}_{\mathbf{m}, k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{f\frac{1}{\mu}}}` is the inner-product matrices for inverse permeabilities projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DElectricField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + src : .time_domain.sources.BaseTDEMSrc + The TDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ + # right now, we are assuming that s_e, s_m do not depend on the model. + return Zero() + + def getAdc(self): + r"""The system matrix for the DC resistivity problem. + + The solution to the DC resistivity problem is necessary at the initial time for + galvanic sources whose currents are non-zero at the initial time. + The discrete solution to the 3D DC resistivity problem is expressed as: + + .. math:: + \mathbf{A_{dc}}\,\boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the DC resistivity system matrix, :math:`\boldsymbol{\phi_0}` + is the discrete solution for the electric potentials at the initial time, and :math:`\mathbf{q_{dc}}` + is the galvanic source term. This method returns the system matrix + for the nodal formulation, i.e.: + + .. math:: + \mathbf{A_{dc}} = \mathbf{G^T \, M_{e\sigma} \, G} + + where :math:`\mathbf{G}` is the nodal gradient operator with imposed boundary conditions, + and :math:`\mathbf{M_{e\sigma}}` is the inner product matrix for conductivities projected to edges. + + The electric fields at the initial time :math:`\mathbf{e_0}` are obtained by applying the + nodal gradient operator. I.e.: + + .. math:: + \mathbf{e_0} = \mathbf{G} \, \boldsymbol{\phi_0} + + See the *Notes* section of the doc strings for :class:`.resistivity.Simulation3DNodal` + for a full description of the nodal DC resistivity formulation. + + Returns + ------- + (n_nodes, n_nodes) sp.sparse.csr_matrix + The system matrix for the DC resistivity problem. + """ + MeSigma = self.MeSigma + Grad = self.mesh.nodal_gradient + Adc = Grad.T.tocsr() * MeSigma * Grad + # Handling Null space of A + Adc[0, 0] = Adc[0, 0] + 1.0 + return Adc + + def getAdcDeriv(self, u, v, adjoint=False): + r"""Derivative operation for the DC resistivity system matrix times a vector. + + The discrete solution to the 3D DC resistivity problem is expressed as: + + .. math:: + \mathbf{A_{dc}}\boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the DC resistivity system matrix, :math:`\boldsymbol{\phi_0}` + is the discrete solution for the electric potentials at the initial time, and :math:`\mathbf{q_{dc}}` + is the galvanic source term. For a vector :math:`\mathbf{v}`, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_{dc}}\boldsymbol{\phi_0})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_{dc}}\boldsymbol{\phi_0})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + u : (n_nodes,) numpy.ndarray + The solution for the fields for the current model; i.e. electric potentials at nodes. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_nodes,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the DC resistivity system matrix times a vector. (n_nodes,) for the standard operation. + (n_param,) for the adjoint operation. + """ Grad = self.mesh.nodal_gradient if not adjoint: return Grad.T * self.MeSigmaDeriv(-u, v, adjoint) else: return self.MeSigmaDeriv(-u, Grad * v, adjoint) - # def clean(self): - # """ - # Clean factors - # """ - # if self.Adcinv is not None: - # self.Adcinv.clean() - ############################################################################### # # @@ -972,32 +1733,122 @@ def getAdcDeriv(self, u, v, adjoint=False): class Simulation3DMagneticField(BaseTDEMSimulation): - r""" - Solve the H-J formulation of Maxwell's equations for the magnetic field h. + r"""3D TDEM simulation in terms of the magnetic field. + + This simulation solves for the magnetic field at each time-step. + In this formulation, the magnetic fields are defined on mesh edges and the + current density is defined on mesh faces; i.e. it is an HJ formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .time_domain.survey.Survey + The time-domain EM survey. + dt_threshold : float + Threshold used when determining the unique time-step lengths. + + Notes + ----- + Here, we start with the quasi-static approximation for Maxwell's equations by neglecting + electric displacement: + + .. math:: + &\nabla \times \vec{e} + \frac{\partial \vec{b}}{\partial t} = - \frac{\partial \vec{s}_m}{\partial t} \\ + &\nabla \times \vec{h} - \vec{j} = \vec{s}_e + + where :math:`\vec{s}_e` is an electric source term that defines a source current density, + and :math:`\vec{s}_m` magnetic source term that defines a source magnetic flux density. + We define the constitutive relations for the electrical resistivity :math:`\rho` + and magnetic permeability :math:`\mu` as: + + .. math:: + \vec{e} &= \rho \vec{e} \\ + \vec{b} &= \mu \vec{h} + + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: - We start with Maxwell's equations in terms of the magnetic field and - current density + .. math:: + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{e} \; dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{e} \times \hat{n} ) \, da + + \int_\Omega \vec{u} \cdot \frac{\partial \vec{b}}{\partial t} \, dv + = - \int_\Omega \vec{u} \cdot \frac{\partial \vec{s}_m}{\partial t} \, dv \\ + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{h} ) \, dv - \int_\Omega \vec{u} \cdot \vec{j} \, dv + = \int_\Omega \vec{u} \cdot \vec{s}_e \, dv\\ + & \int_\Omega \vec{u} \cdot \vec{e} \, dv = \int_\Omega \vec{u} \cdot \rho \vec{j} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{b} \, dv = \int_\Omega \vec{u} \cdot \mu \vec{h} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete magnetic fields :math:`\mathbf{h}` are defined on mesh edges, + and the discrete current densities :math:`\mathbf{j}` are defined on mesh faces. + This implies :math:`\mathbf{b}` must be defined on mesh edges and :math:`\mathbf{e}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: .. math:: + &\mathbf{u_e^T C^T M_f \, e } + \mathbf{u_e^T M_e} \frac{\partial \mathbf{b}}{\partial t} + = - \mathbf{u_e^T} \, \frac{\partial \mathbf{s_m}}{\partial t} \\ + &\mathbf{u_f^T C \, h} - \mathbf{u_f^T j} = \mathbf{u_f^T s_e} \\ + &\mathbf{u_f^T M_f e} = \mathbf{u_f^T M_{f\rho} \, j} \\ + &\mathbf{u_e^T M_e b} = \mathbf{u_e^T M_{e \mu} h} - \nabla \times \rho \mathbf{j} + \mu \frac{\partial h}{\partial t} = \mathbf{s_m} \ - \nabla \times \mathbf{h} - \mathbf{j} = \mathbf{s_e} + where + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces - and eliminate :math:`\mathbf{j}` using + Cancelling like-terms and combining the discrete expressions in terms of the magnetic field, we obtain: .. math:: + \mathbf{C^T M_{f\rho} C \, h} + \mathbf{M_{e\mu}} \frac{\partial \mathbf{h}}{\partial t} + = \mathbf{C^T M_{f\rho} s_e} - \frac{\partial \mathbf{s_m}}{\partial t} - \mathbf{j} = \nabla \times \mathbf{h} - \mathbf{s_e} + Finally, we discretize in time according to backward Euler. The discrete magnetic field + on mesh edges at time :math:`t_k > t_0` is obtained by solving the following at each time-step: + .. math:: + \mathbf{A}_k \mathbf{h}_k = \mathbf{q}_k - \mathbf{B}_k \mathbf{h}_{k-1} - giving + where :math:`\Delta t_k = t_k - t_{k-1}` and .. math:: + &\mathbf{A}_k = \mathbf{C^T M_{f\rho} C} + \frac{1}{\Delta t_k} \mathbf{M_{e\mu}} \\ + &\mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{M_{e\mu}}\\ + &\mathbf{q}_k = \mathbf{C^T M_{f\rho} s}_{\mathbf{e},k} \; + - \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{m},k} + \mathbf{s}_{\mathbf{m},k-1} \big ] - \nabla \times \rho \nabla \times \mathbf{h} + \mu \frac{\partial h}{\partial t} - = \nabla \times \rho \mathbf{s_e} + \mathbf{s_m} + Although the following system is never explicitly formed, we can represent + the solution at all time-steps as: + .. math:: + \begin{bmatrix} + \mathbf{A_1} & & & & \\ + \mathbf{B_2} & \mathbf{A_2} & & & \\ + & & \ddots & & \\ + & & & \mathbf{B_n} & \mathbf{A_n} + \end{bmatrix} + \begin{bmatrix} + \mathbf{h_1} \\ \mathbf{h_2} \\ \vdots \\ \mathbf{h_n} + \end{bmatrix} = + \begin{bmatrix} + \mathbf{q_1} \\ \mathbf{q_2} \\ \vdots \\ \mathbf{q_n} + \end{bmatrix} - + \begin{bmatrix} + \mathbf{B_1 h_0} \\ \mathbf{0} \\ \vdots \\ \mathbf{0} + \end{bmatrix} + + where the magnetic fields at the initial time :math:`\mathbf{h_0}` + are computed analytically or numerically depending on whether the source + carries non-zero current at the initial time. """ @@ -1007,9 +1858,32 @@ class Simulation3DMagneticField(BaseTDEMSimulation): Fields_Derivs = FieldsDerivativesHJ def getAdiag(self, tInd): - """ - System matrix at a given time index + r"""Diagonal system matrix for the given time-step index. + + This method returns the diagonal system matrix for the time-step index provided: + .. math:: + \mathbf{A}_k = \mathbf{C^T M_{f\rho} C} + \frac{1}{\Delta t_k} \mathbf{M_{e\mu}} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{f \rho}}` is the resistivity inner-product matrix on faces + * :math:`\mathbf{M_{e\mu}}` is the permeability inner-product matrix on edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The diagonal system matrix. """ assert tInd >= 0 and tInd < self.nT @@ -1021,6 +1895,52 @@ def getAdiag(self, tInd): return C.T * (MfRho * C) + 1.0 / dt * MeMu def getAdiagDeriv(self, tInd, u, v, adjoint=False): + r"""Derivative operation for the diagonal system matrix times a vector. + + The diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{A}_k = \mathbf{C^T M_{f\rho} C} + \frac{1}{\Delta t_k} \mathbf{M_{e\mu}} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{f \rho}}` is the resistivity inner-product matrix on faces + * :math:`\mathbf{M_{e\mu}}` is the permeability inner-product matrix on edges + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{h_k}` is the discrete solution for time-step *k*, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_k \, h_k})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_k \, h_k})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model; i.e. :math:`\mathbf{h_k}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ assert tInd >= 0 and tInd < self.nT C = self.mesh.edge_curl @@ -1031,6 +1951,29 @@ def getAdiagDeriv(self, tInd, u, v, adjoint=False): return C.T * self.MfRhoDeriv(C * u, v, adjoint) def getAsubdiag(self, tInd): + r"""Sub-diagonal system matrix for the time-step index provided. + + This method returns the sub-diagonal system matrix for the time-step index provided: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{M_{e\mu}} + + where :math:`\Delta t_k` is the step-length and :math:`\mathbf{M_{e \mu}}` is the + permeability inner-product matrix on edges. + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The sub-diagonal system matrix. + """ assert tInd >= 0 and tInd < self.nT dt = self.time_steps[tInd] @@ -1038,9 +1981,81 @@ def getAsubdiag(self, tInd): return -1.0 / dt * self.MeMu def getAsubdiagDeriv(self, tInd, u, v, adjoint=False): + r"""Derivative operation for the sub-diagonal system matrix times a vector. + + The sub-diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{M_{e\mu}} + + where :math:`\Delta t_k` is the step-length and :math:`\mathbf{M_{e \mu}}` is the + permeability inner-product matrix on edges. + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{h_{k-1}}` is the discrete solution for the previous time-step, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{B_k \, h_{k-1}})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{B_k \, h_{k-1}})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_edges,) numpy.ndarray + The solution for the fields for the current model for the previous time-step; + i.e. :math:`\mathbf{h_{k-1}}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ return Zero() def getRHS(self, tInd): + r"""Right-hand sides for the given time index. + + This method returns the right-hand sides for the time index provided. + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q}_k = \mathbf{C^T M_{f\rho} s}_{\mathbf{e},k} \; + - \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{m},k} + \mathbf{s}_{\mathbf{m},k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resisitivites projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + + Returns + ------- + (n_edges, n_sources) numpy.ndarray + The right-hand sides. + """ C = self.mesh.edge_curl MfRho = self.MfRho s_m, s_e = self.getSourceTerm(tInd) @@ -1048,6 +2063,52 @@ def getRHS(self, tInd): return C.T * (MfRho * s_e) + s_m def getRHSDeriv(self, tInd, src, v, adjoint=False): + r"""Derivative of the right-hand side times a vector for a given source and time index. + + The right-hand side for a given source at time index *k* is constructed according to: + + .. math:: + \mathbf{q}_k = \mathbf{C^T M_{f\rho} s}_{\mathbf{e},k} \; + - \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{m},k} + \mathbf{s}_{\mathbf{m},k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resisitivites projected to faces + + See the *Notes* section of the doc strings for :class:`Simulation3DMagneticField` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + src : .time_domain.sources.BaseTDEMSrc + The TDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_edges,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_edges,) for the standard operation. + (n_param,) for the adjoint operation. + """ C = self.mesh.edge_curl s_m, s_e = src.eval(self, self.times[tInd]) @@ -1056,13 +2117,79 @@ def getRHSDeriv(self, tInd, src, v, adjoint=False): # assumes no source derivs return C.T * self.MfRhoDeriv(s_e, v, adjoint) + # I DON'T THINK THIS IS CURRENTLY USED BY THE H-FORMULATION. def getAdc(self): + r"""The system matrix for the DC resistivity problem. + + The solution to the DC resistivity problem is necessary at the initial time for + galvanic sources whose currents are non-zero at the initial time. + The discrete solution to the 3D DC resistivity problem is expressed as: + + .. math:: + \mathbf{A_{dc}}\,\boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the DC resistivity system matrix, :math:`\boldsymbol{\phi_0}` + is the discrete solution for the electric potentials at the initial time, and :math:`\mathbf{q_{dc}}` + is the galvanic source term. This method returns the system matrix + for the cell-centered formulation, i.e.: + + .. math:: + \mathbf{D \, M_{f\rho}^{-1} \, G} + + where :math:`\mathbf{D}` is the face divergence operator, :math:`\mathbf{G}` is the cell gradient + operator with imposed boundary conditions, and :math:`\mathbf{M_{f\rho}}` is the inner product + matrix for resistivities projected to faces. + + See the *Notes* section of the doc strings for + :class:`.resistivity.Simulation3DCellCentered` + for a full description of the cell centered DC resistivity formulation. + + Returns + ------- + (n_cells, n_cells) sp.sparse.csr_matrix + The system matrix for the DC resistivity problem. + """ D = sdiag(self.mesh.cell_volumes) * self.mesh.face_divergence G = D.T MfRhoI = self.MfRhoI return D * MfRhoI * G def getAdcDeriv(self, u, v, adjoint=False): + r"""Derivative operation for the DC resistivity system matrix times a vector. + + The discrete solution to the 3D DC resistivity problem is expressed as: + + .. math:: + \mathbf{A_{dc}}\boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the DC resistivity system matrix, :math:`\boldsymbol{\phi_0}` + is the discrete solution for the electric potentials at the initial time, and :math:`\mathbf{q_{dc}}` + is the galvanic source term. For a vector :math:`\mathbf{v}`, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_{dc}}\boldsymbol{\phi_0})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_{dc}}\boldsymbol{\phi_0})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + u : (n_cells,) numpy.ndarray + The solution for the fields for the current model; i.e. electric potentials at cell centers. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_cells,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the DC resistivity system matrix times a vector. (n_cells,) for the standard operation. + (n_param,) for the adjoint operation. + """ D = sdiag(self.mesh.cell_volumes) * self.mesh.face_divergence G = D.T @@ -1077,12 +2204,124 @@ def getAdcDeriv(self, u, v, adjoint=False): class Simulation3DCurrentDensity(BaseTDEMSimulation): - r""" - Solve the H-J formulation for current density + r"""3D TDEM simulation in terms of the current density. + + This simulation solves for the current density at each time-step. + In this formulation, the magnetic fields are defined on mesh edges and the + current densities are defined on mesh faces; i.e. it is an HJ formulation. + See the *Notes* section for a comprehensive description of the formulation. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh. + survey : .time_domain.survey.Survey + The time-domain EM survey. + dt_threshold : float + Threshold used when determining the unique time-step lengths. + + Notes + ----- + Here, we start with the quasi-static approximation for Maxwell's equations by neglecting + electric displacement: - In this case, we eliminate :math:`\partial \mathbf{h} / \partial t` and - solve for :math:`\mathbf{j}` + .. math:: + &\nabla \times \vec{e} + \frac{\partial \vec{b}}{\partial t} = - \frac{\partial \vec{s}_m}{\partial t} \\ + &\nabla \times \vec{h} - \vec{j} = \vec{s}_e + + where :math:`\vec{s}_e` is an electric source term that defines a source current density, + and :math:`\vec{s}_m` magnetic source term that defines a source magnetic flux density. + We define the constitutive relations for the electrical resistivity :math:`\rho` + and magnetic permeability :math:`\mu` as: + + .. math:: + \vec{e} &= \rho \vec{e} \\ + \vec{b} &= \mu \vec{h} + + We then take the inner products of all previous expressions with a vector test function :math:`\vec{u}`. + Through vector calculus identities and the divergence theorem, we obtain: + + .. math:: + & \int_\Omega (\nabla \times \vec{u}) \cdot \vec{e} \; dv + - \oint_{\partial \Omega} \vec{u} \cdot (\vec{e} \times \hat{n} ) \, da + + \int_\Omega \vec{u} \cdot \frac{\partial \vec{b}}{\partial t} \, dv + = - \int_\Omega \vec{u} \cdot \frac{\partial \vec{s}_m}{\partial t} \, dv \\ + & \int_\Omega \vec{u} \cdot (\nabla \times \vec{h} ) \, dv - \int_\Omega \vec{u} \cdot \vec{j} \, dv + = \int_\Omega \vec{u} \cdot \vec{s}_e \, dv\\ + & \int_\Omega \vec{u} \cdot \vec{e} \, dv = \int_\Omega \vec{u} \cdot \rho \vec{j} \, dv \\ + & \int_\Omega \vec{u} \cdot \vec{b} \, dv = \int_\Omega \vec{u} \cdot \mu \vec{h} \, dv + + Assuming natural boundary conditions, the surface integral is zero. + + The above expressions are discretized in space according to the finite volume method. + The discrete magnetic fields :math:`\mathbf{h}` are defined on mesh edges, + and the discrete current densities :math:`\mathbf{j}` are defined on mesh faces. + This implies :math:`\mathbf{b}` must be defined on mesh edges and :math:`\mathbf{e}` must + be defined on mesh faces. Where :math:`\mathbf{u_e}` and :math:`\mathbf{u_f}` represent + test functions discretized to edges and faces, respectively, we obtain the following + set of discrete inner-products: + + .. math:: + &\mathbf{u_e^T C^T M_f \, e } + \mathbf{u_e^T M_e} \frac{\partial \mathbf{b}}{\partial t} + = - \mathbf{u_e^T} \, \frac{\partial \mathbf{s_m}}{\partial t} \\ + &\mathbf{u_f^T C \, h} - \mathbf{u_f^T j} = \mathbf{u_f^T s_e} \\ + &\mathbf{u_f^T M_f e} = \mathbf{u_f^T M_{f\rho} \, j} \\ + &\mathbf{u_e^T M_e b} = \mathbf{u_e^T M_{e \mu} h} + + where + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_e}` is the edge inner-product matrix + * :math:`\mathbf{M_f}` is the face inner-product matrix + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + * :math:`\mathbf{M_{f\rho}}` is the inner-product matrix for resistivities projected to faces + + Cancelling like-terms and combining the discrete expressions in terms of the current density, we obtain: + + .. math:: + \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho} \, j} + + \frac{\partial \mathbf{j}}{\partial t} = + - \frac{\partial \mathbf{s_e}}{\partial t} + - \mathbf{C M_{e\mu}^{-1}} \frac{\partial \mathbf{s_m}}{\partial t} + + Finally, we discretize in time according to backward Euler. The discrete current density + on mesh edges at time :math:`t_k > t_0` is obtained by solving the following at each time-step: + + .. math:: + \mathbf{A}_k \mathbf{j}_k = \mathbf{q}_k - \mathbf{B}_k \mathbf{j}_{k-1} + + where :math:`\Delta t_k = t_k - t_{k-1}` and + + .. math:: + &\mathbf{A}_k = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + \frac{1}{\Delta t_k} \mathbf{I} \\ + &\mathbf{B}_k = - \frac{1}{\Delta t_k} \mathbf{I}\\ + &\mathbf{q}_k = - \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{e},k} + \mathbf{s}_{\mathbf{e},k-1} \big ] \; + - \; \frac{1}{\Delta t_k} \mathbf{C M_{e\mu}^{-1}} \big [ \mathbf{s}_{\mathbf{m},k} + \mathbf{s}_{\mathbf{m},k-1} \big ] + + Although the following system is never explicitly formed, we can represent + the solution at all time-steps as: + + .. math:: + \begin{bmatrix} + \mathbf{A_1} & & & & \\ + \mathbf{B_2} & \mathbf{A_2} & & & \\ + & & \ddots & & \\ + & & & \mathbf{B_n} & \mathbf{A_n} + \end{bmatrix} + \begin{bmatrix} + \mathbf{j_1} \\ \mathbf{j_2} \\ \vdots \\ \mathbf{j_n} + \end{bmatrix} = + \begin{bmatrix} + \mathbf{q_1} \\ \mathbf{q_2} \\ \vdots \\ \mathbf{q_n} + \end{bmatrix} - + \begin{bmatrix} + \mathbf{B_1 j_0} \\ \mathbf{0} \\ \vdots \\ \mathbf{0} + \end{bmatrix} + + where the current densities at the initial time :math:`\mathbf{j_0}` + are computed analytically or numerically depending on whether the source + carries non-zero current at the initial time. """ _fieldType = "j" @@ -1091,9 +2330,33 @@ class Simulation3DCurrentDensity(BaseTDEMSimulation): Fields_Derivs = FieldsDerivativesHJ def getAdiag(self, tInd): - """ - System matrix at a given time index + r"""Diagonal system matrix for the given time-step index. + + This method returns the diagonal system matrix for the time-step index provided: + + .. math:: + \mathbf{A}_k = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + \frac{1}{\Delta t_k} \mathbf{I} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{e \mu}}` is the permeability inner-product matrix on faces + * :math:`\mathbf{M_{f \rho}}` is the permeability inner-product matrix on edges + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The diagonal system matrix. """ assert tInd >= 0 and tInd < self.nT @@ -1111,6 +2374,53 @@ def getAdiag(self, tInd): return A def getAdiagDeriv(self, tInd, u, v, adjoint=False): + r"""Derivative operation for the diagonal system matrix times a vector. + + The diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{A}_k = \mathbf{C M_{e\mu}^{-1} C^T M_{f\rho}} + \frac{1}{\Delta t_k} \mathbf{I} + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{I}` is the identity matrix + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{M_{e \mu}}` is the permeability inner-product matrix on faces + * :math:`\mathbf{M_{f \rho}}` is the permeability inner-product matrix on edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{j_k}` is the discrete solution for time-step *k*, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_k \, j_k})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_k \, j_k})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model; i.e. :math:`\mathbf{j_k}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ assert tInd >= 0 and tInd < self.nT C = self.mesh.edge_curl @@ -1128,6 +2438,29 @@ def getAdiagDeriv(self, tInd, u, v, adjoint=False): return ADeriv def getAsubdiag(self, tInd): + r"""Sub-diagonal system matrix for the time-step index provided. + + This method returns the sub-diagonal system matrix for the time-step index provided: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{I} + + where :math:`\Delta t_k` is the step-length and :math:`\mathbf{I}` is the + identity matrix. + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The sub-diagonal system matrix. + """ assert tInd >= 0 and tInd < self.nT eye = sp.eye(self.mesh.n_faces) @@ -1138,9 +2471,81 @@ def getAsubdiag(self, tInd): return -1.0 / dt * eye def getAsubdiagDeriv(self, tInd, u, v, adjoint=False): + r"""Derivative operation for the sub-diagonal system matrix times a vector. + + The sub-diagonal system matrix for time-step index *k* is given by: + + .. math:: + \mathbf{B}_k = -\frac{1}{\Delta t_k} \mathbf{I} + + where :math:`\Delta t_k` is the step-length and :math:`\mathbf{I}` is the + identity matrix. + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters, :math:`\mathbf{v}` is a vector + and :math:`\mathbf{j_{k-1}}` is the discrete solution for the previous time-step, + this method assumes the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{B_k \, j_{k-1}})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{B_k \, j_{k-1}})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time-step index; between ``[0, n_steps-1]``. + u : (n_faces,) numpy.ndarray + The solution for the fields for the current model for the previous time-step; + i.e. :math:`\mathbf{j_{k-1}}`. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of system matrix times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ return Zero() def getRHS(self, tInd): + r"""Right-hand sides for the given time index. + + This method returns the right-hand sides for the time index provided. + The right-hand side for each source is constructed according to: + + .. math:: + \mathbf{q}_k = - \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{e},k} + \mathbf{s}_{\mathbf{e},k-1} \big ] \; + - \; \frac{1}{\Delta t_k} \mathbf{C M_{e\mu}^{-1}} \big [ \mathbf{s}_{\mathbf{m},k} + \mathbf{s}_{\mathbf{m},k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + + Returns + ------- + (n_faces, n_sources) numpy.ndarray + The right-hand sides. + """ if tInd == len(self.time_steps): tInd = tInd - 1 @@ -1156,15 +2561,130 @@ def getRHS(self, tInd): return rhs def getRHSDeriv(self, tInd, src, v, adjoint=False): + r"""Derivative of the right-hand side times a vector for a given source and time index. + + The right-hand side for a given source at time index *k* is constructed according to: + + .. math:: + \mathbf{q}_k = - \frac{1}{\Delta t_k} \big [ \mathbf{s}_{\mathbf{e},k} + \mathbf{s}_{\mathbf{e},k-1} \big ] \; + - \; \frac{1}{\Delta t_k} \mathbf{C M_{e\mu}^{-1}} \big [ \mathbf{s}_{\mathbf{m},k} + \mathbf{s}_{\mathbf{m},k-1} \big ] + + where + + * :math:`\Delta t_k` is the step length + * :math:`\mathbf{C}` is the discrete curl operator + * :math:`\mathbf{s_m}` and :math:`\mathbf{s_e}` are the integrated magnetic and electric source terms, respectively + * :math:`\mathbf{M_{e\mu}}` is the inner-product matrix for permeabilities projected to edges + + See the *Notes* section of the doc strings for :class:`Simulation3DCurrentDensity` + for a full description of the formulation. + + Where :math:`\mathbf{m}` are the set of model parameters and :math:`\mathbf{v}` is a vector, + this method returns + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial \mathbf{q_k}}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + tInd : int + The time index; between ``[0, n_steps]``. + src : .time_domain.sources.BaseTDEMSrc + The TDEM source object. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_faces,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the right-hand sides times a vector. (n_faces,) for the standard operation. + (n_param,) for the adjoint operation. + """ return Zero() # assumes no derivs on sources def getAdc(self): + r"""The system matrix for the DC resistivity problem. + + The solution to the DC resistivity problem is necessary at the initial time for + galvanic sources whose currents are non-zero at the initial time. + The discrete solution to the 3D DC resistivity problem is expressed as: + + .. math:: + \mathbf{A_{dc}}\,\boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the DC resistivity system matrix, :math:`\boldsymbol{\phi_0}` + is the discrete solution for the electric potentials at the initial time, and :math:`\mathbf{q_{dc}}` + is the galvanic source term. This method returns the system matrix + for the cell-centered formulation, i.e.: + + .. math:: + \mathbf{D \, M_{f\rho}^{-1} \, G} + + where :math:`\mathbf{D}` is the face divergence operator, :math:`\mathbf{G}` is the cell gradient + operator with imposed boundary conditions, and :math:`\mathbf{M_{f\rho}}` is the inner product + matrix for resistivities projected to faces. + + The current density at the initial time :math:`\mathbf{j_0}` are obtained by applying: + + .. math:: + \mathbf{j_0} = \mathbf{M_{f\rho}^{-1} \, G} \, \boldsymbol{\phi_0} + + See the *Notes* section of the doc strings for :class:`.resistivity.Simulation3DCellCentered` + for a full description of the cell centered DC resistivity formulation. + + Returns + ------- + (n_cells, n_cells) sp.sparse.csr_matrix + The system matrix for the DC resistivity problem. + """ D = sdiag(self.mesh.cell_volumes) * self.mesh.face_divergence G = D.T MfRhoI = self.MfRhoI return D * MfRhoI * G def getAdcDeriv(self, u, v, adjoint=False): + r"""Derivative operation for the DC resistivity system matrix times a vector. + + The discrete solution to the 3D DC resistivity problem is expressed as: + + .. math:: + \mathbf{A_{dc}}\boldsymbol{\phi_0} = \mathbf{q_{dc}} + + where :math:`\mathbf{A_{dc}}` is the DC resistivity system matrix, :math:`\boldsymbol{\phi_0}` + is the discrete solution for the electric potentials at the initial time, and :math:`\mathbf{q_{dc}}` + is the galvanic source term. For a vector :math:`\mathbf{v}`, this method assumes + the discrete solution is fixed and returns + + .. math:: + \frac{\partial (\mathbf{A_{dc}}\boldsymbol{\phi_0})}{\partial \mathbf{m}} \, \mathbf{v} + + Or the adjoint operation + + .. math:: + \frac{\partial (\mathbf{A_{dc}}\boldsymbol{\phi_0})}{\partial \mathbf{m}}^T \, \mathbf{v} + + Parameters + ---------- + u : (n_cells,) numpy.ndarray + The solution for the fields for the current model; i.e. electric potentials at cell centers. + v : numpy.ndarray + The vector. (n_param,) for the standard operation. (n_cells,) for the adjoint operation. + adjoint : bool + Whether to perform the adjoint operation. + + Returns + ------- + numpy.ndarray + Derivative of the DC resistivity system matrix times a vector. (n_cells,) for the standard operation. + (n_param,) for the adjoint operation. + """ D = sdiag(self.mesh.cell_volumes) * self.mesh.face_divergence G = D.T diff --git a/simpeg/electromagnetics/time_domain/simulation_1d.py b/simpeg/electromagnetics/time_domain/simulation_1d.py index 3343ec3fba..a62bb9701e 100644 --- a/simpeg/electromagnetics/time_domain/simulation_1d.py +++ b/simpeg/electromagnetics/time_domain/simulation_1d.py @@ -1,3 +1,5 @@ +from collections import namedtuple + from ..base_1d import BaseEM1DSimulation from .sources import StepOffWaveform from .receivers import ( @@ -12,13 +14,19 @@ from scipy.interpolate import InterpolatedUnivariateSpline as iuSpline from scipy.special import roots_legendre -from empymod import filters -from empymod.transform import get_dlf_points +import libdlf from geoana.kernels.tranverse_electric_reflections import rTE_forward, rTE_gradient +from ..utils.em1d_utils import get_splined_dlf_points from ...utils import validate_type, validate_string +COS_FILTERS = {} +for filter_name in libdlf.fourier.__all__: + fourier_filter = getattr(libdlf.fourier, filter_name) + if "cos" in fourier_filter.values: + COS_FILTERS[filter_name] = fourier_filter + class Simulation1DLayered(BaseEM1DSimulation): """ @@ -26,7 +34,7 @@ class Simulation1DLayered(BaseEM1DSimulation): for a single sounding. """ - def __init__(self, survey=None, time_filter="key_81_CosSin_2009", **kwargs): + def __init__(self, survey=None, time_filter="key_81_2009", **kwargs): super().__init__(survey=survey, **kwargs) self._coefficients_set = False self.time_filter = time_filter @@ -54,18 +62,20 @@ def time_filter(self): @time_filter.setter def time_filter(self, value): + # translate old accepted keys to the names in libdlf for compatibility. + if value in [ + "key_81_CosSin_2009", + "key_201_CosSin_2012", + "key_601_CosSin_2009", + ]: + value = value.replace("CosSin_", "") self._time_filter = validate_string( - "time_filter", - value, - ["key_81_CosSin_2009", "key_201_CosSin_2012", "key_601_CosSin_2009"], + "time_filter", value, list(COS_FILTERS.keys()) ) - - if self._time_filter == "key_81_CosSin_2009": - self._fftfilt = filters.key_81_CosSin_2009() - elif self._time_filter == "key_201_CosSin_2012": - self._fftfilt = filters.key_201_CosSin_2012() - elif self._time_filter == "key_601_CosSin_2009": - self._fftfilt = filters.key_601_CosSin_2009() + filt = COS_FILTERS[self._time_filter]() + cos_filt = namedtuple("CosineFilter", "base cos") + self._fftfilt = cos_filt(filt[0], filt[-1]) + self._coefficients_set = False def get_coefficients(self): if self._coefficients_set is False: @@ -97,8 +107,8 @@ def _compute_coefficients(self): self._compute_hankel_coefficients() survey = self.survey - t_min = np.infty - t_max = -np.infty + t_min = np.inf + t_max = -np.inf x, w = roots_legendre(251) # loop through source and receiver lists to find the minimum and maximum # evaluation times for the step response @@ -123,8 +133,8 @@ def _compute_coefficients(self): f"Unsupported source waveform object of {src.waveform}" ) - omegas, t_spline_points = get_dlf_points(self._fftfilt, np.r_[t_min, t_max], -1) - omegas = omegas.reshape(-1) + omegas, t_spline_points = get_splined_dlf_points(self._fftfilt, t_min, t_max) + n_omega = len(omegas) n_t = len(t_spline_points) @@ -233,13 +243,34 @@ def fields(self, m): sig = self.compute_complex_sigma(frequencies) mu = self.compute_complex_mu(frequencies) - rTE = rTE_forward(frequencies, unique_lambs, sig, mu, self.thicknesses) + # TODO geoana currently only support real mu input. + rTE = rTE_forward(frequencies, unique_lambs, sig, mu.real, self.thicknesses) rTE = rTE[:, inv_lambs] v = ((C0s * rTE) @ self._fhtfilt.j0 + (C1s * rTE) @ self._fhtfilt.j1) @ W.T return self._project_to_data(v.T) - def getJ(self, m, f=None): + def _getJ(self, m, f=None): + """Build Jacobian matrix by blocks. + + This method builds the Jacobian matrix by blocks, each block for a particular + invertible property (receiver height, conductivity, permeability, layer + thickness). Each block of the Jacobian matrix is stored within a dictionary. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + dict + Dictionary containing the blocks of the Jacobian matrix for the invertible + properties. The keys of the dictionary can be `"dh"`, `"ds"`, `"dmu"`, and + `"dthick"`. + """ self.model = m if getattr(self, "_J", None) is None: self._J = {} @@ -278,8 +309,8 @@ def getJ(self, m, f=None): v_dh_temp = ( W @ ( - (C0s_dh * rTE) @ self._fhtfilt.j0 - + (C1s_dh * rTE) @ self._fhtfilt.j1 + ((C0s_dh * rTE) @ self._fhtfilt.j0).real + + ((C1s_dh * rTE) @ self._fhtfilt.j1).real ).T ) # need to re-arange v_dh as it's currently (n_data x n_freqs) @@ -336,6 +367,45 @@ def getJ(self, m, f=None): self._J["dthick"] = self._project_to_data(v_dthick) return self._J + def getJ(self, m, f=None): + r"""Get the Jacobian matrix. + + This method generates and stores the full Jacobian matrix for the + model provided. I.e.: + + .. math:: + \mathbf{J} = \dfrac{\partial f(\mu(\mathbf{m}))}{\partial \mathbf{m}} + + where :math:`f()` is the forward modelling function, :math:`\mu()` is the + mapping, and :math:`\mathbf{m}` is the model vector. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (n_data, n_param) numpy.ndarray + The full Jacobian matrix. + """ + Js = self._getJ(m, f=f) + + # Map parameters with their corresponding derivatives + param_and_derivs = { + "dh": self.hDeriv, + "ds": self.sigmaDeriv, + "dmu": self.muDeriv, + "dthick": self.thicknessesDeriv, + } + + # Compute J matrix + J = sum(Js[param] @ param_and_derivs[param] for param in Js) + + return J + def _project_to_data(self, v): As = self._As if v.ndim == 3: diff --git a/simpeg/electromagnetics/time_domain/sources.py b/simpeg/electromagnetics/time_domain/sources.py index 8fd82da548..c0d7473237 100644 --- a/simpeg/electromagnetics/time_domain/sources.py +++ b/simpeg/electromagnetics/time_domain/sources.py @@ -1,12 +1,17 @@ import warnings +from discretize.utils import mkvc + import numpy as np -from geoana.em.static import CircularLoopWholeSpace, MagneticDipoleWholeSpace +from geoana.em.static import ( + CircularLoopWholeSpace, + MagneticDipoleWholeSpace, + LineCurrentWholeSpace, +) from scipy.constants import mu_0 from ...utils import Zero, sdiag from ...utils.code_utils import ( - deprecate_property, validate_callable, validate_direction, validate_float, @@ -176,13 +181,22 @@ def eval(self, time): # noqa: A003 class RampOffWaveform(BaseWaveform): - """ + """RampOffWaveform([ramp_start,] ramp_end) + A waveform with a linear ramp-off. + ``RampOffWaveform`` can be called with a varying number of positional arguments: + + * ``RampOffWaveform(ramp_end)``: Specify only the ramp end time, with an implied ramp + start time of zero. + * ``RampOffWaveform(ramp_start, ramp_end)``: Specify both the ramp start and end times. + Parameters ---------- - off_time : float, default: 0.0 - time at which the transmitter is turned off in units of seconds + ramp_start : float, optional + Time the ramp off portion of the waveform starts. The default start value is 0. + ramp_end : float + Time at which the ramp off ends. Examples -------- @@ -192,20 +206,115 @@ class RampOffWaveform(BaseWaveform): >>> from simpeg.electromagnetics import time_domain as tdem >>> times = np.linspace(0, 1e-4, 1000) - >>> waveform = tdem.sources.RampOffWaveform(off_time=1e-5) + >>> waveform = tdem.sources.RampOffWaveform(1e-5) >>> plt.plot(times, [waveform.eval(t) for t in times]) >>> plt.show() """ - def __init__(self, off_time=0.0, **kwargs): - super().__init__(off_time=off_time, has_initial_fields=True, **kwargs) + def __init__(self, *args, ramp_start=None, ramp_end=None, **kwargs): + off_time = kwargs.pop("off_time", None) + if off_time is not None: + if len(args) > 1 or ramp_end is not None: + raise TypeError( + "Can not specify both `off_time` and a `ramp_end` value." + ) + ramp_end = off_time + warnings.warn( + "`off_time` keyword arg has been deprecated and will be removed in " + "SimPEG v0.27.0, pass the ramp end time as the last positional argument.`", + DeprecationWarning, + stacklevel=2, + ) + + nargs = len(args) + if nargs == 0: + if ramp_end is None: + raise TypeError( + "RampOffWaveform() requires `ramp_end` to be specified." + ) + if ramp_start is None: + ramp_start = 0.0 + elif nargs == 1: + if ramp_start is not None: + raise TypeError( + "argument for RampOffWaveform() given by name ('ramp_start') and position (position 0)" + ) + if ramp_end is not None: + ramp_start = args[0] + else: + ramp_start = 0 + ramp_end = args[0] + elif nargs == 2: + if ramp_start is not None: + raise TypeError( + "argument for RampOffWaveform() given by name ('ramp_start') and position (position 0)" + ) + if ramp_end is not None: + raise TypeError( + "argument for RampOffWaveform() given by name ('ramp_end') and position (position 1)" + ) + ramp_start, ramp_end = args + else: + raise TypeError( + "Must specify one or two positional arguments for the RampOffWaveform." + ) + + self.ramp_start = ramp_start + super().__init__(off_time=ramp_end, has_initial_fields=True, **kwargs) + + @property + def ramp_start(self): + """Ramp start time + + Sets the time the ramp off begins. + + Returns + ------- + float + The start time of the ramp off for the waveform + """ + return self._ramp_start + + @ramp_start.setter + def ramp_start(self, value): + self._ramp_start = validate_float("ramp_start", value) + + @property + def ramp_end(self): + """Ramp end time, when the current is off. + + Sets the time the ramp off ends. + + Returns + ------- + float + The end time of the ramp off for the waveform + """ + return self._ramp_end + + @ramp_end.setter + def ramp_end(self, value): + """ "off-time of the source""" + self._ramp_end = validate_float( + "ramp_end", value, min_val=self.ramp_start, inclusive_min=False + ) + + @property + def off_time(self): + return self.ramp_end + + @off_time.setter + def off_time(self, value): + self.ramp_end = value def eval(self, time): # noqa: A003 - if abs(time - 0.0) < self.epsilon: + dt = time - self.ramp_start + if dt < self.epsilon: return 1.0 - elif time < self.off_time: - return -1.0 / self.off_time * (time - self.off_time) + elif time < self.ramp_end: + ramp_width = self.ramp_end - self.ramp_start + return 1 - dt / ramp_width else: return 0.0 @@ -213,8 +322,10 @@ def eval_deriv(self, time): t = np.asarray(time, dtype=float) out = np.zeros_like(t) - if self.off_time > 0: - out[(t < self.off_time) & (t >= self.epsilon)] = -1.0 / self.off_time + ramp_width = self.ramp_end - self.ramp_start + out[(t < self.ramp_end) & ((t - self.ramp_start) >= self.epsilon)] = ( + -1.0 / ramp_width + ) if out.ndim == 0: out = out.item() @@ -222,7 +333,7 @@ def eval_deriv(self, time): @property def time_nodes(self): - return np.r_[0.0, self.off_time] + return np.r_[self.ramp_start, self.ramp_end] class RawWaveform(BaseWaveform): @@ -528,18 +639,6 @@ class TriangularWaveform(TrapezoidWaveform): """ def __init__(self, start_time, off_time, peak_time, **kwargs): - if kwargs.get("startTime", None): - AttributeError( - "startTime will be deprecated in 0.17.0. Please update your code to use start_time instead", - ) - if kwargs.get("peak_time", None): - AttributeError( - "peak_time will be deprecated in 0.17.0. Please update your code to use peak_time instead", - ) - if kwargs.get("offTime", None): - AttributeError( - "offTime will be deprecated in 0.17.0. Please update your code to use off_time instead", - ) ramp_on = np.r_[start_time, peak_time] ramp_off = np.r_[peak_time, off_time] @@ -1239,31 +1338,29 @@ def _srcFct(self, obsLoc, coordinates="cartesian"): location=self.location, moment=self.moment, ) - return self._dipole.vector_potential(obsLoc, coordinates=coordinates) + out = self._dipole.vector_potential(obsLoc, coordinates=coordinates) + out[np.isnan(out)] = 0 + return out def _aSrc(self, simulation): coordinates = "cartesian" - if simulation._formulation == "EB": - gridX = simulation.mesh.gridEx - gridY = simulation.mesh.gridEy - gridZ = simulation.mesh.gridEz - - elif simulation._formulation == "HJ": - gridX = simulation.mesh.gridFx - gridY = simulation.mesh.gridFy - gridZ = simulation.mesh.gridFz if simulation.mesh._meshType == "CYL": coordinates = "cylindrical" if simulation.mesh.is_symmetric: - return self._srcFct(gridY)[:, 1] + if simulation._formulation != "EB": + raise AssertionError( + "For cylindrical symmtery, we must use the EB formulation of Maxwell's equations" + ) + return self._srcFct(simulation.mesh.edges, coordinates)[:, 1] - ax = self._srcFct(gridX, coordinates)[:, 0] - ay = self._srcFct(gridY, coordinates)[:, 1] - az = self._srcFct(gridZ, coordinates)[:, 2] - a = np.concatenate((ax, ay, az)) + if simulation._formulation == "EB": + avec = self._srcFct(simulation.mesh.edges, coordinates) + return simulation.mesh.project_edge_vector(avec) - return a + elif simulation._formulation == "HJ": + avec = self._srcFct(simulation.mesh.faces, coordinates) + return simulation.mesh.project_face_vector(avec) def _getAmagnetostatic(self, simulation): if simulation._formulation == "EB": @@ -1320,11 +1417,30 @@ def _phiSrc(self, simulation): def _bSrc(self, simulation): if simulation._formulation == "EB": C = simulation.mesh.edge_curl + return C * self._aSrc(simulation) + + elif simulation._formulation == "HJ": + return self.mu * self._hSrc(simulation) + + def _hSrc(self, simulation): + if simulation._formulation == "EB": + return 1 / self.mu * self._bSrc(simulation) elif simulation._formulation == "HJ": - C = simulation.mesh.edge_curl.T + a = self._aSrc(simulation) - return C * self._aSrc(simulation) + a_boundary = mkvc(self._srcFct(simulation.mesh.boundary_edges)) + a_bc = simulation.mesh.boundary_edge_vector_integral * a_boundary + + return ( + 1.0 + / self.mu + * simulation.MeI + * simulation.mesh.edge_curl.T + * simulation.Mf + * a + - 1 / self.mu * simulation.MeI * a_bc + ) def bInitial(self, simulation): """Compute initial magnetic flux density. @@ -1376,11 +1492,7 @@ def hInitial(self, simulation): if self.waveform.has_initial_fields is False: return Zero() - # if simulation._formulation == 'EB': - # return simulation.MfMui * self.bInitial(simulation) - # elif simulation._formulation == 'HJ': - # return simulation.MeMuI * self.bInitial(simulation) - return 1.0 / self.mu * self.bInitial(simulation) + return self._hSrc(simulation) def s_m(self, simulation, time): """Magnetic source term (s_m) at a given time @@ -1426,11 +1538,7 @@ def s_e(self, simulation, time): self.waveform.has_initial_fields is True and time < simulation.time_steps[1] ): - if simulation._fieldType == "b": - return Zero() - elif simulation._fieldType == "e": - # Compute s_e from vector potential - return C.T * (MfMui * b) + return C.T * (MfMui * b) else: return C.T * (MfMui * b) * self.waveform.eval(time) @@ -1441,11 +1549,7 @@ def s_e(self, simulation, time): self.waveform.has_initial_fields is True and time < simulation.time_steps[1] ): - if simulation._fieldType == "h": - return Zero() - elif simulation._fieldType == "j": - # Compute s_e from vector potential - return C * h + return C * h else: return C * h * self.waveform.eval(time) @@ -1495,16 +1599,10 @@ def __init__( if location is None: location = np.r_[0.0, 0.0, 0.0] - if "moment" in kwargs: - kwargs.pop("moment") - - # Raise error on deprecated arguments - if (key := "N") in kwargs.keys(): - raise TypeError(f"'{key}' property has been removed. Please use 'n_turns'.") self.n_turns = n_turns BaseTDEMSrc.__init__( - self, receiver_list=receiver_list, location=location, moment=None, **kwargs + self, receiver_list=receiver_list, location=location, **kwargs ) self.orientation = orientation @@ -1597,11 +1695,9 @@ def _srcFct(self, obsLoc, coordinates="cartesian"): radius=self.radius, current=self.current, ) - return self.n_turns * self._loop.vector_potential(obsLoc, coordinates) - - N = deprecate_property( - n_turns, "N", "n_turns", removal_version="0.19.0", error=True - ) + out = self._loop.vector_potential(obsLoc, coordinates) + out[np.isnan(out)] = 0 + return self.n_turns * out class LineCurrent(BaseTDEMSrc): @@ -1634,7 +1730,9 @@ def __init__( srcType=None, **kwargs, ): - super().__init__(receiver_list=receiver_list, location=location, **kwargs) + super().__init__( + receiver_list=receiver_list, location=location, srcType=srcType, **kwargs + ) for rx in self.receiver_list: if getattr(rx, "use_source_receiver_offset", False): raise ValueError( @@ -1912,37 +2010,56 @@ def jInitialDeriv(self, simulation, v=None, adjoint=False, f=None): ) def _getAmmr(self, simulation): - if simulation._formulation != "HJ": + if simulation._formulation == "EB": raise NotImplementedError - - vol = simulation.mesh.cell_volumes - Div = sdiag(vol) * simulation.mesh.face_divergence - return ( - simulation.mesh.edge_curl - * simulation.MeMuI - * simulation.mesh.edge_curl.T.tocsr() - - Div.T.tocsr() - * sdiag(1.0 / vol * simulation.mui) - * Div # stabalizing term. See (Chen, Haber & Oldenburg 2002) - ) + elif simulation._formulation == "HJ": + vol = simulation.mesh.cell_volumes + Div = sdiag(vol) * simulation.mesh.face_divergence + return ( + simulation.mesh.edge_curl + * simulation.MeMuI + * simulation.mesh.edge_curl.T.tocsr() + - Div.T.tocsr() + * sdiag(1.0 / vol * simulation.mui) + * Div # stabalizing term. See (Chen, Haber & Oldenburg 2002) + ) def _aInitial(self, simulation): - A = self._getAmmr(simulation) - Ainv = simulation.solver(A) # todo: store this - s_e = self.s_e(simulation, 0) - rhs = s_e + self.jInitial(simulation) - return Ainv * rhs + if self.srcType == "inductive": + # for an inductive source, use the Biot Savart law (from geoana) to compute the vector potential + line_current = LineCurrentWholeSpace(self.location) + if simulation._formulation == "EB": + vector_potential = simulation.mesh.project_edge_vector( + line_current.vector_potential(simulation.mesh.edges) + ) + elif simulation._formulation == "HJ": + vector_potential = simulation.mesh.project_face_vector( + line_current.vector_potential(simulation.mesh.faces) + ) + return self.current * vector_potential + else: + # if a grounded source, solve the MMR problem + A = self._getAmmr(simulation) + Ainv = simulation.solver(A) # todo: store this + s_e = self.s_e(simulation, 0) + rhs = s_e + self.jInitial(simulation) + return Ainv * rhs def _aInitialDeriv(self, simulation, v, adjoint=False): - A = self._getAmmr(simulation) - Ainv = simulation.solver(A) # todo: store this - move it to the simulation + if self.srcType == "inductive": + # the vector potential doesn't depend on the model for an inductive source + return Zero() + else: + # for a grounded source, the derivatives are obtained from the MMR problem + A = self._getAmmr(simulation) + Ainv = simulation.solver(A) # todo: store this - move it to the simulation - if adjoint is True: - return self.jInitialDeriv( - simulation, Ainv * v, adjoint=True - ) # A is symmetric + if adjoint is True: + return self.jInitialDeriv( + simulation, Ainv * v, adjoint=True + ) # A is symmetric - return Ainv * self.jInitialDeriv(simulation, v) + return Ainv * self.jInitialDeriv(simulation, v) def hInitial(self, simulation): """Compute initial magnetic field. @@ -1964,8 +2081,29 @@ def hInitial(self, simulation): if self.waveform.has_initial_fields is False: return Zero() - b = self.bInitial(simulation) - return simulation.MeMuI * b + if simulation._formulation == "EB": + return 1 / self.mu * self.bInitial(simulation) + elif simulation._formulation == "HJ": + a = self._aInitial(simulation) + + if self.srcType == "inductive": + line_current = LineCurrentWholeSpace(self.location) + a_boundary = mkvc( + line_current.vector_potential(simulation.mesh.boundary_edges) + ) + a_bc = simulation.mesh.boundary_edge_vector_integral * a_boundary + + return ( + 1.0 + / self.mu + * simulation.MeI + * simulation.mesh.edge_curl.T + * simulation.Mf + * a + - 1 / self.mu * simulation.MeI * a_bc + ) + else: + return simulation.MeMuI * simulation.mesh.edge_curl.T * a def hInitialDeriv(self, simulation, v, adjoint=False, f=None): """Compute derivative of intitial magnetic field times a vector @@ -2010,11 +2148,13 @@ def bInitial(self, simulation): if self.waveform.has_initial_fields is False: return Zero() - elif simulation._formulation != "HJ": - raise NotImplementedError - a = self._aInitial(simulation) - return simulation.mesh.edge_curl.T * a + if simulation._formulation == "EB": + a = self._aInitial(simulation) + return simulation.mesh.edge_curl * a + elif simulation._formulation == "HJ": + # return simulation.mesh.edge_curl.T * a + return self.mu * self.hInitial(simulation) def bInitialDeriv(self, simulation, v, adjoint=False, f=None): """Compute derivative of intitial magnetic flux density times a vector @@ -2035,8 +2175,6 @@ def bInitialDeriv(self, simulation, v, adjoint=False, f=None): """ if self.waveform.has_initial_fields is False: return Zero() - elif simulation._formulation != "HJ": - raise NotImplementedError if adjoint is True: return self._aInitialDeriv( @@ -2073,11 +2211,14 @@ def s_e(self, simulation, time): # on faces class RawVec_Grounded(LineCurrent): def __init__(self, receiver_list=None, s_e=None, **kwargs): + + srcType = kwargs.pop("srcType", None) + if srcType is not None and srcType != "galvanic": + raise ValueError( + "expected srcType to be 'galvanic' for the RawVec_Grounded" + ) + super().__init__(receiver_list, srcType="galvanic", **kwargs) self.integrate = False - kwargs.pop("srcType", None) - super(RawVec_Grounded, self).__init__( - receiver_list, srcType="galvanic", **kwargs - ) if s_e is not None: self._Mfjs = self._s_e = s_e diff --git a/simpeg/electromagnetics/utils/__init__.py b/simpeg/electromagnetics/utils/__init__.py index bf7fd197b9..ab1970bcbe 100644 --- a/simpeg/electromagnetics/utils/__init__.py +++ b/simpeg/electromagnetics/utils/__init__.py @@ -41,7 +41,6 @@ from .current_utils import ( edge_basis_function, getStraightLineCurrentIntegral, - getSourceTermLineCurrentPolygon, segmented_line_current_source_term, line_through_faces, ) diff --git a/simpeg/electromagnetics/utils/current_utils.py b/simpeg/electromagnetics/utils/current_utils.py index 959f6e1260..698c7a45b2 100644 --- a/simpeg/electromagnetics/utils/current_utils.py +++ b/simpeg/electromagnetics/utils/current_utils.py @@ -511,11 +511,3 @@ def not_aligned_error(i): ) return current - - -def getSourceTermLineCurrentPolygon(xorig, hx, hy, hz, px, py, pz): - """getSourceTermLineCurrentPolygon is deprecated. Use :func:`segmented_line_current_source_term`""" - raise NotImplementedError( - "getSourceTermLineCurrentPolygon has been deprecated and will be" - "removed in SimPEG 0.17.0. Please use segmented_line_current_source_term.", - ) diff --git a/simpeg/electromagnetics/utils/em1d_utils.py b/simpeg/electromagnetics/utils/em1d_utils.py index 91b0ee3f61..fa77c48e1c 100644 --- a/simpeg/electromagnetics/utils/em1d_utils.py +++ b/simpeg/electromagnetics/utils/em1d_utils.py @@ -219,3 +219,39 @@ def LogUniform(f, chi_inf=0.05, del_chi=0.05, tau1=1e-5, tau2=1e-2): return chi_inf + del_chi * ( 1 - np.log((1 + 1j * w * tau2) / (1 + 1j * w * tau1)) / np.log(tau2 / tau1) ) + + +def get_splined_dlf_points(filt, v_min, v_max): + """Get the splined points used for the digital linear filter. + + Parameters + ---------- + filt : namedtuple of numpy.ndarray + The filter parameters loaded from libdlf, in a named tuple containing a `.base` property. + v_min, v_max : float + The minimum and maximum points needed + + Returns + ------- + lamb, spline_points : numpy.ndarray + The points to evaluate at in the filter space, and the + corresponding spline points in the transformed space. + """ + + # the below is adapted from empymod.transform.get_dlf_points + outmax = filt.base[-1] / v_min + outmin = filt.base[0] / v_max + + factor = np.around([filt.base[1] / filt.base[0]], 15) + + pts_per_dec = np.squeeze(1 / np.log(factor)) + + nout = int(np.ceil(np.log(outmax / outmin) * pts_per_dec) + 1) + if nout - filt.base.size < 3: + nout = filt.base.size + 3 + out = np.exp( + np.arange(np.log(outmin), np.log(outmin) + nout / pts_per_dec, 1 / pts_per_dec) + ) + + spline_points = v_max * np.exp(-np.arange(nout - filt.base.size + 1) / pts_per_dec) + return out, spline_points diff --git a/simpeg/electromagnetics/utils/testing_utils.py b/simpeg/electromagnetics/utils/testing_utils.py index ec7ad01a33..86444a8979 100644 --- a/simpeg/electromagnetics/utils/testing_utils.py +++ b/simpeg/electromagnetics/utils/testing_utils.py @@ -4,7 +4,6 @@ from discretize import TensorMesh from ... import maps, utils -from simpeg import SolverLU from simpeg.electromagnetics import frequency_domain as fdem FLR = 1e-20 # "zero", so if residual below this --> pass regardless of order @@ -126,13 +125,6 @@ def getFDEMProblem(fdemType, comp, SrcList, freq, useMu=False, verbose=False): else: raise NotImplementedError() - - try: - from pymatsolver import Pardiso - - prb.solver = Pardiso - except ImportError: - prb.solver = SolverLU # prb.solver_opts = dict(check_accuracy=True) return prb diff --git a/simpeg/electromagnetics/utils/waveform_utils.py b/simpeg/electromagnetics/utils/waveform_utils.py index 55e2c2f8ab..8120777712 100644 --- a/simpeg/electromagnetics/utils/waveform_utils.py +++ b/simpeg/electromagnetics/utils/waveform_utils.py @@ -119,6 +119,6 @@ def integral(quad_time, t): # just do not evaluate the integral at negative times... a = np.maximum(a, 0.0) b = np.maximum(b, 0.0) - val, _ = integrate.quadrature(integral, a, b, tol=0.0, maxiter=500, args=t) + val = integrate.quad(integral, a, b, epsabs=0.0, limit=500, args=t)[0] out[it] -= val return out diff --git a/simpeg/electromagnetics/viscous_remanent_magnetization/receivers.py b/simpeg/electromagnetics/viscous_remanent_magnetization/receivers.py index 5d5823311f..cc1c730704 100644 --- a/simpeg/electromagnetics/viscous_remanent_magnetization/receivers.py +++ b/simpeg/electromagnetics/viscous_remanent_magnetization/receivers.py @@ -30,11 +30,6 @@ class Point(BaseRx): def __init__( self, locations=None, times=None, field_type=None, orientation="z", **kwargs ): - if kwargs.pop("fieldType", None): - raise AttributeError( - "'fieldType' is a deprecated property. Please use 'field_type' instead." - "'fieldType' be removed in SimPEG 0.17.0." - ) if field_type is None: raise AttributeError( "VRM receiver class cannot be instantiated witout 'field_type" @@ -179,18 +174,6 @@ def __init__( quadrature_order=3, **kwargs, ): - if "nTurns" in kwargs: - raise AttributeError( - "'nTurns' is a deprecated property. Please use 'n_turns' instead." - "'nTurns' be removed in SimPEG 0.17.0." - ) - - if "quadOrder" in kwargs: - raise AttributeError( - "'quadOrder' is a deprecated property. Please use 'quadrature_order' instead." - "'quadOrder' be removed in SimPEG 0.17.0." - ) - super(SquareLoop, self).__init__( locations=locations, times=times, diff --git a/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py b/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py index 27cfb040f1..6d4a63cecc 100644 --- a/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py +++ b/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py @@ -15,6 +15,7 @@ from .survey import SurveyVRM from .receivers import Point, SquareLoop +from ...utils.code_utils import deprecate_property ############################################ # BASE VRM PROBLEM CLASS @@ -32,10 +33,16 @@ def __init__( survey=None, refinement_factor=None, refinement_distance=None, - indActive=None, + active_cells=None, **kwargs, ): - super().__init__(mesh=mesh, survey=survey, **kwargs) + # Deprecate indActive argument + if kwargs.pop("indActive", None) is not None: + raise TypeError( + "'indActive' was removed in SimPEG v0.24.0, please use 'active_cells' instead." + ) + self.mesh = mesh + super().__init__(survey=survey, **kwargs) if refinement_distance is None: if refinement_factor is None: @@ -48,18 +55,30 @@ def __init__( * np.arange(1, refinement_factor + 1) ) self.refinement_distance = refinement_distance - if indActive is None: - indActive = np.ones(self.mesh.n_cells, dtype=bool) - self.indActive = indActive - @BaseSimulation.mesh.setter + if active_cells is None: + active_cells = np.ones(self.mesh.n_cells, dtype=bool) + self.active_cells = active_cells + + @property + def mesh(self): + """Mesh for the integral VRM simulations. + + Returns + ------- + discretize.TensorMesh or discretize.TreeMesh + 3D Mesh on which the forward problem is discretized. + """ + return self._mesh + + @mesh.setter def mesh(self, value): value = validate_type( "mesh", value, (discretize.TensorMesh, discretize.TreeMesh), cast=False ) if value.dim != 3: raise ValueError( - f"Mesh must be 3D tensor or 3D tree. Current mesh is {value.dim}" + f"{type(self).__name__} mesh must be 3D, received a {value.dim}D mesh." ) self._mesh = value @@ -108,18 +127,28 @@ def refinement_distance(self, value): ) @property - def indActive(self): + def active_cells(self): """Topography active cells. Returns ------- (mesh.n_cells) numpy.ndarray of bool """ - return self._indActive + return self._active_cells - @indActive.setter - def indActive(self, value): - self._indActive = validate_active_indices("indActive", value, self.mesh.n_cells) + @active_cells.setter + def active_cells(self, value): + self._active_cells = validate_active_indices( + "active_cells", value, self.mesh.n_cells + ) + + indActive = deprecate_property( + active_cells, + "indActive", + "active_cells", + removal_version="0.24.0", + error=True, + ) def _getH0matrix(self, xyz, pp): """ @@ -697,12 +726,12 @@ def _getGeometryMatrix(self, xyzc, xyzh, pp): def _getAMatricies(self): """Returns the full geometric operator""" - indActive = self.indActive + active_cells = self.active_cells # GET CELL INFORMATION FOR FORWARD MODELING meshObj = self.mesh - xyzc = meshObj.gridCC[indActive, :] - xyzh = meshObj.h_gridded[indActive, :] + xyzc = meshObj.gridCC[active_cells, :] + xyzh = meshObj.h_gridded[active_cells, :] # GET LIST OF A MATRICIES A = [] @@ -811,7 +840,7 @@ def __init__(self, mesh, xi=None, xiMap=None, **kwargs): self.xi = xi self.xiMap = xiMap - nAct = list(self.indActive).count(True) + nAct = list(self.active_cells).count(True) if self.xiMap is None: self.xiMap = maps.IdentityMap(nP=nAct) diff --git a/simpeg/fields.py b/simpeg/fields.py index d3eb461c27..f78c994219 100644 --- a/simpeg/fields.py +++ b/simpeg/fields.py @@ -5,24 +5,71 @@ class Fields: - """Fancy Field Storage + r"""Base class for storing fields. + + Fields classes are used to store the discrete field solution for a + corresponding simulation object; see :py:class:`simpeg.simulation.BaseSimulation`. + Generally only one field solution (e.g. ``'eSolution'``, ``'phiSolution'``, ``'bSolution'``) is stored. + However, it may be possible to extract multiple field types (e.g. ``'e'``, ``'b'``, ``'j'``, ``'h'``) + on the fly from the fields object. The field solution that is stored and the + field types that can be extracted depend on the formulation used by the associated simulation. + See the example below to learn how fields are extracted from fields objects. + + Parameters + ---------- + simulation : simpeg.simulation.BaseSimulation + The simulation object used to compute the discrete field solution. + knownFields : dict of {key: str}, optional + Dictionary defining the field solutions that are stored and where + on the mesh they are discretized. E.g. ``{'eSolution': 'E', 'bSolution': 'F'}`` + would store the `eSolution` on edges and `bSolution` on faces. + The ``str`` must be one of {``'CC'``, ``'N'``, ``'E'``, ``'F'``}. + aliasFields : dict of {key: list}, optional + Set aliases to extract different field types from the field solutions that are + stored by the fields object. The ``key`` defines the name you would like to use + when extracting a given field type from the fields object. In order, the list + contains: + + * the key for the known field solution that is used to compute the field type + * where the output field type lives {``'CC'``, ``'N'``, ``'E'``, ``'F'``} + * the name of the method used to compute the output field. + + E.g. ``{'b': ['eSolution', 'F', '_b']}`` is an alias that + would allow you to extract a field type (``'b'``) that lives on mesh faces (``'F'``) + from the E-field solution (``'eSolution'``) by calling a method (``'_b'``). + dtype : dtype or dict of {str : dtype}, optional + Set the Python data type for each numerical field solution that is stored in + the fields object. E.g. ``float``, ``complex``, + ``{'eSolution': complex, 'bSolution': complex}``. Examples -------- - >>> fields = Fields( - ... simulation=simulation, knownFields={"phi": "CC"} - ... ) - >>> fields[:,'phi'] = phi + We want to access the fields for a discrete solution with :math:`\mathbf{e}` discretized + to edges and :math:`\mathbf{b}` discretized to faces. To extract the fields for all sources: + + .. code-block:: python + + f = simulation.fields(m) + e = f[:,'e'] + b = f[:,'b'] + + The array ``e`` returned will have shape (`n_edges`, `n_sources`). And the array ``b`` + returned will have shape (`n_faces`, `n_sources`). We can also extract the fields for + a subset of the source list used for the simulation as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list,'e'] + b = f[source_list,'b'] + """ _dtype = float _knownFields = {} _aliasFields = {} - def __init__( - self, simulation, knownFields=None, aliasFields=None, dtype=None, **kwargs - ): - super().__init__(**kwargs) + def __init__(self, simulation, knownFields=None, aliasFields=None, dtype=None): self.simulation = simulation if knownFields is not None: @@ -45,11 +92,12 @@ def __init__( @property def simulation(self): - """The simulation object that created these fields + """The simulation object used to compute the field solution. Returns ------- simpeg.simulation.BaseSimulation + The simulation object used to compute the field solution. """ return self._simulation @@ -61,39 +109,41 @@ def simulation(self, value): @property def knownFields(self): - """The known fields of this object. - - The dictionary representing the known fields and their locations on the simulation - mesh. The keys are the names of the fields, and the values are the location on - the mesh. + """The field solutions and where they are discretized on the mesh. - >>> fields.knownFields - {'e': 'E', 'phi': 'CC'} + Dictionary defining the field solutions that are stored and where + on the mesh they are discretized. The ``key`` defines the name + of the field solution that is stored, and a ``str`` defines where + on the mesh the stored field solution is discretized. The + ``str`` must be one of {``'CC'``, ``'N'``, ``'E'``, ``'F'``}. - Would represent that the `e` field and `phi` fields are known, and they are - located on the mesh edges and cell centers, respectively. + E.g. ``{'eSolution': 'E', 'bSolution': 'F'}`` + would define the `eSolution` on edges and `bSolution` on faces. Returns ------- dict - They keys are the field names and the values are the field locations. + The keys are the field solution names and the values {'N', 'CC', 'E'. 'F'} + define where the field solution is discretized. """ return self._knownFields @property def aliasFields(self): - """The aliased fields of this object. + """The aliased fields of the object. - The dictionary representing the aliased fields that can be accessed on this - object. The keys are the names of the fields, and the values are a list of the - known field, the aliased field's location on the mesh, and a function that goes - from the known field to the aliased field. + Aliases are defined to extract different field types from the field solutions that are + stored by the fields object. The ``key`` defines the name you would like to use + when extracting a given field type from the fields object. In order, the list + contains: - >>> fields.aliasFields - {'b': ['e', 'F', '_e']} + * the key for the known field solution that is used to compute the field type + * where the output field type lives {``'CC'``, ``'N'``, ``'E'``, ``'F'``} + * the name of the method used to compute the output field. - Would represent that the `e` field and `phi` fields are known, and they are - located on the mesh edges and cell centers, respectively. + E.g. ``{'b': ['eSolution', 'F', '_b']}`` is an alias that + would allow you to extract a field type ('b') that lives on mesh faces ('F') + from the E-field solution ('eSolution') by calling a method ('_b'). Returns ------- @@ -106,28 +156,53 @@ def aliasFields(self): @property def dtype(self): - """The data type of the storage matrix + """Python data type(s) used to store the fields. + + the Python data type for each numerical field solution that is stored in + the fields object. E.g. ``float``, ``complex``, ``{'eSolution': complex, 'bSolution': complex}``. Returns ------- dtype or dict of {str : dtype} + Python data type(s) used to store the fields. """ return self._dtype @property def mesh(self): + """Mesh used by the simulation. + + Returns + ------- + discretize.BaseMesh + Mesh used by the simulation. + """ return self.simulation.mesh @property def survey(self): + """Survey used by the simulation. + + Returns + ------- + simpeg.survey.BaseSurvey + Survey used by the simulation. + """ return self.simulation.survey def startup(self): + """Run startup to connect the simulation's discrete attributes to the fields object.""" pass @property def approxSize(self): - """The approximate cost to storing all of the known fields.""" + """Approximate cost of storing all of the known fields in MB. + + Returns + ------- + int + Approximate cost of storing all of the known fields in MB. + """ sz = 0.0 for f in self.knownFields: loc = self.knownFields[f] @@ -273,21 +348,73 @@ def __contains__(self, other): class TimeFields(Fields): - """Fancy Field Storage for time domain problems - .. code:: python + r"""Base class for storing TDEM fields. + + ``TimeFields`` is a base class for storing discrete field solutions for simulations + that use discrete time-stepping; see :py:class:`simpeg.simulation.BaseTimeSimulation`. + Generally only one field solution (e.g. ``'eSolution'``, ``'phiSolution'``, ``'bSolution'``) is stored. + However, it may be possible to extract multiple field types (e.g. ``'e'``, ``'b'``, ``'j'``, ``'h'``) + on the fly from the fields object. The field solution that is stored and the + field types that can be extracted depend on the formulation used by the associated simulation. + See the example below to learn how fields are extracted from fields objects. + + Parameters + ---------- + simulation : simpeg.simulation.BaseTimeSimulation + The simulation object used to compute the discrete field solution. + knownFields : dict of {key: str}, optional + Dictionary defining the field solutions that are stored and where + on the mesh they are discretized. E.g. ``{'eSolution': 'E', 'bSolution': 'F'}`` + would store the `eSolution` on edges and `bSolution` on faces. + The ``str`` must be one of {``'CC'``, ``'N'``, ``'E'``, ``'F'``}. + aliasFields : dict of {key: list}, optional + Set aliases to extract different field types from the field solutions that are + stored by the fields object. The ``key`` defines the name you would like to use + when extracting a given field type from the fields object. In order, the list + contains: + + * the key for the known field solution that is used to compute the field type + * where the output field type lives {``'CC'``, ``'N'``, ``'E'``, ``'F'``} + * the name of the method used to compute the output field. + + E.g. ``{'b': ['eSolution', 'F', '_b']}`` is an alias that + would allow you to extract a field type ('b') that lives on mesh faces ('F') + from the E-field solution ('eSolution') by calling a method ('_b'). + dtype : dtype or dict of {str : dtype}, optional + Set the Python data type for each numerical field solution that is stored in + the fields object. E.g. ``float``, ``complex``, ``{'eSolution': complex, 'bSolution': complex}``. + + Examples + -------- + We want to access the fields for a discrete solution with :math:`\mathbf{e}` discretized + to edges and :math:`\mathbf{b}` discretized to faces. To extract the fields for all sources: + + .. code-block:: python + + f = simulation.fields(m) + e = f[:, 'e', :] + b = f[:, 'b', :] + + The array ``e`` returned will have shape (`n_edges`, `n_sources`, `n_steps`). And the array ``b`` + returned will have shape (`n_faces`, `n_sources`, `n_steps`). We can also extract the fields for + a subset of the source list used for the simulation and/or a subset of the time steps as follows: + + .. code-block:: python + + f = simulation.fields(m) + e = f[source_list, 'e', t_inds] + b = f[source_list, 'b', t_inds] - fields = TimeFields(simulation=simulation, knownFields={'phi':'CC'}) - fields[:,'phi', timeInd] = phi - print(fields[src0,'phi']) """ @property def simulation(self): - """The simulation object that created these fields + """The simulation object used to compute the field solution. Returns ------- simpeg.simulation.BaseTimeSimulation + The simulation object used to compute the field solution. """ return self._simulation diff --git a/simpeg/flow/richards/empirical.py b/simpeg/flow/richards/empirical.py index d726aaad91..97f6e5b640 100644 --- a/simpeg/flow/richards/empirical.py +++ b/simpeg/flow/richards/empirical.py @@ -1,7 +1,9 @@ +import discretize.base import numpy as np import scipy.sparse as sp from scipy import constants from ... import utils, props +from ...utils import validate_type def _get_projections(u): @@ -34,12 +36,19 @@ class NonLinearModel(props.HasModel): """A non linear model that has dependence on the fields and a model""" counter = None #: A simpeg.utils.Counter object - mesh = None #: A discretize Mesh def __init__(self, mesh, **kwargs): self.mesh = mesh super(NonLinearModel, self).__init__(**kwargs) + @property + def mesh(self): + return self._mesh + + @mesh.setter + def mesh(self, value): + self._mesh = validate_type("mesh", value, discretize.base.BaseMesh, cast=False) + @property def nP(self): """Number of parameters in the model.""" @@ -100,7 +109,7 @@ def __init__( alphaMap=None, beta=3.96, betaMap=None, - **kwargs + **kwargs, ): super().__init__(mesh=mesh, **kwargs) self.theta_r = theta_r @@ -224,7 +233,7 @@ def __init__( AMap=None, gamma=4.74, gammaMap=None, - **kwargs + **kwargs, ): super().__init__(mesh=mesh, **kwargs) self.Ks = Ks @@ -292,7 +301,7 @@ def haverkamp(mesh, **kwargs): Haverkamp_theta, ["Ks", "A", "gamma"], ["alpha", "beta", "theta_r", "theta_s"], - **kwargs + **kwargs, ) @@ -344,7 +353,7 @@ def __init__( nMap=None, alpha=0.036, alphaMap=None, - **kwargs + **kwargs, ): super().__init__(mesh=mesh, **kwargs) self.theta_r = theta_r @@ -499,7 +508,7 @@ def __init__( nMap=None, alpha=0.036, alphaMap=None, - **kwargs + **kwargs, ): super().__init__(mesh=mesh, **kwargs) self.Ks = Ks @@ -807,7 +816,7 @@ def van_genuchten(mesh, **kwargs): Vangenuchten_theta, ["alpha", "n", "Ks", "I"], ["alpha", "n", "theta_r", "theta_s"], - **kwargs + **kwargs, ) diff --git a/simpeg/flow/richards/simulation.py b/simpeg/flow/richards/simulation.py index b897230230..29e5b10a53 100644 --- a/simpeg/flow/richards/simulation.py +++ b/simpeg/flow/richards/simulation.py @@ -1,14 +1,15 @@ +import discretize.base import numpy as np import scipy.sparse as sp import time from ... import utils +from ...base import BasePDESimulation from ...simulation import BaseTimeSimulation from ... import optimization from ...utils import ( validate_type, validate_ndarray_with_shape, - deprecate_property, validate_string, validate_integer, validate_float, @@ -19,7 +20,7 @@ from .empirical import BaseWaterRetention -class SimulationNDCellCentered(BaseTimeSimulation): +class SimulationNDCellCentered(BaseTimeSimulation, BasePDESimulation): """Richards Simulation""" def __init__( @@ -33,11 +34,8 @@ def __init__( do_newton=False, root_finder_max_iter=30, root_finder_tol=1e-4, - **kwargs + **kwargs, ): - debug = kwargs.pop("debug", None) - if debug is not None: - self.debug = debug super().__init__(mesh=mesh, **kwargs) self.hydraulic_conductivity = hydraulic_conductivity self.water_retention = water_retention @@ -53,6 +51,22 @@ def __init__( ) water_retention = NestedModeler(BaseWaterRetention, "water retention curve") + @property + def mesh(self): + """Tensor style mesh for the Richards flow simulation. + + Returns + ------- + discretize.TensorMesh or discretize.TreeMesh + """ + return self._mesh + + @mesh.setter + def mesh(self, value): + self._mesh = validate_type( + "mesh", value, (discretize.TensorMesh, discretize.TreeMesh), cast=False + ) + # TODO: This can also be a function(time, u_ii) @property def boundary_conditions(self): @@ -86,14 +100,6 @@ def initial_conditions(self, value): "initial_conditions", value ) - debug = deprecate_property( - BaseTimeSimulation.verbose, - "debug", - "verbose", - removal_version="0.19.0", - future_warn=True, - ) - @property def method(self): """Formulation used. diff --git a/simpeg/inverse_problem.py b/simpeg/inverse_problem.py index b4f5630bd9..ec24a154ef 100644 --- a/simpeg/inverse_problem.py +++ b/simpeg/inverse_problem.py @@ -1,10 +1,13 @@ +import textwrap + import numpy as np import scipy.sparse as sp import gc + from .data_misfit import BaseDataMisfit from .regularization import BaseRegularization, WeightedLeastSquares, Sparse from .objective_function import BaseObjectiveFunction, ComboObjectiveFunction -from .optimization import Minimize +from .optimization import Minimize, BFGS from .utils import ( call_hooks, timeIt, @@ -12,15 +15,26 @@ validate_float, validate_type, validate_ndarray_with_shape, + get_logger, ) -from .simulation import DefaultSolver +from .version import __version__ as simpeg_version +from .utils import get_default_solver class BaseInvProblem: """BaseInvProblem(dmisfit, reg, opt)""" def __init__( - self, dmisfit, reg, opt, beta=1.0, debug=False, counter=None, **kwargs + self, + dmisfit, + reg, + opt, + beta=1.0, + debug=False, + counter=None, + print_version=True, + init_bfgs=True, + **kwargs, ): super().__init__(**kwargs) assert isinstance(reg, BaseRegularization) or isinstance( @@ -35,6 +49,8 @@ def __init__( self.debug = debug self.counter = counter self.model = None + self.print_version = print_version + self.init_bfgs = init_bfgs # TODO: Remove: (and make iteration printers better!) self.opt.parent = self self.reg.parent = self @@ -86,7 +102,7 @@ def counter(self, value): self._counter = value @property - def dmisfit(self): + def dmisfit(self) -> ComboObjectiveFunction: """The data misfit. Returns @@ -103,7 +119,7 @@ def dmisfit(self, value): self._dmisfit = value @property - def reg(self): + def reg(self) -> ComboObjectiveFunction: """The regularization object for the inversion Returns @@ -134,7 +150,7 @@ def opt(self, value): self._opt = validate_type("opt", value, Minimize, cast=False) @property - def deleteTheseOnModelUpdate(self): + def _delete_on_model_update(self): """A list of properties stored on this object to delete when the model is updated Returns @@ -160,11 +176,20 @@ def model(self, value): value = validate_ndarray_with_shape( "model", value, shape=[("*",), ("*", "*")], dtype=None ) - for prop in self.deleteTheseOnModelUpdate: + for prop in self._delete_on_model_update: if hasattr(self, prop): delattr(self, prop) self._model = value + @property + def init_bfgs(self): + """Initialize BFGS minimizers with the inverse of the regularization's Hessian.""" + return self._init_bfgs + + @init_bfgs.setter + def init_bfgs(self, value): + self._init_bfgs = validate_type("init_bfgs", value, bool) + @call_hooks("startup") def startup(self, m0): """startup(m0) @@ -174,12 +199,17 @@ def startup(self, m0): if self.debug: print("Calling InvProblem.startup") + if self.print_version: + print(f"\nRunning inversion with SimPEG v{simpeg_version}") + + logger = get_logger() + for fct in self.reg.objfcts: if ( hasattr(fct, "reference_model") and getattr(fct, "reference_model", None) is None ): - print( + logger.info( "simpeg.InvProblem will set Regularization.reference_model to m0." ) fct.reference_model = m0 @@ -189,40 +219,35 @@ def startup(self, m0): self.model = m0 - solver = DefaultSolver - set_default = True - for objfct in self.dmisfit.objfcts: - if ( - isinstance(objfct, BaseDataMisfit) - and getattr(objfct.simulation, "solver", None) is not None - ): - solver = objfct.simulation.solver - solver_opts = objfct.simulation.solver_opts - print( + if self.init_bfgs and isinstance(self.opt, BFGS): + + sim = None # Find the first sim in data misfits that has a non None solver attribute + for objfct in self.dmisfit.objfcts: + if ( + isinstance(objfct, BaseDataMisfit) + and getattr(objfct.simulation, "solver", None) is not None + ): + sim = objfct.simulation + break + if sim is not None: + solver = sim.solver + solver_opts = sim.solver_opts + msg = f""" + simpeg.InvProblem is setting bfgsH0 to the inverse of the reg.deriv2 + using the same solver as the {sim.__class__.__name__} simulation with the 'is_symmetric=True` option set. """ - simpeg.InvProblem is setting bfgsH0 to the inverse of the eval2Deriv. - ***Done using same Solver, and solver_opts as the {} problem*** - """.format( - objfct.simulation.__class__.__name__ - ) - ) - set_default = False - break - if set_default: - print( - """ - simpeg.InvProblem is setting bfgsH0 to the inverse of the eval2Deriv. - ***Done using the default solver {} and no solver_opts.*** - """.format( - DefaultSolver.__name__ - ) - ) - solver = DefaultSolver - solver_opts = {} + else: + solver = get_default_solver() + msg = f""" + simpeg.InvProblem is setting bfgsH0 to the inverse of the reg.deriv2. + using the default solver {solver.__name__} with the 'is_symmetric=True` option set. + """ + solver_opts = dict(is_symmetric=True) - self.opt.bfgsH0 = solver( - sp.csr_matrix(self.reg.deriv2(self.model)), **solver_opts - ) + logger.info(textwrap.dedent(msg)) + self.opt.bfgsH0 = solver( + sp.csr_matrix(self.reg.deriv2(self.model)), **solver_opts + ) @property def warmstart(self): diff --git a/simpeg/inversion.py b/simpeg/inversion.py index 9826f8cca9..e444b32f1e 100644 --- a/simpeg/inversion.py +++ b/simpeg/inversion.py @@ -1,7 +1,5 @@ -import numpy as np - -from .optimization import IterationPrinters, StoppingCriteria -from .directives import DirectiveList +from .optimization import IterationPrinters, StoppingCriteria, InexactGaussNewton +from .directives import DirectiveList, UpdatePreconditioner from .utils import timeIt, Counter, validate_type, validate_string @@ -15,7 +13,7 @@ def __init__( counter=None, debug=False, name="BaseInversion", - **kwargs + **kwargs, ): if directiveList is None: directiveList = [] @@ -107,9 +105,13 @@ def run(self, m0): Runs the inversion! """ + if isinstance(self.opt, InexactGaussNewton) and any( + isinstance(drctv, UpdatePreconditioner) for drctv in self.directiveList + ): + self.invProb.init_bfgs = False + self.invProb.startup(m0) self.directiveList.call("initialize") - print("model has any nan: {:b}".format(np.any(np.isnan(self.invProb.model)))) self.m = self.opt.minimize(self.invProb.evalFunction, self.invProb.model) self.directiveList.call("finish") diff --git a/simpeg/maps.py b/simpeg/maps.py deleted file mode 100644 index 8e9cfc7bc8..0000000000 --- a/simpeg/maps.py +++ /dev/null @@ -1,6410 +0,0 @@ -from collections import namedtuple -import warnings -import discretize -import numpy as np -from numpy.polynomial import polynomial -import scipy.sparse as sp -from scipy.sparse.linalg import LinearOperator -from scipy.interpolate import UnivariateSpline -from scipy.constants import mu_0 -from scipy.sparse import csr_matrix as csr -from scipy.special import expit, logit - -from discretize.tests import check_derivative -from discretize import TensorMesh, CylindricalMesh -from discretize.utils import ( - mkvc, - rotation_matrix_from_normals, - Zero, - Identity, - sdiag, - speye, -) - -from .utils import ( - mat_utils, - validate_type, - validate_ndarray_with_shape, - validate_float, - validate_direction, - validate_integer, - validate_string, - validate_active_indices, - validate_list_of_types, -) - - -class IdentityMap: - r"""Identity mapping and the base mapping class for all other SimPEG mappings. - - The ``IdentityMap`` class is used to define the mapping when - the model parameters are the same as the parameters used in the forward - simulation. For a discrete set of model parameters :math:`\mathbf{m}`, - the mapping :math:`\mathbf{u}(\mathbf{m})` is equivalent to applying - the identity matrix; i.e.: - - .. math:: - - \mathbf{u}(\mathbf{m}) = \mathbf{Im} - - The ``IdentityMap`` also acts as the base class for all other SimPEG mapping classes. - - Using the *mesh* or *nP* input arguments, the dimensions of the corresponding - mapping operator can be permanently set; i.e. (*mesh.nC*, *mesh.nC*) or (*nP*, *nP*). - However if both input arguments *mesh* and *nP* are ``None``, the shape of - mapping operator is arbitrary and can act on any vector; i.e. has shape (``*``, ``*``). - - Parameters - ---------- - mesh : discretize.BaseMesh - The number of parameters accepted by the mapping is set to equal the number - of mesh cells. - nP : int, or '*' - Set the number of parameters accepted by the mapping directly. Used if the - number of parameters is known. Used generally when the number of parameters - is not equal to the number of cells in a mesh. - """ - - def __init__(self, mesh=None, nP=None, **kwargs): - if (isinstance(nP, str) and nP == "*") or nP is None: - if mesh is not None: - nP = mesh.n_cells - else: - nP = "*" - else: - try: - nP = int(nP) - except (TypeError, ValueError) as err: - raise TypeError( - f"Unrecognized input of {repr(nP)} for number of parameters, must be an integer or '*'." - ) from err - self.mesh = mesh - self._nP = nP - - super().__init__(**kwargs) - - @property - def nP(self): - r"""Number of parameters the mapping acts on. - - Returns - ------- - int or ``*`` - Number of parameters that the mapping acts on. Returns an - ``int`` if the dimensions of the mapping are set. If the - mapping can act on a vector of any length, ``*`` is returned. - """ - if self._nP != "*": - return int(self._nP) - if self.mesh is None: - return "*" - return int(self.mesh.nC) - - @property - def shape(self): - r"""Dimensions of the mapping operator - - The dimensions of the mesh depend on the input arguments used - during instantiation. If *mesh* is used to define the - identity map, the shape of mapping operator is (*mesh.nC*, *mesh.nC*). - If *nP* is used to define the identity map, the mapping operator - has dimensions (*nP*, *nP*). However if both *mesh* and *nP* are - used to define the identity map, the mapping will have shape - (*mesh.nC*, *nP*)! And if *mesh* and *nP* were ``None`` when - instantiating, the mapping has dimensions (``*``, ``*``) and may - act on a vector of any length. - - Returns - ------- - tuple - Dimensions of the mapping operator. If the dimensions of - the mapping are set, the return is a tuple (``int``,``int``). - If the mapping can act on a vector of arbitrary length, the - return is a tuple (``*``, ``*``). - """ - if self.mesh is None: - return (self.nP, self.nP) - return (self.mesh.nC, self.nP) - - def _transform(self, m): - """ - Changes the model into the physical property. - - .. note:: - - This can be called by the __mul__ property against a - :meth:numpy.ndarray. - - :param numpy.ndarray m: model - :rtype: numpy.ndarray - :return: transformed model - - """ - return m - - def inverse(self, D): - """ - The transform inverse is not implemented. - """ - raise NotImplementedError("The transform inverse is not implemented.") - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the input parameters. - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix or numpy.ndarray - Derivative of the mapping with respect to the model parameters. For an - identity mapping, this is just a sparse identity matrix. If the input - argument *v* is not ``None``, the method returns the derivative times - the vector *v*; which in this case is just *v*. - - Notes - ----- - Let :math:`\mathbf{m}` be a set of model parameters and let :math:`\mathbf{I}` - denote the identity map. Where the identity mapping acting on the model parameters - can be expressed as: - - .. math:: - \mathbf{u} = \mathbf{I m}, - - the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect - to the model parameters; i.e.: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{I} - - For the Identity map **deriv** simply returns a sparse identity matrix. - """ - if v is not None: - return v - if isinstance(self.nP, (int, np.integer)): - return sp.identity(self.nP) - return Identity() - - def test(self, m=None, num=4, **kwargs): - """Derivative test for the mapping. - - This test validates the mapping by performing a convergence test. - - Parameters - ---------- - m : (nP) numpy.ndarray - Starting vector of model parameters for the derivative test - num : int - Number of iterations for the derivative test - kwargs: dict - Keyword arguments and associated values in the dictionary must - match those used in :meth:`discretize.tests.check_derivative` - - Returns - ------- - bool - Returns ``True`` if the test passes - """ - print("Testing {0!s}".format(str(self))) - if m is None: - m = abs(np.random.rand(self.nP)) - if "plotIt" not in kwargs: - kwargs["plotIt"] = False - - assert isinstance( - self.nP, (int, np.integer) - ), "nP must be an integer for {}".format(self.__class__.__name__) - return check_derivative( - lambda m: [self * m, self.deriv(m)], m, num=num, **kwargs - ) - - def _assertMatchesPair(self, pair): - assert ( - isinstance(self, pair) - or isinstance(self, ComboMap) - and isinstance(self.maps[0], pair) - ), "Mapping object must be an instance of a {0!s} class.".format(pair.__name__) - - def __mul__(self, val): - if isinstance(val, IdentityMap): - if ( - not (self.shape[1] == "*" or val.shape[0] == "*") - and not self.shape[1] == val.shape[0] - ): - raise ValueError( - "Dimension mismatch in {0!s} and {1!s}.".format(str(self), str(val)) - ) - return ComboMap([self, val]) - - elif isinstance(val, np.ndarray): - if not self.shape[1] == "*" and not self.shape[1] == val.shape[0]: - raise ValueError( - "Dimension mismatch in {0!s} and np.ndarray{1!s}.".format( - str(self), str(val.shape) - ) - ) - return self._transform(val) - - elif isinstance(val, Zero): - return Zero() - - raise Exception( - "Unrecognized data type to multiply. Try a map or a numpy.ndarray!" - "You used a {} of type {}".format(val, type(val)) - ) - - def dot(self, map1): - r"""Multiply two mappings to create a :class:`simpeg.maps.ComboMap`. - - Let :math:`\mathbf{f}_1` and :math:`\mathbf{f}_2` represent two mapping functions. - Where :math:`\mathbf{m}` represents a set of input model parameters, - the ``dot`` method is used to create a combination mapping: - - .. math:: - u(\mathbf{m}) = f_2(f_1(\mathbf{m})) - - Where :math:`\mathbf{f_1} : M \rightarrow K_1` and acts on the - model first, and :math:`\mathbf{f_2} : K_1 \rightarrow K_2`, the combination - mapping :math:`\mathbf{u} : M \rightarrow K_2`. - - When using the **dot** method, the input argument *map1* represents the first - mapping that is be applied and *self* represents the second mapping - that is be applied. Therefore, the correct syntax for using this method is:: - - self.dot(map1) - - - Parameters - ---------- - map1 : - A SimPEG mapping object. - - Examples - -------- - Here we create a combination mapping that 1) projects a single scalar to - a vector space of length 5, then takes the natural exponent. - - >>> import numpy as np - >>> from simpeg.maps import ExpMap, Projection - - >>> nP1 = 1 - >>> nP2 = 5 - >>> ind = np.zeros(nP1, dtype=int) - - >>> projection_map = Projection(nP1, ind) - >>> projection_map.shape - (5, 1) - - >>> exp_map = ExpMap(nP=5) - >>> exp_map.shape - (5, 5) - - >>> combo_map = exp_map.dot(projection_map) - >>> combo_map.shape - (5, 1) - - >>> m = np.array([2]) - >>> combo_map * m - array([7.3890561, 7.3890561, 7.3890561, 7.3890561, 7.3890561]) - - """ - return self.__mul__(map1) - - def __matmul__(self, map1): - return self.__mul__(map1) - - __numpy_ufunc__ = True - - def __add__(self, map1): - return SumMap([self, map1]) # error-checking done inside of the SumMap - - def __str__(self): - return "{0!s}({1!s},{2!s})".format( - self.__class__.__name__, self.shape[0], self.shape[1] - ) - - def __len__(self): - return 1 - - @property - def mesh(self): - """ - The mesh used for the mapping - - Returns - ------- - discretize.base.BaseMesh or None - """ - return self._mesh - - @mesh.setter - def mesh(self, value): - if value is not None: - value = validate_type("mesh", value, discretize.base.BaseMesh, cast=False) - self._mesh = value - - @property - def is_linear(self): - """Determine whether or not this mapping is a linear operation. - - Returns - ------- - bool - """ - return True - - -class ComboMap(IdentityMap): - r"""Combination mapping constructed by joining a set of other mappings. - - A ``ComboMap`` is a single mapping object made by joining a set - of basic mapping operations by chaining them together, in order. - When creating a ``ComboMap``, the user provides a list of SimPEG mapping objects they wish to join. - The order of the mappings in this list is from last to first; i.e. - :math:`[\mathbf{f}_n , ... , \mathbf{f}_2 , \mathbf{f}_1]`. - - The combination mapping :math:`\mathbf{u}(\mathbf{m})` that acts on a - set of input model parameters :math:`\mathbf{m}` is defined as: - - .. math:: - \mathbf{u}(\mathbf{m}) = f_n(f_{n-1}(\cdots f_1(f_0(\mathbf{m})))) - - Note that any time that you create your own combination mapping, - be sure to test that the derivative is correct. - - Parameters - ---------- - maps : list of simpeg.maps.IdentityMap - A ``list`` of SimPEG mapping objects. The ordering of the mapping - objects in the ``list`` is from last applied to first applied! - - Examples - -------- - Here we create a combination mapping that 1) projects a single scalar to - a vector space of length 5, then takes the natural exponent. - - >>> import numpy as np - >>> from simpeg.maps import ExpMap, Projection, ComboMap - - >>> nP1 = 1 - >>> nP2 = 5 - >>> ind = np.zeros(nP1, dtype=int) - - >>> projection_map = Projection(nP1, ind) - >>> projection_map.shape - (5, 1) - - >>> exp_map = ExpMap(nP=5) - >>> exp_map.shape - (5, 5) - - Recall that the order of the mapping objects is from last applied - to first applied. - - >>> map_list = [exp_map, projection_map] - >>> combo_map = ComboMap(map_list) - >>> combo_map.shape - (5, 1) - - >>> m = np.array([2.]) - >>> combo_map * m - array([7.3890561, 7.3890561, 7.3890561, 7.3890561, 7.3890561]) - - """ - - def __init__(self, maps, **kwargs): - super().__init__(mesh=None, **kwargs) - - self.maps = [] - for ii, m in enumerate(maps): - assert isinstance(m, IdentityMap), "Unrecognized data type, " - "inherit from an IdentityMap or ComboMap!" - - if ( - ii > 0 - and not (self.shape[1] == "*" or m.shape[0] == "*") - and not self.shape[1] == m.shape[0] - ): - prev = self.maps[-1] - - raise ValueError( - "Dimension mismatch in map[{0!s}] ({1!s}, {2!s}) " - "and map[{3!s}] ({4!s}, {5!s}).".format( - prev.__class__.__name__, - prev.shape[0], - prev.shape[1], - m.__class__.__name__, - m.shape[0], - m.shape[1], - ) - ) - - if np.any([isinstance(m, SumMap), isinstance(m, IdentityMap)]): - self.maps += [m] - elif isinstance(m, ComboMap): - self.maps += m.maps - else: - raise ValueError("Map[{0!s}] not supported", m.__class__.__name__) - - @property - def shape(self): - r"""Dimensions of the mapping. - - For a list of SimPEG mappings [:math:`\mathbf{f}_n,...,\mathbf{f}_1`] - that have been joined to create a ``ComboMap``, this method returns - the dimensions of the combination mapping. Recall that the ordering - of the list of mappings is from last to first. - - Returns - ------- - (2) tuple of int - Dimensions of the mapping operator. - """ - return (self.maps[0].shape[0], self.maps[-1].shape[1]) - - @property - def nP(self): - r"""Number of parameters the mapping acts on. - - Returns - ------- - int - Number of parameters that the mapping acts on. - """ - return self.maps[-1].nP - - def _transform(self, m): - for map_i in reversed(self.maps): - m = map_i * m - return m - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the input parameters. - - Any time that you create your own combination mapping, - be sure to test that the derivative is correct. - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. - If the input argument *v* is not ``None``, the method returns - the derivative times the vector *v*. - - Notes - ----- - Let :math:`\mathbf{m}` be a set of model parameters and let - [:math:`\mathbf{f}_n,...,\mathbf{f}_1`] be the list of SimPEG mappings joined - to create a combination mapping. Recall that the list of mappings is ordered - from last applied to first applied. - - Where the combination mapping acting on the model parameters - can be expressed as: - - .. math:: - \mathbf{u}(\mathbf{m}) = f_n(f_{n-1}(\cdots f_1(f_0(\mathbf{m})))) - - The **deriv** method returns the derivative of :math:`\mathbf{u}` with respect - to the model parameters. To do this, we use the chain rule, i.e.: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = - \frac{\partial \mathbf{f_n}}{\partial \mathbf{f_{n-1}}} - \cdots - \frac{\partial \mathbf{f_2}}{\partial \mathbf{f_{1}}} - \frac{\partial \mathbf{f_1}}{\partial \mathbf{m}} - """ - - if v is not None: - deriv = v - else: - deriv = 1 - - mi = m - for map_i in reversed(self.maps): - deriv = map_i.deriv(mi) * deriv - mi = map_i * mi - return deriv - - def __str__(self): - return "ComboMap[{0!s}]({1!s},{2!s})".format( - " * ".join([m.__str__() for m in self.maps]), self.shape[0], self.shape[1] - ) - - def __len__(self): - return len(self.maps) - - @property - def is_linear(self): - return all(m.is_linear for m in self.maps) - - -class LinearMap(IdentityMap): - """A generalized linear mapping. - - A simple map that implements the linear mapping, - - >>> y = A @ x + b - - Parameters - ---------- - A : (M, N) array_like, optional - The matrix operator, can be any object that implements `__matmul__` - and has a `shape` attribute. - b : (M) array_like, optional - Additive part of the linear operation. - """ - - def __init__(self, A, b=None, **kwargs): - kwargs.pop("mesh", None) - kwargs.pop("nP", None) - super().__init__(**kwargs) - self.A = A - self.b = b - - @property - def A(self): - """The linear operator matrix. - - Returns - ------- - LinearOperator - Must support matrix multiplication and have a shape attribute. - """ - return self._A - - @A.setter - def A(self, value): - if not hasattr(value, "__matmul__"): - raise TypeError( - f"{repr(value)} does not implement the matrix multiplication operator." - ) - if not hasattr(value, "shape"): - raise TypeError(f"{repr(value)} does not have a shape attribute.") - self._A = value - self._nP = value.shape[1] - self._shape = value.shape - - @property - def shape(self): - return self._shape - - @property - def b(self): - """Added part of the linear operation. - - Returns - ------- - numpy.ndarray - """ - return self._b - - @b.setter - def b(self, value): - if value is not None: - value = validate_ndarray_with_shape("b", value, shape=(self.shape[0],)) - self._b = value - - def _transform(self, m): - if self.b is None: - return self.A @ m - return self.A @ m + self.b - - def deriv(self, m, v=None): - if v is None: - return self.A - return self.A @ v - - -class Projection(IdentityMap): - r"""Projection mapping. - - ``Projection`` mapping can be used to project and/or rearange model - parameters. For a set of model parameter :math:`\mathbf{m}`, - the mapping :math:`\mathbf{u}(\mathbf{m})` can be defined by a linear - projection matrix :math:`\mathbf{P}` acting on the model, i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} - - The number of model parameters the mapping acts on is - defined by *nP*. Projection and/or rearrangement of the parameters - is defined by *index*. Thus the dimensions of the mapping is - (*nInd*, *nP*). - - Parameters - ---------- - nP : int - Number of model parameters the mapping acts on - index : numpy.ndarray of int - Indexes defining the projection from the model space - - Examples - -------- - Here we define a mapping that rearranges and projects 2 model - parameters to a vector space spanning 4 parameters. - - >>> from simpeg.maps import Projection - >>> import numpy as np - - >>> nP = 2 - >>> index = np.array([1, 0, 1, 0], dtype=int) - >>> mapping = Projection(nP, index) - - >>> m = np.array([6, 8]) - >>> mapping * m - array([8, 6, 8, 6]) - - """ - - def __init__(self, nP, index, **kwargs): - assert isinstance( - index, (np.ndarray, slice, list) - ), "index must be a np.ndarray or slice, not {}".format(type(index)) - super(Projection, self).__init__(nP=nP, **kwargs) - - if isinstance(index, slice): - index = list(range(*index.indices(self.nP))) - - if isinstance(index, np.ndarray): - if index.dtype is np.dtype("bool"): - index = np.where(index)[0] - - self.index = index - self._shape = nI, nP = len(self.index), self.nP - - assert max(index) < nP, "maximum index must be less than {}".format(nP) - - # sparse projection matrix - self.P = sp.csr_matrix((np.ones(nI), (range(nI), self.index)), shape=(nI, nP)) - - def _transform(self, m): - return m[self.index] - - @property - def shape(self): - r"""Dimensions of the mapping. - - Returns - ------- - tuple - Where *nP* is the number of parameters the mapping acts on and - *nInd* is the length of the vector defining the mapping, the - dimensions of the mapping operator is a tuple of the - form (*nInd*, *nP*). - """ - return self._shape - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the input parameters. - - Let :math:`\mathbf{m}` be a set of model parameters and let :math:`\mathbf{P}` - be a matrix denoting the projection mapping. Where the projection mapping acting - on the model parameters can be expressed as: - - .. math:: - \mathbf{u} = \mathbf{P m}, - - the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect - to the model parameters; i.e.: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} - - Note that in this case, **deriv** simply returns a sparse projection matrix. - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - - if v is not None: - return self.P * v - return self.P - - -class SumMap(ComboMap): - """Combination map constructed by summing multiple mappings - to the same vector space. - - A map to add model parameters contributing to the - forward operation e.g. F(m) = F(g(x) + h(y)) - - Assumes that the model vectors defined by g(x) and h(y) - are equal in length. - Allows to assume different things about the model m: - i.e. parametric + voxel models - - Parameters - ---------- - maps : list - A list of SimPEG mapping objects that are being summed. - Each mapping object in the list must act on the same number - of model parameters and must map to the same vector space! - """ - - def __init__(self, maps, **kwargs): - maps = validate_list_of_types("maps", maps, IdentityMap) - - # skip ComboMap's init - super(ComboMap, self).__init__(mesh=None, **kwargs) - - self.maps = [] - for ii, m in enumerate(maps): - if not isinstance(m, IdentityMap): - raise TypeError( - "Unrecognized data type {}, inherit from an " - "IdentityMap!".format(type(m)) - ) - - if ( - ii > 0 - and not (self.shape == "*" or m.shape == "*") - and not self.shape == m.shape - ): - raise ValueError( - "Dimension mismatch in map[{0!s}] ({1!s}, {2!s}) " - "and map[{3!s}] ({4!s}, {5!s}).".format( - self.maps[0].__class__.__name__, - self.maps[0].shape[0], - self.maps[0].shape[1], - m.__class__.__name__, - m.shape[0], - m.shape[1], - ) - ) - - self.maps += [m] - - @property - def shape(self): - """Dimensions of the mapping. - - Returns - ------- - tuple - The dimensions of the mapping. A tuple of the form (``int``,``int``) - """ - return (self.maps[0].shape[0], self.maps[0].shape[1]) - - @property - def nP(self): - r"""Number of parameters the combined mapping acts on. - - Returns - ------- - int - Number of parameters that the mapping acts on. - """ - return self.maps[-1].shape[1] - - def _transform(self, m): - for ii, map_i in enumerate(self.maps): - m0 = m.copy() - m0 = map_i * m0 - - if ii == 0: - mout = m0 - else: - mout += m0 - return mout - - def deriv(self, m, v=None): - """Derivative of mapping with respect to the input parameters - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - - for ii, map_i in enumerate(self.maps): - m0 = m.copy() - - if v is not None: - deriv = v - else: - deriv = sp.eye(self.nP) - - deriv = map_i.deriv(m0, v=deriv) - if ii == 0: - sumDeriv = deriv - else: - sumDeriv += deriv - - return sumDeriv - - -class SurjectUnits(IdentityMap): - r"""Surjective mapping to all mesh cells. - - Let :math:`\mathbf{m}` be a model that contains a physical property value - for *nP* geological units. ``SurjectUnits`` is used to construct a surjective - mapping that projects :math:`\mathbf{m}` to the set of voxel cells defining a mesh. - As a result, the mapping :math:`\mathbf{u(\mathbf{m})}` is defined as - a projection matrix :math:`\mathbf{P}` acting on the model. Thus: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} - - - The mapping therefore has dimensions (*mesh.nC*, *nP*). - - Parameters - ---------- - indices : (nP) list of (mesh.nC) numpy.ndarray - Each entry in the :class:`list` is a boolean :class:`numpy.ndarray` of length - *mesh.nC* that assigns the corresponding physical property value to the - appropriate mesh cells. - - Examples - -------- - For this example, we have a model that defines the property values - for two units. Using ``SurjectUnit``, we construct the mapping from - the model to a 1D mesh where the 1st unit's value is assigned to - all cells whose centers are located at *x < 0* and the 2nd unit's value - is assigned to all cells whose centers are located at *x > 0*. - - >>> from simpeg.maps import SurjectUnits - >>> from discretize import TensorMesh - >>> import numpy as np - - >>> nP = 8 - >>> mesh = TensorMesh([np.ones(nP)], 'C') - >>> unit_1_ind = mesh.cell_centers < 0 - - >>> indices_list = [unit_1_ind, ~unit_1_ind] - >>> mapping = SurjectUnits(indices_list, nP=nP) - - >>> m = np.r_[0.01, 0.05] - >>> mapping * m - array([0.01, 0.01, 0.01, 0.01, 0.05, 0.05, 0.05, 0.05]) - - """ - - def __init__(self, indices, **kwargs): - super().__init__(**kwargs) - self.indices = indices - - @property - def indices(self): - """List assigning a given physical property to specific model cells. - - Each entry in the :class:`list` is a boolean :class:`numpy.ndarray` of length - *mesh.nC* that assigns the corresponding physical property value to the - appropriate mesh cells. - - Returns - ------- - (nP) list of (mesh.n_cells) numpy.ndarray - """ - return self._indices - - @indices.setter - def indices(self, values): - values = validate_type("indices", values, list) - mesh = self.mesh - last_shape = None - for i in range(len(values)): - if mesh is not None: - values[i] = validate_active_indices( - "indices", values[i], self.mesh.n_cells - ) - else: - values[i] = validate_ndarray_with_shape( - "indices", values[i], shape=("*",), dtype=int - ) - if last_shape is not None and last_shape != values[i].shape: - raise ValueError("all indicies must have the same shape.") - last_shape = values[i].shape - self._indices = values - - @property - def P(self): - """ - Projection matrix from model parameters to mesh cells. - """ - if getattr(self, "_P", None) is None: - # sparse projection matrix - row = [] - col = [] - val = [] - for ii, ind in enumerate(self.indices): - col += [ii] * ind.sum() - row += np.where(ind)[0].tolist() - val += [1] * ind.sum() - - self._P = sp.csr_matrix( - (val, (row, col)), shape=(len(self.indices[0]), self.nP) - ) - - # self._P = sp.block_diag([P for ii in range(self.nBlock)]) - - return self._P - - def _transform(self, m): - return self.P * m - - @property - def nP(self): - r"""Number of parameters the mapping acts on. - - Returns - ------- - int - Number of parameters that the mapping acts on. - """ - return len(self.indices) - - @property - def shape(self): - """Dimensions of the mapping - - Returns - ------- - tuple - Dimensions of the mapping. Where *nP* is the number of parameters the - mapping acts on and *mesh.nC* is the number of cells the corresponding - mesh, the return is a tuple of the form (*mesh.nC*, *nP*). - """ - # return self.n_block*len(self.indices[0]), self.n_block*len(self.indices) - return (len(self.indices[0]), self.nP) - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the input parameters. - - Let :math:`\mathbf{m}` be a set of model parameters. The surjective mapping - can be defined as a sparse projection matrix :math:`\mathbf{P}`. Therefore - we can define the surjective mapping acting on the model parameters as: - - .. math:: - \mathbf{u} = \mathbf{P m}, - - the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect - to the model parameters; i.e.: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} - - Note that in this case, **deriv** simply returns a sparse projection matrix. - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. - If the input argument *v* is not ``None``, the method returns - the derivative times the vector *v*. - """ - - if v is not None: - return self.P * v - return self.P - - -class SphericalSystem(IdentityMap): - r"""Mapping vectors from spherical to Cartesian coordinates. - - Let :math:`\mathbf{m}` be a model containing the amplitudes - (:math:`\mathbf{a}`), azimuthal angles (:math:`\mathbf{t}`) - and radial angles (:math:`\mathbf{p}`) for a set of vectors - in spherical space such that: - - .. math:: - \mathbf{m} = \begin{bmatrix} \mathbf{a} \\ \mathbf{t} \\ \mathbf{p} \end{bmatrix} - - ``SphericalSystem`` constructs a mapping :math:`\mathbf{u}(\mathbf{m}) - that converts the set of vectors in spherical coordinates to - their representation in Cartesian coordinates, i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = \begin{bmatrix} \mathbf{v_x} \\ \mathbf{v_y} \\ \mathbf{v_z} \end{bmatrix} - - where :math:`\mathbf{v_x}`, :math:`\mathbf{v_y}` and :math:`\mathbf{v_z}` - store the x, y and z components of the vectors, respectively. - - Using the *mesh* or *nP* input arguments, the dimensions of the corresponding - mapping operator can be permanently set; i.e. (*3\*mesh.nC*, *3\*mesh.nC*) or (*nP*, *nP*). - However if both input arguments *mesh* and *nP* are ``None``, the shape of - mapping operator is arbitrary and can act on any vector whose length - is a multiple of 3; i.e. has shape (``*``, ``*``). - - Notes - ----- - - In Cartesian space, the components of each vector are defined as - - .. math:: - \mathbf{v} = (v_x, v_y, v_z) - - In spherical coordinates, vectors are is defined as: - - .. math:: - \mathbf{v^\prime} = (a, t, p) - - where - - - :math:`a` is the amplitude of the vector - - :math:`t` is the azimuthal angle defined positive from vertical - - :math:`p` is the radial angle defined positive CCW from Easting - - Parameters - ---------- - mesh : discretize.BaseMesh - The number of parameters accepted by the mapping is set to equal - *3\*mesh.nC* . - nP : int - Set the number of parameters accepted by the mapping directly. Used if the - number of parameters is known. Used generally when the number of parameters - is not equal to the number of cells in a mesh. - """ - - def __init__(self, mesh=None, nP=None, **kwargs): - if nP is not None: - assert nP % 3 == 0, "Number of parameters must be a multiple of 3" - super().__init__(mesh, nP, **kwargs) - self.model = None - - def sphericalDeriv(self, model): - if getattr(self, "model", None) is None: - self.model = model - - if getattr(self, "_sphericalDeriv", None) is None or not all( - self.model == model - ): - self.model = model - - # Do a double projection to make sure the parameters are bounded - m_xyz = mat_utils.spherical2cartesian(model.reshape((-1, 3), order="F")) - m_atp = mat_utils.cartesian2spherical( - m_xyz.reshape((-1, 3), order="F") - ).reshape((-1, 3), order="F") - - nC = m_atp[:, 0].shape[0] - - dm_dx = sp.hstack( - [ - sp.diags(np.cos(m_atp[:, 1]) * np.cos(m_atp[:, 2]), 0), - sp.diags( - -m_atp[:, 0] * np.sin(m_atp[:, 1]) * np.cos(m_atp[:, 2]), 0 - ), - sp.diags( - -m_atp[:, 0] * np.cos(m_atp[:, 1]) * np.sin(m_atp[:, 2]), 0 - ), - ] - ) - - dm_dy = sp.hstack( - [ - sp.diags(np.cos(m_atp[:, 1]) * np.sin(m_atp[:, 2]), 0), - sp.diags( - -m_atp[:, 0] * np.sin(m_atp[:, 1]) * np.sin(m_atp[:, 2]), 0 - ), - sp.diags( - m_atp[:, 0] * np.cos(m_atp[:, 1]) * np.cos(m_atp[:, 2]), 0 - ), - ] - ) - - dm_dz = sp.hstack( - [ - sp.diags(np.sin(m_atp[:, 1]), 0), - sp.diags(m_atp[:, 0] * np.cos(m_atp[:, 1]), 0), - csr((nC, nC)), - ] - ) - - self._sphericalDeriv = sp.vstack([dm_dx, dm_dy, dm_dz]) - - return self._sphericalDeriv - - def _transform(self, model): - return mat_utils.spherical2cartesian(model.reshape((-1, 3), order="F")) - - def inverse(self, u): - r"""Maps vectors in Cartesian coordinates to spherical coordinates. - - Let :math:`\mathbf{v_x}`, :math:`\mathbf{v_y}` and :math:`\mathbf{v_z}` - store the x, y and z components of a set of vectors in Cartesian - coordinates such that: - - .. math:: - \mathbf{u} = \begin{bmatrix} \mathbf{x} \\ \mathbf{y} \\ \mathbf{z} \end{bmatrix} - - The inverse mapping recovers the vectors in spherical coordinates, i.e.: - - .. math:: - \mathbf{m}(\mathbf{u}) = \begin{bmatrix} \mathbf{a} \\ \mathbf{t} \\ \mathbf{p} \end{bmatrix} - - where :math:`\mathbf{a}` are the amplitudes, :math:`\mathbf{t}` are the - azimuthal angles and :math:`\mathbf{p}` are the radial angles. - - Parameters - ---------- - u : numpy.ndarray - The x, y and z components of a set of vectors in Cartesian coordinates. - If the mapping is defined for a mesh, the numpy.ndarray has length - *3\*mesh.nC* . - - Returns - ------- - numpy.ndarray - The amplitudes (:math:`\mathbf{a}`), azimuthal angles (:math:`\mathbf{t}`) - and radial angles (:math:`\mathbf{p}`) for the set of vectors in spherical - coordinates. If the mapping is defined for a mesh, the numpy.ndarray has length - *3\*mesh.nC* . - """ - return mat_utils.cartesian2spherical(u.reshape((-1, 3), order="F")) - - @property - def shape(self): - r"""Dimensions of the mapping - - The dimensions of the mesh depend on the input arguments used - during instantiation. If *mesh* is used to define the - mapping, the shape of mapping operator is (*3\*mesh.nC*, *3\*mesh.nC*). - If *nP* is used to define the identity map, the mapping operator - has dimensions (*nP*, *nP*). If *mesh* and *nP* were ``None`` when - instantiating, the mapping has dimensions (``*``, ``*``) and may - act on a vector whose length is a multiple of 3. - - Returns - ------- - tuple - Dimensions of the mapping operator. If the dimensions of - the mapping are set, the return is a tuple (``int``,``int``). - If the mapping can act on a vector of arbitrary length, the - return is a tuple (``*``, ``*``). - """ - # return self.n_block*len(self.indices[0]), self.n_block*len(self.indices) - return (self.nP, self.nP) - - def deriv(self, m, v=None): - """Derivative of mapping with respect to the input parameters - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - - if v is not None: - return self.sphericalDeriv(m) * v - return self.sphericalDeriv(m) - - @property - def is_linear(self): - return False - - -class Wires(object): - r"""Mapping class for organizing multiple parameter types into a single model. - - Let :math:`\mathbf{p_1}` and :math:`\mathbf{p_2}` be vectors that - contain the parameter values for two different parameter types; for example, - electrical conductivity and magnetic permeability. Here, all parameters - are organized into a single model :math:`\mathbf{m}` of the form: - - .. math:: - \mathbf{m} = \begin{bmatrix} \mathbf{p_1} \\ \mathbf{p_2} \end{bmatrix} - - The ``Wires`` class constructs and applies the basic projection mappings - for extracting the values of a particular parameter type from the model. - For example: - - .. math:: - \mathbf{p_1} = \mathbf{P_{\! 1} m} - - where :math:`\mathbf{P_1}` is the projection matrix that extracts parameters - :math:`\mathbf{p_1}` from the complete set of model parameters :math:`\mathbf{m}`. - Likewise, there is a projection matrix for extracting :math:`\mathbf{p_2}`. - This can be extended to a model that containing more than 2 parameter types. - - Parameters - ---------- - args : tuple - Each input argument is a tuple (``str``, ``int``) that provides the name - and number of parameters for a given parameters type. - - Examples - -------- - Here we construct a wire mapping for a model where there - are two parameters types. Note that the number of parameters - of each type does not need to be the same. - - >>> from simpeg.maps import Wires, ReciprocalMap - >>> import numpy as np - - >>> p1 = np.r_[4.5, 2.7, 6.9, 7.1, 1.2] - >>> p2 = np.r_[10., 2., 5.]**-1 - >>> nP1 = len(p1) - >>> nP2 = len(p2) - >>> m = np.r_[p1, p2] - >>> m - array([4.5, 2.7, 6.9, 7.1, 1.2, 0.1, 0.5, 0.2]) - - Here we construct the wire map. The user provides a name - and the number of parameters for each type. The name - provided becomes the name of the method for constructing - the projection mapping. - - >>> wire_map = Wires(('name_1', nP1), ('name_2', nP2)) - - Here, we extract the values for the first parameter type. - - >>> wire_map.name_1 * m - array([4.5, 2.7, 6.9, 7.1, 1.2]) - - And here, we extract the values for the second parameter - type then apply a reciprocal mapping. - - >>> reciprocal_map = ReciprocalMap() - >>> reciprocal_map * wire_map.name_2 * m - array([10., 2., 5.]) - - """ - - def __init__(self, *args): - for arg in args: - assert ( - isinstance(arg, tuple) - and len(arg) == 2 - and isinstance(arg[0], str) - and - # TODO: this should be extended to a slice. - isinstance(arg[1], (int, np.integer)) - ), ( - "Each wire needs to be a tuple: (name, length). " - "You provided: {}".format(arg) - ) - - self._nP = int(np.sum([w[1] for w in args])) - start = 0 - maps = [] - for arg in args: - wire = Projection(self.nP, slice(start, start + arg[1])) - setattr(self, arg[0], wire) - maps += [(arg[0], wire)] - start += arg[1] - self.maps = maps - - self._tuple = namedtuple("Model", [w[0] for w in args]) - - def __mul__(self, val): - assert isinstance(val, np.ndarray) - split = [] - for _, w in self.maps: - split += [w * val] - return self._tuple(*split) - - @property - def nP(self): - r"""Number of parameters the mapping acts on. - - Returns - ------- - int - Number of parameters that the mapping acts on. - """ - return self._nP - - -class SelfConsistentEffectiveMedium(IdentityMap): - r""" - Two phase self-consistent effective medium theory mapping for - ellipsoidal inclusions. The inversion model is the concentration - (volume fraction) of the phase 2 material. - - The inversion model is :math:`\varphi`. We solve for :math:`\sigma` - given :math:`\sigma_0`, :math:`\sigma_1` and :math:`\varphi` . Each of - the following are implicit expressions of the effective conductivity. - They are solved using a fixed point iteration. - - **Spherical Inclusions** - - If the shape of the inclusions are spheres, we use - - .. math:: - - \sum_{j=1}^N (\sigma^* - \sigma_j)R^{j} = 0 - - where :math:`j=[1,N]` is the each material phase, and N is the number - of phases. Currently, the implementation is only set up for 2 phase - materials, so we solve - - .. math:: - - (1-\\varphi)(\sigma - \sigma_0)R^{(0)} + \varphi(\sigma - \sigma_1)R^{(1)} = 0. - - Where :math:`R^{(j)}` is given by - - .. math:: - - R^{(j)} = \left[1 + \frac{1}{3}\frac{\sigma_j - \sigma}{\sigma} \right]^{-1}. - - **Ellipsoids** - - .. todo:: - - Aligned Ellipsoids have not yet been implemented, only randomly - oriented ellipsoids - - If the inclusions are aligned ellipsoids, we solve - - .. math:: - - \sum_{j=1}^N \varphi_j (\Sigma^* - \sigma_j\mathbf{I}) \mathbf{R}^{j, *} = 0 - - where - - .. math:: - - \mathbf{R}^{(j, *)} = \left[ \mathbf{I} + \mathbf{A}_j {\Sigma^{*}}^{-1}(\sigma_j \mathbf{I} - \Sigma^*) \\right]^{-1} - - and the depolarization tensor :math:`\mathbf{A}_j` is given by - - .. math:: - - \mathbf{A}^* = \left[\begin{array}{ccc} - Q & 0 & 0 \\ - 0 & Q & 0 \\ - 0 & 0 & 1-2Q - \end{array}\right] - - for a spheroid aligned along the z-axis. For an oblate spheroid - (:math:`\alpha < 1`, pancake-like) - - .. math:: - - Q = \frac{1}{2}\left( - 1 + \frac{1}{\alpha^2 - 1} \left[ - 1 - \frac{1}{\chi}\tan^{-1}(\chi) - \right] - \right) - - where - - .. math:: - - \chi = \sqrt{\frac{1}{\alpha^2} - 1} - - - For reference, see - `Torquato (2002), Random Heterogeneous Materials `_ - - - """ - - def __init__( - self, - mesh=None, - nP=None, - sigma0=None, - sigma1=None, - alpha0=1.0, - alpha1=1.0, - orientation0="z", - orientation1="z", - random=True, - rel_tol=1e-3, - maxIter=50, - **kwargs, - ): - self._sigstart = None - self.sigma0 = sigma0 - self.sigma1 = sigma1 - self.alpha0 = alpha0 - self.alpha1 = alpha1 - self.orientation0 = orientation0 - self.orientation1 = orientation1 - self.random = random - self.rel_tol = rel_tol - self.maxIter = maxIter - super(SelfConsistentEffectiveMedium, self).__init__(mesh, nP, **kwargs) - - @property - def sigma0(self): - """Physical property value for phase-0 material. - - Returns - ------- - float - """ - return self._sigma0 - - @sigma0.setter - def sigma0(self, value): - self._sigma0 = validate_float("sigma0", value, min_val=0.0) - - @property - def sigma1(self): - """Physical property value for phase-1 material. - - Returns - ------- - float - """ - return self._sigma1 - - @sigma1.setter - def sigma1(self, value): - self._sigma1 = validate_float("sigma1", value, min_val=0.0) - - @property - def alpha0(self): - """Aspect ratio of the phase-0 ellipsoids. - - Returns - ------- - float - """ - return self._alpha0 - - @alpha0.setter - def alpha0(self, value): - self._alpha0 = validate_float("alpha0", value, min_val=0.0) - - @property - def alpha1(self): - """Aspect ratio of the phase-1 ellipsoids. - - Returns - ------- - float - """ - return self._alpha1 - - @alpha1.setter - def alpha1(self, value): - self._alpha1 = validate_float("alpha1", value, min_val=0.0) - - @property - def orientation0(self): - """Orientation of the phase-0 inclusions. - - Returns - ------- - numpy.ndarray - """ - return self._orientation0 - - @orientation0.setter - def orientation0(self, value): - self._orientation0 = validate_direction("orientation0", value, dim=3) - - @property - def orientation1(self): - """Orientation of the phase-0 inclusions. - - Returns - ------- - numpy.ndarray - """ - return self._orientation1 - - @orientation1.setter - def orientation1(self, value): - self._orientation1 = validate_direction("orientation1", value, dim=3) - - @property - def random(self): - """Are the inclusions randomly oriented (True) or preferentially aligned (False)? - - Returns - ------- - bool - """ - return self._random - - @random.setter - def random(self, value): - self._random = validate_type("random", value, bool) - - @property - def rel_tol(self): - """relative tolerance for convergence for the fixed-point iteration. - - Returns - ------- - float - """ - return self._rel_tol - - @rel_tol.setter - def rel_tol(self, value): - self._rel_tol = validate_float( - "rel_tol", value, min_val=0.0, inclusive_min=False - ) - - @property - def maxIter(self): - """Maximum number of iterations for the fixed point iteration calculation. - - Returns - ------- - int - """ - return self._maxIter - - @maxIter.setter - def maxIter(self, value): - self._maxIter = validate_integer("maxIter", value, min_val=0) - - @property - def tol(self): - """ - absolute tolerance for the convergence of the fixed point iteration - calc - """ - if getattr(self, "_tol", None) is None: - self._tol = self.rel_tol * min(self.sigma0, self.sigma1) - return self._tol - - @property - def sigstart(self): - """ - first guess for sigma - """ - return self._sigstart - - @sigstart.setter - def sigstart(self, value): - if value is not None: - value = validate_float("sigstart", value) - self._sigstart = value - - def wiener_bounds(self, phi1): - """Define Wenner Conductivity Bounds - - See Torquato, 2002 - """ - phi0 = 1.0 - phi1 - sigWup = phi0 * self.sigma0 + phi1 * self.sigma1 - sigWlo = 1.0 / (phi0 / self.sigma0 + phi1 / self.sigma1) - W = np.array([sigWlo, sigWup]) - - return W - - def hashin_shtrikman_bounds(self, phi1): - """Hashin Shtrikman bounds - - See Torquato, 2002 - """ - # TODO: this should probably exsist on its own as a util - - phi0 = 1.0 - phi1 - sigWu = self.wiener_bounds(phi1)[1] - sig_tilde = phi0 * self.sigma1 + phi1 * self.sigma0 - - sigma_min = np.min([self.sigma0, self.sigma1]) - sigma_max = np.max([self.sigma0, self.sigma1]) - - sigHSlo = sigWu - ( - (phi0 * phi1 * (self.sigma0 - self.sigma1) ** 2) - / (sig_tilde + 2 * sigma_max) - ) - sigHSup = sigWu - ( - (phi0 * phi1 * (self.sigma0 - self.sigma1) ** 2) - / (sig_tilde + 2 * sigma_min) - ) - - return np.array([sigHSlo, sigHSup]) - - def hashin_shtrikman_bounds_anisotropic(self, phi1): - """Hashin Shtrikman bounds for anisotropic media - - See Torquato, 2002 - """ - phi0 = 1.0 - phi1 - sigWu = self.wiener_bounds(phi1)[1] - - sigma_min = np.min([self.sigma0, self.sigma1]) - sigma_max = np.max([self.sigma0, self.sigma1]) - - phi_min = phi0 if self.sigma1 > self.sigma0 else phi1 - phi_max = phi1 if self.sigma1 > self.sigma0 else phi0 - - amax = ( - -phi0 - * phi1 - * self.getA( - self.alpha1 if self.sigma1 > self.sigma0 else self.alpha0, - self.orientation1 if self.sigma1 > self.sigma0 else self.orientation0, - ) - ) - I = np.eye(3) - - sigHSlo = sigWu * I + ( - (sigma_min - sigma_max) ** 2 - * amax - * np.linalg.inv(sigma_min * I + (sigma_min - sigma_max) / phi_max * amax) - ) - sigHSup = sigWu * I + ( - (sigma_max - sigma_min) ** 2 - * amax - * np.linalg.inv(sigma_max * I + (sigma_max - sigma_min) / phi_min * amax) - ) - - return [sigHSlo, sigHSup] - - def getQ(self, alpha): - """Geometric factor in the depolarization tensor""" - if alpha < 1.0: # oblate spheroid - chi = np.sqrt((1.0 / alpha**2.0) - 1) - return ( - 1.0 / 2.0 * (1 + 1.0 / (alpha**2.0 - 1) * (1.0 - np.arctan(chi) / chi)) - ) - elif alpha > 1.0: # prolate spheroid - chi = np.sqrt(1 - (1.0 / alpha**2.0)) - return ( - 1.0 - / 2.0 - * ( - 1 - + 1.0 - / (alpha**2.0 - 1) - * (1.0 - 1.0 / (2.0 * chi) * np.log((1 + chi) / (1 - chi))) - ) - ) - elif alpha == 1: # sphere - return 1.0 / 3.0 - - def getA(self, alpha, orientation): - """Depolarization tensor""" - Q = self.getQ(alpha) - A = np.diag([Q, Q, 1 - 2 * Q]) - R = rotation_matrix_from_normals(np.r_[0.0, 0.0, 1.0], orientation) - return (R.T).dot(A).dot(R) - - def getR(self, sj, se, alpha, orientation=None): - """Electric field concentration tensor""" - if self.random is True: # isotropic - if alpha == 1.0: - return 3.0 * se / (2.0 * se + sj) - Q = self.getQ(alpha) - return ( - se - / 3.0 - * (2.0 / (se + Q * (sj - se)) + 1.0 / (sj - 2.0 * Q * (sj - se))) - ) - else: # anisotropic - if orientation is None: - raise Exception("orientation must be provided if random=False") - I = np.eye(3) - seinv = np.linalg.inv(se) - Rinv = I + self.getA(alpha, orientation) * seinv * (sj * I - se) - return np.linalg.inv(Rinv) - - def getdR(self, sj, se, alpha, orientation=None): - """ - Derivative of the electric field concentration tensor with respect - to the concentration of the second phase material. - """ - if self.random is True: - if alpha == 1.0: - return 3.0 / (2.0 * se + sj) - 6.0 * se / (2.0 * se + sj) ** 2 - Q = self.getQ(alpha) - return ( - 1 - / 3 - * ( - 2.0 / (se + Q * (sj - se)) - + 1.0 / (sj - 2.0 * Q * (sj - se)) - + se - * ( - -2 * (1 - Q) / (se + Q * (sj - se)) ** 2 - - 2 * Q / (sj - 2.0 * Q * (sj - se)) ** 2 - ) - ) - ) - else: - if orientation is None: - raise Exception("orientation must be provided if random=False") - raise NotImplementedError - - def _sc2phaseEMTSpheroidstransform(self, phi1): - """ - Self Consistent Effective Medium Theory Model Transform, - alpha = aspect ratio (c/a <= 1) - """ - - if not (np.all(0 <= phi1) and np.all(phi1 <= 1)): - warnings.warn("there are phis outside bounds of 0 and 1", stacklevel=2) - phi1 = np.median(np.c_[phi1 * 0, phi1, phi1 * 0 + 1.0]) - - phi0 = 1.0 - phi1 - - # starting guess - if self.sigstart is None: - sige1 = np.mean(self.wiener_bounds(phi1)) - else: - sige1 = self.sigstart - - if self.random is False: - sige1 = sige1 * np.eye(3) - - for _ in range(self.maxIter): - R0 = self.getR(self.sigma0, sige1, self.alpha0, self.orientation0) - R1 = self.getR(self.sigma1, sige1, self.alpha1, self.orientation1) - - den = phi0 * R0 + phi1 * R1 - num = phi0 * self.sigma0 * R0 + phi1 * self.sigma1 * R1 - - if self.random is True: - sige2 = num / den - relerr = np.abs(sige2 - sige1) - else: - sige2 = num * np.linalg.inv(den) - relerr = np.linalg.norm(np.abs(sige2 - sige1).flatten(), np.inf) - - if np.all(relerr <= self.tol): - if self.sigstart is None: - self._sigstart = ( - sige2 # store as a starting point for the next time around - ) - return sige2 - - sige1 = sige2 - # TODO: make this a proper warning, and output relevant info (sigma0, sigma1, phi, sigstart, and relerr) - warnings.warn("Maximum number of iterations reached", stacklevel=2) - - return sige2 - - def _sc2phaseEMTSpheroidsinversetransform(self, sige): - R0 = self.getR(self.sigma0, sige, self.alpha0, self.orientation0) - R1 = self.getR(self.sigma1, sige, self.alpha1, self.orientation1) - - num = -(self.sigma0 - sige) * R0 - den = (self.sigma1 - sige) * R1 - (self.sigma0 - sige) * R0 - - return num / den - - def _sc2phaseEMTSpheroidstransformDeriv(self, sige, phi1): - phi0 = 1.0 - phi1 - - R0 = self.getR(self.sigma0, sige, self.alpha0, self.orientation0) - R1 = self.getR(self.sigma1, sige, self.alpha1, self.orientation1) - - dR0 = self.getdR(self.sigma0, sige, self.alpha0, self.orientation0) - dR1 = self.getdR(self.sigma1, sige, self.alpha1, self.orientation1) - - num = (sige - self.sigma0) * R0 - (sige - self.sigma1) * R1 - den = phi0 * (R0 + (sige - self.sigma0) * dR0) + phi1 * ( - R1 + (sige - self.sigma1) * dR1 - ) - - return sdiag(num / den) - - def _transform(self, m): - return self._sc2phaseEMTSpheroidstransform(m) - - def deriv(self, m): - """ - Derivative of the effective conductivity with respect to the - volume fraction of phase 2 material - """ - sige = self._transform(m) - return self._sc2phaseEMTSpheroidstransformDeriv(sige, m) - - def inverse(self, sige): - """ - Compute the concentration given the effective conductivity - """ - return self._sc2phaseEMTSpheroidsinversetransform(sige) - - @property - def is_linear(self): - return False - - -############################################################################### -# # -# Mesh Independent Maps # -# # -############################################################################### - - -class ExpMap(IdentityMap): - r"""Mapping that computes the natural exponentials of the model parameters. - - Where :math:`\mathbf{m}` is a set of model parameters, ``ExpMap`` creates - a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the natural exponential - of every element in :math:`\mathbf{m}`; i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = exp(\mathbf{m}) - - ``ExpMap`` is commonly used when working with physical properties whose values - span many orders of magnitude (e.g. the electrical conductivity :math:`\sigma`). - By using ``ExpMap``, we can invert for a model that represents the natural log - of a set of physical property values, i.e. when :math:`m = log(\sigma)` - - Parameters - ---------- - mesh : discretize.BaseMesh - The number of parameters accepted by the mapping is set to equal the number - of mesh cells. - nP : int - Set the number of parameters accepted by the mapping directly. Used if the - number of parameters is known. Used generally when the number of parameters - is not equal to the number of cells in a mesh. - """ - - def __init__(self, mesh=None, nP=None, **kwargs): - super().__init__(mesh=mesh, nP=nP, **kwargs) - - def _transform(self, m): - return np.exp(mkvc(m)) - - def inverse(self, D): - r"""Apply the inverse of the exponential mapping to an array. - - For the exponential mapping :math:`\mathbf{u}(\mathbf{m})`, the - inverse mapping on a variable :math:`\mathbf{x}` is performed by taking - the natural logarithms of elements, i.e.: - - .. math:: - \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = log(\mathbf{x}) - - Parameters - ---------- - D : numpy.ndarray - A set of input values - - Returns - ------- - numpy.ndarray - A :class:`numpy.ndarray` containing result of applying the - inverse mapping to the elements in *D*; which in this case - is the natural logarithm. - """ - return np.log(mkvc(D)) - - def deriv(self, m, v=None): - r"""Derivative of mapping with respect to the input parameters. - - For a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the natural - exponential function for each parameter in the model :math:`\mathbf{m}`, - i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = exp(\mathbf{m}), - - the derivative of the mapping with respect to the model is a diagonal - matrix of the form: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} - = \textrm{diag} \big ( exp(\mathbf{m}) \big ) - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - deriv = sdiag(np.exp(mkvc(m))) - if v is not None: - return deriv * v - return deriv - - @property - def is_linear(self): - return False - - -class ReciprocalMap(IdentityMap): - r"""Mapping that computes the reciprocals of the model parameters. - - Where :math:`\mathbf{m}` is a set of model parameters, ``ReciprocalMap`` - creates a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the - reciprocal of every element in :math:`\mathbf{m}`; - i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{m}^{-1} - - Parameters - ---------- - mesh : discretize.BaseMesh - The number of parameters accepted by the mapping is set to equal the number - of mesh cells. - nP : int - Set the number of parameters accepted by the mapping directly. Used if the - number of parameters is known. Used generally when the number of parameters - is not equal to the number of cells in a mesh. - """ - - def __init__(self, mesh=None, nP=None, **kwargs): - super().__init__(mesh=mesh, nP=nP, **kwargs) - - def _transform(self, m): - return 1.0 / mkvc(m) - - def inverse(self, D): - r"""Apply the inverse of the reciprocal mapping to an array. - - For the reciprocal mapping :math:`\mathbf{u}(\mathbf{m})`, - the inverse mapping on a variable :math:`\mathbf{x}` is itself a - reciprocal mapping, i.e.: - - .. math:: - \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = \mathbf{x}^{-1} - - Parameters - ---------- - D : numpy.ndarray - A set of input values - - Returns - ------- - numpy.ndarray - A :class:`numpy.ndarray` containing result of applying the - inverse mapping to the elements in *D*; which in this case - is just a reciprocal mapping. - """ - return 1.0 / mkvc(D) - - def deriv(self, m, v=None): - r"""Derivative of mapping with respect to the input parameters. - - For a mapping that computes the reciprocal for each - parameter in the model :math:`\mathbf{m}`, i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{m}^{-1} - - the derivative of the mapping with respect to the model is a diagonal - matrix of the form: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} - = \textrm{diag} \big ( -\mathbf{m}^{-2} \big ) - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - deriv = sdiag(-mkvc(m) ** (-2)) - if v is not None: - return deriv * v - return deriv - - @property - def is_linear(self): - return False - - -class LogMap(IdentityMap): - r"""Mapping that computes the natural logarithm of the model parameters. - - Where :math:`\mathbf{m}` is a set of model parameters, ``LogMap`` - creates a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the - natural logarithm of every element in - :math:`\mathbf{m}`; i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = \textrm{log}(\mathbf{m}) - - Parameters - ---------- - mesh : discretize.BaseMesh - The number of parameters accepted by the mapping is set to equal the number - of mesh cells. - nP : int - Set the number of parameters accepted by the mapping directly. Used if the - number of parameters is known. Used generally when the number of parameters - is not equal to the number of cells in a mesh. - """ - - def __init__(self, mesh=None, nP=None, **kwargs): - super().__init__(mesh=mesh, nP=nP, **kwargs) - - def _transform(self, m): - return np.log(mkvc(m)) - - def deriv(self, m, v=None): - r"""Derivative of mapping with respect to the input parameters. - - For a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the - natural logarithm for each parameter in the model :math:`\mathbf{m}`, - i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = log(\mathbf{m}) - - the derivative of the mapping with respect to the model is a diagonal - matrix of the form: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} - = \textrm{diag} \big ( \mathbf{m}^{-1} \big ) - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - mod = mkvc(m) - deriv = np.zeros(mod.shape) - tol = 1e-16 # zero - ind = np.greater_equal(np.abs(mod), tol) - deriv[ind] = 1.0 / mod[ind] - if v is not None: - return sdiag(deriv) * v - return sdiag(deriv) - - def inverse(self, m): - r"""Apply the inverse of the natural log mapping to an array. - - For the natural log mapping :math:`\mathbf{u}(\mathbf{m})`, - the inverse mapping on a variable :math:`\mathbf{x}` is performed by - taking the natural exponent of the elements, i.e.: - - .. math:: - \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = exp(\mathbf{x}) - - Parameters - ---------- - D : numpy.ndarray - A set of input values - - Returns - ------- - numpy.ndarray - A :class:`numpy.ndarray` containing result of applying the - inverse mapping to the elements in *D*; which in this case - is the natural exponent. - """ - return np.exp(mkvc(m)) - - @property - def is_linear(self): - return False - - -class LogisticSigmoidMap(IdentityMap): - r"""Mapping that computes the logistic sigmoid of the model parameters. - - Where :math:`\mathbf{m}` is a set of model parameters, ``LogisticSigmoidMap`` creates - a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the logistic sigmoid - of every element in :math:`\mathbf{m}`; i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = sigmoid(\mathbf{m}) = \frac{1}{1+\exp{-\mathbf{m}}} - - ``LogisticSigmoidMap`` transforms values onto the interval (0,1), but can optionally - be scaled and shifted to the interval (a,b). This can be useful for inversion - of data that varies over a log scale and bounded on some interval: - - .. math:: - \mathbf{u}(\mathbf{m}) = a + (b - a) \cdot sigmoid(\mathbf{m}) - - Parameters - ---------- - mesh : discretize.BaseMesh - The number of parameters accepted by the mapping is set to equal the number - of mesh cells. - nP : int - Set the number of parameters accepted by the mapping directly. Used if the - number of parameters is known. Used generally when the number of parameters - is not equal to the number of cells in a mesh. - lower_bound: float or (nP) numpy.ndarray - lower bound (a) for the transform. Default 0. Defined \in \mathbf{u} space. - upper_bound: float or (nP) numpy.ndarray - upper bound (b) for the transform. Default 1. Defined \in \mathbf{u} space. - - """ - - def __init__(self, mesh=None, nP=None, lower_bound=0, upper_bound=1, **kwargs): - super().__init__(mesh=mesh, nP=nP, **kwargs) - lower_bound = np.atleast_1d(lower_bound) - upper_bound = np.atleast_1d(upper_bound) - if self.nP != "*": - # check if lower bound and upper bound broadcast to nP - try: - np.broadcast_shapes(lower_bound.shape, (self.nP,)) - except ValueError as err: - raise ValueError( - f"Lower bound does not broadcast to the number of parameters. " - f"Lower bound shape is {lower_bound.shape} and tried against " - f"{self.nP} parameters." - ) from err - try: - np.broadcast_shapes(upper_bound.shape, (self.nP,)) - except ValueError as err: - raise ValueError( - f"Upper bound does not broadcast to the number of parameters. " - f"Upper bound shape is {upper_bound.shape} and tried against " - f"{self.nP} parameters." - ) from err - # make sure lower and upper bound broadcast to each other... - try: - np.broadcast_shapes(lower_bound.shape, upper_bound.shape) - except ValueError as err: - raise ValueError( - f"Upper bound does not broadcast to the lower bound. " - f"Shapes {upper_bound.shape} and {lower_bound.shape} " - f"are incompatible with each other." - ) from err - - if np.any(lower_bound >= upper_bound): - raise ValueError( - "A lower bound is greater than or equal to the upper bound." - ) - - self._lower_bound = lower_bound - self._upper_bound = upper_bound - - @property - def lower_bound(self): - """The lower bound - - Returns - ------- - numpy.ndarray - """ - return self._lower_bound - - @property - def upper_bound(self): - """The upper bound - - Returns - ------- - numpy.ndarray - """ - return self._upper_bound - - def _transform(self, m): - return self.lower_bound + (self.upper_bound - self.lower_bound) * expit(mkvc(m)) - - def inverse(self, m): - r"""Apply the inverse of the mapping to an array. - - For the logistic sigmoid mapping :math:`\mathbf{u}(\mathbf{m})`, the - inverse mapping on a variable :math:`\mathbf{x}` is performed by taking - the log-odds of elements, i.e.: - - .. math:: - \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = logit(\mathbf{x}) = \log \frac{\mathbf{x}}{1 - \mathbf{x}} - - or scaled and translated to interval (a,b): - .. math:: - \mathbf{m} = logit(\frac{(\mathbf{x} - a)}{b-a}) - - Parameters - ---------- - m : numpy.ndarray - A set of input values - - Returns - ------- - numpy.ndarray - the inverse mapping to the elements in *m*; which in this case - is the log-odds function with scaled and shifted input. - """ - return logit( - (mkvc(m) - self.lower_bound) / (self.upper_bound - self.lower_bound) - ) - - def deriv(self, m, v=None): - r"""Derivative of mapping with respect to the input parameters. - - For a mapping :math:`\mathbf{u}(\mathbf{m})` the derivative of the mapping with - respect to the model is a diagonal matrix of the form: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} - = \textrm{diag} \big ( (b-a)\cdot sigmoid(\mathbf{m})\cdot(1-sigmoid(\mathbf{m})) \big ) - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - numpy.ndarray or scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - sigmoid = expit(mkvc(m)) - deriv = (self.upper_bound - self.lower_bound) * sigmoid * (1.0 - sigmoid) - if v is not None: - return deriv * v - return sdiag(deriv) - - @property - def is_linear(self): - return False - - -class ChiMap(IdentityMap): - r"""Mapping that computes the magnetic permeability given a set of magnetic susceptibilities. - - Where :math:`\boldsymbol{\chi}` is the input model parameters defining a set of magnetic - susceptibilities, ``ChiMap`` creates a mapping :math:`\boldsymbol{\mu}(\boldsymbol{\chi})` - that computes the corresponding magnetic permeabilities of every - element in :math:`\boldsymbol{\chi}`; i.e.: - - .. math:: - \boldsymbol{\mu}(\boldsymbol{\chi}) = \mu_0 \big (1 + \boldsymbol{\chi} \big ) - - where :math:`\mu_0` is the permeability of free space. - - Parameters - ---------- - mesh : discretize.BaseMesh - The number of parameters accepted by the mapping is set to equal the number - of mesh cells. - nP : int - Set the number of parameters accepted by the mapping directly. Used if the - number of parameters is known. Used generally when the number of parameters - is not equal to the number of cells in a mesh. - """ - - def __init__(self, mesh=None, nP=None, **kwargs): - super().__init__(mesh=mesh, nP=nP, **kwargs) - - def _transform(self, m): - return mu_0 * (1 + m) - - def deriv(self, m, v=None): - r"""Derivative of mapping with respect to the input parameters. - - For a mapping :math:`\boldsymbol{\mu}(\boldsymbol{\chi})` that transforms a - set of magnetic susceptibilities :math:`\boldsymbol{\chi}` to their corresponding - magnetic permeabilities, i.e.: - - .. math:: - \boldsymbol{\mu}(\boldsymbol{\chi}) = \mu_0 \big (1 + \boldsymbol{\chi} \big ), - - the derivative of the mapping with respect to the model is the identity - matrix scaled by the permeability of free-space. Thus: - - .. math:: - \frac{\partial \boldsymbol{\mu}}{\partial \boldsymbol{\chi}} = \mu_0 \mathbf{I} - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - if v is not None: - return mu_0 * v - return mu_0 * sp.eye(self.nP) - - def inverse(self, m): - r"""Apply the inverse mapping to an array. - - For the ``ChiMap`` class, the inverse mapping recoveres the set of - magnetic susceptibilities :math:`\boldsymbol{\chi}` from a set of - magnetic permeabilities :math:`\boldsymbol{\mu}`. Thus the inverse - mapping is defined as: - - .. math:: - \boldsymbol{\chi}(\boldsymbol{\mu}) = \frac{\boldsymbol{\mu}}{\mu_0} - 1 - - where :math:`\mu_0` is the permeability of free space. - - Parameters - ---------- - D : numpy.ndarray - A set of input values - - Returns - ------- - numpy.ndarray - A :class:`numpy.ndarray` containing result of applying the - inverse mapping to the elements in *D*; which in this case - represents the conversion of magnetic permeabilities - to their corresponding magnetic susceptibility values. - """ - return m / mu_0 - 1 - - -class MuRelative(IdentityMap): - r"""Mapping that computes the magnetic permeability given a set of relative permeabilities. - - Where :math:`\boldsymbol{\mu_r}` defines a set of relative permeabilities, ``MuRelative`` - creates a mapping :math:`\boldsymbol{\mu}(\boldsymbol{\mu_r})` that computes the - corresponding magnetic permeabilities of every element in :math:`\boldsymbol{\mu_r}`; - i.e.: - - .. math:: - \boldsymbol{\mu}(\boldsymbol{\mu_r}) = \mu_0 \boldsymbol{\mu_r} - - where :math:`\mu_0` is the permeability of free space. - - Parameters - ---------- - mesh : discretize.BaseMesh - The number of parameters accepted by the mapping is set to equal the number - of mesh cells. - nP : int - Set the number of parameters accepted by the mapping directly. Used if the - number of parameters is known. Used generally when the number of parameters - is not equal to the number of cells in a mesh. - """ - - def __init__(self, mesh=None, nP=None, **kwargs): - super().__init__(mesh=mesh, nP=nP, **kwargs) - - def _transform(self, m): - return mu_0 * m - - def deriv(self, m, v=None): - r"""Derivative of mapping with respect to the input parameters. - - For a mapping that transforms a set of relative permeabilities - :math:`\boldsymbol{\mu_r}` to their corresponding magnetic permeabilities, i.e.: - - .. math:: - \boldsymbol{\mu}(\boldsymbol{\mu_r}) = \mu_0 \boldsymbol{\mu_r}, - - the derivative of the mapping with respect to the model is the identity - matrix scaled by the permeability of free-space. Thus: - - .. math:: - \frac{\partial \boldsymbol{\mu}}{\partial \boldsymbol{\mu_r}} = \mu_0 \mathbf{I} - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - if v is not None: - return mu_0 * v - return mu_0 * sp.eye(self.nP) - - def inverse(self, m): - r"""Apply the inverse mapping to an array. - - For the ``MuRelative`` class, the inverse mapping recoveres the set of - relative permeabilities :math:`\boldsymbol{\mu_r}` from a set of - magnetic permeabilities :math:`\boldsymbol{\mu}`. Thus the inverse - mapping is defined as: - - .. math:: - \boldsymbol{\mu_r}(\boldsymbol{\mu}) = \frac{\boldsymbol{\mu}}{\mu_0} - - where :math:`\mu_0` is the permeability of free space. - - Parameters - ---------- - D : numpy.ndarray - A set of input values - - Returns - ------- - numpy.ndarray - A :class:`numpy.ndarray` containing result of applying the - inverse mapping to the elements in *D*; which in this case - represents the conversion of magnetic permeabilities - to their corresponding relative permeability values. - """ - return 1.0 / mu_0 * m - - -class Weighting(IdentityMap): - r"""Mapping that scales the elements of the model by a corresponding set of weights. - - Where :math:`\mathbf{m}` defines the set of input model parameters and - :math:`\mathbf{w}` represents a corresponding set of model weight, - ``Weighting`` constructs a mapping :math:`\mathbf{u}(\mathbf{m})` of the form: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{w} \odot \mathbf{m} - - where :math:`\odot` is the Hadamard product. The mapping may also be - defined using a linear operator as follows: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} \;\;\;\;\; \textrm{where} \;\;\;\;\; \mathbf{P} = diag(\mathbf{w}) - - Parameters - ---------- - mesh : discretize.BaseMesh - The number of parameters accepted by the mapping is set to equal the number - of mesh cells. - nP : int - Set the number of parameters accepted by the mapping directly. Used if the - number of parameters is known. Used generally when the number of parameters - is not equal to the number of cells in a mesh. - weights : (nP) numpy.ndarray - A set of independent model weights. If ``None``, all model weights are set - to *1*. - """ - - def __init__(self, mesh=None, nP=None, weights=None, **kwargs): - if "nC" in kwargs: - raise TypeError( - "`nC` has been removed. Use `nP` to set the number of model " - "parameters." - ) - - super(Weighting, self).__init__(mesh=mesh, nP=nP, **kwargs) - - if weights is None: - weights = np.ones(self.nP) - - self.weights = np.array(weights, dtype=float) - - @property - def shape(self): - """Dimensions of the mapping. - - Returns - ------- - tuple - Dimensions of the mapping. Where *nP* is the number of parameters - the mapping acts on, this method returns a tuple of the form - (*nP*, *nP*). - """ - return (self.nP, self.nP) - - @property - def P(self): - r"""The linear mapping operator - - This property returns the sparse matrix :math:`\mathbf{P}` that carries - out the weighting mapping via matrix-vector product, i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} \;\;\;\;\; \textrm{where} \;\;\;\;\; \mathbf{P} = diag(\mathbf{w}) - - Returns - ------- - scipy.sparse.csr_matrix - Sparse linear mapping operator - """ - return sdiag(self.weights) - - def _transform(self, m): - return self.weights * m - - def inverse(self, D): - r"""Apply the inverse of the weighting mapping to an array. - - For the weighting mapping :math:`\mathbf{u}(\mathbf{m})`, the inverse - mapping on a variable :math:`\mathbf{x}` is performed by multplying each element by - the reciprocal of its corresponding weighting value, i.e.: - - .. math:: - \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = \mathbf{w}^{-1} \odot \mathbf{x} - - where :math:`\odot` is the Hadamard product. The inverse mapping may also be defined - using a linear operator as follows: - - .. math:: - \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = \mathbf{P^{-1} m} - \;\;\;\;\; \textrm{where} \;\;\;\;\; \mathbf{P} = diag(\mathbf{w}) - - Parameters - ---------- - D : numpy.ndarray - A set of input values - - Returns - ------- - numpy.ndarray - A :class:`numpy.ndarray` containing result of applying the - inverse mapping to the elements in *D*; which in this case - is simply dividing each element by its corresponding - weight. - """ - return self.weights ** (-1.0) * D - - def deriv(self, m, v=None): - r"""Derivative of mapping with respect to the input parameters. - - For a weighting mapping :math:`\mathbf{u}(\mathbf{m})` that scales the - input parameters in the model :math:`\mathbf{m}` by their corresponding - weights :math:`\mathbf{w}`; i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{w} \dot \mathbf{m}, - - the derivative of the mapping with respect to the model is a diagonal - matrix of the form: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} - = diag (\mathbf{w}) - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - if v is not None: - return self.weights * v - return self.P - - -class ComplexMap(IdentityMap): - r"""Maps the real and imaginary component values stored in a model to complex values. - - Let :math:`\mathbf{m}` be a model which stores the real and imaginary components of - a set of complex values :math:`\mathbf{z}`. Where the model parameters are organized - into a vector of the form - :math:`\mathbf{m} = [\mathbf{z}^\prime , \mathbf{z}^{\prime\prime}]`, ``ComplexMap`` - constructs the following mapping: - - .. math:: - \mathbf{z}(\mathbf{m}) = \mathbf{z}^\prime + j \mathbf{z}^{\prime\prime} - - Note that the mapping is :math:`\mathbb{R}^{2n} \rightarrow \mathbb{C}^n`. - - Parameters - ---------- - mesh : discretize.BaseMesh - If a mesh is used to construct the mapping, the number of input model - parameters is *2\*mesh.nC* and the number of complex values output from - the mapping is equal to *mesh.nC*. If *mesh* is ``None``, the dimensions - of the mapping are set using the *nP* input argument. - nP : int - Defines the number of input model parameters directly. Must be an even number!!! - In this case, the number of complex values output from the mapping is *nP/2*. - If *nP* = ``None``, the dimensions of the mapping are set using the *mesh* - input argument. - - Examples - -------- - Here we construct a complex mapping on a 1D mesh comprised - of 4 cells. The input model is real-valued array of length 8 - (4 real and 4 imaginary values). The output of the mapping - is a complex array with 4 values. - - >>> from simpeg.maps import ComplexMap - >>> from discretize import TensorMesh - >>> import numpy as np - - >>> nC = 4 - >>> mesh = TensorMesh([np.ones(nC)]) - - >>> z_real = np.ones(nC) - >>> z_imag = 2*np.ones(nC) - >>> m = np.r_[z_real, z_imag] - >>> m - array([1., 1., 1., 1., 2., 2., 2., 2.]) - - >>> mapping = ComplexMap(mesh=mesh) - >>> z = mapping * m - >>> z - array([1.+2.j, 1.+2.j, 1.+2.j, 1.+2.j]) - - """ - - def __init__(self, mesh=None, nP=None, **kwargs): - super().__init__(mesh=mesh, nP=nP, **kwargs) - if nP is not None and mesh is not None: - assert ( - 2 * mesh.nC == nP - ), "Number parameters must be 2 X number of mesh cells." - if nP is not None: - assert nP % 2 == 0, "nP must be even." - self._nP = nP or int(self.mesh.nC * 2) - - @property - def nP(self): - r"""Number of parameters the mapping acts on. - - Returns - ------- - int or '*' - Number of parameters that the mapping acts on. - """ - return self._nP - - @property - def shape(self): - """Dimensions of the mapping - - Returns - ------- - tuple - The dimensions of the mapping. Where *nP* is the number - of input parameters, this property returns a tuple - (*nP/2*, *nP*). - """ - return (int(self.nP / 2), self.nP) - - def _transform(self, m): - nC = int(self.nP / 2) - return m[:nC] + m[nC:] * 1j - - def deriv(self, m, v=None): - r"""Derivative of the complex mapping with respect to the input parameters. - - The complex mapping maps the real and imaginary components stored in a model - of the form :math:`\mathbf{m} = [\mathbf{z}^\prime , \mathbf{z}^{\prime\prime}]` - to their corresponding complex values :math:`\mathbf{z}`, i.e. - - .. math:: - \mathbf{z}(\mathbf{m}) = \mathbf{z}^\prime + j \mathbf{z}^{\prime\prime} - - The derivative of the mapping with respect to the model is block - matrix of the form: - - .. math:: - \frac{\partial \mathbf{z}}{\partial \mathbf{m}} = \big ( \mathbf{I} \;\;\; j\mathbf{I} \big ) - - where :math:`\mathbf{I}` is the identity matrix of shape (*nP/2*, *nP/2*) and - :math:`j = \sqrt{-1}`. - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - - Examples - -------- - Here we construct the derivative operator for the complex mapping on a 1D - mesh comprised of 4 cells. We then demonstrate how the derivative of the - mapping and its adjoint can be applied to a vector. - - >>> from simpeg.maps import ComplexMap - >>> from discretize import TensorMesh - >>> import numpy as np - - >>> nC = 4 - >>> mesh = TensorMesh([np.ones(nC)]) - - >>> m = np.random.rand(2*nC) - >>> mapping = ComplexMap(mesh=mesh) - >>> M = mapping.deriv(m) - - When applying the derivative operator to a vector, it will convert - the real and imaginary values stored in the vector to - complex values; essentially applying the mapping. - - >>> v1 = np.arange(0, 2*nC, 1) - >>> u1 = M * v1 - >>> u1 - array([0.+4.j, 1.+5.j, 2.+6.j, 3.+7.j]) - - When applying the adjoint of the derivative operator to a set of - complex values, the operator will decompose these values into - their real and imaginary components. - - >>> v2 = np.arange(0, nC, 1) + 1j*np.arange(nC, 2*nC, 1) - >>> u2 = M.adjoint() * v2 - >>> u2 - array([0., 1., 2., 3., 4., 5., 6., 7.]) - - """ - nC = self.shape[0] - shp = (nC, nC * 2) - - def fwd(v): - return v[:nC] + v[nC:] * 1j - - def adj(v): - return np.r_[v.real, v.imag] - - if v is not None: - return LinearOperator(shp, matvec=fwd, rmatvec=adj) * v - return LinearOperator(shp, matvec=fwd, rmatvec=adj) - - # inverse = deriv - - -############################################################################### -# # -# Surjection, Injection and Interpolation Maps # -# # -############################################################################### - - -class SurjectFull(IdentityMap): - r"""Mapping a single property value to all mesh cells. - - Let :math:`m` be a model defined by a single physical property value - ``SurjectFull`` construct a surjective mapping that projects :math:`m` - to the set of voxel cells defining a mesh. The mapping - :math:`\mathbf{u(m)}` is a matrix of 1s of shape (*mesh.nC* , 1) that - projects the model to all mesh cells, i.e.: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} - - Parameters - ---------- - mesh : discretize.BaseMesh - A discretize mesh - - """ - - def __init__(self, mesh, **kwargs): - super().__init__(mesh=mesh, **kwargs) - - @property - def nP(self): - r"""Number of parameters the mapping acts on; i.e. 1. - - Returns - ------- - int - Returns an integer value of 1 - """ - return 1 - - def _transform(self, m): - """ - :param m: model (scalar) - :rtype: numpy.ndarray - :return: transformed model - """ - return np.ones(self.mesh.nC) * m - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the input parameters. - - Let :math:`m` be the single parameter that the mapping acts on. The - ``SurjectFull`` class constructs a mapping that can be defined as - a projection matrix :math:`\mathbf{P}`; i.e.: - - .. math:: - \mathbf{u} = \mathbf{P m}, - - the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect - to the model parameters; i.e.: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} - - Note that in this case, **deriv** simply returns the original operator - :math:`\mathbf{P}`; a (*mesh.nC* , 1) numpy.ndarray of 1s. - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - """ - deriv = sp.csr_matrix(np.ones([self.mesh.nC, 1])) - if v is not None: - return deriv * v - return deriv - - -class SurjectVertical1D(IdentityMap): - r"""Map 1D layered Earth model to 2D or 3D tensor mesh. - - Let :math:`m` be a 1D model that defines the property values along - the last dimension of a tensor mesh; i.e. the y-direction for 2D - meshes and the z-direction for 3D meshes. ``SurjectVertical1D`` - construct a surjective mapping from the 1D model to all voxel cells - in the 2D or 3D tensor mesh provided. - - Mathematically, the mapping :math:`\mathbf{u}(\mathbf{m})` can be - represented by a projection matrix: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} - - Parameters - ---------- - mesh : discretize.TensorMesh - A 2D or 3D tensor mesh - - Examples - -------- - Here we define a 1D layered Earth model comprised of 3 layers - on a 1D tensor mesh. We then use ``SurjectVertical1D`` to - construct a mapping which projects the 1D model onto a 2D - tensor mesh. - - >>> from simpeg.maps import SurjectVertical1D - >>> from simpeg.utils import plot_1d_layer_model - >>> from discretize import TensorMesh - >>> import numpy as np - >>> import matplotlib as mpl - >>> import matplotlib.pyplot as plt - - >>> dh = np.ones(20) - >>> mesh1D = TensorMesh([dh], 'C') - >>> mesh2D = TensorMesh([dh, dh], 'CC') - - >>> m = np.zeros(mesh1D.nC) - >>> m[mesh1D.cell_centers < 0] = 10. - >>> m[mesh1D.cell_centers < -5] = 5. - - >>> fig1 = plt.figure(figsize=(5,5)) - >>> ax1 = fig1.add_subplot(111) - >>> plot_1d_layer_model( - >>> mesh1D.h[0], np.flip(m), ax=ax1, z0=0, - >>> scale='linear', show_layers=True, plot_elevation=True - >>> ) - >>> ax1.set_xlim([-0.1, 11]) - >>> ax1.set_title('1D Model') - - >>> mapping = SurjectVertical1D(mesh2D) - >>> u = mapping * m - - >>> fig2 = plt.figure(figsize=(6, 5)) - >>> ax2a = fig2.add_axes([0.1, 0.15, 0.7, 0.8]) - >>> mesh2D.plot_image(u, ax=ax2a, grid=True) - >>> ax2a.set_title('Projected to 2D Mesh') - >>> ax2b = fig2.add_axes([0.83, 0.15, 0.05, 0.8]) - >>> norm = mpl.colors.Normalize(vmin=np.min(m), vmax=np.max(m)) - >>> cbar = mpl.colorbar.ColorbarBase(ax2b, norm=norm, orientation="vertical") - - """ - - def __init__(self, mesh, **kwargs): - assert isinstance( - mesh, (TensorMesh, CylindricalMesh) - ), "Only implemented for tensor meshes" - super().__init__(mesh=mesh, **kwargs) - - @property - def nP(self): - r"""Number of parameters the mapping acts on. - - Returns - ------- - int - Number of parameters the mapping acts on. Should equal the - number of cells along the last dimension of the tensor mesh - supplied when defining the mapping. - """ - return int(self.mesh.vnC[self.mesh.dim - 1]) - - def _transform(self, m): - repNum = np.prod(self.mesh.vnC[: self.mesh.dim - 1]) - return mkvc(m).repeat(repNum) - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the model paramters. - - Let :math:`\mathbf{m}` be a set of parameter values for the 1D model - and let :math:`\mathbf{P}` be a projection matrix that maps the 1D - model the 2D/3D tensor mesh. The forward mapping :math:`\mathbf{u}(\mathbf{m})` - is given by: - - .. math:: - \mathbf{u} = \mathbf{P m}, - - the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect - to the model parameters; i.e.: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} - - Note that in this case, **deriv** simply returns the projection matrix. - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - repNum = np.prod(self.mesh.vnC[: self.mesh.dim - 1]) - repVec = sp.csr_matrix( - (np.ones(repNum), (range(repNum), np.zeros(repNum))), shape=(repNum, 1) - ) - deriv = sp.kron(sp.identity(self.nP), repVec) - if v is not None: - return deriv * v - return deriv - - -class Surject2Dto3D(IdentityMap): - r"""Map 2D tensor model to 3D tensor mesh. - - Let :math:`m` define the parameters for a 2D tensor model. - ``Surject2Dto3D`` constructs a surjective mapping that projects - the 2D tensor model to a 3D tensor mesh. - - Mathematically, the mapping :math:`\mathbf{u}(\mathbf{m})` can be - represented by a projection matrix: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} - - Parameters - ---------- - mesh : discretize.TensorMesh - A 3D tensor mesh - normal : {'y', 'x', 'z'} - Define the projection axis. - - Examples - -------- - Here we project a 3 layered Earth model defined on a 2D tensor mesh - to a 3D tensor mesh. We assume that at for some y-location, we - have a 2D tensor model which defines the physical property distribution - as a function of the *x* and *z* location. Using ``Surject2Dto3D``, - we project the model along the y-axis to obtain a 3D distribution - for the physical property (i.e. a 3D tensor model). - - >>> from simpeg.maps import Surject2Dto3D - >>> from discretize import TensorMesh - >>> import numpy as np - >>> import matplotlib as mpl - >>> import matplotlib.pyplot as plt - - >>> dh = np.ones(20) - >>> mesh2D = TensorMesh([dh, dh], 'CC') - >>> mesh3D = TensorMesh([dh, dh, dh], 'CCC') - - Here, we define the 2D tensor model. - - >>> m = np.zeros(mesh2D.nC) - >>> m[mesh2D.cell_centers[:, 1] < 0] = 10. - >>> m[mesh2D.cell_centers[:, 1] < -5] = 5. - - We then plot the 2D tensor model; which is defined along the - x and z axes. - - >>> fig1 = plt.figure(figsize=(6, 5)) - >>> ax11 = fig1.add_axes([0.1, 0.15, 0.7, 0.8]) - >>> mesh2D.plot_image(m, ax=ax11, grid=True) - >>> ax11.set_ylabel('z') - >>> ax11.set_title('2D Tensor Model') - >>> ax12 = fig1.add_axes([0.83, 0.15, 0.05, 0.8]) - >>> norm1 = mpl.colors.Normalize(vmin=np.min(m), vmax=np.max(m)) - >>> cbar1 = mpl.colorbar.ColorbarBase(ax12, norm=norm1, orientation="vertical") - - By setting *normal = 'Y'* we are projecting along the y-axis. - - >>> mapping = Surject2Dto3D(mesh3D, normal='Y') - >>> u = mapping * m - - Finally we plot a slice of the resulting 3D tensor model. - - >>> fig2 = plt.figure(figsize=(6, 5)) - >>> ax21 = fig2.add_axes([0.1, 0.15, 0.7, 0.8]) - >>> mesh3D.plot_slice(u, ax=ax21, ind=10, normal='Y', grid=True) - >>> ax21.set_ylabel('z') - >>> ax21.set_title('Projected to 3D Mesh (y=0)') - >>> ax22 = fig2.add_axes([0.83, 0.15, 0.05, 0.8]) - >>> norm2 = mpl.colors.Normalize(vmin=np.min(m), vmax=np.max(m)) - >>> cbar2 = mpl.colorbar.ColorbarBase(ax22, norm=norm2, orientation="vertical") - - """ - - def __init__(self, mesh, normal="y", **kwargs): - self.normal = normal - super().__init__(mesh=mesh, **kwargs) - - @IdentityMap.mesh.setter - def mesh(self, value): - value = validate_type("mesh", value, discretize.TensorMesh, cast=False) - if value.dim != 3: - raise ValueError("Surject2Dto3D Only works for a 3D Mesh") - self._mesh = value - - @property - def normal(self): - """The projection axis. - - Returns - ------- - str - """ - return self._normal - - @normal.setter - def normal(self, value): - self._normal = validate_string("normal", value, ("x", "y", "z")) - - @property - def nP(self): - """Number of model properties. - - The number of cells in the - last dimension of the mesh.""" - if self.normal == "z": - return self.mesh.shape_cells[0] * self.mesh.shape_cells[1] - elif self.normal == "y": - return self.mesh.shape_cells[0] * self.mesh.shape_cells[2] - elif self.normal == "x": - return self.mesh.shape_cells[1] * self.mesh.shape_cells[2] - - def _transform(self, m): - m = mkvc(m) - if self.normal == "z": - return mkvc( - m.reshape(self.mesh.vnC[:2], order="F")[:, :, np.newaxis].repeat( - self.mesh.shape_cells[2], axis=2 - ) - ) - elif self.normal == "y": - return mkvc( - m.reshape(self.mesh.vnC[::2], order="F")[:, np.newaxis, :].repeat( - self.mesh.shape_cells[1], axis=1 - ) - ) - elif self.normal == "x": - return mkvc( - m.reshape(self.mesh.vnC[1:], order="F")[np.newaxis, :, :].repeat( - self.mesh.shape_cells[0], axis=0 - ) - ) - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the model paramters. - - Let :math:`\mathbf{m}` be a set of parameter values for the 2D tensor model - and let :math:`\mathbf{P}` be a projection matrix that maps the 2D tensor model - to the 3D tensor mesh. The forward mapping :math:`\mathbf{u}(\mathbf{m})` - is given by: - - .. math:: - \mathbf{u} = \mathbf{P m}, - - the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect - to the model parameters; i.e.: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} - - Note that in this case, **deriv** simply returns the projection matrix. - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - inds = self * np.arange(self.nP) - nC, nP = self.mesh.nC, self.nP - P = sp.csr_matrix((np.ones(nC), (range(nC), inds)), shape=(nC, nP)) - if v is not None: - return P * v - return P - - -class Mesh2Mesh(IdentityMap): - """ - Takes a model on one mesh are translates it to another mesh. - """ - - def __init__(self, meshes, indActive=None, **kwargs): - # Sanity checks for the meshes parameter - try: - mesh, mesh2 = meshes - except TypeError: - raise TypeError("Couldn't unpack 'meshes' into two meshes.") - - super().__init__(mesh=mesh, **kwargs) - - self.mesh2 = mesh2 - # Check dimensions of both meshes - if mesh.dim != mesh2.dim: - raise ValueError( - f"Found meshes with dimensions '{mesh.dim}' and '{mesh2.dim}'. " - + "Both meshes must have the same dimension." - ) - self.indActive = indActive - - # reset to not accepted None for mesh - @IdentityMap.mesh.setter - def mesh(self, value): - self._mesh = validate_type("mesh", value, discretize.base.BaseMesh, cast=False) - - @property - def mesh2(self): - """The source mesh used for the mapping. - - Returns - ------- - discretize.base.BaseMesh - """ - return self._mesh2 - - @mesh2.setter - def mesh2(self, value): - self._mesh2 = validate_type( - "mesh2", value, discretize.base.BaseMesh, cast=False - ) - - @property - def indActive(self): - """Active indices on target mesh. - - Returns - ------- - (mesh.n_cells) numpy.ndarray of bool or none - """ - return self._indActive - - @indActive.setter - def indActive(self, value): - if value is not None: - value = validate_active_indices("indActive", value, self.mesh.n_cells) - self._indActive = value - - @property - def P(self): - if getattr(self, "_P", None) is None: - self._P = self.mesh2.get_interpolation_matrix( - ( - self.mesh.cell_centers[self.indActive, :] - if self.indActive is not None - else self.mesh.cell_centers - ), - "CC", - zeros_outside=True, - ) - return self._P - - @property - def shape(self): - """Number of parameters in the model.""" - if self.indActive is not None: - return (self.indActive.sum(), self.mesh2.nC) - return (self.mesh.nC, self.mesh2.nC) - - @property - def nP(self): - """Number of parameters in the model.""" - return self.mesh2.nC - - def _transform(self, m): - return self.P * m - - def deriv(self, m, v=None): - if v is not None: - return self.P * v - return self.P - - -class InjectActiveCells(IdentityMap): - r"""Map active cells model to all cell of a mesh. - - The ``InjectActiveCells`` class is used to define the mapping when - the model consists of physical property values for a set of active - mesh cells; e.g. cells below topography. For a discrete set of - model parameters :math:`\mathbf{m}` defined on a set of active - cells, the mapping :math:`\mathbf{u}(\mathbf{m})` is defined as: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + \mathbf{d}\, m_\perp - - where :math:`\mathbf{P}` is a (*nC* , *nP*) projection matrix from - active cells to all mesh cells, and :math:`\mathbf{d}` is a - (*nC* , 1) matrix that projects the inactive cell value - :math:`m_\perp` to all inactive mesh cells. - - Parameters - ---------- - mesh : discretize.BaseMesh - A discretize mesh - indActive : numpy.ndarray - Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* - or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. - valInactive : float or numpy.ndarray - The physical property value assigned to all inactive cells in the mesh - - """ - - def __init__(self, mesh, indActive=None, valInactive=0.0, nC=None): - self.mesh = mesh - self.nC = nC or mesh.nC - - self._indActive = validate_active_indices("indActive", indActive, self.nC) - self._nP = np.sum(self.indActive) - - self.P = sp.eye(self.nC, format="csr")[:, self.indActive] - - self.valInactive = valInactive - - @property - def valInactive(self): - """The physical property value assigned to all inactive cells in the mesh. - - Returns - ------- - numpy.ndarray - """ - return self._valInactive - - @valInactive.setter - def valInactive(self, value): - n_inactive = self.nC - self.nP - try: - value = validate_float("valInactive", value) - value = np.full(n_inactive, value) - except Exception: - pass - value = validate_ndarray_with_shape("valInactive", value, shape=(n_inactive,)) - - self._valInactive = np.zeros(self.nC, dtype=float) - self._valInactive[~self.indActive] = value - - @property - def indActive(self): - """ - - Returns - ------- - numpy.ndarray of bool - - """ - return self._indActive - - @property - def shape(self): - """Dimensions of the mapping - - Returns - ------- - tuple of int - Where *nP* is the number of active cells and *nC* is - number of cell in the mesh, **shape** returns a - tuple (*nC* , *nP*). - """ - return (self.nC, self.nP) - - @property - def nP(self): - """Number of parameters the model acts on. - - Returns - ------- - int - Number of parameters the model acts on; i.e. the number of active cells - """ - return int(self.indActive.sum()) - - def _transform(self, m): - if m.ndim > 1: - return self.P * m + self.valInactive[:, None] - return self.P * m + self.valInactive - - def inverse(self, u): - r"""Recover the model parameters (active cells) from a set of physical - property values defined on the entire mesh. - - For a discrete set of model parameters :math:`\mathbf{m}` defined - on a set of active cells, the mapping :math:`\mathbf{u}(\mathbf{m})` - is defined as: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + \mathbf{d} \,m_\perp - - where :math:`\mathbf{P}` is a (*nC* , *nP*) projection matrix from - active cells to all mesh cells, and :math:`\mathbf{d}` is a - (*nC* , 1) matrix that projects the inactive cell value - :math:`m_\perp` to all inactive mesh cells. - - The inverse mapping is given by: - - .. math:: - \mathbf{m}(\mathbf{u}) = \mathbf{P^T u} - - Parameters - ---------- - u : (mesh.nC) numpy.ndarray - A vector which contains physical property values for all - mesh cells. - """ - return self.P.T * u - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the input parameters. - - For a discrete set of model parameters :math:`\mathbf{m}` defined - on a set of active cells, the mapping :math:`\mathbf{u}(\mathbf{m})` - is defined as: - - .. math:: - \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + \mathbf{d} \, m_\perp - - where :math:`\mathbf{P}` is a (*nC* , *nP*) projection matrix from - active cells to all mesh cells, and :math:`\mathbf{d}` is a - (*nC* , 1) matrix that projects the inactive cell value - :math:`m_\perp` to all inactive mesh cells. - - the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect - to the model parameters; i.e.: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} - - Note that in this case, **deriv** simply returns a sparse projection matrix. - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - if v is not None: - return self.P * v - return self.P - - -############################################################################### -# # -# Parametric Maps # -# # -############################################################################### - - -class ParametricCircleMap(IdentityMap): - r"""Mapping for a parameterized circle. - - Define the mapping from a parameterized model for a circle in a wholespace - to all cells within a 2D mesh. For a circle within a wholespace, the - model is defined by 5 parameters: the background physical property value - (:math:`\sigma_0`), the physical property value for the circle - (:math:`\sigma_c`), the x location :math:`x_0` and y location :math:`y_0` - for center of the circle, and the circle's radius (:math:`R`). - - Let :math:`\mathbf{m} = [\sigma_0, \sigma_1, x_0, y_0, R]` be the set of - model parameters the defines a circle within a wholespace. The mapping - :math:`\mathbf{u}(\mathbf{m})` from the parameterized model to all cells - within a 2D mesh is given by: - - .. math:: - - \mathbf{u}(\mathbf{m}) = \sigma_0 + (\sigma_1 - \sigma_0) - \bigg [ \frac{1}{2} + \pi^{-1} \arctan \bigg ( a \big [ \sqrt{(\mathbf{x_c}-x_0)^2 + - (\mathbf{y_c}-y_0)^2} - R \big ] \bigg ) \bigg ] - - where :math:`\mathbf{x_c}` and :math:`\mathbf{y_c}` are vectors storing - the x and y positions of all cell centers for the 2D mesh and :math:`a` - is a user-defined constant which defines the sharpness of boundary of the - circular structure. - - Parameters - ---------- - mesh : discretize.BaseMesh - A 2D discretize mesh - logSigma : bool - If ``True``, parameters :math:`\sigma_0` and :math:`\sigma_1` represent the - natural log of the physical property values for the background and circle, - respectively. - slope : float - A constant for defining the sharpness of the boundary between the circle - and the wholespace. The sharpness increases as *slope* is increased. - - Examples - -------- - Here we define the parameterized model for a circle in a wholespace. We then - create and use a ``ParametricCircleMap`` to map the model to a 2D mesh. - - >>> from simpeg.maps import ParametricCircleMap - >>> from discretize import TensorMesh - >>> import numpy as np - >>> import matplotlib.pyplot as plt - - >>> h = 0.5*np.ones(20) - >>> mesh = TensorMesh([h, h]) - - >>> sigma0, sigma1, x0, y0, R = 0., 10., 4., 6., 2. - >>> model = np.r_[sigma0, sigma1, x0, y0, R] - >>> mapping = ParametricCircleMap(mesh, logSigma=False, slope=2) - - >>> fig = plt.figure(figsize=(5, 5)) - >>> ax = fig.add_subplot(111) - >>> mesh.plot_image(mapping * model, ax=ax) - - """ - - def __init__(self, mesh, logSigma=True, slope=0.1): - super().__init__(mesh=mesh) - if mesh.dim != 2: - raise NotImplementedError( - "Mesh must be 2D, not implemented yet for other dimensions." - ) - # TODO: this should be done through a composition with and ExpMap - self.logSigma = logSigma - self.slope = slope - - @property - def slope(self): - """Sharpness of the boundary. - - Larger number are sharper. - - Returns - ------- - float - """ - return self._slope - - @slope.setter - def slope(self, value): - self._slope = validate_float("slope", value, min_val=0.0, inclusive_min=False) - - @property - def logSigma(self): - """Whether the input needs to be transformed by an exponential - - Returns - ------- - float - """ - return self._logSigma - - @logSigma.setter - def logSigma(self, value): - self._logSigma = validate_type("logSigma", value, bool) - - @property - def nP(self): - r"""Number of parameters the mapping acts on; i.e. 5. - - Returns - ------- - int - The ``ParametricCircleMap`` acts on 5 parameters. - """ - return 5 - - def _transform(self, m): - a = self.slope - sig1, sig2, x, y, r = m[0], m[1], m[2], m[3], m[4] - if self.logSigma: - sig1, sig2 = np.exp(sig1), np.exp(sig2) - X = self.mesh.cell_centers[:, 0] - Y = self.mesh.cell_centers[:, 1] - return sig1 + (sig2 - sig1) * ( - np.arctan(a * (np.sqrt((X - x) ** 2 + (Y - y) ** 2) - r)) / np.pi + 0.5 - ) - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the input parameters. - - Let :math:`\mathbf{m} = [\sigma_0, \sigma_1, x_0, y_0, R]` be the set of - model parameters the defines a circle within a wholespace. The mapping - :math:`\mathbf{u}(\mathbf{m})`from the parameterized model to all cells - within a 2D mesh is given by: - - .. math:: - \mathbf{u}(\mathbf{m}) = \sigma_0 + (\sigma_1 - \sigma_0) - \bigg [ \frac{1}{2} + \pi^{-1} \arctan \bigg ( a \big [ \sqrt{(\mathbf{x_c}-x_0)^2 + - (\mathbf{y_c}-y_0)^2} - R \big ] \bigg ) \bigg ] - - The derivative of the mapping with respect to the model parameters is a - ``numpy.ndarray`` of shape (*mesh.nC*, 5) given by: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = - \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_0} \;\; - \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_1} \;\; - \Bigg [ \frac{\partial \mathbf{u}}{\partial x_0} \;\; - \Bigg [ \frac{\partial \mathbf{u}}{\partial y_0} \;\; - \Bigg [ \frac{\partial \mathbf{u}}{\partial R} - \Bigg ] - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - a = self.slope - sig1, sig2, x, y, r = m[0], m[1], m[2], m[3], m[4] - if self.logSigma: - sig1, sig2 = np.exp(sig1), np.exp(sig2) - X = self.mesh.cell_centers[:, 0] - Y = self.mesh.cell_centers[:, 1] - if self.logSigma: - g1 = ( - -( - np.arctan(a * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2))) / np.pi - + 0.5 - ) - * sig1 - + sig1 - ) - g2 = ( - np.arctan(a * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2))) / np.pi + 0.5 - ) * sig2 - else: - g1 = ( - -( - np.arctan(a * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2))) / np.pi - + 0.5 - ) - + 1.0 - ) - g2 = ( - np.arctan(a * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2))) / np.pi + 0.5 - ) - - g3 = ( - a - * (-X + x) - * (-sig1 + sig2) - / ( - np.pi - * (a**2 * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2)) ** 2 + 1) - * np.sqrt((X - x) ** 2 + (Y - y) ** 2) - ) - ) - - g4 = ( - a - * (-Y + y) - * (-sig1 + sig2) - / ( - np.pi - * (a**2 * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2)) ** 2 + 1) - * np.sqrt((X - x) ** 2 + (Y - y) ** 2) - ) - ) - - g5 = ( - -a - * (-sig1 + sig2) - / (np.pi * (a**2 * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2)) ** 2 + 1)) - ) - - if v is not None: - return sp.csr_matrix(np.c_[g1, g2, g3, g4, g5]) * v - return sp.csr_matrix(np.c_[g1, g2, g3, g4, g5]) - - @property - def is_linear(self): - return False - - -class ParametricPolyMap(IdentityMap): - r"""Mapping for 2 layer model whose interface is defined by a polynomial. - - This mapping is used when the cells lying below the Earth's surface can - be parameterized by a 2 layer model whose interface is defined by a - polynomial function. The model is defined by the physical property - values for each unit (:math:`\sigma_1` and :math:`\sigma_2`) and the - coefficients for the polynomial function (:math:`\mathbf{c}`). - - **For a 2D mesh** , the interface is defined by a polynomial function - of the form: - - .. math:: - p(x) = \sum_{i=0}^N c_i x^i - - where :math:`c_i` are the polynomial coefficients and :math:`N` is - the order of the polynomial. In this case, the model is defined as - - .. math:: - \mathbf{m} = [\sigma_1, \;\sigma_2,\; c_0 ,\;\ldots\; ,\; c_N] - - The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh - is given by: - - .. math:: - - \mathbf{u}(\mathbf{m}) = \sigma_1 + (\sigma_2 - \sigma_1) - \bigg [ \frac{1}{2} + \pi^{-1} \arctan \bigg ( - a \Big ( \mathbf{p}(\mathbf{x_c}) - \mathbf{y_c} \Big ) - \bigg ) \bigg ] - - where :math:`\mathbf{x_c}` and :math:`\mathbf{y_c}` are vectors containing the - x and y cell center locations for all active cells in the mesh, and :math:`a` is a - parameter which defines the sharpness of the boundary between the two layers. - :math:`\mathbf{p}(\mathbf{x_c})` evaluates the polynomial function for - every element in :math:`\mathbf{x_c}`. - - **For a 3D mesh** , the interface is defined by a 2D polynomial function - of the form: - - .. math:: - p(x,y) = - \sum_{j=0}^{N_y} \sum_{i=0}^{N_x} c_{ij} \, x^i y^j - - where :math:`c_{ij}` are the polynomial coefficients. :math:`N_x` - and :math:`N_y` define the order of the polynomial in :math:`x` and - :math:`y`, respectively. In this case, the model is defined as: - - .. math:: - \mathbf{m} = [\sigma_1, \; \sigma_2, \; c_{0,0} , \; c_{1,0} , \;\ldots , \; c_{N_x, N_y}] - - The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh - is given by: - - .. math:: - - \mathbf{u}(\mathbf{m}) = \sigma_1 + (\sigma_2 - \sigma_1) - \bigg [ \frac{1}{2} + \pi^{-1} \arctan \bigg ( - a \Big ( \mathbf{p}(\mathbf{x_c,y_c}) - \mathbf{z_c} \Big ) - \bigg ) \bigg ] - - where :math:`\mathbf{x_c}, \mathbf{y_c}` and :math:`\mathbf{y_z}` are vectors - containing the x, y and z cell center locations for all active cells in the mesh. - :math:`\mathbf{p}(\mathbf{x_c, y_c})` evaluates the polynomial function for - every corresponding pair of :math:`\mathbf{x_c}` and :math:`\mathbf{y_c}` - elements. - - Parameters - ---------- - mesh : discretize.BaseMesh - A discretize mesh - order : int or list of int - Order of the polynomial. For a 2D mesh, this is an ``int``. For a 3D - mesh, the order for both variables is entered separately; i.e. - [*order1* , *order2*]. - logSigma : bool - If ``True``, parameters :math:`\sigma_1` and :math:`\sigma_2` represent - the natural log of a physical property. - normal : {'x', 'y', 'z'} - actInd : numpy.ndarray - Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* - or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. - - Examples - -------- - In this example, we define a 2 layer model whose interface is sharp and lies - along a polynomial function :math:`y(x)=c_0 + c_1 x`. In this case, the model is - defined as :math:`\mathbf{m} = [\sigma_1 , \sigma_2 , c_0 , c_1]`. We construct - a polynomial mapping from the model to the set of active cells (i.e. below the surface), - We then use an active cells mapping to map from the set of active cells to all - cells in the 2D mesh. - - >>> from simpeg.maps import ParametricPolyMap, InjectActiveCells - >>> from discretize import TensorMesh - >>> import numpy as np - >>> import matplotlib.pyplot as plt - - >>> h = 0.5*np.ones(20) - >>> mesh = TensorMesh([h, h]) - >>> ind_active = mesh.cell_centers[:, 1] < 8 - >>> - >>> sig1, sig2, c0, c1 = 10., 5., 2., 0.5 - >>> model = np.r_[sig1, sig2, c0, c1] - - >>> poly_map = ParametricPolyMap( - >>> mesh, order=1, logSigma=False, normal='Y', actInd=ind_active, slope=1e4 - >>> ) - >>> act_map = InjectActiveCells(mesh, ind_active, 0.) - - >>> fig = plt.figure(figsize=(5, 5)) - >>> ax = fig.add_subplot(111) - >>> mesh.plot_image(act_map * poly_map * model, ax=ax) - >>> ax.set_title('Mapping on a 2D mesh') - - Here, we recreate the previous example on a 3D mesh but with a smoother interface. - For a 3D mesh, the 2D polynomial defining the sloping interface is given by - :math:`z(x,y) = c_0 + c_x x + c_y y + c_{xy} xy`. In this case, the model is - defined as :math:`\mathbf{m} = [\sigma_1 , \sigma_2 , c_0 , c_x, c_y, c_{xy}]`. - - >>> mesh = TensorMesh([h, h, h]) - >>> ind_active = mesh.cell_centers[:, 2] < 8 - >>> - >>> sig1, sig2, c0, cx, cy, cxy = 10., 5., 2., 0.5, 0., 0. - >>> model = np.r_[sig1, sig2, c0, cx, cy, cxy] - >>> - >>> poly_map = ParametricPolyMap( - >>> mesh, order=[1, 1], logSigma=False, normal='Z', actInd=ind_active, slope=2 - >>> ) - >>> act_map = InjectActiveCells(mesh, ind_active, 0.) - >>> - >>> fig = plt.figure(figsize=(5, 5)) - >>> ax = fig.add_subplot(111) - >>> mesh.plot_slice(act_map * poly_map * model, ax=ax, normal='Y', ind=10) - >>> ax.set_title('Mapping on a 3D mesh') - - """ - - def __init__(self, mesh, order, logSigma=True, normal="X", actInd=None, slope=1e4): - super().__init__(mesh=mesh) - self.logSigma = logSigma - self.order = order - self.normal = normal - self.slope = slope - - if actInd is None: - actInd = np.ones(mesh.n_cells, dtype=bool) - self.actInd = actInd - - @property - def slope(self): - """Sharpness of the boundary. - - Larger number are sharper. - - Returns - ------- - float - """ - return self._slope - - @slope.setter - def slope(self, value): - self._slope = validate_float("slope", value, min_val=0.0, inclusive_min=False) - - @property - def logSigma(self): - """Whether the input needs to be transformed by an exponential - - Returns - ------- - float - """ - return self._logSigma - - @logSigma.setter - def logSigma(self, value): - self._logSigma = validate_type("logSigma", value, bool) - - @property - def normal(self): - """The projection axis. - - Returns - ------- - str - """ - return self._normal - - @normal.setter - def normal(self, value): - self._normal = validate_string("normal", value, ("x", "y", "z")) - - @property - def actInd(self): - """Active indices of the mesh. - - Returns - ------- - (mesh.n_cells) numpy.ndarray of bool - """ - return self._actInd - - @actInd.setter - def actInd(self, value): - self._actInd = validate_active_indices("actInd", value, self.mesh.n_cells) - self._nC = sum(self._actInd) - - @property - def shape(self): - """Dimensions of the mapping. - - Returns - ------- - tuple of int - The dimensions of the mapping as a tuple of the form - (*nC* , *nP*), where *nP* is the number of model parameters - the mapping acts on and *nC* is the number of active cells - being mapping to. If *actInd* is ``None``, then - *nC = mesh.nC*. - """ - return (self.nC, self.nP) - - @property - def nC(self): - """Number of active cells being mapped too. - - Returns - ------- - int - """ - return self._nC - - @property - def nP(self): - """Number of parameters the mapping acts on. - - Returns - ------- - int - The number of parameters the mapping acts on. - """ - if np.isscalar(self.order): - nP = self.order + 3 - else: - nP = (self.order[0] + 1) * (self.order[1] + 1) + 2 - return nP - - def _transform(self, m): - # Set model parameters - alpha = self.slope - sig1, sig2 = m[0], m[1] - c = m[2:] - if self.logSigma: - sig1, sig2 = np.exp(sig1), np.exp(sig2) - - # 2D - if self.mesh.dim == 2: - X = self.mesh.cell_centers[self.actInd, 0] - Y = self.mesh.cell_centers[self.actInd, 1] - if self.normal == "x": - f = polynomial.polyval(Y, c) - X - elif self.normal == "y": - f = polynomial.polyval(X, c) - Y - else: - raise (Exception("Input for normal = X or Y or Z")) - - # 3D - elif self.mesh.dim == 3: - X = self.mesh.cell_centers[self.actInd, 0] - Y = self.mesh.cell_centers[self.actInd, 1] - Z = self.mesh.cell_centers[self.actInd, 2] - - if self.normal == "x": - f = ( - polynomial.polyval2d( - Y, - Z, - c.reshape((self.order[0] + 1, self.order[1] + 1), order="F"), - ) - - X - ) - elif self.normal == "y": - f = ( - polynomial.polyval2d( - X, - Z, - c.reshape((self.order[0] + 1, self.order[1] + 1), order="F"), - ) - - Y - ) - elif self.normal == "z": - f = ( - polynomial.polyval2d( - X, - Y, - c.reshape((self.order[0] + 1, self.order[1] + 1), order="F"), - ) - - Z - ) - else: - raise (Exception("Input for normal = X or Y or Z")) - - else: - raise (Exception("Only supports 2D or 3D")) - - return sig1 + (sig2 - sig1) * (np.arctan(alpha * f) / np.pi + 0.5) - - def deriv(self, m, v=None): - r"""Derivative of the mapping with respect to the model. - - For a model :math:`\mathbf{m} = [\sigma_1, \sigma_2, \mathbf{c}]`, - the derivative of the mapping with respect to the model parameters is a - ``numpy.ndarray`` of shape (*mesh.nC*, *nP*) of the form: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = - \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_0} \;\; - \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_1} \;\; - \Bigg [ \frac{\partial \mathbf{u}}{\partial c_0} \;\; - \Bigg [ \frac{\partial \mathbf{u}}{\partial c_1} \;\; - \cdots \;\; - \Bigg [ \frac{\partial \mathbf{u}}{\partial c_N} - \Bigg ] - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - - """ - alpha = self.slope - sig1, sig2, c = m[0], m[1], m[2:] - if self.logSigma: - sig1, sig2 = np.exp(sig1), np.exp(sig2) - - # 2D - if self.mesh.dim == 2: - X = self.mesh.cell_centers[self.actInd, 0] - Y = self.mesh.cell_centers[self.actInd, 1] - - if self.normal == "x": - f = polynomial.polyval(Y, c) - X - V = polynomial.polyvander(Y, len(c) - 1) - elif self.normal == "y": - f = polynomial.polyval(X, c) - Y - V = polynomial.polyvander(X, len(c) - 1) - else: - raise (Exception("Input for normal = X or Y")) - - # 3D - elif self.mesh.dim == 3: - X = self.mesh.cell_centers[self.actInd, 0] - Y = self.mesh.cell_centers[self.actInd, 1] - Z = self.mesh.cell_centers[self.actInd, 2] - - if self.normal == "x": - f = ( - polynomial.polyval2d( - Y, Z, c.reshape((self.order[0] + 1, self.order[1] + 1)) - ) - - X - ) - V = polynomial.polyvander2d(Y, Z, self.order) - elif self.normal == "y": - f = ( - polynomial.polyval2d( - X, Z, c.reshape((self.order[0] + 1, self.order[1] + 1)) - ) - - Y - ) - V = polynomial.polyvander2d(X, Z, self.order) - elif self.normal == "z": - f = ( - polynomial.polyval2d( - X, Y, c.reshape((self.order[0] + 1, self.order[1] + 1)) - ) - - Z - ) - V = polynomial.polyvander2d(X, Y, self.order) - else: - raise (Exception("Input for normal = X or Y or Z")) - - if self.logSigma: - g1 = -(np.arctan(alpha * f) / np.pi + 0.5) * sig1 + sig1 - g2 = (np.arctan(alpha * f) / np.pi + 0.5) * sig2 - else: - g1 = -(np.arctan(alpha * f) / np.pi + 0.5) + 1.0 - g2 = np.arctan(alpha * f) / np.pi + 0.5 - - g3 = sdiag(alpha * (sig2 - sig1) / (1.0 + (alpha * f) ** 2) / np.pi) * V - - if v is not None: - return sp.csr_matrix(np.c_[g1, g2, g3]) * v - return sp.csr_matrix(np.c_[g1, g2, g3]) - - @property - def is_linear(self): - return False - - -class ParametricSplineMap(IdentityMap): - r"""Mapping to parameterize the boundary between two geological units using - spline interpolation. - - .. math:: - - g = f(x)-y - - Define the model as: - - .. math:: - - m = [\sigma_1, \sigma_2, y] - - Parameters - ---------- - mesh : discretize.BaseMesh - A discretize mesh - pts : (n) numpy.ndarray - Points for the 1D spline tie points. - ptsv : (2) array_like - Points for linear interpolation between two splines in 3D. - order : int - Order of the spline mapping; e.g. 3 is cubic spline - logSigma : bool - If ``True``, :math:`\sigma_1` and :math:`\sigma_2` represent the natural - log of some physical property value for each unit. - normal : {'x', 'y', 'z'} - Defines the general direction of the normal vector for the interface. - slope : float - Parameter for defining the sharpness of the boundary. The sharpness is increased - if *slope* is large. - - Examples - -------- - In this example, we define a 2 layered model with a sloping - interface on a 2D mesh. The model consists of the physical - property values for the layers and the known elevations - for the interface at the horizontal positions supplied when - creating the mapping. - - >>> from simpeg.maps import ParametricSplineMap - >>> from discretize import TensorMesh - >>> import numpy as np - >>> import matplotlib.pyplot as plt - - >>> h = 0.5*np.ones(20) - >>> mesh = TensorMesh([h, h]) - - >>> x = np.linspace(0, 10, 6) - >>> y = 0.5*x + 2.5 - - >>> model = np.r_[10., 0., y] - >>> mapping = ParametricSplineMap(mesh, x, order=2, normal='Y', slope=2) - - >>> fig = plt.figure(figsize=(5, 5)) - >>> ax = fig.add_subplot(111) - >>> mesh.plot_image(mapping * model, ax=ax) - - """ - - def __init__( - self, mesh, pts, ptsv=None, order=3, logSigma=True, normal="x", slope=1e4 - ): - super().__init__(mesh=mesh) - self.slope = slope - self.logSigma = logSigma - self.normal = normal - self.order = order - self.pts = pts - self.ptsv = ptsv - self.spl = None - - @IdentityMap.mesh.setter - def mesh(self, value): - self._mesh = validate_type( - "mesh", value, discretize.base.BaseTensorMesh, cast=False - ) - - @property - def slope(self): - """Sharpness of the boundary. - - Larger number are sharper. - - Returns - ------- - float - """ - return self._slope - - @slope.setter - def slope(self, value): - self._slope = validate_float("slope", value, min_val=0.0, inclusive_min=False) - - @property - def logSigma(self): - """Whether the input needs to be transformed by an exponential - - Returns - ------- - float - """ - return self._logSigma - - @logSigma.setter - def logSigma(self, value): - self._logSigma = validate_type("logSigma", value, bool) - - @property - def normal(self): - """The projection axis. - - Returns - ------- - str - """ - return self._normal - - @normal.setter - def normal(self, value): - self._normal = validate_string("normal", value, ("x", "y", "z")) - - @property - def order(self): - """Order of the spline mapping. - - Returns - ------- - int - """ - return self._order - - @order.setter - def order(self, value): - self._order = validate_integer("order", value, min_val=1) - - @property - def pts(self): - """Points for the spline. - - Returns - ------- - numpy.ndarray - """ - return self._pts - - @pts.setter - def pts(self, value): - self._pts = validate_ndarray_with_shape("pts", value, shape=("*"), dtype=float) - - @property - def npts(self): - """The number of points. - - Returns - ------- - int - """ - return self._pts.shape[0] - - @property - def ptsv(self): - """Bottom and top values for the 3D spline surface. - - In 3D, two splines are created and linearly interpolated between these two - points. - - Returns - ------- - (2) numpy.ndarray - """ - return self._ptsv - - @ptsv.setter - def ptsv(self, value): - if value is not None: - value = validate_ndarray_with_shape("ptsv", value, shape=(2,)) - self._ptsv = value - - @property - def nP(self): - r"""Number of parameters the mapping acts on - - Returns - ------- - int - Number of parameters the mapping acts on. - - **2D mesh:** the mapping acts on *mesh.nC + 2* parameters - - **3D mesh:** the mapping acts on *2\*mesh.nC + 2* parameters - """ - if self.mesh.dim == 2: - return np.size(self.pts) + 2 - elif self.mesh.dim == 3: - return np.size(self.pts) * 2 + 2 - else: - raise (Exception("Only supports 2D and 3D")) - - def _transform(self, m): - # Set model parameters - alpha = self.slope - sig1, sig2 = m[0], m[1] - c = m[2:] - if self.logSigma: - sig1, sig2 = np.exp(sig1), np.exp(sig2) - # 2D - if self.mesh.dim == 2: - X = self.mesh.cell_centers[:, 0] - Y = self.mesh.cell_centers[:, 1] - self.spl = UnivariateSpline(self.pts, c, k=self.order, s=0) - if self.normal == "x": - f = self.spl(Y) - X - elif self.normal == "y": - f = self.spl(X) - Y - else: - raise (Exception("Input for normal = X or Y or Z")) - - # 3D: - # Comments: - # Make two spline functions and link them using linear interpolation. - # This is not quite direct extension of 2D to 3D case - # Using 2D interpolation is possible - - elif self.mesh.dim == 3: - X = self.mesh.cell_centers[:, 0] - Y = self.mesh.cell_centers[:, 1] - Z = self.mesh.cell_centers[:, 2] - - npts = np.size(self.pts) - if np.mod(c.size, 2): - raise (Exception("Put even points!")) - - self.spl = { - "splb": UnivariateSpline(self.pts, c[:npts], k=self.order, s=0), - "splt": UnivariateSpline(self.pts, c[npts:], k=self.order, s=0), - } - - if self.normal == "x": - zb = self.ptsv[0] - zt = self.ptsv[1] - flines = (self.spl["splt"](Y) - self.spl["splb"](Y)) * (Z - zb) / ( - zt - zb - ) + self.spl["splb"](Y) - f = flines - X - # elif self.normal =='Y': - # elif self.normal =='Z': - else: - raise (Exception("Input for normal = X or Y or Z")) - else: - raise (Exception("Only supports 2D and 3D")) - - return sig1 + (sig2 - sig1) * (np.arctan(alpha * f) / np.pi + 0.5) - - def deriv(self, m, v=None): - alpha = self.slope - sig1, sig2, c = m[0], m[1], m[2:] - if self.logSigma: - sig1, sig2 = np.exp(sig1), np.exp(sig2) - # 2D - if self.mesh.dim == 2: - X = self.mesh.cell_centers[:, 0] - Y = self.mesh.cell_centers[:, 1] - - if self.normal == "x": - f = self.spl(Y) - X - elif self.normal == "y": - f = self.spl(X) - Y - else: - raise (Exception("Input for normal = X or Y or Z")) - # 3D - elif self.mesh.dim == 3: - X = self.mesh.cell_centers[:, 0] - Y = self.mesh.cell_centers[:, 1] - Z = self.mesh.cell_centers[:, 2] - - if self.normal == "x": - zb = self.ptsv[0] - zt = self.ptsv[1] - flines = (self.spl["splt"](Y) - self.spl["splb"](Y)) * (Z - zb) / ( - zt - zb - ) + self.spl["splb"](Y) - f = flines - X - # elif self.normal =='Y': - # elif self.normal =='Z': - else: - raise (Exception("Not Implemented for Y and Z, your turn :)")) - - if self.logSigma: - g1 = -(np.arctan(alpha * f) / np.pi + 0.5) * sig1 + sig1 - g2 = (np.arctan(alpha * f) / np.pi + 0.5) * sig2 - else: - g1 = -(np.arctan(alpha * f) / np.pi + 0.5) + 1.0 - g2 = np.arctan(alpha * f) / np.pi + 0.5 - - if self.mesh.dim == 2: - g3 = np.zeros((self.mesh.nC, self.npts)) - if self.normal == "y": - # Here we use perturbation to compute sensitivity - # TODO: bit more generalization of this ... - # Modfications for X and Z directions ... - for i in range(np.size(self.pts)): - ctemp = c[i] - ind = np.argmin(abs(self.mesh.cell_centers_y - ctemp)) - ca = c.copy() - cb = c.copy() - dy = self.mesh.h[1][ind] * 1.5 - ca[i] = ctemp + dy - cb[i] = ctemp - dy - spla = UnivariateSpline(self.pts, ca, k=self.order, s=0) - splb = UnivariateSpline(self.pts, cb, k=self.order, s=0) - fderiv = (spla(X) - splb(X)) / (2 * dy) - g3[:, i] = ( - sdiag(alpha * (sig2 - sig1) / (1.0 + (alpha * f) ** 2) / np.pi) - * fderiv - ) - - elif self.mesh.dim == 3: - g3 = np.zeros((self.mesh.nC, self.npts * 2)) - if self.normal == "x": - # Here we use perturbation to compute sensitivity - for i in range(self.npts * 2): - ctemp = c[i] - ind = np.argmin(abs(self.mesh.cell_centers_y - ctemp)) - ca = c.copy() - cb = c.copy() - dy = self.mesh.h[1][ind] * 1.5 - ca[i] = ctemp + dy - cb[i] = ctemp - dy - - # treat bottom boundary - if i < self.npts: - splba = UnivariateSpline( - self.pts, ca[: self.npts], k=self.order, s=0 - ) - splbb = UnivariateSpline( - self.pts, cb[: self.npts], k=self.order, s=0 - ) - flinesa = ( - (self.spl["splt"](Y) - splba(Y)) * (Z - zb) / (zt - zb) - + splba(Y) - - X - ) - flinesb = ( - (self.spl["splt"](Y) - splbb(Y)) * (Z - zb) / (zt - zb) - + splbb(Y) - - X - ) - - # treat top boundary - else: - splta = UnivariateSpline( - self.pts, ca[self.npts :], k=self.order, s=0 - ) - spltb = UnivariateSpline( - self.pts, ca[self.npts :], k=self.order, s=0 - ) - flinesa = ( - (self.spl["splt"](Y) - splta(Y)) * (Z - zb) / (zt - zb) - + splta(Y) - - X - ) - flinesb = ( - (self.spl["splt"](Y) - spltb(Y)) * (Z - zb) / (zt - zb) - + spltb(Y) - - X - ) - fderiv = (flinesa - flinesb) / (2 * dy) - g3[:, i] = ( - sdiag(alpha * (sig2 - sig1) / (1.0 + (alpha * f) ** 2) / np.pi) - * fderiv - ) - else: - raise (Exception("Not Implemented for Y and Z, your turn :)")) - - if v is not None: - return sp.csr_matrix(np.c_[g1, g2, g3]) * v - return sp.csr_matrix(np.c_[g1, g2, g3]) - - @property - def is_linear(self): - return False - - -class BaseParametric(IdentityMap): - """Base class for parametric mappings from simple geological structures to meshes. - - Parameters - ---------- - mesh : discretize.BaseMesh - A discretize mesh - indActive : numpy.ndarray, optional - Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* - or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. - slope : float, optional - Directly set the scaling parameter *slope* which sets the sharpness of boundaries - between units. - slopeFact : float, optional - Set sharpness of boundaries between units based on minimum cell size. If set, - the scalaing parameter *slope = slopeFact / dh*. - - """ - - def __init__(self, mesh, slope=None, slopeFact=1.0, indActive=None, **kwargs): - super(BaseParametric, self).__init__(mesh, **kwargs) - self.indActive = indActive - self.slopeFact = slopeFact - if slope is not None: - self.slope = slope - - @property - def slope(self): - """Defines the sharpness of the boundaries. - - Returns - ------- - float - """ - return self._slope - - @slope.setter - def slope(self, value): - self._slope = validate_float("slope", value, min_val=0.0) - - @property - def slopeFact(self): - """Defines the slope scaled by the mesh. - - Returns - ------- - float - """ - return self._slopeFact - - @slopeFact.setter - def slopeFact(self, value): - self._slopeFact = validate_float("slopeFact", value, min_val=0.0) - self.slope = self._slopeFact / self.mesh.edge_lengths.min() - - @property - def indActive(self): - return self._indActive - - @indActive.setter - def indActive(self, value): - if value is not None: - value = validate_active_indices("indActive", value, self.mesh.n_cells) - self._indActive = value - - @property - def x(self): - """X cell center locations (active) for the output of the mapping. - - Returns - ------- - (n_active) numpy.ndarray - X cell center locations (active) for the output of the mapping. - """ - if getattr(self, "_x", None) is None: - if self.mesh.dim == 1: - self._x = [ - ( - self.mesh.cell_centers - if self.indActive is None - else self.mesh.cell_centers[self.indActive] - ) - ][0] - else: - self._x = [ - ( - self.mesh.cell_centers[:, 0] - if self.indActive is None - else self.mesh.cell_centers[self.indActive, 0] - ) - ][0] - return self._x - - @property - def y(self): - """Y cell center locations (active) for the output of the mapping. - - Returns - ------- - (n_active) numpy.ndarray - Y cell center locations (active) for the output of the mapping. - """ - if getattr(self, "_y", None) is None: - if self.mesh.dim > 1: - self._y = [ - ( - self.mesh.cell_centers[:, 1] - if self.indActive is None - else self.mesh.cell_centers[self.indActive, 1] - ) - ][0] - else: - self._y = None - return self._y - - @property - def z(self): - """Z cell center locations (active) for the output of the mapping. - - Returns - ------- - (n_active) numpy.ndarray - Z cell center locations (active) for the output of the mapping. - """ - if getattr(self, "_z", None) is None: - if self.mesh.dim > 2: - self._z = [ - ( - self.mesh.cell_centers[:, 2] - if self.indActive is None - else self.mesh.cell_centers[self.indActive, 2] - ) - ][0] - else: - self._z = None - return self._z - - def _atanfct(self, val, slope): - return np.arctan(slope * val) / np.pi + 0.5 - - def _atanfctDeriv(self, val, slope): - # d/dx(atan(x)) = 1/(1+x**2) - x = slope * val - dx = -slope - return (1.0 / (1 + x**2)) / np.pi * dx - - @property - def is_linear(self): - return False - - -class ParametricLayer(BaseParametric): - r"""Mapping for a horizontal layer within a wholespace. - - This mapping is used when the cells lying below the Earth's surface can - be parameterized by horizontal layer within a homogeneous medium. - The model is defined by the physical property value for the background - (:math:`\sigma_0`), the physical property value for the layer - (:math:`\sigma_1`), the elevation for the middle of the layer (:math:`z_L`) - and the thickness of the layer :math:`h`. - - For this mapping, the set of input model parameters are organized: - - .. math:: - \mathbf{m} = [\sigma_0, \;\sigma_1,\; z_L , \; h] - - The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh - is given by: - - .. math:: - - \mathbf{u}(\mathbf{m}) = \sigma_0 + \frac{(\sigma_1 - \sigma_0)}{\pi} \Bigg [ - \arctan \Bigg ( a \bigg ( \mathbf{z_c} - z_L + \frac{h}{2} \bigg ) \Bigg ) - - \arctan \Bigg ( a \bigg ( \mathbf{z_c} - z_L - \frac{h}{2} \bigg ) \Bigg ) \Bigg ] - - where :math:`\mathbf{z_c}` is a vectors containing the vertical cell center - locations for all active cells in the mesh, and :math:`a` is a - parameter which defines the sharpness of the boundaries between the layer - and the background. - - Parameters - ---------- - mesh : discretize.BaseMesh - A discretize mesh - indActive : numpy.ndarray - Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* - or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. - slope : float - Directly define the constant *a* in the mapping function which defines the - sharpness of the boundaries. - slopeFact : float - Scaling factor for the sharpness of the boundaries based on cell size. - Using this option, we set *a = slopeFact / dh*. - - Examples - -------- - In this example, we define a layer in a wholespace whose interface is sharp. - We construct the mapping from the model to the set of active cells - (i.e. below the surface), We then use an active cells mapping to map from - the set of active cells to all cells in the mesh. - - >>> from simpeg.maps import ParametricLayer, InjectActiveCells - >>> from discretize import TensorMesh - >>> import numpy as np - >>> import matplotlib.pyplot as plt - - >>> dh = 0.25*np.ones(40) - >>> mesh = TensorMesh([dh, dh]) - >>> ind_active = mesh.cell_centers[:, 1] < 8 - - >>> sig0, sig1, zL, h = 5., 10., 4., 2 - >>> model = np.r_[sig0, sig1, zL, h] - - >>> layer_map = ParametricLayer( - >>> mesh, indActive=ind_active, slope=4 - >>> ) - >>> act_map = InjectActiveCells(mesh, ind_active, 0.) - - >>> fig = plt.figure(figsize=(5, 5)) - >>> ax = fig.add_subplot(111) - >>> mesh.plot_image(act_map * layer_map * model, ax=ax) - - """ - - def __init__(self, mesh, **kwargs): - super().__init__(mesh, **kwargs) - - @property - def nP(self): - """Number of model parameters the mapping acts on; i.e 4 - - Returns - ------- - int - Returns an integer value of *4*. - """ - return 4 - - @property - def shape(self): - """Dimensions of the mapping - - Returns - ------- - tuple of int - Where *nP=4* is the number of parameters the mapping acts on - and *nAct* is the number of active cells in the mesh, **shape** - returns a tuple (*nAct* , *4*). - """ - if self.indActive is not None: - return (sum(self.indActive), self.nP) - return (self.mesh.nC, self.nP) - - def mDict(self, m): - r"""Return model parameters as a dictionary. - - For a model :math:`\mathbf{m} = [\sigma_0, \;\sigma_1,\; z_L , \; h]`, - **mDict** returns a dictionary:: - - {"val_background": m[0], "val_layer": m[1], "layer_center": m[2], "layer_thickness": m[3]} - - Returns - ------- - dict - The model as a dictionary - """ - return { - "val_background": m[0], - "val_layer": m[1], - "layer_center": m[2], - "layer_thickness": m[3], - } - - def _atanLayer(self, mDict): - if self.mesh.dim == 2: - z = self.y - elif self.mesh.dim == 3: - z = self.z - - layer_bottom = mDict["layer_center"] - mDict["layer_thickness"] / 2.0 - layer_top = mDict["layer_center"] + mDict["layer_thickness"] / 2.0 - - return self._atanfct(z - layer_bottom, self.slope) * self._atanfct( - z - layer_top, -self.slope - ) - - def _atanLayerDeriv_layer_center(self, mDict): - if self.mesh.dim == 2: - z = self.y - elif self.mesh.dim == 3: - z = self.z - - layer_bottom = mDict["layer_center"] - mDict["layer_thickness"] / 2.0 - layer_top = mDict["layer_center"] + mDict["layer_thickness"] / 2.0 - - return self._atanfctDeriv(z - layer_bottom, self.slope) * self._atanfct( - z - layer_top, -self.slope - ) + self._atanfct(z - layer_bottom, self.slope) * self._atanfctDeriv( - z - layer_top, -self.slope - ) - - def _atanLayerDeriv_layer_thickness(self, mDict): - if self.mesh.dim == 2: - z = self.y - elif self.mesh.dim == 3: - z = self.z - - layer_bottom = mDict["layer_center"] - mDict["layer_thickness"] / 2.0 - layer_top = mDict["layer_center"] + mDict["layer_thickness"] / 2.0 - - return -0.5 * self._atanfctDeriv(z - layer_bottom, self.slope) * self._atanfct( - z - layer_top, -self.slope - ) + 0.5 * self._atanfct(z - layer_bottom, self.slope) * self._atanfctDeriv( - z - layer_top, -self.slope - ) - - def layer_cont(self, mDict): - return mDict["val_background"] + ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayer(mDict) - - def _transform(self, m): - mDict = self.mDict(m) - return self.layer_cont(mDict) - - def _deriv_val_background(self, mDict): - return np.ones_like(self.x) - self._atanLayer(mDict) - - def _deriv_val_layer(self, mDict): - return self._atanLayer(mDict) - - def _deriv_layer_center(self, mDict): - return ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayerDeriv_layer_center(mDict) - - def _deriv_layer_thickness(self, mDict): - return ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayerDeriv_layer_thickness(mDict) - - def deriv(self, m): - r"""Derivative of the mapping with respect to the input parameters. - - Let :math:`\mathbf{m} = [\sigma_0, \;\sigma_1,\; z_L , \; h]` be the set of - model parameters the defines a layer within a wholespace. The mapping - :math:`\mathbf{u}(\mathbf{m})`from the parameterized model to all - active cells is given by: - - .. math:: - \mathbf{u}(\mathbf{m}) = \sigma_0 + \frac{(\sigma_1 - \sigma_0)}{\pi} \Bigg [ - \arctan \Bigg ( a \bigg ( \mathbf{z_c} - z_L + \frac{h}{2} \bigg ) \Bigg ) - - \arctan \Bigg ( a \bigg ( \mathbf{z_c} - z_L - \frac{h}{2} \bigg ) \Bigg ) \Bigg ] - - where :math:`\mathbf{z_c}` is a vectors containing the vertical cell center - locations for all active cells in the mesh. The derivative of the mapping - with respect to the model parameters is a ``numpy.ndarray`` of - shape (*nAct*, *4*) given by: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = - \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_0} \;\; - \frac{\partial \mathbf{u}}{\partial \sigma_1} \;\; - \frac{\partial \mathbf{u}}{\partial z_L} \;\; - \frac{\partial \mathbf{u}}{\partial h} - \Bigg ] - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - - mDict = self.mDict(m) - - return sp.csr_matrix( - np.vstack( - [ - self._deriv_val_background(mDict), - self._deriv_val_layer(mDict), - self._deriv_layer_center(mDict), - self._deriv_layer_thickness(mDict), - ] - ).T - ) - - -class ParametricBlock(BaseParametric): - r"""Mapping for a rectangular block within a wholespace. - - This mapping is used when the cells lying below the Earth's surface can - be parameterized by rectangular block within a homogeneous medium. - The model is defined by the physical property value for the background - (:math:`\sigma_0`), the physical property value for the block - (:math:`\sigma_b`), parameters for the center of the block - (:math:`x_b [,y_b, z_b]`) and parameters for the dimensions along - each Cartesian direction (:math:`dx [,dy, dz]`) - - For this mapping, the set of input model parameters are organized: - - .. math:: - \mathbf{m} = \begin{cases} - 1D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx] \\ - 2D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx,\; y_b , \; dy] \\ - 3D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx,\; y_b , \; dy,\; z_b , \; dz] - \end{cases} - - The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh - is given by: - - .. math:: - - \mathbf{u}(\mathbf{m}) = \sigma_0 + (\sigma_b - \sigma_0) \bigg [ \frac{1}{2} + - \pi^{-1} \arctan \bigg ( a \, \boldsymbol{\eta} \big ( - x_b, y_b, z_b, dx, dy, dz \big ) \bigg ) \bigg ] - - where *a* is a parameter that impacts the sharpness of the arctan function, and - - .. math:: - \boldsymbol{\eta} \big ( x_b, y_b, z_b, dx, dy, dz \big ) = 1 - - \sum_{\xi \in (x,y,z)} \bigg [ \bigg ( \frac{2(\boldsymbol{\xi_c} - \xi_b)}{d\xi} \bigg )^2 + \varepsilon^2 - \bigg ]^{p/2} - - Parameters :math:`p` and :math:`\varepsilon` define the parameters of the Ekblom - function. :math:`\boldsymbol{\xi_c}` is a place holder for vectors containing - the x, [y and z] cell center locations of the mesh, :math:`\xi_b` is a placeholder - for the x[, y and z] location for the center of the block, and :math:`d\xi` is a - placeholder for the x[, y and z] dimensions of the block. - - Parameters - ---------- - mesh : discretize.BaseMesh - A discretize mesh - indActive : numpy.ndarray - Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* - or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. - slope : float - Directly define the constant *a* in the mapping function which defines the - sharpness of the boundaries. - slopeFact : float - Scaling factor for the sharpness of the boundaries based on cell size. - Using this option, we set *a = slopeFact / dh*. - epsilon : float - Epsilon value used in the ekblom representation of the block - p : float - p-value used in the ekblom representation of the block. - - Examples - -------- - In this example, we define a rectangular block in a wholespace whose - interface is sharp. We construct the mapping from the model to the - set of active cells (i.e. below the surface), We then use an active - cells mapping to map from the set of active cells to all cells in the mesh. - - >>> from simpeg.maps import ParametricBlock, InjectActiveCells - >>> from discretize import TensorMesh - >>> import numpy as np - >>> import matplotlib.pyplot as plt - - >>> dh = 0.5*np.ones(20) - >>> mesh = TensorMesh([dh, dh]) - >>> ind_active = mesh.cell_centers[:, 1] < 8 - - >>> sig0, sigb, xb, Lx, yb, Ly = 5., 10., 5., 4., 4., 2. - >>> model = np.r_[sig0, sigb, xb, Lx, yb, Ly] - - >>> block_map = ParametricBlock(mesh, indActive=ind_active) - >>> act_map = InjectActiveCells(mesh, ind_active, 0.) - - >>> fig = plt.figure(figsize=(5, 5)) - >>> ax = fig.add_subplot(111) - >>> mesh.plot_image(act_map * block_map * model, ax=ax) - - """ - - def __init__(self, mesh, epsilon=1e-6, p=10, **kwargs): - self.epsilon = epsilon - self.p = p - super(ParametricBlock, self).__init__(mesh, **kwargs) - - @property - def epsilon(self): - """epsilon value used in the ekblom representation of the block. - - Returns - ------- - float - """ - return self._epsilon - - @epsilon.setter - def epsilon(self, value): - self._epsilon = validate_float("epsilon", value, min_val=0.0) - - @property - def p(self): - """p-value used in the ekblom representation of the block. - - Returns - ------- - float - """ - return self._p - - @p.setter - def p(self, value): - self._p = validate_float("p", value, min_val=0.0) - - @property - def nP(self): - """Number of parameters the mapping acts on. - - Returns - ------- - int - The number of the parameters defining the model depends on the dimension - of the mesh. *nP* - - - =4 for a 1D mesh - - =6 for a 2D mesh - - =8 for a 3D mesh - """ - if self.mesh.dim == 1: - return 4 - if self.mesh.dim == 2: - return 6 - elif self.mesh.dim == 3: - return 8 - - @property - def shape(self): - """Dimensions of the mapping - - Returns - ------- - tuple of int - Where *nP* is the number of parameters the mapping acts on - and *nAct* is the number of active cells in the mesh, **shape** - returns a tuple (*nAct* , *nP*). - """ - if self.indActive is not None: - return (sum(self.indActive), self.nP) - return (self.mesh.nC, self.nP) - - def _mDict1d(self, m): - return { - "val_background": m[0], - "val_block": m[1], - "x0": m[2], - "dx": m[3], - } - - def _mDict2d(self, m): - mDict = self._mDict1d(m) - mDict.update( - { - # 'theta_x': m[4], - "y0": m[4], - "dy": m[5], - # 'theta_y': m[7] - } - ) - return mDict - - def _mDict3d(self, m): - mDict = self._mDict2d(m) - mDict.update( - { - "z0": m[6], - "dz": m[7], - # 'theta_z': m[10] - } - ) - return mDict - - def mDict(self, m): - r"""Return model parameters as a dictionary. - - Returns - ------- - dict - The model as a dictionary - """ - return getattr(self, "_mDict{}d".format(self.mesh.dim))(m) - - def _ekblom(self, val): - return (val**2 + self.epsilon**2) ** (self.p / 2.0) - - def _ekblomDeriv(self, val): - return (self.p / 2) * (val**2 + self.epsilon**2) ** ((self.p / 2) - 1) * 2 * val - - # def _rotation(self, mDict): - # if self.mesh.dim == 2: - - # elif self.mesh.dim == 3: - - def _block1D(self, mDict): - return 1 - (self._ekblom((self.x - mDict["x0"]) / (0.5 * mDict["dx"]))) - - def _block2D(self, mDict): - return 1 - ( - self._ekblom((self.x - mDict["x0"]) / (0.5 * mDict["dx"])) - + self._ekblom((self.y - mDict["y0"]) / (0.5 * mDict["dy"])) - ) - - def _block3D(self, mDict): - return 1 - ( - self._ekblom((self.x - mDict["x0"]) / (0.5 * mDict["dx"])) - + self._ekblom((self.y - mDict["y0"]) / (0.5 * mDict["dy"])) - + self._ekblom((self.z - mDict["z0"]) / (0.5 * mDict["dz"])) - ) - - def _transform(self, m): - mDict = self.mDict(m) - return mDict["val_background"] + ( - mDict["val_block"] - mDict["val_background"] - ) * self._atanfct( - getattr(self, "_block{}D".format(self.mesh.dim))(mDict), slope=self.slope - ) - - def _deriv_val_background(self, mDict): - return 1 - self._atanfct( - getattr(self, "_block{}D".format(self.mesh.dim))(mDict), slope=self.slope - ) - - def _deriv_val_block(self, mDict): - return self._atanfct( - getattr(self, "_block{}D".format(self.mesh.dim))(mDict), slope=self.slope - ) - - def _deriv_center_block(self, mDict, orientation): - x = getattr(self, orientation) - x0 = mDict["{}0".format(orientation)] - dx = mDict["d{}".format(orientation)] - return (mDict["val_block"] - mDict["val_background"]) * ( - self._atanfctDeriv( - getattr(self, "_block{}D".format(self.mesh.dim))(mDict), - slope=self.slope, - ) - * (self._ekblomDeriv((x - x0) / (0.5 * dx))) - / -(0.5 * dx) - ) - - def _deriv_width_block(self, mDict, orientation): - x = getattr(self, orientation) - x0 = mDict["{}0".format(orientation)] - dx = mDict["d{}".format(orientation)] - return (mDict["val_block"] - mDict["val_background"]) * ( - self._atanfctDeriv( - getattr(self, "_block{}D".format(self.mesh.dim))(mDict), - slope=self.slope, - ) - * (self._ekblomDeriv((x - x0) / (0.5 * dx)) * (-(x - x0) / (0.5 * dx**2))) - ) - - def _deriv1D(self, mDict): - return np.vstack( - [ - self._deriv_val_background(mDict), - self._deriv_val_block(mDict), - self._deriv_center_block(mDict, "x"), - self._deriv_width_block(mDict, "x"), - ] - ).T - - def _deriv2D(self, mDict): - return np.vstack( - [ - self._deriv_val_background(mDict), - self._deriv_val_block(mDict), - self._deriv_center_block(mDict, "x"), - self._deriv_width_block(mDict, "x"), - self._deriv_center_block(mDict, "y"), - self._deriv_width_block(mDict, "y"), - ] - ).T - - def _deriv3D(self, mDict): - return np.vstack( - [ - self._deriv_val_background(mDict), - self._deriv_val_block(mDict), - self._deriv_center_block(mDict, "x"), - self._deriv_width_block(mDict, "x"), - self._deriv_center_block(mDict, "y"), - self._deriv_width_block(mDict, "y"), - self._deriv_center_block(mDict, "z"), - self._deriv_width_block(mDict, "z"), - ] - ).T - - def deriv(self, m): - r"""Derivative of the mapping with respect to the input parameters. - - Let :math:`\mathbf{m} = [\sigma_0, \;\sigma_1,\; x_b, \; dx, (\; y_b, \; dy, \; z_b , dz)]` - be the set of model parameters the defines a block/ellipsoid within a wholespace. - The mapping :math:`\mathbf{u}(\mathbf{m})` from the parameterized model to all - active cells is given by: - - The derivative of the mapping :math:`\mathbf{u}(\mathbf{m})` with respect to - the model parameters is a ``numpy.ndarray`` of shape (*nAct*, *nP*) given by: - - .. math:: - \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \Bigg [ - \frac{\partial \mathbf{u}}{\partial \sigma_0} \;\; - \frac{\partial \mathbf{u}}{\partial \sigma_1} \;\; - \frac{\partial \mathbf{u}}{\partial x_b} \;\; - \frac{\partial \mathbf{u}}{\partial dx} \;\; - \frac{\partial \mathbf{u}}{\partial y_b} \;\; - \frac{\partial \mathbf{u}}{\partial dy} \;\; - \frac{\partial \mathbf{u}}{\partial z_b} \;\; - \frac{\partial \mathbf{u}}{\partial dz} - \Bigg ) \Bigg ] - - Parameters - ---------- - m : (nP) numpy.ndarray - A vector representing a set of model parameters - v : (nP) numpy.ndarray - If not ``None``, the method returns the derivative times the vector *v* - - Returns - ------- - scipy.sparse.csr_matrix - Derivative of the mapping with respect to the model parameters. If the - input argument *v* is not ``None``, the method returns the derivative times - the vector *v*. - """ - return sp.csr_matrix( - getattr(self, "_deriv{}D".format(self.mesh.dim))(self.mDict(m)) - ) - - -class ParametricEllipsoid(ParametricBlock): - r"""Mapping for a rectangular block within a wholespace. - - This mapping is used when the cells lying below the Earth's surface can - be parameterized by an ellipsoid within a homogeneous medium. - The model is defined by the physical property value for the background - (:math:`\sigma_0`), the physical property value for the layer - (:math:`\sigma_b`), parameters for the center of the ellipsoid - (:math:`x_b [,y_b, z_b]`) and parameters for the dimensions along - each Cartesian direction (:math:`dx [,dy, dz]`) - - For this mapping, the set of input model parameters are organized: - - .. math:: - \mathbf{m} = \begin{cases} - 1D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx] \\ - 2D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx,\; y_b , \; dy] \\ - 3D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx,\; y_b , \; dy,\; z_b , \; dz] - \end{cases} - - The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh - is given by: - - .. math:: - - \mathbf{u}(\mathbf{m}) = \sigma_0 + (\sigma_b - \sigma_0) \bigg [ \frac{1}{2} + - \pi^{-1} \arctan \bigg ( a \, \boldsymbol{\eta} \big ( - x_b, y_b, z_b, dx, dy, dz \big ) \bigg ) \bigg ] - - where *a* is a parameter that impacts the sharpness of the arctan function, and - - .. math:: - \boldsymbol{\eta} \big ( x_b, y_b, z_b, dx, dy, dz \big ) = 1 - - \sum_{\xi \in (x,y,z)} \bigg [ \bigg ( \frac{2(\boldsymbol{\xi_c} - \xi_b)}{d\xi} \bigg )^2 + \varepsilon^2 - \bigg ] - - :math:`\boldsymbol{\xi_c}` is a place holder for vectors containing - the x, [y and z] cell center locations of the mesh, :math:`\xi_b` is a placeholder - for the x[, y and z] location for the center of the block, and :math:`d\xi` is a - placeholder for the x[, y and z] dimensions of the block. - - Parameters - ---------- - mesh : discretize.BaseMesh - A discretize mesh - indActive : numpy.ndarray - Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* - or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. - slope : float - Directly define the constant *a* in the mapping function which defines the - sharpness of the boundaries. - slopeFact : float - Scaling factor for the sharpness of the boundaries based on cell size. - Using this option, we set *a = slopeFact / dh*. - epsilon : float - Epsilon value used in the ekblom representation of the block - - Examples - -------- - In this example, we define an ellipse in a wholespace whose - interface is sharp. We construct the mapping from the model to the - set of active cells (i.e. below the surface), We then use an active - cells mapping to map from the set of active cells to all cells in the mesh. - - >>> from simpeg.maps import ParametricEllipsoid, InjectActiveCells - >>> from discretize import TensorMesh - >>> import numpy as np - >>> import matplotlib.pyplot as plt - - >>> dh = 0.5*np.ones(20) - >>> mesh = TensorMesh([dh, dh]) - >>> ind_active = mesh.cell_centers[:, 1] < 8 - - >>> sig0, sigb, xb, Lx, yb, Ly = 5., 10., 5., 4., 4., 3. - >>> model = np.r_[sig0, sigb, xb, Lx, yb, Ly] - - >>> ellipsoid_map = ParametricEllipsoid(mesh, indActive=ind_active) - >>> act_map = InjectActiveCells(mesh, ind_active, 0.) - - >>> fig = plt.figure(figsize=(5, 5)) - >>> ax = fig.add_subplot(111) - >>> mesh.plot_image(act_map * ellipsoid_map * model, ax=ax) - - """ - - def __init__(self, mesh, **kwargs): - super(ParametricEllipsoid, self).__init__(mesh, p=2, **kwargs) - - -class ParametricCasingAndLayer(ParametricLayer): - """ - Parametric layered space with casing. - - .. code:: python - - m = [val_background, - val_layer, - val_casing, - val_insideCasing, - layer_center, - layer_thickness, - casing_radius, - casing_thickness, - casing_bottom, - casing_top - ] - - """ - - def __init__(self, mesh, **kwargs): - assert ( - mesh._meshType == "CYL" - ), "Parametric Casing in a layer map only works for a cyl mesh." - - super().__init__(mesh, **kwargs) - - @property - def nP(self): - return 10 - - @property - def shape(self): - if self.indActive is not None: - return (sum(self.indActive), self.nP) - return (self.mesh.nC, self.nP) - - def mDict(self, m): - # m = [val_background, val_layer, val_casing, val_insideCasing, - # layer_center, layer_thickness, casing_radius, casing_thickness, - # casing_bottom, casing_top] - - return { - "val_background": m[0], - "val_layer": m[1], - "val_casing": m[2], - "val_insideCasing": m[3], - "layer_center": m[4], - "layer_thickness": m[5], - "casing_radius": m[6], - "casing_thickness": m[7], - "casing_bottom": m[8], - "casing_top": m[9], - } - - def casing_a(self, mDict): - return mDict["casing_radius"] - 0.5 * mDict["casing_thickness"] - - def casing_b(self, mDict): - return mDict["casing_radius"] + 0.5 * mDict["casing_thickness"] - - def _atanCasingLength(self, mDict): - return self._atanfct(self.z - mDict["casing_top"], -self.slope) * self._atanfct( - self.z - mDict["casing_bottom"], self.slope - ) - - def _atanCasingLengthDeriv_casing_top(self, mDict): - return self._atanfctDeriv( - self.z - mDict["casing_top"], -self.slope - ) * self._atanfct(self.z - mDict["casing_bottom"], self.slope) - - def _atanCasingLengthDeriv_casing_bottom(self, mDict): - return self._atanfct( - self.z - mDict["casing_top"], -self.slope - ) * self._atanfctDeriv(self.z - mDict["casing_bottom"], self.slope) - - def _atanInsideCasing(self, mDict): - return self._atanCasingLength(mDict) * self._atanfct( - self.x - self.casing_a(mDict), -self.slope - ) - - def _atanInsideCasingDeriv_casing_radius(self, mDict): - return self._atanCasingLength(mDict) * self._atanfctDeriv( - self.x - self.casing_a(mDict), -self.slope - ) - - def _atanInsideCasingDeriv_casing_thickness(self, mDict): - return ( - self._atanCasingLength(mDict) - * -0.5 - * self._atanfctDeriv(self.x - self.casing_a(mDict), -self.slope) - ) - - def _atanInsideCasingDeriv_casing_top(self, mDict): - return self._atanCasingLengthDeriv_casing_top(mDict) * self._atanfct( - self.x - self.casing_a(mDict), -self.slope - ) - - def _atanInsideCasingDeriv_casing_bottom(self, mDict): - return self._atanCasingLengthDeriv_casing_bottom(mDict) * self._atanfct( - self.x - self.casing_a(mDict), -self.slope - ) - - def _atanCasing(self, mDict): - return ( - self._atanCasingLength(mDict) - * self._atanfct(self.x - self.casing_a(mDict), self.slope) - * self._atanfct(self.x - self.casing_b(mDict), -self.slope) - ) - - def _atanCasingDeriv_casing_radius(self, mDict): - return self._atanCasingLength(mDict) * ( - self._atanfctDeriv(self.x - self.casing_a(mDict), self.slope) - * self._atanfct(self.x - self.casing_b(mDict), -self.slope) - + self._atanfct(self.x - self.casing_a(mDict), self.slope) - * self._atanfctDeriv(self.x - self.casing_b(mDict), -self.slope) - ) - - def _atanCasingDeriv_casing_thickness(self, mDict): - return self._atanCasingLength(mDict) * ( - -0.5 - * self._atanfctDeriv(self.x - self.casing_a(mDict), self.slope) - * self._atanfct(self.x - self.casing_b(mDict), -self.slope) - + self._atanfct(self.x - self.casing_a(mDict), self.slope) - * 0.5 - * self._atanfctDeriv(self.x - self.casing_b(mDict), -self.slope) - ) - - def _atanCasingDeriv_casing_bottom(self, mDict): - return ( - self._atanCasingLengthDeriv_casing_bottom(mDict) - * self._atanfct(self.x - self.casing_a(mDict), self.slope) - * self._atanfct(self.x - self.casing_b(mDict), -self.slope) - ) - - def _atanCasingDeriv_casing_top(self, mDict): - return ( - self._atanCasingLengthDeriv_casing_top(mDict) - * self._atanfct(self.x - self.casing_a(mDict), self.slope) - * self._atanfct(self.x - self.casing_b(mDict), -self.slope) - ) - - def layer_cont(self, mDict): - # contribution from the layered background - return mDict["val_background"] + ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayer(mDict) - - def _transform(self, m): - mDict = self.mDict(m) - - # assemble the model - layer = self.layer_cont(mDict) - casing = (mDict["val_casing"] - layer) * self._atanCasing(mDict) - insideCasing = (mDict["val_insideCasing"] - layer) * self._atanInsideCasing( - mDict - ) - - return layer + casing + insideCasing - - def _deriv_val_background(self, mDict): - # contribution from the layered background - d_layer_cont_dval_background = 1.0 - self._atanLayer(mDict) - d_casing_cont_dval_background = ( - -1.0 * d_layer_cont_dval_background * self._atanCasing(mDict) - ) - d_insideCasing_cont_dval_background = ( - -1.0 * d_layer_cont_dval_background * self._atanInsideCasing(mDict) - ) - return ( - d_layer_cont_dval_background - + d_casing_cont_dval_background - + d_insideCasing_cont_dval_background - ) - - def _deriv_val_layer(self, mDict): - d_layer_cont_dval_layer = self._atanLayer(mDict) - d_casing_cont_dval_layer = ( - -1.0 * d_layer_cont_dval_layer * self._atanCasing(mDict) - ) - d_insideCasing_cont_dval_layer = ( - -1.0 * d_layer_cont_dval_layer * self._atanInsideCasing(mDict) - ) - return ( - d_layer_cont_dval_layer - + d_casing_cont_dval_layer - + d_insideCasing_cont_dval_layer - ) - - def _deriv_val_casing(self, mDict): - d_layer_cont_dval_casing = 0.0 - d_casing_cont_dval_casing = self._atanCasing(mDict) - d_insideCasing_cont_dval_casing = 0.0 - return ( - d_layer_cont_dval_casing - + d_casing_cont_dval_casing - + d_insideCasing_cont_dval_casing - ) - - def _deriv_val_insideCasing(self, mDict): - d_layer_cont_dval_insideCasing = 0.0 - d_casing_cont_dval_insideCasing = 0.0 - d_insideCasing_cont_dval_insideCasing = self._atanInsideCasing(mDict) - return ( - d_layer_cont_dval_insideCasing - + d_casing_cont_dval_insideCasing - + d_insideCasing_cont_dval_insideCasing - ) - - def _deriv_layer_center(self, mDict): - d_layer_cont_dlayer_center = ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayerDeriv_layer_center(mDict) - d_casing_cont_dlayer_center = -d_layer_cont_dlayer_center * self._atanCasing( - mDict - ) - d_insideCasing_cont_dlayer_center = ( - -d_layer_cont_dlayer_center * self._atanInsideCasing(mDict) - ) - return ( - d_layer_cont_dlayer_center - + d_casing_cont_dlayer_center - + d_insideCasing_cont_dlayer_center - ) - - def _deriv_layer_thickness(self, mDict): - d_layer_cont_dlayer_thickness = ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayerDeriv_layer_thickness(mDict) - d_casing_cont_dlayer_thickness = ( - -d_layer_cont_dlayer_thickness * self._atanCasing(mDict) - ) - d_insideCasing_cont_dlayer_thickness = ( - -d_layer_cont_dlayer_thickness * self._atanInsideCasing(mDict) - ) - return ( - d_layer_cont_dlayer_thickness - + d_casing_cont_dlayer_thickness - + d_insideCasing_cont_dlayer_thickness - ) - - def _deriv_casing_radius(self, mDict): - layer = self.layer_cont(mDict) - d_layer_cont_dcasing_radius = 0.0 - d_casing_cont_dcasing_radius = ( - mDict["val_casing"] - layer - ) * self._atanCasingDeriv_casing_radius(mDict) - d_insideCasing_cont_dcasing_radius = ( - mDict["val_insideCasing"] - layer - ) * self._atanInsideCasingDeriv_casing_radius(mDict) - return ( - d_layer_cont_dcasing_radius - + d_casing_cont_dcasing_radius - + d_insideCasing_cont_dcasing_radius - ) - - def _deriv_casing_thickness(self, mDict): - d_layer_cont_dcasing_thickness = 0.0 - d_casing_cont_dcasing_thickness = ( - mDict["val_casing"] - self.layer_cont(mDict) - ) * self._atanCasingDeriv_casing_thickness(mDict) - d_insideCasing_cont_dcasing_thickness = ( - mDict["val_insideCasing"] - self.layer_cont(mDict) - ) * self._atanInsideCasingDeriv_casing_thickness(mDict) - return ( - d_layer_cont_dcasing_thickness - + d_casing_cont_dcasing_thickness - + d_insideCasing_cont_dcasing_thickness - ) - - def _deriv_casing_bottom(self, mDict): - d_layer_cont_dcasing_bottom = 0.0 - d_casing_cont_dcasing_bottom = ( - mDict["val_casing"] - self.layer_cont(mDict) - ) * self._atanCasingDeriv_casing_bottom(mDict) - d_insideCasing_cont_dcasing_bottom = ( - mDict["val_insideCasing"] - self.layer_cont(mDict) - ) * self._atanInsideCasingDeriv_casing_bottom(mDict) - return ( - d_layer_cont_dcasing_bottom - + d_casing_cont_dcasing_bottom - + d_insideCasing_cont_dcasing_bottom - ) - - def _deriv_casing_top(self, mDict): - d_layer_cont_dcasing_top = 0.0 - d_casing_cont_dcasing_top = ( - mDict["val_casing"] - self.layer_cont(mDict) - ) * self._atanCasingDeriv_casing_top(mDict) - d_insideCasing_cont_dcasing_top = ( - mDict["val_insideCasing"] - self.layer_cont(mDict) - ) * self._atanInsideCasingDeriv_casing_top(mDict) - return ( - d_layer_cont_dcasing_top - + d_casing_cont_dcasing_top - + d_insideCasing_cont_dcasing_top - ) - - def deriv(self, m): - mDict = self.mDict(m) - - return sp.csr_matrix( - np.vstack( - [ - self._deriv_val_background(mDict), - self._deriv_val_layer(mDict), - self._deriv_val_casing(mDict), - self._deriv_val_insideCasing(mDict), - self._deriv_layer_center(mDict), - self._deriv_layer_thickness(mDict), - self._deriv_casing_radius(mDict), - self._deriv_casing_thickness(mDict), - self._deriv_casing_bottom(mDict), - self._deriv_casing_top(mDict), - ] - ).T - ) - - -class ParametricBlockInLayer(ParametricLayer): - """ - Parametric Block in a Layered Space - - For 2D: - - .. code:: python - - m = [val_background, - val_layer, - val_block, - layer_center, - layer_thickness, - block_x0, - block_dx - ] - - For 3D: - - .. code:: python - - m = [val_background, - val_layer, - val_block, - layer_center, - layer_thickness, - block_x0, - block_y0, - block_dx, - block_dy - ] - - **Required** - - :param discretize.base.BaseMesh mesh: SimPEG Mesh, 2D or 3D - - **Optional** - - :param float slopeFact: arctan slope factor - divided by the minimum h - spacing to give the slope of the arctan - functions - :param float slope: slope of the arctan function - :param numpy.ndarray indActive: bool vector with - - """ - - def __init__(self, mesh, **kwargs): - super().__init__(mesh, **kwargs) - - @property - def nP(self): - if self.mesh.dim == 2: - return 7 - elif self.mesh.dim == 3: - return 9 - - @property - def shape(self): - if self.indActive is not None: - return (sum(self.indActive), self.nP) - return (self.mesh.nC, self.nP) - - def _mDict2d(self, m): - return { - "val_background": m[0], - "val_layer": m[1], - "val_block": m[2], - "layer_center": m[3], - "layer_thickness": m[4], - "x0": m[5], - "dx": m[6], - } - - def _mDict3d(self, m): - return { - "val_background": m[0], - "val_layer": m[1], - "val_block": m[2], - "layer_center": m[3], - "layer_thickness": m[4], - "x0": m[5], - "y0": m[6], - "dx": m[7], - "dy": m[8], - } - - def mDict(self, m): - if self.mesh.dim == 2: - return self._mDict2d(m) - elif self.mesh.dim == 3: - return self._mDict3d(m) - - def xleft(self, mDict): - return mDict["x0"] - 0.5 * mDict["dx"] - - def xright(self, mDict): - return mDict["x0"] + 0.5 * mDict["dx"] - - def yleft(self, mDict): - return mDict["y0"] - 0.5 * mDict["dy"] - - def yright(self, mDict): - return mDict["y0"] + 0.5 * mDict["dy"] - - def _atanBlock2d(self, mDict): - return ( - self._atanLayer(mDict) - * self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - ) - - def _atanBlock2dDeriv_layer_center(self, mDict): - return ( - self._atanLayerDeriv_layer_center(mDict) - * self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - ) - - def _atanBlock2dDeriv_layer_thickness(self, mDict): - return ( - self._atanLayerDeriv_layer_thickness(mDict) - * self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - ) - - def _atanBlock2dDeriv_x0(self, mDict): - return self._atanLayer(mDict) * ( - ( - self._atanfctDeriv(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - ) - + ( - self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfctDeriv(self.x - self.xright(mDict), -self.slope) - ) - ) - - def _atanBlock2dDeriv_dx(self, mDict): - return self._atanLayer(mDict) * ( - ( - self._atanfctDeriv(self.x - self.xleft(mDict), self.slope) - * -0.5 - * self._atanfct(self.x - self.xright(mDict), -self.slope) - ) - + ( - self._atanfct(self.x - self.xleft(mDict), self.slope) - * 0.5 - * self._atanfctDeriv(self.x - self.xright(mDict), -self.slope) - ) - ) - - def _atanBlock3d(self, mDict): - return ( - self._atanLayer(mDict) - * self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - * self._atanfct(self.y - self.yleft(mDict), self.slope) - * self._atanfct(self.y - self.yright(mDict), -self.slope) - ) - - def _atanBlock3dDeriv_layer_center(self, mDict): - return ( - self._atanLayerDeriv_layer_center(mDict) - * self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - * self._atanfct(self.y - self.yleft(mDict), self.slope) - * self._atanfct(self.y - self.yright(mDict), -self.slope) - ) - - def _atanBlock3dDeriv_layer_thickness(self, mDict): - return ( - self._atanLayerDeriv_layer_thickness(mDict) - * self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - * self._atanfct(self.y - self.yleft(mDict), self.slope) - * self._atanfct(self.y - self.yright(mDict), -self.slope) - ) - - def _atanBlock3dDeriv_x0(self, mDict): - return self._atanLayer(mDict) * ( - ( - self._atanfctDeriv(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - * self._atanfct(self.y - self.yleft(mDict), self.slope) - * self._atanfct(self.y - self.yright(mDict), -self.slope) - ) - + ( - self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfctDeriv(self.x - self.xright(mDict), -self.slope) - * self._atanfct(self.y - self.yleft(mDict), self.slope) - * self._atanfct(self.y - self.yright(mDict), -self.slope) - ) - ) - - def _atanBlock3dDeriv_y0(self, mDict): - return self._atanLayer(mDict) * ( - ( - self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - * self._atanfctDeriv(self.y - self.yleft(mDict), self.slope) - * self._atanfct(self.y - self.yright(mDict), -self.slope) - ) - + ( - self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - * self._atanfct(self.y - self.yleft(mDict), self.slope) - * self._atanfctDeriv(self.y - self.yright(mDict), -self.slope) - ) - ) - - def _atanBlock3dDeriv_dx(self, mDict): - return self._atanLayer(mDict) * ( - ( - self._atanfctDeriv(self.x - self.xleft(mDict), self.slope) - * -0.5 - * self._atanfct(self.x - self.xright(mDict), -self.slope) - * self._atanfct(self.y - self.yleft(mDict), self.slope) - * self._atanfct(self.y - self.yright(mDict), -self.slope) - ) - + ( - self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfctDeriv(self.x - self.xright(mDict), -self.slope) - * 0.5 - * self._atanfct(self.y - self.yleft(mDict), self.slope) - * self._atanfct(self.y - self.yright(mDict), -self.slope) - ) - ) - - def _atanBlock3dDeriv_dy(self, mDict): - return self._atanLayer(mDict) * ( - ( - self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - * self._atanfctDeriv(self.y - self.yleft(mDict), self.slope) - * -0.5 - * self._atanfct(self.y - self.yright(mDict), -self.slope) - ) - + ( - self._atanfct(self.x - self.xleft(mDict), self.slope) - * self._atanfct(self.x - self.xright(mDict), -self.slope) - * self._atanfct(self.y - self.yleft(mDict), self.slope) - * self._atanfctDeriv(self.y - self.yright(mDict), -self.slope) - * 0.5 - ) - ) - - def _transform2d(self, m): - mDict = self.mDict(m) - # assemble the model - # contribution from the layered background - layer_cont = mDict["val_background"] + ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayer(mDict) - - # perturbation due to the blocks - block_cont = (mDict["val_block"] - layer_cont) * self._atanBlock2d(mDict) - - return layer_cont + block_cont - - def _deriv2d_val_background(self, mDict): - d_layer_dval_background = np.ones_like(self.x) - self._atanLayer(mDict) - d_block_dval_background = (-d_layer_dval_background) * self._atanBlock2d(mDict) - return d_layer_dval_background + d_block_dval_background - - def _deriv2d_val_layer(self, mDict): - d_layer_dval_layer = self._atanLayer(mDict) - d_block_dval_layer = (-d_layer_dval_layer) * self._atanBlock2d(mDict) - return d_layer_dval_layer + d_block_dval_layer - - def _deriv2d_val_block(self, mDict): - d_layer_dval_block = 0.0 - d_block_dval_block = (1.0 - d_layer_dval_block) * self._atanBlock2d(mDict) - return d_layer_dval_block + d_block_dval_block - - def _deriv2d_layer_center(self, mDict): - d_layer_dlayer_center = ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayerDeriv_layer_center(mDict) - d_block_dlayer_center = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock2dDeriv_layer_center( - mDict - ) - d_layer_dlayer_center * self._atanBlock2d( - mDict - ) - return d_layer_dlayer_center + d_block_dlayer_center - - def _deriv2d_layer_thickness(self, mDict): - d_layer_dlayer_thickness = ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayerDeriv_layer_thickness(mDict) - d_block_dlayer_thickness = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock2dDeriv_layer_thickness( - mDict - ) - d_layer_dlayer_thickness * self._atanBlock2d( - mDict - ) - return d_layer_dlayer_thickness + d_block_dlayer_thickness - - def _deriv2d_x0(self, mDict): - d_layer_dx0 = 0.0 - d_block_dx0 = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock2dDeriv_x0(mDict) - return d_layer_dx0 + d_block_dx0 - - def _deriv2d_dx(self, mDict): - d_layer_ddx = 0.0 - d_block_ddx = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock2dDeriv_dx(mDict) - return d_layer_ddx + d_block_ddx - - def _deriv2d(self, m): - mDict = self.mDict(m) - - return np.vstack( - [ - self._deriv2d_val_background(mDict), - self._deriv2d_val_layer(mDict), - self._deriv2d_val_block(mDict), - self._deriv2d_layer_center(mDict), - self._deriv2d_layer_thickness(mDict), - self._deriv2d_x0(mDict), - self._deriv2d_dx(mDict), - ] - ).T - - def _transform3d(self, m): - # parse model - mDict = self.mDict(m) - - # assemble the model - # contribution from the layered background - layer_cont = mDict["val_background"] + ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayer(mDict) - # perturbation due to the block - block_cont = (mDict["val_block"] - layer_cont) * self._atanBlock3d(mDict) - - return layer_cont + block_cont - - def _deriv3d_val_background(self, mDict): - d_layer_dval_background = np.ones_like(self.x) - self._atanLayer(mDict) - d_block_dval_background = (-d_layer_dval_background) * self._atanBlock3d(mDict) - return d_layer_dval_background + d_block_dval_background - - def _deriv3d_val_layer(self, mDict): - d_layer_dval_layer = self._atanLayer(mDict) - d_block_dval_layer = (-d_layer_dval_layer) * self._atanBlock3d(mDict) - return d_layer_dval_layer + d_block_dval_layer - - def _deriv3d_val_block(self, mDict): - d_layer_dval_block = 0.0 - d_block_dval_block = (1.0 - d_layer_dval_block) * self._atanBlock3d(mDict) - return d_layer_dval_block + d_block_dval_block - - def _deriv3d_layer_center(self, mDict): - d_layer_dlayer_center = ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayerDeriv_layer_center(mDict) - d_block_dlayer_center = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock3dDeriv_layer_center( - mDict - ) - d_layer_dlayer_center * self._atanBlock3d( - mDict - ) - return d_layer_dlayer_center + d_block_dlayer_center - - def _deriv3d_layer_thickness(self, mDict): - d_layer_dlayer_thickness = ( - mDict["val_layer"] - mDict["val_background"] - ) * self._atanLayerDeriv_layer_thickness(mDict) - d_block_dlayer_thickness = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock3dDeriv_layer_thickness( - mDict - ) - d_layer_dlayer_thickness * self._atanBlock3d( - mDict - ) - return d_layer_dlayer_thickness + d_block_dlayer_thickness - - def _deriv3d_x0(self, mDict): - d_layer_dx0 = 0.0 - d_block_dx0 = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock3dDeriv_x0(mDict) - return d_layer_dx0 + d_block_dx0 - - def _deriv3d_y0(self, mDict): - d_layer_dy0 = 0.0 - d_block_dy0 = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock3dDeriv_y0(mDict) - return d_layer_dy0 + d_block_dy0 - - def _deriv3d_dx(self, mDict): - d_layer_ddx = 0.0 - d_block_ddx = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock3dDeriv_dx(mDict) - return d_layer_ddx + d_block_ddx - - def _deriv3d_dy(self, mDict): - d_layer_ddy = 0.0 - d_block_ddy = ( - mDict["val_block"] - self.layer_cont(mDict) - ) * self._atanBlock3dDeriv_dy(mDict) - return d_layer_ddy + d_block_ddy - - def _deriv3d(self, m): - mDict = self.mDict(m) - - return np.vstack( - [ - self._deriv3d_val_background(mDict), - self._deriv3d_val_layer(mDict), - self._deriv3d_val_block(mDict), - self._deriv3d_layer_center(mDict), - self._deriv3d_layer_thickness(mDict), - self._deriv3d_x0(mDict), - self._deriv3d_y0(mDict), - self._deriv3d_dx(mDict), - self._deriv3d_dy(mDict), - ] - ).T - - def _transform(self, m): - if self.mesh.dim == 2: - return self._transform2d(m) - elif self.mesh.dim == 3: - return self._transform3d(m) - - def deriv(self, m): - if self.mesh.dim == 2: - return sp.csr_matrix(self._deriv2d(m)) - elif self.mesh.dim == 3: - return sp.csr_matrix(self._deriv3d(m)) - - -class TileMap(IdentityMap): - """ - Mapping for tiled inversion. - - Uses volume averaging to map a model defined on a global mesh to the - local mesh. Everycell in the local mesh must also be in the global mesh. - """ - - def __init__( - self, - global_mesh, - global_active, - local_mesh, - tol=1e-8, - components=1, - **kwargs, - ): - """ - Parameters - ---------- - global_mesh : discretize.TreeMesh - Global TreeMesh defining the entire domain. - global_active : numpy.ndarray of bool or int - Defines the active cells in the global mesh. - local_mesh : discretize.TreeMesh - Local TreeMesh for the simulation. - tol : float, optional - Tolerance to avoid zero division - components : int, optional - Number of components in the model. E.g. a vector model in 3D would have 3 - components. - """ - super().__init__(mesh=None, **kwargs) - self._global_mesh = validate_type( - "global_mesh", global_mesh, discretize.TreeMesh, cast=False - ) - self._local_mesh = validate_type( - "local_mesh", local_mesh, discretize.TreeMesh, cast=False - ) - - self._global_active = validate_active_indices( - "global_active", global_active, self.global_mesh.n_cells - ) - - self._tol = validate_float("tol", tol, min_val=0.0, inclusive_min=False) - self._components = validate_integer("components", components, min_val=1) - - # trigger creation of P - self.P - - @property - def global_mesh(self): - """Global TreeMesh defining the entire domain. - - Returns - ------- - discretize.TreeMesh - """ - return self._global_mesh - - @property - def local_mesh(self): - """Local TreeMesh defining the local domain. - - Returns - ------- - discretize.TreeMesh - """ - return self._local_mesh - - @property - def global_active(self): - """Defines the active cells in the global mesh. - - Returns - ------- - (global_mesh.n_cells) numpy.ndarray of bool - """ - return self._global_active - - @property - def local_active(self): - """ - This is the local_active of the global_active used in the global problem. - - Returns - ------- - (local_mesh.n_cells) numpy.ndarray of bool - """ - return self._local_active - - @property - def tol(self): - """Tolerance to avoid zero division. - - Returns - ------- - float - """ - return self._tol - - @property - def components(self): - """Number of components in the model. - - Returns - ------- - int - """ - return self._components - - @property - def P(self): - """ - Set the projection matrix with partial volumes - """ - if getattr(self, "_P", None) is None: - in_local = self.local_mesh._get_containing_cell_indexes( - self.global_mesh.cell_centers - ) - - P = ( - sp.csr_matrix( - ( - self.global_mesh.cell_volumes, - (in_local, np.arange(self.global_mesh.nC)), - ), - shape=(self.local_mesh.nC, self.global_mesh.nC), - ) - * speye(self.global_mesh.nC)[:, self.global_active] - ) - - self._local_active = mkvc(np.sum(P, axis=1) > 0) - - P = P[self.local_active, :] - - self._P = sp.block_diag( - [ - sdiag(1.0 / self.local_mesh.cell_volumes[self.local_active]) * P - for ii in range(self.components) - ] - ) - - return self._P - - def _transform(self, m): - return self.P * m - - @property - def shape(self): - """ - Shape of the matrix operation (number of indices x nP) - """ - return self.P.shape - - def deriv(self, m, v=None): - """ - :param numpy.ndarray m: model - :rtype: scipy.sparse.csr_matrix - :return: derivative of transformed model - """ - if v is not None: - return self.P * v - return self.P - - -############################################################################### -# # -# Maps for petrophsyics clusters # -# # -############################################################################### - - -class PolynomialPetroClusterMap(IdentityMap): - """ - Modeling polynomial relationships between physical properties - - Parameters - ---------- - coeffxx : array_like, optional - Coefficients for the xx component. Default is [0, 1] - coeffxy : array_like, optional - Coefficients for the xy component. Default is [0] - coeffyx : array_like, optional - Coefficients for the yx component. Default is [0] - coeffyy : array_like, optional - Coefficients for the yy component. Default is [0, 1] - """ - - def __init__( - self, - coeffxx=None, - coeffxy=None, - coeffyx=None, - coeffyy=None, - mesh=None, - nP=None, - **kwargs, - ): - if coeffxx is None: - coeffxx = np.r_[0.0, 1.0] - if coeffxy is None: - coeffxy = np.r_[0.0] - if coeffyx is None: - coeffyx = np.r_[0.0] - if coeffyy is None: - coeffyy = np.r_[0.0, 1.0] - - self._coeffxx = validate_ndarray_with_shape("coeffxx", coeffxx, shape=("*",)) - self._coeffxy = validate_ndarray_with_shape("coeffxy", coeffxy, shape=("*",)) - self._coeffyx = validate_ndarray_with_shape("coeffyx", coeffyx, shape=("*",)) - self._coeffyy = validate_ndarray_with_shape("coeffyy", coeffyy, shape=("*",)) - - self._polynomialxx = polynomial.Polynomial(self.coeffxx) - self._polynomialxy = polynomial.Polynomial(self.coeffxy) - self._polynomialyx = polynomial.Polynomial(self.coeffyx) - self._polynomialyy = polynomial.Polynomial(self.coeffyy) - self._polynomialxx_deriv = self._polynomialxx.deriv(m=1) - self._polynomialxy_deriv = self._polynomialxy.deriv(m=1) - self._polynomialyx_deriv = self._polynomialyx.deriv(m=1) - self._polynomialyy_deriv = self._polynomialyy.deriv(m=1) - - super().__init__(mesh=mesh, nP=nP, **kwargs) - - @property - def coeffxx(self): - """Coefficients for the xx component. - - Returns - ------- - numpy.ndarray - """ - return self._coeffxx - - @property - def coeffxy(self): - """Coefficients for the xy component. - - Returns - ------- - numpy.ndarray - """ - return self._coeffxy - - @property - def coeffyx(self): - """Coefficients for the yx component. - - Returns - ------- - numpy.ndarray - """ - return self._coeffyx - - @property - def coeffyy(self): - """Coefficients for the yy component. - - Returns - ------- - numpy.ndarray - """ - return self._coeffyy - - def _transform(self, m): - out = m.copy() - out[:, 0] = self._polynomialxx(m[:, 0]) + self._polynomialxy(m[:, 1]) - out[:, 1] = self._polynomialyx(m[:, 0]) + self._polynomialyy(m[:, 1]) - return out - - def inverse(self, D): - r""" - :param numpy.array D: physical property - :rtype: numpy.array - :return: model - - The *transformInverse* changes the physical property into the - model. - - .. math:: - - m = \log{\sigma} - - """ - raise NotImplementedError("Inverse is not implemented.") - - def _derivmatrix(self, m): - return np.r_[ - [ - [ - self._polynomialxx_deriv(m[:, 0])[0], - self._polynomialyx_deriv(m[:, 0])[0], - ], - [ - self._polynomialxy_deriv(m[:, 1])[0], - self._polynomialyy_deriv(m[:, 1])[0], - ], - ] - ] - - def deriv(self, m, v=None): - """""" - if v is None: - out = self._derivmatrix(m.reshape(-1, 2)) - return out - else: - out = np.dot(self._derivmatrix(m.reshape(-1, 2)), v.reshape(2, -1)) - return out - - @property - def is_linear(self): - return False diff --git a/simpeg/maps/__init__.py b/simpeg/maps/__init__.py new file mode 100644 index 0000000000..e912ef6bef --- /dev/null +++ b/simpeg/maps/__init__.py @@ -0,0 +1,36 @@ +from ._base import ( + ComboMap, + IdentityMap, + LinearMap, + Projection, + SphericalSystem, + SumMap, + TileMap, + Wires, +) +from ._clustering import PolynomialPetroClusterMap +from ._injection import Mesh2Mesh, InjectActiveCells +from ._property_maps import ( + ChiMap, + ComplexMap, + EffectiveSusceptibilityMap, + ExpMap, + LogisticSigmoidMap, + LogMap, + MuRelative, + ReciprocalMap, + SelfConsistentEffectiveMedium, + Weighting, +) +from ._parametric import ( + BaseParametric, + ParametricBlock, + ParametricBlockInLayer, + ParametricCasingAndLayer, + ParametricCircleMap, + ParametricEllipsoid, + ParametricLayer, + ParametricPolyMap, + ParametricSplineMap, +) +from ._surjection import Surject2Dto3D, SurjectFull, SurjectUnits, SurjectVertical1D diff --git a/simpeg/maps/_base.py b/simpeg/maps/_base.py new file mode 100644 index 0000000000..9369f1fb75 --- /dev/null +++ b/simpeg/maps/_base.py @@ -0,0 +1,1341 @@ +""" +Base and general map classes. +""" + +from collections import namedtuple +import discretize +import numpy as np +import scipy.sparse as sp +from scipy.sparse import csr_matrix as csr +from discretize.tests import check_derivative +from discretize.utils import Zero, Identity, mkvc, speye, sdiag +import uuid + +from ..utils import ( + mat_utils, + validate_type, + validate_ndarray_with_shape, + validate_list_of_types, + validate_active_indices, + validate_integer, + validate_float, +) +from ..typing import RandomSeed + + +class IdentityMap: + r"""Identity mapping and the base mapping class for all other SimPEG mappings. + + The ``IdentityMap`` class is used to define the mapping when + the model parameters are the same as the parameters used in the forward + simulation. For a discrete set of model parameters :math:`\mathbf{m}`, + the mapping :math:`\mathbf{u}(\mathbf{m})` is equivalent to applying + the identity matrix; i.e.: + + .. math:: + + \mathbf{u}(\mathbf{m}) = \mathbf{Im} + + The ``IdentityMap`` also acts as the base class for all other SimPEG mapping classes. + + Using the *mesh* or *nP* input arguments, the dimensions of the corresponding + mapping operator can be permanently set; i.e. (*mesh.nC*, *mesh.nC*) or (*nP*, *nP*). + However if both input arguments *mesh* and *nP* are ``None``, the shape of + mapping operator is arbitrary and can act on any vector; i.e. has shape (``*``, ``*``). + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal the number + of mesh cells. + nP : int, or '*' + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + """ + + def __init__(self, mesh=None, nP=None, **kwargs): + if (isinstance(nP, str) and nP == "*") or nP is None: + if mesh is not None: + nP = mesh.n_cells + else: + nP = "*" + else: + try: + nP = int(nP) + except (TypeError, ValueError) as err: + raise TypeError( + f"Unrecognized input of {repr(nP)} for number of parameters, must be an integer or '*'." + ) from err + self.mesh = mesh + self._nP = nP + + self._uuid = uuid.uuid4() + + super().__init__(**kwargs) + + @property + def nP(self): + r"""Number of parameters the mapping acts on. + + Returns + ------- + int or ``*`` + Number of parameters that the mapping acts on. Returns an + ``int`` if the dimensions of the mapping are set. If the + mapping can act on a vector of any length, ``*`` is returned. + """ + if self._nP != "*": + return int(self._nP) + if self.mesh is None: + return "*" + return int(self.mesh.nC) + + @property + def shape(self): + r"""Dimensions of the mapping operator + + The dimensions of the mesh depend on the input arguments used + during instantiation. If *mesh* is used to define the + identity map, the shape of mapping operator is (*mesh.nC*, *mesh.nC*). + If *nP* is used to define the identity map, the mapping operator + has dimensions (*nP*, *nP*). However if both *mesh* and *nP* are + used to define the identity map, the mapping will have shape + (*mesh.nC*, *nP*)! And if *mesh* and *nP* were ``None`` when + instantiating, the mapping has dimensions (``*``, ``*``) and may + act on a vector of any length. + + Returns + ------- + tuple + Dimensions of the mapping operator. If the dimensions of + the mapping are set, the return is a tuple (``int``,``int``). + If the mapping can act on a vector of arbitrary length, the + return is a tuple (``*``, ``*``). + """ + if self.mesh is None: + return (self.nP, self.nP) + return (self.mesh.nC, self.nP) + + def _transform(self, m): + """ + Changes the model into the physical property. + + .. note:: + + This can be called by the __mul__ property against a + :meth:numpy.ndarray. + + :param numpy.ndarray m: model + :rtype: numpy.ndarray + :return: transformed model + + """ + return m + + def inverse(self, D): + """ + The transform inverse is not implemented. + """ + raise NotImplementedError("The transform inverse is not implemented.") + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the input parameters. + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix or numpy.ndarray + Derivative of the mapping with respect to the model parameters. For an + identity mapping, this is just a sparse identity matrix. If the input + argument *v* is not ``None``, the method returns the derivative times + the vector *v*; which in this case is just *v*. + + Notes + ----- + Let :math:`\mathbf{m}` be a set of model parameters and let :math:`\mathbf{I}` + denote the identity map. Where the identity mapping acting on the model parameters + can be expressed as: + + .. math:: + \mathbf{u} = \mathbf{I m}, + + the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect + to the model parameters; i.e.: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{I} + + For the Identity map **deriv** simply returns a sparse identity matrix. + """ + if v is not None: + return v + if isinstance(self.nP, (int, np.integer)): + return sp.identity(self.nP) + return Identity() + + def test(self, m=None, num=4, random_seed: RandomSeed | None = None, **kwargs): + """Derivative test for the mapping. + + This test validates the mapping by performing a convergence test. + + Parameters + ---------- + m : (nP) numpy.ndarray + Starting vector of model parameters for the derivative test + num : int + Number of iterations for the derivative test + random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed used for generating a random array for ``m`` if it's + None. It can either be an int, a predefined Numpy random number + generator, or any valid input to ``numpy.random.default_rng``. + kwargs: dict + Keyword arguments and associated values in the dictionary must + match those used in :meth:`discretize.tests.check_derivative` + + Returns + ------- + bool + Returns ``True`` if the test passes + """ + print("Testing {0!s}".format(str(self))) + rng = np.random.default_rng(seed=random_seed) + if m is None: + m = rng.uniform(size=self.nP) + if "plotIt" not in kwargs: + kwargs["plotIt"] = False + + assert isinstance( + self.nP, (int, np.integer) + ), "nP must be an integer for {}".format(self.__class__.__name__) + return check_derivative( + lambda m: [self * m, self.deriv(m)], m, num=num, random_seed=rng, **kwargs + ) + + def _assertMatchesPair(self, pair): + assert ( + isinstance(self, pair) + or isinstance(self, ComboMap) + and isinstance(self.maps[0], pair) + ), "Mapping object must be an instance of a {0!s} class.".format(pair.__name__) + + def __mul__(self, val): + if isinstance(val, IdentityMap): + if ( + not (self.shape[1] == "*" or val.shape[0] == "*") + and not self.shape[1] == val.shape[0] + ): + raise ValueError( + "Dimension mismatch in {0!s} and {1!s}.".format(str(self), str(val)) + ) + return ComboMap([self, val]) + + elif isinstance(val, np.ndarray): + if not self.shape[1] == "*" and not self.shape[1] == val.shape[0]: + raise ValueError( + "Dimension mismatch in {0!s} and np.ndarray{1!s}.".format( + str(self), str(val.shape) + ) + ) + return self._transform(val) + + elif isinstance(val, Zero): + return Zero() + + raise Exception( + "Unrecognized data type to multiply. Try a map or a numpy.ndarray!" + "You used a {} of type {}".format(val, type(val)) + ) + + def dot(self, map1): + r"""Multiply two mappings to create a :class:`simpeg.maps.ComboMap`. + + Let :math:`\mathbf{f}_1` and :math:`\mathbf{f}_2` represent two mapping functions. + Where :math:`\mathbf{m}` represents a set of input model parameters, + the ``dot`` method is used to create a combination mapping: + + .. math:: + u(\mathbf{m}) = f_2(f_1(\mathbf{m})) + + Where :math:`\mathbf{f_1} : M \rightarrow K_1` and acts on the + model first, and :math:`\mathbf{f_2} : K_1 \rightarrow K_2`, the combination + mapping :math:`\mathbf{u} : M \rightarrow K_2`. + + When using the **dot** method, the input argument *map1* represents the first + mapping that is be applied and *self* represents the second mapping + that is be applied. Therefore, the correct syntax for using this method is:: + + self.dot(map1) + + + Parameters + ---------- + map1 : + A SimPEG mapping object. + + Examples + -------- + Here we create a combination mapping that 1) projects a single scalar to + a vector space of length 5, then takes the natural exponent. + + >>> import numpy as np + >>> from simpeg.maps import ExpMap, Projection + + >>> nP1 = 1 + >>> nP2 = 5 + >>> ind = np.zeros(nP1, dtype=int) + + >>> projection_map = Projection(nP1, ind) + >>> projection_map.shape + (5, 1) + + >>> exp_map = ExpMap(nP=5) + >>> exp_map.shape + (5, 5) + + >>> combo_map = exp_map.dot(projection_map) + >>> combo_map.shape + (5, 1) + + >>> m = np.array([2]) + >>> combo_map * m + array([7.3890561, 7.3890561, 7.3890561, 7.3890561, 7.3890561]) + + """ + return self.__mul__(map1) + + def __matmul__(self, map1): + return self.__mul__(map1) + + __numpy_ufunc__ = True + + def __add__(self, map1): + return SumMap([self, map1]) # error-checking done inside of the SumMap + + def __str__(self): + return "{0!s}({1!s},{2!s})".format( + self.__class__.__name__, self.shape[0], self.shape[1] + ) + + def __len__(self): + return 1 + + @property + def mesh(self): + """ + The mesh used for the mapping + + Returns + ------- + discretize.base.BaseMesh or None + """ + return self._mesh + + @mesh.setter + def mesh(self, value): + if value is not None: + value = validate_type("mesh", value, discretize.base.BaseMesh, cast=False) + self._mesh = value + + @property + def is_linear(self): + """Determine whether or not this mapping is a linear operation. + + Returns + ------- + bool + """ + return True + + +class ComboMap(IdentityMap): + r"""Combination mapping constructed by joining a set of other mappings. + + A ``ComboMap`` is a single mapping object made by joining a set + of basic mapping operations by chaining them together, in order. + When creating a ``ComboMap``, the user provides a list of SimPEG mapping objects they wish to join. + The order of the mappings in this list is from last to first; i.e. + :math:`[\mathbf{f}_n , ... , \mathbf{f}_2 , \mathbf{f}_1]`. + + The combination mapping :math:`\mathbf{u}(\mathbf{m})` that acts on a + set of input model parameters :math:`\mathbf{m}` is defined as: + + .. math:: + \mathbf{u}(\mathbf{m}) = f_n(f_{n-1}(\cdots f_1(f_0(\mathbf{m})))) + + Note that any time that you create your own combination mapping, + be sure to test that the derivative is correct. + + Parameters + ---------- + maps : list of simpeg.maps.IdentityMap + A ``list`` of SimPEG mapping objects. The ordering of the mapping + objects in the ``list`` is from last applied to first applied! + + Examples + -------- + Here we create a combination mapping that 1) projects a single scalar to + a vector space of length 5, then takes the natural exponent. + + >>> import numpy as np + >>> from simpeg.maps import ExpMap, Projection, ComboMap + + >>> nP1 = 1 + >>> nP2 = 5 + >>> ind = np.zeros(nP1, dtype=int) + + >>> projection_map = Projection(nP1, ind) + >>> projection_map.shape + (5, 1) + + >>> exp_map = ExpMap(nP=5) + >>> exp_map.shape + (5, 5) + + Recall that the order of the mapping objects is from last applied + to first applied. + + >>> map_list = [exp_map, projection_map] + >>> combo_map = ComboMap(map_list) + >>> combo_map.shape + (5, 1) + + >>> m = np.array([2.]) + >>> combo_map * m + array([7.3890561, 7.3890561, 7.3890561, 7.3890561, 7.3890561]) + + """ + + def __init__(self, maps, **kwargs): + super().__init__(mesh=None, **kwargs) + + self.maps = [] + for ii, m in enumerate(maps): + assert isinstance(m, IdentityMap), "Unrecognized data type, " + "inherit from an IdentityMap or ComboMap!" + + if ( + ii > 0 + and not (self.shape[1] == "*" or m.shape[0] == "*") + and not self.shape[1] == m.shape[0] + ): + prev = self.maps[-1] + + raise ValueError( + "Dimension mismatch in map[{0!s}] ({1!s}, {2!s}) " + "and map[{3!s}] ({4!s}, {5!s}).".format( + prev.__class__.__name__, + prev.shape[0], + prev.shape[1], + m.__class__.__name__, + m.shape[0], + m.shape[1], + ) + ) + + if np.any([isinstance(m, SumMap), isinstance(m, IdentityMap)]): + self.maps += [m] + elif isinstance(m, ComboMap): + self.maps += m.maps + else: + raise ValueError("Map[{0!s}] not supported", m.__class__.__name__) + + @property + def shape(self): + r"""Dimensions of the mapping. + + For a list of SimPEG mappings [:math:`\mathbf{f}_n,...,\mathbf{f}_1`] + that have been joined to create a ``ComboMap``, this method returns + the dimensions of the combination mapping. Recall that the ordering + of the list of mappings is from last to first. + + Returns + ------- + (2) tuple of int + Dimensions of the mapping operator. + """ + return (self.maps[0].shape[0], self.maps[-1].shape[1]) + + @property + def nP(self): + r"""Number of parameters the mapping acts on. + + Returns + ------- + int + Number of parameters that the mapping acts on. + """ + return self.maps[-1].nP + + def _transform(self, m): + for map_i in reversed(self.maps): + m = map_i * m + return m + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the input parameters. + + Any time that you create your own combination mapping, + be sure to test that the derivative is correct. + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. + If the input argument *v* is not ``None``, the method returns + the derivative times the vector *v*. + + Notes + ----- + Let :math:`\mathbf{m}` be a set of model parameters and let + [:math:`\mathbf{f}_n,...,\mathbf{f}_1`] be the list of SimPEG mappings joined + to create a combination mapping. Recall that the list of mappings is ordered + from last applied to first applied. + + Where the combination mapping acting on the model parameters + can be expressed as: + + .. math:: + \mathbf{u}(\mathbf{m}) = f_n(f_{n-1}(\cdots f_1(f_0(\mathbf{m})))) + + The **deriv** method returns the derivative of :math:`\mathbf{u}` with respect + to the model parameters. To do this, we use the chain rule, i.e.: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = + \frac{\partial \mathbf{f_n}}{\partial \mathbf{f_{n-1}}} + \cdots + \frac{\partial \mathbf{f_2}}{\partial \mathbf{f_{1}}} + \frac{\partial \mathbf{f_1}}{\partial \mathbf{m}} + """ + + if v is not None: + deriv = v + else: + deriv = 1 + + mi = m + for map_i in reversed(self.maps): + deriv = map_i.deriv(mi) * deriv + mi = map_i * mi + return deriv + + def __str__(self): + return "ComboMap[{0!s}]({1!s},{2!s})".format( + " * ".join([m.__str__() for m in self.maps]), self.shape[0], self.shape[1] + ) + + def __len__(self): + return len(self.maps) + + @property + def is_linear(self): + return all(m.is_linear for m in self.maps) + + +class LinearMap(IdentityMap): + """A generalized linear mapping. + + A simple map that implements the linear mapping, + + >>> y = A @ x + b + + Parameters + ---------- + A : (M, N) array_like, optional + The matrix operator, can be any object that implements `__matmul__` + and has a `shape` attribute. + b : (M) array_like, optional + Additive part of the linear operation. + """ + + def __init__(self, A, b=None, **kwargs): + kwargs.pop("mesh", None) + kwargs.pop("nP", None) + super().__init__(**kwargs) + self.A = A + self.b = b + + @property + def A(self): + """The linear operator matrix. + + Returns + ------- + LinearOperator + Must support matrix multiplication and have a shape attribute. + """ + return self._A + + @A.setter + def A(self, value): + if not hasattr(value, "__matmul__"): + raise TypeError( + f"{repr(value)} does not implement the matrix multiplication operator." + ) + if not hasattr(value, "shape"): + raise TypeError(f"{repr(value)} does not have a shape attribute.") + self._A = value + self._nP = value.shape[1] + self._shape = value.shape + + @property + def shape(self): + return self._shape + + @property + def b(self): + """Added part of the linear operation. + + Returns + ------- + numpy.ndarray + """ + return self._b + + @b.setter + def b(self, value): + if value is not None: + value = validate_ndarray_with_shape("b", value, shape=(self.shape[0],)) + self._b = value + + def _transform(self, m): + if self.b is None: + return self.A @ m + return self.A @ m + self.b + + def deriv(self, m, v=None): + if v is None: + return self.A + return self.A @ v + + +class Projection(IdentityMap): + r"""Projection mapping. + + ``Projection`` mapping can be used to project and/or rearange model + parameters. For a set of model parameter :math:`\mathbf{m}`, + the mapping :math:`\mathbf{u}(\mathbf{m})` can be defined by a linear + projection matrix :math:`\mathbf{P}` acting on the model, i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + + The number of model parameters the mapping acts on is + defined by *nP*. Projection and/or rearrangement of the parameters + is defined by *index*. Thus the dimensions of the mapping is + (*nInd*, *nP*). + + Parameters + ---------- + nP : int + Number of model parameters the mapping acts on + index : numpy.ndarray of int + Indexes defining the projection from the model space + + Examples + -------- + Here we define a mapping that rearranges and projects 2 model + parameters to a vector space spanning 4 parameters. + + >>> from simpeg.maps import Projection + >>> import numpy as np + + >>> nP = 2 + >>> index = np.array([1, 0, 1, 0], dtype=int) + >>> mapping = Projection(nP, index) + + >>> m = np.array([6, 8]) + >>> mapping * m + array([8, 6, 8, 6]) + + """ + + def __init__(self, nP, index, **kwargs): + assert isinstance( + index, (np.ndarray, slice, list) + ), "index must be a np.ndarray or slice, not {}".format(type(index)) + super(Projection, self).__init__(nP=nP, **kwargs) + + if isinstance(index, slice): + index = list(range(*index.indices(self.nP))) + + if isinstance(index, np.ndarray): + if index.dtype is np.dtype("bool"): + index = np.where(index)[0] + + self.index = index + self._shape = nI, nP = len(self.index), self.nP + + assert max(index) < nP, "maximum index must be less than {}".format(nP) + + # sparse projection matrix + self.P = sp.csr_matrix((np.ones(nI), (range(nI), self.index)), shape=(nI, nP)) + + def _transform(self, m): + return m[self.index] + + @property + def shape(self): + r"""Dimensions of the mapping. + + Returns + ------- + tuple + Where *nP* is the number of parameters the mapping acts on and + *nInd* is the length of the vector defining the mapping, the + dimensions of the mapping operator is a tuple of the + form (*nInd*, *nP*). + """ + return self._shape + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the input parameters. + + Let :math:`\mathbf{m}` be a set of model parameters and let :math:`\mathbf{P}` + be a matrix denoting the projection mapping. Where the projection mapping acting + on the model parameters can be expressed as: + + .. math:: + \mathbf{u} = \mathbf{P m}, + + the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect + to the model parameters; i.e.: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} + + Note that in this case, **deriv** simply returns a sparse projection matrix. + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + + if v is not None: + return self.P * v + return self.P + + +class SumMap(ComboMap): + """Combination map constructed by summing multiple mappings + to the same vector space. + + A map to add model parameters contributing to the + forward operation e.g. F(m) = F(g(x) + h(y)) + + Assumes that the model vectors defined by g(x) and h(y) + are equal in length. + Allows to assume different things about the model m: + i.e. parametric + voxel models + + Parameters + ---------- + maps : list + A list of SimPEG mapping objects that are being summed. + Each mapping object in the list must act on the same number + of model parameters and must map to the same vector space! + """ + + def __init__(self, maps, **kwargs): + maps = validate_list_of_types("maps", maps, IdentityMap) + + # skip ComboMap's init + super(ComboMap, self).__init__(mesh=None, **kwargs) + + self.maps = [] + for ii, m in enumerate(maps): + if not isinstance(m, IdentityMap): + raise TypeError( + "Unrecognized data type {}, inherit from an " + "IdentityMap!".format(type(m)) + ) + + if ( + ii > 0 + and not (self.shape == "*" or m.shape == "*") + and not self.shape == m.shape + ): + raise ValueError( + "Dimension mismatch in map[{0!s}] ({1!s}, {2!s}) " + "and map[{3!s}] ({4!s}, {5!s}).".format( + self.maps[0].__class__.__name__, + self.maps[0].shape[0], + self.maps[0].shape[1], + m.__class__.__name__, + m.shape[0], + m.shape[1], + ) + ) + + self.maps += [m] + + @property + def shape(self): + """Dimensions of the mapping. + + Returns + ------- + tuple + The dimensions of the mapping. A tuple of the form (``int``,``int``) + """ + return (self.maps[0].shape[0], self.maps[0].shape[1]) + + @property + def nP(self): + r"""Number of parameters the combined mapping acts on. + + Returns + ------- + int + Number of parameters that the mapping acts on. + """ + return self.maps[-1].shape[1] + + def _transform(self, m): + for ii, map_i in enumerate(self.maps): + m0 = m.copy() + m0 = map_i * m0 + + if ii == 0: + mout = m0 + else: + mout += m0 + return mout + + def deriv(self, m, v=None): + """Derivative of mapping with respect to the input parameters + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + + for ii, map_i in enumerate(self.maps): + m0 = m.copy() + + if v is not None: + deriv = v + else: + deriv = sp.eye(self.nP) + + deriv = map_i.deriv(m0, v=deriv) + if ii == 0: + sumDeriv = deriv + else: + sumDeriv += deriv + + return sumDeriv + + +class SphericalSystem(IdentityMap): + r"""Mapping vectors from spherical to Cartesian coordinates. + + Let :math:`\mathbf{m}` be a model containing the amplitudes + (:math:`\mathbf{a}`), azimuthal angles (:math:`\mathbf{t}`) + and radial angles (:math:`\mathbf{p}`) for a set of vectors + in spherical space such that: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{a} \\ \mathbf{t} \\ \mathbf{p} \end{bmatrix} + + ``SphericalSystem`` constructs a mapping :math:`\mathbf{u}(\mathbf{m}) + that converts the set of vectors in spherical coordinates to + their representation in Cartesian coordinates, i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = \begin{bmatrix} \mathbf{v_x} \\ \mathbf{v_y} \\ \mathbf{v_z} \end{bmatrix} + + where :math:`\mathbf{v_x}`, :math:`\mathbf{v_y}` and :math:`\mathbf{v_z}` + store the x, y and z components of the vectors, respectively. + + Using the *mesh* or *nP* input arguments, the dimensions of the corresponding + mapping operator can be permanently set; i.e. (*3\*mesh.nC*, *3\*mesh.nC*) or (*nP*, *nP*). + However if both input arguments *mesh* and *nP* are ``None``, the shape of + mapping operator is arbitrary and can act on any vector whose length + is a multiple of 3; i.e. has shape (``*``, ``*``). + + Notes + ----- + + In Cartesian space, the components of each vector are defined as + + .. math:: + \mathbf{v} = (v_x, v_y, v_z) + + In spherical coordinates, vectors are is defined as: + + .. math:: + \mathbf{v^\prime} = (a, t, p) + + where + + - :math:`a` is the amplitude of the vector + - :math:`t` is the azimuthal angle defined positive from vertical + - :math:`p` is the radial angle defined positive CCW from Easting + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal + *3\*mesh.nC* . + nP : int + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + """ + + def __init__(self, mesh=None, nP=None, **kwargs): + if nP is not None: + assert nP % 3 == 0, "Number of parameters must be a multiple of 3" + super().__init__(mesh, nP, **kwargs) + self.model = None + + def sphericalDeriv(self, model): + if getattr(self, "model", None) is None: + self.model = model + + if getattr(self, "_sphericalDeriv", None) is None or not all( + self.model == model + ): + self.model = model + + # Do a double projection to make sure the parameters are bounded + m_xyz = mat_utils.spherical2cartesian(model.reshape((-1, 3), order="F")) + m_atp = mat_utils.cartesian2spherical( + m_xyz.reshape((-1, 3), order="F") + ).reshape((-1, 3), order="F") + + nC = m_atp[:, 0].shape[0] + + dm_dx = sp.hstack( + [ + sp.diags(np.cos(m_atp[:, 1]) * np.cos(m_atp[:, 2]), 0), + sp.diags( + -m_atp[:, 0] * np.sin(m_atp[:, 1]) * np.cos(m_atp[:, 2]), 0 + ), + sp.diags( + -m_atp[:, 0] * np.cos(m_atp[:, 1]) * np.sin(m_atp[:, 2]), 0 + ), + ] + ) + + dm_dy = sp.hstack( + [ + sp.diags(np.cos(m_atp[:, 1]) * np.sin(m_atp[:, 2]), 0), + sp.diags( + -m_atp[:, 0] * np.sin(m_atp[:, 1]) * np.sin(m_atp[:, 2]), 0 + ), + sp.diags( + m_atp[:, 0] * np.cos(m_atp[:, 1]) * np.cos(m_atp[:, 2]), 0 + ), + ] + ) + + dm_dz = sp.hstack( + [ + sp.diags(np.sin(m_atp[:, 1]), 0), + sp.diags(m_atp[:, 0] * np.cos(m_atp[:, 1]), 0), + csr((nC, nC)), + ] + ) + + self._sphericalDeriv = sp.vstack([dm_dx, dm_dy, dm_dz]) + + return self._sphericalDeriv + + def _transform(self, model): + return mat_utils.spherical2cartesian(model.reshape((-1, 3), order="F")) + + def inverse(self, u): + r"""Maps vectors in Cartesian coordinates to spherical coordinates. + + Let :math:`\mathbf{v_x}`, :math:`\mathbf{v_y}` and :math:`\mathbf{v_z}` + store the x, y and z components of a set of vectors in Cartesian + coordinates such that: + + .. math:: + \mathbf{u} = \begin{bmatrix} \mathbf{x} \\ \mathbf{y} \\ \mathbf{z} \end{bmatrix} + + The inverse mapping recovers the vectors in spherical coordinates, i.e.: + + .. math:: + \mathbf{m}(\mathbf{u}) = \begin{bmatrix} \mathbf{a} \\ \mathbf{t} \\ \mathbf{p} \end{bmatrix} + + where :math:`\mathbf{a}` are the amplitudes, :math:`\mathbf{t}` are the + azimuthal angles and :math:`\mathbf{p}` are the radial angles. + + Parameters + ---------- + u : numpy.ndarray + The x, y and z components of a set of vectors in Cartesian coordinates. + If the mapping is defined for a mesh, the numpy.ndarray has length + *3\*mesh.nC* . + + Returns + ------- + numpy.ndarray + The amplitudes (:math:`\mathbf{a}`), azimuthal angles (:math:`\mathbf{t}`) + and radial angles (:math:`\mathbf{p}`) for the set of vectors in spherical + coordinates. If the mapping is defined for a mesh, the numpy.ndarray has length + *3\*mesh.nC* . + """ + return mat_utils.cartesian2spherical(u.reshape((-1, 3), order="F")) + + @property + def shape(self): + r"""Dimensions of the mapping + + The dimensions of the mesh depend on the input arguments used + during instantiation. If *mesh* is used to define the + mapping, the shape of mapping operator is (*3\*mesh.nC*, *3\*mesh.nC*). + If *nP* is used to define the identity map, the mapping operator + has dimensions (*nP*, *nP*). If *mesh* and *nP* were ``None`` when + instantiating, the mapping has dimensions (``*``, ``*``) and may + act on a vector whose length is a multiple of 3. + + Returns + ------- + tuple + Dimensions of the mapping operator. If the dimensions of + the mapping are set, the return is a tuple (``int``,``int``). + If the mapping can act on a vector of arbitrary length, the + return is a tuple (``*``, ``*``). + """ + # return self.n_block*len(self.indices[0]), self.n_block*len(self.indices) + return (self.nP, self.nP) + + def deriv(self, m, v=None): + """Derivative of mapping with respect to the input parameters + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + + if v is not None: + return self.sphericalDeriv(m) * v + return self.sphericalDeriv(m) + + @property + def is_linear(self): + return False + + +class Wires(object): + r"""Mapping class for organizing multiple parameter types into a single model. + + Let :math:`\mathbf{p_1}` and :math:`\mathbf{p_2}` be vectors that + contain the parameter values for two different parameter types; for example, + electrical conductivity and magnetic permeability. Here, all parameters + are organized into a single model :math:`\mathbf{m}` of the form: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{p_1} \\ \mathbf{p_2} \end{bmatrix} + + The ``Wires`` class constructs and applies the basic projection mappings + for extracting the values of a particular parameter type from the model. + For example: + + .. math:: + \mathbf{p_1} = \mathbf{P_{\! 1} m} + + where :math:`\mathbf{P_1}` is the projection matrix that extracts parameters + :math:`\mathbf{p_1}` from the complete set of model parameters :math:`\mathbf{m}`. + Likewise, there is a projection matrix for extracting :math:`\mathbf{p_2}`. + This can be extended to a model that containing more than 2 parameter types. + + Parameters + ---------- + args : tuple + Each input argument is a tuple (``str``, ``int``) that provides the name + and number of parameters for a given parameters type. + + Examples + -------- + Here we construct a wire mapping for a model where there + are two parameters types. Note that the number of parameters + of each type does not need to be the same. + + >>> from simpeg.maps import Wires, ReciprocalMap + >>> import numpy as np + + >>> p1 = np.r_[4.5, 2.7, 6.9, 7.1, 1.2] + >>> p2 = np.r_[10., 2., 5.]**-1 + >>> nP1 = len(p1) + >>> nP2 = len(p2) + >>> m = np.r_[p1, p2] + >>> m + array([4.5, 2.7, 6.9, 7.1, 1.2, 0.1, 0.5, 0.2]) + + Here we construct the wire map. The user provides a name + and the number of parameters for each type. The name + provided becomes the name of the method for constructing + the projection mapping. + + >>> wire_map = Wires(('name_1', nP1), ('name_2', nP2)) + + Here, we extract the values for the first parameter type. + + >>> wire_map.name_1 * m + array([4.5, 2.7, 6.9, 7.1, 1.2]) + + And here, we extract the values for the second parameter + type then apply a reciprocal mapping. + + >>> reciprocal_map = ReciprocalMap() + >>> reciprocal_map * wire_map.name_2 * m + array([10., 2., 5.]) + + """ + + def __init__(self, *args): + for arg in args: + assert ( + isinstance(arg, tuple) + and len(arg) == 2 + and isinstance(arg[0], str) + and + # TODO: this should be extended to a slice. + isinstance(arg[1], (int, np.integer)) + ), ( + "Each wire needs to be a tuple: (name, length). " + "You provided: {}".format(arg) + ) + + self._nP = int(np.sum([w[1] for w in args])) + start = 0 + maps = [] + for arg in args: + wire = Projection(self.nP, slice(start, start + arg[1])) + setattr(self, arg[0], wire) + maps += [(arg[0], wire)] + start += arg[1] + self.maps = maps + + self._tuple = namedtuple("Model", [w[0] for w in args]) + + def __mul__(self, val): + assert isinstance(val, np.ndarray) + split = [] + for _, w in self.maps: + split += [w * val] + return self._tuple(*split) + + @property + def nP(self): + r"""Number of parameters the mapping acts on. + + Returns + ------- + int + Number of parameters that the mapping acts on. + """ + return self._nP + + +class TileMap(IdentityMap): + """Mapping for tiled inversion. + + Uses volume averaging to map a model defined on a global mesh to the + local mesh. Everycell in the local mesh must also be in the global mesh. + + Parameters + ---------- + global_mesh : discretize.TreeMesh + Global TreeMesh defining the entire domain. + global_active : numpy.ndarray of bool or int + Defines the active cells in the global mesh. + local_mesh : discretize.TreeMesh + Local TreeMesh for the simulation. + tol : float, optional + Tolerance to avoid zero division + components : int, optional + Number of components in the model. E.g. a vector model in 3D would have 3 + components. + """ + + def __init__( + self, + global_mesh, + global_active, + local_mesh, + tol=1e-8, + components=1, + **kwargs, + ): + super().__init__(mesh=None, **kwargs) + self._global_mesh = validate_type( + "global_mesh", global_mesh, discretize.TreeMesh, cast=False + ) + self._local_mesh = validate_type( + "local_mesh", local_mesh, discretize.TreeMesh, cast=False + ) + + self._global_active = validate_active_indices( + "global_active", global_active, self.global_mesh.n_cells + ) + + self._tol = validate_float("tol", tol, min_val=0.0, inclusive_min=False) + self._components = validate_integer("components", components, min_val=1) + + # trigger creation of P + self.P + + @property + def global_mesh(self): + """Global TreeMesh defining the entire domain. + + Returns + ------- + discretize.TreeMesh + """ + return self._global_mesh + + @property + def local_mesh(self): + """Local TreeMesh defining the local domain. + + Returns + ------- + discretize.TreeMesh + """ + return self._local_mesh + + @property + def global_active(self): + """Defines the active cells in the global mesh. + + Returns + ------- + (global_mesh.n_cells) numpy.ndarray of bool + """ + return self._global_active + + @property + def local_active(self): + """ + This is the local_active of the global_active used in the global problem. + + Returns + ------- + (local_mesh.n_cells) numpy.ndarray of bool + """ + return self._local_active + + @property + def tol(self): + """Tolerance to avoid zero division. + + Returns + ------- + float + """ + return self._tol + + @property + def components(self): + """Number of components in the model. + + Returns + ------- + int + """ + return self._components + + @property + def P(self): + """ + Set the projection matrix with partial volumes + """ + if getattr(self, "_P", None) is None: + in_local = self.local_mesh.get_containing_cells( + self.global_mesh.cell_centers + ) + + P = ( + sp.csr_matrix( + ( + self.global_mesh.cell_volumes, + (in_local, np.arange(self.global_mesh.nC)), + ), + shape=(self.local_mesh.nC, self.global_mesh.nC), + ) + * speye(self.global_mesh.nC)[:, self.global_active] + ) + + self._local_active = mkvc(np.sum(P, axis=1) > 0) + + P = P[self.local_active, :] + + self._P = sp.block_diag( + [ + sdiag(1.0 / self.local_mesh.cell_volumes[self.local_active]) * P + for ii in range(self.components) + ] + ) + + return self._P + + def _transform(self, m): + return self.P * m + + @property + def shape(self): + """ + Shape of the matrix operation (number of indices x nP) + """ + return self.P.shape + + def deriv(self, m, v=None): + """ + :param numpy.ndarray m: model + :rtype: scipy.sparse.csr_matrix + :return: derivative of transformed model + """ + if v is not None: + return self.P * v + return self.P diff --git a/simpeg/maps/_clustering.py b/simpeg/maps/_clustering.py new file mode 100644 index 0000000000..5761e92d72 --- /dev/null +++ b/simpeg/maps/_clustering.py @@ -0,0 +1,152 @@ +""" +Map classes for petrophysics clusters. +""" + +import numpy as np +from numpy.polynomial import polynomial + + +from ..utils import validate_ndarray_with_shape + +from ._base import IdentityMap + + +class PolynomialPetroClusterMap(IdentityMap): + """ + Modeling polynomial relationships between physical properties + + Parameters + ---------- + coeffxx : array_like, optional + Coefficients for the xx component. Default is [0, 1] + coeffxy : array_like, optional + Coefficients for the xy component. Default is [0] + coeffyx : array_like, optional + Coefficients for the yx component. Default is [0] + coeffyy : array_like, optional + Coefficients for the yy component. Default is [0, 1] + """ + + def __init__( + self, + coeffxx=None, + coeffxy=None, + coeffyx=None, + coeffyy=None, + mesh=None, + nP=None, + **kwargs, + ): + if coeffxx is None: + coeffxx = np.r_[0.0, 1.0] + if coeffxy is None: + coeffxy = np.r_[0.0] + if coeffyx is None: + coeffyx = np.r_[0.0] + if coeffyy is None: + coeffyy = np.r_[0.0, 1.0] + + self._coeffxx = validate_ndarray_with_shape("coeffxx", coeffxx, shape=("*",)) + self._coeffxy = validate_ndarray_with_shape("coeffxy", coeffxy, shape=("*",)) + self._coeffyx = validate_ndarray_with_shape("coeffyx", coeffyx, shape=("*",)) + self._coeffyy = validate_ndarray_with_shape("coeffyy", coeffyy, shape=("*",)) + + self._polynomialxx = polynomial.Polynomial(self.coeffxx) + self._polynomialxy = polynomial.Polynomial(self.coeffxy) + self._polynomialyx = polynomial.Polynomial(self.coeffyx) + self._polynomialyy = polynomial.Polynomial(self.coeffyy) + self._polynomialxx_deriv = self._polynomialxx.deriv(m=1) + self._polynomialxy_deriv = self._polynomialxy.deriv(m=1) + self._polynomialyx_deriv = self._polynomialyx.deriv(m=1) + self._polynomialyy_deriv = self._polynomialyy.deriv(m=1) + + super().__init__(mesh=mesh, nP=nP, **kwargs) + + @property + def coeffxx(self): + """Coefficients for the xx component. + + Returns + ------- + numpy.ndarray + """ + return self._coeffxx + + @property + def coeffxy(self): + """Coefficients for the xy component. + + Returns + ------- + numpy.ndarray + """ + return self._coeffxy + + @property + def coeffyx(self): + """Coefficients for the yx component. + + Returns + ------- + numpy.ndarray + """ + return self._coeffyx + + @property + def coeffyy(self): + """Coefficients for the yy component. + + Returns + ------- + numpy.ndarray + """ + return self._coeffyy + + def _transform(self, m): + out = m.copy() + out[:, 0] = self._polynomialxx(m[:, 0]) + self._polynomialxy(m[:, 1]) + out[:, 1] = self._polynomialyx(m[:, 0]) + self._polynomialyy(m[:, 1]) + return out + + def inverse(self, D): + r""" + :param numpy.array D: physical property + :rtype: numpy.array + :return: model + + The *transformInverse* changes the physical property into the + model. + + .. math:: + + m = \log{\sigma} + + """ + raise NotImplementedError("Inverse is not implemented.") + + def _derivmatrix(self, m): + return np.r_[ + [ + [ + self._polynomialxx_deriv(m[:, 0])[0], + self._polynomialyx_deriv(m[:, 0])[0], + ], + [ + self._polynomialxy_deriv(m[:, 1])[0], + self._polynomialyy_deriv(m[:, 1])[0], + ], + ] + ] + + def deriv(self, m, v=None): + """""" + if v is None: + out = self._derivmatrix(m.reshape(-1, 2)) + return out + else: + out = np.dot(self._derivmatrix(m.reshape(-1, 2)), v.reshape(2, -1)) + return out + + @property + def is_linear(self): + return False diff --git a/simpeg/maps/_injection.py b/simpeg/maps/_injection.py new file mode 100644 index 0000000000..68c02c2e61 --- /dev/null +++ b/simpeg/maps/_injection.py @@ -0,0 +1,342 @@ +""" +Injection and interpolation map classes. +""" + +import discretize +import numpy as np +import scipy.sparse as sp +from numbers import Number + +from ..utils import ( + validate_type, + validate_ndarray_with_shape, + validate_float, + validate_active_indices, +) +from ._base import IdentityMap +from ..utils.code_utils import deprecate_property + + +class Mesh2Mesh(IdentityMap): + """ + Takes a model on one mesh are translates it to another mesh. + """ + + def __init__(self, meshes, active_cells=None, **kwargs): + # Sanity checks for the meshes parameter + try: + mesh, mesh2 = meshes + except TypeError: + raise TypeError("Couldn't unpack 'meshes' into two meshes.") + + # Deprecate indActive argument + if kwargs.pop("indActive", None) is not None: + raise TypeError( + "'indActive' was removed in SimPEG v0.24.0, please use 'active_cells' instead.", + ) + + super().__init__(mesh=mesh, **kwargs) + + self.mesh2 = mesh2 + # Check dimensions of both meshes + if mesh.dim != mesh2.dim: + raise ValueError( + f"Found meshes with dimensions '{mesh.dim}' and '{mesh2.dim}'. " + + "Both meshes must have the same dimension." + ) + + self.active_cells = active_cells + + # reset to not accepted None for mesh + @IdentityMap.mesh.setter + def mesh(self, value): + self._mesh = validate_type("mesh", value, discretize.base.BaseMesh, cast=False) + + @property + def mesh2(self): + """The source mesh used for the mapping. + + Returns + ------- + discretize.base.BaseMesh + """ + return self._mesh2 + + @mesh2.setter + def mesh2(self, value): + self._mesh2 = validate_type( + "mesh2", value, discretize.base.BaseMesh, cast=False + ) + + @property + def active_cells(self): + """Active indices on target mesh. + + Returns + ------- + (mesh.n_cells) numpy.ndarray of bool or none + """ + return self._active_cells + + @active_cells.setter + def active_cells(self, value): + if value is not None: + value = validate_active_indices("active_cells", value, self.mesh.n_cells) + self._active_cells = value + + indActive = deprecate_property( + active_cells, + "indActive", + "active_cells", + removal_version="0.24.0", + error=True, + ) + + @property + def P(self): + if getattr(self, "_P", None) is None: + self._P = self.mesh2.get_interpolation_matrix( + ( + self.mesh.cell_centers[self.active_cells, :] + if self.active_cells is not None + else self.mesh.cell_centers + ), + "CC", + zeros_outside=True, + ) + return self._P + + @property + def shape(self): + """Number of parameters in the model.""" + if self.active_cells is not None: + return (self.active_cells.sum(), self.mesh2.nC) + return (self.mesh.nC, self.mesh2.nC) + + @property + def nP(self): + """Number of parameters in the model.""" + return self.mesh2.nC + + def _transform(self, m): + return self.P * m + + def deriv(self, m, v=None): + if v is not None: + return self.P * v + return self.P + + +class InjectActiveCells(IdentityMap): + r"""Map active cells model to all cell of a mesh. + + The ``InjectActiveCells`` class is used to define the mapping when + the model consists of physical property values for a set of active + mesh cells; e.g. cells below topography. For a discrete set of + model parameters :math:`\mathbf{m}` defined on a set of active + cells, the mapping :math:`\mathbf{u}(\mathbf{m})` is defined as: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + \mathbf{d}\, m_\perp + + where :math:`\mathbf{P}` is a (*nC* , *nP*) projection matrix from + active cells to all mesh cells, and :math:`\mathbf{d}` is a + (*nC* , 1) matrix that projects the inactive cell value + :math:`m_\perp` to all inactive mesh cells. + + Parameters + ---------- + mesh : discretize.BaseMesh + A discretize mesh + active_cells : numpy.ndarray + Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* + or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. + value_inactive : float or numpy.ndarray + The physical property value assigned to all inactive cells in the mesh + """ + + def __init__( + self, + mesh, + active_cells=None, + value_inactive=0.0, + nC=None, + **kwargs, + ): + self.mesh = mesh + self.nC = nC or mesh.nC + + # Deprecate indActive argument + if kwargs.pop("indActive", None) is not None: + raise TypeError( + "'indActive' was removed in SimPEG v0.24.0, please use 'active_cells' instead." + ) + # Deprecate valInactive argument + if kwargs.pop("valInactive", None) is not None: + raise TypeError( + "'valInactive' was removed in SimPEG v0.24.0, please use 'value_inactive' instead." + ) + if kwargs: # TODO Remove this when removing kwargs argument. + raise TypeError("Unsupported keyword argument " + kwargs.popitem()[0]) + + self.active_cells = active_cells + self._nP = np.sum(self.active_cells) + + self.P = sp.eye(self.nC, format="csr")[:, self.active_cells] + + self.value_inactive = value_inactive + + @property + def value_inactive(self): + """The physical property value assigned to all inactive cells in the mesh. + + Returns + ------- + numpy.ndarray + """ + return self._value_inactive + + @value_inactive.setter + def value_inactive(self, value): + n_inactive = self.nC - self.nP + if isinstance(value, Number): + value = validate_float("value_inactive", value) + value = np.full(n_inactive, value) + value = validate_ndarray_with_shape( + "value_inactive", value, shape=(n_inactive,) + ) + value_inactive = np.zeros(self.nC, dtype=float) + value_inactive[~self.active_cells] = value + self._value_inactive = value_inactive + + valInactive = deprecate_property( + value_inactive, + "valInactive", + "value_inactive", + removal_version="0.24.0", + error=True, + ) + + @property + def active_cells(self): + """A boolean array representing the active values in the map's output array. + + Returns + ------- + numpy.ndarray of bool + + """ + return self._active_cells + + @active_cells.setter + def active_cells(self, value): + if value is not None: + value = validate_active_indices("active_cells", value, self.nC) + self._active_cells = value + + indActive = deprecate_property( + active_cells, + "indActive", + "active_cells", + removal_version="0.24.0", + error=True, + ) + + @property + def shape(self): + """Dimensions of the mapping + + Returns + ------- + tuple of int + Where *nP* is the number of active cells and *nC* is + number of cell in the mesh, **shape** returns a + tuple (*nC* , *nP*). + """ + return (self.nC, self.nP) + + @property + def nP(self): + """Number of parameters the model acts on. + + Returns + ------- + int + Number of parameters the model acts on; i.e. the number of active cells + """ + return int(self.active_cells.sum()) + + def _transform(self, m): + if m.ndim > 1: + return self.P * m + self.value_inactive[:, None] + return self.P * m + self.value_inactive + + def inverse(self, u): + r"""Recover the model parameters (active cells) from a set of physical + property values defined on the entire mesh. + + For a discrete set of model parameters :math:`\mathbf{m}` defined + on a set of active cells, the mapping :math:`\mathbf{u}(\mathbf{m})` + is defined as: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + \mathbf{d} \,m_\perp + + where :math:`\mathbf{P}` is a (*nC* , *nP*) projection matrix from + active cells to all mesh cells, and :math:`\mathbf{d}` is a + (*nC* , 1) matrix that projects the inactive cell value + :math:`m_\perp` to all inactive mesh cells. + + The inverse mapping is given by: + + .. math:: + \mathbf{m}(\mathbf{u}) = \mathbf{P^T u} + + Parameters + ---------- + u : (mesh.nC) numpy.ndarray + A vector which contains physical property values for all + mesh cells. + """ + return self.P.T * u + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the input parameters. + + For a discrete set of model parameters :math:`\mathbf{m}` defined + on a set of active cells, the mapping :math:`\mathbf{u}(\mathbf{m})` + is defined as: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + \mathbf{d} \, m_\perp + + where :math:`\mathbf{P}` is a (*nC* , *nP*) projection matrix from + active cells to all mesh cells, and :math:`\mathbf{d}` is a + (*nC* , 1) matrix that projects the inactive cell value + :math:`m_\perp` to all inactive mesh cells. + + the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect + to the model parameters; i.e.: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} + + Note that in this case, **deriv** simply returns a sparse projection matrix. + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + if v is not None: + return self.P * v + return self.P diff --git a/simpeg/maps/_parametric.py b/simpeg/maps/_parametric.py new file mode 100644 index 0000000000..4837b7c6fd --- /dev/null +++ b/simpeg/maps/_parametric.py @@ -0,0 +1,2692 @@ +""" +Parametric map classes. +""" + +import discretize +import numpy as np +from numpy.polynomial import polynomial +import scipy.sparse as sp +from scipy.interpolate import UnivariateSpline + +from discretize.utils import sdiag + +from ..utils import ( + validate_type, + validate_ndarray_with_shape, + validate_float, + validate_integer, + validate_string, + validate_active_indices, +) +from ._base import IdentityMap +from ..utils.code_utils import deprecate_property + + +class ParametricCircleMap(IdentityMap): + r"""Mapping for a parameterized circle. + + Define the mapping from a parameterized model for a circle in a wholespace + to all cells within a 2D mesh. For a circle within a wholespace, the + model is defined by 5 parameters: the background physical property value + (:math:`\sigma_0`), the physical property value for the circle + (:math:`\sigma_c`), the x location :math:`x_0` and y location :math:`y_0` + for center of the circle, and the circle's radius (:math:`R`). + + Let :math:`\mathbf{m} = [\sigma_0, \sigma_1, x_0, y_0, R]` be the set of + model parameters the defines a circle within a wholespace. The mapping + :math:`\mathbf{u}(\mathbf{m})` from the parameterized model to all cells + within a 2D mesh is given by: + + .. math:: + + \mathbf{u}(\mathbf{m}) = \sigma_0 + (\sigma_1 - \sigma_0) + \bigg [ \frac{1}{2} + \pi^{-1} \arctan \bigg ( a \big [ \sqrt{(\mathbf{x_c}-x_0)^2 + + (\mathbf{y_c}-y_0)^2} - R \big ] \bigg ) \bigg ] + + where :math:`\mathbf{x_c}` and :math:`\mathbf{y_c}` are vectors storing + the x and y positions of all cell centers for the 2D mesh and :math:`a` + is a user-defined constant which defines the sharpness of boundary of the + circular structure. + + Parameters + ---------- + mesh : discretize.BaseMesh + A 2D discretize mesh + logSigma : bool + If ``True``, parameters :math:`\sigma_0` and :math:`\sigma_1` represent the + natural log of the physical property values for the background and circle, + respectively. + slope : float + A constant for defining the sharpness of the boundary between the circle + and the wholespace. The sharpness increases as *slope* is increased. + + Examples + -------- + Here we define the parameterized model for a circle in a wholespace. We then + create and use a ``ParametricCircleMap`` to map the model to a 2D mesh. + + >>> from simpeg.maps import ParametricCircleMap + >>> from discretize import TensorMesh + >>> import numpy as np + >>> import matplotlib.pyplot as plt + + >>> h = 0.5*np.ones(20) + >>> mesh = TensorMesh([h, h]) + + >>> sigma0, sigma1, x0, y0, R = 0., 10., 4., 6., 2. + >>> model = np.r_[sigma0, sigma1, x0, y0, R] + >>> mapping = ParametricCircleMap(mesh, logSigma=False, slope=2) + + >>> fig = plt.figure(figsize=(5, 5)) + >>> ax = fig.add_subplot(111) + >>> mesh.plot_image(mapping * model, ax=ax) + + """ + + def __init__(self, mesh, logSigma=True, slope=0.1): + super().__init__(mesh=mesh) + if mesh.dim != 2: + raise NotImplementedError( + "Mesh must be 2D, not implemented yet for other dimensions." + ) + # TODO: this should be done through a composition with and ExpMap + self.logSigma = logSigma + self.slope = slope + + @property + def slope(self): + """Sharpness of the boundary. + + Larger number are sharper. + + Returns + ------- + float + """ + return self._slope + + @slope.setter + def slope(self, value): + self._slope = validate_float("slope", value, min_val=0.0, inclusive_min=False) + + @property + def logSigma(self): + """Whether the input needs to be transformed by an exponential + + Returns + ------- + float + """ + return self._logSigma + + @logSigma.setter + def logSigma(self, value): + self._logSigma = validate_type("logSigma", value, bool) + + @property + def nP(self): + r"""Number of parameters the mapping acts on; i.e. 5. + + Returns + ------- + int + The ``ParametricCircleMap`` acts on 5 parameters. + """ + return 5 + + def _transform(self, m): + a = self.slope + sig1, sig2, x, y, r = m[0], m[1], m[2], m[3], m[4] + if self.logSigma: + sig1, sig2 = np.exp(sig1), np.exp(sig2) + X = self.mesh.cell_centers[:, 0] + Y = self.mesh.cell_centers[:, 1] + return sig1 + (sig2 - sig1) * ( + np.arctan(a * (np.sqrt((X - x) ** 2 + (Y - y) ** 2) - r)) / np.pi + 0.5 + ) + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the input parameters. + + Let :math:`\mathbf{m} = [\sigma_0, \sigma_1, x_0, y_0, R]` be the set of + model parameters the defines a circle within a wholespace. The mapping + :math:`\mathbf{u}(\mathbf{m})`from the parameterized model to all cells + within a 2D mesh is given by: + + .. math:: + \mathbf{u}(\mathbf{m}) = \sigma_0 + (\sigma_1 - \sigma_0) + \bigg [ \frac{1}{2} + \pi^{-1} \arctan \bigg ( a \big [ \sqrt{(\mathbf{x_c}-x_0)^2 + + (\mathbf{y_c}-y_0)^2} - R \big ] \bigg ) \bigg ] + + The derivative of the mapping with respect to the model parameters is a + ``numpy.ndarray`` of shape (*mesh.nC*, 5) given by: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = + \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_0} \;\; + \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_1} \;\; + \Bigg [ \frac{\partial \mathbf{u}}{\partial x_0} \;\; + \Bigg [ \frac{\partial \mathbf{u}}{\partial y_0} \;\; + \Bigg [ \frac{\partial \mathbf{u}}{\partial R} + \Bigg ] + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + a = self.slope + sig1, sig2, x, y, r = m[0], m[1], m[2], m[3], m[4] + if self.logSigma: + sig1, sig2 = np.exp(sig1), np.exp(sig2) + X = self.mesh.cell_centers[:, 0] + Y = self.mesh.cell_centers[:, 1] + if self.logSigma: + g1 = ( + -( + np.arctan(a * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2))) / np.pi + + 0.5 + ) + * sig1 + + sig1 + ) + g2 = ( + np.arctan(a * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2))) / np.pi + 0.5 + ) * sig2 + else: + g1 = ( + -( + np.arctan(a * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2))) / np.pi + + 0.5 + ) + + 1.0 + ) + g2 = ( + np.arctan(a * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2))) / np.pi + 0.5 + ) + + g3 = ( + a + * (-X + x) + * (-sig1 + sig2) + / ( + np.pi + * (a**2 * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2)) ** 2 + 1) + * np.sqrt((X - x) ** 2 + (Y - y) ** 2) + ) + ) + + g4 = ( + a + * (-Y + y) + * (-sig1 + sig2) + / ( + np.pi + * (a**2 * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2)) ** 2 + 1) + * np.sqrt((X - x) ** 2 + (Y - y) ** 2) + ) + ) + + g5 = ( + -a + * (-sig1 + sig2) + / (np.pi * (a**2 * (-r + np.sqrt((X - x) ** 2 + (Y - y) ** 2)) ** 2 + 1)) + ) + + if v is not None: + return sp.csr_matrix(np.c_[g1, g2, g3, g4, g5]) * v + return sp.csr_matrix(np.c_[g1, g2, g3, g4, g5]) + + @property + def is_linear(self): + return False + + +class ParametricPolyMap(IdentityMap): + r"""Mapping for 2 layer model whose interface is defined by a polynomial. + + This mapping is used when the cells lying below the Earth's surface can + be parameterized by a 2 layer model whose interface is defined by a + polynomial function. The model is defined by the physical property + values for each unit (:math:`\sigma_1` and :math:`\sigma_2`) and the + coefficients for the polynomial function (:math:`\mathbf{c}`). + + **For a 2D mesh** , the interface is defined by a polynomial function + of the form: + + .. math:: + p(x) = \sum_{i=0}^N c_i x^i + + where :math:`c_i` are the polynomial coefficients and :math:`N` is + the order of the polynomial. In this case, the model is defined as + + .. math:: + \mathbf{m} = [\sigma_1, \;\sigma_2,\; c_0 ,\;\ldots\; ,\; c_N] + + The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh + is given by: + + .. math:: + + \mathbf{u}(\mathbf{m}) = \sigma_1 + (\sigma_2 - \sigma_1) + \bigg [ \frac{1}{2} + \pi^{-1} \arctan \bigg ( + a \Big ( \mathbf{p}(\mathbf{x_c}) - \mathbf{y_c} \Big ) + \bigg ) \bigg ] + + where :math:`\mathbf{x_c}` and :math:`\mathbf{y_c}` are vectors containing the + x and y cell center locations for all active cells in the mesh, and :math:`a` is a + parameter which defines the sharpness of the boundary between the two layers. + :math:`\mathbf{p}(\mathbf{x_c})` evaluates the polynomial function for + every element in :math:`\mathbf{x_c}`. + + **For a 3D mesh** , the interface is defined by a 2D polynomial function + of the form: + + .. math:: + p(x,y) = + \sum_{j=0}^{N_y} \sum_{i=0}^{N_x} c_{ij} \, x^i y^j + + where :math:`c_{ij}` are the polynomial coefficients. :math:`N_x` + and :math:`N_y` define the order of the polynomial in :math:`x` and + :math:`y`, respectively. In this case, the model is defined as: + + .. math:: + \mathbf{m} = [\sigma_1, \; \sigma_2, \; c_{0,0} , \; c_{1,0} , \;\ldots , \; c_{N_x, N_y}] + + The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh + is given by: + + .. math:: + + \mathbf{u}(\mathbf{m}) = \sigma_1 + (\sigma_2 - \sigma_1) + \bigg [ \frac{1}{2} + \pi^{-1} \arctan \bigg ( + a \Big ( \mathbf{p}(\mathbf{x_c,y_c}) - \mathbf{z_c} \Big ) + \bigg ) \bigg ] + + where :math:`\mathbf{x_c}, \mathbf{y_c}` and :math:`\mathbf{y_z}` are vectors + containing the x, y and z cell center locations for all active cells in the mesh. + :math:`\mathbf{p}(\mathbf{x_c, y_c})` evaluates the polynomial function for + every corresponding pair of :math:`\mathbf{x_c}` and :math:`\mathbf{y_c}` + elements. + + Parameters + ---------- + mesh : discretize.BaseMesh + A discretize mesh + order : int or list of int + Order of the polynomial. For a 2D mesh, this is an ``int``. For a 3D + mesh, the order for both variables is entered separately; i.e. + [*order1* , *order2*]. + logSigma : bool + If ``True``, parameters :math:`\sigma_1` and :math:`\sigma_2` represent + the natural log of a physical property. + normal : {'x', 'y', 'z'} + active_cells : (n_cells) numpy.ndarray, optional + Active cells array. Can be a boolean ``numpy.ndarray`` of length + ``mesh.n_cells`` or a ``numpy.ndarray`` of ``int`` containing the + indices of the active cells. + + Examples + -------- + In this example, we define a 2 layer model whose interface is sharp and lies + along a polynomial function :math:`y(x)=c_0 + c_1 x`. In this case, the model is + defined as :math:`\mathbf{m} = [\sigma_1 , \sigma_2 , c_0 , c_1]`. We construct + a polynomial mapping from the model to the set of active cells (i.e. below the surface), + We then use an active cells mapping to map from the set of active cells to all + cells in the 2D mesh. + + >>> from simpeg.maps import ParametricPolyMap, InjectActiveCells + >>> from discretize import TensorMesh + >>> import numpy as np + >>> import matplotlib.pyplot as plt + + >>> h = 0.5*np.ones(20) + >>> mesh = TensorMesh([h, h]) + >>> ind_active = mesh.cell_centers[:, 1] < 8 + >>> + >>> sig1, sig2, c0, c1 = 10., 5., 2., 0.5 + >>> model = np.r_[sig1, sig2, c0, c1] + + >>> poly_map = ParametricPolyMap( + >>> mesh, order=1, logSigma=False, normal='Y', active_cells=ind_active, slope=1e4 + >>> ) + >>> act_map = InjectActiveCells(mesh, ind_active, 0.) + + >>> fig = plt.figure(figsize=(5, 5)) + >>> ax = fig.add_subplot(111) + >>> mesh.plot_image(act_map * poly_map * model, ax=ax) + >>> ax.set_title('Mapping on a 2D mesh') + + Here, we recreate the previous example on a 3D mesh but with a smoother interface. + For a 3D mesh, the 2D polynomial defining the sloping interface is given by + :math:`z(x,y) = c_0 + c_x x + c_y y + c_{xy} xy`. In this case, the model is + defined as :math:`\mathbf{m} = [\sigma_1 , \sigma_2 , c_0 , c_x, c_y, c_{xy}]`. + + >>> mesh = TensorMesh([h, h, h]) + >>> ind_active = mesh.cell_centers[:, 2] < 8 + >>> + >>> sig1, sig2, c0, cx, cy, cxy = 10., 5., 2., 0.5, 0., 0. + >>> model = np.r_[sig1, sig2, c0, cx, cy, cxy] + >>> + >>> poly_map = ParametricPolyMap( + >>> mesh, order=[1, 1], logSigma=False, normal='Z', active_cells=ind_active, slope=2 + >>> ) + >>> act_map = InjectActiveCells(mesh, ind_active, 0.) + >>> + >>> fig = plt.figure(figsize=(5, 5)) + >>> ax = fig.add_subplot(111) + >>> mesh.plot_slice(act_map * poly_map * model, ax=ax, normal='Y', ind=10) + >>> ax.set_title('Mapping on a 3D mesh') + + """ + + def __init__( + self, + mesh, + order, + logSigma=True, + normal="X", + active_cells=None, + slope=1e4, + **kwargs, + ): + super().__init__(mesh=mesh) + self.logSigma = logSigma + self.order = order + self.normal = normal + self.slope = slope + + # Deprecate indActive argument + if kwargs.pop("indActive", None) is not None: + raise TypeError( + "'indActive' was removed in SimPEG v0.24.0, please use 'active_cells' instead." + ) + if kwargs: # TODO Remove this when removing kwargs argument. + raise TypeError("Unsupported keyword argument " + kwargs.popitem()[0]) + + if active_cells is None: + active_cells = np.ones(mesh.n_cells, dtype=bool) + self.active_cells = active_cells + + @property + def slope(self): + """Sharpness of the boundary. + + Larger number are sharper. + + Returns + ------- + float + """ + return self._slope + + @slope.setter + def slope(self, value): + self._slope = validate_float("slope", value, min_val=0.0, inclusive_min=False) + + @property + def logSigma(self): + """Whether the input needs to be transformed by an exponential + + Returns + ------- + float + """ + return self._logSigma + + @logSigma.setter + def logSigma(self, value): + self._logSigma = validate_type("logSigma", value, bool) + + @property + def normal(self): + """The projection axis. + + Returns + ------- + str + """ + return self._normal + + @normal.setter + def normal(self, value): + self._normal = validate_string("normal", value, ("x", "y", "z")) + + @property + def active_cells(self): + """Active indices of the mesh. + + Returns + ------- + (mesh.n_cells) numpy.ndarray of bool + """ + return self._active_cells + + @active_cells.setter + def active_cells(self, value): + self._active_cells = validate_active_indices( + "active_cells", value, self.mesh.n_cells + ) + self._nC = sum(self._active_cells) + + actInd = deprecate_property( + active_cells, + "actInd", + "active_cells", + removal_version="0.24.0", + error=True, + ) + + @property + def shape(self): + """Dimensions of the mapping. + + Returns + ------- + tuple of int + The dimensions of the mapping as a tuple of the form + (*nC* , *nP*), where *nP* is the number of model parameters + the mapping acts on and *nC* is the number of active cells + being mapping to. If ``active_cells`` is ``None``, then + *nC = mesh.nC*. + """ + return (self.nC, self.nP) + + @property + def nC(self): + """Number of active cells being mapped too. + + Returns + ------- + int + """ + return self._nC + + @property + def nP(self): + """Number of parameters the mapping acts on. + + Returns + ------- + int + The number of parameters the mapping acts on. + """ + if np.isscalar(self.order): + nP = self.order + 3 + else: + nP = (self.order[0] + 1) * (self.order[1] + 1) + 2 + return nP + + def _transform(self, m): + # Set model parameters + alpha = self.slope + sig1, sig2 = m[0], m[1] + c = m[2:] + if self.logSigma: + sig1, sig2 = np.exp(sig1), np.exp(sig2) + + # 2D + if self.mesh.dim == 2: + X = self.mesh.cell_centers[self.active_cells, 0] + Y = self.mesh.cell_centers[self.active_cells, 1] + if self.normal == "x": + f = polynomial.polyval(Y, c) - X + elif self.normal == "y": + f = polynomial.polyval(X, c) - Y + else: + raise (Exception("Input for normal = X or Y or Z")) + + # 3D + elif self.mesh.dim == 3: + X = self.mesh.cell_centers[self.active_cells, 0] + Y = self.mesh.cell_centers[self.active_cells, 1] + Z = self.mesh.cell_centers[self.active_cells, 2] + + if self.normal == "x": + f = ( + polynomial.polyval2d( + Y, + Z, + c.reshape((self.order[0] + 1, self.order[1] + 1), order="F"), + ) + - X + ) + elif self.normal == "y": + f = ( + polynomial.polyval2d( + X, + Z, + c.reshape((self.order[0] + 1, self.order[1] + 1), order="F"), + ) + - Y + ) + elif self.normal == "z": + f = ( + polynomial.polyval2d( + X, + Y, + c.reshape((self.order[0] + 1, self.order[1] + 1), order="F"), + ) + - Z + ) + else: + raise (Exception("Input for normal = X or Y or Z")) + + else: + raise (Exception("Only supports 2D or 3D")) + + return sig1 + (sig2 - sig1) * (np.arctan(alpha * f) / np.pi + 0.5) + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the model. + + For a model :math:`\mathbf{m} = [\sigma_1, \sigma_2, \mathbf{c}]`, + the derivative of the mapping with respect to the model parameters is a + ``numpy.ndarray`` of shape (*mesh.nC*, *nP*) of the form: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = + \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_0} \;\; + \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_1} \;\; + \Bigg [ \frac{\partial \mathbf{u}}{\partial c_0} \;\; + \Bigg [ \frac{\partial \mathbf{u}}{\partial c_1} \;\; + \cdots \;\; + \Bigg [ \frac{\partial \mathbf{u}}{\partial c_N} + \Bigg ] + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + + """ + alpha = self.slope + sig1, sig2, c = m[0], m[1], m[2:] + if self.logSigma: + sig1, sig2 = np.exp(sig1), np.exp(sig2) + + # 2D + if self.mesh.dim == 2: + X = self.mesh.cell_centers[self.active_cells, 0] + Y = self.mesh.cell_centers[self.active_cells, 1] + + if self.normal == "x": + f = polynomial.polyval(Y, c) - X + V = polynomial.polyvander(Y, len(c) - 1) + elif self.normal == "y": + f = polynomial.polyval(X, c) - Y + V = polynomial.polyvander(X, len(c) - 1) + else: + raise (Exception("Input for normal = X or Y")) + + # 3D + elif self.mesh.dim == 3: + X = self.mesh.cell_centers[self.active_cells, 0] + Y = self.mesh.cell_centers[self.active_cells, 1] + Z = self.mesh.cell_centers[self.active_cells, 2] + + if self.normal == "x": + f = ( + polynomial.polyval2d( + Y, Z, c.reshape((self.order[0] + 1, self.order[1] + 1)) + ) + - X + ) + V = polynomial.polyvander2d(Y, Z, self.order) + elif self.normal == "y": + f = ( + polynomial.polyval2d( + X, Z, c.reshape((self.order[0] + 1, self.order[1] + 1)) + ) + - Y + ) + V = polynomial.polyvander2d(X, Z, self.order) + elif self.normal == "z": + f = ( + polynomial.polyval2d( + X, Y, c.reshape((self.order[0] + 1, self.order[1] + 1)) + ) + - Z + ) + V = polynomial.polyvander2d(X, Y, self.order) + else: + raise (Exception("Input for normal = X or Y or Z")) + + if self.logSigma: + g1 = -(np.arctan(alpha * f) / np.pi + 0.5) * sig1 + sig1 + g2 = (np.arctan(alpha * f) / np.pi + 0.5) * sig2 + else: + g1 = -(np.arctan(alpha * f) / np.pi + 0.5) + 1.0 + g2 = np.arctan(alpha * f) / np.pi + 0.5 + + g3 = sdiag(alpha * (sig2 - sig1) / (1.0 + (alpha * f) ** 2) / np.pi) * V + + if v is not None: + return sp.csr_matrix(np.c_[g1, g2, g3]) * v + return sp.csr_matrix(np.c_[g1, g2, g3]) + + @property + def is_linear(self): + return False + + +class ParametricSplineMap(IdentityMap): + r"""Mapping to parameterize the boundary between two geological units using + spline interpolation. + + .. math:: + + g = f(x)-y + + Define the model as: + + .. math:: + + m = [\sigma_1, \sigma_2, y] + + Parameters + ---------- + mesh : discretize.BaseMesh + A discretize mesh + pts : (n) numpy.ndarray + Points for the 1D spline tie points. + ptsv : (2) array_like + Points for linear interpolation between two splines in 3D. + order : int + Order of the spline mapping; e.g. 3 is cubic spline + logSigma : bool + If ``True``, :math:`\sigma_1` and :math:`\sigma_2` represent the natural + log of some physical property value for each unit. + normal : {'x', 'y', 'z'} + Defines the general direction of the normal vector for the interface. + slope : float + Parameter for defining the sharpness of the boundary. The sharpness is increased + if *slope* is large. + + Examples + -------- + In this example, we define a 2 layered model with a sloping + interface on a 2D mesh. The model consists of the physical + property values for the layers and the known elevations + for the interface at the horizontal positions supplied when + creating the mapping. + + >>> from simpeg.maps import ParametricSplineMap + >>> from discretize import TensorMesh + >>> import numpy as np + >>> import matplotlib.pyplot as plt + + >>> h = 0.5*np.ones(20) + >>> mesh = TensorMesh([h, h]) + + >>> x = np.linspace(0, 10, 6) + >>> y = 0.5*x + 2.5 + + >>> model = np.r_[10., 0., y] + >>> mapping = ParametricSplineMap(mesh, x, order=2, normal='Y', slope=2) + + >>> fig = plt.figure(figsize=(5, 5)) + >>> ax = fig.add_subplot(111) + >>> mesh.plot_image(mapping * model, ax=ax) + + """ + + def __init__( + self, mesh, pts, ptsv=None, order=3, logSigma=True, normal="x", slope=1e4 + ): + super().__init__(mesh=mesh) + self.slope = slope + self.logSigma = logSigma + self.normal = normal + self.order = order + self.pts = pts + self.ptsv = ptsv + self.spl = None + + @IdentityMap.mesh.setter + def mesh(self, value): + self._mesh = validate_type( + "mesh", value, discretize.base.BaseTensorMesh, cast=False + ) + + @property + def slope(self): + """Sharpness of the boundary. + + Larger number are sharper. + + Returns + ------- + float + """ + return self._slope + + @slope.setter + def slope(self, value): + self._slope = validate_float("slope", value, min_val=0.0, inclusive_min=False) + + @property + def logSigma(self): + """Whether the input needs to be transformed by an exponential + + Returns + ------- + float + """ + return self._logSigma + + @logSigma.setter + def logSigma(self, value): + self._logSigma = validate_type("logSigma", value, bool) + + @property + def normal(self): + """The projection axis. + + Returns + ------- + str + """ + return self._normal + + @normal.setter + def normal(self, value): + self._normal = validate_string("normal", value, ("x", "y", "z")) + + @property + def order(self): + """Order of the spline mapping. + + Returns + ------- + int + """ + return self._order + + @order.setter + def order(self, value): + self._order = validate_integer("order", value, min_val=1) + + @property + def pts(self): + """Points for the spline. + + Returns + ------- + numpy.ndarray + """ + return self._pts + + @pts.setter + def pts(self, value): + self._pts = validate_ndarray_with_shape("pts", value, shape=("*"), dtype=float) + + @property + def npts(self): + """The number of points. + + Returns + ------- + int + """ + return self._pts.shape[0] + + @property + def ptsv(self): + """Bottom and top values for the 3D spline surface. + + In 3D, two splines are created and linearly interpolated between these two + points. + + Returns + ------- + (2) numpy.ndarray + """ + return self._ptsv + + @ptsv.setter + def ptsv(self, value): + if value is not None: + value = validate_ndarray_with_shape("ptsv", value, shape=(2,)) + self._ptsv = value + + @property + def nP(self): + r"""Number of parameters the mapping acts on + + Returns + ------- + int + Number of parameters the mapping acts on. + - **2D mesh:** the mapping acts on *mesh.nC + 2* parameters + - **3D mesh:** the mapping acts on *2\*mesh.nC + 2* parameters + """ + if self.mesh.dim == 2: + return np.size(self.pts) + 2 + elif self.mesh.dim == 3: + return np.size(self.pts) * 2 + 2 + else: + raise (Exception("Only supports 2D and 3D")) + + def _transform(self, m): + # Set model parameters + alpha = self.slope + sig1, sig2 = m[0], m[1] + c = m[2:] + if self.logSigma: + sig1, sig2 = np.exp(sig1), np.exp(sig2) + # 2D + if self.mesh.dim == 2: + X = self.mesh.cell_centers[:, 0] + Y = self.mesh.cell_centers[:, 1] + self.spl = UnivariateSpline(self.pts, c, k=self.order, s=0) + if self.normal == "x": + f = self.spl(Y) - X + elif self.normal == "y": + f = self.spl(X) - Y + else: + raise (Exception("Input for normal = X or Y or Z")) + + # 3D: + # Comments: + # Make two spline functions and link them using linear interpolation. + # This is not quite direct extension of 2D to 3D case + # Using 2D interpolation is possible + + elif self.mesh.dim == 3: + X = self.mesh.cell_centers[:, 0] + Y = self.mesh.cell_centers[:, 1] + Z = self.mesh.cell_centers[:, 2] + + npts = np.size(self.pts) + if np.mod(c.size, 2): + raise (Exception("Put even points!")) + + self.spl = { + "splb": UnivariateSpline(self.pts, c[:npts], k=self.order, s=0), + "splt": UnivariateSpline(self.pts, c[npts:], k=self.order, s=0), + } + + if self.normal == "x": + zb = self.ptsv[0] + zt = self.ptsv[1] + flines = (self.spl["splt"](Y) - self.spl["splb"](Y)) * (Z - zb) / ( + zt - zb + ) + self.spl["splb"](Y) + f = flines - X + # elif self.normal =='Y': + # elif self.normal =='Z': + else: + raise (Exception("Input for normal = X or Y or Z")) + else: + raise (Exception("Only supports 2D and 3D")) + + return sig1 + (sig2 - sig1) * (np.arctan(alpha * f) / np.pi + 0.5) + + def deriv(self, m, v=None): + alpha = self.slope + sig1, sig2, c = m[0], m[1], m[2:] + if self.logSigma: + sig1, sig2 = np.exp(sig1), np.exp(sig2) + # 2D + if self.mesh.dim == 2: + X = self.mesh.cell_centers[:, 0] + Y = self.mesh.cell_centers[:, 1] + + if self.normal == "x": + f = self.spl(Y) - X + elif self.normal == "y": + f = self.spl(X) - Y + else: + raise (Exception("Input for normal = X or Y or Z")) + # 3D + elif self.mesh.dim == 3: + X = self.mesh.cell_centers[:, 0] + Y = self.mesh.cell_centers[:, 1] + Z = self.mesh.cell_centers[:, 2] + + if self.normal == "x": + zb = self.ptsv[0] + zt = self.ptsv[1] + flines = (self.spl["splt"](Y) - self.spl["splb"](Y)) * (Z - zb) / ( + zt - zb + ) + self.spl["splb"](Y) + f = flines - X + # elif self.normal =='Y': + # elif self.normal =='Z': + else: + raise (Exception("Not Implemented for Y and Z, your turn :)")) + + if self.logSigma: + g1 = -(np.arctan(alpha * f) / np.pi + 0.5) * sig1 + sig1 + g2 = (np.arctan(alpha * f) / np.pi + 0.5) * sig2 + else: + g1 = -(np.arctan(alpha * f) / np.pi + 0.5) + 1.0 + g2 = np.arctan(alpha * f) / np.pi + 0.5 + + if self.mesh.dim == 2: + g3 = np.zeros((self.mesh.nC, self.npts)) + if self.normal == "y": + # Here we use perturbation to compute sensitivity + # TODO: bit more generalization of this ... + # Modfications for X and Z directions ... + for i in range(np.size(self.pts)): + ctemp = c[i] + ind = np.argmin(abs(self.mesh.cell_centers_y - ctemp)) + ca = c.copy() + cb = c.copy() + dy = self.mesh.h[1][ind] * 1.5 + ca[i] = ctemp + dy + cb[i] = ctemp - dy + spla = UnivariateSpline(self.pts, ca, k=self.order, s=0) + splb = UnivariateSpline(self.pts, cb, k=self.order, s=0) + fderiv = (spla(X) - splb(X)) / (2 * dy) + g3[:, i] = ( + sdiag(alpha * (sig2 - sig1) / (1.0 + (alpha * f) ** 2) / np.pi) + * fderiv + ) + + elif self.mesh.dim == 3: + g3 = np.zeros((self.mesh.nC, self.npts * 2)) + if self.normal == "x": + # Here we use perturbation to compute sensitivity + for i in range(self.npts * 2): + ctemp = c[i] + ind = np.argmin(abs(self.mesh.cell_centers_y - ctemp)) + ca = c.copy() + cb = c.copy() + dy = self.mesh.h[1][ind] * 1.5 + ca[i] = ctemp + dy + cb[i] = ctemp - dy + + # treat bottom boundary + if i < self.npts: + splba = UnivariateSpline( + self.pts, ca[: self.npts], k=self.order, s=0 + ) + splbb = UnivariateSpline( + self.pts, cb[: self.npts], k=self.order, s=0 + ) + flinesa = ( + (self.spl["splt"](Y) - splba(Y)) * (Z - zb) / (zt - zb) + + splba(Y) + - X + ) + flinesb = ( + (self.spl["splt"](Y) - splbb(Y)) * (Z - zb) / (zt - zb) + + splbb(Y) + - X + ) + + # treat top boundary + else: + splta = UnivariateSpline( + self.pts, ca[self.npts :], k=self.order, s=0 + ) + spltb = UnivariateSpline( + self.pts, ca[self.npts :], k=self.order, s=0 + ) + flinesa = ( + (self.spl["splt"](Y) - splta(Y)) * (Z - zb) / (zt - zb) + + splta(Y) + - X + ) + flinesb = ( + (self.spl["splt"](Y) - spltb(Y)) * (Z - zb) / (zt - zb) + + spltb(Y) + - X + ) + fderiv = (flinesa - flinesb) / (2 * dy) + g3[:, i] = ( + sdiag(alpha * (sig2 - sig1) / (1.0 + (alpha * f) ** 2) / np.pi) + * fderiv + ) + else: + raise (Exception("Not Implemented for Y and Z, your turn :)")) + + if v is not None: + return sp.csr_matrix(np.c_[g1, g2, g3]) * v + return sp.csr_matrix(np.c_[g1, g2, g3]) + + @property + def is_linear(self): + return False + + +class BaseParametric(IdentityMap): + """Base class for parametric mappings from simple geological structures to meshes. + + Parameters + ---------- + mesh : discretize.BaseMesh + A discretize mesh + slope : float, optional + Directly set the scaling parameter *slope* which sets the sharpness of boundaries + between units. + slopeFact : float, optional + Set sharpness of boundaries between units based on minimum cell size. If set, + the scalaing parameter *slope = slopeFact / dh*. + active_cells : numpy.ndarray, optional + Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* + or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. + + """ + + def __init__( + self, + mesh, + slope=None, + slopeFact=1.0, + active_cells=None, + **kwargs, + ): + # Deprecate indActive argument + if kwargs.pop("indActive", None) is not None: + raise TypeError( + "'indActive' was removed in SimPEG v0.24.0, please use 'active_cells' instead." + ) + + super(BaseParametric, self).__init__(mesh, **kwargs) + + self.active_cells = active_cells + self.slopeFact = slopeFact + if slope is not None: + self.slope = slope + + @property + def slope(self): + """Defines the sharpness of the boundaries. + + Returns + ------- + float + """ + return self._slope + + @slope.setter + def slope(self, value): + self._slope = validate_float("slope", value, min_val=0.0) + + @property + def slopeFact(self): + """Defines the slope scaled by the mesh. + + Returns + ------- + float + """ + return self._slopeFact + + @slopeFact.setter + def slopeFact(self, value): + self._slopeFact = validate_float("slopeFact", value, min_val=0.0) + self.slope = self._slopeFact / self.mesh.edge_lengths.min() + + @property + def active_cells(self): + return self._active_cells + + @active_cells.setter + def active_cells(self, value): + if value is not None: + value = validate_active_indices("active_cells", value, self.mesh.n_cells) + self._active_cells = value + + indActive = deprecate_property( + active_cells, + "indActive", + "active_cells", + removal_version="0.24.0", + error=True, + ) + + @property + def x(self): + """X cell center locations (active) for the output of the mapping. + + Returns + ------- + (n_active) numpy.ndarray + X cell center locations (active) for the output of the mapping. + """ + if getattr(self, "_x", None) is None: + if self.mesh.dim == 1: + self._x = [ + ( + self.mesh.cell_centers + if self.active_cells is None + else self.mesh.cell_centers[self.active_cells] + ) + ][0] + else: + self._x = [ + ( + self.mesh.cell_centers[:, 0] + if self.active_cells is None + else self.mesh.cell_centers[self.active_cells, 0] + ) + ][0] + return self._x + + @property + def y(self): + """Y cell center locations (active) for the output of the mapping. + + Returns + ------- + (n_active) numpy.ndarray + Y cell center locations (active) for the output of the mapping. + """ + if getattr(self, "_y", None) is None: + if self.mesh.dim > 1: + self._y = [ + ( + self.mesh.cell_centers[:, 1] + if self.active_cells is None + else self.mesh.cell_centers[self.active_cells, 1] + ) + ][0] + else: + self._y = None + return self._y + + @property + def z(self): + """Z cell center locations (active) for the output of the mapping. + + Returns + ------- + (n_active) numpy.ndarray + Z cell center locations (active) for the output of the mapping. + """ + if getattr(self, "_z", None) is None: + if self.mesh.dim > 2: + self._z = [ + ( + self.mesh.cell_centers[:, 2] + if self.active_cells is None + else self.mesh.cell_centers[self.active_cells, 2] + ) + ][0] + else: + self._z = None + return self._z + + def _atanfct(self, val, slope): + return np.arctan(slope * val) / np.pi + 0.5 + + def _atanfctDeriv(self, val, slope): + # d/dx(atan(x)) = 1/(1+x**2) + x = slope * val + dx = -slope + return (1.0 / (1 + x**2)) / np.pi * dx + + @property + def is_linear(self): + return False + + +class ParametricLayer(BaseParametric): + r"""Mapping for a horizontal layer within a wholespace. + + This mapping is used when the cells lying below the Earth's surface can + be parameterized by horizontal layer within a homogeneous medium. + The model is defined by the physical property value for the background + (:math:`\sigma_0`), the physical property value for the layer + (:math:`\sigma_1`), the elevation for the middle of the layer (:math:`z_L`) + and the thickness of the layer :math:`h`. + + For this mapping, the set of input model parameters are organized: + + .. math:: + \mathbf{m} = [\sigma_0, \;\sigma_1,\; z_L , \; h] + + The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh + is given by: + + .. math:: + + \mathbf{u}(\mathbf{m}) = \sigma_0 + \frac{(\sigma_1 - \sigma_0)}{\pi} \Bigg [ + \arctan \Bigg ( a \bigg ( \mathbf{z_c} - z_L + \frac{h}{2} \bigg ) \Bigg ) + - \arctan \Bigg ( a \bigg ( \mathbf{z_c} - z_L - \frac{h}{2} \bigg ) \Bigg ) \Bigg ] + + where :math:`\mathbf{z_c}` is a vectors containing the vertical cell center + locations for all active cells in the mesh, and :math:`a` is a + parameter which defines the sharpness of the boundaries between the layer + and the background. + + Parameters + ---------- + mesh : discretize.BaseMesh + A discretize mesh + active_cells : numpy.ndarray, optional + Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* + or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. + slope : float + Directly define the constant *a* in the mapping function which defines the + sharpness of the boundaries. + slopeFact : float + Scaling factor for the sharpness of the boundaries based on cell size. + Using this option, we set *a = slopeFact / dh*. + + Examples + -------- + In this example, we define a layer in a wholespace whose interface is sharp. + We construct the mapping from the model to the set of active cells + (i.e. below the surface), We then use an active cells mapping to map from + the set of active cells to all cells in the mesh. + + >>> from simpeg.maps import ParametricLayer, InjectActiveCells + >>> from discretize import TensorMesh + >>> import numpy as np + >>> import matplotlib.pyplot as plt + + >>> dh = 0.25*np.ones(40) + >>> mesh = TensorMesh([dh, dh]) + >>> ind_active = mesh.cell_centers[:, 1] < 8 + + >>> sig0, sig1, zL, h = 5., 10., 4., 2 + >>> model = np.r_[sig0, sig1, zL, h] + + >>> layer_map = ParametricLayer( + >>> mesh, active_cells=ind_active, slope=4 + >>> ) + >>> act_map = InjectActiveCells(mesh, ind_active, 0.) + + >>> fig = plt.figure(figsize=(5, 5)) + >>> ax = fig.add_subplot(111) + >>> mesh.plot_image(act_map * layer_map * model, ax=ax) + + """ + + @property + def nP(self): + """Number of model parameters the mapping acts on; i.e 4 + + Returns + ------- + int + Returns an integer value of *4*. + """ + return 4 + + @property + def shape(self): + """Dimensions of the mapping + + Returns + ------- + tuple of int + Where *nP=4* is the number of parameters the mapping acts on + and *nAct* is the number of active cells in the mesh, **shape** + returns a tuple (*nAct* , *4*). + """ + if self.active_cells is not None: + return (sum(self.active_cells), self.nP) + return (self.mesh.nC, self.nP) + + def mDict(self, m): + r"""Return model parameters as a dictionary. + + For a model :math:`\mathbf{m} = [\sigma_0, \;\sigma_1,\; z_L , \; h]`, + **mDict** returns a dictionary:: + + {"val_background": m[0], "val_layer": m[1], "layer_center": m[2], "layer_thickness": m[3]} + + Returns + ------- + dict + The model as a dictionary + """ + return { + "val_background": m[0], + "val_layer": m[1], + "layer_center": m[2], + "layer_thickness": m[3], + } + + def _atanLayer(self, mDict): + if self.mesh.dim == 2: + z = self.y + elif self.mesh.dim == 3: + z = self.z + + layer_bottom = mDict["layer_center"] - mDict["layer_thickness"] / 2.0 + layer_top = mDict["layer_center"] + mDict["layer_thickness"] / 2.0 + + return self._atanfct(z - layer_bottom, self.slope) * self._atanfct( + z - layer_top, -self.slope + ) + + def _atanLayerDeriv_layer_center(self, mDict): + if self.mesh.dim == 2: + z = self.y + elif self.mesh.dim == 3: + z = self.z + + layer_bottom = mDict["layer_center"] - mDict["layer_thickness"] / 2.0 + layer_top = mDict["layer_center"] + mDict["layer_thickness"] / 2.0 + + return self._atanfctDeriv(z - layer_bottom, self.slope) * self._atanfct( + z - layer_top, -self.slope + ) + self._atanfct(z - layer_bottom, self.slope) * self._atanfctDeriv( + z - layer_top, -self.slope + ) + + def _atanLayerDeriv_layer_thickness(self, mDict): + if self.mesh.dim == 2: + z = self.y + elif self.mesh.dim == 3: + z = self.z + + layer_bottom = mDict["layer_center"] - mDict["layer_thickness"] / 2.0 + layer_top = mDict["layer_center"] + mDict["layer_thickness"] / 2.0 + + return -0.5 * self._atanfctDeriv(z - layer_bottom, self.slope) * self._atanfct( + z - layer_top, -self.slope + ) + 0.5 * self._atanfct(z - layer_bottom, self.slope) * self._atanfctDeriv( + z - layer_top, -self.slope + ) + + def layer_cont(self, mDict): + return mDict["val_background"] + ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayer(mDict) + + def _transform(self, m): + mDict = self.mDict(m) + return self.layer_cont(mDict) + + def _deriv_val_background(self, mDict): + return np.ones_like(self.x) - self._atanLayer(mDict) + + def _deriv_val_layer(self, mDict): + return self._atanLayer(mDict) + + def _deriv_layer_center(self, mDict): + return ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayerDeriv_layer_center(mDict) + + def _deriv_layer_thickness(self, mDict): + return ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayerDeriv_layer_thickness(mDict) + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the input parameters. + + Let :math:`\mathbf{m} = [\sigma_0, \;\sigma_1,\; z_L , \; h]` be the set of + model parameters the defines a layer within a wholespace. The mapping + :math:`\mathbf{u}(\mathbf{m})`from the parameterized model to all + active cells is given by: + + .. math:: + \mathbf{u}(\mathbf{m}) = \sigma_0 + \frac{(\sigma_1 - \sigma_0)}{\pi} \Bigg [ + \arctan \Bigg ( a \bigg ( \mathbf{z_c} - z_L + \frac{h}{2} \bigg ) \Bigg ) + - \arctan \Bigg ( a \bigg ( \mathbf{z_c} - z_L - \frac{h}{2} \bigg ) \Bigg ) \Bigg ] + + where :math:`\mathbf{z_c}` is a vectors containing the vertical cell center + locations for all active cells in the mesh. The derivative of the mapping + with respect to the model parameters is a ``numpy.ndarray`` of + shape (*nAct*, *4*) given by: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = + \Bigg [ \frac{\partial \mathbf{u}}{\partial \sigma_0} \;\; + \frac{\partial \mathbf{u}}{\partial \sigma_1} \;\; + \frac{\partial \mathbf{u}}{\partial z_L} \;\; + \frac{\partial \mathbf{u}}{\partial h} + \Bigg ] + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + mDict = self.mDict(m) + derivative = sp.csr_matrix( + np.vstack( + [ + self._deriv_val_background(mDict), + self._deriv_val_layer(mDict), + self._deriv_layer_center(mDict), + self._deriv_layer_thickness(mDict), + ] + ).T + ) + if v is not None: + return derivative @ v + return derivative + + +class ParametricBlock(BaseParametric): + r"""Mapping for a rectangular block within a wholespace. + + This mapping is used when the cells lying below the Earth's surface can + be parameterized by rectangular block within a homogeneous medium. + The model is defined by the physical property value for the background + (:math:`\sigma_0`), the physical property value for the block + (:math:`\sigma_b`), parameters for the center of the block + (:math:`x_b [,y_b, z_b]`) and parameters for the dimensions along + each Cartesian direction (:math:`dx [,dy, dz]`) + + For this mapping, the set of input model parameters are organized: + + .. math:: + \mathbf{m} = \begin{cases} + 1D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx] \\ + 2D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx,\; y_b , \; dy] \\ + 3D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx,\; y_b , \; dy,\; z_b , \; dz] + \end{cases} + + The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh + is given by: + + .. math:: + + \mathbf{u}(\mathbf{m}) = \sigma_0 + (\sigma_b - \sigma_0) \bigg [ \frac{1}{2} + + \pi^{-1} \arctan \bigg ( a \, \boldsymbol{\eta} \big ( + x_b, y_b, z_b, dx, dy, dz \big ) \bigg ) \bigg ] + + where *a* is a parameter that impacts the sharpness of the arctan function, and + + .. math:: + \boldsymbol{\eta} \big ( x_b, y_b, z_b, dx, dy, dz \big ) = 1 - + \sum_{\xi \in (x,y,z)} \bigg [ \bigg ( \frac{2(\boldsymbol{\xi_c} - \xi_b)}{d\xi} \bigg )^2 + \varepsilon^2 + \bigg ]^{p/2} + + Parameters :math:`p` and :math:`\varepsilon` define the parameters of the Ekblom + function. :math:`\boldsymbol{\xi_c}` is a place holder for vectors containing + the x, [y and z] cell center locations of the mesh, :math:`\xi_b` is a placeholder + for the x[, y and z] location for the center of the block, and :math:`d\xi` is a + placeholder for the x[, y and z] dimensions of the block. + + Parameters + ---------- + mesh : discretize.BaseMesh + A discretize mesh + active_cells : numpy.ndarray + Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* + or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. + slope : float + Directly define the constant *a* in the mapping function which defines the + sharpness of the boundaries. + slopeFact : float + Scaling factor for the sharpness of the boundaries based on cell size. + Using this option, we set *a = slopeFact / dh*. + epsilon : float + Epsilon value used in the ekblom representation of the block + p : float + p-value used in the ekblom representation of the block. + + Examples + -------- + In this example, we define a rectangular block in a wholespace whose + interface is sharp. We construct the mapping from the model to the + set of active cells (i.e. below the surface), We then use an active + cells mapping to map from the set of active cells to all cells in the mesh. + + >>> from simpeg.maps import ParametricBlock, InjectActiveCells + >>> from discretize import TensorMesh + >>> import numpy as np + >>> import matplotlib.pyplot as plt + + >>> dh = 0.5*np.ones(20) + >>> mesh = TensorMesh([dh, dh]) + >>> ind_active = mesh.cell_centers[:, 1] < 8 + + >>> sig0, sigb, xb, Lx, yb, Ly = 5., 10., 5., 4., 4., 2. + >>> model = np.r_[sig0, sigb, xb, Lx, yb, Ly] + + >>> block_map = ParametricBlock(mesh, active_cells=ind_active) + >>> act_map = InjectActiveCells(mesh, ind_active, 0.) + + >>> fig = plt.figure(figsize=(5, 5)) + >>> ax = fig.add_subplot(111) + >>> mesh.plot_image(act_map * block_map * model, ax=ax) + + """ + + def __init__(self, mesh, epsilon=1e-6, p=10, **kwargs): + self.epsilon = epsilon + self.p = p + super(ParametricBlock, self).__init__(mesh, **kwargs) + + @property + def epsilon(self): + """epsilon value used in the ekblom representation of the block. + + Returns + ------- + float + """ + return self._epsilon + + @epsilon.setter + def epsilon(self, value): + self._epsilon = validate_float("epsilon", value, min_val=0.0) + + @property + def p(self): + """p-value used in the ekblom representation of the block. + + Returns + ------- + float + """ + return self._p + + @p.setter + def p(self, value): + self._p = validate_float("p", value, min_val=0.0) + + @property + def nP(self): + """Number of parameters the mapping acts on. + + Returns + ------- + int + The number of the parameters defining the model depends on the dimension + of the mesh. *nP* + + - =4 for a 1D mesh + - =6 for a 2D mesh + - =8 for a 3D mesh + """ + if self.mesh.dim == 1: + return 4 + if self.mesh.dim == 2: + return 6 + elif self.mesh.dim == 3: + return 8 + + @property + def shape(self): + """Dimensions of the mapping + + Returns + ------- + tuple of int + Where *nP* is the number of parameters the mapping acts on + and *nAct* is the number of active cells in the mesh, **shape** + returns a tuple (*nAct* , *nP*). + """ + if self.active_cells is not None: + return (sum(self.active_cells), self.nP) + return (self.mesh.nC, self.nP) + + def _mDict1d(self, m): + return { + "val_background": m[0], + "val_block": m[1], + "x0": m[2], + "dx": m[3], + } + + def _mDict2d(self, m): + mDict = self._mDict1d(m) + mDict.update( + { + # 'theta_x': m[4], + "y0": m[4], + "dy": m[5], + # 'theta_y': m[7] + } + ) + return mDict + + def _mDict3d(self, m): + mDict = self._mDict2d(m) + mDict.update( + { + "z0": m[6], + "dz": m[7], + # 'theta_z': m[10] + } + ) + return mDict + + def mDict(self, m): + r"""Return model parameters as a dictionary. + + Returns + ------- + dict + The model as a dictionary + """ + return getattr(self, "_mDict{}d".format(self.mesh.dim))(m) + + def _ekblom(self, val): + return (val**2 + self.epsilon**2) ** (self.p / 2.0) + + def _ekblomDeriv(self, val): + return (self.p / 2) * (val**2 + self.epsilon**2) ** ((self.p / 2) - 1) * 2 * val + + # def _rotation(self, mDict): + # if self.mesh.dim == 2: + + # elif self.mesh.dim == 3: + + def _block1D(self, mDict): + return 1 - (self._ekblom((self.x - mDict["x0"]) / (0.5 * mDict["dx"]))) + + def _block2D(self, mDict): + return 1 - ( + self._ekblom((self.x - mDict["x0"]) / (0.5 * mDict["dx"])) + + self._ekblom((self.y - mDict["y0"]) / (0.5 * mDict["dy"])) + ) + + def _block3D(self, mDict): + return 1 - ( + self._ekblom((self.x - mDict["x0"]) / (0.5 * mDict["dx"])) + + self._ekblom((self.y - mDict["y0"]) / (0.5 * mDict["dy"])) + + self._ekblom((self.z - mDict["z0"]) / (0.5 * mDict["dz"])) + ) + + def _transform(self, m): + mDict = self.mDict(m) + return mDict["val_background"] + ( + mDict["val_block"] - mDict["val_background"] + ) * self._atanfct( + getattr(self, "_block{}D".format(self.mesh.dim))(mDict), slope=self.slope + ) + + def _deriv_val_background(self, mDict): + return 1 - self._atanfct( + getattr(self, "_block{}D".format(self.mesh.dim))(mDict), slope=self.slope + ) + + def _deriv_val_block(self, mDict): + return self._atanfct( + getattr(self, "_block{}D".format(self.mesh.dim))(mDict), slope=self.slope + ) + + def _deriv_center_block(self, mDict, orientation): + x = getattr(self, orientation) + x0 = mDict["{}0".format(orientation)] + dx = mDict["d{}".format(orientation)] + return (mDict["val_block"] - mDict["val_background"]) * ( + self._atanfctDeriv( + getattr(self, "_block{}D".format(self.mesh.dim))(mDict), + slope=self.slope, + ) + * (self._ekblomDeriv((x - x0) / (0.5 * dx))) + / -(0.5 * dx) + ) + + def _deriv_width_block(self, mDict, orientation): + x = getattr(self, orientation) + x0 = mDict["{}0".format(orientation)] + dx = mDict["d{}".format(orientation)] + return (mDict["val_block"] - mDict["val_background"]) * ( + self._atanfctDeriv( + getattr(self, "_block{}D".format(self.mesh.dim))(mDict), + slope=self.slope, + ) + * (self._ekblomDeriv((x - x0) / (0.5 * dx)) * (-(x - x0) / (0.5 * dx**2))) + ) + + def _deriv1D(self, mDict): + return np.vstack( + [ + self._deriv_val_background(mDict), + self._deriv_val_block(mDict), + self._deriv_center_block(mDict, "x"), + self._deriv_width_block(mDict, "x"), + ] + ).T + + def _deriv2D(self, mDict): + return np.vstack( + [ + self._deriv_val_background(mDict), + self._deriv_val_block(mDict), + self._deriv_center_block(mDict, "x"), + self._deriv_width_block(mDict, "x"), + self._deriv_center_block(mDict, "y"), + self._deriv_width_block(mDict, "y"), + ] + ).T + + def _deriv3D(self, mDict): + return np.vstack( + [ + self._deriv_val_background(mDict), + self._deriv_val_block(mDict), + self._deriv_center_block(mDict, "x"), + self._deriv_width_block(mDict, "x"), + self._deriv_center_block(mDict, "y"), + self._deriv_width_block(mDict, "y"), + self._deriv_center_block(mDict, "z"), + self._deriv_width_block(mDict, "z"), + ] + ).T + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the input parameters. + + Let :math:`\mathbf{m} = [\sigma_0, \;\sigma_1,\; x_b, \; dx, (\; y_b, \; dy, \; z_b , dz)]` + be the set of model parameters the defines a block/ellipsoid within a wholespace. + The mapping :math:`\mathbf{u}(\mathbf{m})` from the parameterized model to all + active cells is given by: + + The derivative of the mapping :math:`\mathbf{u}(\mathbf{m})` with respect to + the model parameters is a ``numpy.ndarray`` of shape (*nAct*, *nP*) given by: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \Bigg [ + \frac{\partial \mathbf{u}}{\partial \sigma_0} \;\; + \frac{\partial \mathbf{u}}{\partial \sigma_1} \;\; + \frac{\partial \mathbf{u}}{\partial x_b} \;\; + \frac{\partial \mathbf{u}}{\partial dx} \;\; + \frac{\partial \mathbf{u}}{\partial y_b} \;\; + \frac{\partial \mathbf{u}}{\partial dy} \;\; + \frac{\partial \mathbf{u}}{\partial z_b} \;\; + \frac{\partial \mathbf{u}}{\partial dz} + \Bigg ) \Bigg ] + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + derivative = sp.csr_matrix( + getattr(self, "_deriv{}D".format(self.mesh.dim))(self.mDict(m)) + ) + if v is not None: + return derivative @ v + return derivative + + +class ParametricEllipsoid(ParametricBlock): + r"""Mapping for a rectangular block within a wholespace. + + This mapping is used when the cells lying below the Earth's surface can + be parameterized by an ellipsoid within a homogeneous medium. + The model is defined by the physical property value for the background + (:math:`\sigma_0`), the physical property value for the layer + (:math:`\sigma_b`), parameters for the center of the ellipsoid + (:math:`x_b [,y_b, z_b]`) and parameters for the dimensions along + each Cartesian direction (:math:`dx [,dy, dz]`) + + For this mapping, the set of input model parameters are organized: + + .. math:: + \mathbf{m} = \begin{cases} + 1D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx] \\ + 2D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx,\; y_b , \; dy] \\ + 3D: \;\; [\sigma_0, \;\sigma_b,\; x_b , \; dx,\; y_b , \; dy,\; z_b , \; dz] + \end{cases} + + The mapping :math:`\mathbf{u}(\mathbf{m})` from the model to the mesh + is given by: + + .. math:: + + \mathbf{u}(\mathbf{m}) = \sigma_0 + (\sigma_b - \sigma_0) \bigg [ \frac{1}{2} + + \pi^{-1} \arctan \bigg ( a \, \boldsymbol{\eta} \big ( + x_b, y_b, z_b, dx, dy, dz \big ) \bigg ) \bigg ] + + where *a* is a parameter that impacts the sharpness of the arctan function, and + + .. math:: + \boldsymbol{\eta} \big ( x_b, y_b, z_b, dx, dy, dz \big ) = 1 - + \sum_{\xi \in (x,y,z)} \bigg [ \bigg ( \frac{2(\boldsymbol{\xi_c} - \xi_b)}{d\xi} \bigg )^2 + \varepsilon^2 + \bigg ] + + :math:`\boldsymbol{\xi_c}` is a place holder for vectors containing + the x, [y and z] cell center locations of the mesh, :math:`\xi_b` is a placeholder + for the x[, y and z] location for the center of the block, and :math:`d\xi` is a + placeholder for the x[, y and z] dimensions of the block. + + Parameters + ---------- + mesh : discretize.BaseMesh + A discretize mesh + active_cells : numpy.ndarray + Active cells array. Can be a boolean ``numpy.ndarray`` of length *mesh.nC* + or a ``numpy.ndarray`` of ``int`` containing the indices of the active cells. + slope : float + Directly define the constant *a* in the mapping function which defines the + sharpness of the boundaries. + slopeFact : float + Scaling factor for the sharpness of the boundaries based on cell size. + Using this option, we set *a = slopeFact / dh*. + epsilon : float + Epsilon value used in the ekblom representation of the block + + Examples + -------- + In this example, we define an ellipse in a wholespace whose + interface is sharp. We construct the mapping from the model to the + set of active cells (i.e. below the surface), We then use an active + cells mapping to map from the set of active cells to all cells in the mesh. + + >>> from simpeg.maps import ParametricEllipsoid, InjectActiveCells + >>> from discretize import TensorMesh + >>> import numpy as np + >>> import matplotlib.pyplot as plt + + >>> dh = 0.5*np.ones(20) + >>> mesh = TensorMesh([dh, dh]) + >>> ind_active = mesh.cell_centers[:, 1] < 8 + + >>> sig0, sigb, xb, Lx, yb, Ly = 5., 10., 5., 4., 4., 3. + >>> model = np.r_[sig0, sigb, xb, Lx, yb, Ly] + + >>> ellipsoid_map = ParametricEllipsoid(mesh, active_cells=ind_active) + >>> act_map = InjectActiveCells(mesh, ind_active, 0.) + + >>> fig = plt.figure(figsize=(5, 5)) + >>> ax = fig.add_subplot(111) + >>> mesh.plot_image(act_map * ellipsoid_map * model, ax=ax) + + """ + + def __init__(self, mesh, **kwargs): + super(ParametricEllipsoid, self).__init__(mesh, p=2, **kwargs) + + +class ParametricCasingAndLayer(ParametricLayer): + """ + Parametric layered space with casing. + + .. code:: python + + m = [val_background, + val_layer, + val_casing, + val_insideCasing, + layer_center, + layer_thickness, + casing_radius, + casing_thickness, + casing_bottom, + casing_top + ] + + """ + + def __init__(self, mesh, **kwargs): + assert ( + mesh._meshType == "CYL" + ), "Parametric Casing in a layer map only works for a cyl mesh." + + super().__init__(mesh, **kwargs) + + @property + def nP(self): + return 10 + + @property + def shape(self): + if self.active_cells is not None: + return (sum(self.active_cells), self.nP) + return (self.mesh.nC, self.nP) + + def mDict(self, m): + # m = [val_background, val_layer, val_casing, val_insideCasing, + # layer_center, layer_thickness, casing_radius, casing_thickness, + # casing_bottom, casing_top] + + return { + "val_background": m[0], + "val_layer": m[1], + "val_casing": m[2], + "val_insideCasing": m[3], + "layer_center": m[4], + "layer_thickness": m[5], + "casing_radius": m[6], + "casing_thickness": m[7], + "casing_bottom": m[8], + "casing_top": m[9], + } + + def casing_a(self, mDict): + return mDict["casing_radius"] - 0.5 * mDict["casing_thickness"] + + def casing_b(self, mDict): + return mDict["casing_radius"] + 0.5 * mDict["casing_thickness"] + + def _atanCasingLength(self, mDict): + return self._atanfct(self.z - mDict["casing_top"], -self.slope) * self._atanfct( + self.z - mDict["casing_bottom"], self.slope + ) + + def _atanCasingLengthDeriv_casing_top(self, mDict): + return self._atanfctDeriv( + self.z - mDict["casing_top"], -self.slope + ) * self._atanfct(self.z - mDict["casing_bottom"], self.slope) + + def _atanCasingLengthDeriv_casing_bottom(self, mDict): + return self._atanfct( + self.z - mDict["casing_top"], -self.slope + ) * self._atanfctDeriv(self.z - mDict["casing_bottom"], self.slope) + + def _atanInsideCasing(self, mDict): + return self._atanCasingLength(mDict) * self._atanfct( + self.x - self.casing_a(mDict), -self.slope + ) + + def _atanInsideCasingDeriv_casing_radius(self, mDict): + return self._atanCasingLength(mDict) * self._atanfctDeriv( + self.x - self.casing_a(mDict), -self.slope + ) + + def _atanInsideCasingDeriv_casing_thickness(self, mDict): + return ( + self._atanCasingLength(mDict) + * -0.5 + * self._atanfctDeriv(self.x - self.casing_a(mDict), -self.slope) + ) + + def _atanInsideCasingDeriv_casing_top(self, mDict): + return self._atanCasingLengthDeriv_casing_top(mDict) * self._atanfct( + self.x - self.casing_a(mDict), -self.slope + ) + + def _atanInsideCasingDeriv_casing_bottom(self, mDict): + return self._atanCasingLengthDeriv_casing_bottom(mDict) * self._atanfct( + self.x - self.casing_a(mDict), -self.slope + ) + + def _atanCasing(self, mDict): + return ( + self._atanCasingLength(mDict) + * self._atanfct(self.x - self.casing_a(mDict), self.slope) + * self._atanfct(self.x - self.casing_b(mDict), -self.slope) + ) + + def _atanCasingDeriv_casing_radius(self, mDict): + return self._atanCasingLength(mDict) * ( + self._atanfctDeriv(self.x - self.casing_a(mDict), self.slope) + * self._atanfct(self.x - self.casing_b(mDict), -self.slope) + + self._atanfct(self.x - self.casing_a(mDict), self.slope) + * self._atanfctDeriv(self.x - self.casing_b(mDict), -self.slope) + ) + + def _atanCasingDeriv_casing_thickness(self, mDict): + return self._atanCasingLength(mDict) * ( + -0.5 + * self._atanfctDeriv(self.x - self.casing_a(mDict), self.slope) + * self._atanfct(self.x - self.casing_b(mDict), -self.slope) + + self._atanfct(self.x - self.casing_a(mDict), self.slope) + * 0.5 + * self._atanfctDeriv(self.x - self.casing_b(mDict), -self.slope) + ) + + def _atanCasingDeriv_casing_bottom(self, mDict): + return ( + self._atanCasingLengthDeriv_casing_bottom(mDict) + * self._atanfct(self.x - self.casing_a(mDict), self.slope) + * self._atanfct(self.x - self.casing_b(mDict), -self.slope) + ) + + def _atanCasingDeriv_casing_top(self, mDict): + return ( + self._atanCasingLengthDeriv_casing_top(mDict) + * self._atanfct(self.x - self.casing_a(mDict), self.slope) + * self._atanfct(self.x - self.casing_b(mDict), -self.slope) + ) + + def layer_cont(self, mDict): + # contribution from the layered background + return mDict["val_background"] + ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayer(mDict) + + def _transform(self, m): + mDict = self.mDict(m) + + # assemble the model + layer = self.layer_cont(mDict) + casing = (mDict["val_casing"] - layer) * self._atanCasing(mDict) + insideCasing = (mDict["val_insideCasing"] - layer) * self._atanInsideCasing( + mDict + ) + + return layer + casing + insideCasing + + def _deriv_val_background(self, mDict): + # contribution from the layered background + d_layer_cont_dval_background = 1.0 - self._atanLayer(mDict) + d_casing_cont_dval_background = ( + -1.0 * d_layer_cont_dval_background * self._atanCasing(mDict) + ) + d_insideCasing_cont_dval_background = ( + -1.0 * d_layer_cont_dval_background * self._atanInsideCasing(mDict) + ) + return ( + d_layer_cont_dval_background + + d_casing_cont_dval_background + + d_insideCasing_cont_dval_background + ) + + def _deriv_val_layer(self, mDict): + d_layer_cont_dval_layer = self._atanLayer(mDict) + d_casing_cont_dval_layer = ( + -1.0 * d_layer_cont_dval_layer * self._atanCasing(mDict) + ) + d_insideCasing_cont_dval_layer = ( + -1.0 * d_layer_cont_dval_layer * self._atanInsideCasing(mDict) + ) + return ( + d_layer_cont_dval_layer + + d_casing_cont_dval_layer + + d_insideCasing_cont_dval_layer + ) + + def _deriv_val_casing(self, mDict): + d_layer_cont_dval_casing = 0.0 + d_casing_cont_dval_casing = self._atanCasing(mDict) + d_insideCasing_cont_dval_casing = 0.0 + return ( + d_layer_cont_dval_casing + + d_casing_cont_dval_casing + + d_insideCasing_cont_dval_casing + ) + + def _deriv_val_insideCasing(self, mDict): + d_layer_cont_dval_insideCasing = 0.0 + d_casing_cont_dval_insideCasing = 0.0 + d_insideCasing_cont_dval_insideCasing = self._atanInsideCasing(mDict) + return ( + d_layer_cont_dval_insideCasing + + d_casing_cont_dval_insideCasing + + d_insideCasing_cont_dval_insideCasing + ) + + def _deriv_layer_center(self, mDict): + d_layer_cont_dlayer_center = ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayerDeriv_layer_center(mDict) + d_casing_cont_dlayer_center = -d_layer_cont_dlayer_center * self._atanCasing( + mDict + ) + d_insideCasing_cont_dlayer_center = ( + -d_layer_cont_dlayer_center * self._atanInsideCasing(mDict) + ) + return ( + d_layer_cont_dlayer_center + + d_casing_cont_dlayer_center + + d_insideCasing_cont_dlayer_center + ) + + def _deriv_layer_thickness(self, mDict): + d_layer_cont_dlayer_thickness = ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayerDeriv_layer_thickness(mDict) + d_casing_cont_dlayer_thickness = ( + -d_layer_cont_dlayer_thickness * self._atanCasing(mDict) + ) + d_insideCasing_cont_dlayer_thickness = ( + -d_layer_cont_dlayer_thickness * self._atanInsideCasing(mDict) + ) + return ( + d_layer_cont_dlayer_thickness + + d_casing_cont_dlayer_thickness + + d_insideCasing_cont_dlayer_thickness + ) + + def _deriv_casing_radius(self, mDict): + layer = self.layer_cont(mDict) + d_layer_cont_dcasing_radius = 0.0 + d_casing_cont_dcasing_radius = ( + mDict["val_casing"] - layer + ) * self._atanCasingDeriv_casing_radius(mDict) + d_insideCasing_cont_dcasing_radius = ( + mDict["val_insideCasing"] - layer + ) * self._atanInsideCasingDeriv_casing_radius(mDict) + return ( + d_layer_cont_dcasing_radius + + d_casing_cont_dcasing_radius + + d_insideCasing_cont_dcasing_radius + ) + + def _deriv_casing_thickness(self, mDict): + d_layer_cont_dcasing_thickness = 0.0 + d_casing_cont_dcasing_thickness = ( + mDict["val_casing"] - self.layer_cont(mDict) + ) * self._atanCasingDeriv_casing_thickness(mDict) + d_insideCasing_cont_dcasing_thickness = ( + mDict["val_insideCasing"] - self.layer_cont(mDict) + ) * self._atanInsideCasingDeriv_casing_thickness(mDict) + return ( + d_layer_cont_dcasing_thickness + + d_casing_cont_dcasing_thickness + + d_insideCasing_cont_dcasing_thickness + ) + + def _deriv_casing_bottom(self, mDict): + d_layer_cont_dcasing_bottom = 0.0 + d_casing_cont_dcasing_bottom = ( + mDict["val_casing"] - self.layer_cont(mDict) + ) * self._atanCasingDeriv_casing_bottom(mDict) + d_insideCasing_cont_dcasing_bottom = ( + mDict["val_insideCasing"] - self.layer_cont(mDict) + ) * self._atanInsideCasingDeriv_casing_bottom(mDict) + return ( + d_layer_cont_dcasing_bottom + + d_casing_cont_dcasing_bottom + + d_insideCasing_cont_dcasing_bottom + ) + + def _deriv_casing_top(self, mDict): + d_layer_cont_dcasing_top = 0.0 + d_casing_cont_dcasing_top = ( + mDict["val_casing"] - self.layer_cont(mDict) + ) * self._atanCasingDeriv_casing_top(mDict) + d_insideCasing_cont_dcasing_top = ( + mDict["val_insideCasing"] - self.layer_cont(mDict) + ) * self._atanInsideCasingDeriv_casing_top(mDict) + return ( + d_layer_cont_dcasing_top + + d_casing_cont_dcasing_top + + d_insideCasing_cont_dcasing_top + ) + + def deriv(self, m, v=None): + mDict = self.mDict(m) + derivative = sp.csr_matrix( + np.vstack( + [ + self._deriv_val_background(mDict), + self._deriv_val_layer(mDict), + self._deriv_val_casing(mDict), + self._deriv_val_insideCasing(mDict), + self._deriv_layer_center(mDict), + self._deriv_layer_thickness(mDict), + self._deriv_casing_radius(mDict), + self._deriv_casing_thickness(mDict), + self._deriv_casing_bottom(mDict), + self._deriv_casing_top(mDict), + ] + ).T + ) + if v is not None: + return derivative @ v + return derivative + + +class ParametricBlockInLayer(ParametricLayer): + """ + Parametric Block in a Layered Space + + For 2D: + + .. code:: python + + m = [val_background, + val_layer, + val_block, + layer_center, + layer_thickness, + block_x0, + block_dx + ] + + For 3D: + + .. code:: python + + m = [val_background, + val_layer, + val_block, + layer_center, + layer_thickness, + block_x0, + block_y0, + block_dx, + block_dy + ] + + **Required** + + :param discretize.base.BaseMesh mesh: SimPEG Mesh, 2D or 3D + + **Optional** + + :param float slopeFact: arctan slope factor - divided by the minimum h + spacing to give the slope of the arctan + functions + :param float slope: slope of the arctan function + :param numpy.ndarray active_cells: bool vector with + + """ + + def __init__(self, mesh, **kwargs): + super().__init__(mesh, **kwargs) + + @property + def nP(self): + if self.mesh.dim == 2: + return 7 + elif self.mesh.dim == 3: + return 9 + + @property + def shape(self): + if self.active_cells is not None: + return (sum(self.active_cells), self.nP) + return (self.mesh.nC, self.nP) + + def _mDict2d(self, m): + return { + "val_background": m[0], + "val_layer": m[1], + "val_block": m[2], + "layer_center": m[3], + "layer_thickness": m[4], + "x0": m[5], + "dx": m[6], + } + + def _mDict3d(self, m): + return { + "val_background": m[0], + "val_layer": m[1], + "val_block": m[2], + "layer_center": m[3], + "layer_thickness": m[4], + "x0": m[5], + "y0": m[6], + "dx": m[7], + "dy": m[8], + } + + def mDict(self, m): + if self.mesh.dim == 2: + return self._mDict2d(m) + elif self.mesh.dim == 3: + return self._mDict3d(m) + + def xleft(self, mDict): + return mDict["x0"] - 0.5 * mDict["dx"] + + def xright(self, mDict): + return mDict["x0"] + 0.5 * mDict["dx"] + + def yleft(self, mDict): + return mDict["y0"] - 0.5 * mDict["dy"] + + def yright(self, mDict): + return mDict["y0"] + 0.5 * mDict["dy"] + + def _atanBlock2d(self, mDict): + return ( + self._atanLayer(mDict) + * self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + ) + + def _atanBlock2dDeriv_layer_center(self, mDict): + return ( + self._atanLayerDeriv_layer_center(mDict) + * self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + ) + + def _atanBlock2dDeriv_layer_thickness(self, mDict): + return ( + self._atanLayerDeriv_layer_thickness(mDict) + * self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + ) + + def _atanBlock2dDeriv_x0(self, mDict): + return self._atanLayer(mDict) * ( + ( + self._atanfctDeriv(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + ) + + ( + self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfctDeriv(self.x - self.xright(mDict), -self.slope) + ) + ) + + def _atanBlock2dDeriv_dx(self, mDict): + return self._atanLayer(mDict) * ( + ( + self._atanfctDeriv(self.x - self.xleft(mDict), self.slope) + * -0.5 + * self._atanfct(self.x - self.xright(mDict), -self.slope) + ) + + ( + self._atanfct(self.x - self.xleft(mDict), self.slope) + * 0.5 + * self._atanfctDeriv(self.x - self.xright(mDict), -self.slope) + ) + ) + + def _atanBlock3d(self, mDict): + return ( + self._atanLayer(mDict) + * self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + * self._atanfct(self.y - self.yleft(mDict), self.slope) + * self._atanfct(self.y - self.yright(mDict), -self.slope) + ) + + def _atanBlock3dDeriv_layer_center(self, mDict): + return ( + self._atanLayerDeriv_layer_center(mDict) + * self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + * self._atanfct(self.y - self.yleft(mDict), self.slope) + * self._atanfct(self.y - self.yright(mDict), -self.slope) + ) + + def _atanBlock3dDeriv_layer_thickness(self, mDict): + return ( + self._atanLayerDeriv_layer_thickness(mDict) + * self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + * self._atanfct(self.y - self.yleft(mDict), self.slope) + * self._atanfct(self.y - self.yright(mDict), -self.slope) + ) + + def _atanBlock3dDeriv_x0(self, mDict): + return self._atanLayer(mDict) * ( + ( + self._atanfctDeriv(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + * self._atanfct(self.y - self.yleft(mDict), self.slope) + * self._atanfct(self.y - self.yright(mDict), -self.slope) + ) + + ( + self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfctDeriv(self.x - self.xright(mDict), -self.slope) + * self._atanfct(self.y - self.yleft(mDict), self.slope) + * self._atanfct(self.y - self.yright(mDict), -self.slope) + ) + ) + + def _atanBlock3dDeriv_y0(self, mDict): + return self._atanLayer(mDict) * ( + ( + self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + * self._atanfctDeriv(self.y - self.yleft(mDict), self.slope) + * self._atanfct(self.y - self.yright(mDict), -self.slope) + ) + + ( + self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + * self._atanfct(self.y - self.yleft(mDict), self.slope) + * self._atanfctDeriv(self.y - self.yright(mDict), -self.slope) + ) + ) + + def _atanBlock3dDeriv_dx(self, mDict): + return self._atanLayer(mDict) * ( + ( + self._atanfctDeriv(self.x - self.xleft(mDict), self.slope) + * -0.5 + * self._atanfct(self.x - self.xright(mDict), -self.slope) + * self._atanfct(self.y - self.yleft(mDict), self.slope) + * self._atanfct(self.y - self.yright(mDict), -self.slope) + ) + + ( + self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfctDeriv(self.x - self.xright(mDict), -self.slope) + * 0.5 + * self._atanfct(self.y - self.yleft(mDict), self.slope) + * self._atanfct(self.y - self.yright(mDict), -self.slope) + ) + ) + + def _atanBlock3dDeriv_dy(self, mDict): + return self._atanLayer(mDict) * ( + ( + self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + * self._atanfctDeriv(self.y - self.yleft(mDict), self.slope) + * -0.5 + * self._atanfct(self.y - self.yright(mDict), -self.slope) + ) + + ( + self._atanfct(self.x - self.xleft(mDict), self.slope) + * self._atanfct(self.x - self.xright(mDict), -self.slope) + * self._atanfct(self.y - self.yleft(mDict), self.slope) + * self._atanfctDeriv(self.y - self.yright(mDict), -self.slope) + * 0.5 + ) + ) + + def _transform2d(self, m): + mDict = self.mDict(m) + # assemble the model + # contribution from the layered background + layer_cont = mDict["val_background"] + ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayer(mDict) + + # perturbation due to the blocks + block_cont = (mDict["val_block"] - layer_cont) * self._atanBlock2d(mDict) + + return layer_cont + block_cont + + def _deriv2d_val_background(self, mDict): + d_layer_dval_background = np.ones_like(self.x) - self._atanLayer(mDict) + d_block_dval_background = (-d_layer_dval_background) * self._atanBlock2d(mDict) + return d_layer_dval_background + d_block_dval_background + + def _deriv2d_val_layer(self, mDict): + d_layer_dval_layer = self._atanLayer(mDict) + d_block_dval_layer = (-d_layer_dval_layer) * self._atanBlock2d(mDict) + return d_layer_dval_layer + d_block_dval_layer + + def _deriv2d_val_block(self, mDict): + d_layer_dval_block = 0.0 + d_block_dval_block = (1.0 - d_layer_dval_block) * self._atanBlock2d(mDict) + return d_layer_dval_block + d_block_dval_block + + def _deriv2d_layer_center(self, mDict): + d_layer_dlayer_center = ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayerDeriv_layer_center(mDict) + d_block_dlayer_center = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock2dDeriv_layer_center( + mDict + ) - d_layer_dlayer_center * self._atanBlock2d( + mDict + ) + return d_layer_dlayer_center + d_block_dlayer_center + + def _deriv2d_layer_thickness(self, mDict): + d_layer_dlayer_thickness = ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayerDeriv_layer_thickness(mDict) + d_block_dlayer_thickness = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock2dDeriv_layer_thickness( + mDict + ) - d_layer_dlayer_thickness * self._atanBlock2d( + mDict + ) + return d_layer_dlayer_thickness + d_block_dlayer_thickness + + def _deriv2d_x0(self, mDict): + d_layer_dx0 = 0.0 + d_block_dx0 = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock2dDeriv_x0(mDict) + return d_layer_dx0 + d_block_dx0 + + def _deriv2d_dx(self, mDict): + d_layer_ddx = 0.0 + d_block_ddx = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock2dDeriv_dx(mDict) + return d_layer_ddx + d_block_ddx + + def _deriv2d(self, m): + mDict = self.mDict(m) + + return np.vstack( + [ + self._deriv2d_val_background(mDict), + self._deriv2d_val_layer(mDict), + self._deriv2d_val_block(mDict), + self._deriv2d_layer_center(mDict), + self._deriv2d_layer_thickness(mDict), + self._deriv2d_x0(mDict), + self._deriv2d_dx(mDict), + ] + ).T + + def _transform3d(self, m): + # parse model + mDict = self.mDict(m) + + # assemble the model + # contribution from the layered background + layer_cont = mDict["val_background"] + ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayer(mDict) + # perturbation due to the block + block_cont = (mDict["val_block"] - layer_cont) * self._atanBlock3d(mDict) + + return layer_cont + block_cont + + def _deriv3d_val_background(self, mDict): + d_layer_dval_background = np.ones_like(self.x) - self._atanLayer(mDict) + d_block_dval_background = (-d_layer_dval_background) * self._atanBlock3d(mDict) + return d_layer_dval_background + d_block_dval_background + + def _deriv3d_val_layer(self, mDict): + d_layer_dval_layer = self._atanLayer(mDict) + d_block_dval_layer = (-d_layer_dval_layer) * self._atanBlock3d(mDict) + return d_layer_dval_layer + d_block_dval_layer + + def _deriv3d_val_block(self, mDict): + d_layer_dval_block = 0.0 + d_block_dval_block = (1.0 - d_layer_dval_block) * self._atanBlock3d(mDict) + return d_layer_dval_block + d_block_dval_block + + def _deriv3d_layer_center(self, mDict): + d_layer_dlayer_center = ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayerDeriv_layer_center(mDict) + d_block_dlayer_center = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock3dDeriv_layer_center( + mDict + ) - d_layer_dlayer_center * self._atanBlock3d( + mDict + ) + return d_layer_dlayer_center + d_block_dlayer_center + + def _deriv3d_layer_thickness(self, mDict): + d_layer_dlayer_thickness = ( + mDict["val_layer"] - mDict["val_background"] + ) * self._atanLayerDeriv_layer_thickness(mDict) + d_block_dlayer_thickness = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock3dDeriv_layer_thickness( + mDict + ) - d_layer_dlayer_thickness * self._atanBlock3d( + mDict + ) + return d_layer_dlayer_thickness + d_block_dlayer_thickness + + def _deriv3d_x0(self, mDict): + d_layer_dx0 = 0.0 + d_block_dx0 = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock3dDeriv_x0(mDict) + return d_layer_dx0 + d_block_dx0 + + def _deriv3d_y0(self, mDict): + d_layer_dy0 = 0.0 + d_block_dy0 = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock3dDeriv_y0(mDict) + return d_layer_dy0 + d_block_dy0 + + def _deriv3d_dx(self, mDict): + d_layer_ddx = 0.0 + d_block_ddx = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock3dDeriv_dx(mDict) + return d_layer_ddx + d_block_ddx + + def _deriv3d_dy(self, mDict): + d_layer_ddy = 0.0 + d_block_ddy = ( + mDict["val_block"] - self.layer_cont(mDict) + ) * self._atanBlock3dDeriv_dy(mDict) + return d_layer_ddy + d_block_ddy + + def _deriv3d(self, m): + mDict = self.mDict(m) + + return np.vstack( + [ + self._deriv3d_val_background(mDict), + self._deriv3d_val_layer(mDict), + self._deriv3d_val_block(mDict), + self._deriv3d_layer_center(mDict), + self._deriv3d_layer_thickness(mDict), + self._deriv3d_x0(mDict), + self._deriv3d_y0(mDict), + self._deriv3d_dx(mDict), + self._deriv3d_dy(mDict), + ] + ).T + + def _transform(self, m): + if self.mesh.dim == 2: + return self._transform2d(m) + elif self.mesh.dim == 3: + return self._transform3d(m) + + def deriv(self, m, v=None): + derivative = ( + sp.csr_matrix(self._deriv2d(m)) + if self.mesh.dim == 2 + else sp.csr_matrix(self._deriv3d(m)) + ) + if v is not None: + return derivative @ v + return derivative diff --git a/simpeg/maps/_property_maps.py b/simpeg/maps/_property_maps.py new file mode 100644 index 0000000000..0da639e23e --- /dev/null +++ b/simpeg/maps/_property_maps.py @@ -0,0 +1,1528 @@ +""" +Maps that transform physical properties from one space to another. +""" + +import warnings +from numbers import Real +import numpy as np +import scipy.sparse as sp +from scipy.constants import mu_0 +from scipy.special import expit, logit +from discretize.utils import mkvc, sdiag, rotation_matrix_from_normals + +from ._base import IdentityMap + +from ..utils import validate_integer, validate_direction, validate_float, validate_type + + +class ExpMap(IdentityMap): + r"""Mapping that computes the natural exponentials of the model parameters. + + Where :math:`\mathbf{m}` is a set of model parameters, ``ExpMap`` creates + a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the natural exponential + of every element in :math:`\mathbf{m}`; i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = exp(\mathbf{m}) + + ``ExpMap`` is commonly used when working with physical properties whose values + span many orders of magnitude (e.g. the electrical conductivity :math:`\sigma`). + By using ``ExpMap``, we can invert for a model that represents the natural log + of a set of physical property values, i.e. when :math:`m = log(\sigma)` + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal the number + of mesh cells. + nP : int + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + """ + + def __init__(self, mesh=None, nP=None, **kwargs): + super().__init__(mesh=mesh, nP=nP, **kwargs) + + def _transform(self, m): + return np.exp(mkvc(m)) + + def inverse(self, D): + r"""Apply the inverse of the exponential mapping to an array. + + For the exponential mapping :math:`\mathbf{u}(\mathbf{m})`, the + inverse mapping on a variable :math:`\mathbf{x}` is performed by taking + the natural logarithms of elements, i.e.: + + .. math:: + \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = log(\mathbf{x}) + + Parameters + ---------- + D : numpy.ndarray + A set of input values + + Returns + ------- + numpy.ndarray + A :class:`numpy.ndarray` containing result of applying the + inverse mapping to the elements in *D*; which in this case + is the natural logarithm. + """ + return np.log(mkvc(D)) + + def deriv(self, m, v=None): + r"""Derivative of mapping with respect to the input parameters. + + For a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the natural + exponential function for each parameter in the model :math:`\mathbf{m}`, + i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = exp(\mathbf{m}), + + the derivative of the mapping with respect to the model is a diagonal + matrix of the form: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} + = \textrm{diag} \big ( exp(\mathbf{m}) \big ) + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + deriv = sdiag(np.exp(mkvc(m))) + if v is not None: + return deriv * v + return deriv + + @property + def is_linear(self): + return False + + +class ReciprocalMap(IdentityMap): + r"""Mapping that computes the reciprocals of the model parameters. + + Where :math:`\mathbf{m}` is a set of model parameters, ``ReciprocalMap`` + creates a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the + reciprocal of every element in :math:`\mathbf{m}`; + i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{m}^{-1} + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal the number + of mesh cells. + nP : int + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + """ + + def __init__(self, mesh=None, nP=None, **kwargs): + super().__init__(mesh=mesh, nP=nP, **kwargs) + + def _transform(self, m): + return 1.0 / mkvc(m) + + def inverse(self, D): + r"""Apply the inverse of the reciprocal mapping to an array. + + For the reciprocal mapping :math:`\mathbf{u}(\mathbf{m})`, + the inverse mapping on a variable :math:`\mathbf{x}` is itself a + reciprocal mapping, i.e.: + + .. math:: + \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = \mathbf{x}^{-1} + + Parameters + ---------- + D : numpy.ndarray + A set of input values + + Returns + ------- + numpy.ndarray + A :class:`numpy.ndarray` containing result of applying the + inverse mapping to the elements in *D*; which in this case + is just a reciprocal mapping. + """ + return 1.0 / mkvc(D) + + def deriv(self, m, v=None): + r"""Derivative of mapping with respect to the input parameters. + + For a mapping that computes the reciprocal for each + parameter in the model :math:`\mathbf{m}`, i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{m}^{-1} + + the derivative of the mapping with respect to the model is a diagonal + matrix of the form: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} + = \textrm{diag} \big ( -\mathbf{m}^{-2} \big ) + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + deriv = sdiag(-mkvc(m) ** (-2)) + if v is not None: + return deriv * v + return deriv + + @property + def is_linear(self): + return False + + +class LogMap(IdentityMap): + r"""Mapping that computes the natural logarithm of the model parameters. + + Where :math:`\mathbf{m}` is a set of model parameters, ``LogMap`` + creates a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the + natural logarithm of every element in + :math:`\mathbf{m}`; i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = \textrm{log}(\mathbf{m}) + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal the number + of mesh cells. + nP : int + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + """ + + def __init__(self, mesh=None, nP=None, **kwargs): + super().__init__(mesh=mesh, nP=nP, **kwargs) + + def _transform(self, m): + return np.log(mkvc(m)) + + def deriv(self, m, v=None): + r"""Derivative of mapping with respect to the input parameters. + + For a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the + natural logarithm for each parameter in the model :math:`\mathbf{m}`, + i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = log(\mathbf{m}) + + the derivative of the mapping with respect to the model is a diagonal + matrix of the form: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} + = \textrm{diag} \big ( \mathbf{m}^{-1} \big ) + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + mod = mkvc(m) + deriv = np.zeros(mod.shape) + tol = 1e-16 # zero + ind = np.greater_equal(np.abs(mod), tol) + deriv[ind] = 1.0 / mod[ind] + if v is not None: + return sdiag(deriv) * v + return sdiag(deriv) + + def inverse(self, m): + r"""Apply the inverse of the natural log mapping to an array. + + For the natural log mapping :math:`\mathbf{u}(\mathbf{m})`, + the inverse mapping on a variable :math:`\mathbf{x}` is performed by + taking the natural exponent of the elements, i.e.: + + .. math:: + \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = exp(\mathbf{x}) + + Parameters + ---------- + D : numpy.ndarray + A set of input values + + Returns + ------- + numpy.ndarray + A :class:`numpy.ndarray` containing result of applying the + inverse mapping to the elements in *D*; which in this case + is the natural exponent. + """ + return np.exp(mkvc(m)) + + @property + def is_linear(self): + return False + + +class LogisticSigmoidMap(IdentityMap): + r"""Mapping that computes the logistic sigmoid of the model parameters. + + Where :math:`\mathbf{m}` is a set of model parameters, ``LogisticSigmoidMap`` creates + a mapping :math:`\mathbf{u}(\mathbf{m})` that computes the logistic sigmoid + of every element in :math:`\mathbf{m}`; i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = sigmoid(\mathbf{m}) = \frac{1}{1+\exp{-\mathbf{m}}} + + ``LogisticSigmoidMap`` transforms values onto the interval (0,1), but can optionally + be scaled and shifted to the interval (a,b). This can be useful for inversion + of data that varies over a log scale and bounded on some interval: + + .. math:: + \mathbf{u}(\mathbf{m}) = a + (b - a) \cdot sigmoid(\mathbf{m}) + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal the number + of mesh cells. + nP : int + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + lower_bound: float or (nP) numpy.ndarray + lower bound (a) for the transform. Default 0. Defined \in \mathbf{u} space. + upper_bound: float or (nP) numpy.ndarray + upper bound (b) for the transform. Default 1. Defined \in \mathbf{u} space. + + """ + + def __init__(self, mesh=None, nP=None, lower_bound=0, upper_bound=1, **kwargs): + super().__init__(mesh=mesh, nP=nP, **kwargs) + lower_bound = np.atleast_1d(lower_bound) + upper_bound = np.atleast_1d(upper_bound) + if self.nP != "*": + # check if lower bound and upper bound broadcast to nP + try: + np.broadcast_shapes(lower_bound.shape, (self.nP,)) + except ValueError as err: + raise ValueError( + f"Lower bound does not broadcast to the number of parameters. " + f"Lower bound shape is {lower_bound.shape} and tried against " + f"{self.nP} parameters." + ) from err + try: + np.broadcast_shapes(upper_bound.shape, (self.nP,)) + except ValueError as err: + raise ValueError( + f"Upper bound does not broadcast to the number of parameters. " + f"Upper bound shape is {upper_bound.shape} and tried against " + f"{self.nP} parameters." + ) from err + # make sure lower and upper bound broadcast to each other... + try: + np.broadcast_shapes(lower_bound.shape, upper_bound.shape) + except ValueError as err: + raise ValueError( + f"Upper bound does not broadcast to the lower bound. " + f"Shapes {upper_bound.shape} and {lower_bound.shape} " + f"are incompatible with each other." + ) from err + + if np.any(lower_bound >= upper_bound): + raise ValueError( + "A lower bound is greater than or equal to the upper bound." + ) + + self._lower_bound = lower_bound + self._upper_bound = upper_bound + + @property + def lower_bound(self): + """The lower bound + + Returns + ------- + numpy.ndarray + """ + return self._lower_bound + + @property + def upper_bound(self): + """The upper bound + + Returns + ------- + numpy.ndarray + """ + return self._upper_bound + + def _transform(self, m): + return self.lower_bound + (self.upper_bound - self.lower_bound) * expit(mkvc(m)) + + def inverse(self, m): + r"""Apply the inverse of the mapping to an array. + + For the logistic sigmoid mapping :math:`\mathbf{u}(\mathbf{m})`, the + inverse mapping on a variable :math:`\mathbf{x}` is performed by taking + the log-odds of elements, i.e.: + + .. math:: + \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = logit(\mathbf{x}) = \log \frac{\mathbf{x}}{1 - \mathbf{x}} + + or scaled and translated to interval (a,b): + .. math:: + \mathbf{m} = logit(\frac{(\mathbf{x} - a)}{b-a}) + + Parameters + ---------- + m : numpy.ndarray + A set of input values + + Returns + ------- + numpy.ndarray + the inverse mapping to the elements in *m*; which in this case + is the log-odds function with scaled and shifted input. + """ + return logit( + (mkvc(m) - self.lower_bound) / (self.upper_bound - self.lower_bound) + ) + + def deriv(self, m, v=None): + r"""Derivative of mapping with respect to the input parameters. + + For a mapping :math:`\mathbf{u}(\mathbf{m})` the derivative of the mapping with + respect to the model is a diagonal matrix of the form: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} + = \textrm{diag} \big ( (b-a)\cdot sigmoid(\mathbf{m})\cdot(1-sigmoid(\mathbf{m})) \big ) + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + numpy.ndarray or scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + sigmoid = expit(mkvc(m)) + deriv = (self.upper_bound - self.lower_bound) * sigmoid * (1.0 - sigmoid) + if v is not None: + return deriv * v + return sdiag(deriv) + + @property + def is_linear(self): + return False + + +class ChiMap(IdentityMap): + r"""Mapping that computes the magnetic permeability given a set of magnetic susceptibilities. + + Where :math:`\boldsymbol{\chi}` is the input model parameters defining a set of magnetic + susceptibilities, ``ChiMap`` creates a mapping :math:`\boldsymbol{\mu}(\boldsymbol{\chi})` + that computes the corresponding magnetic permeabilities of every + element in :math:`\boldsymbol{\chi}`; i.e.: + + .. math:: + \boldsymbol{\mu}(\boldsymbol{\chi}) = \mu_0 \big (1 + \boldsymbol{\chi} \big ) + + where :math:`\mu_0` is the permeability of free space. + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal the number + of mesh cells. + nP : int + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + """ + + def __init__(self, mesh=None, nP=None, **kwargs): + super().__init__(mesh=mesh, nP=nP, **kwargs) + + def _transform(self, m): + return mu_0 * (1 + m) + + def deriv(self, m, v=None): + r"""Derivative of mapping with respect to the input parameters. + + For a mapping :math:`\boldsymbol{\mu}(\boldsymbol{\chi})` that transforms a + set of magnetic susceptibilities :math:`\boldsymbol{\chi}` to their corresponding + magnetic permeabilities, i.e.: + + .. math:: + \boldsymbol{\mu}(\boldsymbol{\chi}) = \mu_0 \big (1 + \boldsymbol{\chi} \big ), + + the derivative of the mapping with respect to the model is the identity + matrix scaled by the permeability of free-space. Thus: + + .. math:: + \frac{\partial \boldsymbol{\mu}}{\partial \boldsymbol{\chi}} = \mu_0 \mathbf{I} + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + if v is not None: + return mu_0 * v + return mu_0 * sp.eye(self.nP) + + def inverse(self, m): + r"""Apply the inverse mapping to an array. + + For the ``ChiMap`` class, the inverse mapping recoveres the set of + magnetic susceptibilities :math:`\boldsymbol{\chi}` from a set of + magnetic permeabilities :math:`\boldsymbol{\mu}`. Thus the inverse + mapping is defined as: + + .. math:: + \boldsymbol{\chi}(\boldsymbol{\mu}) = \frac{\boldsymbol{\mu}}{\mu_0} - 1 + + where :math:`\mu_0` is the permeability of free space. + + Parameters + ---------- + D : numpy.ndarray + A set of input values + + Returns + ------- + numpy.ndarray + A :class:`numpy.ndarray` containing result of applying the + inverse mapping to the elements in *D*; which in this case + represents the conversion of magnetic permeabilities + to their corresponding magnetic susceptibility values. + """ + return m / mu_0 - 1 + + +class EffectiveSusceptibilityMap(IdentityMap): + r"""Effective susceptibility Map + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal the number + of mesh cells. + nP : int + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + ambient_field_magnitude : float + The magnitude of the ambient geomagnetic field in nT. + + Notes + ----- + This map converts effective susceptibility values (:math:`\chi_\text{eff}`) into magnetic + polarization (:math:`\mathbf{I}`): + + .. math:: + \mathbf{I} = \mu_0 \mathbf{M} = \chi_\text{eff} \lVert \mathbf{B}_0 \rVert + + where :math:`\mathbf{M}` is the magnetization vector, and + :math:`\lVert \mathbf{B}_0 \rVert` is the magnitude of the ambient field in nT. + """ + + def __init__(self, ambient_field_magnitude, mesh=None, nP=None, **kwargs): + super().__init__(mesh=mesh, nP=nP, **kwargs) + if not isinstance(ambient_field_magnitude, Real): + raise TypeError( + "ambient_field_magnitude must be a float (or int convertible to float)" + ) + self.ambient_field_magnitude = ambient_field_magnitude + + def _transform(self, m): + return m * self.ambient_field_magnitude + + def deriv(self, m, v=None): + if v is not None: + return self.ambient_field_magnitude * v + return self.ambient_field_magnitude * sp.eye(self.nP) + + def inverse(self, m): + return m / self.ambient_field_magnitude + + +class MuRelative(IdentityMap): + r"""Mapping that computes the magnetic permeability given a set of relative permeabilities. + + Where :math:`\boldsymbol{\mu_r}` defines a set of relative permeabilities, ``MuRelative`` + creates a mapping :math:`\boldsymbol{\mu}(\boldsymbol{\mu_r})` that computes the + corresponding magnetic permeabilities of every element in :math:`\boldsymbol{\mu_r}`; + i.e.: + + .. math:: + \boldsymbol{\mu}(\boldsymbol{\mu_r}) = \mu_0 \boldsymbol{\mu_r} + + where :math:`\mu_0` is the permeability of free space. + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal the number + of mesh cells. + nP : int + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + """ + + def __init__(self, mesh=None, nP=None, **kwargs): + super().__init__(mesh=mesh, nP=nP, **kwargs) + + def _transform(self, m): + return mu_0 * m + + def deriv(self, m, v=None): + r"""Derivative of mapping with respect to the input parameters. + + For a mapping that transforms a set of relative permeabilities + :math:`\boldsymbol{\mu_r}` to their corresponding magnetic permeabilities, i.e.: + + .. math:: + \boldsymbol{\mu}(\boldsymbol{\mu_r}) = \mu_0 \boldsymbol{\mu_r}, + + the derivative of the mapping with respect to the model is the identity + matrix scaled by the permeability of free-space. Thus: + + .. math:: + \frac{\partial \boldsymbol{\mu}}{\partial \boldsymbol{\mu_r}} = \mu_0 \mathbf{I} + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + if v is not None: + return mu_0 * v + return mu_0 * sp.eye(self.nP) + + def inverse(self, m): + r"""Apply the inverse mapping to an array. + + For the ``MuRelative`` class, the inverse mapping recoveres the set of + relative permeabilities :math:`\boldsymbol{\mu_r}` from a set of + magnetic permeabilities :math:`\boldsymbol{\mu}`. Thus the inverse + mapping is defined as: + + .. math:: + \boldsymbol{\mu_r}(\boldsymbol{\mu}) = \frac{\boldsymbol{\mu}}{\mu_0} + + where :math:`\mu_0` is the permeability of free space. + + Parameters + ---------- + D : numpy.ndarray + A set of input values + + Returns + ------- + numpy.ndarray + A :class:`numpy.ndarray` containing result of applying the + inverse mapping to the elements in *D*; which in this case + represents the conversion of magnetic permeabilities + to their corresponding relative permeability values. + """ + return 1.0 / mu_0 * m + + +class Weighting(IdentityMap): + r"""Mapping that scales the elements of the model by a corresponding set of weights. + + Where :math:`\mathbf{m}` defines the set of input model parameters and + :math:`\mathbf{w}` represents a corresponding set of model weight, + ``Weighting`` constructs a mapping :math:`\mathbf{u}(\mathbf{m})` of the form: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{w} \odot \mathbf{m} + + where :math:`\odot` is the Hadamard product. The mapping may also be + defined using a linear operator as follows: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} \;\;\;\;\; \textrm{where} \;\;\;\;\; \mathbf{P} = diag(\mathbf{w}) + + Parameters + ---------- + mesh : discretize.BaseMesh + The number of parameters accepted by the mapping is set to equal the number + of mesh cells. + nP : int + Set the number of parameters accepted by the mapping directly. Used if the + number of parameters is known. Used generally when the number of parameters + is not equal to the number of cells in a mesh. + weights : (nP) numpy.ndarray + A set of independent model weights. If ``None``, all model weights are set + to *1*. + """ + + def __init__(self, mesh=None, nP=None, weights=None, **kwargs): + if "nC" in kwargs: + raise TypeError( + "`nC` has been removed. Use `nP` to set the number of model " + "parameters." + ) + + super(Weighting, self).__init__(mesh=mesh, nP=nP, **kwargs) + + if weights is None: + weights = np.ones(self.nP) + + self.weights = np.array(weights, dtype=float) + + @property + def shape(self): + """Dimensions of the mapping. + + Returns + ------- + tuple + Dimensions of the mapping. Where *nP* is the number of parameters + the mapping acts on, this method returns a tuple of the form + (*nP*, *nP*). + """ + return (self.nP, self.nP) + + @property + def P(self): + r"""The linear mapping operator + + This property returns the sparse matrix :math:`\mathbf{P}` that carries + out the weighting mapping via matrix-vector product, i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} \;\;\;\;\; \textrm{where} \;\;\;\;\; \mathbf{P} = diag(\mathbf{w}) + + Returns + ------- + scipy.sparse.csr_matrix + Sparse linear mapping operator + """ + return sdiag(self.weights) + + def _transform(self, m): + return self.weights * m + + def inverse(self, D): + r"""Apply the inverse of the weighting mapping to an array. + + For the weighting mapping :math:`\mathbf{u}(\mathbf{m})`, the inverse + mapping on a variable :math:`\mathbf{x}` is performed by multplying each element by + the reciprocal of its corresponding weighting value, i.e.: + + .. math:: + \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = \mathbf{w}^{-1} \odot \mathbf{x} + + where :math:`\odot` is the Hadamard product. The inverse mapping may also be defined + using a linear operator as follows: + + .. math:: + \mathbf{m} = \mathbf{u}^{-1}(\mathbf{x}) = \mathbf{P^{-1} m} + \;\;\;\;\; \textrm{where} \;\;\;\;\; \mathbf{P} = diag(\mathbf{w}) + + Parameters + ---------- + D : numpy.ndarray + A set of input values + + Returns + ------- + numpy.ndarray + A :class:`numpy.ndarray` containing result of applying the + inverse mapping to the elements in *D*; which in this case + is simply dividing each element by its corresponding + weight. + """ + return self.weights ** (-1.0) * D + + def deriv(self, m, v=None): + r"""Derivative of mapping with respect to the input parameters. + + For a weighting mapping :math:`\mathbf{u}(\mathbf{m})` that scales the + input parameters in the model :math:`\mathbf{m}` by their corresponding + weights :math:`\mathbf{w}`; i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{w} \dot \mathbf{m}, + + the derivative of the mapping with respect to the model is a diagonal + matrix of the form: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} + = diag (\mathbf{w}) + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + if v is not None: + return self.weights * v + return self.P + + +class ComplexMap(IdentityMap): + r"""Maps the real and imaginary component values stored in a model to complex values. + + Let :math:`\mathbf{m}` be a model which stores the real and imaginary components of + a set of complex values :math:`\mathbf{z}`. Where the model parameters are organized + into a vector of the form + :math:`\mathbf{m} = [\mathbf{z}^\prime , \mathbf{z}^{\prime\prime}]`, ``ComplexMap`` + constructs the following mapping: + + .. math:: + \mathbf{z}(\mathbf{m}) = \mathbf{z}^\prime + j \mathbf{z}^{\prime\prime} + + Note that the mapping is :math:`\mathbb{R}^{2n} \rightarrow \mathbb{C}^n`. + + Parameters + ---------- + mesh : discretize.BaseMesh + If a mesh is used to construct the mapping, the number of input model + parameters is *2\*mesh.nC* and the number of complex values output from + the mapping is equal to *mesh.nC*. If *mesh* is ``None``, the dimensions + of the mapping are set using the *nP* input argument. + nP : int + Defines the number of input model parameters directly. Must be an even number!!! + In this case, the number of complex values output from the mapping is *nP/2*. + If *nP* = ``None``, the dimensions of the mapping are set using the *mesh* + input argument. + + Examples + -------- + Here we construct a complex mapping on a 1D mesh comprised + of 4 cells. The input model is real-valued array of length 8 + (4 real and 4 imaginary values). The output of the mapping + is a complex array with 4 values. + + >>> from simpeg.maps import ComplexMap + >>> from discretize import TensorMesh + >>> import numpy as np + + >>> nC = 4 + >>> mesh = TensorMesh([np.ones(nC)]) + + >>> z_real = np.ones(nC) + >>> z_imag = 2*np.ones(nC) + >>> m = np.r_[z_real, z_imag] + >>> m + array([1., 1., 1., 1., 2., 2., 2., 2.]) + + >>> mapping = ComplexMap(mesh=mesh) + >>> z = mapping * m + >>> z + array([1.+2.j, 1.+2.j, 1.+2.j, 1.+2.j]) + + """ + + def __init__(self, mesh=None, nP=None, **kwargs): + super().__init__(mesh=mesh, nP=nP, **kwargs) + if nP is not None and mesh is not None: + assert ( + 2 * mesh.nC == nP + ), "Number parameters must be 2 X number of mesh cells." + if nP is not None: + assert nP % 2 == 0, "nP must be even." + self._nP = nP or int(self.mesh.nC * 2) + + @property + def nP(self): + r"""Number of parameters the mapping acts on. + + Returns + ------- + int or '*' + Number of parameters that the mapping acts on. + """ + return self._nP + + @property + def shape(self): + """Dimensions of the mapping + + Returns + ------- + tuple + The dimensions of the mapping. Where *nP* is the number + of input parameters, this property returns a tuple + (*nP/2*, *nP*). + """ + return (int(self.nP / 2), self.nP) + + def _transform(self, m): + nC = int(self.nP / 2) + return m[:nC] + m[nC:] * 1j + + def deriv(self, m, v=None): + r"""Derivative of the complex mapping with respect to the input parameters. + + The complex mapping maps the real and imaginary components stored in a model + of the form :math:`\mathbf{m} = [\mathbf{z}^\prime , \mathbf{z}^{\prime\prime}]` + to their corresponding complex values :math:`\mathbf{z}`, i.e. + + .. math:: + \mathbf{z}(\mathbf{m}) = \mathbf{z}^\prime + j \mathbf{z}^{\prime\prime} + + The derivative of the mapping with respect to the model is block + matrix of the form: + + .. math:: + \frac{\partial \mathbf{z}}{\partial \mathbf{m}} = \big ( \mathbf{I} \;\;\; j\mathbf{I} \big ) + + where :math:`\mathbf{I}` is the identity matrix of shape (*nP/2*, *nP/2*) and + :math:`j = \sqrt{-1}`. + + .. important:: + + Calculating the transpose of the derivative of the + :class:`~simpeg.maps.ComplexMap` as follows doesn't return the adjoint of + the matrix, but its transpose: + + .. code:: python + + complex_map = ComplexMap(...) + derivative = complex_map.deriv(m) + derivative.T # this is not the complex adjoint + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + + Examples + -------- + Here we construct the derivative operator for the complex mapping on a 1D + mesh comprised of 4 cells. We then demonstrate how the derivative of the + mapping and its adjoint can be applied to a vector. + + >>> from simpeg.maps import ComplexMap + >>> from discretize import TensorMesh + >>> import numpy as np + + >>> nC = 4 + >>> mesh = TensorMesh([np.ones(nC)]) + + >>> m = np.random.rand(2*nC) + >>> mapping = ComplexMap(mesh=mesh) + >>> M = mapping.deriv(m) + + When applying the derivative operator to a vector, it will convert + the real and imaginary values stored in the vector to + complex values; essentially applying the mapping. + + >>> v1 = np.arange(0, 2*nC, 1) + >>> u1 = M * v1 + >>> u1 + array([0.+4.j, 1.+5.j, 2.+6.j, 3.+7.j]) + + When applying the adjoint of the derivative operator to a set of + complex values, the operator will decompose these values into + their real and imaginary components. + + >>> v2 = np.arange(0, nC, 1) + 1j*np.arange(nC, 2*nC, 1) + >>> u2 = M.adjoint() * v2 + >>> u2 + array([0., 1., 2., 3., 4., 5., 6., 7.]) + + """ + nC = self.shape[0] + if v is not None: + return v[:nC] + v[nC:] * 1j + return sp.diags([1, 1j], [0, nC], [nC, 2 * nC]) + + +class SelfConsistentEffectiveMedium(IdentityMap): + r""" + Two phase self-consistent effective medium theory mapping for + ellipsoidal inclusions. The inversion model is the concentration + (volume fraction) of the phase 2 material. + + The inversion model is :math:`\varphi`. We solve for :math:`\sigma` + given :math:`\sigma_0`, :math:`\sigma_1` and :math:`\varphi` . Each of + the following are implicit expressions of the effective conductivity. + They are solved using a fixed point iteration. + + **Spherical Inclusions** + + If the shape of the inclusions are spheres, we use + + .. math:: + + \sum_{j=1}^N (\sigma^* - \sigma_j)R^{j} = 0 + + where :math:`j=[1,N]` is the each material phase, and N is the number + of phases. Currently, the implementation is only set up for 2 phase + materials, so we solve + + .. math:: + + (1-\\varphi)(\sigma - \sigma_0)R^{(0)} + \varphi(\sigma - \sigma_1)R^{(1)} = 0. + + Where :math:`R^{(j)}` is given by + + .. math:: + + R^{(j)} = \left[1 + \frac{1}{3}\frac{\sigma_j - \sigma}{\sigma} \right]^{-1}. + + **Ellipsoids** + + .. todo:: + + Aligned Ellipsoids have not yet been implemented, only randomly + oriented ellipsoids + + If the inclusions are aligned ellipsoids, we solve + + .. math:: + + \sum_{j=1}^N \varphi_j (\Sigma^* - \sigma_j\mathbf{I}) \mathbf{R}^{j, *} = 0 + + where + + .. math:: + + \mathbf{R}^{(j, *)} = \left[ \mathbf{I} + \mathbf{A}_j {\Sigma^{*}}^{-1}(\sigma_j \mathbf{I} - \Sigma^*) \\right]^{-1} + + and the depolarization tensor :math:`\mathbf{A}_j` is given by + + .. math:: + + \mathbf{A}^* = \left[\begin{array}{ccc} + Q & 0 & 0 \\ + 0 & Q & 0 \\ + 0 & 0 & 1-2Q + \end{array}\right] + + for a spheroid aligned along the z-axis. For an oblate spheroid + (:math:`\alpha < 1`, pancake-like) + + .. math:: + + Q = \frac{1}{2}\left( + 1 + \frac{1}{\alpha^2 - 1} \left[ + 1 - \frac{1}{\chi}\tan^{-1}(\chi) + \right] + \right) + + where + + .. math:: + + \chi = \sqrt{\frac{1}{\alpha^2} - 1} + + + For reference, see + `Torquato (2002), Random Heterogeneous Materials `_ + + + """ + + def __init__( + self, + mesh=None, + nP=None, + sigma0=None, + sigma1=None, + alpha0=1.0, + alpha1=1.0, + orientation0="z", + orientation1="z", + random=True, + rel_tol=1e-3, + maxIter=50, + **kwargs, + ): + self._sigstart = None + self.sigma0 = sigma0 + self.sigma1 = sigma1 + self.alpha0 = alpha0 + self.alpha1 = alpha1 + self.orientation0 = orientation0 + self.orientation1 = orientation1 + self.random = random + self.rel_tol = rel_tol + self.maxIter = maxIter + super(SelfConsistentEffectiveMedium, self).__init__(mesh, nP, **kwargs) + + @property + def sigma0(self): + """Physical property value for phase-0 material. + + Returns + ------- + float + """ + return self._sigma0 + + @sigma0.setter + def sigma0(self, value): + self._sigma0 = validate_float("sigma0", value, min_val=0.0) + + @property + def sigma1(self): + """Physical property value for phase-1 material. + + Returns + ------- + float + """ + return self._sigma1 + + @sigma1.setter + def sigma1(self, value): + self._sigma1 = validate_float("sigma1", value, min_val=0.0) + + @property + def alpha0(self): + """Aspect ratio of the phase-0 ellipsoids. + + Returns + ------- + float + """ + return self._alpha0 + + @alpha0.setter + def alpha0(self, value): + self._alpha0 = validate_float("alpha0", value, min_val=0.0) + + @property + def alpha1(self): + """Aspect ratio of the phase-1 ellipsoids. + + Returns + ------- + float + """ + return self._alpha1 + + @alpha1.setter + def alpha1(self, value): + self._alpha1 = validate_float("alpha1", value, min_val=0.0) + + @property + def orientation0(self): + """Orientation of the phase-0 inclusions. + + Returns + ------- + numpy.ndarray + """ + return self._orientation0 + + @orientation0.setter + def orientation0(self, value): + self._orientation0 = validate_direction("orientation0", value, dim=3) + + @property + def orientation1(self): + """Orientation of the phase-0 inclusions. + + Returns + ------- + numpy.ndarray + """ + return self._orientation1 + + @orientation1.setter + def orientation1(self, value): + self._orientation1 = validate_direction("orientation1", value, dim=3) + + @property + def random(self): + """Are the inclusions randomly oriented (True) or preferentially aligned (False)? + + Returns + ------- + bool + """ + return self._random + + @random.setter + def random(self, value): + self._random = validate_type("random", value, bool) + + @property + def rel_tol(self): + """relative tolerance for convergence for the fixed-point iteration. + + Returns + ------- + float + """ + return self._rel_tol + + @rel_tol.setter + def rel_tol(self, value): + self._rel_tol = validate_float( + "rel_tol", value, min_val=0.0, inclusive_min=False + ) + + @property + def maxIter(self): + """Maximum number of iterations for the fixed point iteration calculation. + + Returns + ------- + int + """ + return self._maxIter + + @maxIter.setter + def maxIter(self, value): + self._maxIter = validate_integer("maxIter", value, min_val=0) + + @property + def tol(self): + """ + absolute tolerance for the convergence of the fixed point iteration + calc + """ + if getattr(self, "_tol", None) is None: + self._tol = self.rel_tol * min(self.sigma0, self.sigma1) + return self._tol + + @property + def sigstart(self): + """ + first guess for sigma + """ + return self._sigstart + + @sigstart.setter + def sigstart(self, value): + if value is not None: + value = validate_float("sigstart", value) + self._sigstart = value + + def wiener_bounds(self, phi1): + """Define Wenner Conductivity Bounds + + See Torquato, 2002 + """ + phi0 = 1.0 - phi1 + sigWup = phi0 * self.sigma0 + phi1 * self.sigma1 + sigWlo = 1.0 / (phi0 / self.sigma0 + phi1 / self.sigma1) + W = np.array([sigWlo, sigWup]) + + return W + + def hashin_shtrikman_bounds(self, phi1): + """Hashin Shtrikman bounds + + See Torquato, 2002 + """ + # TODO: this should probably exsist on its own as a util + + phi0 = 1.0 - phi1 + sigWu = self.wiener_bounds(phi1)[1] + sig_tilde = phi0 * self.sigma1 + phi1 * self.sigma0 + + sigma_min = np.min([self.sigma0, self.sigma1]) + sigma_max = np.max([self.sigma0, self.sigma1]) + + sigHSlo = sigWu - ( + (phi0 * phi1 * (self.sigma0 - self.sigma1) ** 2) + / (sig_tilde + 2 * sigma_max) + ) + sigHSup = sigWu - ( + (phi0 * phi1 * (self.sigma0 - self.sigma1) ** 2) + / (sig_tilde + 2 * sigma_min) + ) + + return np.array([sigHSlo, sigHSup]) + + def hashin_shtrikman_bounds_anisotropic(self, phi1): + """Hashin Shtrikman bounds for anisotropic media + + See Torquato, 2002 + """ + phi0 = 1.0 - phi1 + sigWu = self.wiener_bounds(phi1)[1] + + sigma_min = np.min([self.sigma0, self.sigma1]) + sigma_max = np.max([self.sigma0, self.sigma1]) + + phi_min = phi0 if self.sigma1 > self.sigma0 else phi1 + phi_max = phi1 if self.sigma1 > self.sigma0 else phi0 + + amax = ( + -phi0 + * phi1 + * self.getA( + self.alpha1 if self.sigma1 > self.sigma0 else self.alpha0, + self.orientation1 if self.sigma1 > self.sigma0 else self.orientation0, + ) + ) + I = np.eye(3) + + sigHSlo = sigWu * I + ( + (sigma_min - sigma_max) ** 2 + * amax + * np.linalg.inv(sigma_min * I + (sigma_min - sigma_max) / phi_max * amax) + ) + sigHSup = sigWu * I + ( + (sigma_max - sigma_min) ** 2 + * amax + * np.linalg.inv(sigma_max * I + (sigma_max - sigma_min) / phi_min * amax) + ) + + return [sigHSlo, sigHSup] + + def getQ(self, alpha): + """Geometric factor in the depolarization tensor""" + if alpha < 1.0: # oblate spheroid + chi = np.sqrt((1.0 / alpha**2.0) - 1) + return ( + 1.0 / 2.0 * (1 + 1.0 / (alpha**2.0 - 1) * (1.0 - np.arctan(chi) / chi)) + ) + elif alpha > 1.0: # prolate spheroid + chi = np.sqrt(1 - (1.0 / alpha**2.0)) + return ( + 1.0 + / 2.0 + * ( + 1 + + 1.0 + / (alpha**2.0 - 1) + * (1.0 - 1.0 / (2.0 * chi) * np.log((1 + chi) / (1 - chi))) + ) + ) + elif alpha == 1: # sphere + return 1.0 / 3.0 + + def getA(self, alpha, orientation): + """Depolarization tensor""" + Q = self.getQ(alpha) + A = np.diag([Q, Q, 1 - 2 * Q]) + R = rotation_matrix_from_normals(np.r_[0.0, 0.0, 1.0], orientation) + return (R.T).dot(A).dot(R) + + def getR(self, sj, se, alpha, orientation=None): + """Electric field concentration tensor""" + if self.random is True: # isotropic + if alpha == 1.0: + return 3.0 * se / (2.0 * se + sj) + Q = self.getQ(alpha) + return ( + se + / 3.0 + * (2.0 / (se + Q * (sj - se)) + 1.0 / (sj - 2.0 * Q * (sj - se))) + ) + else: # anisotropic + if orientation is None: + raise Exception("orientation must be provided if random=False") + I = np.eye(3) + seinv = np.linalg.inv(se) + Rinv = I + self.getA(alpha, orientation) * seinv * (sj * I - se) + return np.linalg.inv(Rinv) + + def getdR(self, sj, se, alpha, orientation=None): + """ + Derivative of the electric field concentration tensor with respect + to the concentration of the second phase material. + """ + if self.random is True: + if alpha == 1.0: + return 3.0 / (2.0 * se + sj) - 6.0 * se / (2.0 * se + sj) ** 2 + Q = self.getQ(alpha) + return ( + 1 + / 3 + * ( + 2.0 / (se + Q * (sj - se)) + + 1.0 / (sj - 2.0 * Q * (sj - se)) + + se + * ( + -2 * (1 - Q) / (se + Q * (sj - se)) ** 2 + - 2 * Q / (sj - 2.0 * Q * (sj - se)) ** 2 + ) + ) + ) + else: + if orientation is None: + raise Exception("orientation must be provided if random=False") + raise NotImplementedError + + def _sc2phaseEMTSpheroidstransform(self, phi1): + """ + Self Consistent Effective Medium Theory Model Transform, + alpha = aspect ratio (c/a <= 1) + """ + + if not (np.all(0 <= phi1) and np.all(phi1 <= 1)): + warnings.warn("there are phis outside bounds of 0 and 1", stacklevel=2) + phi1 = np.median(np.c_[phi1 * 0, phi1, phi1 * 0 + 1.0]) + + phi0 = 1.0 - phi1 + + # starting guess + if self.sigstart is None: + sige1 = np.mean(self.wiener_bounds(phi1)) + else: + sige1 = self.sigstart + + if self.random is False: + sige1 = sige1 * np.eye(3) + + for _ in range(self.maxIter): + R0 = self.getR(self.sigma0, sige1, self.alpha0, self.orientation0) + R1 = self.getR(self.sigma1, sige1, self.alpha1, self.orientation1) + + den = phi0 * R0 + phi1 * R1 + num = phi0 * self.sigma0 * R0 + phi1 * self.sigma1 * R1 + + if self.random is True: + sige2 = num / den + relerr = np.abs(sige2 - sige1) + else: + sige2 = num * np.linalg.inv(den) + relerr = np.linalg.norm(np.abs(sige2 - sige1).flatten(), np.inf) + + if np.all(relerr <= self.tol): + if self.sigstart is None: + self._sigstart = ( + sige2 # store as a starting point for the next time around + ) + return sige2 + + sige1 = sige2 + # TODO: make this a proper warning, and output relevant info (sigma0, sigma1, phi, sigstart, and relerr) + warnings.warn("Maximum number of iterations reached", stacklevel=2) + + return sige2 + + def _sc2phaseEMTSpheroidsinversetransform(self, sige): + R0 = self.getR(self.sigma0, sige, self.alpha0, self.orientation0) + R1 = self.getR(self.sigma1, sige, self.alpha1, self.orientation1) + + num = -(self.sigma0 - sige) * R0 + den = (self.sigma1 - sige) * R1 - (self.sigma0 - sige) * R0 + + return num / den + + def _sc2phaseEMTSpheroidstransformDeriv(self, sige, phi1): + phi0 = 1.0 - phi1 + + R0 = self.getR(self.sigma0, sige, self.alpha0, self.orientation0) + R1 = self.getR(self.sigma1, sige, self.alpha1, self.orientation1) + + dR0 = self.getdR(self.sigma0, sige, self.alpha0, self.orientation0) + dR1 = self.getdR(self.sigma1, sige, self.alpha1, self.orientation1) + + num = (sige - self.sigma0) * R0 - (sige - self.sigma1) * R1 + den = phi0 * (R0 + (sige - self.sigma0) * dR0) + phi1 * ( + R1 + (sige - self.sigma1) * dR1 + ) + + return sdiag(num / den) + + def _transform(self, m): + return self._sc2phaseEMTSpheroidstransform(m) + + def deriv(self, m, v=None): + """ + Derivative of the effective conductivity with respect to the + volume fraction of phase 2 material + """ + sige = self._transform(m) + derivative = self._sc2phaseEMTSpheroidstransformDeriv(sige, m) + if v is not None: + return derivative @ v + return derivative + + def inverse(self, sige): + """ + Compute the concentration given the effective conductivity + """ + return self._sc2phaseEMTSpheroidsinversetransform(sige) + + @property + def is_linear(self): + return False diff --git a/simpeg/maps/_surjection.py b/simpeg/maps/_surjection.py new file mode 100644 index 0000000000..edcfa1f420 --- /dev/null +++ b/simpeg/maps/_surjection.py @@ -0,0 +1,568 @@ +""" +Surjection map classes. +""" + +import discretize +import numpy as np +import scipy.sparse as sp +from discretize import TensorMesh, CylindricalMesh +from discretize.utils import mkvc + +from ..utils import ( + validate_type, + validate_ndarray_with_shape, + validate_string, + validate_active_indices, +) +from ._base import IdentityMap + + +class SurjectFull(IdentityMap): + r"""Mapping a single property value to all mesh cells. + + Let :math:`m` be a model defined by a single physical property value + ``SurjectFull`` construct a surjective mapping that projects :math:`m` + to the set of voxel cells defining a mesh. The mapping + :math:`\mathbf{u(m)}` is a matrix of 1s of shape (*mesh.nC* , 1) that + projects the model to all mesh cells, i.e.: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + + Parameters + ---------- + mesh : discretize.BaseMesh + A discretize mesh + + """ + + def __init__(self, mesh, **kwargs): + super().__init__(mesh=mesh, **kwargs) + + @property + def nP(self): + r"""Number of parameters the mapping acts on; i.e. 1. + + Returns + ------- + int + Returns an integer value of 1 + """ + return 1 + + def _transform(self, m): + """ + :param m: model (scalar) + :rtype: numpy.ndarray + :return: transformed model + """ + return np.ones(self.mesh.nC) * m + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the input parameters. + + Let :math:`m` be the single parameter that the mapping acts on. The + ``SurjectFull`` class constructs a mapping that can be defined as + a projection matrix :math:`\mathbf{P}`; i.e.: + + .. math:: + \mathbf{u} = \mathbf{P m}, + + the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect + to the model parameters; i.e.: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} + + Note that in this case, **deriv** simply returns the original operator + :math:`\mathbf{P}`; a (*mesh.nC* , 1) numpy.ndarray of 1s. + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + """ + deriv = sp.csr_matrix(np.ones([self.mesh.nC, 1])) + if v is not None: + return deriv * v + return deriv + + +class SurjectVertical1D(IdentityMap): + r"""Map 1D layered Earth model to 2D or 3D tensor mesh. + + Let :math:`m` be a 1D model that defines the property values along + the last dimension of a tensor mesh; i.e. the y-direction for 2D + meshes and the z-direction for 3D meshes. ``SurjectVertical1D`` + construct a surjective mapping from the 1D model to all voxel cells + in the 2D or 3D tensor mesh provided. + + Mathematically, the mapping :math:`\mathbf{u}(\mathbf{m})` can be + represented by a projection matrix: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + + Parameters + ---------- + mesh : discretize.TensorMesh + A 2D or 3D tensor mesh + + Examples + -------- + Here we define a 1D layered Earth model comprised of 3 layers + on a 1D tensor mesh. We then use ``SurjectVertical1D`` to + construct a mapping which projects the 1D model onto a 2D + tensor mesh. + + >>> from simpeg.maps import SurjectVertical1D + >>> from simpeg.utils import plot_1d_layer_model + >>> from discretize import TensorMesh + >>> import numpy as np + >>> import matplotlib as mpl + >>> import matplotlib.pyplot as plt + + >>> dh = np.ones(20) + >>> mesh1D = TensorMesh([dh], 'C') + >>> mesh2D = TensorMesh([dh, dh], 'CC') + + >>> m = np.zeros(mesh1D.nC) + >>> m[mesh1D.cell_centers < 0] = 10. + >>> m[mesh1D.cell_centers < -5] = 5. + + >>> fig1 = plt.figure(figsize=(5,5)) + >>> ax1 = fig1.add_subplot(111) + >>> plot_1d_layer_model( + >>> mesh1D.h[0], np.flip(m), ax=ax1, z0=0, + >>> scale='linear', show_layers=True, plot_elevation=True + >>> ) + >>> ax1.set_xlim([-0.1, 11]) + >>> ax1.set_title('1D Model') + + >>> mapping = SurjectVertical1D(mesh2D) + >>> u = mapping * m + + >>> fig2 = plt.figure(figsize=(6, 5)) + >>> ax2a = fig2.add_axes([0.1, 0.15, 0.7, 0.8]) + >>> mesh2D.plot_image(u, ax=ax2a, grid=True) + >>> ax2a.set_title('Projected to 2D Mesh') + >>> ax2b = fig2.add_axes([0.83, 0.15, 0.05, 0.8]) + >>> norm = mpl.colors.Normalize(vmin=np.min(m), vmax=np.max(m)) + >>> cbar = mpl.colorbar.ColorbarBase(ax2b, norm=norm, orientation="vertical") + + """ + + def __init__(self, mesh, **kwargs): + assert isinstance( + mesh, (TensorMesh, CylindricalMesh) + ), "Only implemented for tensor meshes" + super().__init__(mesh=mesh, **kwargs) + + @property + def nP(self): + r"""Number of parameters the mapping acts on. + + Returns + ------- + int + Number of parameters the mapping acts on. Should equal the + number of cells along the last dimension of the tensor mesh + supplied when defining the mapping. + """ + return int(self.mesh.vnC[self.mesh.dim - 1]) + + def _transform(self, m): + repNum = np.prod(self.mesh.vnC[: self.mesh.dim - 1]) + return mkvc(m).repeat(repNum) + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the model paramters. + + Let :math:`\mathbf{m}` be a set of parameter values for the 1D model + and let :math:`\mathbf{P}` be a projection matrix that maps the 1D + model the 2D/3D tensor mesh. The forward mapping :math:`\mathbf{u}(\mathbf{m})` + is given by: + + .. math:: + \mathbf{u} = \mathbf{P m}, + + the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect + to the model parameters; i.e.: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} + + Note that in this case, **deriv** simply returns the projection matrix. + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + repNum = np.prod(self.mesh.vnC[: self.mesh.dim - 1]) + repVec = sp.csr_matrix( + (np.ones(repNum), (range(repNum), np.zeros(repNum))), shape=(repNum, 1) + ) + deriv = sp.kron(sp.identity(self.nP), repVec) + if v is not None: + return deriv * v + return deriv + + +class Surject2Dto3D(IdentityMap): + r"""Map 2D tensor model to 3D tensor mesh. + + Let :math:`m` define the parameters for a 2D tensor model. + ``Surject2Dto3D`` constructs a surjective mapping that projects + the 2D tensor model to a 3D tensor mesh. + + Mathematically, the mapping :math:`\mathbf{u}(\mathbf{m})` can be + represented by a projection matrix: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + + Parameters + ---------- + mesh : discretize.TensorMesh + A 3D tensor mesh + normal : {'y', 'x', 'z'} + Define the projection axis. + + Examples + -------- + Here we project a 3 layered Earth model defined on a 2D tensor mesh + to a 3D tensor mesh. We assume that at for some y-location, we + have a 2D tensor model which defines the physical property distribution + as a function of the *x* and *z* location. Using ``Surject2Dto3D``, + we project the model along the y-axis to obtain a 3D distribution + for the physical property (i.e. a 3D tensor model). + + >>> from simpeg.maps import Surject2Dto3D + >>> from discretize import TensorMesh + >>> import numpy as np + >>> import matplotlib as mpl + >>> import matplotlib.pyplot as plt + + >>> dh = np.ones(20) + >>> mesh2D = TensorMesh([dh, dh], 'CC') + >>> mesh3D = TensorMesh([dh, dh, dh], 'CCC') + + Here, we define the 2D tensor model. + + >>> m = np.zeros(mesh2D.nC) + >>> m[mesh2D.cell_centers[:, 1] < 0] = 10. + >>> m[mesh2D.cell_centers[:, 1] < -5] = 5. + + We then plot the 2D tensor model; which is defined along the + x and z axes. + + >>> fig1 = plt.figure(figsize=(6, 5)) + >>> ax11 = fig1.add_axes([0.1, 0.15, 0.7, 0.8]) + >>> mesh2D.plot_image(m, ax=ax11, grid=True) + >>> ax11.set_ylabel('z') + >>> ax11.set_title('2D Tensor Model') + >>> ax12 = fig1.add_axes([0.83, 0.15, 0.05, 0.8]) + >>> norm1 = mpl.colors.Normalize(vmin=np.min(m), vmax=np.max(m)) + >>> cbar1 = mpl.colorbar.ColorbarBase(ax12, norm=norm1, orientation="vertical") + + By setting *normal = 'Y'* we are projecting along the y-axis. + + >>> mapping = Surject2Dto3D(mesh3D, normal='Y') + >>> u = mapping * m + + Finally we plot a slice of the resulting 3D tensor model. + + >>> fig2 = plt.figure(figsize=(6, 5)) + >>> ax21 = fig2.add_axes([0.1, 0.15, 0.7, 0.8]) + >>> mesh3D.plot_slice(u, ax=ax21, ind=10, normal='Y', grid=True) + >>> ax21.set_ylabel('z') + >>> ax21.set_title('Projected to 3D Mesh (y=0)') + >>> ax22 = fig2.add_axes([0.83, 0.15, 0.05, 0.8]) + >>> norm2 = mpl.colors.Normalize(vmin=np.min(m), vmax=np.max(m)) + >>> cbar2 = mpl.colorbar.ColorbarBase(ax22, norm=norm2, orientation="vertical") + + """ + + def __init__(self, mesh, normal="y", **kwargs): + self.normal = normal + super().__init__(mesh=mesh, **kwargs) + + @IdentityMap.mesh.setter + def mesh(self, value): + value = validate_type("mesh", value, discretize.TensorMesh, cast=False) + if value.dim != 3: + raise ValueError("Surject2Dto3D Only works for a 3D Mesh") + self._mesh = value + + @property + def normal(self): + """The projection axis. + + Returns + ------- + str + """ + return self._normal + + @normal.setter + def normal(self, value): + self._normal = validate_string("normal", value, ("x", "y", "z")) + + @property + def nP(self): + """Number of model properties. + + The number of cells in the + last dimension of the mesh.""" + if self.normal == "z": + return self.mesh.shape_cells[0] * self.mesh.shape_cells[1] + elif self.normal == "y": + return self.mesh.shape_cells[0] * self.mesh.shape_cells[2] + elif self.normal == "x": + return self.mesh.shape_cells[1] * self.mesh.shape_cells[2] + + def _transform(self, m): + m = mkvc(m) + if self.normal == "z": + return mkvc( + m.reshape(self.mesh.vnC[:2], order="F")[:, :, np.newaxis].repeat( + self.mesh.shape_cells[2], axis=2 + ) + ) + elif self.normal == "y": + return mkvc( + m.reshape(self.mesh.vnC[::2], order="F")[:, np.newaxis, :].repeat( + self.mesh.shape_cells[1], axis=1 + ) + ) + elif self.normal == "x": + return mkvc( + m.reshape(self.mesh.vnC[1:], order="F")[np.newaxis, :, :].repeat( + self.mesh.shape_cells[0], axis=0 + ) + ) + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the model paramters. + + Let :math:`\mathbf{m}` be a set of parameter values for the 2D tensor model + and let :math:`\mathbf{P}` be a projection matrix that maps the 2D tensor model + to the 3D tensor mesh. The forward mapping :math:`\mathbf{u}(\mathbf{m})` + is given by: + + .. math:: + \mathbf{u} = \mathbf{P m}, + + the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect + to the model parameters; i.e.: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} + + Note that in this case, **deriv** simply returns the projection matrix. + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. If the + input argument *v* is not ``None``, the method returns the derivative times + the vector *v*. + """ + inds = self * np.arange(self.nP) + nC, nP = self.mesh.nC, self.nP + P = sp.csr_matrix((np.ones(nC), (range(nC), inds)), shape=(nC, nP)) + if v is not None: + return P * v + return P + + +class SurjectUnits(IdentityMap): + r"""Surjective mapping to all mesh cells. + + Let :math:`\mathbf{m}` be a model that contains a physical property value + for *nP* geological units. ``SurjectUnits`` is used to construct a surjective + mapping that projects :math:`\mathbf{m}` to the set of voxel cells defining a mesh. + As a result, the mapping :math:`\mathbf{u(\mathbf{m})}` is defined as + a projection matrix :math:`\mathbf{P}` acting on the model. Thus: + + .. math:: + \mathbf{u}(\mathbf{m}) = \mathbf{Pm} + + + The mapping therefore has dimensions (*mesh.nC*, *nP*). + + Parameters + ---------- + indices : (nP) list of (mesh.nC) numpy.ndarray + Each entry in the :class:`list` is a boolean :class:`numpy.ndarray` of length + *mesh.nC* that assigns the corresponding physical property value to the + appropriate mesh cells. + + Examples + -------- + For this example, we have a model that defines the property values + for two units. Using ``SurjectUnit``, we construct the mapping from + the model to a 1D mesh where the 1st unit's value is assigned to + all cells whose centers are located at *x < 0* and the 2nd unit's value + is assigned to all cells whose centers are located at *x > 0*. + + >>> from simpeg.maps import SurjectUnits + >>> from discretize import TensorMesh + >>> import numpy as np + + >>> nP = 8 + >>> mesh = TensorMesh([np.ones(nP)], 'C') + >>> unit_1_ind = mesh.cell_centers < 0 + + >>> indices_list = [unit_1_ind, ~unit_1_ind] + >>> mapping = SurjectUnits(indices_list, nP=nP) + + >>> m = np.r_[0.01, 0.05] + >>> mapping * m + array([0.01, 0.01, 0.01, 0.01, 0.05, 0.05, 0.05, 0.05]) + + """ + + def __init__(self, indices, **kwargs): + super().__init__(**kwargs) + self.indices = indices + + @property + def indices(self): + """List assigning a given physical property to specific model cells. + + Each entry in the :class:`list` is a boolean :class:`numpy.ndarray` of length + *mesh.nC* that assigns the corresponding physical property value to the + appropriate mesh cells. + + Returns + ------- + (nP) list of (mesh.n_cells) numpy.ndarray + """ + return self._indices + + @indices.setter + def indices(self, values): + values = validate_type("indices", values, list) + mesh = self.mesh + last_shape = None + for i in range(len(values)): + if mesh is not None: + values[i] = validate_active_indices( + "indices", values[i], self.mesh.n_cells + ) + else: + values[i] = validate_ndarray_with_shape( + "indices", values[i], shape=("*",), dtype=int + ) + if last_shape is not None and last_shape != values[i].shape: + raise ValueError("all indicies must have the same shape.") + last_shape = values[i].shape + self._indices = values + + @property + def P(self): + """ + Projection matrix from model parameters to mesh cells. + """ + if getattr(self, "_P", None) is None: + # sparse projection matrix + row = [] + col = [] + val = [] + for ii, ind in enumerate(self.indices): + col += [ii] * ind.sum() + row += np.where(ind)[0].tolist() + val += [1] * ind.sum() + + self._P = sp.csr_matrix( + (val, (row, col)), shape=(len(self.indices[0]), self.nP) + ) + + # self._P = sp.block_diag([P for ii in range(self.nBlock)]) + + return self._P + + def _transform(self, m): + return self.P * m + + @property + def nP(self): + r"""Number of parameters the mapping acts on. + + Returns + ------- + int + Number of parameters that the mapping acts on. + """ + return len(self.indices) + + @property + def shape(self): + """Dimensions of the mapping + + Returns + ------- + tuple + Dimensions of the mapping. Where *nP* is the number of parameters the + mapping acts on and *mesh.nC* is the number of cells the corresponding + mesh, the return is a tuple of the form (*mesh.nC*, *nP*). + """ + # return self.n_block*len(self.indices[0]), self.n_block*len(self.indices) + return (len(self.indices[0]), self.nP) + + def deriv(self, m, v=None): + r"""Derivative of the mapping with respect to the input parameters. + + Let :math:`\mathbf{m}` be a set of model parameters. The surjective mapping + can be defined as a sparse projection matrix :math:`\mathbf{P}`. Therefore + we can define the surjective mapping acting on the model parameters as: + + .. math:: + \mathbf{u} = \mathbf{P m}, + + the **deriv** method returns the derivative of :math:`\mathbf{u}` with respect + to the model parameters; i.e.: + + .. math:: + \frac{\partial \mathbf{u}}{\partial \mathbf{m}} = \mathbf{P} + + Note that in this case, **deriv** simply returns a sparse projection matrix. + + Parameters + ---------- + m : (nP) numpy.ndarray + A vector representing a set of model parameters + v : (nP) numpy.ndarray + If not ``None``, the method returns the derivative times the vector *v* + + Returns + ------- + scipy.sparse.csr_matrix + Derivative of the mapping with respect to the model parameters. + If the input argument *v* is not ``None``, the method returns + the derivative times the vector *v*. + """ + + if v is not None: + return self.P * v + return self.P diff --git a/simpeg/meta/dask_sim.py b/simpeg/meta/dask_sim.py index bddf091920..701dffe323 100644 --- a/simpeg/meta/dask_sim.py +++ b/simpeg/meta/dask_sim.py @@ -13,6 +13,18 @@ from operator import add import warnings +from dask.base import normalize_token + + +@normalize_token.register(IdentityMap) +def _normalize_map(mapping): + return mapping._uuid.hex + + +@normalize_token.register(BaseSimulation) +def _normalize_simulation(sim): + return sim._uuid.hex + def _store_model(mapping, sim, model): sim.model = mapping * model @@ -59,13 +71,17 @@ def _get_jtj_diag(mapping, sim, model, field, w, apply_map=False): return np.asarray((sim_jtj @ m_deriv).power(2).sum(axis=0)).flatten() -def _reduce(client, operation, items): +def _reduce(client, operation, items, workers): + # first sort by workers so items on the same workers are mapped together. + items = [val for (_, val) in sorted(zip(workers, items), key=lambda x: x[0])] while len(items) > 1: - new_reduce = client.map(operation, items[::2], items[1::2]) + new_reduce = client.map(operation, items[::2], items[1::2], pure=False) if len(items) % 2 == 1: - new_reduce[-1] = client.submit(operation, new_reduce[-1], items[-1]) + new_reduce[-1] = client.submit( + operation, new_reduce[-1], items[-1], pure=False + ) items = new_reduce - return client.gather(items[0]) + return items[0].result() def _validate_type_or_future_of_type( @@ -84,9 +100,11 @@ def _validate_type_or_future_of_type( if workers is None: objects = client.scatter(objects) else: + # If workers are already set, move the object to the respective worker. tmp = [] for obj, worker in zip(objects, workers): - tmp.append(client.scatter([obj], workers=worker)[0]) + future = client.scatter(obj, workers=worker) + tmp.append(future) objects = tmp except TypeError: pass @@ -99,14 +117,43 @@ def _validate_type_or_future_of_type( # Figure out where everything lives who = client.who_has(objects) if workers is None: - workers = [] + # Because we only ever want to allow execution on a single consistent + # worker for each simulation-mapping pair, we need to do a bit of sanity + # checking to choose which worker if the object exists on multiple + # workers. + + # find out how objects have been assigned to each worker. + workers_assign_count = {} for obj in objects: - workers.append(who[obj.key]) + workers = who[obj.key] + for worker in workers: + workers_assign_count[worker] = workers_assign_count.get(worker, 0) + 1 + + # then loop through and if they exist on multiple workers, + # choose the worker with the fewest assignments. + # then decrement any other workers + worker_assignments = [] + for obj in objects: + workers = who[obj.key] + n_assigned = len(objects) + assigned = None + for worker in workers: + n_test = workers_assign_count[worker] + # choose the worker with the least assigned tasks: + if n_test < n_assigned: + assigned = worker + n_assigned = n_test + # discount workers who had this object but were not chosen: + for worker in workers: + if worker != assigned: + workers_assign_count[worker] -= 1 + worker_assignments.append(assigned) + workers = worker_assignments else: # Issue a warning if the future is not on the expected worker for i, (obj, worker) in enumerate(zip(objects, workers)): - obj_owner = client.who_has(obj)[obj.key] - if obj_owner != worker: + obj_owners = client.who_has(obj)[obj.key] + if worker not in obj_owners: warnings.warn( f"{property_name} {i} is not on the expected worker.", stacklevel=2 ) @@ -115,7 +162,9 @@ def _validate_type_or_future_of_type( futures = [] for obj, worker in zip(objects, workers): futures.append( - client.submit(lambda v: not isinstance(v, obj_type), obj, workers=worker) + client.submit( + lambda v: not isinstance(v, obj_type), obj, workers=worker, pure=False + ) ) is_not_obj = np.array(client.gather(futures)) if np.any(is_not_obj): @@ -159,7 +208,9 @@ def _make_survey(self): vnD = [] client = self.client for sim, worker in zip(self.simulations, self._workers): - vnD.append(client.submit(lambda s: s.survey.nD, sim, workers=worker)) + vnD.append( + client.submit(lambda s: s.survey.nD, sim, workers=worker, pure=False) + ) vnD = client.gather(vnD) survey._vnD = vnD return survey @@ -214,7 +265,9 @@ def mappings(self, value): ) # validate mapping shapes and simulation shapes - model_len = client.submit(lambda v: v.shape[1], mappings[0]).result() + model_len = client.submit( + lambda v: v.shape[1], mappings[0], pure=False + ).result() def check_mapping(mapping, sim, model_len): if mapping.shape[1] != model_len: @@ -236,10 +289,12 @@ def check_mapping(mapping, sim, model_len): error_checks = [] for mapping, sim, worker in zip(mappings, self.simulations, workers): - # if it was a repeat sim, this should cause the simulation to be transfered - # to each worker. + # if it was a repeat sim, this should cause the simulation to be transferred + # to each worker if it was originally passed as a future. error_checks.append( - client.submit(check_mapping, mapping, sim, model_len, workers=worker) + client.submit( + check_mapping, mapping, sim, model_len, workers=worker, pure=False + ) ) error_checks = np.asarray(client.gather(error_checks)) @@ -247,7 +302,7 @@ def check_mapping(mapping, sim, model_len): raise ValueError("All mappings must have the same input length") if np.any(error_checks == 2): raise ValueError( - f"Simulations and mappings at indices {np.where(error_checks==2)}" + f"Simulations and mappings at indices {np.where(error_checks == 2)}" f" are inconsistent." ) @@ -266,6 +321,7 @@ def _model_map(self): lambda v: v.shape[1], self.mappings[0], workers=self._workers[0], + pure=False, ) n_m = client.gather(n_m) self.__model_map = IdentityMap(nP=n_m) @@ -291,7 +347,7 @@ def model(self, value): # Only send the model to the internal simulations if it was updated. if updated: client = self.client - [self._m_as_future] = client.scatter([self._model], broadcast=True) + self._m_as_future = client.scatter(self._model, broadcast=True, hash=False) if not self._repeat_sim: futures = [] for mapping, sim, worker in zip( @@ -304,6 +360,7 @@ def model(self, value): sim, self._m_as_future, workers=worker, + pure=False, ) ) self.client.gather( @@ -325,6 +382,7 @@ def fields(self, m): m_future, self._repeat_sim, workers=worker, + pure=False, ) ) return f @@ -349,6 +407,7 @@ def dpred(self, m=None, f=None): field, self._repeat_sim, workers=worker, + pure=False, ) ) return np.concatenate(client.gather(dpred)) @@ -359,7 +418,7 @@ def Jvec(self, m, v, f=None): if f is None: f = self.fields(m) client = self.client - [v_future] = client.scatter([v], broadcast=True) + v_future = client.scatter(v, broadcast=True, hash=False) j_vec = [] for mapping, sim, worker, field in zip( self.mappings, self.simulations, self._workers, f @@ -374,6 +433,7 @@ def Jvec(self, m, v, f=None): v_future, self._repeat_sim, workers=worker, + pure=False, ) ) return np.concatenate(self.client.gather(j_vec)) @@ -398,11 +458,12 @@ def Jtvec(self, m, v, f=None): v[self._data_offsets[i] : self._data_offsets[i + 1]], self._repeat_sim, workers=worker, + pure=False, ) ) # Do the sum by a reduction operation to avoid gathering a vector # of size n_simulations by n_model parameters on the head. - return _reduce(client, add, jt_vec) + return _reduce(client, add, jt_vec, workers=self._workers) def getJtJdiag(self, m, W=None, f=None): self.model = m @@ -430,9 +491,10 @@ def getJtJdiag(self, m, W=None, f=None): sim_w, self._repeat_sim, workers=worker, + pure=False, ) ) - self._jtjdiag = _reduce(client, add, jtj_diag) + self._jtjdiag = _reduce(client, add, jtj_diag, workers=self._workers) return self._jtjdiag @@ -461,7 +523,9 @@ def __init__(self, simulations, mappings, client): def _make_survey(self): survey = BaseSurvey([]) client = self.client - n_d = client.submit(lambda s: s.survey.nD, self.simulations[0]).result() + n_d = client.submit( + lambda s: s.survey.nD, self.simulations[0], pure=False + ).result() survey._vnD = [ n_d, ] @@ -473,11 +537,15 @@ def simulations(self, value): simulations, workers = _validate_type_or_future_of_type( "simulations", value, BaseSimulation, client, return_workers=True ) - n_d = client.submit(lambda s: s.survey.nD, simulations[0], workers=workers[0]) + n_d = client.submit( + lambda s: s.survey.nD, simulations[0], workers=workers[0], pure=False + ) sim_check = [] for sim, worker in zip(simulations, workers): sim_check.append( - client.submit(lambda s, n: s.survey.nD != n, sim, n_d, workers=worker) + client.submit( + lambda s, n: s.survey.nD != n, sim, n_d, workers=worker, pure=False + ) ) if np.any(client.gather(sim_check)): raise ValueError("All simulations must have the same number of data.") @@ -493,16 +561,18 @@ def dpred(self, m=None, f=None): dpred = [] for sim, worker, field in zip(self.simulations, self._workers, f): dpred.append( - client.submit(_calc_dpred, None, sim, None, field, workers=worker) + client.submit( + _calc_dpred, None, sim, None, field, workers=worker, pure=False + ) ) - return _reduce(client, add, dpred) + return _reduce(client, add, dpred, workers=self._workers) def Jvec(self, m, v, f=None): self.model = m if f is None: f = self.fields(m) client = self.client - [v_future] = client.scatter([v], broadcast=True) + v_future = client.scatter(v, broadcast=True, hash=False) j_vec = [] for mapping, sim, worker, field in zip( self.mappings, self._simulations, self._workers, f @@ -516,9 +586,10 @@ def Jvec(self, m, v, f=None): field, v_future, workers=worker, + pure=False, ) ) - return _reduce(client, add, j_vec) + return _reduce(client, add, j_vec, workers=self._workers) def Jtvec(self, m, v, f=None): self.model = m @@ -538,11 +609,12 @@ def Jtvec(self, m, v, f=None): field, v, workers=worker, + pure=False, ) ) # Do the sum by a reduction operation to avoid gathering a vector # of size n_simulations by n_model parameters on the head. - return _reduce(client, add, jt_vec) + return _reduce(client, add, jt_vec, workers=self._workers) def getJtJdiag(self, m, W=None, f=None): self.model = m @@ -567,9 +639,10 @@ def getJtJdiag(self, m, W=None, f=None): field, W, workers=worker, + pure=False, ) ) - self._jtjdiag = _reduce(client, add, jtj_diag) + self._jtjdiag = _reduce(client, add, jtj_diag, workers=self._workers) return self._jtjdiag @@ -607,7 +680,9 @@ def __init__(self, simulation, mappings, client): def _make_survey(self): survey = BaseSurvey([]) - nD = self.client.submit(lambda s: s.survey.nD, self.simulation).result() + nD = self.client.submit( + lambda s: s.survey.nD, self.simulation, pure=False + ).result() survey._vnD = len(self.mappings) * [nD] return survey @@ -630,12 +705,12 @@ def simulation(self, value): client = self.client if isinstance(value, BaseSimulation): # Scatter sim to every client - [ - value, - ] = client.scatter([value], broadcast=True) + value = client.scatter(value, broadcast=True) if not ( isinstance(value, Future) - and client.submit(lambda s: isinstance(s, BaseSimulation), value).result() + and client.submit( + lambda s: isinstance(s, BaseSimulation), value, pure=False + ).result() ): raise TypeError( "simulation must be an instance of BaseSimulation or a Future that returns" diff --git a/simpeg/meta/multiprocessing.py b/simpeg/meta/multiprocessing.py index 23e025688b..05edf03469 100644 --- a/simpeg/meta/multiprocessing.py +++ b/simpeg/meta/multiprocessing.py @@ -8,10 +8,9 @@ class SimpleFuture: """Represents an object stored on a seperate simulation process.""" - def __init__(self, item_id, t_queue, r_queue): + def __init__(self, item_id, sim_process): self.item_id = item_id - self.t_queue = t_queue - self.r_queue = r_queue + self.sim_process = sim_process # This doesn't quite work well yet, # Due to the fact that some fields objects from the PDE @@ -25,8 +24,8 @@ def __init__(self, item_id, t_queue, r_queue): # return item def __del__(self): - # Tell the child process that this object is no longer needed in its cache. - self.t_queue.put(("del_item", (self.item_id,))) + if self.sim_process.is_alive(): + self.sim_process.task_queue.put(("del_item", (self.item_id,))) class _SimulationProcess(Process): @@ -119,7 +118,7 @@ def set_sim(self, sim): self._check_closed() self.task_queue.put(("set_sim", (sim,))) key = self.result_queue.get() - future = SimpleFuture(key, self.task_queue, self.result_queue) + future = SimpleFuture(key, self) self._my_sim = future return future @@ -133,7 +132,7 @@ def get_fields(self): sim = self._my_sim self.task_queue.put((1, (sim.item_id,))) key = self.result_queue.get() - future = SimpleFuture(key, self.task_queue, self.result_queue) + future = SimpleFuture(key, self) return future def start_dpred(self, f_future): @@ -169,6 +168,15 @@ def result(self): self._check_closed() return self.result_queue.get() + def join(self, timeout=None): + self._check_closed() + self.task_queue.put(None) + self.task_queue.close() + self.result_queue.close() + self.task_queue.join_thread() + self.result_queue.join_thread() + super().join(timeout=timeout) + class MultiprocessingMetaSimulation(MetaSimulation): """Multiprocessing version of simulation of simulations. @@ -193,11 +201,11 @@ class MultiprocessingMetaSimulation(MetaSimulation): ... sim = MultiprocessingMetaSimulation(...) ... sim.dpred(model) - You must also be sure to call sim.close() before discarding + You must also be sure to call `sim.join()` before discarding this worker to kill the subprocesses that are created, as you would with - any other multiprocessing queue. + any other multiprocessing process. - >>> sim.close() + >>> sim.join() Parameters ---------- @@ -213,12 +221,6 @@ class MultiprocessingMetaSimulation(MetaSimulation): to `multiprocessing.cpu_count()`. The number of processes spawned will be the minimum of this number and the number of simulations. - Notes - ----- - On Unix systems with python version 3.8 the default `fork` method of starting the - processes has lead to program stalls in certain cases. If you encounter this - try setting the start method to `spawn'. - >>> import multiprocessing as mp >>> mp.set_start_method("spawn") """ @@ -344,7 +346,6 @@ def getJtJdiag(self, m, W=None, f=None): def join(self, timeout=None): for p in self._sim_processes: if p.is_alive(): - p.task_queue.put(None) p.join(timeout=timeout) diff --git a/simpeg/meta/simulation.py b/simpeg/meta/simulation.py index ae9846475a..f2ebc30fdb 100644 --- a/simpeg/meta/simulation.py +++ b/simpeg/meta/simulation.py @@ -325,8 +325,8 @@ def getJtJdiag(self, m, W=None, f=None): return self._jtjdiag @property - def deleteTheseOnModelUpdate(self): - return super().deleteTheseOnModelUpdate + ["_jtjdiag"] + def _delete_on_model_update(self): + return super()._delete_on_model_update + ["_jtjdiag"] class SumMetaSimulation(MetaSimulation): diff --git a/simpeg/objective_function.py b/simpeg/objective_function.py index 1e864d1b6e..9188168b79 100644 --- a/simpeg/objective_function.py +++ b/simpeg/objective_function.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import numbers import numpy as np import scipy.sparse as sp @@ -9,6 +7,7 @@ from .maps import IdentityMap from .props import BaseSimPEG from .utils import timeIt, Zero, Identity +from .typing import RandomSeed __all__ = ["BaseObjectiveFunction", "ComboObjectiveFunction", "L2ObjectiveFunction"] @@ -194,27 +193,43 @@ def deriv2(self, m, v=None, **kwargs): ) ) - def _test_deriv(self, x=None, num=4, plotIt=False, **kwargs): + def _test_deriv( + self, + x=None, + num=4, + plotIt=False, + random_seed: RandomSeed | None = None, + **kwargs, + ): print("Testing {0!s} Deriv".format(self.__class__.__name__)) + rng = np.random.default_rng(seed=random_seed) if x is None: - if self.nP == "*": - x = np.random.randn(np.random.randint(1e2, high=1e3)) - else: - x = np.random.randn(self.nP) - + n_params = rng.integers(low=100, high=1_000) if self.nP == "*" else self.nP + x = rng.standard_normal(size=n_params) return check_derivative( - lambda m: [self(m), self.deriv(m)], x, num=num, plotIt=plotIt, **kwargs + lambda m: [self(m), self.deriv(m)], + x, + num=num, + plotIt=plotIt, + random_seed=rng, + **kwargs, ) - def _test_deriv2(self, x=None, num=4, plotIt=False, **kwargs): + def _test_deriv2( + self, + x=None, + num=4, + plotIt=False, + random_seed: RandomSeed | None = None, + **kwargs, + ): print("Testing {0!s} Deriv2".format(self.__class__.__name__)) + rng = np.random.default_rng(seed=random_seed) if x is None: - if self.nP == "*": - x = np.random.randn(np.random.randint(1e2, high=1e3)) - else: - x = np.random.randn(self.nP) + n_params = rng.integers(low=100, high=1_000) if self.nP == "*" else self.nP + x = rng.standard_normal(size=n_params) - v = x + 0.1 * np.random.rand(len(x)) + v = x + 0.1 * rng.uniform(size=len(x)) expectedOrder = kwargs.pop("expectedOrder", 1) return check_derivative( lambda m: [self.deriv(m).dot(v), self.deriv2(m, v=v)], @@ -222,10 +237,11 @@ def _test_deriv2(self, x=None, num=4, plotIt=False, **kwargs): num=num, expectedOrder=expectedOrder, plotIt=plotIt, + random_seed=rng, **kwargs, ) - def test(self, x=None, num=4, **kwargs): + def test(self, x=None, num=4, random_seed: RandomSeed | None = None, **kwargs): """Run a convergence test on both the first and second derivatives. They should be second order! @@ -236,6 +252,11 @@ def test(self, x=None, num=4, **kwargs): The evaluation point for the Taylor expansion. num : int The number of iterations in the convergence test. + random_seed : :class:`~simpeg.typing.RandomSeed` or None, optional + Random seed used for generating a random array for ``x`` if it's + None, and the ``v`` array for testing the second derivatives. It + can either be an int, a predefined Numpy random number generator, + or any valid input to ``numpy.random.default_rng``. Returns ------- @@ -243,8 +264,10 @@ def test(self, x=None, num=4, **kwargs): ``True`` if both tests pass. ``False`` if either test fails. """ - deriv = self._test_deriv(x=x, num=num, **kwargs) - deriv2 = self._test_deriv2(x=x, num=num, plotIt=False, **kwargs) + deriv = self._test_deriv(x=x, num=num, random_seed=random_seed, **kwargs) + deriv2 = self._test_deriv2( + x=x, num=num, plotIt=False, random_seed=random_seed, **kwargs + ) return deriv & deriv2 __numpy_ufunc__ = True @@ -393,6 +416,7 @@ def __init__( self.objfcts = objfcts self._multipliers = multipliers self._unpack_on_add = unpack_on_add + self._last_obj_vals = [np.nan] * len(objfcts) def __len__(self): return len(self.multipliers) @@ -431,6 +455,7 @@ def multipliers(self, value): def __call__(self, m, f=None): """Evaluate the objective functions for a given model.""" fct = 0.0 + obj_vals = [] for i, phi in enumerate(self): multiplier, objfct = phi if multiplier == 0.0: # don't evaluate the fct @@ -440,6 +465,8 @@ def __call__(self, m, f=None): else: objective_func_value = objfct(m) fct += multiplier * objective_func_value + obj_vals.append(objective_func_value) + self._last_obj_vals = obj_vals return fct def deriv(self, m, f=None): diff --git a/simpeg/optimization.py b/simpeg/optimization.py index edc68b1bd5..76c328e3cd 100644 --- a/simpeg/optimization.py +++ b/simpeg/optimization.py @@ -1,7 +1,89 @@ +""" +======================================================== +SimPEG Optimizers (:mod:`simpeg.optimization`) +======================================================== +.. currentmodule:: simpeg.optimization + +Optimizers +========== + +These optimizers are available within SimPEG for use during inversion. + +Unbound Optimizers +------------------ + +These optimizers all work on unbound minimization functions. + +.. autosummary:: + :toctree: generated/ + + SteepestDescent + BFGS + GaussNewton + InexactGaussNewton + +Box Bounded Optimizers +---------------------- +These optimizers support box bound constraints on the model parameters + +.. autosummary:: + :toctree: generated/ + + ProjectedGradient + ProjectedGNCG + +Root Finding +------------ +.. autosummary:: + :toctree: generated/ + + NewtonRoot + +Minimization Base Classes +=========================== + +These classes are usually inherited or used by the optimization algorithms +above to control their execution. + +Base Minimizer +-------------- +.. autosummary:: + :toctree: generated/ + + Minimize + + +Minimizer Mixins +---------------- +.. autosummary:: + :toctree: generated/ + + Remember + Bounded + InexactCG + +Iteration Printers and Stoppers +------------------------------- +.. autosummary:: + :toctree: generated/ + + IterationPrinters + StoppingCriteria + +""" + +import warnings +from collections.abc import Callable +from typing import Any, Optional + import numpy as np +import numpy.typing as npt import scipy.sparse as sp +from discretize.utils import Identity + +from pymatsolver import Solver, SolverCG -from .utils.solver_utils import SolverWrapI, Solver, SolverDiag +from .typing import MinimizeCallable from .utils import ( call_hooks, check_stoppers, @@ -12,6 +94,11 @@ print_line, print_stoppers, print_done, + validate_float, + validate_integer, + validate_type, + validate_ndarray_with_shape, + deprecate_property, ) norm = np.linalg.norm @@ -25,13 +112,12 @@ "GaussNewton", "InexactGaussNewton", "ProjectedGradient", + "ProjectedGNCG", "NewtonRoot", "StoppingCriteria", "IterationPrinters", ] -SolverICG = SolverWrapI(sp.linalg.cg, checkAccuracy=False) - class StoppingCriteria(object): """docstring for StoppingCriteria""" @@ -124,112 +210,153 @@ class StoppingCriteria(object): class IterationPrinters(object): """docstring for IterationPrinters""" - iteration = {"title": "#", "value": lambda M: M.iter, "width": 5, "format": "%3d"} - f = {"title": "f", "value": lambda M: M.f, "width": 10, "format": "%1.2e"} + iteration = { + "title": "#", + "value": lambda M: M.iter, + "width": 5, + "format": lambda v: f"{v:3d}", + } + f = { + "title": "f", + "value": lambda M: M.f, + "width": 10, + "format": lambda v: f"{v:1.2e}", + } norm_g = { "title": "|proj(x-g)-x|", - "value": lambda M: norm(M.projection(M.xc - M.g) - M.xc), + "value": lambda M: ( + None if M.iter == 0 else norm(M.projection(M.xc - M.g) - M.xc) + ), "width": 15, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", + } + totalLS = { + "title": "LS", + "value": lambda M: None if M.iter == 0 else M.iterLS, + "width": 5, + "format": lambda v: f"{v:d}", } - totalLS = {"title": "LS", "value": lambda M: M.iterLS, "width": 5, "format": "%d"} iterationLS = { "title": "#", "value": lambda M: (M.iter, M.iterLS), "width": 5, - "format": "%3d.%d", + "format": lambda v: f"{v[0]:3d}.{v[1]:d}", + } + LS_ft = { + "title": "ft", + "value": lambda M: M._LS_ft, + "width": 10, + "format": lambda v: f"{v:1.2e}", + } + LS_t = { + "title": "t", + "value": lambda M: M._LS_t, + "width": 10, + "format": lambda v: f"{v:0.5f}", } - LS_ft = {"title": "ft", "value": lambda M: M._LS_ft, "width": 10, "format": "%1.2e"} - LS_t = {"title": "t", "value": lambda M: M._LS_t, "width": 10, "format": "%0.5f"} LS_armijoGoldstein = { "title": "f + alp*g.T*p", "value": lambda M: M.f + M.LSreduction * M._LS_descent, "width": 16, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", } LS_WolfeCurvature = { "title": "alp*g.T*p", "str": "%d : ft = %1.4e >= alp*descent = %1.4e", "value": lambda M: M.LScurvature * M._LS_descent, "width": 16, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", } itType = { "title": "itType", "value": lambda M: M._itType, "width": 8, - "format": "%s", + "format": lambda v: f"{v:s}", } aSet = { "title": "aSet", - "value": lambda M: np.sum(M.activeSet(M.xc)), + "value": lambda M: None if M.iter == 0 else np.sum(M.activeSet(M.xc)), "width": 8, - "format": "%d", + "format": lambda v: f"{v:d}", } bSet = { "title": "bSet", - "value": lambda M: np.sum(M.bindingSet(M.xc)), + "value": lambda M: None if M.iter == 0 else np.sum(M.bindingSet(M.xc)), "width": 8, - "format": "%d", + "format": lambda v: f"{v:d}", } comment = { "title": "Comment", "value": lambda M: M.comment, "width": 12, - "format": "%s", + "format": lambda v: f"{v:s}", } beta = { "title": "beta", "value": lambda M: M.parent.beta, "width": 10, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", } phi_d = { "title": "phi_d", - "value": lambda M: M.parent.phi_d * M.parent.opt.factor, + "value": lambda M: M.parent.phi_d, "width": 10, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", } phi_m = { "title": "phi_m", - "value": lambda M: M.parent.phi_m * M.parent.opt.factor, + "value": lambda M: M.parent.phi_m, "width": 10, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", } phi_s = { "title": "phi_s", - "value": lambda M: M.parent.phi_s * M.parent.opt.factor, + "value": lambda M: M.parent.phi_s, "width": 10, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", } phi_x = { "title": "phi_x", - "value": lambda M: M.parent.phi_x * M.parent.opt.factor, + "value": lambda M: M.parent.phi_x, "width": 10, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", } phi_y = { "title": "phi_y", - "value": lambda M: M.parent.phi_y * M.parent.opt.factor, + "value": lambda M: M.parent.phi_y, "width": 10, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", } phi_z = { "title": "phi_z", - "value": lambda M: M.parent.phi_z * M.parent.opt.factor, + "value": lambda M: M.parent.phi_z, "width": 10, - "format": "%1.2e", + "format": lambda v: f"{v:1.2e}", } iterationCG = { - "title": "iterCG", - "value": lambda M: M.cg_count, + "title": "iter_CG", + "value": lambda M: getattr(M, "cg_count", None), "width": 10, - "format": "%3d", + "format": lambda v: f"{v:d}", + } + + iteration_CG_rel_residual = { + "title": "CG |Ax-b|/|b|", + "value": lambda M: getattr(M, "cg_rel_resid", None), + "width": 15, + "format": lambda v: f"{v:1.2e}", + } + + iteration_CG_abs_residual = { + "title": "CG |Ax-b|", + "value": lambda M: getattr(M, "cg_abs_resid", None), + "width": 11, + "format": lambda v: f"{v:1.2e}", } @@ -252,6 +379,7 @@ class Minimize(object): tolX = 1e-1 #: Tolerance on norm(x) movement tolG = 1e-1 #: Tolerance on gradient norm eps = 1e-5 #: Small value + require_decrease = True #: Require decrease in the objective function. If False, we will still take a step when the linesearch fails stopNextIteration = False #: Stops the optimization program nicely. use_WolfeCurvature = False #: add the Wolfe Curvature criteria for line search @@ -266,7 +394,6 @@ class Minimize(object): parent = None #: This is the parent of the optimization routine. print_type = None - factor = 1.0 def __init__(self, **kwargs): set_kwargs(self, **kwargs) @@ -287,7 +414,6 @@ def __init__(self, **kwargs): ] if self.print_type == "ubc": - self.factor = 2.0 self.stoppers = [StoppingCriteria.iteration] self.printers = [ IterationPrinters.iteration, @@ -313,74 +439,71 @@ def __init__(self, **kwargs): ] @property - def callback(self): + def callback(self) -> Optional[Callable[[np.ndarray], Any]]: + """A used defined callback function. + + Returns + ------- + None or Callable[[np.ndarray], Any] + The optional user supplied callback function accepting the current iteration + value as an input. + """ return getattr(self, "_callback", None) @callback.setter - def callback(self, value): + def callback(self, value: Callable[[np.ndarray], Any]): if self.callback is not None: print( - "The callback on the {0!s} Optimization was " - "replaced.".format(self.__class__.__name__) + f"The callback on the {self.__class__.__name__} minimizer was replaced." ) self._callback = value @timeIt - def minimize(self, evalFunction, x0): + def minimize(self, evalFunction: MinimizeCallable, x0: np.ndarray) -> np.ndarray: """minimize(evalFunction, x0) Minimizes the function (evalFunction) starting at the location x0. - :param callable evalFunction: function handle that evaluates: f, g, H = F(x) - :param numpy.ndarray x0: starting location - :rtype: numpy.ndarray - :return: x, the last iterate of the optimization algorithm - - evalFunction is a function handle:: - - (f[, g][, H]) = evalFunction(x, return_g=False, return_H=False ) - - def evalFunction(x, return_g=False, return_H=False): - out = (f,) - if return_g: - out += (g,) - if return_H: - out += (H,) - return out if len(out) > 1 else out[0] - - - The algorithm for general minimization is as follows:: - - startup(x0) - printInit() - - while True: - doStartIteration() - f, g, H = evalFunction(xc) - printIter() - if stoppingCriteria(): break - p = findSearchDirection() - p = scaleSearchDirection(p) - xt, passLS = modifySearchDirection(p) - if not passLS: - xt, caught = modifySearchDirectionBreak(p) - if not caught: return xc - doEndIteration(xt) - - print_done() - finish() - return xc + Parameters + ---------- + evalFunction : callable + The objective function to be minimized:: + + evalFunction( + x: numpy.ndarray, + return_g: bool, + return_H: bool + ) -> ( + float + | tuple[float, numpy.ndarray] + | tuple[float, LinearOperator] + | tuple[float, numpy.ndarray, LinearOperator] + ) + + That will optionally return the gradient as a ``numpy.ndarray`` and the Hessian as any class + that supports matrix vector multiplication using the `*` operator. + + x0 : numpy.ndarray + Initial guess. + + Returns + ------- + x_min : numpy.ndarray + The last iterate of the optimization algorithm. """ self.evalFunction = evalFunction self.startup(x0) self.printInit() - if self.print_type != "ubc": - print("x0 has any nan: {:b}".format(np.any(np.isnan(x0)))) + if np.any(np.isnan(x0)): + raise ValueError("x0 has a nan.") + self.f = evalFunction( + self.xc, return_g=False, return_H=False + ) # will stash the fields objects + self.printIter() while True: self.doStartIteration() self.f, self.g, self.H = evalFunction(self.xc, return_g=True, return_H=True) - self.printIter() if self.stoppingCriteria(): break self.searchDirection = self.findSearchDirection() @@ -390,9 +513,12 @@ def evalFunction(x, return_g=False, return_H=False): p = self.scaleSearchDirection(self.searchDirection) xt, passLS = self.modifySearchDirection(p) if not passLS: - xt, caught = self.modifySearchDirectionBreak(p) - if not caught: - return self.xc + if self.require_decrease is True: + xt, caught = self.modifySearchDirectionBreak(p) + if not caught: + return self.xc + else: + print("Linesearch failed. Stepping anyways...") self.doEndIteration(xt) if self.stopNextIteration: break @@ -403,9 +529,8 @@ def evalFunction(x, return_g=False, return_H=False): return self.xc @call_hooks("startup") - def startup(self, x0): - """ - **startup** is called at the start of any new minimize call. + def startup(self, x0: np.ndarray) -> None: + """Called at the start of any new minimize call. This will set:: @@ -413,16 +538,21 @@ def startup(self, x0): xc = x0 iter = iterLS = 0 - :param numpy.ndarray x0: initial x - :rtype: None - :return: None + Parameters + ---------- + x0 : numpy.ndarray + initial x """ self.iter = 0 self.iterLS = 0 self.stopNextIteration = False - x0 = self.projection(x0) # ensure that we start of feasible. + try: + x0 = self.projection(x0) # ensure that we start of feasible. + except Exception as err: + raise RuntimeError("Initial model is not projectable") from err + self.x0 = x0 self.xc = x0 self.f_last = np.nan @@ -430,34 +560,34 @@ def startup(self, x0): @count @call_hooks("doStartIteration") - def doStartIteration(self): - """doStartIteration() - - **doStartIteration** is called at the start of each minimize - iteration. - - :rtype: None - :return: None - """ + def doStartIteration(self) -> None: + """Called at the start of each minimize iteration.""" pass - def printInit(self, inLS=False): - """ - **printInit** is called at the beginning of the optimization - routine. + def printInit(self, inLS: bool = False) -> None: + """Called at the beginning of the optimization routine. If there is a parent object, printInit will check for a parent.printInit function and call that. + Parameters + ---------- + inLS : bool + Whether this is being called from a line search. + """ pad = " " * 10 if inLS else "" name = self.name if not inLS else self.nameLS print_titles(self, self.printers if not inLS else self.printersLS, name, pad) @call_hooks("printIter") - def printIter(self, inLS=False): - """ - **printIter** is called directly after function evaluations. + def printIter(self, inLS: bool = False) -> None: + """Called directly after function evaluations. + + Parameters + ---------- + inLS : bool + Whether this is being called from a line search. If there is a parent object, printIter will check for a parent.printIter function and call that. @@ -466,13 +596,17 @@ def printIter(self, inLS=False): pad = " " * 10 if inLS else "" print_line(self, self.printers if not inLS else self.printersLS, pad=pad) - def printDone(self, inLS=False): - """ - **printDone** is called at the end of the optimization routine. + def printDone(self, inLS: bool = False) -> None: + """Called at the end of the optimization routine. If there is a parent object, printDone will check for a parent.printDone function and call that. + Parameters + ---------- + inLS : bool + Whether this is being called from a line search. + """ pad = " " * 10 if inLS else "" stop, done = ( @@ -492,7 +626,6 @@ def printDone(self, inLS=False): self.printers, pad=pad, ) - print(self.print_target) except AttributeError: print_done( self, @@ -503,18 +636,11 @@ def printDone(self, inLS=False): print_stoppers(self, stoppers, pad="", stop=stop, done=done) @call_hooks("finish") - def finish(self): - """finish() - - **finish** is called at the end of the optimization. - - :rtype: None - :return: None - - """ + def finish(self) -> None: + """Called at the end of the optimization.""" pass - def stoppingCriteria(self, inLS=False): + def stoppingCriteria(self, inLS: bool = False) -> bool: if self.iter == 0: self.f0 = self.f self.g0 = self.g @@ -522,63 +648,70 @@ def stoppingCriteria(self, inLS=False): @timeIt @call_hooks("projection") - def projection(self, p): - """projection(p) + def projection(self, p: np.ndarray) -> np.ndarray: + """Projects a model onto bounds (if given) - projects the search direction. + By default, no projection is applied. - by default, no projection is applied. + Parameters + ---------- + p : numpy.ndarray + The model to project - :param numpy.ndarray p: searchDirection - :rtype: numpy.ndarray - :return: p, projected search direction + Returns + ------- + numpy.ndarray + The projected model. """ return p @timeIt - def findSearchDirection(self): - """findSearchDirection() + def findSearchDirection(self) -> np.ndarray: + """Return the direction to search along for a minimum value. - **findSearchDirection** should return an approximation of: + Returns + ------- + numpy.ndarray + The search direction. - .. math:: + Notes + ----- + This should usually return an approximation of: - H p = - g + .. math:: - Where you are solving for the search direction, p + p = - H^{-1} g The default is: .. math:: - H = I - p = - g - And corresponds to SteepestDescent. + Corresponding to the steepest descent direction The latest function evaluations are present in:: self.f, self.g, self.H - - :rtype: numpy.ndarray - :return: p, Search Direction """ return -self.g @count - def scaleSearchDirection(self, p): - """scaleSearchDirection(p) + def scaleSearchDirection(self, p: np.ndarray) -> np.ndarray: + """Scales the search direction if appropriate. - **scaleSearchDirection** should scale the search direction if - appropriate. + Set the parameter ``maxStep`` in the minimize object, to scale back + the search direction to a maximum size. - Set the parameter **maxStep** in the minimize object, to scale back - the gradient to a maximum size. + Parameters + ---------- + p : numpy.ndarray + The current search direction. - :param numpy.ndarray p: searchDirection - :rtype: numpy.ndarray - :return: p, Scaled Search Direction + Returns + ------- + numpy.ndarray + The scaled search direction. """ if self.maxStep < np.abs(p.max()): @@ -588,12 +721,21 @@ def scaleSearchDirection(self, p): nameLS = "Armijo linesearch" #: The line-search name @timeIt - def modifySearchDirection(self, p): - """modifySearchDirection(p) + def modifySearchDirection(self, p: np.ndarray) -> np.ndarray: + """Changes the search direction based on some sort of linesearch or trust-region criteria. + + Parameters + ---------- + p : numpy.ndarray + The current search direction. - **modifySearchDirection** changes the search direction based on - some sort of linesearch or trust-region criteria. + Returns + ------- + numpy.ndarray + The modified search direction. + Notes + ----- By default, an Armijo backtracking linesearch is preformed with the following parameters: @@ -604,11 +746,7 @@ def modifySearchDirection(self, p): If the linesearch is completed, and a descent direction is found, passLS is returned as True. - Else, a modifySearchDirectionBreak call is preformed. - - :param numpy.ndarray p: searchDirection - :rtype: tuple - :return: (xt, passLS) numpy.ndarray, bool + Else, a `modifySearchDirectionBreak` call is preformed. """ # Projected Armijo linesearch self._LS_t = 1.0 @@ -644,11 +782,8 @@ def modifySearchDirection(self, p): return self._LS_xt, self.iterLS < self.maxIterLS @count - def modifySearchDirectionBreak(self, p): - """modifySearchDirectionBreak(p) - - Code is called if modifySearchDirection fails - to find a descent direction. + def modifySearchDirectionBreak(self, p: np.ndarray) -> np.ndarray: + """Called if modifySearchDirection fails to find a descent direction. The search direction is passed as input and this function must pass back both a new searchDirection, @@ -657,9 +792,18 @@ def modifySearchDirectionBreak(self, p): By default, no additional work is done, and the evalFunction returns a False indicating the break was not caught. - :param numpy.ndarray p: searchDirection - :rtype: tuple - :return: (xt, breakCaught) numpy.ndarray, bool + Parameters + ---------- + p : numpy.ndarray + The failed search direction. + + Returns + ------- + xt : numpy.ndarray + An alternative search direction to use. + was_caught : bool + Whether the break was caught. The minimization algorithm will + break early if ``not was_caught``. """ self.printDone(inLS=True) print("The linesearch got broken. Boo.") @@ -667,24 +811,26 @@ def modifySearchDirectionBreak(self, p): @count @call_hooks("doEndIteration") - def doEndIteration(self, xt): - """doEndIteration(xt) - - **doEndIteration** is called at the end of each minimize iteration. + def doEndIteration(self, xt: np.ndarray) -> None: + """Operation called at the end of each minimize iteration. By default, function values and x locations are shuffled to store 1 past iteration in memory. - self.xc must be updated in this code. - - :param numpy.ndarray xt: tested new iterate that ensures a descent direction. - :rtype: None - :return: None + Parameters + ---------- + xt : numpy.ndarray + An accepted model at the end of each iteration. """ # store old values self.f_last = self.f + if hasattr(self, "_LS_ft"): + self.f = self._LS_ft + + # the current iterate, `self.xc`, must be set in this function if overridden in a base class self.x_last, self.xc = self.xc, xt self.iter += 1 + self.printIter() # before callbacks (from directives...) if self.debug: self.printDone() @@ -760,66 +906,93 @@ def _doEndIterationRemember(self, *args): self._rememberList[param[0]].append(param[1](self)) -class ProjectedGradient(Minimize, Remember): - name = "Projected Gradient" - - maxIterCG = 5 - tolCG = 1e-1 +class Bounded(object): + """Mixin class for bounded minimizers - lower = -np.inf - upper = np.inf + Parameters + ---------- + lower, upper : float or numpy.ndarray, optional + The lower and upper bounds. + """ - def __init__(self, **kwargs): - super(ProjectedGradient, self).__init__(**kwargs) + def __init__( + self, + *, + lower: None | float | npt.NDArray[np.float64], + upper: None | float | npt.NDArray[np.float64] = None, + **kwargs, + ): + self.lower = lower + self.upper = upper + super().__init__(**kwargs) - self.stoppers.append(StoppingCriteria.bindingSet) - self.stoppersLS.append(StoppingCriteria.bindingSet_LS) + @property + def lower(self) -> None | float | npt.NDArray[np.float64]: + """The lower bound value. - self.printers.extend( - [ - IterationPrinters.itType, - IterationPrinters.aSet, - IterationPrinters.bSet, - IterationPrinters.comment, - ] - ) + Returns + ------- + lower : None, float, numpy.ndarray + """ + return self._lower - def _startup(self, x0): - # ensure bound vectors are the same size as the model - if not isinstance(self.lower, np.ndarray): - self.lower = np.ones_like(x0) * self.lower - if not isinstance(self.upper, np.ndarray): - self.upper = np.ones_like(x0) * self.upper + @lower.setter + def lower(self, value): + if value is not None: + try: + value = validate_float("lower", value) + except TypeError: + value = validate_ndarray_with_shape("lower", value, shape=("*",)) + self._lower = value - self.explorePG = True - self.exploreCG = False - self.stopDoingPG = False + @property + def upper(self) -> None | float | npt.NDArray[np.float64]: + """The upper bound value. - self._itType = "SD" - self.comment = "" + Returns + ------- + upper : None, float, numpy.ndarray + """ + return self._upper - self.aSet_prev = self.activeSet(x0) + @upper.setter + def upper(self, value): + if value is not None: + try: + value = validate_float("upper", value) + except TypeError: + value = validate_ndarray_with_shape("upper", value, shape=("*",)) + self._upper = value @count - def projection(self, x): + def projection(self, x: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]: """projection(x) Make sure we are feasible. """ - return np.median(np.c_[self.lower, x, self.upper], axis=1) + if self.lower is not None: + x = np.maximum(x, self.lower) + if self.upper is not None: + x = np.minimum(x, self.upper) + return x @count - def activeSet(self, x): + def activeSet(self, x: npt.NDArray[np.float64]) -> npt.NDArray[bool]: """activeSet(x) If we are on a bound """ - return np.logical_or(x == self.lower, x == self.upper) + out = np.zeros(x.shape, dtype=bool) + if self.lower is not None: + out |= x <= self.lower + if self.upper is not None: + out |= x >= self.upper + return out @count - def inactiveSet(self, x): + def inactiveSet(self, x: npt.NDArray[np.float64]) -> npt.NDArray[bool]: """inactiveSet(x) The free variables. @@ -828,7 +1001,7 @@ def inactiveSet(self, x): return np.logical_not(self.activeSet(x)) @count - def bindingSet(self, x): + def bindingSet(self, x: npt.NDArray[np.float64]) -> npt.NDArray[bool]: """bindingSet(x) If we are on a bound and the negative gradient points away from the @@ -837,9 +1010,160 @@ def bindingSet(self, x): Optimality condition. (Satisfies Kuhn-Tucker) MoreToraldo91 """ - bind_up = np.logical_and(x == self.lower, self.g >= 0) - bind_low = np.logical_and(x == self.upper, self.g <= 0) - return np.logical_or(bind_up, bind_low) + out = np.zeros(x.shape, dtype=bool) + if self.lower is not None: + out |= (x <= self.lower) & (self.g >= 0) + if self.upper is not None: + out |= (x >= self.upper) & (self.g <= 0) + return out + + +class InexactCG(object): + """Mixin to hold common parameters for a CG solver. + + Parameters + ---------- + cg_rtol : float, optional + Relative tolerance stopping condition on the CG residual + cg_atol : float, optional + Absolute tolerance stopping condition on the CG residual + cg_maxiter : int, optional + Maximum number of CG iterations to perform + + Notes + ----- + + The convergence check for CG is: + >>> norm(A @ x_k - b) <= max(cg_rtol * norm(A @ x_0 - b), cg_atol) + + See Also + -------- + scipy.sparse.linalg.cg + + """ + + def __init__( + self, + *, + cg_rtol: float = 1e-1, + cg_atol: float = 0, + cg_maxiter: int = 5, + **kwargs, + ): + + if (val := kwargs.pop("tolCG", None)) is not None: + self.tolCG = val # Deprecated cg_rtol + else: + self.cg_rtol = cg_rtol + self.cg_atol = cg_atol + + if (val := kwargs.pop("maxIterCG", None)) is not None: + self.maxIterCG = val + else: + self.cg_maxiter = cg_maxiter + + super().__init__(**kwargs) + + @property + def cg_atol(self) -> float: + """Absolute tolerance for inner CG iterations. + + CG iterations are terminated if: + >>> norm(A @ x_k - b) <= max(cg_rtol * norm(A @ x_0 - b), cg_atol) + + or if the maximum number of CG iterations is reached. + + Returns + ------- + float + + See Also + -------- + cg_rtol, scipy.sparse.linalg.cg + """ + return self._cg_atol + + @cg_atol.setter + def cg_atol(self, value): + self._cg_atol = validate_float("cg_atol", value, min_val=0, inclusive_min=True) + + @property + def cg_rtol(self) -> float: + """Relative tolerance for inner CG iterations. + + CG iterations are terminated if: + >>> norm(A @ x_k - b) <= max(cg_rtol * norm(A @ x_0 - b), cg_atol) + + or if the maximum number of CG iterations is reached. + + Returns + ------- + float + + See Also + -------- + cg_rtol, scipy.sparse.linalg.cg + """ + return self._cg_rtol + + @cg_rtol.setter + def cg_rtol(self, value): + self._cg_rtol = validate_float("cg_rtol", value, min_val=0, inclusive_min=True) + + @property + def cg_maxiter(self) -> int: + """Maximum number of CG iterations. + Returns + ------- + int + """ + return self._cg_maxiter + + @cg_maxiter.setter + def cg_maxiter(self, value): + self._cg_maxiter = validate_integer("cg_maxiter", value, min_val=1) + + maxIterCG = deprecate_property( + cg_maxiter, old_name="maxIterCG", removal_version="0.26.0", future_warn=True + ) + tolCG = deprecate_property( + cg_rtol, old_name="tolCG", removal_version="0.26.0", future_warn=True + ) + + +class ProjectedGradient(Bounded, InexactCG, Minimize, Remember): + name = "Projected Gradient" + + def __init__( + self, *, lower=-np.inf, upper=np.inf, cg_rtol=1e-1, cg_maxiter=5, **kwargs + ): + super().__init__( + lower=lower, upper=upper, cg_rtol=cg_rtol, cg_maxiter=cg_maxiter, **kwargs + ) + + self.stoppers.append(StoppingCriteria.bindingSet) + self.stoppersLS.append(StoppingCriteria.bindingSet_LS) + + self.printers.extend( + [ + IterationPrinters.itType, + IterationPrinters.aSet, + IterationPrinters.bSet, + IterationPrinters.comment, + ] + ) + + def startup(self, x0): + super().startup(x0) + + self.explorePG = True + self.exploreCG = False + self.stopDoingPG = False + + self._itType = "SD" + self.comment = "" + + self.aSet_prev = self.activeSet(x0) @timeIt def findSearchDirection(self): @@ -892,8 +1216,13 @@ def reduceHess(v): operator = sp.linalg.LinearOperator( (shape[1], shape[1]), reduceHess, dtype=self.xc.dtype ) + p, info = sp.linalg.cg( - operator, -Z.T * self.g, tol=self.tolCG, maxiter=self.maxIterCG + operator, + -Z.T * self.g, + rtol=self.cg_rtol, + atol=self.cg_atol, + maxiter=self.cg_maxiter, ) p = Z * p # bring up to full size # aSet_after = self.activeSet(self.xc+p) @@ -937,9 +1266,6 @@ class BFGS(Minimize, Remember): name = "BFGS" nbfgs = 10 - def __init__(self, **kwargs): - Minimize.__init__(self, **kwargs) - @property def bfgsH0(self): """ @@ -948,12 +1274,7 @@ def bfgsH0(self): Must be a simpeg.Solver """ if getattr(self, "_bfgsH0", None) is None: - print( - """ - Default solver: SolverDiag is being used in bfgsH0 - """ - ) - self._bfgsH0 = SolverDiag(sp.identity(self.xc.size)) + self._bfgsH0 = Identity() return self._bfgsH0 @bfgsH0.setter @@ -1021,7 +1342,7 @@ def findSearchDirection(self): return Solver(self.H) * (-self.g) -class InexactGaussNewton(BFGS, Minimize, Remember): +class InexactGaussNewton(InexactCG, BFGS): r""" Minimizes using CG as the inexact solver of @@ -1038,13 +1359,21 @@ class InexactGaussNewton(BFGS, Minimize, Remember): """ - def __init__(self, **kwargs): - Minimize.__init__(self, **kwargs) + def __init__( + self, + *, + cg_rtol: float = 1e-1, + cg_atol: float = 0.0, + cg_maxiter: int = 5, + **kwargs, + ): + super().__init__( + cg_rtol=cg_rtol, cg_atol=cg_atol, cg_maxiter=cg_maxiter, **kwargs + ) - name = "Inexact Gauss Newton" + self._was_default_hinv = False - maxIterCG = 5 - tolCG = 1e-1 + name = "Inexact Gauss Newton" @property def approxHinv(self): @@ -1060,7 +1389,9 @@ def approxHinv(self): M = sp.linalg.LinearOperator( (self.xc.size, self.xc.size), self.bfgs, dtype=self.xc.dtype ) + self._was_default_hinv = True return M + self._was_default_hinv = False return _approxHinv @approxHinv.setter @@ -1069,12 +1400,20 @@ def approxHinv(self, value): @timeIt def findSearchDirection(self): - Hinv = SolverICG( - self.H, M=self.approxHinv, tol=self.tolCG, maxiter=self.maxIterCG + Hinv = SolverCG( + self.H, + M=self.approxHinv, + rtol=self.cg_rtol, + atol=self.cg_atol, + maxiter=self.cg_maxiter, ) p = Hinv * (-self.g) return p + def _doEndIteration_BFGS(self, xt): + if self._was_default_hinv: + super()._doEndIteration_BFGS(xt) + class SteepestDescent(Minimize, Remember): name = "Steepest Descent" @@ -1178,65 +1517,109 @@ def evalFunction(x, return_g=False): return x -class ProjectedGNCG(BFGS, Minimize, Remember): - def __init__(self, **kwargs): - Minimize.__init__(self, **kwargs) - - name = "Projected GNCG" - - maxIterCG = 5 - tolCG = 1e-1 - cg_count = 0 - stepOffBoundsFact = 1e-2 # perturbation of the inactive set off the bounds - stepActiveset = True - lower = -np.inf - upper = np.inf - - def _startup(self, x0): - # ensure bound vectors are the same size as the model - if not isinstance(self.lower, np.ndarray): - self.lower = np.ones_like(x0) * self.lower - if not isinstance(self.upper, np.ndarray): - self.upper = np.ones_like(x0) * self.upper +class ProjectedGNCG(Bounded, InexactGaussNewton): + def __init__( + self, + *, + lower: None | float | npt.NDArray[np.float64] = -np.inf, + upper: None | float | npt.NDArray[np.float64] = np.inf, + cg_maxiter: int = 5, + cg_rtol: float = None, + cg_atol: float = None, + step_active_set: bool = True, + active_set_grad_scale: float = 1e-2, + **kwargs, + ): + if (val := kwargs.pop("tolCG", None)) is not None: + # Deprecated path when tolCG is passed. + self.tolCG = val + cg_atol = val + cg_rtol = 0.0 + elif cg_rtol is None and cg_atol is None: + # Note these defaults match previous settings... + # but they're not good in general... + # Ideally they will change to cg_rtol=1E-3 and cg_atol=0.0 + warnings.warn( + "The defaults for ProjectedGNCG will change in SimPEG 0.26.0. If you want to maintain the " + "previous behavior, explicitly set 'cg_atol=1E-3' and 'cg_rtol=0.0'.", + FutureWarning, + stacklevel=2, + ) + cg_atol = 1e-3 + cg_rtol = 0.0 + # defaults for if someone passes just cg_rtol or just cg_atol (to be removed on deprecation removal) + # These will likely be the future defaults + elif cg_atol is None: + cg_atol = 0.0 + elif cg_rtol is None: + cg_rtol = 1e-3 + + if (val := kwargs.pop("stepActiveSet", None)) is not None: + self.stepActiveSet = val + else: + self.step_active_set = step_active_set - @count - def projection(self, x): - """projection(x) + if (val := kwargs.pop("stepOffBoundsFact", None)) is not None: + self.stepOffBoundsFact = val + else: + self.active_set_grad_scale = active_set_grad_scale + + super().__init__( + lower=lower, + upper=upper, + cg_maxiter=cg_maxiter, + cg_rtol=cg_rtol, + cg_atol=cg_atol, + **kwargs, + ) - Make sure we are feasible. + # initialize some tracking parameters + self.cg_count = 0 + self.cg_abs_resid = np.inf + self.cg_rel_resid = np.inf - """ - return np.median(np.c_[self.lower, x, self.upper], axis=1) + self.printers.extend( + [ + IterationPrinters.iterationCG, + IterationPrinters.iteration_CG_rel_residual, + IterationPrinters.iteration_CG_abs_residual, + ] + ) - @count - def activeSet(self, x): - """activeSet(x) + name = "Projected GNCG" - If we are on a bound + @property + def step_active_set(self) -> bool: + """Whether to include the active set's gradient in the step direction. + Returns + ------- + bool """ - return np.logical_or(x <= self.lower, x >= self.upper) + return self._step_active_set + + @step_active_set.setter + def step_active_set(self, value: bool): + self._step_active_set = validate_type("step_active_set", value, bool) @property - def approxHinv(self): - """ - The approximate Hessian inverse is used to precondition CG. + def active_set_grad_scale(self) -> float: + """Scalar to apply to the active set's gradient - Default uses BFGS, with an initial H0 of *bfgsH0*. + if `step_active_set` is `True`, then the active set's gradient is multiplied by this value + when including it in the search direction. - Must be a scipy.sparse.linalg.LinearOperator + Returns + ------- + float """ - _approxHinv = getattr(self, "_approxHinv", None) - if _approxHinv is None: - M = sp.linalg.LinearOperator( - (self.xc.size, self.xc.size), self.bfgs, dtype=self.xc.dtype - ) - return M - return _approxHinv + return self._active_set_grad_scale - @approxHinv.setter - def approxHinv(self, value): - self._approxHinv = value + @active_set_grad_scale.setter + def active_set_grad_scale(self, value: float): + self._active_set_grad_scale = validate_float( + "active_set_grad_scale", value, min_val=0, inclusive_min=True + ) @timeIt def findSearchDirection(self): @@ -1244,58 +1627,107 @@ def findSearchDirection(self): findSearchDirection() Finds the search direction based on projected CG """ + # remember, "active" means cell with values equal to the limit + # "inactive" are cells with values inside the limits. + + # The basic logic of this method is to do CG iterations only + # on the inactive set, then also add a scaled gradient for the + # active set, (if that gradient points away from the limits.) + self.cg_count = 0 - Active = self.activeSet(self.xc) - temp = sum((np.ones_like(self.xc.size) - Active)) + active = self.activeSet(self.xc) + inactive = ~active step = np.zeros(self.g.size) - resid = -(1 - Active) * self.g + resid = inactive * (-self.g) - r = resid - (1 - Active) * (self.H * step) + r = resid # - Inactive * (self.H * step)# step is zero p = self.approxHinv * r sold = np.dot(r, p) count = 0 + r_norm0 = norm(r) - while np.all([np.linalg.norm(r) > self.tolCG, count < self.maxIterCG]): + atol = max(self.cg_rtol * norm(r_norm0), self.cg_atol) + if self.debug: + print(f"CG Target tolerance: {atol}") + r_norm = r_norm0 + while r_norm > atol and count < self.cg_maxiter: + if self.debug: + print(f"CG Iteration: {count}, residual norm: {r_norm}") count += 1 - q = (1 - Active) * (self.H * p) + q = inactive * (self.H * p) alpha = sold / (np.dot(p, q)) step += alpha * p r -= alpha * q + r_norm = norm(r) h = self.approxHinv * r snew = np.dot(r, h) - p = h + (snew / sold * p) + p = h + (snew / sold) * p sold = snew # End CG Iterations - self.cg_count += count - - # Take a gradient step on the active cells if exist - if temp != self.xc.size: - rhs_a = (Active) * -self.g - + self.cg_count = count + self.cg_abs_resid = r_norm + self.cg_rel_resid = r_norm / r_norm0 + + # Also include the gradient for cells on the boundary + # if that gradient would move them away from the boundary. + # aka, active and not bound. + bound = self.bindingSet(self.xc) + active_not_bound = active & (~bound) + if self.step_active_set and np.any(active_not_bound): + rhs_a = active_not_bound * -self.g + + # active means x == boundary + # bound means x == boundary and g == 0 or -g points beyond boundary + # active and not bound means + # x == boundary and g neq 0 and g points inside + # so can safely discard a non-zero check on + # if np.any(rhs_a) + + # reasonable guess at the step length for the gradient on the + # active cell boundaries. Basically scale it to have the same + # maximum as the cg step on the cells that are not on the + # boundary. dm_i = max(abs(step)) dm_a = max(abs(rhs_a)) - # perturb inactive set off of bounds so that they are included - # in the step - step = step + self.stepOffBoundsFact * (rhs_a * dm_i / dm_a) + # add the active set's gradients. + step += self.active_set_grad_scale * (rhs_a * dm_i / dm_a) - # Only keep gradients going in the right direction on the active - # set - indx = ((self.xc <= self.lower) & (step < 0)) | ( - (self.xc >= self.upper) & (step > 0) - ) - step[indx] = 0.0 + # Only keep search directions going in the right direction + step[bound] = 0 return step + + stepActiveSet = deprecate_property( + step_active_set, + old_name="stepActiveSet", + removal_version="0.26.0", + future_warn=True, + ) + + stepOffBoundsFact = deprecate_property( + active_set_grad_scale, + old_name="stepOffBoundsFact", + removal_version="0.26.0", + future_warn=True, + ) + + # This was the weird part from before... the default tolerance was used as an absolute tolerance... + tolCG = deprecate_property( + InexactGaussNewton.cg_atol, + old_name="tolCG", + removal_version="0.26.0", + future_warn=True, + ) diff --git a/simpeg/potential_fields/_numba_utils.py b/simpeg/potential_fields/_numba_utils.py new file mode 100644 index 0000000000..58f216a1a8 --- /dev/null +++ b/simpeg/potential_fields/_numba_utils.py @@ -0,0 +1,255 @@ +""" +Utility functions for Numba implementations + +These functions are meant to be used both in the Numba-based gravity and +magnetic simulations. +""" + +import numpy as np + +try: + from numba import jit +except ImportError: + # Define dummy jit decorator + def jit(*args, **kwargs): + return lambda f: f + + +@jit(nopython=True) +def kernels_in_nodes_to_cell(kernels, nodes_indices): + """ + Evaluate integral on a given cell from evaluation of kernels on nodes + + Parameters + ---------- + kernels : (n_active_nodes,) array + Array with kernel values on each one of the nodes in the mesh. + nodes_indices : (8,) array of int + Indices of the nodes for the current cell in "F" order (x changes + faster than y, and y faster than z). + + Returns + ------- + float + """ + result = ( + -kernels[nodes_indices[0]] + + kernels[nodes_indices[1]] + + kernels[nodes_indices[2]] + - kernels[nodes_indices[3]] + + kernels[nodes_indices[4]] + - kernels[nodes_indices[5]] + - kernels[nodes_indices[6]] + + kernels[nodes_indices[7]] + ) + return result + + +@jit(nopython=True) +def evaluate_kernels_on_cell( + easting, + northing, + upward, + prism_west, + prism_east, + prism_south, + prism_north, + prism_bottom, + prism_top, + kernel_x, + kernel_y, + kernel_z, +): + r""" + Evaluate three kernel functions on every shifted vertex of a prism. + + .. note:: + + This function was inspired in the ``_evaluate_kernel`` function in + Choclo (released under BSD 3-clause Licence): + https://www.fatiando.org/choclo + + Parameters + ---------- + easting, northing, upward : float + Easting, northing and upward coordinates of the observation point. Must + be in meters. + prism_west, prism_east : floats + The West and East boundaries of the prism. Must be in meters. + prism_south, prism_north : floats + The South and North boundaries of the prism. Must be in meters. + prism_bottom, prism_top : floats + The bottom and top boundaries of the prism. Must be in meters. + kernel_x, kernel_y, kernel_z : callable + Kernel functions that will be evaluated on each one of the shifted + vertices of the prism. + + Returns + ------- + result_x, result_y, result_z : floats + Evaluation of the kernel functions on each one of the vertices of the + prism. + + Notes + ----- + This function evaluates each numerical kernel :math:`k(x, y, z)` on each one + of the vertices of the prism: + + .. math:: + + v(\mathbf{p}) = + \Bigg\lvert \Bigg\lvert \Bigg\lvert + k(x, y, z) + \Bigg\rvert_{X_1}^{X_2} + \Bigg\rvert_{Y_1}^{Y_2} + \Bigg\rvert_{Z_1}^{Z_2} + + where :math:`X_1`, :math:`X_2`, :math:`Y_1`, :math:`Y_2`, :math:`Z_1` and + :math:`Z_2` are boundaries of the rectangular prism in the *shifted + coordinates* defined by the Cartesian coordinate system with its origin + located on the observation point :math:`\mathbf{p}`. + """ + # Initialize result floats to zero + result_x, result_y, result_z = 0.0, 0.0, 0.0 + # Iterate over the vertices of the prism + for i in range(2): + # Compute shifted easting coordinate + if i == 0: + shift_east = prism_east - easting + else: + shift_east = prism_west - easting + shift_east_sq = shift_east**2 + for j in range(2): + # Compute shifted northing coordinate + if j == 0: + shift_north = prism_north - northing + else: + shift_north = prism_south - northing + shift_north_sq = shift_north**2 + for k in range(2): + # Compute shifted upward coordinate + if k == 0: + shift_upward = prism_top - upward + else: + shift_upward = prism_bottom - upward + shift_upward_sq = shift_upward**2 + # Compute the radius + radius = np.sqrt(shift_east_sq + shift_north_sq + shift_upward_sq) + # If i, j or k is 1, the corresponding shifted + # coordinate will refer to the lower boundary, + # meaning the corresponding term should have a minus + # sign. + result_x += (-1) ** (i + j + k) * kernel_x( + shift_east, shift_north, shift_upward, radius + ) + result_y += (-1) ** (i + j + k) * kernel_y( + shift_east, shift_north, shift_upward, radius + ) + result_z += (-1) ** (i + j + k) * kernel_z( + shift_east, shift_north, shift_upward, radius + ) + return result_x, result_y, result_z + + +@jit(nopython=True) +def evaluate_six_kernels_on_cell( + easting, + northing, + upward, + prism_west, + prism_east, + prism_south, + prism_north, + prism_bottom, + prism_top, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, +): + r""" + Evaluate six kernel functions on every shifted vertex of a prism. + + Similar to ``evaluate_kernels_on_cell``, but designed to evaluate six kernels + instead of three. This function comes useful for magnetic forwards, when six kernels + are needed to be evaluated. + + .. note:: + + This function was inspired in the ``_evaluate_kernel`` function in + Choclo (released under BSD 3-clause Licence): + https://www.fatiando.org/choclo + + Parameters + ---------- + easting, northing, upward : float + Easting, northing and upward coordinates of the observation point. Must + be in meters. + prism_west, prism_east : floats + The West and East boundaries of the prism. Must be in meters. + prism_south, prism_north : floats + The South and North boundaries of the prism. Must be in meters. + prism_bottom, prism_top : floats + The bottom and top boundaries of the prism. Must be in meters. + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callable + Kernel functions that will be evaluated on each one of the shifted + vertices of the prism. + + Returns + ------- + result_xx, result_yy, result_zz, result_xy, result_xz, result_yz : float + Evaluation of the kernel functions on each one of the vertices of the + prism. + """ + # Initialize result floats to zero + result_xx, result_yy, result_zz = 0.0, 0.0, 0.0 + result_xy, result_xz, result_yz = 0.0, 0.0, 0.0 + # Iterate over the vertices of the prism + for i in range(2): + # Compute shifted easting coordinate + if i == 0: + shift_east = prism_east - easting + else: + shift_east = prism_west - easting + shift_east_sq = shift_east**2 + for j in range(2): + # Compute shifted northing coordinate + if j == 0: + shift_north = prism_north - northing + else: + shift_north = prism_south - northing + shift_north_sq = shift_north**2 + for k in range(2): + # Compute shifted upward coordinate + if k == 0: + shift_upward = prism_top - upward + else: + shift_upward = prism_bottom - upward + shift_upward_sq = shift_upward**2 + # Compute the radius + radius = np.sqrt(shift_east_sq + shift_north_sq + shift_upward_sq) + # If i, j or k is 1, the corresponding shifted + # coordinate will refer to the lower boundary, + # meaning the corresponding term should have a minus + # sign. + result_xx += (-1) ** (i + j + k) * kernel_xx( + shift_east, shift_north, shift_upward, radius + ) + result_yy += (-1) ** (i + j + k) * kernel_yy( + shift_east, shift_north, shift_upward, radius + ) + result_zz += (-1) ** (i + j + k) * kernel_zz( + shift_east, shift_north, shift_upward, radius + ) + result_xy += (-1) ** (i + j + k) * kernel_xy( + shift_east, shift_north, shift_upward, radius + ) + result_xz += (-1) ** (i + j + k) * kernel_xz( + shift_east, shift_north, shift_upward, radius + ) + result_yz += (-1) ** (i + j + k) * kernel_yz( + shift_east, shift_north, shift_upward, radius + ) + return result_xx, result_yy, result_zz, result_xy, result_xz, result_yz diff --git a/simpeg/potential_fields/base.py b/simpeg/potential_fields/base.py index d457483e3e..2781c26c14 100644 --- a/simpeg/potential_fields/base.py +++ b/simpeg/potential_fields/base.py @@ -3,12 +3,16 @@ import discretize import numpy as np -from scipy.sparse import csr_matrix as csr - -from simpeg.utils import mkvc +from discretize import TensorMesh, TreeMesh from ..simulation import LinearSimulation from ..utils import validate_active_indices, validate_integer, validate_string +from ..utils.code_utils import deprecate_property, validate_type + +try: + import choclo +except ImportError: + choclo = None ############################################################################### # # @@ -35,7 +39,7 @@ class BasePFSimulation(LinearSimulation): ---------- mesh : discretize.TensorMesh or discretize.TreeMesh A 3D tensor or tree mesh. - ind_active : np.ndarray of int or bool + active_cells : np.ndarray of int or bool Indices array denoting the active topography cells. store_sensitivities : {'ram', 'disk', 'forward_only'} Options for storing sensitivities. There are 3 options @@ -47,7 +51,14 @@ class BasePFSimulation(LinearSimulation): n_processes : None or int, optional The number of processes to use in the internal multiprocessing pool for forward modeling. The default value of 1 will not use multiprocessing. Any other setting - will. `None` implies setting by the number of cpus. + will. `None` implies setting by the number of cpus. If engine is + ``"choclo"``, then this argument will be ignored. + engine : {"geoana", "choclo"}, optional + Choose which engine should be used to run the forward model. + numba_parallel : bool, optional + If True, the simulation will run in parallel. If False, it will + run in serial. If ``engine`` is not ``"choclo"`` this argument will be + ignored. Notes ----- @@ -69,37 +80,42 @@ class BasePFSimulation(LinearSimulation): def __init__( self, mesh, - ind_active=None, + active_cells=None, store_sensitivities="ram", n_processes=1, sensitivity_dtype=np.float32, + engine="geoana", + numba_parallel=True, **kwargs, ): - # If deprecated property set with kwargs - if "actInd" in kwargs: - raise AttributeError( - "actInd was removed in SimPEG 0.17.0, please use ind_active" - ) - - if "forwardOnly" in kwargs: - raise AttributeError( - "forwardOnly was removed in SimPEG 0.17.0, please set store_sensitivities='forward_only'" + # removed ind_active argument + if kwargs.pop("ind_active", None) is not None: + raise TypeError( + "'ind_active' has been removed in " + "SimPEG v0.24.0, please use 'active_cells' instead.", ) + self.mesh = mesh self.store_sensitivities = store_sensitivities self.sensitivity_dtype = sensitivity_dtype - super().__init__(mesh, **kwargs) - self.solver = None + self.engine = engine + self.numba_parallel = numba_parallel + super().__init__(**kwargs) self.n_processes = n_processes + # Check sensitivity_path when engine is "choclo" + self._check_engine_and_sensitivity_path() + # Find non-zero cells indices - if ind_active is None: - ind_active = np.ones(mesh.n_cells, dtype=bool) + if active_cells is None: + active_cells = np.ones(mesh.n_cells, dtype=bool) else: - ind_active = validate_active_indices("ind_active", ind_active, mesh.n_cells) - self._ind_active = ind_active + active_cells = validate_active_indices( + "active_cells", active_cells, mesh.n_cells + ) + self._active_cells = active_cells - self.nC = int(sum(ind_active)) + self.nC = int(sum(active_cells)) if isinstance(mesh, discretize.TensorMesh): nodes = mesh.nodes @@ -122,16 +138,36 @@ def __init__( inds[:-1, 1:, 1:].reshape(-1, order="F"), inds[1:, 1:, 1:].reshape(-1, order="F"), ] - cell_nodes = np.stack(cell_nodes, axis=-1)[ind_active] + cell_nodes = np.stack(cell_nodes, axis=-1)[active_cells] elif isinstance(mesh, discretize.TreeMesh): nodes = np.r_[mesh.nodes, mesh.hanging_nodes] - cell_nodes = mesh.cell_nodes[ind_active] + cell_nodes = mesh.cell_nodes[active_cells] else: raise ValueError("Mesh must be 3D tensor or Octree.") unique, unique_inv = np.unique(cell_nodes.T, return_inverse=True) self._nodes = nodes[unique] # unique active nodes self._unique_inv = unique_inv.reshape(cell_nodes.T.shape) + @property + def mesh(self): + """Mesh for the integral potential field simulations. + + Returns + ------- + discretize.TensorMesh or discretize.TreeMesh + 3D Mesh on which the forward problem is discretized. + """ + return self._mesh + + @mesh.setter + def mesh(self, value): + value = validate_type("mesh", value, (TensorMesh, TreeMesh), cast=False) + if value.dim != 3: + raise ValueError( + f"{type(self).__name__} mesh must be 3D, received a {value.dim}D mesh." + ) + self._mesh = value + @property def store_sensitivities(self): """Options for storing sensitivities. @@ -180,6 +216,11 @@ def sensitivity_dtype(self, value): @property def n_processes(self): + """ + Number of processes to use for forward modeling. + + If ``engine`` is ``"choclo"``, then this property will be ignored. + """ return self._n_processes @n_processes.setter @@ -189,15 +230,71 @@ def n_processes(self, value): self._n_processes = value @property - def ind_active(self): - """Active topography cells. + def engine(self) -> str: + """ + Engine that will be used to run the simulation. + + It can be either ``"geoana"`` or "``choclo``". + """ + return self._engine + + @engine.setter + def engine(self, value: str): + validate_string( + "engine", value, string_list=("geoana", "choclo"), case_sensitive=True + ) + if value == "choclo" and choclo is None: + raise ImportError( + "The choclo package couldn't be found." + "Running a gravity simulation with 'engine=\"choclo\"' needs " + "choclo to be installed." + "\nTry installing choclo with:" + "\n pip install choclo" + "\nor:" + "\n conda install choclo" + ) + self._engine = value + + @property + def numba_parallel(self) -> bool: + """ + Run simulation in parallel or single-threaded when using Numba. + + If True, the simulation will run in parallel. If False, it will + run in serial. + + .. important:: + + If ``engine`` is not ``"choclo"`` this property will be ignored. + """ + return self._numba_parallel + + @numba_parallel.setter + def numba_parallel(self, value: bool): + if not isinstance(value, bool): + raise TypeError( + f"Invalid 'numba_parallel' value of type {type(value)}. Must be a bool." + ) + self._numba_parallel = value + + @property + def active_cells(self): + """Active cells in the mesh. Returns ------- (n_cell) numpy.ndarray of bool - Returns the active topography cells + Returns the active cells in the mesh. """ - return self._ind_active + return self._active_cells + + ind_active = deprecate_property( + active_cells, + "ind_active", + "active_cells", + removal_version="0.24.0", + error=True, + ) def linear_operator(self): """Return linear operator. @@ -255,6 +352,61 @@ def linear_operator(self): np.save(sens_name, kernel) return kernel + def _check_engine_and_sensitivity_path(self): + """ + Check if sensitivity_path is a file if engine is set to "choclo" + """ + if ( + self.engine == "choclo" + and self.store_sensitivities == "disk" + and os.path.isdir(self.sensitivity_path) + ): + raise ValueError( + f"The passed sensitivity_path '{self.sensitivity_path}' is " + "a directory. " + "When using 'choclo' as the engine, 'senstivity_path' " + "should be the path to a new or existing file." + ) + + def _get_active_nodes(self): + """ + Return locations of nodes only for active cells + + Also return an array containing the indices of the "active nodes" for + each active cell in the mesh + """ + # Get all nodes in the mesh + if isinstance(self.mesh, discretize.TreeMesh): + nodes = self.mesh.total_nodes + elif isinstance(self.mesh, discretize.TensorMesh): + nodes = self.mesh.nodes + else: + raise TypeError(f"Invalid mesh of type {self.mesh.__class__.__name__}.") + # Get original cell_nodes + cell_nodes = self.mesh.cell_nodes + # If all cells in the mesh are active, return nodes and cell_nodes + if self.nC == self.mesh.n_cells: + return nodes, cell_nodes + # Keep only the cell_nodes for active cells + cell_nodes = cell_nodes[self.active_cells] + # Get the unique indices of the nodes that belong to every active cell + # (these indices correspond to the original `nodes` array) + unique_nodes, active_cell_nodes = np.unique(cell_nodes, return_inverse=True) + # Select only the nodes that belong to the active cells (active nodes) + active_nodes = nodes[unique_nodes] + # Reshape indices of active cell nodes for each active cell in the mesh + active_cell_nodes = active_cell_nodes.reshape(cell_nodes.shape) + return active_nodes, active_cell_nodes + + def _get_components_and_receivers(self): + """Generator for receiver locations and their field components.""" + if not hasattr(self.survey, "source_field"): + raise AttributeError( + f"The survey '{self.survey}' has no 'source_field' attribute." + ) + for receiver_object in self.survey.source_field.receiver_list: + yield receiver_object.components, receiver_object.locations + class BaseEquivalentSourceLayerSimulation(BasePFSimulation): """Base equivalent source layer simulation class. @@ -272,10 +424,7 @@ class BaseEquivalentSourceLayerSimulation(BasePFSimulation): """ def __init__(self, mesh, cell_z_top, cell_z_bottom, **kwargs): - if mesh.dim != 2: - raise AttributeError("Mesh to equivalent source layer must be 2D.") - - super().__init__(mesh, **kwargs) + super().__init__(mesh=mesh, **kwargs) if isinstance(cell_z_top, (int, float)): cell_z_top = float(cell_z_top) * np.ones(self.nC) @@ -289,6 +438,8 @@ def __init__(self, mesh, cell_z_top, cell_z_bottom, **kwargs): "cells, and match the number of active cells.", ) + self._cell_z_top, self._cell_z_bottom = cell_z_top, cell_z_bottom + all_nodes = self._nodes[self._unique_inv] all_nodes = [ np.c_[all_nodes[0], cell_z_bottom], @@ -303,6 +454,29 @@ def __init__(self, mesh, cell_z_top, cell_z_bottom, **kwargs): self._nodes = np.stack(all_nodes, axis=0) self._unique_inv = None + @property + def cell_z_top(self) -> np.ndarray: + """ + Elevations for the top face of all cells in the layer. + """ + return self._cell_z_top + + @property + def cell_z_bottom(self) -> np.ndarray: + """ + Elevations for the bottom face of all cells in the layer. + """ + return self._cell_z_bottom + + @BasePFSimulation.mesh.setter + def mesh(self, value): + value = validate_type("mesh", value, (TensorMesh, TreeMesh), cast=False) + if value.dim != 2: + raise ValueError( + f"{type(self).__name__} mesh must be 2D, received a {value.dim}D mesh." + ) + self._mesh = value + def progress(iteration, prog, final): """Progress (% complete) for constructing sensitivity matrix. @@ -330,104 +504,7 @@ def progress(iteration, prog, final): return prog -def get_dist_wgt(mesh, receiver_locations, actv, R, R0): - """Compute distance weights for potential field simulations. - - Parameters - ---------- - mesh : discretize.BaseMesh - A discretize mesh - receiver_locations : (n, 3) numpy.ndarray - Observation locations [x, y, z] - actv : (n_cell) numpy.ndarray of bool - Active cells vector [0:air , 1: ground] - R : float - Decay factor (mag=3, grav =2) - R0 : float - Stabilization factor. Usually a fraction of the minimum cell size - - Returns - ------- - wr : (n_cell) numpy.ndarray - Distance weighting model; 0 for all inactive cells - """ - # Find non-zero cells - if actv.dtype == "bool": - inds = ( - np.asarray([inds for inds, elem in enumerate(actv, 1) if elem], dtype=int) - - 1 - ) - else: - inds = actv - - nC = len(inds) - - # Create active cell projector - P = csr((np.ones(nC), (inds, range(nC))), shape=(mesh.nC, nC)) - - # Geometrical constant - p = 1 / np.sqrt(3) - - # Create cell center location - Ym, Xm, Zm = np.meshgrid( - mesh.cell_centers_y, mesh.cell_centers_x, mesh.cell_centers_z +def get_dist_wgt(*args, **kwargs): + raise NotImplementedError( + "The get_dist_wgt function has been removed in SimPEG 0.24.0, please import simpeg.utils.distance_weighting." ) - hY, hX, hZ = np.meshgrid(mesh.h[1], mesh.h[0], mesh.h[2]) - - # Remove air cells - Xm = P.T * mkvc(Xm) - Ym = P.T * mkvc(Ym) - Zm = P.T * mkvc(Zm) - - hX = P.T * mkvc(hX) - hY = P.T * mkvc(hY) - hZ = P.T * mkvc(hZ) - - V = P.T * mkvc(mesh.cell_volumes) - wr = np.zeros(nC) - - ndata = receiver_locations.shape[0] - count = -1 - print("Begin calculation of distance weighting for R= " + str(R)) - - for dd in range(ndata): - nx1 = (Xm - hX * p - receiver_locations[dd, 0]) ** 2 - nx2 = (Xm + hX * p - receiver_locations[dd, 0]) ** 2 - - ny1 = (Ym - hY * p - receiver_locations[dd, 1]) ** 2 - ny2 = (Ym + hY * p - receiver_locations[dd, 1]) ** 2 - - nz1 = (Zm - hZ * p - receiver_locations[dd, 2]) ** 2 - nz2 = (Zm + hZ * p - receiver_locations[dd, 2]) ** 2 - - R1 = np.sqrt(nx1 + ny1 + nz1) - R2 = np.sqrt(nx1 + ny1 + nz2) - R3 = np.sqrt(nx2 + ny1 + nz1) - R4 = np.sqrt(nx2 + ny1 + nz2) - R5 = np.sqrt(nx1 + ny2 + nz1) - R6 = np.sqrt(nx1 + ny2 + nz2) - R7 = np.sqrt(nx2 + ny2 + nz1) - R8 = np.sqrt(nx2 + ny2 + nz2) - - temp = ( - (R1 + R0) ** -R - + (R2 + R0) ** -R - + (R3 + R0) ** -R - + (R4 + R0) ** -R - + (R5 + R0) ** -R - + (R6 + R0) ** -R - + (R7 + R0) ** -R - + (R8 + R0) ** -R - ) - - wr = wr + (V * temp / 8.0) ** 2.0 - - count = progress(dd, count, ndata) - - wr = np.sqrt(wr) / V - wr = mkvc(wr) - wr = np.sqrt(wr / (np.max(wr))) - - print("Done 100% ...distance weighting completed!!\n") - - return wr diff --git a/simpeg/potential_fields/gravity/_numba/_2d_mesh.py b/simpeg/potential_fields/gravity/_numba/_2d_mesh.py new file mode 100644 index 0000000000..06cb489a37 --- /dev/null +++ b/simpeg/potential_fields/gravity/_numba/_2d_mesh.py @@ -0,0 +1,455 @@ +""" +Numba functions for gravity simulation on 2D meshes. + +These functions assumes 3D prisms formed by a 2D mesh plus top and bottom boundaries for +each prism. +""" + +import numpy as np + +try: + import choclo +except ImportError: + # Define dummy jit decorator + def jit(*args, **kwargs): + return lambda f: f + + choclo = None +else: + from numba import jit, prange + + +def _forward_gravity( + receivers, + cells_bounds, + top, + bottom, + densities, + fields, + forward_func, + constant_factor, +): + """ + Forward gravity fields of 2D meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_function = jit(nopython=True, parallel=True)(_forward_gravity) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + densities : (n_active_cells) numpy.ndarray + Array with densities of each active cell in the mesh. + fields : (n_receivers) numpy.ndarray + Array full of zeros where the gravity fields on each receiver will be + stored. This could be a preallocated array or a slice of it. + forward_func : callable + Forward function that will be evaluated on each node of the mesh. Choose + one of the forward functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + ``fields`` array. + + Notes + ----- + The constant factor is applied here to each element of fields because + it's more efficient than doing it afterwards: it would require to + index the elements that corresponds to each component. + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Forward model the gravity field of each cell on each receiver location + for i in prange(n_receivers): + for j in range(n_cells): + fields[i] += constant_factor * forward_func( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + densities[j], + ) + + +def _sensitivity_gravity( + receivers, + cells_bounds, + top, + bottom, + sensitivity_matrix, + forward_func, + constant_factor, +): + """ + Fill the sensitivity matrix + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_function = jit(nopython=True, parallel=True)(_sensitivity_gravity) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + sensitivity_matrix : (n_receivers, n_active_nodes) array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + forward_func : callable + Forward function that will be evaluated on each node of the mesh. Choose + one of the forward functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + + Notes + ----- + The constant factor is applied here to each row of the sensitivity matrix + because it's more efficient than doing it afterwards: it would require to + index the rows that corresponds to each component. + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + for j in range(n_cells): + sensitivity_matrix[i, j] = constant_factor * forward_func( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + 1.0, # use unitary density to get sensitivities + ) + + +@jit(nopython=True, parallel=False) +def _g_t_dot_v_serial( + receivers, + cells_bounds, + top, + bottom, + forward_func, + constant_factor, + vector, + result, +): + """ + Compute ``G.T @ v`` in serial, without building G, for a 2D mesh. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + forward_func : callable + Forward function that will be evaluated on each node of the mesh. Choose + one of the forward functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + + Notes + ----- + This function is meant to be run in serial. Writing to the ``result`` array + inside a parallel loop over the receivers generates a race condition that + leads to corrupted outputs. + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + for i in range(n_receivers): + for j in range(n_cells): + # Compute the i-th row of the sensitivity matrix and multiply it by the + # i-th element of the vector. + result[j] += constant_factor * forward_func( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + vector[i], + ) + + +@jit(nopython=True, parallel=True) +def _g_t_dot_v_parallel( + receivers, + cells_bounds, + top, + bottom, + forward_func, + constant_factor, + vector, + result, +): + """ + Compute ``G.T @ v`` in parallel, without building G, for a 2D mesh. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + forward_func : callable + Forward function that will be evaluated on each node of the mesh. Choose + one of the forward functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + + Notes + ----- + This function is meant to be run in parallel. + This implementation instructs each thread to allocate their own array for + the current row of the sensitivity matrix. After computing the elements of + that row, it gets added to the running ``result`` array through a reduction + operation handled by Numba. + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + for i in prange(n_receivers): + # Allocate array for the current row of the sensitivity matrix + local_row = np.empty(n_cells) + for j in range(n_cells): + # Compute the i-th row of the sensitivity matrix and multiply it by the + # i-th element of the vector. + local_row[j] = constant_factor * forward_func( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + vector[i], + ) + # Apply reduction operation to add the values of the row to the running + # result. Avoid slicing the `result` array when updating it to avoid + # racing conditions, just add the `local_row` to the `results` + # variable. + result += local_row + + +@jit(nopython=True, parallel=False) +def _diagonal_G_T_dot_G_serial( + receivers, + cells_bounds, + top, + bottom, + forward_func, + constant_factor, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` without storing ``G``, in serial. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + forward_func : callable + Forward function that will be evaluated on each node of the mesh. Choose + one of the forward functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in serial. Use the + ``_diagonal_G_T_dot_G_parallel`` one for parallelized computations. + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in range(n_receivers): + for j in range(n_cells): + g_element = constant_factor * forward_func( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + 1.0, # use unitary density to get sensitivities + ) + diagonal[j] += weights[i] * g_element**2 + + +@jit(nopython=True, parallel=True) +def _diagonal_G_T_dot_G_parallel( + receivers, + cells_bounds, + top, + bottom, + forward_func, + constant_factor, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` without storing ``G``, in parallel. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + forward_func : callable + Forward function that will be evaluated on each node of the mesh. Choose + one of the forward functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in parallel. Use the + ``_diagonal_G_T_dot_G_serial`` one for serialized computations. + + This implementation instructs each thread to allocate their own array for + the diagonal elements of ``G.T @ G`` that correspond to a single receiver. + After computing them, the ``local_diagonal`` array gets added to the running + ``diagonal`` array through a reduction operation handled by Numba. + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate array for the diagonal elements for the current receiver. + local_diagonal = np.empty(n_cells) + for j in range(n_cells): + g_element = constant_factor * forward_func( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + 1.0, # use unitary density to get sensitivities + ) + local_diagonal[j] = weights[i] * g_element**2 + # Add the result to the diagonal. + # Apply reduction operation to add the values of the local diagonal to + # the running diagonal array. Avoid slicing the `diagonal` array when + # updating it to avoid racing conditions, just add the `local_diagonal` + # to the `diagonal` variable. + diagonal += local_diagonal + + +# Define a dictionary with decorated versions of the Numba functions. +NUMBA_FUNCTIONS_2D = { + "sensitivity": { + parallel: jit(nopython=True, parallel=parallel)(_sensitivity_gravity) + for parallel in (True, False) + }, + "forward": { + parallel: jit(nopython=True, parallel=parallel)(_forward_gravity) + for parallel in (True, False) + }, + "gt_dot_v": { + False: _g_t_dot_v_serial, + True: _g_t_dot_v_parallel, + }, + "diagonal_gtg": { + False: _diagonal_G_T_dot_G_serial, + True: _diagonal_G_T_dot_G_parallel, + }, +} diff --git a/simpeg/potential_fields/gravity/_numba/_3d_mesh.py b/simpeg/potential_fields/gravity/_numba/_3d_mesh.py new file mode 100644 index 0000000000..996455cfe9 --- /dev/null +++ b/simpeg/potential_fields/gravity/_numba/_3d_mesh.py @@ -0,0 +1,519 @@ +""" +Numba functions for gravity simulation on 3D meshes. +""" + +import numpy as np + +try: + import choclo +except ImportError: + # Define dummy jit decorator + def jit(*args, **kwargs): + return lambda f: f + + choclo = None +else: + from numba import jit, prange + +from ..._numba_utils import kernels_in_nodes_to_cell + + +def _forward_gravity( + receivers, + nodes, + densities, + fields, + cell_nodes, + kernel_func, + constant_factor, +): + """ + Forward model the gravity field of active cells on receivers + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_forward_gravity = jit(nopython=True, parallel=True)(_forward_gravity) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + densities : (n_active_cells) numpy.ndarray + Array with densities of each active cell in the mesh. + fields : (n_receivers) numpy.ndarray + Array full of zeros where the gravity fields on each receiver will be + stored. This could be a preallocated array or a slice of it. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + kernel_func : callable + Kernel function that will be evaluated on each node of the mesh. Choose + one of the kernel functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + ``fields`` array. + + Notes + ----- + The constant factor is applied here to each element of fields because + it's more efficient than doing it afterwards: it would require to + index the elements that corresponds to each component. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vector for kernels evaluated on mesh nodes + kernels = np.empty(n_nodes) + for j in range(n_nodes): + kernels[j] = _evaluate_kernel( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + nodes[j, 0], + nodes[j, 1], + nodes[j, 2], + kernel_func, + ) + # Compute fields from the kernel values + for k in range(n_cells): + fields[i] += ( + constant_factor + * densities[k] + * kernels_in_nodes_to_cell( + kernels, + cell_nodes[k, :], + ) + ) + + +def _sensitivity_gravity( + receivers, + nodes, + sensitivity_matrix, + cell_nodes, + kernel_func, + constant_factor, +): + """ + Fill the sensitivity matrix + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_sensitivity = jit(nopython=True, parallel=True)(_sensitivity_gravity) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + sensitivity_matrix : (n_receivers, n_active_nodes) array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + kernel_func : callable + Kernel function that will be evaluated on each node of the mesh. Choose + one of the kernel functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + + Notes + ----- + The constant factor is applied here to each row of the sensitivity matrix + because it's more efficient than doing it afterwards: it would require to + index the rows that corresponds to each component. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vector for kernels evaluated on mesh nodes + kernels = np.empty(n_nodes) + for j in range(n_nodes): + kernels[j] = _evaluate_kernel( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + nodes[j, 0], + nodes[j, 1], + nodes[j, 2], + kernel_func, + ) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + sensitivity_matrix[i, k] = constant_factor * kernels_in_nodes_to_cell( + kernels, + cell_nodes[k, :], + ) + + +@jit(nopython=True, parallel=False) +def _diagonal_G_T_dot_G_serial( + receivers, + nodes, + cell_nodes, + kernel_func, + constant_factor, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` without storing ``G``, in serial. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + kernel_func : callable + Kernel function that will be evaluated on each node of the mesh. Choose + one of the kernel functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in serial. Use the + ``_diagonal_G_T_dot_G_parallel`` one for parallelized computations. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vector for kernels evaluated on mesh nodes + kernels = np.empty(n_nodes) + for j in range(n_nodes): + kernels[j] = _evaluate_kernel( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + nodes[j, 0], + nodes[j, 1], + nodes[j, 2], + kernel_func, + ) + # Add diagonal components in the running result. + for k in range(n_cells): + diagonal[k] += ( + weights[i] + * ( + constant_factor + * kernels_in_nodes_to_cell( + kernels, + cell_nodes[k, :], + ) + ) + ** 2 + ) + + +@jit(nopython=True, parallel=True) +def _diagonal_G_T_dot_G_parallel( + receivers, + nodes, + cell_nodes, + kernel_func, + constant_factor, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` without storing ``G``, in parallel. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + kernel_func : callable + Kernel function that will be evaluated on each node of the mesh. Choose + one of the kernel functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in parallel. Use the + ``_diagonal_G_T_dot_G_serial`` one for serialized computations. + + This implementation instructs each thread to allocate their own array for + the current row of the sensitivity matrix. After computing the elements of + that row, it gets added to the running ``result`` array through a reduction + operation handled by Numba. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vector for kernels evaluated on mesh nodes + kernels = np.empty(n_nodes) + for j in range(n_nodes): + kernels[j] = _evaluate_kernel( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + nodes[j, 0], + nodes[j, 1], + nodes[j, 2], + kernel_func, + ) + # Allocate array for the diagonal elements for the current receiver. + local_diagonal = np.empty(n_cells) + for k in range(n_cells): + local_diagonal[k] = ( + weights[i] + * ( + constant_factor + * kernels_in_nodes_to_cell( + kernels, + cell_nodes[k, :], + ) + ) + ** 2 + ) + # Add the result to the diagonal. + # Apply reduction operation to add the values of the local diagonal to + # the running diagonal array. Avoid slicing the `diagonal` array when + # updating it to avoid racing conditions, just add the `local_diagonal` + # to the `diagonal` variable. + diagonal += local_diagonal + + +@jit(nopython=True, parallel=False) +def _sensitivity_gravity_t_dot_v_serial( + receivers, + nodes, + cell_nodes, + kernel_func, + constant_factor, + vector, + result, +): + """ + Compute ``G.T @ v`` in serial, without building G. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + kernel_func : callable + Kernel function that will be evaluated on each node of the mesh. Choose + one of the kernel functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + + Notes + ----- + This function is meant to be run in serial. Writing to the ``result`` array + inside a parallel loop over the receivers generates a race condition that + leads to corrupted outputs. + + A parallel implementation of this function is available in + ``_sensitivity_gravity_t_dot_v_parallel``. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in range(n_receivers): + # Allocate vector for kernels evaluated on mesh nodes + kernels = np.empty(n_nodes) + for j in range(n_nodes): + kernels[j] = _evaluate_kernel( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + nodes[j, 0], + nodes[j, 1], + nodes[j, 2], + kernel_func, + ) + # Compute the i-th row of the sensitivity matrix and multiply it by the + # i-th element of the vector. + for k in range(n_cells): + result[k] += ( + constant_factor + * vector[i] + * kernels_in_nodes_to_cell( + kernels, + cell_nodes[k, :], + ) + ) + + +@jit(nopython=True, parallel=True) +def _sensitivity_gravity_t_dot_v_parallel( + receivers, + nodes, + cell_nodes, + kernel_func, + constant_factor, + vector, + result, +): + """ + Compute ``G.T @ v`` in parallel without building G. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + kernel_func : callable + Kernel function that will be evaluated on each node of the mesh. Choose + one of the kernel functions in ``choclo.prism``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + + Notes + ----- + This function is meant to be run in parallel. + This implementation instructs each thread to allocate their own array for + the current row of the sensitivity matrix. After computing the elements of + that row, it gets added to the running ``result`` array through a reduction + operation handled by Numba. + + A serialized implementation of this function is available in + ``_sensitivity_gravity_t_dot_v_serial``. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vector for kernels evaluated on mesh nodes + kernels = np.empty(n_nodes) + # Allocate array for the current row of the sensitivity matrix + local_row = np.empty(n_cells) + for j in range(n_nodes): + kernels[j] = _evaluate_kernel( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + nodes[j, 0], + nodes[j, 1], + nodes[j, 2], + kernel_func, + ) + # Compute fields from the kernel values + for k in range(n_cells): + local_row[k] = ( + constant_factor + * vector[i] + * kernels_in_nodes_to_cell( + kernels, + cell_nodes[k, :], + ) + ) + # Apply reduction operation to add the values of the row to the running + # result. Avoid slicing the `result` array when updating it to avoid + # racing conditions, just add the `local_row` to the `results` + # variable. + result += local_row + + +@jit(nopython=True) +def _evaluate_kernel( + receiver_x, receiver_y, receiver_z, node_x, node_y, node_z, kernel_func +): + """ + Evaluate a kernel function for a single node and receiver + + Parameters + ---------- + receiver_x, receiver_y, receiver_z : floats + Coordinates of the receiver. + node_x, node_y, node_z : floats + Coordinates of the node. + kernel_func : callable + Kernel function that should be evaluated. For example, use one of the + kernel functions in ``choclo.prism``. + + Returns + ------- + float + Kernel evaluated on the given node and receiver. + """ + dx = node_x - receiver_x + dy = node_y - receiver_y + dz = node_z - receiver_z + distance = np.sqrt(dx**2 + dy**2 + dz**2) + return kernel_func(dx, dy, dz, distance) + + +# Define a dictionary with decorated versions of the Numba functions. +NUMBA_FUNCTIONS_3D = { + "sensitivity": { + parallel: jit(nopython=True, parallel=parallel)(_sensitivity_gravity) + for parallel in (True, False) + }, + "forward": { + parallel: jit(nopython=True, parallel=parallel)(_forward_gravity) + for parallel in (True, False) + }, + "diagonal_gtg": { + False: _diagonal_G_T_dot_G_serial, + True: _diagonal_G_T_dot_G_parallel, + }, + "gt_dot_v": { + False: _sensitivity_gravity_t_dot_v_serial, + True: _sensitivity_gravity_t_dot_v_parallel, + }, +} diff --git a/simpeg/potential_fields/gravity/_numba/__init__.py b/simpeg/potential_fields/gravity/_numba/__init__.py new file mode 100644 index 0000000000..8f9cac68a9 --- /dev/null +++ b/simpeg/potential_fields/gravity/_numba/__init__.py @@ -0,0 +1,13 @@ +""" +Numba functions for gravity simulations. +""" + +from ._2d_mesh import NUMBA_FUNCTIONS_2D +from ._3d_mesh import NUMBA_FUNCTIONS_3D + +try: + import choclo +except ImportError: + choclo = None + +__all__ = ["choclo", "NUMBA_FUNCTIONS_3D", "NUMBA_FUNCTIONS_2D"] diff --git a/simpeg/potential_fields/gravity/_numba_functions.py b/simpeg/potential_fields/gravity/_numba_functions.py deleted file mode 100644 index 1d6b363b27..0000000000 --- a/simpeg/potential_fields/gravity/_numba_functions.py +++ /dev/null @@ -1,251 +0,0 @@ -""" -Numba functions for gravity simulation using Choclo. -""" - -import numpy as np - -try: - import choclo -except ImportError: - # Define dummy jit decorator - def jit(*args, **kwargs): - return lambda f: f - - choclo = None -else: - from numba import jit, prange - - -def _forward_gravity( - receivers, - nodes, - densities, - fields, - cell_nodes, - kernel_func, - constant_factor, -): - """ - Forward model the gravity field of active cells on receivers - - This function should be used with a `numba.jit` decorator, for example: - - ..code:: - - from numba import jit - - jit_forward_gravity = jit(nopython=True, parallel=True)(_forward_gravity) - - Parameters - ---------- - receivers : (n_receivers, 3) numpy.ndarray - Array with the locations of the receivers - nodes : (n_active_nodes, 3) numpy.ndarray - Array with the location of the mesh nodes. - densities : (n_active_cells) numpy.ndarray - Array with densities of each active cell in the mesh. - fields : (n_receivers) numpy.ndarray - Array full of zeros where the gravity fields on each receiver will be - stored. This could be a preallocated array or a slice of it. - cell_nodes : (n_active_cells, 8) numpy.ndarray - Array of integers, where each row contains the indices of the nodes for - each active cell in the mesh. - kernel_func : callable - Kernel function that will be evaluated on each node of the mesh. Choose - one of the kernel functions in ``choclo.prism``. - constant_factor : float - Constant factor that will be used to multiply each element of the - ``fields`` array. - - Notes - ----- - The constant factor is applied here to each element of fields because - it's more efficient than doing it afterwards: it would require to - index the elements that corresponds to each component. - """ - n_receivers = receivers.shape[0] - n_nodes = nodes.shape[0] - n_cells = cell_nodes.shape[0] - # Evaluate kernel function on each node, for each receiver location - for i in prange(n_receivers): - # Allocate vector for kernels evaluated on mesh nodes - kernels = np.empty(n_nodes) - for j in range(n_nodes): - kernels[j] = _evaluate_kernel( - receivers[i, 0], - receivers[i, 1], - receivers[i, 2], - nodes[j, 0], - nodes[j, 1], - nodes[j, 2], - kernel_func, - ) - # Compute fields from the kernel values - for k in range(n_cells): - fields[i] += ( - constant_factor - * densities[k] - * _kernels_in_nodes_to_cell( - kernels, - cell_nodes[k, 0], - cell_nodes[k, 1], - cell_nodes[k, 2], - cell_nodes[k, 3], - cell_nodes[k, 4], - cell_nodes[k, 5], - cell_nodes[k, 6], - cell_nodes[k, 7], - ) - ) - - -def _sensitivity_gravity( - receivers, - nodes, - sensitivity_matrix, - cell_nodes, - kernel_func, - constant_factor, -): - """ - Fill the sensitivity matrix - - This function should be used with a `numba.jit` decorator, for example: - - ..code:: - - from numba import jit - - jit_sensitivity = jit(nopython=True, parallel=True)(_sensitivity_gravity) - - Parameters - ---------- - receivers : (n_receivers, 3) numpy.ndarray - Array with the locations of the receivers - nodes : (n_active_nodes, 3) numpy.ndarray - Array with the location of the mesh nodes. - sensitivity_matrix : (n_receivers, n_active_nodes) array - Empty 2d array where the sensitivity matrix elements will be filled. - This could be a preallocated empty array or a slice of it. - cell_nodes : (n_active_cells, 8) numpy.ndarray - Array of integers, where each row contains the indices of the nodes for - each active cell in the mesh. - kernel_func : callable - Kernel function that will be evaluated on each node of the mesh. Choose - one of the kernel functions in ``choclo.prism``. - constant_factor : float - Constant factor that will be used to multiply each element of the - sensitivity matrix. - - Notes - ----- - The constant factor is applied here to each row of the sensitivity matrix - because it's more efficient than doing it afterwards: it would require to - index the rows that corresponds to each component. - """ - n_receivers = receivers.shape[0] - n_nodes = nodes.shape[0] - n_cells = cell_nodes.shape[0] - # Evaluate kernel function on each node, for each receiver location - for i in prange(n_receivers): - # Allocate vector for kernels evaluated on mesh nodes - kernels = np.empty(n_nodes) - for j in range(n_nodes): - kernels[j] = _evaluate_kernel( - receivers[i, 0], - receivers[i, 1], - receivers[i, 2], - nodes[j, 0], - nodes[j, 1], - nodes[j, 2], - kernel_func, - ) - # Compute sensitivity matrix elements from the kernel values - for k in range(n_cells): - sensitivity_matrix[i, k] = constant_factor * _kernels_in_nodes_to_cell( - kernels, - cell_nodes[k, 0], - cell_nodes[k, 1], - cell_nodes[k, 2], - cell_nodes[k, 3], - cell_nodes[k, 4], - cell_nodes[k, 5], - cell_nodes[k, 6], - cell_nodes[k, 7], - ) - - -@jit(nopython=True) -def _evaluate_kernel( - receiver_x, receiver_y, receiver_z, node_x, node_y, node_z, kernel_func -): - """ - Evaluate a kernel function for a single node and receiver - - Parameters - ---------- - receiver_x, receiver_y, receiver_z : floats - Coordinates of the receiver. - node_x, node_y, node_z : floats - Coordinates of the node. - kernel_func : callable - Kernel function that should be evaluated. For example, use one of the - kernel functions in ``choclo.prism``. - - Returns - ------- - float - Kernel evaluated on the given node and receiver. - """ - dx = node_x - receiver_x - dy = node_y - receiver_y - dz = node_z - receiver_z - distance = np.sqrt(dx**2 + dy**2 + dz**2) - return kernel_func(dx, dy, dz, distance) - - -@jit(nopython=True) -def _kernels_in_nodes_to_cell( - kernels, - nodes_indices_0, - nodes_indices_1, - nodes_indices_2, - nodes_indices_3, - nodes_indices_4, - nodes_indices_5, - nodes_indices_6, - nodes_indices_7, -): - """ - Evaluate integral on a given cell from evaluation of kernels on nodes - - Parameters - ---------- - kernels : (n_active_nodes,) numpy.ndarray - Array with kernel values on each one of the nodes in the mesh. - nodes_indices : ints - Indices of the nodes for the current cell in "F" order (x changes - faster than y, and y faster than z). - - Returns - ------- - float - """ - result = ( - -kernels[nodes_indices_0] - + kernels[nodes_indices_1] - + kernels[nodes_indices_2] - - kernels[nodes_indices_3] - + kernels[nodes_indices_4] - - kernels[nodes_indices_5] - - kernels[nodes_indices_6] - + kernels[nodes_indices_7] - ) - return result - - -# Define decorated versions of these functions -_sensitivity_gravity_parallel = jit(nopython=True, parallel=True)(_sensitivity_gravity) -_sensitivity_gravity_serial = jit(nopython=True, parallel=False)(_sensitivity_gravity) -_forward_gravity_parallel = jit(nopython=True, parallel=True)(_forward_gravity) -_forward_gravity_serial = jit(nopython=True, parallel=False)(_forward_gravity) diff --git a/simpeg/potential_fields/gravity/receivers.py b/simpeg/potential_fields/gravity/receivers.py index de54848cf5..c6318b021a 100644 --- a/simpeg/potential_fields/gravity/receivers.py +++ b/simpeg/potential_fields/gravity/receivers.py @@ -20,7 +20,7 @@ class Point(survey.BaseRx): .. important:: - Gradient components ("gxx", "gyy", "gzz", "gxy", "gxz", "gyz") are + Gradient components ("gxx", "gyy", "gzz", "gxy", "gxz", "gyz", "guv") are returned in Eotvos (:math:`10^{-9} s^{-2}`). Parameters @@ -41,7 +41,7 @@ class Point(survey.BaseRx): - "gyy" --> y-derivative of the y-component - "gyz" --> z-derivative of the y-component (and visa versa) - "gzz" --> z-derivative of the z-component - - "guv" --> UV component + - "guv" --> UV component, i.e., (gyy - gxx) / 2 See also -------- diff --git a/simpeg/potential_fields/gravity/simulation.py b/simpeg/potential_fields/gravity/simulation.py index 311b19dfc5..434ba52383 100644 --- a/simpeg/potential_fields/gravity/simulation.py +++ b/simpeg/potential_fields/gravity/simulation.py @@ -1,10 +1,12 @@ -import os +from __future__ import annotations +import hashlib import warnings import numpy as np -import discretize +from numpy.typing import NDArray import scipy.constants as constants from geoana.kernels import prism_fz, prism_fzx, prism_fzy, prism_fzz from scipy.constants import G as NewtG +from scipy.sparse.linalg import LinearOperator, aslinearoperator from simpeg import props from simpeg.utils import mkvc, sdiag @@ -12,13 +14,8 @@ from ...base import BasePDESimulation from ..base import BaseEquivalentSourceLayerSimulation, BasePFSimulation -from ._numba_functions import ( - choclo, - _sensitivity_gravity_serial, - _sensitivity_gravity_parallel, - _forward_gravity_serial, - _forward_gravity_parallel, -) +from ._numba import choclo, NUMBA_FUNCTIONS_3D, NUMBA_FUNCTIONS_2D + if choclo is not None: from numba import jit @@ -32,6 +29,48 @@ def kernel_uv(easting, northing, upward, radius): ) return result + @jit(nopython=True) + def gravity_uv( + easting, + northing, + upward, + prism_west, + prism_east, + prism_south, + prism_north, + prism_bottom, + prism_top, + density, + ): + """Forward model the Guv gradiometry component.""" + result = 0.5 * ( + choclo.prism.gravity_nn( + easting, + northing, + upward, + prism_west, + prism_east, + prism_south, + prism_north, + prism_bottom, + prism_top, + density, + ) + - choclo.prism.gravity_ee( + easting, + northing, + upward, + prism_west, + prism_east, + prism_south, + prism_north, + prism_bottom, + prism_top, + density, + ) + ) + return result + CHOCLO_KERNELS = { "gx": choclo.prism.kernel_e, "gy": choclo.prism.kernel_n, @@ -45,6 +84,19 @@ def kernel_uv(easting, northing, upward, radius): "guv": kernel_uv, } + CHOCLO_FORWARD_FUNCS = { + "gx": choclo.prism.gravity_e, + "gy": choclo.prism.gravity_n, + "gz": choclo.prism.gravity_u, + "gxx": choclo.prism.gravity_ee, + "gyy": choclo.prism.gravity_nn, + "gzz": choclo.prism.gravity_uu, + "gxy": choclo.prism.gravity_en, + "gxz": choclo.prism.gravity_eu, + "gyz": choclo.prism.gravity_nu, + "guv": gravity_uv, + } + def _get_conversion_factor(component): """ @@ -60,22 +112,25 @@ def _get_conversion_factor(component): class Simulation3DIntegral(BasePFSimulation): - """ + r""" Gravity simulation in integral form. - .. important:: - - Density model is assumed to be in g/cc. + .. note:: - .. important:: + The gravity simulation assumes the following units for its inputs and outputs: - Acceleration components ("gx", "gy", "gz") are returned in mgal - (:math:`10^{-5} m/s^2`). + - Density model is assumed to be in gram per cubic centimeter (g/cc). + - Acceleration components (``"gx"``, ``"gy"``, ``"gz"``) are returned in mgal + (:math:`10^{-5} \text{m}/\text{s}^2`). + - Gradient components (``"gxx"``, ``"gyy"``, ``"gzz"``, ``"gxy"``, ``"gxz"``, + ``"gyz"``, ``"guv"``) are returned in Eotvos (:math:`10^{-9} s^{-2}`). .. important:: - Gradient components ("gxx", "gyy", "gzz", "gxy", "gxz", "gyz") are - returned in Eotvos (:math:`10^{-9} s^{-2}`). + Following SimPEG convention for the right-handed xyz coordinate system, the + z axis points *upwards*. Therefore, the ``"gz"`` component corresponds to the + **upward** component of the gravity acceleration vector. + Parameters ---------- @@ -83,28 +138,29 @@ class Simulation3DIntegral(BasePFSimulation): Mesh use to run the gravity simulation. survey : simpeg.potential_fields.gravity.Survey Gravity survey with information of the receivers. - ind_active : (n_cells) numpy.ndarray, optional + active_cells : (n_cells) numpy.ndarray, optional Array that indicates which cells in ``mesh`` are active cells. - rho : numpy.ndarray (optional) + rho : numpy.ndarray, optional Density array for the active cells in the mesh. - rhoMap : Mapping (optional) + rhoMap : Mapping, optional Model mapping. sensitivity_dtype : numpy.dtype, optional Data type that will be used to build the sensitivity matrix. - store_sensitivities : str + store_sensitivities : {"ram", "disk", "forward_only"} Options for storing sensitivity matrix. There are 3 options - 'ram': sensitivities are stored in the computer's RAM - 'disk': sensitivities are written to a directory - 'forward_only': you intend only do perform a forward simulation and - sensitivities do not need to be stored + sensitivities do not need to be stored. The sensitivity matrix ``G`` + is never created, but it'll be defined as + a :class:`~scipy.sparse.linalg.LinearOperator`. sensitivity_path : str, optional Path to store the sensitivity matrix if ``store_sensitivities`` is set to ``"disk"``. Default to "./sensitivities". - engine : str, optional - Choose which engine should be used to run the forward model: - ``"geoana"`` or "``choclo``". + engine : {"geoana", "choclo"}, optional + Choose which engine should be used to run the forward model. numba_parallel : bool, optional If True, the simulation will run in parallel. If False, it will run in serial. If ``engine`` is not ``"choclo"`` this argument will be @@ -122,51 +178,11 @@ def __init__( numba_parallel=True, **kwargs, ): - super().__init__(mesh, **kwargs) + super().__init__(mesh, engine=engine, numba_parallel=numba_parallel, **kwargs) self.rho = rho self.rhoMap = rhoMap - self._G = None - self._gtg_diagonal = None self.modelMap = self.rhoMap - self.numba_parallel = numba_parallel - self.engine = engine - self._sanity_checks_engine(kwargs) - if self.engine == "choclo": - # Check dimensions of the mesh - if self.mesh.dim != 3: - raise ValueError( - f"Invalid mesh with {self.mesh.dim} dimensions. " - "Only 3D meshes are supported when using 'choclo' as engine." - ) - # Define jit functions - if numba_parallel: - self._sensitivity_gravity = _sensitivity_gravity_parallel - self._forward_gravity = _forward_gravity_parallel - else: - self._sensitivity_gravity = _sensitivity_gravity_serial - self._forward_gravity = _forward_gravity_serial - - def _sanity_checks_engine(self, kwargs): - """ - Sanity checks for the engine parameter. - Needs the kwargs passed to the __init__ method to raise some warnings. - Will set n_processes to None if it's present in kwargs. - """ - if self.engine not in ("choclo", "geoana"): - raise ValueError( - f"Invalid engine '{self.engine}'. Choose from 'geoana' or 'choclo'." - ) - if self.engine == "choclo" and choclo is None: - raise ImportError( - "The choclo package couldn't be found." - "Running a gravity simulation with 'engine=\"choclo\"' needs " - "choclo to be installed." - "\nTry installing choclo with:" - "\n pip install choclo" - "\nor:" - "\n conda install choclo" - ) # Warn if n_processes has been passed if self.engine == "choclo" and "n_processes" in kwargs: warnings.warn( @@ -176,15 +192,6 @@ def _sanity_checks_engine(self, kwargs): stacklevel=1, ) self.n_processes = None - # Sanity checks for sensitivity_path when using choclo and storing in disk - if self.engine == "choclo" and self.store_sensitivities == "disk": - if os.path.isdir(self.sensitivity_path): - raise ValueError( - f"The passed sensitivity_path '{self.sensitivity_path}' is " - "a directory. " - "When using 'choclo' as the engine, 'senstivity_path' " - "should be the path to a new or existing file." - ) def fields(self, m): """ @@ -192,8 +199,8 @@ def fields(self, m): Parameters ---------- - m : (n_active_cells,) numpy.ndarray - Array with values for the model. + m : (n_param,) numpy.ndarray + The model parameters. Returns ------- @@ -201,6 +208,7 @@ def fields(self, m): Gravity fields generated by the given model on every receiver location. """ + # Need to assign the model, so the rho property can be accessed. self.model = m if self.store_sensitivities == "forward_only": # Compute the linear operation without forming the full dense G @@ -213,66 +221,227 @@ def fields(self, m): return np.asarray(fields) def getJtJdiag(self, m, W=None, f=None): + r""" + Compute diagonal of :math:`\mathbf{J}^T \mathbf{J}``. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + W : (nD, nD) np.ndarray or scipy.sparse.sparray, optional + Diagonal matrix with the square root of the weights. If not None, + the function returns the diagonal of + :math:`\mathbf{J}^T \mathbf{W}^T \mathbf{W} \mathbf{J}``. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (n_active_cells) np.ndarray + Array with the diagonal of ``J.T @ J``. + + Notes + ----- + If ``store_sensitivities`` is ``"forward_only"``, the ``G`` matrix is + never allocated in memory, and the diagonal is obtained by + accumulation, computing each element of the ``G`` matrix on the fly. + + This method caches the diagonal ``G.T @ W.T @ W @ G`` and the sha256 + hash of the diagonal of the ``W`` matrix. This way, if same weights are + passed to it, it reuses the cached diagonal so it doesn't need to be + recomputed. + If new weights are passed, the cache is updated with the latest + diagonal of ``G.T @ W.T @ W @ G``. """ - Return the diagonal of JtJ - """ + # Need to assign the model, so the rhoDeriv can be computed (if the + # model is None, the rhoDeriv is going to be Zero). self.model = m - if W is None: - W = np.ones(self.survey.nD) - else: - W = W.diagonal() ** 2 - if getattr(self, "_gtg_diagonal", None) is None: - diag = np.zeros(self.G.shape[1]) - for i in range(len(W)): - diag += W[i] * (self.G[i] * self.G[i]) - self._gtg_diagonal = diag - else: - diag = self._gtg_diagonal - return mkvc((sdiag(np.sqrt(diag)) @ self.rhoDeriv).power(2).sum(axis=0)) + # We should probably check that W is diagonal. Let's assume it for now. + weights = ( + W.diagonal() ** 2 + if W is not None + else np.ones(self.survey.nD, dtype=np.float64) + ) + + # Compute gtg (G.T @ W.T @ W @ G) if it's not cached, or if the + # weights are not the same. + weights_sha256 = hashlib.sha256(weights) + use_cached_gtg = ( + hasattr(self, "_gtg_diagonal") + and hasattr(self, "_weights_sha256") + and self._weights_sha256.digest() == weights_sha256.digest() + ) + if not use_cached_gtg: + self._gtg_diagonal = self._get_gtg_diagonal(weights) + self._weights_sha256 = weights_sha256 + + # Multiply the gtg_diagonal by the derivative of the mapping + diagonal = mkvc( + (sdiag(np.sqrt(self._gtg_diagonal)) @ self.rhoDeriv).power(2).sum(axis=0) + ) + return diagonal - def getJ(self, m, f=None): + def _get_gtg_diagonal(self, weights: NDArray) -> NDArray: """ - Sensitivity matrix + Compute the diagonal of ``G.T @ W.T @ W @ G``. + + Parameters + ---------- + weights : np.ndarray + Weights array: diagonal of ``W.T @ W``. + + Returns + ------- + np.ndarray + """ + match self.store_sensitivities, self.engine: + case ("forward_only", "geoana"): + msg = ( + "Computing the diagonal of G.T @ G with " + 'store_sensitivities="forward_only" and engine="geoana" ' + "hasn't been implemented yet. " + 'Choose store_sensitivities="ram" or "disk", ' + 'or another engine, like "choclo".' + ) + raise NotImplementedError(msg) + case ("forward_only", "choclo"): + gtg_diagonal = self._gtg_diagonal_without_building_g(weights) + case (_, _): + # In Einstein notation, the j-th element of the diagonal is: + # d_j = w_i * G_{ij} * G_{ij} + gtg_diagonal = np.asarray( + np.einsum("i,ij,ij->j", weights, self.G, self.G) + ) + return gtg_diagonal + + def getJ(self, m, f=None) -> NDArray[np.float64 | np.float32] | LinearOperator: + r""" + Sensitivity matrix :math:`\mathbf{J}`. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (nD, n_active_cells) np.ndarray or scipy.sparse.linalg.LinearOperator. + Array or :class:`~scipy.sparse.linalg.LinearOperator` for the + :math:`\mathbf{J}` matrix. + A :class:`~scipy.sparse.linalg.LinearOperator` will be returned if + ``store_sensitivities`` is ``"forward_only"``, otherwise a dense + array will be returned. + + Notes + ----- + If ``store_sensitivities`` is ``"ram"`` or ``"disk"``, a dense array + for the ``J`` matrix is returned. + A :class:`~scipy.sparse.linalg.LinearOperator` is returned if + ``store_sensitivities`` is ``"forward_only"``. This object can perform + operations like ``J @ m`` or ``J.T @ v`` without allocating the full + ``J`` matrix in memory. """ - return self.G.dot(self.rhoDeriv) + # Need to assign the model, so the rhoDeriv can be computed (if the + # model is None, the rhoDeriv is going to be Zero). + self.model = m + rhoDeriv = ( + self.rhoDeriv + if not isinstance(self.G, LinearOperator) + else aslinearoperator(self.rhoDeriv) + ) + return self.G @ rhoDeriv def Jvec(self, m, v, f=None): """ - Sensitivity times a vector + Dot product between sensitivity matrix and a vector. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. This array is used to compute the ``J`` + matrix. + v : (n_param,) numpy.ndarray + Vector used in the matrix-vector multiplication. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (nD,) numpy.ndarray + + Notes + ----- + If ``store_sensitivities`` is set to ``"forward_only"``, then the + matrix `G` is never fully constructed, and the dot product is computed + by accumulation, computing the matrix elements on the fly. Otherwise, + the full matrix ``G`` is constructed and stored either in memory or + disk. """ + # Need to assign the model, so the rhoDeriv can be computed (if the + # model is None, the rhoDeriv is going to be Zero). + self.model = m dmu_dm_v = self.rhoDeriv @ v return self.G @ dmu_dm_v.astype(self.sensitivity_dtype, copy=False) def Jtvec(self, m, v, f=None): """ - Sensitivity transposed times a vector + Dot product between transposed sensitivity matrix and a vector. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. This array is used to compute the ``J`` + matrix. + v : (nD,) numpy.ndarray + Vector used in the matrix-vector multiplication. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (nD,) numpy.ndarray + + Notes + ----- + If ``store_sensitivities`` is set to ``"forward_only"``, then the + matrix `G` is never fully constructed, and the dot product is computed + by accumulation, computing the matrix elements on the fly. Otherwise, + the full matrix ``G`` is constructed and stored either in memory or + disk. """ + # Need to assign the model, so the rhoDeriv can be computed (if the + # model is None, the rhoDeriv is going to be Zero). + self.model = m Jtvec = self.G.T @ v.astype(self.sensitivity_dtype, copy=False) return np.asarray(self.rhoDeriv.T @ Jtvec) @property - def G(self): + def G(self) -> NDArray | np.memmap | LinearOperator: """ - Gravity forward operator + Gravity forward operator. """ - if getattr(self, "_G", None) is None: - if self.engine == "choclo": - self._G = self._sensitivity_matrix() - else: - self._G = self.linear_operator() + if not hasattr(self, "_G"): + match self.engine, self.store_sensitivities: + case ("choclo", "forward_only"): + self._G = self._sensitivity_matrix_as_operator() + case ("choclo", _): + self._G = self._sensitivity_matrix() + case ("geoana", "forward_only"): + msg = ( + "Accessing matrix G with " + 'store_sensitivities="forward_only" and engine="geoana" ' + "hasn't been implemented yet. " + 'Choose store_sensitivities="ram" or "disk", ' + 'or another engine, like "choclo".' + ) + raise NotImplementedError(msg) + case ("geoana", _): + self._G = self.linear_operator() return self._G - @property - def gtg_diagonal(self): - """ - Diagonal of GtG - """ - if getattr(self, "_gtg_diagonal", None) is None: - return None - - return self._gtg_diagonal - def evaluate_integral(self, receiver_location, components): """ Compute the forward linear relationship between the model and the physics at a point @@ -384,6 +553,8 @@ def _forward(self, densities): (nD,) numpy.ndarray Always return a ``np.float64`` array. """ + # Get Numba function + forward_func = NUMBA_FUNCTIONS_3D["forward"][self.numba_parallel] # Gather active nodes and the indices of the nodes for each active cell active_nodes, active_cell_nodes = self._get_active_nodes() # Allocate fields array @@ -399,7 +570,7 @@ def _forward(self, densities): vector_slice = slice( index_offset + i, index_offset + n_elements, n_components ) - self._forward_gravity( + forward_func( receivers, active_nodes, densities, @@ -413,12 +584,14 @@ def _forward(self, densities): def _sensitivity_matrix(self): """ - Compute the sensitivity matrix G + Compute the sensitivity matrix ``G``. Returns ------- (nD, n_active_cells) numpy.ndarray """ + # Get Numba function + sensitivity_func = NUMBA_FUNCTIONS_3D["sensitivity"][self.numba_parallel] # Gather active nodes and the indices of the nodes for each active cell active_nodes, active_cell_nodes = self._get_active_nodes() # Allocate sensitivity matrix @@ -444,7 +617,7 @@ def _sensitivity_matrix(self): matrix_slice = slice( index_offset + i, index_offset + n_rows, n_components ) - self._sensitivity_gravity( + sensitivity_func( receivers, active_nodes, sensitivity_matrix[matrix_slice, :], @@ -455,44 +628,100 @@ def _sensitivity_matrix(self): index_offset += n_rows return sensitivity_matrix - def _get_active_nodes(self): + def _sensitivity_matrix_transpose_dot_vec(self, vector): """ - Return locations of nodes only for active cells + Compute ``G.T @ v`` without building ``G``. + + Parameters + ---------- + vector : (nD) numpy.ndarray + Vector used in the dot product. - Also return an array containing the indices of the "active nodes" for - each active cell in the mesh + Returns + ------- + (n_active_cells) numpy.ndarray """ - # Get all nodes in the mesh - if isinstance(self.mesh, discretize.TreeMesh): - nodes = self.mesh.total_nodes - elif isinstance(self.mesh, discretize.TensorMesh): - nodes = self.mesh.nodes - else: - raise TypeError(f"Invalid mesh of type {self.mesh.__class__.__name__}.") - # Get original cell_nodes but only for active cells - cell_nodes = self.mesh.cell_nodes - # If all cells in the mesh are active, return nodes and cell_nodes - if self.nC == self.mesh.n_cells: - return nodes, cell_nodes - # Keep only the cell_nodes for active cells - cell_nodes = cell_nodes[self.ind_active] - # Get the unique indices of the nodes that belong to every active cell - # (these indices correspond to the original `nodes` array) - unique_nodes, active_cell_nodes = np.unique(cell_nodes, return_inverse=True) - # Select only the nodes that belong to the active cells (active nodes) - active_nodes = nodes[unique_nodes] - # Reshape indices of active cell nodes for each active cell in the mesh - active_cell_nodes = active_cell_nodes.reshape(cell_nodes.shape) - return active_nodes, active_cell_nodes - - def _get_components_and_receivers(self): - """Generator for receiver locations and their field components.""" - if not hasattr(self.survey, "source_field"): - raise AttributeError( - f"The survey '{self.survey}' has no 'source_field' attribute." - ) - for receiver_object in self.survey.source_field.receiver_list: - yield receiver_object.components, receiver_object.locations + # Get Numba function + sensitivity_t_dot_v_func = NUMBA_FUNCTIONS_3D["gt_dot_v"][self.numba_parallel] + # Gather active nodes and the indices of the nodes for each active cell + active_nodes, active_cell_nodes = self._get_active_nodes() + # Allocate resulting array + result = np.zeros(self.nC) + # Start filling the result array + index_offset = 0 + for components, receivers in self._get_components_and_receivers(): + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + kernel_func = CHOCLO_KERNELS[component] + conversion_factor = _get_conversion_factor(component) + vector_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + sensitivity_t_dot_v_func( + receivers, + active_nodes, + active_cell_nodes, + kernel_func, + constants.G * conversion_factor, + vector[vector_slice], + result, + ) + index_offset += n_rows + return result + + def _sensitivity_matrix_as_operator(self): + """ + Create a LinearOperator for the sensitivity matrix G. + + Returns + ------- + scipy.sparse.linalg.LinearOperator + """ + shape = (self.survey.nD, self.nC) + linear_op = LinearOperator( + shape=shape, + matvec=self._forward, + rmatvec=self._sensitivity_matrix_transpose_dot_vec, + dtype=np.float64, + ) + return linear_op + + def _gtg_diagonal_without_building_g(self, weights): + """ + Compute the diagonal of ``G.T @ G`` without building the ``G`` matrix. + + Parameters + ----------- + weights : (nD,) array + Array with data weights. It should be the diagonal of the ``W`` + matrix, squared. + + Returns + ------- + (n_active_cells) numpy.ndarray + """ + # Get Numba function + diagonal_gtg_func = NUMBA_FUNCTIONS_3D["diagonal_gtg"][self.numba_parallel] + # Gather active nodes and the indices of the nodes for each active cell + active_nodes, active_cell_nodes = self._get_active_nodes() + # Allocate array for the diagonal of G.T @ G + diagonal = np.zeros(self.nC, dtype=np.float64) + # Start filling the diagonal array + for components, receivers in self._get_components_and_receivers(): + for component in components: + kernel_func = CHOCLO_KERNELS[component] + conversion_factor = _get_conversion_factor(component) + diagonal_gtg_func( + receivers, + active_nodes, + active_cell_nodes, + kernel_func, + constants.G * conversion_factor, + weights, + diagonal, + ) + return diagonal class SimulationEquivalentSourceLayer( @@ -506,13 +735,209 @@ class SimulationEquivalentSourceLayer( mesh : discretize.BaseMesh A 2D tensor or tree mesh defining discretization along the x and y directions cell_z_top : numpy.ndarray or float - Define the elevations for the top face of all cells in the layer. If an array it should be the same size as - the active cell set. + Define the elevations for the top face of all cells in the layer. + If an array it should be the same size as the active cell set. cell_z_bottom : numpy.ndarray or float - Define the elevations for the bottom face of all cells in the layer. If an array it should be the same size as - the active cell set. + Define the elevations for the bottom face of all cells in the layer. + If an array it should be the same size as the active cell set. + engine : {"geoana", "choclo"}, optional + Choose which engine should be used to run the forward model. + numba_parallel : bool, optional + If True, the simulation will run in parallel. If False, it will + run in serial. If ``engine`` is not ``"choclo"`` this argument will be + ignored. """ + def __init__( + self, + mesh, + cell_z_top, + cell_z_bottom, + engine="geoana", + numba_parallel=True, + **kwargs, + ): + super().__init__( + mesh, + cell_z_top, + cell_z_bottom, + engine=engine, + numba_parallel=numba_parallel, + **kwargs, + ) + + def _forward(self, densities): + """ + Forward model the fields of active cells in the mesh on receivers. + + Parameters + ---------- + densities : (n_active_cells) numpy.ndarray + Array containing the densities of the active cells in the mesh, in + g/cc. + + Returns + ------- + (nD,) numpy.ndarray + Always return a ``np.float64`` array. + """ + # Get Numba function + forward_func = NUMBA_FUNCTIONS_2D["forward"][self.numba_parallel] + # Get cells in the 2D mesh and keep only active cells + cells_bounds_active = self.mesh.cell_bounds[self.active_cells] + # Allocate fields array + fields = np.zeros(self.survey.nD, dtype=self.sensitivity_dtype) + # Compute fields + index_offset = 0 + for components, receivers in self._get_components_and_receivers(): + n_components = len(components) + n_elements = n_components * receivers.shape[0] + for i, component in enumerate(components): + choclo_forward_func = CHOCLO_FORWARD_FUNCS[component] + conversion_factor = _get_conversion_factor(component) + vector_slice = slice( + index_offset + i, index_offset + n_elements, n_components + ) + forward_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + densities, + fields[vector_slice], + choclo_forward_func, + conversion_factor, + ) + index_offset += n_elements + return fields + + def _sensitivity_matrix(self): + """ + Compute the sensitivity matrix G + + Returns + ------- + (nD, n_active_cells) numpy.ndarray + """ + # Get Numba function + sensitivity_func = NUMBA_FUNCTIONS_2D["sensitivity"][self.numba_parallel] + # Get cells in the 2D mesh and keep only active cells + cells_bounds_active = self.mesh.cell_bounds[self.active_cells] + # Allocate sensitivity matrix + shape = (self.survey.nD, self.nC) + if self.store_sensitivities == "disk": + sensitivity_matrix = np.memmap( + self.sensitivity_path, + shape=shape, + dtype=self.sensitivity_dtype, + order="C", # it's more efficient to write in row major + mode="w+", + ) + else: + sensitivity_matrix = np.empty(shape, dtype=self.sensitivity_dtype) + # Start filling the sensitivity matrix + index_offset = 0 + for components, receivers in self._get_components_and_receivers(): + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + choclo_forward_func = CHOCLO_FORWARD_FUNCS[component] + conversion_factor = _get_conversion_factor(component) + matrix_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + sensitivity_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + sensitivity_matrix[matrix_slice, :], + choclo_forward_func, + conversion_factor, + ) + index_offset += n_rows + return sensitivity_matrix + + def _sensitivity_matrix_transpose_dot_vec(self, vector): + """ + Compute ``G.T @ v`` without building ``G``. + + Parameters + ---------- + vector : (nD) numpy.ndarray + Vector used in the dot product. + + Returns + ------- + (n_active_cells) numpy.ndarray + """ + # Get Numba function + g_t_dot_v_func = NUMBA_FUNCTIONS_2D["gt_dot_v"][self.numba_parallel] + # Get cells in the 2D mesh and keep only active cells + cells_bounds_active = self.mesh.cell_bounds[self.active_cells] + # Allocate resulting array + result = np.zeros(self.nC) + # Start filling the result array + index_offset = 0 + for components, receivers in self._get_components_and_receivers(): + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + choclo_forward_func = CHOCLO_FORWARD_FUNCS[component] + conversion_factor = _get_conversion_factor(component) + vector_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + g_t_dot_v_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + choclo_forward_func, + conversion_factor, + vector[vector_slice], + result, + ) + index_offset += n_rows + return result + + def _gtg_diagonal_without_building_g(self, weights): + """ + Compute the diagonal of ``G.T @ G`` without building the ``G`` matrix. + + Parameters + ----------- + weights : (nD,) array + Array with data weights. It should be the diagonal of the ``W`` + matrix, squared. + + Returns + ------- + (n_active_cells) numpy.ndarray + """ + # Get Numba function + diagonal_gtg_func = NUMBA_FUNCTIONS_2D["diagonal_gtg"][self.numba_parallel] + # Get cells in the 2D mesh and keep only active cells + cells_bounds_active = self.mesh.cell_bounds[self.active_cells] + # Allocate array for the diagonal of G.T @ G + diagonal = np.zeros(self.nC, dtype=np.float64) + # Start filling the diagonal array + for components, receivers in self._get_components_and_receivers(): + for component in components: + choclo_forward_func = CHOCLO_FORWARD_FUNCS[component] + conversion_factor = _get_conversion_factor(component) + diagonal_gtg_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + choclo_forward_func, + conversion_factor, + weights, + diagonal, + ) + return diagonal + class Simulation3DDifferential(BasePDESimulation): r"""Finite volume simulation class for gravity. diff --git a/simpeg/potential_fields/gravity/survey.py b/simpeg/potential_fields/gravity/survey.py index 2808e2489a..7a9cb84c2a 100644 --- a/simpeg/potential_fields/gravity/survey.py +++ b/simpeg/potential_fields/gravity/survey.py @@ -1,5 +1,5 @@ from ...survey import BaseSurvey -from ...utils.code_utils import validate_type +from ...utils.code_utils import validate_list_of_types from .sources import SourceField @@ -13,10 +13,34 @@ class Survey(BaseSurvey): """ def __init__(self, source_field, **kwargs): - self.source_field = validate_type( - "source_field", source_field, SourceField, cast=False + if "source_list" in kwargs: + msg = ( + "source_list is not a valid argument to gravity.Survey. " + "Use source_field instead." + ) + raise TypeError(msg) + super().__init__(source_list=source_field, **kwargs) + + @BaseSurvey.source_list.setter + def source_list(self, new_list): + new_list = validate_list_of_types( + "source_list", new_list, SourceField, ensure_unique=True, min_n=1, max_n=1 ) - super().__init__(source_list=None, **kwargs) + self._source_list = new_list + + @property + def source_field(self): + """A source object that contains the gravity receivers. + + Returns + ------- + simpeg.potential_fields.gravity.sources.SourceField + """ + return self.source_list[0] + + @source_field.setter + def source_field(self, new_src): + self.source_list = new_src def eval(self, fields): # noqa: A003 """Evaluate the field @@ -74,17 +98,6 @@ def nD(self): """ return sum(receiver.nD for receiver in self.source_field.receiver_list) - @property - def components(self): - """Number of components measured at each receiver. - - Returns - ------- - int - Number of components measured at each receiver. - """ - return self.source_field.receiver_list[0].components - def _location_component_iterator(self): for rx in self.source_field.receiver_list: for loc in rx.locations: diff --git a/simpeg/potential_fields/magnetics/__init__.py b/simpeg/potential_fields/magnetics/__init__.py index 52612898b8..b757d912ed 100644 --- a/simpeg/potential_fields/magnetics/__init__.py +++ b/simpeg/potential_fields/magnetics/__init__.py @@ -48,5 +48,5 @@ Simulation3DDifferential, ) from .survey import Survey -from .sources import SourceField, UniformBackgroundField +from .sources import UniformBackgroundField from .receivers import Point diff --git a/simpeg/potential_fields/magnetics/_numba/_2d_mesh.py b/simpeg/potential_fields/magnetics/_numba/_2d_mesh.py new file mode 100644 index 0000000000..cb75bc8285 --- /dev/null +++ b/simpeg/potential_fields/magnetics/_numba/_2d_mesh.py @@ -0,0 +1,2279 @@ +""" +Numba functions for magnetic simulation of rectangular prisms on 2D meshes. + +These functions assumes 3D prisms formed by a 2D mesh plus top and bottom boundaries for +each prism. +""" + +import numpy as np + +try: + import choclo +except ImportError: + # Define dummy jit decorator + def jit(*args, **kwargs): + return lambda f: f + + choclo = None +else: + from numba import jit, prange + +from ..._numba_utils import evaluate_kernels_on_cell, evaluate_six_kernels_on_cell + + +def _forward_mag( + receivers, + cells_bounds, + top, + bottom, + model, + fields, + regional_field, + forward_func, + scalar_model, +): + """ + Forward model single magnetic component for 2D meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_forward = jit(nopython=True, parallel=True)(_forward_mag) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + model : (n_active_cells) or (3 * n_active_cells) array + Array containing the susceptibilities (scalar) or effective + susceptibilities (vector) of the active cells in the mesh, in SI + units. + Susceptibilities are expected if ``scalar_model`` is True, + and the array should have ``n_active_cells`` elements. + Effective susceptibilities are expected if ``scalar_model`` is False, + and the array should have ``3 * n_active_cells`` elements. + fields : (n_receivers) array + Array full of zeros where the magnetic component on each receiver will + be stored. This could be a preallocated array or a slice of it. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + forward_func : callable + Forward function that will be evaluated on each node of the mesh. Choose + one of the forward functions in ``choclo.prism``. + scalar_model : bool + If True, the forward will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the forward will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + + Notes + ----- + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Forward model the magnetic component of each cell on each receiver location + for i in prange(n_receivers): + for j in range(n_cells): + # Define magnetization vector of the cell + # (we we'll divide by mu_0 when adding the forward modelled field) + if scalar_model: + # model is susceptibility, so the vector is parallel to the + # regional field + magnetization_x = model[j] * fx + magnetization_y = model[j] * fy + magnetization_z = model[j] * fz + else: + # model is effective susceptibility (vector) + magnetization_x = model[j] + magnetization_y = model[j + n_cells] + magnetization_z = model[j + 2 * n_cells] + # Forward the magnetic component + fields[i] += ( + regional_field_amplitude + * forward_func( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + magnetization_x, + magnetization_y, + magnetization_z, + ) + / choclo.constants.VACUUM_MAGNETIC_PERMEABILITY + ) + + +def _forward_tmi( + receivers, + cells_bounds, + top, + bottom, + model, + fields, + regional_field, + scalar_model, +): + """ + Forward model the TMI for 2D meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_forward = jit(nopython=True, parallel=True)(_forward_tmi) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + model : (n_active_cells) or (3 * n_active_cells) + Array with the susceptibility (scalar model) or the effective + susceptibility (vector model) of each active cell in the mesh. + If the model is scalar, the ``model`` array should have + ``n_active_cells`` elements and ``scalar_model`` should be True. + If the model is vector, the ``model`` array should have + ``3 * n_active_cells`` elements and ``scalar_model`` should be False. + fields : (n_receivers) array + Array full of zeros where the TMI on each receiver will be stored. This + could be a preallocated array or a slice of it. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + scalar_model : bool + If True, the forward will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the forward will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + + Notes + ----- + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Forward model the magnetic component of each cell on each receiver location + for i in prange(n_receivers): + for j in range(n_cells): + # Define magnetization vector of the cell + # (we we'll divide by mu_0 when adding the forward modelled field) + if scalar_model: + # model is susceptibility, so the vector is parallel to the + # regional field + magnetization_x = model[j] * fx + magnetization_y = model[j] * fy + magnetization_z = model[j] * fz + else: + # model is effective susceptibility (vector) + magnetization_x = model[j] + magnetization_y = model[j + n_cells] + magnetization_z = model[j + 2 * n_cells] + # Forward the magnetic field vector and compute tmi + bx, by, bz = choclo.prism.magnetic_field( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + magnetization_x, + magnetization_y, + magnetization_z, + ) + fields[i] += ( + regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + / choclo.constants.VACUUM_MAGNETIC_PERMEABILITY + ) + + +def _forward_tmi_derivative( + receivers, + cells_bounds, + top, + bottom, + model, + fields, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + scalar_model, +): + r""" + Forward model a TMI derivative for 2D meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_forward = jit(nopython=True, parallel=True)(_forward_tmi_derivative) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + model : (n_active_cells) or (3 * n_active_cells) + Array with the susceptibility (scalar model) or the effective + susceptibility (vector model) of each active cell in the mesh. + If the model is scalar, the ``model`` array should have + ``n_active_cells`` elements and ``scalar_model`` should be True. + If the model is vector, the ``model`` array should have + ``3 * n_active_cells`` elements and ``scalar_model`` should be False. + fields : (n_receivers) array + Array full of zeros where the TMI on each receiver will be stored. This + could be a preallocated array or a slice of it. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + scalar_model : bool + If True, the forward will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the forward will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + + Notes + ----- + + About the kernel functions + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + To compute the :math:`\alpha` derivative of the TMI :math:`\Delta T` (with + :math:`\alpha \in \{x, y, z\}` we need to evaluate third order kernels + functions for the prism. The kernels we need to evaluate can be obtained by + fixing one of the subindices to the direction of the derivative + (:math:`\alpha`) and cycle through combinations of the other two. + + For ``tmi_x`` we need to pass: + + .. code:: + + kernel_xx=kernel_eee, kernel_yy=kernel_enn, kernel_zz=kernel_euu, + kernel_xy=kernel_een, kernel_xz=kernel_eeu, kernel_yz=kernel_enu + + For ``tmi_y`` we need to pass: + + .. code:: + + kernel_xx=kernel_een, kernel_yy=kernel_nnn, kernel_zz=kernel_nuu, + kernel_xy=kernel_enn, kernel_xz=kernel_enu, kernel_yz=kernel_nnu + + For ``tmi_z`` we need to pass: + + .. code:: + + kernel_xx=kernel_eeu, kernel_yy=kernel_nnu, kernel_zz=kernel_uuu, + kernel_xy=kernel_enu, kernel_xz=kernel_euu, kernel_yz=kernel_nuu + + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + """ + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Forward model the magnetic component of each cell on each receiver location + for i in prange(n_receivers): + for j in range(n_cells): + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + ) + if scalar_model: + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + fields[i] += ( + model[j] + * regional_field_amplitude + * (fx * bx + fy * by + fz * bz) + / (4 * np.pi) + ) + else: + model_x = model[j] + model_y = model[j + n_cells] + model_z = model[j + 2 * n_cells] + bx = uxx * model_x + uxy * model_y + uxz * model_z + by = uxy * model_x + uyy * model_y + uyz * model_z + bz = uxz * model_x + uyz * model_y + uzz * model_z + fields[i] += ( + regional_field_amplitude * (bx * fx + by * fy + bz * fz) / 4 / np.pi + ) + + +def _sensitivity_mag( + receivers, + cells_bounds, + top, + bottom, + sensitivity_matrix, + regional_field, + kernel_x, + kernel_y, + kernel_z, + scalar_model, +): + r""" + Fill the sensitivity matrix for single mag component for 2d meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_sensitivity = jit(nopython=True, parallel=True)(_sensitivity_mag) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + sensitivity_matrix : array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + The array should have a shape of ``(n_receivers, n_active_cells)`` + if ``scalar_model`` is True. + The array should have a shape of ``(n_receivers, 3 * n_active_cells)`` + if ``scalar_model`` is False. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + scalar_model : bool + If True, the sensitivity matrix is built to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is built to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the kernel functions + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + For computing the ``bx`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_ee, kernel_y=kernel_en, kernel_z=kernel_eu + + + For computing the ``by`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_en, kernel_y=kernel_nn, kernel_z=kernel_nu + + For computing the ``bz`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_eu, kernel_y=kernel_nu, kernel_z=kernel_uu + + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + About the sensitivity matrix + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Each row of the sensitivity matrix corresponds to a single receiver + location. + + If ``scalar_model`` is True, then each element of the row will + correspond to the partial derivative of the selected magnetic component + with respect to the susceptibility of each cell in the mesh. + + If ``scalar_model`` is False, then each row can be split in three sections + containing: + + * the partial derivatives of the selected magnetic component with respect + to the _x_ component of the effective susceptibility of each cell; then + * the partial derivatives of the selected magnetic component with respect + to the _y_ component of the effective susceptibility of each cell; and then + * the partial derivatives of the selected magnetic component with respect + to the _z_ component of the effective susceptibility of each cell. + + So, if we call :math:`B_j` the magnetic field component on the receiver + :math:`j`, and :math:`\bar{\chi}^{(i)} = (\chi_x^{(i)}, \chi_y^{(i)}, + \chi_z^{(i)})` the effective susceptibility of the active cell :math:`i`, + then each row of the sensitivity matrix will be: + + .. math:: + + \left[ + \frac{\partial B_j}{\partial \chi_x^{(1)}}, + \dots, + \frac{\partial B_j}{\partial \chi_x^{(N)}}, + \frac{\partial B_j}{\partial \chi_y^{(1)}}, + \dots, + \frac{\partial B_j}{\partial \chi_y^{(N)}}, + \frac{\partial B_j}{\partial \chi_z^{(1)}}, + \dots, + \frac{\partial B_j}{\partial \chi_z^{(N)}} + \right] + + where :math:`N` is the total number of active cells. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + # Fill the sensitivity matrix + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + for i in prange(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + ux, uy, uz = evaluate_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_x, + kernel_y, + kernel_z, + ) + if scalar_model: + sensitivity_matrix[i, j] = ( + constant_factor + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + else: + sensitivity_matrix[i, j] = ( + constant_factor * regional_field_amplitude * ux + ) + sensitivity_matrix[i, j + n_cells] = ( + constant_factor * regional_field_amplitude * uy + ) + sensitivity_matrix[i, j + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * uz + ) + + +def _sensitivity_tmi( + receivers, + cells_bounds, + top, + bottom, + sensitivity_matrix, + regional_field, + scalar_model, +): + r""" + Fill the sensitivity matrix TMI for 2d meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_tmi = jit(nopython=True, parallel=True)(_sensitivity_tmi) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + sensitivity_matrix : array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + The array should have a shape of ``(n_receivers, n_active_cells)`` + if ``scalar_model`` is True. + The array should have a shape of ``(n_receivers, 3 * n_active_cells)`` + if ``scalar_model`` is False. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + About the sensitivity matrix + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Each row of the sensitivity matrix corresponds to a single receiver + location. + + If ``scalar_model`` is True, then each element of the row will + correspond to the partial derivative of the tmi + with respect to the susceptibility of each cell in the mesh. + + If ``scalar_model`` is False, then each row can be split in three sections + containing: + + * the partial derivatives of the tmi with respect + to the _x_ component of the effective susceptibility of each cell; then + * the partial derivatives of the tmi with respect + to the _y_ component of the effective susceptibility of each cell; and then + * the partial derivatives of the tmi with respect + to the _z_ component of the effective susceptibility of each cell. + + So, if we call :math:`T_j` the tmi on the receiver + :math:`j`, and :math:`\bar{\chi}^{(i)} = (\chi_x^{(i)}, \chi_y^{(i)}, + \chi_z^{(i)})` the effective susceptibility of the active cell :math:`i`, + then each row of the sensitivity matrix will be: + + .. math:: + + \left[ + \frac{\partial T_j}{\partial \chi_x^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_x^{(N)}}, + \frac{\partial T_j}{\partial \chi_y^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_y^{(N)}}, + \frac{\partial T_j}{\partial \chi_z^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_z^{(N)}} + \right] + + where :math:`N` is the total number of active cells. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + # Fill the sensitivity matrix + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + for i in prange(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + choclo.prism.kernel_ee, + choclo.prism.kernel_nn, + choclo.prism.kernel_uu, + choclo.prism.kernel_en, + choclo.prism.kernel_eu, + choclo.prism.kernel_nu, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: + sensitivity_matrix[i, j] = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + sensitivity_matrix[i, j] = ( + constant_factor * regional_field_amplitude * bx + ) + sensitivity_matrix[i, j + n_cells] = ( + constant_factor * regional_field_amplitude * by + ) + sensitivity_matrix[i, j + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * bz + ) + + +def _sensitivity_tmi_derivative( + receivers, + cells_bounds, + top, + bottom, + sensitivity_matrix, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + scalar_model, +): + r""" + Fill the sensitivity matrix TMI for 2d meshes. + + This function is designed to be used with equivalent sources, where the + mesh is a 2D mesh (prism layer). The top and bottom boundaries of each cell + are passed through the ``top`` and ``bottom`` arrays. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_tmi = jit(nopython=True, parallel=True)(_sensitivity_tmi_derivative) + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + sensitivity_matrix : array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + The array should have a shape of ``(n_receivers, n_active_cells)`` + if ``scalar_model`` is True. + The array should have a shape of ``(n_receivers, 3 * n_active_cells)`` + if ``scalar_model`` is False. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + # Fill the sensitivity matrix + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + for i in prange(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: + sensitivity_matrix[i, j] = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + sensitivity_matrix[i, j] = ( + constant_factor * regional_field_amplitude * bx + ) + sensitivity_matrix[i, j + n_cells] = ( + constant_factor * regional_field_amplitude * by + ) + sensitivity_matrix[i, j + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * bz + ) + + +@jit(nopython=True, parallel=False) +def _tmi_sensitivity_t_dot_v_serial( + receivers, + cells_bounds, + top, + bottom, + regional_field, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` for TMI on 2d meshes, in serial. + + This function doesn't allocates the ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in serial. Writing to the ``result`` array + inside a parallel loop over the receivers generates a race condition that + leads to corrupted outputs. + + A parallel implementation of this function is available in + ``_tmi_sensitivity_t_dot_v_parallel``. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in range(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + choclo.prism.kernel_ee, + choclo.prism.kernel_nn, + choclo.prism.kernel_uu, + choclo.prism.kernel_en, + choclo.prism.kernel_eu, + choclo.prism.kernel_nu, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: + result[j] += ( + constant_factor + * vector[i] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + result[j] += constant_factor * vector[i] * regional_field_amplitude * bx + result[j + n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * by + ) + result[j + 2 * n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * bz + ) + + +@jit(nopython=True, parallel=True) +def _tmi_sensitivity_t_dot_v_parallel( + receivers, + cells_bounds, + top, + bottom, + regional_field, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` for TMI on 2d meshes, in parallel. + + This function doesn't allocates the ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in parallel. + This implementation instructs each thread to allocate their own array for + the current row of the sensitivity matrix. After computing the elements of + that row, it gets added to the running ``result`` array through a reduction + operation handled by Numba. + + A parallel implementation of this function is available in + ``_tmi_sensitivity_t_dot_v_serial``. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + result_size = result.size + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate array for the current row of the sensitivity matrix + local_row = np.empty(result_size) + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + choclo.prism.kernel_ee, + choclo.prism.kernel_nn, + choclo.prism.kernel_uu, + choclo.prism.kernel_en, + choclo.prism.kernel_eu, + choclo.prism.kernel_nu, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: + local_row[j] = ( + constant_factor + * vector[i] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + local_row[j] = ( + constant_factor * vector[i] * regional_field_amplitude * bx + ) + local_row[j + n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * by + ) + local_row[j + 2 * n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * bz + ) + # Apply reduction operation to add the values of the row to the running + # result. Avoid slicing the `result` array when updating it to avoid + # racing conditions, just add the `local_row` to the `results` + # variable. + result += local_row + + +@jit(nopython=True, parallel=False) +def _mag_sensitivity_t_dot_v_serial( + receivers, + cells_bounds, + top, + bottom, + regional_field, + kernel_x, + kernel_y, + kernel_z, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` for a single magnetic component on 2d meshes, in serial. + + This function doesn't allocates the ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in serial. Writing to the ``result`` array + inside a parallel loop over the receivers generates a race condition that + leads to corrupted outputs. + + A parallel implementation of this function is available in + ``_mag_sensitivity_t_dot_v_parallel``. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in range(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + ux, uy, uz = evaluate_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_x, + kernel_y, + kernel_z, + ) + if scalar_model: + result[j] += ( + constant_factor + * vector[i] + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + else: + result[j] += constant_factor * vector[i] * regional_field_amplitude * ux + result[j + n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * uy + ) + result[j + 2 * n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * uz + ) + + +@jit(nopython=True, parallel=True) +def _mag_sensitivity_t_dot_v_parallel( + receivers, + cells_bounds, + top, + bottom, + regional_field, + kernel_x, + kernel_y, + kernel_z, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` for a single magnetic component on 2d meshes, in parallel. + + This function doesn't allocates the ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in parallel. + This implementation instructs each thread to allocate their own array for + the current row of the sensitivity matrix. After computing the elements of + that row, it gets added to the running ``result`` array through a reduction + operation handled by Numba. + + A parallel implementation of this function is available in + ``_mag_sensitivity_t_dot_v_parallel``. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + result_size = result.size + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate array for the current row of the sensitivity matrix + local_row = np.empty(result_size) + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + ux, uy, uz = evaluate_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_x, + kernel_y, + kernel_z, + ) + if scalar_model: + local_row[j] = ( + constant_factor + * vector[i] + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + else: + local_row[j] = ( + constant_factor * vector[i] * regional_field_amplitude * ux + ) + local_row[j + n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * uy + ) + local_row[j + 2 * n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * uz + ) + # Apply reduction operation to add the values of the row to the running + # result. Avoid slicing the `result` array when updating it to avoid + # racing conditions, just add the `local_row` to the `results` + # variable. + result += local_row + + +@jit(nopython=True, parallel=False) +def _tmi_derivative_sensitivity_t_dot_v_serial( + receivers, + cells_bounds, + top, + bottom, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` for a TMI derivative on 2d meshes, in serial. + + This function doesn't allocates the ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in serial. Writing to the ``result`` array + inside a parallel loop over the receivers generates a race condition that + leads to corrupted outputs. + + A parallel implementation of this function is available in + ``_tmi_derivative_sensitivity_t_dot_v_parallel``. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in range(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: + result[j] += ( + constant_factor + * vector[i] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + result[j] += constant_factor * vector[i] * regional_field_amplitude * bx + result[j + n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * by + ) + result[j + 2 * n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * bz + ) + + +@jit(nopython=True, parallel=True) +def _tmi_derivative_sensitivity_t_dot_v_parallel( + receivers, + cells_bounds, + top, + bottom, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` for a TMI derivative on 2d meshes, in parallel. + + This function doesn't allocates the ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in parallel. + This implementation instructs each thread to allocate their own array for + the current row of the sensitivity matrix. After computing the elements of + that row, it gets added to the running ``result`` array through a reduction + operation handled by Numba. + + A parallel implementation of this function is available in + ``_tmi_derivative_sensitivity_t_dot_v_parallel``. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + result_size = result.size + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate array for the current row of the sensitivity matrix + local_row = np.empty(result_size) + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: + local_row[j] = ( + constant_factor + * vector[i] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + local_row[j] = ( + constant_factor * vector[i] * regional_field_amplitude * bx + ) + local_row[j + n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * by + ) + local_row[j + 2 * n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * bz + ) + # Apply reduction operation to add the values of the row to the running + # result. Avoid slicing the `result` array when updating it to avoid + # racing conditions, just add the `local_row` to the `results` + # variable. + result += local_row + + +@jit(nopython=True, parallel=False) +def _diagonal_G_T_dot_G_tmi_serial( + receivers, + cells_bounds, + top, + bottom, + regional_field, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for TMI without storing ``G``, in serial. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in serial. Use the + ``_diagonal_G_T_dot_G_tmi_parallel`` one for parallelized computations. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in range(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + choclo.prism.kernel_ee, + choclo.prism.kernel_nn, + choclo.prism.kernel_uu, + choclo.prism.kernel_en, + choclo.prism.kernel_eu, + choclo.prism.kernel_nu, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + diagonal[j] += weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + diagonal[j] += weights[i] * (const * bx) ** 2 + diagonal[j + n_cells] += weights[i] * (const * by) ** 2 + diagonal[j + 2 * n_cells] += weights[i] * (const * bz) ** 2 + + +@jit(nopython=True, parallel=True) +def _diagonal_G_T_dot_G_tmi_parallel( + receivers, + cells_bounds, + top, + bottom, + regional_field, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for TMI without storing ``G``, in parallel. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in parallel. Use the + ``_diagonal_G_T_dot_G_tmi_serial`` one for serialized computations. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + diagonal_size = diagonal.size + constant_factor = 1 / 4 / np.pi + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate array for the diagonal elements for the current receiver. + local_diagonal = np.empty(diagonal_size) + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + choclo.prism.kernel_ee, + choclo.prism.kernel_nn, + choclo.prism.kernel_uu, + choclo.prism.kernel_en, + choclo.prism.kernel_eu, + choclo.prism.kernel_nu, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + local_diagonal[j] = weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + local_diagonal[j] = weights[i] * (const * bx) ** 2 + local_diagonal[j + n_cells] = weights[i] * (const * by) ** 2 + local_diagonal[j + 2 * n_cells] = weights[i] * (const * bz) ** 2 + # Add the result to the diagonal. + # Apply reduction operation to add the values of the local diagonal to + # the running diagonal array. Avoid slicing the `diagonal` array when + # updating it to avoid racing conditions, just add the `local_diagonal` + # to the `diagonal` variable. + diagonal += local_diagonal + + +@jit(nopython=True, parallel=False) +def _diagonal_G_T_dot_G_mag_serial( + receivers, + cells_bounds, + top, + bottom, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for components without storing ``G``, in serial. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in serial. Use the + ``_diagonal_G_T_dot_G_mag_parallel`` one for parallelized computations. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in range(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + ux, uy, uz = evaluate_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_x, + kernel_y, + kernel_z, + ) + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + diagonal[j] += weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + diagonal[j] += weights[i] * (const * ux) ** 2 + diagonal[j + n_cells] += weights[i] * (const * uy) ** 2 + diagonal[j + 2 * n_cells] += weights[i] * (const * uz) ** 2 + + +@jit(nopython=True, parallel=True) +def _diagonal_G_T_dot_G_mag_parallel( + receivers, + cells_bounds, + top, + bottom, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for component without storing ``G``, in parallel. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in parallel. Use the + ``_diagonal_G_T_dot_G_mag_serial`` one for serialized computations. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + diagonal_size = diagonal.size + constant_factor = 1 / 4 / np.pi + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate array for the diagonal elements for the current receiver. + local_diagonal = np.empty(diagonal_size) + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + ux, uy, uz = evaluate_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_x, + kernel_y, + kernel_z, + ) + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + local_diagonal[j] = weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + local_diagonal[j] = weights[i] * (const * ux) ** 2 + local_diagonal[j + n_cells] = weights[i] * (const * uy) ** 2 + local_diagonal[j + 2 * n_cells] = weights[i] * (const * uz) ** 2 + # Add the result to the diagonal. + # Apply reduction operation to add the values of the local diagonal to + # the running diagonal array. Avoid slicing the `diagonal` array when + # updating it to avoid racing conditions, just add the `local_diagonal` + # to the `diagonal` variable. + diagonal += local_diagonal + + +@jit(nopython=True, parallel=False) +def _diagonal_G_T_dot_G_tmi_deriv_serial( + receivers, + cells_bounds, + top, + bottom, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for TMI derivative, in serial. + + This function doesn't need to store the ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in serial. Use the + ``_diagonal_G_T_dot_G_tmi_deriv_parallel`` one for parallelized computations. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + + constant_factor = 1 / 4 / np.pi + + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in range(n_receivers): + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + diagonal[j] += weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + diagonal[j] += weights[i] * (const * bx) ** 2 + diagonal[j + n_cells] += weights[i] * (const * by) ** 2 + diagonal[j + 2 * n_cells] += weights[i] * (const * bz) ** 2 + + +@jit(nopython=True, parallel=True) +def _diagonal_G_T_dot_G_tmi_deriv_parallel( + receivers, + cells_bounds, + top, + bottom, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for TMI without storing ``G``, in parallel. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + cells_bounds : (n_active_cells, 4) numpy.ndarray + Array with the bounds of each active cell in the 2D mesh. For each row, the + bounds should be passed in the following order: ``x_min``, ``x_max``, + ``y_min``, ``y_max``. + top : (n_active_cells) np.ndarray + Array with the top boundaries of each active cell in the 2D mesh. + bottom : (n_active_cells) np.ndarray + Array with the bottom boundaries of each active cell in the 2D mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells,) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in parallel. Use the + ``_diagonal_G_T_dot_G_tmi_serial`` one for serialized computations. + """ + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + diagonal_size = diagonal.size + constant_factor = 1 / 4 / np.pi + n_receivers = receivers.shape[0] + n_cells = cells_bounds.shape[0] + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate array for the diagonal elements for the current receiver. + local_diagonal = np.empty(diagonal_size) + for j in range(n_cells): + # Evaluate kernels for the current cell and receiver + uxx, uyy, uzz, uxy, uxz, uyz = evaluate_six_kernels_on_cell( + receivers[i, 0], + receivers[i, 1], + receivers[i, 2], + cells_bounds[j, 0], + cells_bounds[j, 1], + cells_bounds[j, 2], + cells_bounds[j, 3], + bottom[j], + top[j], + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + ) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + local_diagonal[j] = weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + local_diagonal[j] = weights[i] * (const * bx) ** 2 + local_diagonal[j + n_cells] = weights[i] * (const * by) ** 2 + local_diagonal[j + 2 * n_cells] = weights[i] * (const * bz) ** 2 + # Add the result to the diagonal. + # Apply reduction operation to add the values of the local diagonal to + # the running diagonal array. Avoid slicing the `diagonal` array when + # updating it to avoid racing conditions, just add the `local_diagonal` + # to the `diagonal` variable. + diagonal += local_diagonal + + +NUMBA_FUNCTIONS_2D = { + "forward": { + "tmi": { + parallel: jit(nopython=True, parallel=parallel)(_forward_tmi) + for parallel in (True, False) + }, + "magnetic_component": { + parallel: jit(nopython=True, parallel=parallel)(_forward_mag) + for parallel in (True, False) + }, + "tmi_derivative": { + parallel: jit(nopython=True, parallel=parallel)(_forward_tmi_derivative) + for parallel in (True, False) + }, + }, + "sensitivity": { + "tmi": { + parallel: jit(nopython=True, parallel=parallel)(_sensitivity_tmi) + for parallel in (True, False) + }, + "magnetic_component": { + parallel: jit(nopython=True, parallel=parallel)(_sensitivity_mag) + for parallel in (True, False) + }, + "tmi_derivative": { + parallel: jit(nopython=True, parallel=parallel)(_sensitivity_tmi_derivative) + for parallel in (True, False) + }, + }, + "gt_dot_v": { + "tmi": { + False: _tmi_sensitivity_t_dot_v_serial, + True: _tmi_sensitivity_t_dot_v_parallel, + }, + "magnetic_component": { + False: _mag_sensitivity_t_dot_v_serial, + True: _mag_sensitivity_t_dot_v_parallel, + }, + "tmi_derivative": { + False: _tmi_derivative_sensitivity_t_dot_v_serial, + True: _tmi_derivative_sensitivity_t_dot_v_parallel, + }, + }, + "diagonal_gtg": { + "tmi": { + False: _diagonal_G_T_dot_G_tmi_serial, + True: _diagonal_G_T_dot_G_tmi_parallel, + }, + "magnetic_component": { + False: _diagonal_G_T_dot_G_mag_serial, + True: _diagonal_G_T_dot_G_mag_parallel, + }, + "tmi_derivative": { + False: _diagonal_G_T_dot_G_tmi_deriv_serial, + True: _diagonal_G_T_dot_G_tmi_deriv_parallel, + }, + }, +} diff --git a/simpeg/potential_fields/magnetics/_numba/_3d_mesh.py b/simpeg/potential_fields/magnetics/_numba/_3d_mesh.py new file mode 100644 index 0000000000..57ba260371 --- /dev/null +++ b/simpeg/potential_fields/magnetics/_numba/_3d_mesh.py @@ -0,0 +1,2420 @@ +""" +Numba functions for magnetic simulation of rectangular prisms +""" + +import numpy as np + +try: + import choclo +except ImportError: + # Define dummy jit decorator + def jit(*args, **kwargs): + return lambda f: f + + choclo = None +else: + from numba import jit, prange + +from ..._numba_utils import kernels_in_nodes_to_cell + + +def _sensitivity_mag( + receivers, + nodes, + sensitivity_matrix, + cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, +): + r""" + Fill the sensitivity matrix for single mag component + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_sensitivity_mag = jit(nopython=True, parallel=True)(_sensitivity_mag) + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + sensitivity_matrix : array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + The array should have a shape of ``(n_receivers, n_active_cells)`` + if ``scalar_model`` is True. + The array should have a shape of ``(n_receivers, 3 * n_active_cells)`` + if ``scalar_model`` is False. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is built to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is built to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the kernel functions + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + For computing the ``bx`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_ee, kernel_y=kernel_en, kernel_z=kernel_eu + + + For computing the ``by`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_en, kernel_y=kernel_nn, kernel_z=kernel_nu + + For computing the ``bz`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_eu, kernel_y=kernel_nu, kernel_z=kernel_uu + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + About the sensitivity matrix + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Each row of the sensitivity matrix corresponds to a single receiver + location. + + If ``scalar_model`` is True, then each element of the row will + correspond to the partial derivative of the selected magnetic component + with respect to the susceptibility of each cell in the mesh. + + If ``scalar_model`` is False, then each row can be split in three sections + containing: + + * the partial derivatives of the selected magnetic component with respect + to the _x_ component of the effective susceptibility of each cell; then + * the partial derivatives of the selected magnetic component with respect + to the _y_ component of the effective susceptibility of each cell; and then + * the partial derivatives of the selected magnetic component with respect + to the _z_ component of the effective susceptibility of each cell. + + So, if we call :math:`B_j` the magnetic field component on the receiver + :math:`j`, and :math:`\bar{\chi}^{(i)} = (\chi_x^{(i)}, \chi_y^{(i)}, + \chi_z^{(i)})` the effective susceptibility of the active cell :math:`i`, + then each row of the sensitivity matrix will be: + + .. math:: + + \left[ + \frac{\partial B_j}{\partial \chi_x^{(1)}}, + \dots, + \frac{\partial B_j}{\partial \chi_x^{(N)}}, + \frac{\partial B_j}{\partial \chi_y^{(1)}}, + \dots, + \frac{\partial B_j}{\partial \chi_y^{(N)}}, + \frac{\partial B_j}{\partial \chi_z^{(1)}}, + \dots, + \frac{\partial B_j}{\partial \chi_z^{(N)}} + \right] + + where :math:`N` is the total number of active cells. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kx, ky, kz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kx[j] = kernel_x(dx, dy, dz, distance) + ky[j] = kernel_y(dx, dy, dz, distance) + kz[j] = kernel_z(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + ux = kernels_in_nodes_to_cell(kx, nodes_indices) + uy = kernels_in_nodes_to_cell(ky, nodes_indices) + uz = kernels_in_nodes_to_cell(kz, nodes_indices) + if scalar_model: + sensitivity_matrix[i, k] = ( + constant_factor + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + else: + sensitivity_matrix[i, k] = ( + constant_factor * regional_field_amplitude * ux + ) + sensitivity_matrix[i, k + n_cells] = ( + constant_factor * regional_field_amplitude * uy + ) + sensitivity_matrix[i, k + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * uz + ) + + +def _sensitivity_tmi( + receivers, + nodes, + sensitivity_matrix, + cell_nodes, + regional_field, + constant_factor, + scalar_model, +): + r""" + Fill the sensitivity matrix for TMI + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_sensitivity_tmi = jit(nopython=True, parallel=True)(_sensitivity_tmi) + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + sensitivity_matrix : array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + The array should have a shape of ``(n_receivers, n_active_cells)`` + if ``scalar_model`` is True. + The array should have a shape of ``(n_receivers, 3 * n_active_cells)`` + if ``scalar_model`` is False. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + About the sensitivity matrix + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Each row of the sensitivity matrix corresponds to a single receiver + location. + + If ``scalar_model`` is True, then each element of the row will + correspond to the partial derivative of the tmi + with respect to the susceptibility of each cell in the mesh. + + If ``scalar_model`` is False, then each row can be split in three sections + containing: + + * the partial derivatives of the tmi with respect + to the _x_ component of the effective susceptibility of each cell; then + * the partial derivatives of the tmi with respect + to the _y_ component of the effective susceptibility of each cell; and then + * the partial derivatives of the tmi with respect + to the _z_ component of the effective susceptibility of each cell. + + So, if we call :math:`T_j` the tmi on the receiver + :math:`j`, and :math:`\bar{\chi}^{(i)} = (\chi_x^{(i)}, \chi_y^{(i)}, + \chi_z^{(i)})` the effective susceptibility of the active cell :math:`i`, + then each row of the sensitivity matrix will be: + + .. math:: + + \left[ + \frac{\partial T_j}{\partial \chi_x^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_x^{(N)}}, + \frac{\partial T_j}{\partial \chi_y^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_y^{(N)}}, + \frac{\partial T_j}{\partial \chi_z^{(1)}}, + \dots, + \frac{\partial T_j}{\partial \chi_z^{(N)}} + \right] + + where :math:`N` is the total number of active cells. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = choclo.prism.kernel_ee(dx, dy, dz, distance) + kyy[j] = choclo.prism.kernel_nn(dx, dy, dz, distance) + kzz[j] = choclo.prism.kernel_uu(dx, dy, dz, distance) + kxy[j] = choclo.prism.kernel_en(dx, dy, dz, distance) + kxz[j] = choclo.prism.kernel_eu(dx, dy, dz, distance) + kyz[j] = choclo.prism.kernel_nu(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + # Fill the sensitivity matrix element(s) that correspond to the + # current active cell + if scalar_model: + sensitivity_matrix[i, k] = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + sensitivity_matrix[i, k] = ( + constant_factor * regional_field_amplitude * bx + ) + sensitivity_matrix[i, k + n_cells] = ( + constant_factor * regional_field_amplitude * by + ) + sensitivity_matrix[i, k + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * bz + ) + + +def _sensitivity_tmi_derivative( + receivers, + nodes, + sensitivity_matrix, + cell_nodes, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, +): + r""" + Fill the sensitivity matrix for a TMI derivative. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_sens = jit(nopython=True, parallel=True)(_sensitivity_tmi_derivative) + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + sensitivity_matrix : array + Empty 2d array where the sensitivity matrix elements will be filled. + This could be a preallocated empty array or a slice of it. + The array should have a shape of ``(n_receivers, n_active_cells)`` + if ``scalar_model`` is True. + The array should have a shape of ``(n_receivers, 3 * n_active_cells)`` + if ``scalar_model`` is False. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the kernel functions + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + To compute the :math:`\alpha` derivative of the TMI :math:`\Delta T` (with + :math:`\alpha \in \{x, y, z\}` we need to evaluate third order kernels + functions for the prism. The kernels we need to evaluate can be obtained by + fixing one of the subindices to the direction of the derivative + (:math:`\alpha`) and cycle through combinations of the other two. + + For ``tmi_x`` we need to pass: + + .. code:: + + kernel_xx=kernel_eee, kernel_yy=kernel_enn, kernel_zz=kernel_euu, + kernel_xy=kernel_een, kernel_xz=kernel_eeu, kernel_yz=kernel_enu + + For ``tmi_y`` we need to pass: + + .. code:: + + kernel_xx=kernel_een, kernel_yy=kernel_nnn, kernel_zz=kernel_nuu, + kernel_xy=kernel_enn, kernel_xz=kernel_enu, kernel_yz=kernel_nnu + + For ``tmi_z`` we need to pass: + + .. code:: + + kernel_xx=kernel_eeu, kernel_yy=kernel_nnu, kernel_zz=kernel_uuu, + kernel_xy=kernel_enu, kernel_xz=kernel_euu, kernel_yz=kernel_nuu + + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + About the sensitivity matrix + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + Each row of the sensitivity matrix corresponds to a single receiver + location. + + If ``scalar_model`` is True, then each element of the row will + correspond to the partial derivative of the tmi derivative (spatial) + with respect to the susceptibility of each cell in the mesh. + + If ``scalar_model`` is False, then each row can be split in three sections + containing: + + * the partial derivatives of the tmi derivative with respect + to the _x_ component of the effective susceptibility of each cell; then + * the partial derivatives of the tmi derivative with respect + to the _y_ component of the effective susceptibility of each cell; and then + * the partial derivatives of the tmi derivative with respect + to the _z_ component of the effective susceptibility of each cell. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = kernel_xx(dx, dy, dz, distance) + kyy[j] = kernel_yy(dx, dy, dz, distance) + kzz[j] = kernel_zz(dx, dy, dz, distance) + kxy[j] = kernel_xy(dx, dy, dz, distance) + kxz[j] = kernel_xz(dx, dy, dz, distance) + kyz[j] = kernel_yz(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + # Fill the sensitivity matrix element(s) that correspond to the + # current active cell + if scalar_model: + sensitivity_matrix[i, k] = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + sensitivity_matrix[i, k] = ( + constant_factor * regional_field_amplitude * bx + ) + sensitivity_matrix[i, k + n_cells] = ( + constant_factor * regional_field_amplitude * by + ) + sensitivity_matrix[i, k + 2 * n_cells] = ( + constant_factor * regional_field_amplitude * bz + ) + + +@jit(nopython=True, parallel=False) +def _mag_sensitivity_t_dot_v_serial( + receivers, + nodes, + cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` in serial, without building G, for a single magnetic component. + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in serial. Writing to the ``result`` array + inside a parallel loop over the receivers generates a race condition that + leads to corrupted outputs. + + A parallel implementation of this function is available in + ``_mag_sensitivity_t_dot_v_parallel``. + + See also + -------- + _sensitivity_mag + Compute the sensitivity matrix for a single magnetic component by + allocating it in memory. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kx, ky, kz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kx[j] = kernel_x(dx, dy, dz, distance) + ky[j] = kernel_y(dx, dy, dz, distance) + kz[j] = kernel_z(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + ux = kernels_in_nodes_to_cell(kx, nodes_indices) + uy = kernels_in_nodes_to_cell(ky, nodes_indices) + uz = kernels_in_nodes_to_cell(kz, nodes_indices) + if scalar_model: + result[k] += ( + constant_factor + * vector[i] + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + else: + result[k] += constant_factor * vector[i] * regional_field_amplitude * ux + result[k + n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * uy + ) + result[k + 2 * n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * uz + ) + + +@jit(nopython=True, parallel=True) +def _mag_sensitivity_t_dot_v_parallel( + receivers, + nodes, + cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` in parallel, without building G, for a single magnetic component + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in parallel. + This implementation instructs each thread to allocate their own array for + the current row of the sensitivity matrix. After computing the elements of + that row, it gets added to the running ``result`` array through a reduction + operation handled by Numba. + + A serialized implementation of this function is available in + ``_mag_sensitivity_t_dot_v_serial``. + + See also + -------- + _sensitivity_mag + Compute the sensitivity matrix for a single magnetic component by + allocating it in memory. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + result_size = result.size + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kx, ky, kz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate array for the current row of the sensitivity matrix + local_row = np.empty(result_size) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kx[j] = kernel_x(dx, dy, dz, distance) + ky[j] = kernel_y(dx, dy, dz, distance) + kz[j] = kernel_z(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + ux = kernels_in_nodes_to_cell(kx, nodes_indices) + uy = kernels_in_nodes_to_cell(ky, nodes_indices) + uz = kernels_in_nodes_to_cell(kz, nodes_indices) + if scalar_model: + local_row[k] = ( + constant_factor + * vector[i] + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + else: + local_row[k] = ( + constant_factor * vector[i] * regional_field_amplitude * ux + ) + local_row[k + n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * uy + ) + local_row[k + 2 * n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * uz + ) + # Apply reduction operation to add the values of the row to the running + # result. Avoid slicing the `result` array when updating it to avoid + # racing conditions, just add the `local_row` to the `results` + # variable. + result += local_row + + +@jit(nopython=True, parallel=False) +def _tmi_sensitivity_t_dot_v_serial( + receivers, + nodes, + cell_nodes, + regional_field, + constant_factor, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` in serial, without building G, for TMI. + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in serial. Writing to the ``result`` array + inside a parallel loop over the receivers generates a race condition that + leads to corrupted outputs. + + A parallel implementation of this function is available in + ``_tmi_sensitivity_t_dot_v_parallel``. + + See also + -------- + _sensitivity_tmi + Compute the sensitivity matrix for TMI by allocating it in memory. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = choclo.prism.kernel_ee(dx, dy, dz, distance) + kyy[j] = choclo.prism.kernel_nn(dx, dy, dz, distance) + kzz[j] = choclo.prism.kernel_uu(dx, dy, dz, distance) + kxy[j] = choclo.prism.kernel_en(dx, dy, dz, distance) + kxz[j] = choclo.prism.kernel_eu(dx, dy, dz, distance) + kyz[j] = choclo.prism.kernel_nu(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + # Fill the sensitivity matrix element(s) that correspond to the + # current active cell + if scalar_model: + result[k] += ( + constant_factor + * vector[i] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + result[k] += constant_factor * vector[i] * regional_field_amplitude * bx + result[k + n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * by + ) + result[k + 2 * n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * bz + ) + + +@jit(nopython=True, parallel=True) +def _tmi_sensitivity_t_dot_v_parallel( + receivers, + nodes, + cell_nodes, + regional_field, + constant_factor, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` in parallel, without building G, for TMI. + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in parallel. + This implementation instructs each thread to allocate their own array for + the current row of the sensitivity matrix. After computing the elements of + that row, it gets added to the running ``result`` array through a reduction + operation handled by Numba. + + A serialized implementation of this function is available in + ``_tmi_sensitivity_t_dot_v_serial``. + + See also + -------- + _sensitivity_tmi + Compute the sensitivity matrix for TMI by allocating it in memory. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + result_size = result.size + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate array for the current row of the sensitivity matrix + local_row = np.empty(result_size) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = choclo.prism.kernel_ee(dx, dy, dz, distance) + kyy[j] = choclo.prism.kernel_nn(dx, dy, dz, distance) + kzz[j] = choclo.prism.kernel_uu(dx, dy, dz, distance) + kxy[j] = choclo.prism.kernel_en(dx, dy, dz, distance) + kxz[j] = choclo.prism.kernel_eu(dx, dy, dz, distance) + kyz[j] = choclo.prism.kernel_nu(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + # Fill the sensitivity matrix element(s) that correspond to the + # current active cell + if scalar_model: + local_row[k] = ( + constant_factor + * vector[i] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + local_row[k] = ( + constant_factor * vector[i] * regional_field_amplitude * bx + ) + local_row[k + n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * by + ) + local_row[k + 2 * n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * bz + ) + # Apply reduction operation to add the values of the row to the running + # result. Avoid slicing the `result` array when updating it to avoid + # racing conditions, just add the `local_row` to the `results` + # variable. + result += local_row + + +@jit(nopython=True, parallel=False) +def _tmi_derivative_sensitivity_t_dot_v_serial( + receivers, + nodes, + cell_nodes, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` in serial, without building G, for a spatial TMI derivative. + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in serial. Writing to the ``result`` array + inside a parallel loop over the receivers generates a race condition that + leads to corrupted outputs. + + A parallel implementation of this function is available in + ``_tmi_derivative_sensitivity_t_dot_v_parallel``. + + See also + -------- + _sensitivity_tmi_derivative + Compute the sensitivity matrix for a TMI derivative by allocating it in memory. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = kernel_xx(dx, dy, dz, distance) + kyy[j] = kernel_yy(dx, dy, dz, distance) + kzz[j] = kernel_zz(dx, dy, dz, distance) + kxy[j] = kernel_xy(dx, dy, dz, distance) + kxz[j] = kernel_xz(dx, dy, dz, distance) + kyz[j] = kernel_yz(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + # Fill the sensitivity matrix element(s) that correspond to the + # current active cell + if scalar_model: + result[k] += ( + constant_factor + * vector[i] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + result[k] += constant_factor * vector[i] * regional_field_amplitude * bx + result[k + n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * by + ) + result[k + 2 * n_cells] += ( + constant_factor * vector[i] * regional_field_amplitude * bz + ) + + +@jit(nopython=True, parallel=True) +def _tmi_derivative_sensitivity_t_dot_v_parallel( + receivers, + nodes, + cell_nodes, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + vector, + result, +): + r""" + Compute ``G.T @ v`` in parallel, without building G, for a spatial TMI derivative. + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + vector : (n_receivers) numpy.ndarray + Array that represents the vector used in the dot product. + result : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Running result array where the output of the dot product will be added to. + The array should have ``n_active_cells`` elements if ``scalar_model`` + is True, or ``3 * n_active_cells`` otherwise. + + Notes + ----- + This function is meant to be run in parallel. + This implementation instructs each thread to allocate their own array for + the current row of the sensitivity matrix. After computing the elements of + that row, it gets added to the running ``result`` array through a reduction + operation handled by Numba. + + A serialized implementation of this function is available in + ``_tmi_derivative_sensitivity_t_dot_v_serial``. + + See also + -------- + _sensitivity_tmi_derivative + Compute the sensitivity matrix for a TMI derivative by allocating it in memory. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + result_size = result.size + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate array for the current row of the sensitivity matrix + local_row = np.empty(result_size) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = kernel_xx(dx, dy, dz, distance) + kyy[j] = kernel_yy(dx, dy, dz, distance) + kzz[j] = kernel_zz(dx, dy, dz, distance) + kxy[j] = kernel_xy(dx, dy, dz, distance) + kxz[j] = kernel_xz(dx, dy, dz, distance) + kyz[j] = kernel_yz(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + # Fill the sensitivity matrix element(s) that correspond to the + # current active cell + if scalar_model: + local_row[k] = ( + constant_factor + * vector[i] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + local_row[k] = ( + constant_factor * vector[i] * regional_field_amplitude * bx + ) + local_row[k + n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * by + ) + local_row[k + 2 * n_cells] = ( + constant_factor * vector[i] * regional_field_amplitude * bz + ) + # Apply reduction operation to add the values of the row to the running + # result. Avoid slicing the `result` array when updating it to avoid + # racing conditions, just add the `local_row` to the `results` + # variable. + result += local_row + + +@jit(nopython=True, parallel=False) +def _diagonal_G_T_dot_G_mag_serial( + receivers, + nodes, + cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for single magnetic component, in serial. + + This function doesn't store the full ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in serial. Use the + ``_diagonal_G_T_dot_G_mag_parallel`` one for parallelized computations. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kx, ky, kz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kx[j] = kernel_x(dx, dy, dz, distance) + ky[j] = kernel_y(dx, dy, dz, distance) + kz[j] = kernel_z(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + ux = kernels_in_nodes_to_cell(kx, nodes_indices) + uy = kernels_in_nodes_to_cell(ky, nodes_indices) + uz = kernels_in_nodes_to_cell(kz, nodes_indices) + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + diagonal[k] += weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + diagonal[k] += weights[i] * (const * ux) ** 2 + diagonal[k + n_cells] += weights[i] * (const * uy) ** 2 + diagonal[k + 2 * n_cells] += weights[i] * (const * uz) ** 2 + + +@jit(nopython=True, parallel=True) +def _diagonal_G_T_dot_G_mag_parallel( + receivers, + nodes, + cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for single magnetic component, in parallel. + + This function doesn't store the full ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in parallel. Use the + ``_diagonal_G_T_dot_G_mag_serial`` one for serialized computations. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + diagonal_size = diagonal.size + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kx, ky, kz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate array for the diagonal elements for the current receiver. + local_diagonal = np.empty(diagonal_size) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kx[j] = kernel_x(dx, dy, dz, distance) + ky[j] = kernel_y(dx, dy, dz, distance) + kz[j] = kernel_z(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + ux = kernels_in_nodes_to_cell(kx, nodes_indices) + uy = kernels_in_nodes_to_cell(ky, nodes_indices) + uz = kernels_in_nodes_to_cell(kz, nodes_indices) + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + local_diagonal[k] = weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + local_diagonal[k] = weights[i] * (const * ux) ** 2 + local_diagonal[k + n_cells] = weights[i] * (const * uy) ** 2 + local_diagonal[k + 2 * n_cells] = weights[i] * (const * uz) ** 2 + # Add the result to the diagonal. + # Apply reduction operation to add the values of the local diagonal to + # the running diagonal array. Avoid slicing the `diagonal` array when + # updating it to avoid racing conditions, just add the `local_diagonal` + # to the `diagonal` variable. + diagonal += local_diagonal + + +@jit(nopython=True, parallel=False) +def _diagonal_G_T_dot_G_tmi_serial( + receivers, + nodes, + cell_nodes, + regional_field, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for TMI, in serial. + + This function doesn't store the full ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in serial. Use the + ``_diagonal_G_T_dot_G_tmi_parallel`` one for parallelized computations. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = choclo.prism.kernel_ee(dx, dy, dz, distance) + kyy[j] = choclo.prism.kernel_nn(dx, dy, dz, distance) + kzz[j] = choclo.prism.kernel_uu(dx, dy, dz, distance) + kxy[j] = choclo.prism.kernel_en(dx, dy, dz, distance) + kxz[j] = choclo.prism.kernel_eu(dx, dy, dz, distance) + kyz[j] = choclo.prism.kernel_nu(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + diagonal[k] += weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + diagonal[k] += weights[i] * (const * bx) ** 2 + diagonal[k + n_cells] += weights[i] * (const * by) ** 2 + diagonal[k + 2 * n_cells] += weights[i] * (const * bz) ** 2 + + +@jit(nopython=True, parallel=True) +def _diagonal_G_T_dot_G_tmi_parallel( + receivers, + nodes, + cell_nodes, + regional_field, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for TMI, in parallel. + + This function doesn't store the full ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in parallel. Use the + ``_diagonal_G_T_dot_G_tmi_serial`` one for serialized computations. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + diagonal_size = diagonal.size + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate array for the diagonal elements for the current receiver. + local_diagonal = np.empty(diagonal_size) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = choclo.prism.kernel_ee(dx, dy, dz, distance) + kyy[j] = choclo.prism.kernel_nn(dx, dy, dz, distance) + kzz[j] = choclo.prism.kernel_uu(dx, dy, dz, distance) + kxy[j] = choclo.prism.kernel_en(dx, dy, dz, distance) + kxz[j] = choclo.prism.kernel_eu(dx, dy, dz, distance) + kyz[j] = choclo.prism.kernel_nu(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + local_diagonal[k] = weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + local_diagonal[k] = weights[i] * (const * bx) ** 2 + local_diagonal[k + n_cells] = weights[i] * (const * by) ** 2 + local_diagonal[k + 2 * n_cells] = weights[i] * (const * bz) ** 2 + + # Add the result to the diagonal. + # Apply reduction operation to add the values of the local diagonal to + # the running diagonal array. Avoid slicing the `diagonal` array when + # updating it to avoid racing conditions, just add the `local_diagonal` + # to the `diagonal` variable. + diagonal += local_diagonal + + +@jit(nopython=True, parallel=False) +def _diagonal_G_T_dot_G_tmi_deriv_serial( + receivers, + nodes, + cell_nodes, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for TMI derivatives, in serial. + + This function doesn't store the full ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in serial. Use the + ``_diagonal_G_T_dot_G_tmi_deriv_parallel`` one for parallelized computations. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = kernel_xx(dx, dy, dz, distance) + kyy[j] = kernel_yy(dx, dy, dz, distance) + kzz[j] = kernel_zz(dx, dy, dz, distance) + kxy[j] = kernel_xy(dx, dy, dz, distance) + kxz[j] = kernel_xz(dx, dy, dz, distance) + kyz[j] = kernel_yz(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + diagonal[k] += weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + diagonal[k] += weights[i] * (const * bx) ** 2 + diagonal[k + n_cells] += weights[i] * (const * by) ** 2 + diagonal[k + 2 * n_cells] += weights[i] * (const * bz) ** 2 + + +@jit(nopython=True, parallel=True) +def _diagonal_G_T_dot_G_tmi_deriv_parallel( + receivers, + nodes, + cell_nodes, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + weights, + diagonal, +): + """ + Diagonal of ``G.T @ W.T @ W @ G`` for TMI derivatives, in parallel. + + This function doesn't store the full ``G`` matrix in memory. + + Parameters + ---------- + receivers : (n_receivers, 3) numpy.ndarray + Array with the locations of the receivers + nodes : (n_active_nodes, 3) numpy.ndarray + Array with the location of the mesh nodes. + cell_nodes : (n_active_cells, 8) numpy.ndarray + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the result will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the result will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + weights : (n_receivers,) numpy.ndarray + Array with data weights. It should be the diagonal of the ``W`` matrix, + squared. + diagonal : (n_active_cells) or (3 * n_active_cells) numpy.ndarray + Array where the diagonal of ``G.T @ G`` will be added to. + + Notes + ----- + This function is meant to be run in parallel. Use the + ``_diagonal_G_T_dot_G_tmi_serial`` one for serialized computations. + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + diagonal_size = diagonal.size + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate array for the diagonal elements for the current receiver. + local_diagonal = np.empty(diagonal_size) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = kernel_xx(dx, dy, dz, distance) + kyy[j] = kernel_yy(dx, dy, dz, distance) + kzz[j] = kernel_zz(dx, dy, dz, distance) + kxy[j] = kernel_xy(dx, dy, dz, distance) + kxz[j] = kernel_xz(dx, dy, dz, distance) + kyz[j] = kernel_yz(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + + if scalar_model: + g_element = ( + constant_factor + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + local_diagonal[k] = weights[i] * g_element**2 + else: + const = constant_factor * regional_field_amplitude + local_diagonal[k] = weights[i] * (const * bx) ** 2 + local_diagonal[k + n_cells] = weights[i] * (const * by) ** 2 + local_diagonal[k + 2 * n_cells] = weights[i] * (const * bz) ** 2 + + # Add the result to the diagonal. + # Apply reduction operation to add the values of the local diagonal to + # the running diagonal array. Avoid slicing the `diagonal` array when + # updating it to avoid racing conditions, just add the `local_diagonal` + # to the `diagonal` variable. + diagonal += local_diagonal + + +def _forward_mag( + receivers, + nodes, + model, + fields, + cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, +): + """ + Forward model single magnetic component + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_forward = jit(nopython=True, parallel=True)(_forward_mag) + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + model : (n_active_cells) or (3 * n_active_cells) array + Array containing the susceptibilities (scalar) or effective + susceptibilities (vector) of the active cells in the mesh, in SI + units. + Susceptibilities are expected if ``scalar_model`` is True, + and the array should have ``n_active_cells`` elements. + Effective susceptibilities are expected if ``scalar_model`` is False, + and the array should have ``3 * n_active_cells`` elements. + fields : (n_receivers) array + Array full of zeros where the magnetic component on each receiver will + be stored. This could be a preallocated array or a slice of it. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_x, kernel_y, kernel_z : callable + Kernels used to compute the desired magnetic component. For example, + for computing bx we need to use ``kernel_x=kernel_ee``, + ``kernel_y=kernel_en``, ``kernel_z=kernel_eu``. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the forward will be computed assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the forward will be computed assuming that the ``model`` has + effective susceptibilities (vector model) for each active cell. + + Notes + ----- + + About the kernel functions + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + For computing the ``bx`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_ee, kernel_y=kernel_en, kernel_z=kernel_eu + + + For computing the ``by`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_en, kernel_y=kernel_nn, kernel_z=kernel_nu + + For computing the ``bz`` component of the magnetic field we need to use the + following kernels: + + .. code:: + + kernel_x=kernel_eu, kernel_y=kernel_nu, kernel_z=kernel_uu + + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kx, ky, kz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kx[j] = kernel_x(dx, dy, dz, distance) + ky[j] = kernel_y(dx, dy, dz, distance) + kz[j] = kernel_z(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + ux = kernels_in_nodes_to_cell(kx, nodes_indices) + uy = kernels_in_nodes_to_cell(ky, nodes_indices) + uz = kernels_in_nodes_to_cell(kz, nodes_indices) + if scalar_model: + fields[i] += ( + constant_factor + * model[k] + * regional_field_amplitude + * (ux * fx + uy * fy + uz * fz) + ) + else: + fields[i] += ( + constant_factor + * regional_field_amplitude + * ( + ux * model[k] + + uy * model[k + n_cells] + + uz * model[k + 2 * n_cells] + ) + ) + + +def _forward_tmi( + receivers, + nodes, + model, + fields, + cell_nodes, + regional_field, + constant_factor, + scalar_model, +): + """ + Forward model the TMI + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_forward = jit(nopython=True, parallel=True)(_forward_tmi) + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + model : (n_active_cells) or (3 * n_active_cells) + Array with the susceptibility (scalar model) or the effective + susceptibility (vector model) of each active cell in the mesh. + If the model is scalar, the ``model`` array should have + ``n_active_cells`` elements and ``scalar_model`` should be True. + If the model is vector, the ``model`` array should have + ``3 * n_active_cells`` elements and ``scalar_model`` should be False. + fields : (n_receivers) array + Array full of zeros where the TMI on each receiver will be stored. This + could be a preallocated array or a slice of it. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = choclo.prism.kernel_ee(dx, dy, dz, distance) + kyy[j] = choclo.prism.kernel_nn(dx, dy, dz, distance) + kzz[j] = choclo.prism.kernel_uu(dx, dy, dz, distance) + kxy[j] = choclo.prism.kernel_en(dx, dy, dz, distance) + kxz[j] = choclo.prism.kernel_eu(dx, dy, dz, distance) + kyz[j] = choclo.prism.kernel_nu(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: + fields[i] += ( + constant_factor + * model[k] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + fields[i] += ( + constant_factor + * regional_field_amplitude + * ( + bx * model[k] + + by * model[k + n_cells] + + bz * model[k + 2 * n_cells] + ) + ) + + +def _forward_tmi_derivative( + receivers, + nodes, + model, + fields, + cell_nodes, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, +): + r""" + Forward model a TMI derivative. + + This function should be used with a `numba.jit` decorator, for example: + + .. code:: + + from numba import jit + + jit_forward = jit(nopython=True, parallel=True)(_forward_tmi_derivative) + + Parameters + ---------- + receivers : (n_receivers, 3) array + Array with the locations of the receivers + nodes : (n_active_nodes, 3) array + Array with the location of the mesh nodes. + model : (n_active_cells) or (3 * n_active_cells) + Array with the susceptibility (scalar model) or the effective + susceptibility (vector model) of each active cell in the mesh. + If the model is scalar, the ``model`` array should have + ``n_active_cells`` elements and ``scalar_model`` should be True. + If the model is vector, the ``model`` array should have + ``3 * n_active_cells`` elements and ``scalar_model`` should be False. + fields : (n_receivers) array + Array full of zeros where the TMI derivative on each receiver will be + stored. This could be a preallocated array or a slice of it. + cell_nodes : (n_active_cells, 8) array + Array of integers, where each row contains the indices of the nodes for + each active cell in the mesh. + regional_field : (3,) array + Array containing the x, y and z components of the regional magnetic + field (uniform background field). + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz : callables + Kernel functions used for computing the desired TMI derivative. + constant_factor : float + Constant factor that will be used to multiply each element of the + sensitivity matrix. + scalar_model : bool + If True, the sensitivity matrix is build to work with scalar models + (susceptibilities). + If False, the sensitivity matrix is build to work with vector models + (effective susceptibilities). + + Notes + ----- + + About the kernel functions + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + To compute the :math:`\alpha` derivative of the TMI :math:`\Delta T` (with + :math:`\alpha \in \{x, y, z\}` we need to evaluate third order kernels + functions for the prism. The kernels we need to evaluate can be obtained by + fixing one of the subindices to the direction of the derivative + (:math:`\alpha`) and cycle through combinations of the other two. + + For ``tmi_x`` we need to pass: + + .. code:: + + kernel_xx=kernel_eee, kernel_yy=kernel_enn, kernel_zz=kernel_euu, + kernel_xy=kernel_een, kernel_xz=kernel_eeu, kernel_yz=kernel_enu + + For ``tmi_y`` we need to pass: + + .. code:: + + kernel_xx=kernel_een, kernel_yy=kernel_nnn, kernel_zz=kernel_nuu, + kernel_xy=kernel_enn, kernel_xz=kernel_enu, kernel_yz=kernel_nnu + + For ``tmi_z`` we need to pass: + + .. code:: + + kernel_xx=kernel_eeu, kernel_yy=kernel_nnu, kernel_zz=kernel_uuu, + kernel_xy=kernel_enu, kernel_xz=kernel_euu, kernel_yz=kernel_nuu + + + About the model array + ^^^^^^^^^^^^^^^^^^^^^ + + The ``model`` must always be a 1d array: + + * If ``scalar_model`` is ``True``, then ``model`` should be a 1d array with + the same number of elements as active cells in the mesh. It should store + the magnetic susceptibilities of each active cell in SI units. + * If ``scalar_model`` is ``False``, then ``model`` should be a 1d array + with a number of elements equal to three times the active cells in the + mesh. It should store the components of the magnetization vector of each + active cell in :math:`Am^{-1}`. The order in which the components should + be passed are: + * every _easting_ component of each active cell, + * then every _northing_ component of each active cell, + * and finally every _upward_ component of each active cell. + + """ + n_receivers = receivers.shape[0] + n_nodes = nodes.shape[0] + n_cells = cell_nodes.shape[0] + fx, fy, fz = regional_field + regional_field_amplitude = np.sqrt(fx**2 + fy**2 + fz**2) + fx /= regional_field_amplitude + fy /= regional_field_amplitude + fz /= regional_field_amplitude + # Evaluate kernel function on each node, for each receiver location + for i in prange(n_receivers): + # Allocate vectors for kernels evaluated on mesh nodes + kxx, kyy, kzz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + kxy, kxz, kyz = np.empty(n_nodes), np.empty(n_nodes), np.empty(n_nodes) + # Allocate small vector for the nodes indices for a given cell + nodes_indices = np.empty(8, dtype=cell_nodes.dtype) + for j in range(n_nodes): + dx = nodes[j, 0] - receivers[i, 0] + dy = nodes[j, 1] - receivers[i, 1] + dz = nodes[j, 2] - receivers[i, 2] + distance = np.sqrt(dx**2 + dy**2 + dz**2) + kxx[j] = kernel_xx(dx, dy, dz, distance) + kyy[j] = kernel_yy(dx, dy, dz, distance) + kzz[j] = kernel_zz(dx, dy, dz, distance) + kxy[j] = kernel_xy(dx, dy, dz, distance) + kxz[j] = kernel_xz(dx, dy, dz, distance) + kyz[j] = kernel_yz(dx, dy, dz, distance) + # Compute sensitivity matrix elements from the kernel values + for k in range(n_cells): + nodes_indices = cell_nodes[k, :] + uxx = kernels_in_nodes_to_cell(kxx, nodes_indices) + uyy = kernels_in_nodes_to_cell(kyy, nodes_indices) + uzz = kernels_in_nodes_to_cell(kzz, nodes_indices) + uxy = kernels_in_nodes_to_cell(kxy, nodes_indices) + uxz = kernels_in_nodes_to_cell(kxz, nodes_indices) + uyz = kernels_in_nodes_to_cell(kyz, nodes_indices) + bx = uxx * fx + uxy * fy + uxz * fz + by = uxy * fx + uyy * fy + uyz * fz + bz = uxz * fx + uyz * fy + uzz * fz + if scalar_model: + fields[i] += ( + constant_factor + * model[k] + * regional_field_amplitude + * (bx * fx + by * fy + bz * fz) + ) + else: + fields[i] += ( + constant_factor + * regional_field_amplitude + * ( + bx * model[k] + + by * model[k + n_cells] + + bz * model[k + 2 * n_cells] + ) + ) + + +NUMBA_FUNCTIONS_3D = { + "forward": { + "tmi": { + parallel: jit(nopython=True, parallel=parallel)(_forward_tmi) + for parallel in (True, False) + }, + "magnetic_component": { + parallel: jit(nopython=True, parallel=parallel)(_forward_mag) + for parallel in (True, False) + }, + "tmi_derivative": { + parallel: jit(nopython=True, parallel=parallel)(_forward_tmi_derivative) + for parallel in (True, False) + }, + }, + "sensitivity": { + "tmi": { + parallel: jit(nopython=True, parallel=parallel)(_sensitivity_tmi) + for parallel in (True, False) + }, + "magnetic_component": { + parallel: jit(nopython=True, parallel=parallel)(_sensitivity_mag) + for parallel in (True, False) + }, + "tmi_derivative": { + parallel: jit(nopython=True, parallel=parallel)(_sensitivity_tmi_derivative) + for parallel in (True, False) + }, + }, + "gt_dot_v": { + "tmi": { + False: _tmi_sensitivity_t_dot_v_serial, + True: _tmi_sensitivity_t_dot_v_parallel, + }, + "magnetic_component": { + False: _mag_sensitivity_t_dot_v_serial, + True: _mag_sensitivity_t_dot_v_parallel, + }, + "tmi_derivative": { + False: _tmi_derivative_sensitivity_t_dot_v_serial, + True: _tmi_derivative_sensitivity_t_dot_v_parallel, + }, + }, + "diagonal_gtg": { + "tmi": { + False: _diagonal_G_T_dot_G_tmi_serial, + True: _diagonal_G_T_dot_G_tmi_parallel, + }, + "magnetic_component": { + False: _diagonal_G_T_dot_G_mag_serial, + True: _diagonal_G_T_dot_G_mag_parallel, + }, + "tmi_derivative": { + False: _diagonal_G_T_dot_G_tmi_deriv_serial, + True: _diagonal_G_T_dot_G_tmi_deriv_serial, + }, + }, +} diff --git a/simpeg/potential_fields/magnetics/_numba/__init__.py b/simpeg/potential_fields/magnetics/_numba/__init__.py new file mode 100644 index 0000000000..b0acae3ea3 --- /dev/null +++ b/simpeg/potential_fields/magnetics/_numba/__init__.py @@ -0,0 +1,14 @@ +""" +Numba functions for magnetic simulations. +""" + +from ._2d_mesh import NUMBA_FUNCTIONS_2D +from ._3d_mesh import NUMBA_FUNCTIONS_3D + +try: + import choclo +except ImportError: + choclo = None + + +__all__ = ["choclo", "NUMBA_FUNCTIONS_3D", "NUMBA_FUNCTIONS_2D"] diff --git a/simpeg/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py index 5d3d27171c..6b403ddda8 100644 --- a/simpeg/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -1,5 +1,9 @@ +import hashlib +import warnings import numpy as np +from numpy.typing import NDArray import scipy.sparse as sp +from functools import cached_property from geoana.kernels import ( prism_fxxy, prism_fxxz, @@ -10,21 +14,147 @@ prism_fzzz, ) from scipy.constants import mu_0 +from scipy.sparse.linalg import LinearOperator, aslinearoperator -from simpeg import Solver, props, utils +from simpeg import props, utils from simpeg.utils import mat_utils, mkvc, sdiag -from simpeg.utils.code_utils import deprecate_property, validate_string, validate_type +from simpeg.utils.code_utils import validate_string, validate_type from ...base import BaseMagneticPDESimulation from ..base import BaseEquivalentSourceLayerSimulation, BasePFSimulation -from .analytics import CongruousMagBC from .survey import Survey +from ._numba import choclo, NUMBA_FUNCTIONS_3D, NUMBA_FUNCTIONS_2D + +if choclo is not None: + CHOCLO_SUPPORTED_COMPONENTS = { + "tmi", + "bx", + "by", + "bz", + "bxx", + "byy", + "bzz", + "bxy", + "bxz", + "byz", + "tmi_x", + "tmi_y", + "tmi_z", + } + CHOCLO_KERNELS = { + "bx": (choclo.prism.kernel_ee, choclo.prism.kernel_en, choclo.prism.kernel_eu), + "by": (choclo.prism.kernel_en, choclo.prism.kernel_nn, choclo.prism.kernel_nu), + "bz": (choclo.prism.kernel_eu, choclo.prism.kernel_nu, choclo.prism.kernel_uu), + "bxx": ( + choclo.prism.kernel_eee, + choclo.prism.kernel_een, + choclo.prism.kernel_eeu, + ), + "byy": ( + choclo.prism.kernel_enn, + choclo.prism.kernel_nnn, + choclo.prism.kernel_nnu, + ), + "bzz": ( + choclo.prism.kernel_euu, + choclo.prism.kernel_nuu, + choclo.prism.kernel_uuu, + ), + "bxy": ( + choclo.prism.kernel_een, + choclo.prism.kernel_enn, + choclo.prism.kernel_enu, + ), + "bxz": ( + choclo.prism.kernel_eeu, + choclo.prism.kernel_enu, + choclo.prism.kernel_euu, + ), + "byz": ( + choclo.prism.kernel_enu, + choclo.prism.kernel_nnu, + choclo.prism.kernel_nuu, + ), + "tmi_x": ( + choclo.prism.kernel_eee, + choclo.prism.kernel_enn, + choclo.prism.kernel_euu, + choclo.prism.kernel_een, + choclo.prism.kernel_eeu, + choclo.prism.kernel_enu, + ), + "tmi_y": ( + choclo.prism.kernel_een, + choclo.prism.kernel_nnn, + choclo.prism.kernel_nuu, + choclo.prism.kernel_enn, + choclo.prism.kernel_enu, + choclo.prism.kernel_nnu, + ), + "tmi_z": ( + choclo.prism.kernel_eeu, + choclo.prism.kernel_nnu, + choclo.prism.kernel_uuu, + choclo.prism.kernel_enu, + choclo.prism.kernel_euu, + choclo.prism.kernel_nuu, + ), + } + CHOCLO_FORWARD_FUNCS = { + "bx": choclo.prism.magnetic_e, + "by": choclo.prism.magnetic_n, + "bz": choclo.prism.magnetic_u, + "bxx": choclo.prism.magnetic_ee, + "byy": choclo.prism.magnetic_nn, + "bzz": choclo.prism.magnetic_uu, + "bxy": choclo.prism.magnetic_en, + "bxz": choclo.prism.magnetic_eu, + "byz": choclo.prism.magnetic_nu, + } + class Simulation3DIntegral(BasePFSimulation): """ - magnetic simulation in integral form. + Magnetic simulation in integral form. + Parameters + ---------- + mesh : discretize.TreeMesh or discretize.TensorMesh + Mesh use to run the magnetic simulation. + survey : simpeg.potential_fields.magnetics.Survey + Magnetic survey with information of the receivers. + active_cells : (n_cells) numpy.ndarray, optional + Array that indicates which cells in ``mesh`` are active cells. + chi : numpy.ndarray, optional + Susceptibility array for the active cells in the mesh. + chiMap : Mapping, optional + Model mapping. + model_type : str, optional + Whether the model are susceptibilities of the cells (``"scalar"``), + or effective susceptibilities (``"vector"``). + is_amplitude_data : bool, optional + If True, the returned fields will be the amplitude of the magnetic + field. If False, the fields will be returned unmodified. + sensitivity_dtype : numpy.dtype, optional + Data type that will be used to build the sensitivity matrix. + store_sensitivities : {"ram", "disk", "forward_only"} + Options for storing sensitivity matrix. There are 3 options + + - 'ram': sensitivities are stored in the computer's RAM + - 'disk': sensitivities are written to a directory + - 'forward_only': you intend only do perform a forward simulation and + sensitivities do not need to be stored + + sensitivity_path : str, optional + Path to store the sensitivity matrix if ``store_sensitivities`` is set + to ``"disk"``. Default to "./sensitivities". + engine : {"geoana", "choclo"}, optional + Choose which engine should be used to run the forward model. + numba_parallel : bool, optional + If True, the simulation will run in parallel. If False, it will + run in serial. If ``engine`` is not ``"choclo"`` this argument will be + ignored. """ chi, chiMap, chiDeriv = props.Invertible("Magnetic Susceptibility (SI)") @@ -36,19 +166,29 @@ def __init__( chiMap=None, model_type="scalar", is_amplitude_data=False, - **kwargs + engine="geoana", + numba_parallel=True, + **kwargs, ): self.model_type = model_type - super().__init__(mesh, **kwargs) + super().__init__(mesh, engine=engine, numba_parallel=numba_parallel, **kwargs) self.chi = chi self.chiMap = chiMap - self._G = None self._M = None - self._gtg_diagonal = None self.is_amplitude_data = is_amplitude_data self.modelMap = self.chiMap + # Warn if n_processes has been passed + if self.engine == "choclo" and "n_processes" in kwargs: + warnings.warn( + "The 'n_processes' will be ignored when selecting 'choclo' as the " + "engine in the magnetic simulation.", + UserWarning, + stacklevel=1, + ) + self.n_processes = None + @property def model_type(self): """Type of magnetization model @@ -103,7 +243,10 @@ def fields(self, model): self.model = model # model = self.chiMap * model if self.store_sensitivities == "forward_only": - fields = mkvc(self.linear_operator()) + if self.engine == "choclo": + fields = self._forward(self.chi) + else: + fields = mkvc(self.linear_operator()) else: fields = np.asarray( self.G @ self.chi.astype(self.sensitivity_dtype, copy=False) @@ -115,23 +258,32 @@ def fields(self, model): return fields @property - def G(self): - if getattr(self, "_G", None) is None: - self._G = self.linear_operator() - + def G(self) -> NDArray | np.memmap | LinearOperator: + if not hasattr(self, "_G"): + match self.engine, self.store_sensitivities: + case ("choclo", "forward_only"): + self._G = self._sensitivity_matrix_as_operator() + case ("choclo", _): + self._G = self._sensitivity_matrix() + case ("geoana", "forward_only"): + msg = ( + "Accessing matrix G with " + 'store_sensitivities="forward_only" and engine="geoana" ' + "hasn't been implemented yet. " + 'Choose store_sensitivities="ram" or "disk", ' + 'or another engine, like "choclo".' + ) + raise NotImplementedError(msg) + case ("geoana", _): + self._G = self.linear_operator() return self._G - modelType = deprecate_property( - model_type, "modelType", "model_type", removal_version="0.18.0", error=True - ) - @property def nD(self): """ Number of data """ - self._nD = self.survey.receiver_locations.shape[0] - + self._nD = self.survey.nD if not self.is_amplitude_data else self.survey.nD // 3 return self._nD @property @@ -145,42 +297,195 @@ def tmi_projection(self): return self._tmi_projection - def getJtJdiag(self, m, W=None, f=None): + def getJ(self, m, f=None) -> NDArray[np.float64 | np.float32] | LinearOperator: + r""" + Sensitivity matrix :math:`\mathbf{J}`. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (nD, n_params) np.ndarray or scipy.sparse.linalg.LinearOperator. + Array or :class:`~scipy.sparse.linalg.LinearOperator` for the + :math:`\mathbf{J}` matrix. + A :class:`~scipy.sparse.linalg.LinearOperator` will be returned if + ``store_sensitivities`` is ``"forward_only"``, otherwise a dense + array will be returned. + + Notes + ----- + If ``store_sensitivities`` is ``"ram"`` or ``"disk"``, a dense array + for the ``J`` matrix is returned. + A :class:`~scipy.sparse.linalg.LinearOperator` is returned if + ``store_sensitivities`` is ``"forward_only"``. This object can perform + operations like ``J @ m`` or ``J.T @ v`` without allocating the full + ``J`` matrix in memory. """ - Return the diagonal of JtJ + if self.is_amplitude_data: + msg = ( + "The `getJ` method is not yet implemented to work with " + "`is_amplitude_data`." + ) + raise NotImplementedError(msg) + + # Need to assign the model, so the chiDeriv can be computed (if the + # model is None, the chiDeriv is going to be Zero). + self.model = m + chiDeriv = ( + self.chiDeriv + if not isinstance(self.G, LinearOperator) + else aslinearoperator(self.chiDeriv) + ) + return self.G @ chiDeriv + + def getJtJdiag(self, m, W=None, f=None): + r""" + Compute diagonal of :math:`\mathbf{J}^T \mathbf{J}``. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + W : (nD, nD) np.ndarray or scipy.sparse.sparray, optional + Diagonal matrix with the square root of the weights. If not None, + the function returns the diagonal of + :math:`\mathbf{J}^T \mathbf{W}^T \mathbf{W} \mathbf{J}``. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (nparam) np.ndarray + Array with the diagonal of ``J.T @ J``. + + Notes + ----- + If ``store_sensitivities`` is ``"forward_only"``, the ``G`` matrix is + never allocated in memory, and the diagonal is obtained by + accumulation, computing each element of the ``G`` matrix on the fly. + + This method caches the diagonal ``G.T @ W.T @ W @ G`` and the sha256 + hash of the diagonal of the ``W`` matrix. This way, if same weights are + passed to it, it reuses the cached diagonal so it doesn't need to be + recomputed. + If new weights are passed, the cache is updated with the latest + diagonal of ``G.T @ W.T @ W @ G``. """ + # Need to assign the model, so the chiDeriv can be computed (if the + # model is None, the chiDeriv is going to be Zero). self.model = m - if W is None: - W = np.ones(self.survey.nD) - else: - W = W.diagonal() ** 2 - if getattr(self, "_gtg_diagonal", None) is None: - diag = np.zeros(self.G.shape[1]) - if not self.is_amplitude_data: - for i in range(len(W)): - diag += W[i] * (self.G[i] * self.G[i]) - else: + # We should probably check that W is diagonal. Let's assume it for now. + weights = ( + W.diagonal() ** 2 + if W is not None + else np.ones(self.survey.nD, dtype=np.float64) + ) + + # Compute gtg (G.T @ W.T @ W @ G) if it's not cached, or if the + # weights are not the same. + weights_sha256 = hashlib.sha256(weights) + use_cached_gtg = ( + hasattr(self, "_gtg_diagonal") + and hasattr(self, "_weights_sha256") + and self._weights_sha256.digest() == weights_sha256.digest() + ) + if not use_cached_gtg: + self._gtg_diagonal = self._get_gtg_diagonal(weights) + self._weights_sha256 = weights_sha256 + + # Multiply the gtg_diagonal by the derivative of the mapping + diagonal = mkvc( + (sdiag(np.sqrt(self._gtg_diagonal)) @ self.chiDeriv).power(2).sum(axis=0) + ) + return diagonal + + def _get_gtg_diagonal(self, weights: NDArray) -> NDArray: + """ + Compute the diagonal of ``G.T @ W.T @ W @ G``. + + Parameters + ---------- + weights : np.ndarray + Weights array: diagonal of ``W.T @ W``. + + Returns + ------- + np.ndarray + """ + match (self.engine, self.store_sensitivities, self.is_amplitude_data): + case ("geoana", "forward_only", _): + msg = ( + "Computing the diagonal of `G.T @ G` using " + "`'forward_only'` and `'geoana'` as engine hasn't been " + "implemented yet." + ) + raise NotImplementedError(msg) + case ("choclo", "forward_only", True): + msg = ( + "Computing the diagonal of `G.T @ G` using " + "`'forward_only'` and `is_amplitude_data` hasn't been " + "implemented yet." + ) + raise NotImplementedError(msg) + case ("choclo", "forward_only", False): + gtg_diagonal = self._gtg_diagonal_without_building_g(weights) + case (_, _, False): + # In Einstein notation, the j-th element of the diagonal is: + # d_j = w_i * G_{ij} * G_{ij} + gtg_diagonal = np.asarray( + np.einsum("i,ij,ij->j", weights, self.G, self.G) + ) + case (_, _, True): ampDeriv = self.ampDeriv Gx = self.G[::3] Gy = self.G[1::3] Gz = self.G[2::3] - for i in range(len(W)): + gtg_diagonal = np.zeros(self.G.shape[1]) + for i in range(weights.size): row = ( ampDeriv[0, i] * Gx[i] + ampDeriv[1, i] * Gy[i] + ampDeriv[2, i] * Gz[i] ) - diag += W[i] * (row * row) - self._gtg_diagonal = diag - else: - diag = self._gtg_diagonal - return mkvc((sdiag(np.sqrt(diag)) @ self.chiDeriv).power(2).sum(axis=0)) + gtg_diagonal += weights[i] * (row * row) + return gtg_diagonal def Jvec(self, m, v, f=None): + """ + Dot product between sensitivity matrix and a vector. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. This array is used to compute the ``J`` + matrix. + v : (n_param,) numpy.ndarray + Vector used in the matrix-vector multiplication. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (nD,) numpy.ndarray + + Notes + ----- + If ``store_sensitivities`` is set to ``"forward_only"``, then the + matrix `G` is never fully constructed, and the dot product is computed + by accumulation, computing the matrix elements on the fly. Otherwise, + the full matrix ``G`` is constructed and stored either in memory or + disk. + """ + # Need to assign the model, so the chiDeriv can be computed (if the + # model is None, the chiDeriv is going to be Zero). self.model = m dmu_dm_v = self.chiDeriv @ v - Jvec = self.G @ dmu_dm_v.astype(self.sensitivity_dtype, copy=False) if self.is_amplitude_data: @@ -188,10 +493,37 @@ def Jvec(self, m, v, f=None): Jvec = Jvec.reshape((-1, 3)).T # reshape((3, -1), order="F") ampDeriv_Jvec = self.ampDeriv * Jvec return ampDeriv_Jvec[0] + ampDeriv_Jvec[1] + ampDeriv_Jvec[2] - else: - return Jvec + + return Jvec def Jtvec(self, m, v, f=None): + """ + Dot product between transposed sensitivity matrix and a vector. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. This array is used to compute the ``J`` + matrix. + v : (nD,) numpy.ndarray + Vector used in the matrix-vector multiplication. + f : Ignored + Not used, present here for API consistency by convention. + + Returns + ------- + (nD,) numpy.ndarray + + Notes + ----- + If ``store_sensitivities`` is set to ``"forward_only"``, then the + matrix `G` is never fully constructed, and the dot product is computed + by accumulation, computing the matrix elements on the fly. Otherwise, + the full matrix ``G`` is constructed and stored either in memory or + disk. + """ + # Need to assign the model, so the chiDeriv can be computed (if the + # model is None, the chiDeriv is going to be Zero). self.model = m if self.is_amplitude_data: @@ -488,12 +820,417 @@ def evaluate_integral(self, receiver_location, components): ) @property - def deleteTheseOnModelUpdate(self): - deletes = super().deleteTheseOnModelUpdate + def _delete_on_model_update(self): + deletes = super()._delete_on_model_update if self.is_amplitude_data: deletes = deletes + ["_gtg_diagonal", "_ampDeriv"] return deletes + def _forward(self, model): + """ + Forward model the fields of active cells in the mesh on receivers. + + Parameters + ---------- + model : (n_active_cells) or (3 * n_active_cells) array + Array containing the susceptibilities (scalar) or effective + susceptibilities (vector) of the active cells in the mesh, in SI + units. + Susceptibilities are expected if ``model_type`` is ``"scalar"``, + and the array should have ``n_active_cells`` elements. + Effective susceptibilities are expected if ``model_type`` is + ``"vector"``, and the array should have ``3 * n_active_cells`` + elements. + + Returns + ------- + (nD, ) array + Always return a ``np.float64`` array. + """ + # Gather active nodes and the indices of the nodes for each active cell + active_nodes, active_cell_nodes = self._get_active_nodes() + # Get regional field + regional_field = self.survey.source_field.b0 + # Allocate fields array + fields = np.zeros(self.survey.nD, dtype=self.sensitivity_dtype) + # Define the constant factor + constant_factor = 1 / 4 / np.pi + # Start computing the fields + index_offset = 0 + scalar_model = self.model_type == "scalar" + for components, receivers in self._get_components_and_receivers(): + if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): + raise NotImplementedError( + f"Other components besides {CHOCLO_SUPPORTED_COMPONENTS} " + "aren't implemented yet." + ) + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + vector_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + if component == "tmi": + forward_func = NUMBA_FUNCTIONS_3D["forward"]["tmi"][ + self.numba_parallel + ] + forward_func( + receivers, + active_nodes, + model, + fields[vector_slice], + active_cell_nodes, + regional_field, + constant_factor, + scalar_model, + ) + elif component in ("tmi_x", "tmi_y", "tmi_z"): + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( + CHOCLO_KERNELS[component] + ) + forward_func = NUMBA_FUNCTIONS_3D["forward"]["tmi_derivative"][ + self.numba_parallel + ] + forward_func( + receivers, + active_nodes, + model, + fields[vector_slice], + active_cell_nodes, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + ) + else: + kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] + forward_func = NUMBA_FUNCTIONS_3D["forward"]["magnetic_component"][ + self.numba_parallel + ] + forward_func( + receivers, + active_nodes, + model, + fields[vector_slice], + active_cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + ) + index_offset += n_rows + return fields + + def _sensitivity_matrix(self): + """ + Compute the sensitivity matrix G + + Returns + ------- + (nD, n_active_cells) array + """ + # Gather active nodes and the indices of the nodes for each active cell + active_nodes, active_cell_nodes = self._get_active_nodes() + # Get regional field + regional_field = self.survey.source_field.b0 + # Allocate sensitivity matrix + scalar_model = self.model_type == "scalar" + n_columns = self.nC if scalar_model else 3 * self.nC + shape = (self.survey.nD, n_columns) + if self.store_sensitivities == "disk": + sensitivity_matrix = np.memmap( + self.sensitivity_path, + shape=shape, + dtype=self.sensitivity_dtype, + order="C", # it's more efficient to write in row major + mode="w+", + ) + else: + sensitivity_matrix = np.empty(shape, dtype=self.sensitivity_dtype) + # Define the constant factor + constant_factor = 1 / 4 / np.pi + # Start filling the sensitivity matrix + index_offset = 0 + for components, receivers in self._get_components_and_receivers(): + if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): + raise NotImplementedError( + f"Other components besides {CHOCLO_SUPPORTED_COMPONENTS} " + "aren't implemented yet." + ) + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + matrix_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + if component == "tmi": + sensitivity_func = NUMBA_FUNCTIONS_3D["sensitivity"]["tmi"][ + self.numba_parallel + ] + sensitivity_func( + receivers, + active_nodes, + sensitivity_matrix[matrix_slice, :], + active_cell_nodes, + regional_field, + constant_factor, + scalar_model, + ) + elif component in ("tmi_x", "tmi_y", "tmi_z"): + sensitivity_func = NUMBA_FUNCTIONS_3D["sensitivity"][ + "tmi_derivative" + ][self.numba_parallel] + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( + CHOCLO_KERNELS[component] + ) + sensitivity_func( + receivers, + active_nodes, + sensitivity_matrix[matrix_slice, :], + active_cell_nodes, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + ) + else: + sensitivity_func = NUMBA_FUNCTIONS_3D["sensitivity"][ + "magnetic_component" + ][self.numba_parallel] + kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] + sensitivity_func( + receivers, + active_nodes, + sensitivity_matrix[matrix_slice, :], + active_cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + ) + index_offset += n_rows + return sensitivity_matrix + + def _sensitivity_matrix_as_operator(self): + """ + Create a LinearOperator for the sensitivity matrix G. + + Returns + ------- + scipy.sparse.linalg.LinearOperator + """ + n_columns = self.nC if self.model_type == "scalar" else self.nC * 3 + shape = (self.survey.nD, n_columns) + linear_op = LinearOperator( + shape=shape, + matvec=self._forward, + rmatvec=self._sensitivity_matrix_transpose_dot_vec, + dtype=np.float64, + ) + return linear_op + + def _sensitivity_matrix_transpose_dot_vec(self, vector): + """ + Compute ``G.T @ v`` without building ``G``. + + Parameters + ---------- + vector : (nD) numpy.ndarray + Vector used in the dot product. + + Returns + ------- + (n_active_cells) or (3 * n_active_cells) numpy.ndarray + """ + # Gather active nodes and the indices of the nodes for each active cell + active_nodes, active_cell_nodes = self._get_active_nodes() + # Get regional field + regional_field = self.survey.source_field.b0 + + # Allocate resulting array. + scalar_model = self.model_type == "scalar" + result = np.zeros(self.nC if scalar_model else 3 * self.nC) + + # Define the constant factor + constant_factor = 1 / 4 / np.pi + + # Fill the result array + index_offset = 0 + for components, receivers in self._get_components_and_receivers(): + if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): + raise NotImplementedError( + f"Other components besides {CHOCLO_SUPPORTED_COMPONENTS} " + "aren't implemented yet." + ) + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + vector_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + if component == "tmi": + gt_dot_v_func = NUMBA_FUNCTIONS_3D["gt_dot_v"]["tmi"][ + self.numba_parallel + ] + gt_dot_v_func( + receivers, + active_nodes, + active_cell_nodes, + regional_field, + constant_factor, + scalar_model, + vector[vector_slice], + result, + ) + elif component in ("tmi_x", "tmi_y", "tmi_z"): + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( + CHOCLO_KERNELS[component] + ) + gt_dot_v_func = NUMBA_FUNCTIONS_3D["gt_dot_v"]["tmi_derivative"][ + self.numba_parallel + ] + gt_dot_v_func( + receivers, + active_nodes, + active_cell_nodes, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + vector[vector_slice], + result, + ) + else: + kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] + gt_dot_v_func = NUMBA_FUNCTIONS_3D["gt_dot_v"][ + "magnetic_component" + ][self.numba_parallel] + gt_dot_v_func( + receivers, + active_nodes, + active_cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + vector[vector_slice], + result, + ) + index_offset += n_rows + return result + + def _gtg_diagonal_without_building_g(self, weights): + """ + Compute the diagonal of ``G.T @ G`` without building the ``G`` matrix. + + Parameters + ----------- + weights : (nD,) array + Array with data weights. It should be the diagonal of the ``W`` + matrix, squared. + + Returns + ------- + (n_active_cells) numpy.ndarray + """ + # Gather active nodes and the indices of the nodes for each active cell + active_nodes, active_cell_nodes = self._get_active_nodes() + # Get regional field + regional_field = self.survey.source_field.b0 + # Define the constant factor + constant_factor = 1 / 4 / np.pi + + # Allocate array for the diagonal + scalar_model = self.model_type == "scalar" + n_columns = self.nC if scalar_model else 3 * self.nC + diagonal = np.zeros(n_columns, dtype=np.float64) + + # Start filling the diagonal array + for components, receivers in self._get_components_and_receivers(): + if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): + raise NotImplementedError( + f"Other components besides {CHOCLO_SUPPORTED_COMPONENTS} " + "aren't implemented yet." + ) + for component in components: + if component == "tmi": + diagonal_gtg_func = NUMBA_FUNCTIONS_3D["diagonal_gtg"]["tmi"][ + self.numba_parallel + ] + diagonal_gtg_func( + receivers, + active_nodes, + active_cell_nodes, + regional_field, + constant_factor, + scalar_model, + weights, + diagonal, + ) + elif component in ("tmi_x", "tmi_y", "tmi_z"): + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( + CHOCLO_KERNELS[component] + ) + diagonal_gtg_func = NUMBA_FUNCTIONS_3D["diagonal_gtg"][ + "tmi_derivative" + ][self.numba_parallel] + diagonal_gtg_func( + receivers, + active_nodes, + active_cell_nodes, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + weights, + diagonal, + ) + else: + kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] + diagonal_gtg_func = NUMBA_FUNCTIONS_3D["diagonal_gtg"][ + "magnetic_component" + ][self.numba_parallel] + diagonal_gtg_func( + receivers, + active_nodes, + active_cell_nodes, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + weights, + diagonal, + ) + return diagonal + class SimulationEquivalentSourceLayer( BaseEquivalentSourceLayerSimulation, Simulation3DIntegral @@ -506,517 +1243,958 @@ class SimulationEquivalentSourceLayer( mesh : discretize.BaseMesh A 2D tensor or tree mesh defining discretization along the x and y directions cell_z_top : numpy.ndarray or float - Define the elevations for the top face of all cells in the layer + Define the elevations for the top face of all cells in the layer. + If an array it should be the same size as the active cell set. cell_z_bottom : numpy.ndarray or float - Define the elevations for the bottom face of all cells in the layer + Define the elevations for the bottom face of all cells in the layer. + If an array it should be the same size as the active cell set. + engine : {"geoana", "choclo"}, optional + Choose which engine should be used to run the forward model. + numba_parallel : bool, optional + If True, the simulation will run in parallel. If False, it will + run in serial. If ``engine`` is not ``"choclo"`` this argument will be + ignored. """ - -class Simulation3DDifferential(BaseMagneticPDESimulation): - """ - Secondary field approach using differential equations! - """ - - def __init__(self, mesh, survey=None, **kwargs): - super().__init__(mesh, survey=survey, **kwargs) - - Pbc, Pin, self._Pout = self.mesh.get_BC_projections( - "neumann", discretization="CC" + def __init__( + self, + mesh, + cell_z_top, + cell_z_bottom, + engine="geoana", + numba_parallel=True, + **kwargs, + ): + super().__init__( + mesh, + cell_z_top, + cell_z_bottom, + engine=engine, + numba_parallel=numba_parallel, + **kwargs, ) - Dface = self.mesh.face_divergence - Mc = sdiag(self.mesh.cell_volumes) - self._Div = Mc * Dface * Pin.T.tocsr() * Pin - - @property - def survey(self): - """The survey for this simulation. + def _forward(self, model): + """ + Forward model the fields of active cells in the mesh on receivers. + + Parameters + ---------- + model : (n_active_cells) or (3 * n_active_cells) array + Array containing the susceptibilities (scalar) or effective + susceptibilities (vector) of the active cells in the mesh, in SI + units. + Susceptibilities are expected if ``model_type`` is ``"scalar"``, + and the array should have ``n_active_cells`` elements. + Effective susceptibilities are expected if ``model_type`` is + ``"vector"``, and the array should have ``3 * n_active_cells`` + elements. Returns ------- - simpeg.potential_fields.magnetics.survey.Survey + (nD, ) array + Always return a ``np.float64`` array. """ - if self._survey is None: - raise AttributeError("Simulation must have a survey") - return self._survey - - @survey.setter - def survey(self, obj): - if obj is not None: - obj = validate_type("survey", obj, Survey, cast=False) - self._survey = obj + # Get cells in the 2D mesh and keep only active cells + cells_bounds_active = self.mesh.cell_bounds[self.active_cells] + # Get regional field + regional_field = self.survey.source_field.b0 + # Allocate fields array + fields = np.zeros(self.survey.nD, dtype=self.sensitivity_dtype) + # Start computing the fields + index_offset = 0 + scalar_model = self.model_type == "scalar" + for components, receivers in self._get_components_and_receivers(): + if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): + raise NotImplementedError( + f"Other components besides {CHOCLO_SUPPORTED_COMPONENTS} " + "aren't implemented yet." + ) + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + vector_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + if component == "tmi": + forward_func = NUMBA_FUNCTIONS_2D["forward"]["tmi"][ + self.numba_parallel + ] + forward_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + model, + fields[vector_slice], + regional_field, + scalar_model, + ) + elif component in ("tmi_x", "tmi_y", "tmi_z"): + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( + CHOCLO_KERNELS[component] + ) + forward_func = NUMBA_FUNCTIONS_2D["forward"]["tmi_derivative"][ + self.numba_parallel + ] + forward_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + model, + fields[vector_slice], + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + scalar_model, + ) + else: + choclo_forward_func = CHOCLO_FORWARD_FUNCS[component] + forward_func = NUMBA_FUNCTIONS_2D["forward"]["magnetic_component"][ + self.numba_parallel + ] + forward_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + model, + fields[vector_slice], + regional_field, + choclo_forward_func, + scalar_model, + ) + index_offset += n_rows + return fields - @property - def MfMuI(self): - return self._MfMuI + def _sensitivity_matrix(self): + """ + Compute the sensitivity matrix G - @property - def MfMui(self): - return self._MfMui + Returns + ------- + (nD, n_active_cells) array + """ + # Get cells in the 2D mesh and keep only active cells + cells_bounds_active = self.mesh.cell_bounds[self.active_cells] + # Get regional field + regional_field = self.survey.source_field.b0 + # Allocate sensitivity matrix + scalar_model = self.model_type == "scalar" + n_columns = self.nC if scalar_model else 3 * self.nC + shape = (self.survey.nD, n_columns) + if self.store_sensitivities == "disk": + sensitivity_matrix = np.memmap( + self.sensitivity_path, + shape=shape, + dtype=self.sensitivity_dtype, + order="C", # it's more efficient to write in row major + mode="w+", + ) + else: + sensitivity_matrix = np.empty(shape, dtype=self.sensitivity_dtype) + # Start filling the sensitivity matrix + index_offset = 0 + for components, receivers in self._get_components_and_receivers(): + if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): + raise NotImplementedError( + f"Other components besides {CHOCLO_SUPPORTED_COMPONENTS} " + "aren't implemented yet." + ) + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + matrix_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + if component == "tmi": + sensitivity_func = NUMBA_FUNCTIONS_2D["sensitivity"]["tmi"][ + self.numba_parallel + ] + sensitivity_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + sensitivity_matrix[matrix_slice, :], + regional_field, + scalar_model, + ) + elif component in ("tmi_x", "tmi_y", "tmi_z"): + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( + CHOCLO_KERNELS[component] + ) + sensitivity_func = NUMBA_FUNCTIONS_2D["sensitivity"][ + "tmi_derivative" + ][self.numba_parallel] + sensitivity_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + sensitivity_matrix[matrix_slice, :], + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + scalar_model, + ) + else: + kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] + sensitivity_func = NUMBA_FUNCTIONS_2D["sensitivity"][ + "magnetic_component" + ][self.numba_parallel] + sensitivity_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + sensitivity_matrix[matrix_slice, :], + regional_field, + kernel_x, + kernel_y, + kernel_z, + scalar_model, + ) + index_offset += n_rows + return sensitivity_matrix - @property - def MfMu0(self): - return self._MfMu0 - - def makeMassMatrices(self, m): - mu = self.muMap * m - self._MfMui = self.mesh.get_face_inner_product(1.0 / mu) / self.mesh.dim - # self._MfMui = self.mesh.get_face_inner_product(1./mu) - # TODO: this will break if tensor mu - self._MfMuI = sdiag(1.0 / self._MfMui.diagonal()) - self._MfMu0 = self.mesh.get_face_inner_product(1.0 / mu_0) / self.mesh.dim - # self._MfMu0 = self.mesh.get_face_inner_product(1/mu_0) + def _sensitivity_matrix_transpose_dot_vec(self, vector): + """ + Compute ``G.T @ v`` without building ``G``. - @utils.requires("survey") - def getB0(self): - b0 = self.survey.source_field.b0 - B0 = np.r_[ - b0[0] * np.ones(self.mesh.nFx), - b0[1] * np.ones(self.mesh.nFy), - b0[2] * np.ones(self.mesh.nFz), - ] - return B0 + Parameters + ---------- + vector : (nD) numpy.ndarray + Vector used in the dot product. - def getRHS(self, m): - r""" + Returns + ------- + (n_active_cells) or (3 * n_active_cells) numpy.ndarray + """ + # Get regional field + regional_field = self.survey.source_field.b0 + # Get cells in the 2D mesh and keep only active cells + cells_bounds_active = self.mesh.cell_bounds[self.active_cells] + # Allocate resulting array + scalar_model = self.model_type == "scalar" + result = np.zeros(self.nC if scalar_model else 3 * self.nC) + # Start filling the result array + index_offset = 0 + for components, receivers in self._get_components_and_receivers(): + if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): + raise NotImplementedError( + f"Other components besides {CHOCLO_SUPPORTED_COMPONENTS} " + "aren't implemented yet." + ) + n_components = len(components) + n_rows = n_components * receivers.shape[0] + for i, component in enumerate(components): + vector_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + if component == "tmi": + gt_dot_v_func = NUMBA_FUNCTIONS_2D["gt_dot_v"]["tmi"][ + self.numba_parallel + ] + gt_dot_v_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + regional_field, + scalar_model, + vector[vector_slice], + result, + ) + elif component in ("tmi_x", "tmi_y", "tmi_z"): + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( + CHOCLO_KERNELS[component] + ) + gt_dot_v_func = NUMBA_FUNCTIONS_2D["gt_dot_v"]["tmi_derivative"][ + self.numba_parallel + ] + gt_dot_v_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + scalar_model, + vector[vector_slice], + result, + ) + else: + kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] + gt_dot_v_func = NUMBA_FUNCTIONS_2D["gt_dot_v"][ + "magnetic_component" + ][self.numba_parallel] + gt_dot_v_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + regional_field, + kernel_x, + kernel_y, + kernel_z, + scalar_model, + vector[vector_slice], + result, + ) + index_offset += n_rows + return result - .. math :: + def _gtg_diagonal_without_building_g(self, weights): + """ + Compute the diagonal of ``G.T @ G`` without building the ``G`` matrix. - \mathbf{rhs} = - \Div(\MfMui)^{-1}\mathbf{M}^f_{\mu_0^{-1}}\mathbf{B}_0 - - \Div\mathbf{B}_0 - +\diag(v)\mathbf{D} \mathbf{P}_{out}^T \mathbf{B}_{sBC} + Parameters + ----------- + weights : (nD,) array + Array with data weights. It should be the diagonal of the ``W`` + matrix, squared. + Returns + ------- + (n_active_cells) numpy.ndarray """ - B0 = self.getB0() + # Get regional field + regional_field = self.survey.source_field.b0 + # Get cells in the 2D mesh and keep only active cells + cells_bounds_active = self.mesh.cell_bounds[self.active_cells] + # Define the constant factor + constant_factor = 1 / 4 / np.pi + # Allocate array for the diagonal + scalar_model = self.model_type == "scalar" + n_columns = self.nC if scalar_model else 3 * self.nC + diagonal = np.zeros(n_columns, dtype=np.float64) + # Start filling the diagonal array + for components, receivers in self._get_components_and_receivers(): + if not CHOCLO_SUPPORTED_COMPONENTS.issuperset(components): + raise NotImplementedError( + f"Other components besides {CHOCLO_SUPPORTED_COMPONENTS} " + "aren't implemented yet." + ) + for component in components: + if component == "tmi": + diagonal_gtg_func = NUMBA_FUNCTIONS_2D["diagonal_gtg"]["tmi"][ + self.numba_parallel + ] + diagonal_gtg_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + regional_field, + constant_factor, + scalar_model, + weights, + diagonal, + ) + elif component in ("tmi_x", "tmi_y", "tmi_z"): + kernel_xx, kernel_yy, kernel_zz, kernel_xy, kernel_xz, kernel_yz = ( + CHOCLO_KERNELS[component] + ) + diagonal_gtg_func = NUMBA_FUNCTIONS_2D["diagonal_gtg"][ + "tmi_derivative" + ][self.numba_parallel] + diagonal_gtg_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + regional_field, + kernel_xx, + kernel_yy, + kernel_zz, + kernel_xy, + kernel_xz, + kernel_yz, + constant_factor, + scalar_model, + weights, + diagonal, + ) + else: + kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] + diagonal_gtg_func = NUMBA_FUNCTIONS_2D["diagonal_gtg"][ + "magnetic_component" + ][self.numba_parallel] + diagonal_gtg_func( + receivers, + cells_bounds_active, + self.cell_z_top, + self.cell_z_bottom, + regional_field, + kernel_x, + kernel_y, + kernel_z, + constant_factor, + scalar_model, + weights, + diagonal, + ) + return diagonal - mu = self.muMap * m - chi = mu / mu_0 - 1 - # Temporary fix - Bbc, Bbc_const = CongruousMagBC(self.mesh, self.survey.source_field.b0, chi) - self.Bbc = Bbc - self.Bbc_const = Bbc_const - # return self._Div*self.MfMuI*self.MfMu0*B0 - self._Div*B0 + - # Mc*Dface*self._Pout.T*Bbc - return self._Div * self.MfMuI * self.MfMu0 * B0 - self._Div * B0 +class Simulation3DDifferential(BaseMagneticPDESimulation): + r"""A secondary field simulation for magnetic data. - def getA(self, m): - r""" - GetA creates and returns the A matrix for the Magnetics problem + Parameters + ---------- + mesh : discretize.base.BaseMesh + survey : magnetics.survey.Survey + mu : float, array_like + Magnetic Permeability Model (H/ m). Set this for forward + modeling or to fix while inverting for remanence. This is used if + ``muMap`` is None. + muMap : simpeg.maps.IdentityMap, optional + The mapping used to go from the simulation model to ``mu``. Set this + to invert for ``mu``. + rem : float, array_like + Magnetic Polarization :math:`\mu_0 \mathbf{M}` (nT). Set this for forward + modeling or to fix remanent magnetization while inverting for permeability. + This is used if ``remMap`` is None. + remMap : simpeg.maps.IdentityMap, optional + The mapping used to go from the simulation model to :math:`\mu_0 \mathbf{M}`. + Set this to invert for :math:`\mu_0 \mathbf{M}`. + storeJ: bool + Whether to store the sensitivity matrix. If set to True + solver_dtype: dtype, optional + Data type to use for the matrix that gets passed to the ``solver``. + Default to `numpy.float64`. + + + Notes + ----- + This simulation solves for the magnetostatic PDE: + + .. math:: + + \nabla \cdot \mathbf{B} = 0 + + where the constitutive relation is specified as: + + .. math:: + + \mathbf{B} = \mu\mathbf{H} + \mu_0\mathbf{M_r} + + where :math:`\mathbf{M_r}` is a fixed magnetization unaffected by the inducing field + and :math:`\mu\mathbf{H}` is the induced magnetization. + """ + + _Ainv = None - The A matrix has the form: + rem, remMap, remDeriv = props.Invertible( + "Magnetic Polarization (nT)", optional=True + ) - .. math :: + _supported_components = ("tmi", "bx", "by", "bz") - \mathbf{A} = \Div(\MfMui)^{-1}\Div^{T} + def __init__( + self, + mesh, + survey=None, + mu=None, + muMap=None, + rem=None, + remMap=None, + storeJ=False, + solver_dtype=np.float64, + **kwargs, + ): + if mu is None: + mu = mu_0 - """ - return self._Div * self.MfMuI * self._Div.T.tocsr() + super().__init__(mesh=mesh, survey=survey, mu=mu, muMap=muMap, **kwargs) - def fields(self, m): - r""" - Return magnetic potential (u) and flux (B) + self.rem = rem + self.remMap = remMap - u: defined on the cell center [nC x 1] - B: defined on the cell center [nG x 1] + self.storeJ = storeJ + self.solver_dtype = solver_dtype - After we compute u, then we update B. + self._MfMu0i = self.mesh.get_face_inner_product(1.0 / mu_0) + self._Div = self.Mcc * self.mesh.face_divergence + self._DivT = self._Div.T.tocsr() + self._Mf_vec_deriv = self.mesh.get_face_inner_product_deriv( + np.ones(self.mesh.n_cells * 3) + )(np.ones(self.mesh.n_faces)) - .. math :: + self.solver_opts = {"is_symmetric": True, "is_positive_definite": True} - \mathbf{B}_s = - (\MfMui)^{-1}\mathbf{M}^f_{\mu_0^{-1}}\mathbf{B}_0 - - \mathbf{B}_0 - - (\MfMui)^{-1}\Div^T \mathbf{u} + self._Jmatrix = None + self._stored_fields = None - """ - self.makeMassMatrices(m) - A = self.getA(m) - rhs = self.getRHS(m) - Ainv = self.solver(A, **self.solver_opts) - u = Ainv * rhs - B0 = self.getB0() - B = self.MfMuI * self.MfMu0 * B0 - B0 - self.MfMuI * self._Div.T * u - Ainv.clean() + @property + def survey(self): + """The magnetic survey object. - return {"B": B, "u": u} + Returns + ------- + simpeg.potential_fields.magnetics.Survey + """ + if self._survey is None: + raise AttributeError("Simulation must have a survey") + return self._survey - @utils.timeIt - def Jvec(self, m, v, u=None): - r""" - Computing Jacobian multiplied by vector + @survey.setter + def survey(self, value): + if value is not None: + value = validate_type("survey", value, Survey, cast=False) + unsupported_components = { + component + for source in value.source_list + for receiver in source.receiver_list + for component in receiver.components + if component not in self._supported_components + } + if unsupported_components: + msg = ( + f"Found unsupported magnetic components " + f"'{', '.join(c for c in unsupported_components)}' in the survey." + f"The {type(self).__name__} currently supports the following " + f"components: {', '.join(c for c in self._supported_components)}" + ) + raise NotImplementedError(msg) + self._survey = value - By setting our problem as + @property + def storeJ(self): + """Whether to store the sensitivity matrix - .. math :: + Returns + ------- + bool + """ + return self._storeJ - \mathbf{C}(\mathbf{m}, \mathbf{u}) = \mathbf{A}\mathbf{u} - \mathbf{rhs} = 0 + @storeJ.setter + def storeJ(self, value): + self._storeJ = validate_type("storeJ", value, bool) - And taking derivative w.r.t m + @property + def solver_dtype(self): + """ + Data type used by the solver. - .. math :: + Returns + ------- + numpy.dtype + Either np.float32 or np.float64 + """ + return self._solver_dtype - \nabla \mathbf{C}(\mathbf{m}, \mathbf{u}) = - \nabla_m \mathbf{C}(\mathbf{m}) \delta \mathbf{m} + - \nabla_u \mathbf{C}(\mathbf{u}) \delta \mathbf{u} = 0 + @solver_dtype.setter + def solver_dtype(self, value): + """ + Set the solver dtype. Must be np.float32 or np.float64. + """ + if value not in (np.float32, np.float64): + msg = ( + f"Invalid `solver_dtype` '{value}'. " + "It must be np.float32 or np.float64." + ) + raise ValueError(msg) + self._solver_dtype = value - \frac{\delta \mathbf{u}}{\delta \mathbf{m}} = - - [\nabla_u \mathbf{C}(\mathbf{u})]^{-1}\nabla_m \mathbf{C}(\mathbf{m}) + @cached_property + @utils.requires("survey") + def _b0(self): + # Todo: Experiment with avoiding array of constants + b0 = self.survey.source_field.b0 + b0 = np.r_[ + b0[0] * np.ones(self.mesh.nFx), + b0[1] * np.ones(self.mesh.nFy), + b0[2] * np.ones(self.mesh.nFz), + ] + return b0 - With some linear algebra we can have + @property + def _stored_fields(self): + return self.__stored_fields - .. math :: + @_stored_fields.setter + def _stored_fields(self, value): + self.__stored_fields = value - \nabla_u \mathbf{C}(\mathbf{u}) = \mathbf{A} + @_stored_fields.deleter + def _stored_fields(self): + self.__stored_fields = None - \nabla_m \mathbf{C}(\mathbf{m}) = - \frac{\partial \mathbf{A}} {\partial \mathbf{m}} (\mathbf{m}) \mathbf{u} - - \frac{\partial \mathbf{rhs}(\mathbf{m})}{\partial \mathbf{m}} + def _getRHS(self, m): + self.model = m - .. math :: + rhs = 0 - \frac{\partial \mathbf{A}}{\partial \mathbf{m}}(\mathbf{m})\mathbf{u} = - \frac{\partial \mathbf{\mu}}{\partial \mathbf{m}} - \left[\Div \diag (\Div^T \mathbf{u}) \dMfMuI \right] + if not np.isscalar(self.mu) or not np.allclose(self.mu, mu_0): + rhs += ( + self._Div * self.MfMuiI * self._MfMu0i * self._b0 - self._Div * self._b0 + ) - \dMfMuI = - \diag(\MfMui)^{-1}_{vec} - \mathbf{Av}_{F2CC}^T\diag(\mathbf{v})\diag(\frac{1}{\mu^2}) + if self.rem is not None: + rhs += ( + self._Div + * ( + self.MfMuiI + * self.mesh.get_face_inner_product( + self.rem + / np.tile(self.mu * np.ones(self.mesh.n_cells), self.mesh.dim) + ) + ).diagonal() + ) - \frac{\partial \mathbf{rhs}(\mathbf{m})}{\partial \mathbf{m}} = - \frac{\partial \mathbf{\mu}}{\partial \mathbf{m}} - \left[ - \Div \diag(\M^f_{\mu_{0}^{-1}}\mathbf{B}_0) \dMfMuI - \right] - - \diag(\mathbf{v}) \mathbf{D} \mathbf{P}_{out}^T - \frac{\partial B_{sBC}}{\partial \mathbf{m}} + return rhs - In the end, + def _getA(self): + A = self._Div * self.MfMuiI * self._DivT - .. math :: + A = A.astype(self.solver_dtype) - \frac{\delta \mathbf{u}}{\delta \mathbf{m}} = - - [ \mathbf{A} ]^{-1} - \left[ - \frac{\partial \mathbf{A}}{\partial \mathbf{m}}(\mathbf{m})\mathbf{u} - - \frac{\partial \mathbf{rhs}(\mathbf{m})}{\partial \mathbf{m}} - \right] + return A - A little tricky point here is we are not interested in potential (u), but interested in magnetic flux (B). - Thus, we need sensitivity for B. Now we take derivative of B w.r.t m and have + def fields(self, m): + self.model = m - .. math :: + if self._stored_fields is None: - \frac{\delta \mathbf{B}} {\delta \mathbf{m}} = - \frac{\partial \mathbf{\mu} } {\partial \mathbf{m} } - \left[ - \diag(\M^f_{\mu_{0}^{-1} } \mathbf{B}_0) \dMfMuI \ - - \diag (\Div^T\mathbf{u})\dMfMuI - \right ] + if self._Ainv is None: + self._Ainv = self.solver(self._getA(), **self.solver_opts) - - (\MfMui)^{-1}\Div^T\frac{\delta\mathbf{u}}{\delta \mathbf{m}} + rhs = self._getRHS(m) - Finally we evaluate the above, but we should remember that + u = self._Ainv * rhs + b_field = -self.MfMuiI * self._DivT * u - .. note :: + if not np.isscalar(self.mu) or not np.allclose(self.mu, mu_0): + b_field += self._MfMu0i * self.MfMuiI * self._b0 - self._b0 - We only want to evaluate + if self.rem is not None: + b_field += ( + self.MfMuiI + * self.mesh.get_face_inner_product( + self.rem + / np.tile(self.mu * np.ones(self.mesh.n_cells), self.mesh.dim) + ) + ).diagonal() - .. math :: + fields = {"b": b_field, "u": u} + self._stored_fields = fields - \mathbf{J}\mathbf{v} = - \frac{\delta \mathbf{P}\mathbf{B}} {\delta \mathbf{m}}\mathbf{v} + else: + fields = self._stored_fields - Since forming sensitivity matrix is very expensive in that this - monster is "big" and "dense" matrix!! + return fields - """ - if u is None: - u = self.fields(m) + def dpred(self, m=None, f=None): + self.model = m + if f is not None: + return self._projectFields(f) - B, u = u["B"], u["u"] - mu = self.muMap * (m) - dmu_dm = self.muDeriv - # dchidmu = sdiag(1 / mu_0 * np.ones(self.mesh.nC)) + if f is None: + f = self.fields(m) - vol = self.mesh.cell_volumes - Div = self._Div - P = self.survey.projectFieldsDeriv(B) # Projection matrix - B0 = self.getB0() + dpred = self._projectFields(f) - MfMuIvec = 1 / self.MfMui.diagonal() - dMfMuI = sdiag(MfMuIvec**2) * self.mesh.aveF2CC.T * sdiag(vol * 1.0 / mu**2) + return dpred - # A = self._Div*self.MfMuI*self._Div.T - # RHS = Div*MfMuI*MfMu0*B0 - Div*B0 + Mc*Dface*Pout.T*Bbc - # C(m,u) = A*m-rhs - # dudm = -(dCdu)^(-1)dCdm + def magnetic_polarization(self, m=None): + r""" + Computes the total magnetic polarization :math:`\mu_0\mathbf{M}`. - dCdu = self.getA(m) # = A - dCdm_A = Div * (sdiag(Div.T * u) * dMfMuI * dmu_dm) - dCdm_RHS1 = Div * (sdiag(self.MfMu0 * B0) * dMfMuI) - # temp1 = (Dface * (self._Pout.T * self.Bbc_const * self.Bbc)) - # dCdm_RHS2v = (sdiag(vol) * temp1) * \ - # np.inner(vol, dchidmu * dmu_dm * v) + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. - # dCdm_RHSv = dCdm_RHS1*(dmu_dm*v) + dCdm_RHS2v - dCdm_RHSv = dCdm_RHS1 * (dmu_dm * v) - dCdm_v = dCdm_A * v - dCdm_RHSv + Returns + ------- + mu0_m : np.ndarray + The magnetic polarization :math:`\mu_0 \mathbf{M}` in nanoteslas (nT), defined on the mesh faces. + The result is ordered as a concatenation of the x, y, and z face components + (i.e., ``[Mx_faces, My_faces, Mz_faces]``). - Ainv = self.solver(dCdu, **self.solver_opts) - sol = Ainv * dCdm_v - dudm = -sol - dBdmv = ( - sdiag(self.MfMu0 * B0) * (dMfMuI * (dmu_dm * v)) - - sdiag(Div.T * u) * (dMfMuI * (dmu_dm * v)) - - self.MfMuI * (Div.T * (dudm)) - ) + """ + self.model = m + f = self.fields(m) + b_field, u = f["b"], f["u"] + MfMu0iI = self.mesh.get_face_inner_product(1.0 / mu_0, invert_matrix=True) - Ainv.clean() + mu0_h = -MfMu0iI * self._DivT * u + mu0_m = b_field - mu0_h - return mkvc(P * dBdmv) + return mu0_m - @utils.timeIt - def Jtvec(self, m, v, u=None): - r""" - Computing Jacobian^T multiplied by vector. + def Jvec(self, m, v, f=None): + self.model = m - .. math :: + if f is None: + f = self.fields(m) - (\frac{\delta \mathbf{P}\mathbf{B}} {\delta \mathbf{m}})^{T} = - \left[ - \mathbf{P}_{deriv}\frac{\partial \mathbf{\mu} } {\partial \mathbf{m} } - \left[ - \diag(\M^f_{\mu_{0}^{-1} } \mathbf{B}_0) \dMfMuI - - \diag (\Div^T\mathbf{u})\dMfMuI - \right ] - \right]^{T} - - - \left[ - \mathbf{P}_{deriv}(\MfMui)^{-1} \Div^T - \frac{\delta\mathbf{u}}{\delta \mathbf{m}} - \right]^{T} + if self.storeJ: + J = self.getJ(m, f=f) + return J.dot(v) - where + return self._Jvec(m, v, f) - .. math :: + def Jtvec(self, m, v, f=None): + self.model = m - \mathbf{P}_{derv} = \frac{\partial \mathbf{P}}{\partial\mathbf{B}} + if f is None: + f = self.fields(m) - .. note :: + if self.storeJ: + J = self.getJ(m, f=f) + return np.asarray(J.T.dot(v)) - Here we only want to compute + return self._Jtvec(m, v, f) - .. math :: + def getJ(self, m, f=None): + self.model = m + if self._Jmatrix: + return self._Jmatrix + if f is None: + f = self.fields(m) + if m.size < self.survey.nD: + J = self._Jvec(m, v=None, f=f) + else: + J = self._Jtvec(m, v=None, f=f).T - \mathbf{J}^{T}\mathbf{v} = - (\frac{\delta \mathbf{P}\mathbf{B}} {\delta \mathbf{m}})^{T} \mathbf{v} + if self.storeJ: + self._Jmatrix = J + return J - """ - if u is None: - u = self.fields(m) + def _Jtvec(self, m, v, f): + b_field, u = f["b"], f["u"] - B, u = u["B"], u["u"] - mu = self.mapping * (m) - dmu_dm = self.mapping.deriv(m) - # dchidmu = sdiag(1 / mu_0 * np.ones(self.mesh.nC)) + Q = self._projectFieldsDeriv(b_field) - vol = self.mesh.cell_volumes - Div = self._Div - P = self.survey.projectFieldsDeriv(B) # Projection matrix - B0 = self.getB0() + if v is None: + v = np.eye(Q.shape[0]) + divt_solve_q = ( + self._DivT * (self._Ainv * ((Q * self.MfMuiI * -self._DivT).T * v)) + + Q.T * v + ) + del v + else: + divt_solve_q = ( + self._DivT * (self._Ainv * ((-self._Div * (self.MfMuiI.T * (Q.T * v))))) + + Q.T * v + ) - MfMuIvec = 1 / self.MfMui.diagonal() - dMfMuI = sdiag(MfMuIvec**2) * self.mesh.aveF2CC.T * sdiag(vol * 1.0 / mu**2) + mu_vec = np.tile(self.mu * np.ones(self.mesh.n_cells), self.mesh.dim) - # A = self._Div*self.MfMuI*self._Div.T - # RHS = Div*MfMuI*MfMu0*B0 - Div*B0 + Mc*Dface*Pout.T*Bbc - # C(m,u) = A*m-rhs - # dudm = -(dCdu)^(-1)dCdm + Jtv = 0 - dCdu = self.getA(m) - s = Div * (self.MfMuI.T * (P.T * v)) + if self.remMap is not None: + Mf_rem_deriv = self._Mf_vec_deriv * sp.diags(1 / mu_vec) * self.remDeriv + Jtv += (self.MfMuiI * Mf_rem_deriv).T * (divt_solve_q) - Ainv = self.solver(dCdu.T, **self.solver_opts) - sol = Ainv * s + if self.muMap is not None: + Jtv += self.MfMuiIDeriv(self._DivT * u, -divt_solve_q, adjoint=True) + Jtv += self.MfMuiIDeriv( + self._b0, self._MfMu0i.T * (divt_solve_q), adjoint=True + ) - Ainv.clean() + if self.rem is not None: + Mf_r_mui = self.mesh.get_face_inner_product( + self.rem / mu_vec + ).diagonal() + mu_vec_i_deriv = sp.vstack( + (self.muiDeriv, self.muiDeriv, self.muiDeriv) + ) - # dCdm_A = Div * ( sdiag( Div.T * u )* dMfMuI *dmu_dm ) - # dCdm_Atsol = ( dMfMuI.T*( sdiag( Div.T * u ) * (Div.T * dmu_dm)) ) * sol - dCdm_Atsol = (dmu_dm.T * dMfMuI.T * (sdiag(Div.T * u) * Div.T)) * sol + Mf_r_mui_deriv = ( + self._Mf_vec_deriv * sp.diags(self.rem) * mu_vec_i_deriv + ) - # dCdm_RHS1 = Div * (sdiag( self.MfMu0*B0 ) * dMfMuI) - # dCdm_RHS1tsol = (dMfMuI.T*( sdiag( self.MfMu0*B0 ) ) * Div.T * dmu_dm) * sol - dCdm_RHS1tsol = (dmu_dm.T * dMfMuI.T * (sdiag(self.MfMu0 * B0)) * Div.T) * sol + Jtv += ( + self.MfMuiIDeriv(Mf_r_mui, divt_solve_q, adjoint=True) + + (Mf_r_mui_deriv.T * self.MfMuiI.T) * divt_solve_q + ) - # temp1 = (Dface*(self._Pout.T*self.Bbc_const*self.Bbc)) - # temp1sol = (Dface.T * (sdiag(vol) * sol)) - # temp2 = self.Bbc_const * (self._Pout.T * self.Bbc).T - # dCdm_RHS2v = (sdiag(vol)*temp1)*np.inner(vol, dchidmu*dmu_dm*v) - # dCdm_RHS2tsol = (dmu_dm.T * dchidmu.T * vol) * np.inner(temp2, temp1sol) + return Jtv - # dCdm_RHSv = dCdm_RHS1*(dmu_dm*v) + dCdm_RHS2v + def _Jvec(self, m, v, f): - # temporary fix - # dCdm_RHStsol = dCdm_RHS1tsol - dCdm_RHS2tsol - dCdm_RHStsol = dCdm_RHS1tsol + if v is None: + v = np.eye(m.shape[0]) - # dCdm_RHSv = dCdm_RHS1*(dmu_dm*v) + dCdm_RHS2v - # dCdm_v = dCdm_A*v - dCdm_RHSv + b_field, u = f["b"], f["u"] - Ctv = dCdm_Atsol - dCdm_RHStsol + Q = self._projectFieldsDeriv(b_field) + C = -self.MfMuiI * self._DivT - # B = self.MfMuI*self.MfMu0*B0-B0-self.MfMuI*self._Div.T*u - # dBdm = d\mudm*dBd\mu - # dPBdm^T*v = Atemp^T*P^T*v - Btemp^T*P^T*v - Ctv + db_dm = 0 + dCmu_dm = 0 - Atemp = sdiag(self.MfMu0 * B0) * (dMfMuI * (dmu_dm)) - Btemp = sdiag(Div.T * u) * (dMfMuI * (dmu_dm)) - Jtv = Atemp.T * (P.T * v) - Btemp.T * (P.T * v) - Ctv + mu_vec = np.tile(self.mu * np.ones(self.mesh.n_cells), self.mesh.dim) - return mkvc(Jtv) + if self.remMap is not None: + Mf_rem_deriv = self._Mf_vec_deriv * sp.diags(1 / mu_vec) * self.remDeriv + db_dm += self.MfMuiI * Mf_rem_deriv * v - @property - def Qfx(self): - if getattr(self, "_Qfx", None) is None: - self._Qfx = self.mesh.get_interpolation_matrix( - self.survey.receiver_locations, "Fx" - ) - return self._Qfx + if self.muMap is not None: + dCmu_dm += self.MfMuiIDeriv(self._DivT @ u, v, adjoint=False) + db_dm += self._MfMu0i * self.MfMuiIDeriv(self._b0, v, adjoint=False) - @property - def Qfy(self): - if getattr(self, "_Qfy", None) is None: - self._Qfy = self.mesh.get_interpolation_matrix( - self.survey.receiver_locations, "Fy" - ) - return self._Qfy + if self.rem is not None: + Mf_r_mui = self.mesh.get_face_inner_product( + self.rem / mu_vec + ).diagonal() + mu_vec_i_deriv = sp.vstack( + (self.muiDeriv, self.muiDeriv, self.muiDeriv) + ) + Mf_r_mui_deriv = ( + self._Mf_vec_deriv * sp.diags(self.rem) * mu_vec_i_deriv + ) + db_dm += self.MfMuiIDeriv(Mf_r_mui, v, adjoint=False) + ( + self.MfMuiI * Mf_r_mui_deriv * v + ) - @property - def Qfz(self): - if getattr(self, "_Qfz", None) is None: - self._Qfz = self.mesh.get_interpolation_matrix( - self.survey.receiver_locations, "Fz" - ) - return self._Qfz + Ainv_Ddm = self._Ainv * (self._Div * (-dCmu_dm + db_dm)) - def projectFields(self, u): - r""" - This function projects the fields onto the data space. - Especially, here for we use total magnetic intensity (TMI) data, - which is common in practice. - First we project our B on to data location + Jv = Q * (C * Ainv_Ddm + (-dCmu_dm + db_dm)) - .. math:: + return Jv - \mathbf{B}_{rec} = \mathbf{P} \mathbf{B} + @cached_property + def _Qfx(self): + Qfx = self.mesh.get_interpolation_matrix(self.survey.receiver_locations, "Fx") + return Qfx - then we take the dot product between B and b_0 + @cached_property + def _Qfy(self): + Qfy = self.mesh.get_interpolation_matrix(self.survey.receiver_locations, "Fy") + return Qfy - .. math :: + @cached_property + def _Qfz(self): + Qfz = self.mesh.get_interpolation_matrix(self.survey.receiver_locations, "Fz") + return Qfz - \text{TMI} = \vec{B}_s \cdot \hat{B}_0 + def _projectFields(self, f): - """ - # TODO: There can be some different tyes of data like |B| or B - components = self.survey.components + rx_list = self.survey.source_field.receiver_list + components = [] + for rx in rx_list: + components.extend(rx.components) + components = set(components) - fields = {} if "bx" in components or "tmi" in components: - fields["bx"] = self.Qfx * u["B"] + bx = self._Qfx * f["b"] if "by" in components or "tmi" in components: - fields["by"] = self.Qfy * u["B"] + by = self._Qfy * f["b"] if "bz" in components or "tmi" in components: - fields["bz"] = self.Qfz * u["B"] + bz = self._Qfz * f["b"] if "tmi" in components: - bx = fields["bx"] - by = fields["by"] - bz = fields["bz"] - # Generate unit vector - B0 = self.survey.source_field.b0 - Bot = np.sqrt(B0[0] ** 2 + B0[1] ** 2 + B0[2] ** 2) - box = B0[0] / Bot - boy = B0[1] / Bot - boz = B0[2] / Bot - fields["tmi"] = bx * box + by * boy + bz * boz - - return np.concatenate([fields[comp] for comp in components]) - - @utils.count - def projectFieldsDeriv(self, B): - r""" - This function projects the fields onto the data space. - - .. math:: - - \frac{\partial d_\text{pred}}{\partial \mathbf{B}} = \mathbf{P} + b0 = self.survey.source_field.b0 + tmi = np.sqrt( + (bx + b0[0]) ** 2 + (by + b0[1]) ** 2 + (bz + b0[2]) ** 2 + ) - np.sqrt(b0[0] ** 2 + b0[1] ** 2 + b0[2] ** 2) + + n_total = 0 + total_data_list = [] + for rx in rx_list: + data = {} + rx_n_locs = rx.locations.shape[0] + if "bx" in rx.components: + data["bx"] = bx[n_total : n_total + rx_n_locs] + if "by" in rx.components: + data["by"] = by[n_total : n_total + rx_n_locs] + if "bz" in rx.components: + data["bz"] = bz[n_total : n_total + rx_n_locs] + if "tmi" in rx.components: + data["tmi"] = tmi[n_total : n_total + rx_n_locs] + + n_total += rx_n_locs + + total_data_list.append( + np.concatenate([data[comp] for comp in rx.components]) + ) - Especially, this function is for TMI data type - """ + if len(total_data_list) == 1: + return total_data_list[0] - components = self.survey.components + return np.concatenate(total_data_list, axis=0) - fields = {} - if "bx" in components or "tmi" in components: - fields["bx"] = self.Qfx - if "by" in components or "tmi" in components: - fields["by"] = self.Qfy - if "bz" in components or "tmi" in components: - fields["bz"] = self.Qfz + @utils.count + def _projectFieldsDeriv(self, bs): + rx_list = self.survey.source_field.receiver_list + components = [] + for rx in rx_list: + components.extend(rx.components) + components = set(components) if "tmi" in components: - bx = fields["bx"] - by = fields["by"] - bz = fields["bz"] - # Generate unit vector - B0 = self.survey.source_field.b0 - Bot = np.sqrt(B0[0] ** 2 + B0[1] ** 2 + B0[2] ** 2) - box = B0[0] / Bot - boy = B0[1] / Bot - boz = B0[2] / Bot - fields["tmi"] = bx * box + by * boy + bz * boz + b0 = self.survey.source_field.b0 + bot = np.sqrt(b0[0] ** 2 + b0[1] ** 2 + b0[2] ** 2) - return sp.vstack([fields[comp] for comp in components]) + bx = self._Qfx * bs + by = self._Qfy * bs + bz = self._Qfz * bs - def projectFieldsAsVector(self, B): - bfx = self.Qfx * B - bfy = self.Qfy * B - bfz = self.Qfz * B + dpred = ( + np.sqrt((bx + b0[0]) ** 2 + (by + b0[1]) ** 2 + (bz + b0[2]) ** 2) - bot + ) - return np.r_[bfx, bfy, bfz] + dDhalf_dD = sdiag(1 / (dpred + bot)) + xterm = sdiag(b0[0] + bx) * self._Qfx + yterm = sdiag(b0[1] + by) * self._Qfy + zterm = sdiag(b0[2] + bz) * self._Qfz -def MagneticsDiffSecondaryInv(mesh, model, data, **kwargs): - """ - Inversion module for MagneticsDiffSecondary + Qtmi = dDhalf_dD * (xterm + yterm + zterm) - """ - from simpeg import ( - directives, - inversion, - objective_function, - optimization, - regularization, - ) + n_total = 0 + total_data_list = [] + for rx in rx_list: + data = {} + rx_n_locs = rx.locations.shape[0] + if "bx" in rx.components: + data["bx"] = self._Qfx[n_total : n_total + rx_n_locs][:] + if "by" in rx.components: + data["by"] = self._Qfy[n_total : n_total + rx_n_locs][:] + if "bz" in rx.components: + data["bz"] = self._Qfz[n_total : n_total + rx_n_locs][:] + if "tmi" in rx.components: + data["tmi"] = Qtmi[n_total : n_total + rx_n_locs][:] - prob = Simulation3DDifferential(mesh, survey=data, mu=model) + n_total += rx_n_locs - miter = kwargs.get("maxIter", 10) + total_data_list.append(sp.vstack([data[comp] for comp in rx.components])) - # Create an optimization program - opt = optimization.InexactGaussNewton(maxIter=miter) - opt.bfgsH0 = Solver(sp.identity(model.nP), flag="D") - # Create a regularization program - reg = regularization.WeightedLeastSquares(model) - # Create an objective function - beta = directives.BetaSchedule(beta0=1e0) - obj = objective_function.BaseObjFunction(prob, reg, beta=beta) - # Create an inversion object - inv = inversion.BaseInversion(obj, opt) + if len(total_data_list) == 1: + return total_data_list[0] - return inv, reg + return sp.vstack(total_data_list) + + @property + def _delete_on_model_update(self): + toDelete = super()._delete_on_model_update + if self._stored_fields is not None: + toDelete = toDelete + ["_stored_fields"] + if self.muMap is not None: + if self._Ainv is not None: + toDelete = toDelete + ["_Ainv"] + if self._Jmatrix is not None: + toDelete = toDelete + ["_Jmatrix"] + return toDelete diff --git a/simpeg/potential_fields/magnetics/sources.py b/simpeg/potential_fields/magnetics/sources.py index 0e4000e9c2..56c9e8662c 100644 --- a/simpeg/potential_fields/magnetics/sources.py +++ b/simpeg/potential_fields/magnetics/sources.py @@ -1,6 +1,8 @@ from ...survey import BaseSrc -from simpeg.utils.mat_utils import dip_azimuth2cartesian -from simpeg.utils.code_utils import deprecate_class, validate_float +from ...utils.mat_utils import dip_azimuth2cartesian +from ...utils.code_utils import validate_float, validate_list_of_types + +from .receivers import Point class UniformBackgroundField(BaseSrc): @@ -11,39 +13,46 @@ class UniformBackgroundField(BaseSrc): Parameters ---------- - receiver_list : list of simpeg.potential_fields.magnetics.Point - amplitude : float, optional - amplitude of the inducing backgound field, usually this is in units of nT. - inclination : float, optional - Dip angle in degrees from the horizon, positive points into the earth. - declination : float, optional + receiver_list : simpeg.potential_fields.magnetics.Point, list of simpeg.potential_fields.magnetics.Point or None + Point magnetic receivers. + amplitude : float + Amplitude of the inducing background field, usually this is in + units of nT. + inclination : float + Dip angle in degrees from the horizon, positive value into the earth. + declination : float Azimuthal angle in degrees from north, positive clockwise. """ def __init__( self, - receiver_list=None, - amplitude=50000.0, - inclination=90.0, - declination=0.0, - **kwargs, + receiver_list: Point | list[Point] | None, + amplitude: float, + inclination: float, + declination: float, ): - # Raise errors on 'parameters' argument - # The parameters argument was supported in the deprecated SourceField - # class. We would like to raise an error in case the user passes it - # so the class doesn't behave differently than expected. - if (key := "parameters") in kwargs: - raise TypeError( - f"'{key}' property has been removed." - "Please pass the amplitude, inclination and declination" - " through their own arguments." - ) - self.amplitude = amplitude self.inclination = inclination self.declination = declination + super().__init__(receiver_list=receiver_list) + + @property + def receiver_list(self): + """ + List of receivers associated with the survey. + + Returns + ------- + list of simpeg.potential_fields.magnetics.Point + List of magnetic receivers associated with the survey + """ + return self._receiver_list - super().__init__(receiver_list=receiver_list, **kwargs) + @receiver_list.setter + def receiver_list(self, value): + self._receiver_list = validate_list_of_types( + "receiver_list", value, Point, ensure_unique=True + ) @property def amplitude(self): @@ -98,29 +107,3 @@ def b0(self): self.amplitude * dip_azimuth2cartesian(self.inclination, self.declination).squeeze() ) - - -@deprecate_class(removal_version="0.19.0", error=True) -class SourceField(UniformBackgroundField): - """Source field for magnetics integral formulation - - Parameters - ---------- - receivers_list : list of simpeg.potential_fields.receivers.Point - List of magnetics receivers - parameters : (3) array_like of float - Define the Earth's inducing field according to - [*amplitude*, *inclination*, *declination*] where: - - - *amplitude* is the field intensity in nT - - *inclination* is the inclination of the Earth's field in degrees - - *declination* is the declination of the Earth's field in degrees - """ - - def __init__(self, receiver_list=None, parameters=(50000, 90, 0)): - super().__init__( - receiver_list=receiver_list, - amplitude=parameters[0], - inclination=parameters[1], - declination=parameters[2], - ) diff --git a/simpeg/potential_fields/magnetics/survey.py b/simpeg/potential_fields/magnetics/survey.py index 56a2c3296c..d06f090811 100644 --- a/simpeg/potential_fields/magnetics/survey.py +++ b/simpeg/potential_fields/magnetics/survey.py @@ -1,6 +1,6 @@ import numpy as np from ...survey import BaseSurvey -from ...utils.code_utils import validate_type +from ...utils.code_utils import validate_list_of_types from .sources import UniformBackgroundField @@ -14,10 +14,39 @@ class Survey(BaseSurvey): """ def __init__(self, source_field, **kwargs): - self.source_field = validate_type( - "source_field", source_field, UniformBackgroundField, cast=False + if "source_list" in kwargs: + msg = ( + "source_list is not a valid argument to gravity.Survey. " + "Use source_field instead." + ) + raise TypeError(msg) + super().__init__(source_list=source_field, **kwargs) + + @BaseSurvey.source_list.setter + def source_list(self, new_list): + # mag simulations only support 1 source... for now... + self._source_list = validate_list_of_types( + "source_list", + new_list, + UniformBackgroundField, + ensure_unique=True, + min_n=1, + max_n=1, ) - super().__init__(source_list=None, **kwargs) + + @property + def source_field(self): + """A source defining the Earth's inducing field and containing the magnetic receivers. + + Returns + ------- + simpeg.potential_fields.magnetics.sources.UniformBackgroundField + """ + return self.source_list[0] + + @source_field.setter + def source_field(self, new_src): + self.source_list = new_src def eval(self, fields): # noqa: A003 """Compute the fields @@ -68,20 +97,6 @@ def nD(self): """ return sum(rx.nD for rx in self.source_field.receiver_list) - @property - def components(self): - """Field components - - Returns - ------- - list of str - Components of the field being measured - """ - comps = [] - for rx in self.source_field.receiver_list: - comps += rx.components - return comps - def _location_component_iterator(self): for rx in self.source_field.receiver_list: for loc in rx.locations: diff --git a/simpeg/props.py b/simpeg/props.py index 5a55f77b0f..ee9ac01ce3 100644 --- a/simpeg/props.py +++ b/simpeg/props.py @@ -1,5 +1,8 @@ +import warnings + import numpy as np +from simpeg.utils import deprecate_property from .maps import IdentityMap, ReciprocalMap from .utils import Zero, validate_type, validate_ndarray_with_shape @@ -358,7 +361,7 @@ def _has_nested_models(self): # TODO: rename to _delete_on_model_update @property - def deleteTheseOnModelUpdate(self): + def _delete_on_model_update(self): """A list of properties stored on this object to delete when the model is updated Returns @@ -368,6 +371,13 @@ def deleteTheseOnModelUpdate(self): """ return [] + deleteTheseOnModelUpdate = deprecate_property( + _delete_on_model_update, + "deleteTheseOnModelUpdate", + removal_version="0.25.0", + error=True, + ) + #: List of matrix names to have their factors cleared on a model update @property def clean_on_model_update(self): @@ -377,6 +387,12 @@ def clean_on_model_update(self): ------- list of str """ + warnings.warn( + "clean_on_model_update has been deprecated due to repeated functionality encompassed" + " by the _delete_on_model_update method", + FutureWarning, + stacklevel=2, + ) return [] @property @@ -447,15 +463,10 @@ def model(self, value): and np.allclose(previous_value, value) ): # cached properties to delete - for prop in self.deleteTheseOnModelUpdate: + for prop in self._delete_on_model_update: if hasattr(self, prop): delattr(self, prop) - # matrix factors to clear - for mat in self.clean_on_model_update: - if getattr(self, mat, None) is not None: - getattr(self, mat).clean() # clean factors - setattr(self, mat, None) # set to none updated = True self._model = value @@ -469,12 +480,6 @@ def model(self, value): def model(self): self._model = (None,) # cached properties to delete - for prop in self.deleteTheseOnModelUpdate: + for prop in self._delete_on_model_update: if hasattr(self, prop): delattr(self, prop) - - # matrix factors to clear - for mat in self.clean_on_model_update: - if getattr(self, mat, None) is not None: - getattr(self, mat).clean() # clean factors - setattr(self, mat, None) # set to none diff --git a/simpeg/regularization/__init__.py b/simpeg/regularization/__init__.py index dbc30a5984..cd8b64b821 100644 --- a/simpeg/regularization/__init__.py +++ b/simpeg/regularization/__init__.py @@ -148,7 +148,6 @@ """ -from ..utils.code_utils import deprecate_class from .base import ( BaseRegularization, WeightedLeastSquares, @@ -172,87 +171,3 @@ AmplitudeSmoothnessFirstOrder, ) from ._gradient import SmoothnessFullGradient - - -@deprecate_class(removal_version="0.19.0", error=True) -class SimpleSmall(Smallness): - """Deprecated class, replaced by Smallness.""" - - pass - - -@deprecate_class(removal_version="0.19.0", error=True) -class SimpleSmoothDeriv(SmoothnessFirstOrder): - """Deprecated class, replaced by SmoothnessFirstOrder.""" - - pass - - -@deprecate_class(removal_version="0.19.0", error=True) -class Simple(WeightedLeastSquares): - """Deprecated class, replaced by WeightedLeastSquares.""" - - def __init__(self, mesh=None, alpha_x=1.0, alpha_y=1.0, alpha_z=1.0, **kwargs): - # These alphas are now refered to as length_scalse in the - # new WeightedLeastSquares regularization - super().__init__( - mesh=mesh, - length_scale_x=alpha_x, - length_scale_y=alpha_y, - length_scale_z=alpha_z, - **kwargs - ) - - -@deprecate_class(removal_version="0.19.0", error=True) -class Tikhonov(WeightedLeastSquares): - """Deprecated class, replaced by WeightedLeastSquares.""" - - def __init__( - self, mesh=None, alpha_s=1e-6, alpha_x=1.0, alpha_y=1.0, alpha_z=1.0, **kwargs - ): - super().__init__( - mesh=mesh, - alpha_s=alpha_s, - alpha_x=alpha_x, - alpha_y=alpha_y, - alpha_z=alpha_z, - **kwargs - ) - - -@deprecate_class(removal_version="0.19.0", error=True) -class Small(Smallness): - """Deprecated class, replaced by Smallness.""" - - pass - - -@deprecate_class(removal_version="0.19.0", error=True) -class SmoothDeriv(SmoothnessFirstOrder): - """Deprecated class, replaced by SmoothnessFirstOrder.""" - - pass - - -@deprecate_class(removal_version="0.19.0", error=True) -class SmoothDeriv2(SmoothnessSecondOrder): - """Deprecated class, replaced by SmoothnessSecondOrder.""" - - pass - - -@deprecate_class(removal_version="0.19.0", error=True) -class PGIwithNonlinearRelationshipsSmallness(PGIsmallness): - """Deprecated class, replaced by PGIsmallness.""" - - def __init__(self, gmm, **kwargs): - super().__init__(gmm, non_linear_relationships=True, **kwargs) - - -@deprecate_class(removal_version="0.19.0", error=True) -class PGIwithRelationships(PGI): - """Deprecated class, replaced by PGI.""" - - def __init__(self, mesh, gmmref, **kwargs): - super().__init__(mesh, gmmref, non_linear_relationships=True, **kwargs) diff --git a/simpeg/regularization/_gradient.py b/simpeg/regularization/_gradient.py index 570e4727aa..fbf3451483 100644 --- a/simpeg/regularization/_gradient.py +++ b/simpeg/regularization/_gradient.py @@ -18,15 +18,16 @@ class SmoothnessFullGradient(BaseRegularization): ---------- mesh : discretize.BaseMesh The mesh object to use for regularization. The mesh should either have - a `cell_gradient` or a `stencil_cell_gradient` defined. + a ``cell_gradient`` or a ``stencil_cell_gradient`` defined. alphas : (mesh.dim,) or (mesh.n_cells, mesh.dim) array_like of float, optional. - The weights of the regularization for each axis. This can be defined for each cell - in the mesh. Default is uniform weights equal to the smallest edge length squared. + The weights of the regularization for each axis. This can be defined + for each cell in the mesh. Default is uniform weights equal to the + smallest edge length squared. reg_dirs : (mesh.dim, mesh.dim) or (mesh.n_cells, mesh.dim, mesh.dim) array_like of float - Matrix or list of matrices whose columns represent the regularization directions. - Each matrix should be orthonormal. Default is Identity. + Matrix or list of matrices whose columns represent the regularization + directions. Each matrix should be orthonormal. Default is Identity. ortho_check : bool, optional - Whether to check `reg_dirs` for orthogonality. + Whether to check ``reg_dirs`` for orthogonality. **kwargs Keyword arguments passed to the parent class ``BaseRegularization``. @@ -41,19 +42,23 @@ class SmoothnessFullGradient(BaseRegularization): We can instead create a measure that smooths twice as much in the 1st dimension than it does in the second dimension. + >>> reg = SmoothnessFullGradient(mesh, [2, 1]) - The `alphas` parameter can also be indepenant for each cell. Here we set all cells - lower than 0.5 in the x2 to twice as much in the first dimension - otherwise it is uniform smoothing. + The ``alphas`` parameter can also be independent for each cell. Here we set + all cells lower than 0.5 in the ``x2`` to twice as much in the first + dimension otherwise it is uniform smoothing. + >>> alphas = np.ones((mesh.n_cells, mesh.dim)) >>> alphas[mesh.cell_centers[:, 1] < 0.5] = [2, 1] >>> reg = SmoothnessFullGradient(mesh, alphas) - We can also rotate the axis in which we want to preferentially smooth. Say we want to - smooth twice as much along the +x1,+x2 diagonal as we do along the -x1,+x2 diagonal, - effectively rotating our smoothing 45 degrees. Note and the columns of the matrix - represent the directional vectors (not the rows). + We can also rotate the axis in which we want to preferentially smooth. Say + we want to smooth twice as much along the +x1,+x2 diagonal as we do along + the -x1,+x2 diagonal, effectively rotating our smoothing 45 degrees. Note + and the columns of the matrix represent the directional vectors (not the + rows). + >>> sqrt2 = np.sqrt(2) >>> reg_dirs = np.array([ ... [sqrt2, -sqrt2], @@ -63,20 +68,25 @@ class SmoothnessFullGradient(BaseRegularization): Notes ----- - The regularization object is the discretized form of the continuous regularization + The regularization object is the discretized form of the continuous + regularization + + .. math:: - ..math: - f(m) = \int_V \nabla m \cdot \mathbf{a} \nabla m \hspace{5pt} \partial V + f(m) = \int_V \nabla m \cdot \mathbf{a} \nabla m \, \text{d} V - The tensor quantity `a` is used to represent the potential preferential directions of - regularization. `a` must be symmetric positive semi-definite with an eigendecomposition of: + The tensor quantity :math:`\mathbf{a}` is used to represent the potential + preferential directions of regularization. :math:`\mathbf{a}` must be + symmetric positive semi-definite with an eigendecomposition of: + + .. math:: - ..math: \mathbf{a} = \mathbf{Q}\mathbf{L}\mathbf{Q}^{-1} - `Q` is then the regularization directions ``reg_dirs``, and `L` is represents the weighting - along each direction, with ``alphas`` along its diagonal. These are multiplied to form the - anisotropic alpha used for rotated gradients. + :math:`\mathbf{Q}` is then the regularization directions ``reg_dirs``, and + :math:`\mathbf{L}` is represents the weighting + along each direction, with ``alphas`` along its diagonal. These are + multiplied to form the anisotropic alpha used for rotated gradients. """ def __init__(self, mesh, alphas=None, reg_dirs=None, ortho_check=True, **kwargs): @@ -170,18 +180,28 @@ def __init__(self, mesh, alphas=None, reg_dirs=None, ortho_check=True, **kwargs) def __call__(self, m): G = self.cell_gradient M_f = self.W - r = G @ (self.mapping * (self._delta_m(m))) + dm = ( + self.mapping * m - self.mapping * self.reference_model + if self.reference_model is not None + else self.mapping * m + ) + r = G @ dm return r @ M_f @ r def deriv(self, m): - m_d = self.mapping.deriv(self._delta_m(m)) + m_d = self.mapping.deriv(m) G = self.cell_gradient M_f = self.W - r = G @ (self.mapping * (self._delta_m(m))) + dm = ( + self.mapping * m - self.mapping * self.reference_model + if self.reference_model is not None + else self.mapping * m + ) + r = G @ dm return 2 * (m_d.T * (G.T @ (M_f @ r))) def deriv2(self, m, v=None): - m_d = self.mapping.deriv(self._delta_m(m)) + m_d = self.mapping.deriv(m) G = self.cell_gradient M_f = self.W if v is None: diff --git a/simpeg/regularization/base.py b/simpeg/regularization/base.py index 823c7fd0de..54d939cfb8 100644 --- a/simpeg/regularization/base.py +++ b/simpeg/regularization/base.py @@ -1,17 +1,13 @@ -from __future__ import annotations - import numpy as np from discretize.base import BaseMesh -from typing import TYPE_CHECKING from .. import maps from ..objective_function import BaseObjectiveFunction, ComboObjectiveFunction from .. import utils from .regularization_mesh import RegularizationMesh -from simpeg.utils.code_utils import deprecate_property, validate_ndarray_with_shape +from simpeg.utils.code_utils import validate_ndarray_with_shape -if TYPE_CHECKING: - from scipy.sparse import csr_matrix +from scipy.sparse import csr_matrix class BaseRegularization(BaseObjectiveFunction): @@ -71,18 +67,6 @@ def __init__( "It must be a dictionary with strings as keys and arrays as values." ) - # Raise errors on deprecated arguments: avoid old code that still uses - # them to silently fail - if (key := "indActive") in kwargs: - raise TypeError( - f"'{key}' argument has been removed. " - "Please use 'active_cells' instead." - ) - if (key := "cell_weights") in kwargs: - raise TypeError( - f"'{key}' argument has been removed. Please use 'weights' instead." - ) - super().__init__(nP=None, mapping=None, **kwargs) self._regularization_mesh = mesh self._weights = {} @@ -125,14 +109,6 @@ def active_cells(self, values: np.ndarray | None): if volume_term: self.set_weights(volume=self.regularization_mesh.vol) - indActive = deprecate_property( - active_cells, - "indActive", - "active_cells", - "0.19.0", - error=True, - ) - @property def model(self) -> np.ndarray: """The model parameters. @@ -260,14 +236,6 @@ def reference_model(self, values: np.ndarray | float): ) self._reference_model = values - mref = deprecate_property( - reference_model, - "mref", - "reference_model", - "0.19.0", - error=True, - ) - @property def regularization_mesh(self) -> RegularizationMesh: """Regularization mesh. @@ -282,36 +250,11 @@ def regularization_mesh(self) -> RegularizationMesh: """ return self._regularization_mesh - regmesh = deprecate_property( - regularization_mesh, - "regmesh", - "regularization_mesh", - "0.19.0", - error=True, - ) - - @property - def cell_weights(self) -> np.ndarray: - """Deprecated property for 'volume' and user defined weights.""" - raise AttributeError( - "'cell_weights' has been removed. " - "Please access weights using the `set_weights`, `get_weights`, and " - "`remove_weights` methods." - ) - - @cell_weights.setter - def cell_weights(self, value): - raise AttributeError( - "'cell_weights' has been removed. " - "Please access weights using the `set_weights`, `get_weights`, and " - "`remove_weights` methods." - ) - def get_weights(self, key) -> np.ndarray: """Cell weights for a given key. Parameters - ------------ + ---------- key: str Name of the weights requested. @@ -543,8 +486,9 @@ class Smallness(BaseRegularization): Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` cells that are active in the inversion. If ``None``, all cells are active. mapping : None, simpeg.maps.BaseMap - The mapping from the model parameters to the active cells in the inversion. - If ``None``, the mapping is the identity map. + The mapping function applied to the model parameters. Use a mapping to + get from model parameters to physical properties in active cells in the + mesh. If ``None``, the mapping is the identity map. reference_model : None, (n_param, ) numpy.ndarray Reference model. If ``None``, the reference model in the inversion is set to the starting model. @@ -558,38 +502,85 @@ class Smallness(BaseRegularization): Notes ----- - We define the regularization function (objective function) for smallness as: + In case no mapping or reference model is used, we define the regularization function + (objective function) for smallness as: .. math:: - \phi (m) = \int_\Omega \, w(r) \, - \Big [ m(r) - m^{(ref)}(r) \Big ]^2 \, dv - where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model and :math:`w(r)` - is a user-defined weighting function. + \phi (m) = \int_\Omega \, w(\mathbf{r}) \, + \left\lvert + m(\mathbf{r}) - m^\text{ref}(\mathbf{r}) + \right\rvert^2 \, + d\mathbf{r} + + where :math:`m(\mathbf{r})` is the model, :math:`m^\text{ref}(\mathbf{r})` is the + reference model, and :math:`w(\mathbf{r})` is a user-defined weighting function. - For implementation within SimPEG, the regularization function and its variables - must be discretized onto a `mesh`. The discretized approximation for the regularization - function (objective function) is expressed in linear form as: + For the implementation within SimPEG, the regularization function and its + variables must be discretized onto a `mesh`. The discretized approximation + for the regularization function (objective function) is expressed in linear + form as: .. math:: - \phi (\mathbf{m}) = \sum_i - \tilde{w}_i \, \bigg | \, m_i - m_i^{(ref)} \, \bigg |^2 - where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the mesh and - :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account - for cell dimensions in the discretization and 2) apply any user-defined weighting. + \phi (\mathbf{m}) = \sum_i \tilde{w}_i \, + \left\lvert \, + m_i - m_i^\text{ref} \, + \right\rvert^2 + + where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on + the mesh, and :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting + constants that: + + 1. account for cell dimensions in the discretization, and + 2. apply any user-defined weighting. + This is equivalent to an objective function of the form: .. math:: + \phi (\mathbf{m}) = - \Big \| \mathbf{W} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + \left\lVert + \mathbf{W} \left[ \mathbf{m} - \mathbf{m}^\text{ref} \right] + \right\rVert^2 - where + where :math:`\mathbf{m}` is the model vector, :math:`\mathbf{m}^\text{ref}` is the + reference model vector, and :math:`\mathbf{W}` is the weighting matrix, - - :math:`\mathbf{m}^{(ref)}` is a reference model (set using `reference_model`), and - - :math:`\mathbf{W}` is the weighting matrix. + **Mapping function** - **Custom weights and the weighting matrix:** + In case make use of a mapping function :math:`\mu` that maps values of the model + into a different space, then the regularization function for smallness gets defined + as: + + .. math:: + + \phi (m) = \int_\Omega \, w(\mathbf{r}) \, + \left\lvert + \mu(m(\mathbf{r})) - \mu(m^\text{ref}(\mathbf{r})) + \right\rvert^2 \, d\mathbf{r} + + In a discretized form, the previous equation is expressed as: + + .. math:: + + \phi (\mathbf{m}) = \sum_i + \tilde{w}_i \, + \left\lvert \, + \mu(m_i) - \mu(m_i^\text{ref}) \, + \right\rvert^2 + + And in matrix form: + + .. math:: + + \phi (\mathbf{m}) = + \left\lVert + \mathbf{W} \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \right\rVert^2. + + + **Custom weights and the weighting matrix** Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of custom cell weights. The weighting applied within the objective function is given by: @@ -631,10 +622,13 @@ def f_m(self, m) -> np.ndarray: For smallness regularization, the regularization kernel function is given by: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} - where :math:`\mathbf{m}` are the discrete model parameters and :math:`\mathbf{m}^{(ref)}` - is a reference model. For a more detailed description, see the *Notes* section below. + \mathbf{f_m}(\mathbf{m}) = \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) + + where :math:`\mathbf{m}` are the discrete model parameters, + :math:`\mathbf{m}^\text{ref}` + is a reference model, and :math:`\mu` is the mapping function. + For a more detailed description, see the *Notes* section below. Parameters ---------- @@ -651,36 +645,59 @@ def f_m(self, m) -> np.ndarray: The objective function for smallness regularization is given by: .. math:: + \phi_m (\mathbf{m}) = - \Big \| \mathbf{W} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + \left\lVert + \mathbf{W} + \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \right\rVert^2 - where :math:`\mathbf{m}` are the discrete model parameters defined on the mesh (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, and :math:`\mathbf{W}` is - the weighting matrix. See the :class:`Smallness` class documentation for more detail. + where :math:`\mathbf{m}` are the discrete model parameters defined on + the mesh (model), :math:`\mathbf{m}^\text{ref}` is the reference + model, :math:`\mu` is the mapping function, and :math:`\mathbf{W}` is + the weighting matrix. + See the :class:`Smallness` class documentation for more details. We define the regularization kernel function :math:`\mathbf{f_m}` as: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} + + \mathbf{f_m}(\mathbf{m}) = + \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) such that .. math:: - \phi_m (\mathbf{m}) = \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 + + \phi_m(\mathbf{m}) = \left\lVert \mathbf{W} \, \mathbf{f_m} \right\rVert^2 """ - return self.mapping * self._delta_m(m) + f_m = ( + self.mapping * m - self.mapping * self.reference_model + if self.reference_model is not None + else self.mapping * m + ) + return f_m def f_m_deriv(self, m) -> csr_matrix: r"""Derivative of the regularization kernel function. - For ``Smallness`` regularization, the derivative of the regularization kernel function - with respect to the model is given by: + For ``Smallness`` regularization, the derivative of the regularization + kernel function with respect to the model is given by: .. math:: - \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{I} - where :math:`\mathbf{I}` is the identity matrix. + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = + \frac{\partial \mu(\mathbf{m})}{\partial \mathbf{m}} + + where :math:`\mu` is the mapping function. If the mapping is the + identity function (:math:`\mu(\mathbf{m}) = \mathbf{m}`) then the + derivative of the kernel function is the is the identity matrix + :math:`\mathbf{I}`: + + .. math:: + + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{I} Parameters ---------- @@ -697,31 +714,45 @@ def f_m_deriv(self, m) -> csr_matrix: The objective function for smallness regularization is given by: .. math:: + \phi_m (\mathbf{m}) = - \Big \| \mathbf{W} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + \left\lVert + \mathbf{W} + \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \right\rVert^2 - where :math:`\mathbf{m}` are the discrete model parameters defined on the mesh (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, and :math:`\mathbf{W}` is - the weighting matrix. See the :class:`Smallness` class documentation for more detail. + where :math:`\mathbf{m}` are the discrete model parameters defined on + the mesh (model), :math:`\mathbf{m}^{(ref)}` is the reference model, + :math:`\mu` is the mapping function, and :math:`\mathbf{W}` is the + weighting matrix. See the :class:`Smallness` class documentation for + more details. We define the regularization kernel function :math:`\mathbf{f_m}` as: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} + + \mathbf{f_m}(\mathbf{m}) = \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) such that .. math:: - \phi_m (\mathbf{m}) = \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 + + \phi_m (\mathbf{m}) = + \left\lVert + \mathbf{W} \, \mathbf{f_m} + \right\rVert^2 Thus, the derivative with respect to the model is: .. math:: - \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{I} - where :math:`\mathbf{I}` is the identity matrix. + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = + \frac{\partial \mu(\mathbf{m})}{\partial \mathbf{m}} + + where :math:`\mu` is the mapping function, and :math:`\mathbf{I}` is + the identity matrix. """ - return self.mapping.deriv(self._delta_m(m)) + return self.mapping.deriv(m) class SmoothnessFirstOrder(BaseRegularization): @@ -771,48 +802,96 @@ class SmoothnessFirstOrder(BaseRegularization): along the x-direction as: .. math:: - \phi (m) = \int_\Omega \, w(r) \, - \bigg [ \frac{\partial m}{\partial x} \bigg ]^2 \, dv - where :math:`m(r)` is the model and :math:`w(r)` is a user-defined weighting function. + \phi (m) = \int_\Omega \, w(\mathbf{r}) \, + \left\lvert + \frac{\partial m(\mathbf{r})}{\partial x} + \right\rvert^2 \, d\mathbf{r} + + where :math:`m(\mathbf{r})` is the model, and :math:`w(\mathbf{r})` is + a user-defined weighting function. - For implementation within SimPEG, the regularization function and its variables - must be discretized onto a `mesh`. The discretized approximation for the regularization - function (objective function) is expressed in linear form as: + For the implementation within SimPEG, the regularization function and its + variables must be discretized onto a `mesh`. The discretized approximation + for the regularization function (objective function) is expressed in linear + form as: .. math:: - \phi (\mathbf{m}) = \sum_i - \tilde{w}_i \, \bigg | \, \frac{\partial m_i}{\partial x} \, \bigg |^2 - where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the mesh - and :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that - 1) account for cell dimensions in the discretization and 2) apply any user-defined weighting. + \phi (\mathbf{m}) = \sum_i \tilde{w}_i \, + \left\lvert \, + \frac{\partial m_i}{\partial x} \, + \right\rvert^2 + + where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on + the mesh, and :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting + constants that: + + 1. account for cell dimensions in the discretization, and + 2. apply any user-defined weighting. + This is equivalent to an objective function of the form: .. math:: - \phi (\mathbf{m}) = \Big \| \mathbf{W \, G_x m } \, \Big \|^2 - where + \phi (\mathbf{m}) = + \left\lVert + \mathbf{W} \mathbf{G_x} \mathbf{m} + \right\rVert^2 + + where :math:`\mathbf{m}` is the model vector, + :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction, + and :math:`\mathbf{W}` is the weighting matrix. - - :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction, and - - :math:`\mathbf{W}` is the weighting matrix. + .. note:: + + Note that since :math:`\mathbf{G_x}` maps from cell centers to x-faces, + :math:`\mathbf{W}` is an operator that acts on variables living on x-faces. - Note that since :math:`\mathbf{G_x}` maps from cell centers to x-faces, - :math:`\mathbf{W}` is an operator that acts on variables living on x-faces. **Reference model in smoothness:** - Gradients/interfaces within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be - preserved by including the reference model the regularization. + Gradients/interfaces within a discrete reference model :math:`\mathbf{m}^\text{ref}` + can be preserved by including the reference model the regularization. In this case, the objective function becomes: .. math:: - \phi (\mathbf{m}) = \Big \| \mathbf{W G_x} - \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - This functionality is used by setting a reference model with the - `reference_model` property, and by setting the `reference_model_in_smooth` parameter - to ``True``. + \phi(\mathbf{m}) = + \left\lVert + \mathbf{W G_x} + \left[ \mathbf{m} - \mathbf{m}^\text{ref} \right] + \right\rVert^2 + + This functionality is used by setting a reference model with the `reference_model` + property, and by setting the `reference_model_in_smooth` parameter to ``True``. + + + **Mapping function:** + + In case make use of a mapping function :math:`\mu` that maps values of the model + into a different space, then the regularization function for first-order smoothness + along the x-direction as: + + .. math:: + + \phi (m) = \int_\Omega \, w(\mathbf{r}) \, + \left\lvert + \frac{\partial}{\partial x} + \left[ \mu(m) - \mu(m^\text{ref}) \right] + \right\rvert^2 \, d\mathbf{r} + + In matrix form, the previous equation is expressed as: + + .. math:: + + \phi (\mathbf{m}) = + \left\lVert + \mathbf{W} + \mathbf{G_x} + \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \right\rVert^2. + **Custom weights and the weighting matrix:** @@ -942,11 +1021,14 @@ def f_m(self, m): the regularization kernel function is given by: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - where :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction - (i.e. x-derivative), :math:`\mathbf{m}` are the discrete model parameters defined on the - mesh and :math:`\mathbf{m}^{(ref)}` is the reference model (optional). + \mathbf{f_m}(\mathbf{m}) = + \mathbf{G_x} \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + + where :math:`\mathbf{G_x}` is the partial cell gradient operator along the + x-direction (i.e. x-derivative), :math:`\mathbf{m}` are the discrete model + parameters defined on the mesh, :math:`\mathbf{m}^{(ref)}` is the reference + model (optional), and :math:`\mu` is the mapping function. Similarly for smoothness along y and z. Parameters @@ -965,45 +1047,63 @@ def f_m(self, m): is given by: .. math:: - \phi_m (\mathbf{m}) = - \Big \| \mathbf{W G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + + \phi (\mathbf{m}) = + \lVert + \mathbf{W} + \mathbf{G_x} + \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \rVert^2. where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{G_x}` is the partial - cell gradient operator along the x-direction (i.e. x-derivative), and :math:`\mathbf{W}` is - the weighting matrix. Similar for smoothness along y and z. + :math:`\mathbf{m}^\text{ref}` is the reference model, :math:`\mathbf{G_x}` is the + partial cell gradient operator along the x-direction (i.e. x-derivative), + :math:`\mu` is the mapping function, and :math:`\mathbf{W}` is the weighting + matrix. + Similar for smoothness along y and z. See the :class:`SmoothnessFirstOrder` class documentation for more detail. We define the regularization kernel function :math:`\mathbf{f_m}` as: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - such that + \mathbf{f_m}(\mathbf{m}) = + \mathbf{G_x} \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + + such that: .. math:: - \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^2 + + \phi_m(\mathbf{m}) = \lVert \mathbf{W \, f_m} \rVert^2. + """ - dfm_dl = self.mapping * self._delta_m(m) + dfm_dl = ( + self.mapping * m - self.mapping * self.reference_model + if self.reference_model is not None and self.reference_model_in_smooth + else self.mapping * m + ) if self.units is not None and self.units.lower() == "radian": return ( utils.mat_utils.coterminal(self.cell_gradient.sign() @ dfm_dl) / self._cell_distances ) + return self.cell_gradient @ dfm_dl def f_m_deriv(self, m) -> csr_matrix: r"""Derivative of the regularization kernel function. - For first-order smoothness regularization in the x-direction, the derivative of the - regularization kernel function with respect to the model is given by: + For first-order smoothness regularization in the x-direction, the derivative of + the regularization kernel function with respect to the model is given by: .. math:: - \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{G_x} + + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = + \mathbf{G_x} \frac{\partial \mu(\mathbf{m})}{\partial \mathbf{m}} where :math:`\mathbf{G_x}` is the partial cell gradient operator along x - (i.e. the x-derivative). + (i.e. the x-derivative), and :math:`\mu` is the mapping function. Parameters ---------- @@ -1021,31 +1121,44 @@ def f_m_deriv(self, m) -> csr_matrix: is given by: .. math:: - \phi_m (\mathbf{m}) = - \Big \| \mathbf{W G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + + \phi (\mathbf{m}) = + \left\lVert + \mathbf{W} + \mathbf{G_x} + \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \right\rVert^2. + where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{G_x}` is the partial - cell gradient operator along the x-direction (i.e. x-derivative), and :math:`\mathbf{W}` is - the weighting matrix. Similar for smoothness along y and z. + :math:`\mathbf{m}^\text{ref}` is the reference model, :math:`\mathbf{G_x}` is + the partial cell gradient operator along the x-direction (i.e. x-derivative), + :math:`\mu` is the mapping function, and :math:`\mathbf{W}` is the weighting + matrix. + Similar for smoothness along y and z. See the :class:`SmoothnessFirstOrder` class documentation for more detail. We define the regularization kernel function :math:`\mathbf{f_m}` as: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - such that + \mathbf{f_m}(\mathbf{m}) = + \mathbf{G_x} \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + + such that: .. math:: - \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^2 + + \phi_m(\mathbf{m}) = \lVert \mathbf{W \, f_m} \rVert^2. The derivative with respect to the model is therefore: .. math:: - \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{G_x} + + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} + = \mathbf{G_x} \frac{\partial \mu(\mathbf{m})}{\partial \mathbf{m}} """ - return self.cell_gradient @ self.mapping.deriv(self._delta_m(m)) + return self.cell_gradient @ self.mapping.deriv(m) @property def W(self) -> csr_matrix: @@ -1136,45 +1249,92 @@ class SmoothnessSecondOrder(SmoothnessFirstOrder): smoothness along the x-direction as: .. math:: - \phi (m) = \int_\Omega \, w(r) \, - \bigg [ \frac{\partial^2 m}{\partial x^2} \bigg ]^2 \, dv - where :math:`m(r)` is the model and :math:`w(r)` is a user-defined weighting function. + \phi (m) = \int_\Omega \, w(\mathbf{r}) \, + \left\lvert + \frac{\partial^2 m(\mathbf{r})}{\partial x^2} + \right\rvert^2 \, d\mathbf{r} + + where :math:`m(\mathbf{r})` is the model, and :math:`w(\mathbf{r})` is + a user-defined weighting function. - For implementation within SimPEG, the regularization function and its variables - must be discretized onto a `mesh`. The discretized approximation for the regularization - function (objective function) is expressed in linear form as: + For the implementation within SimPEG, the regularization function and its + variables must be discretized onto a `mesh`. The discretized approximation + for the regularization function (objective function) is expressed in linear + form as: .. math:: - \phi (\mathbf{m}) = \sum_i - \tilde{w}_i \, \bigg | \, \frac{\partial^2 m_i}{\partial x^2} \, \bigg |^2 - where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on the - mesh and :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that - 1) account for cell dimensions in the discretization and 2) apply any user-defined weighting. + \phi (\mathbf{m}) = \sum_i \tilde{w}_i \, + \left\lvert \, + \frac{\partial^2 m_i}{\partial x^2} \, + \right\rvert^2 + + where :math:`m_i \in \mathbf{m}` are the discrete model parameter values defined on + the mesh, and :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting + constants that: + + 1. account for cell dimensions in the discretization, and + 2. apply any user-defined weighting. + This is equivalent to an objective function of the form: .. math:: - \phi (\mathbf{m}) = \big \| \mathbf{W \, L_x \, m } \, \big \|^2 - where + \phi (\mathbf{m}) = + \left\lVert + \mathbf{W} \mathbf{L_x} \mathbf{m} + \right\rVert^2 + + where :math:`\mathbf{m}` is the model vector, + :math:`\mathbf{L_x}` is the second-order derivative operator with respect to + :math:`x`, and :math:`\mathbf{W}` is the weighting matrix. - - :math:`\mathbf{L_x}` is a second-order derivative operator with respect to :math:`x`, and - - :math:`\mathbf{W}` is the weighting matrix. **Reference model in smoothness:** - Second-order smoothness within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be - preserved by including the reference model the smoothness regularization function. + Second-order smoothness within a discrete reference model + :math:`\mathbf{m}^\text{ref}` can be preserved by including the reference model the + smoothness regularization function. In this case, the objective function becomes: .. math:: - \phi (\mathbf{m}) = \Big \| \mathbf{W L_x} - \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 - This functionality is used by setting a reference model with the - `reference_model` property, and by setting the `reference_model_in_smooth` parameter - to ``True``. + \phi(\mathbf{m}) = + \left\lVert + \mathbf{W L_x} + \left[ \mathbf{m} - \mathbf{m}^\text{ref} \right] + \right\rVert^2 + + This functionality is used by setting a reference model with the `reference_model` + property, and by setting the `reference_model_in_smooth` parameter to ``True``. + + + **Mapping function:** + + In case make use of a mapping function :math:`\mu` that maps values of the model + into a different space, then the regularization function for second-order smoothness + along the x-direction as: + + .. math:: + + \phi (m) = \int_\Omega \, w(\mathbf{r}) \, + \left\lvert + \frac{\partial^2}{\partial x^2} + \left[ \mu(m) - \mu(m^\text{ref}) \right] + \right\rvert^2 \, d\mathbf{r} + + In matrix form, the previous equation is expressed as: + + .. math:: + + \phi (\mathbf{m}) = + \left\lVert + \mathbf{W} + \mathbf{L_x} + \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \right\rVert^2. + **Custom weights and the weighting matrix:** @@ -1210,11 +1370,14 @@ def f_m(self, m): the regularization kernel function is given by: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] - where where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model (optional), :math:`\mathbf{L_x}` - is the discrete second order x-derivative operator. + \mathbf{f_m}(\mathbf{m}) = + \mathbf{L_x} \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + + where :math:`\mathbf{L_x}` is the discrete second order x-derivative operator, + :math:`\mathbf{m}` are the discrete model parameters defined on the mesh, + :math:`\mathbf{m}^{(ref)}` is the reference model (optional), and :math:`\mu` is + the mapping function. Parameters ---------- @@ -1232,26 +1395,39 @@ def f_m(self, m): is given by: .. math:: - \phi_m (\mathbf{m}) = - \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + + \phi(\mathbf{m}) = + \lVert + \mathbf{W} + \mathbf{L_x} + \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \rVert^2. where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{L_x}` is the - second-order x-derivative operator, and :math:`\mathbf{W}` is - the weighting matrix. Similar for smoothness along y and z. + :math:`\mathbf{m}^\text{ref}` is the reference model, :math:`\mathbf{L_x}` is + the second-order x-derivative operator, :math:`\mu` is the mapping function, and + :math:`\mathbf{W}` is the weighting matrix. + Similar for smoothness along y and z. See the :class:`SmoothnessSecondOrder` class documentation for more detail. We define the regularization kernel function :math:`\mathbf{f_m}` as: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] + + \mathbf{f_m}(\mathbf{m}) = + \mathbf{L_x} \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] such that .. math:: - \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^2 + + \phi_m(\mathbf{m}) = \lVert \mathbf{W \, f_m} \rVert^2. """ - dfm_dl = self.mapping * self._delta_m(m) + dfm_dl = ( + self.mapping * m - self.mapping * self.reference_model + if self.reference_model is not None and self.reference_model_in_smooth + else self.mapping * m + ) if self.units is not None and self.units.lower() == "radian": return self.cell_gradient.T @ ( @@ -1270,9 +1446,12 @@ def f_m_deriv(self, m) -> csr_matrix: regularization kernel function with respect to the model is given by: .. math:: - \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{L_x} - where :math:`\mathbf{L_x}` is the second-order derivative operator with respect to x. + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = + \mathbf{L_x} \frac{\partial \mu(\mathbf{m})}{\partial \mathbf{m}} + + where :math:`\mathbf{L_x}` is the second-order derivative operator with respect + to x, and :math:`\mu` is the mapping function. Parameters ---------- @@ -1290,35 +1469,42 @@ def f_m_deriv(self, m) -> csr_matrix: is given by: .. math:: - \phi_m (\mathbf{m}) = - \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + + \phi (\mathbf{m}) = + \left\lVert + \mathbf{W} + \mathbf{L_x} + \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] + \right\rVert^2. where :math:`\mathbf{m}` are the discrete model parameters (model), - :math:`\mathbf{m}^{(ref)}` is the reference model, :math:`\mathbf{L_x}` is the - second-order x-derivative operator, and :math:`\mathbf{W}` is - the weighting matrix. Similar for smoothness along y and z. + :math:`\mathbf{m}^\text{ref}` is the reference model, :math:`\mathbf{L_x}` is + the second-order x-derivative operator, :math:`\mu` is the mapping function and + :math:`\mathbf{W}` is the weighting matrix. + Similar for smoothness along y and z. See the :class:`SmoothnessSecondOrder` class documentation for more detail. We define the regularization kernel function :math:`\mathbf{f_m}` as: .. math:: - \mathbf{f_m}(\mathbf{m}) = \mathbf{L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] + + \mathbf{f_m}(\mathbf{m}) = + \mathbf{L_x} \left[ \mu(\mathbf{m}) - \mu(\mathbf{m}^\text{ref}) \right] such that .. math:: - \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^2 + + \phi_m(\mathbf{m}) = \lVert \mathbf{W \, f_m} \rVert^2. The derivative of the regularization kernel function with respect to the model is: .. math:: - \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{L_x} + + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} + = \mathbf{L_x} \frac{\partial \mu(\mathbf{m})}{\partial \mathbf{m}} """ - return ( - self.cell_gradient.T - @ self.cell_gradient - @ self.mapping.deriv(self._delta_m(m)) - ) + return self.cell_gradient.T @ self.cell_gradient @ self.mapping.deriv(m) @property def W(self) -> csr_matrix: @@ -1562,19 +1748,6 @@ def __init__( ) self._regularization_mesh = mesh - # Raise errors on deprecated arguments: avoid old code that still uses - # them to silently fail - if (key := "indActive") in kwargs: - raise TypeError( - f"'{key}' argument has been removed. " - "Please use 'active_cells' instead." - ) - - if (key := "cell_weights") in kwargs: - raise TypeError( - f"'{key}' argument has been removed. Please use 'weights' instead." - ) - self.alpha_s = alpha_s if alpha_x is not None: if length_scale_x is not None: @@ -2082,14 +2255,6 @@ def active_cells(self, values: np.ndarray): for objfct in self.objfcts: objfct.active_cells = active_cells - indActive = deprecate_property( - active_cells, - "indActive", - "active_cells", - "0.19.0", - error=True, - ) - @property def reference_model(self) -> np.ndarray: """Reference model. @@ -2112,14 +2277,6 @@ def reference_model(self, values: np.ndarray | float): self._reference_model = values - mref = deprecate_property( - reference_model, - "mref", - "reference_model", - "0.19.0", - error=True, - ) - @property def model(self) -> np.ndarray: """The model associated with regularization. diff --git a/simpeg/regularization/pgi.py b/simpeg/regularization/pgi.py index 08a8f4ddef..0193647251 100644 --- a/simpeg/regularization/pgi.py +++ b/simpeg/regularization/pgi.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import copy import warnings @@ -10,7 +8,6 @@ from ..objective_function import ComboObjectiveFunction from ..utils import ( Identity, - deprecate_property, mkvc, sdiag, timeIt, @@ -72,10 +69,10 @@ class PGIsmallness(Smallness): ``tensor``, ``QuadTree`` or ``Octree`` meshes. approx_gradient : bool If ``True``, use the L2-approximation of the gradient by assuming - physical property values of different types are uncorrelated. + the physical property distributions of each geologic units are distinct approx_eval : bool If ``True``, use the L2-approximation evaluation of the smallness term by assuming - physical property values of different types are uncorrelated. + the physical property distributions of each geologic units are distinct approx_hessian : bool Approximate the Hessian of the regularization function. non_linear_relationship : bool @@ -472,30 +469,22 @@ def __call__(self, m, external_weights=True): r0 = (W * mkvc(dmr)).reshape(dmr.shape, order="F") if self.gmm.covariance_type == "tied": - r1 = np.r_[ - [np.dot(self.gmm.precisions_, np.r_[r0[i]]) for i in range(len(r0))] - ] + r1 = mkvc(np.dot(self.gmm.precisions_, r0.T).T) elif ( self.gmm.covariance_type == "diag" or self.gmm.covariance_type == "spherical" ): - r1 = np.r_[ - [ - np.dot( - self.gmm.precisions_[membership[i]] - * np.eye(len(self.wiresmap.maps)), - np.r_[r0[i]], - ) - for i in range(len(r0)) - ] - ] + r1 = np.zeros_like(r0) + for i in range(self.gmm.n_components): + selection = membership == i + r1[selection] = r0[selection] * self.gmm.precisions_[i].T + r1 = mkvc(r1) else: - r1 = np.r_[ - [ - np.dot(self.gmm.precisions_[membership[i]], np.r_[r0[i]]) - for i in range(len(r0)) - ] - ] + r1 = np.zeros_like(r0) + for i in range(self.gmm.n_components): + selection = membership == i + r1[selection] = (self.gmm.precisions_[i] @ r0[selection].T).T + r1 = mkvc(r1) return mkvc(r0).dot(mkvc(r1)) @@ -569,25 +558,17 @@ def deriv(self, m): if self.non_linear_relationships: raise Exception("Not implemented") - r = mkvc( - np.r_[[np.dot(self.gmm.precisions_, r0[i]) for i in range(len(r0))]] - ) + r = mkvc(np.dot(self.gmm.precisions_, r0.T).T) elif ( self.gmm.covariance_type == "diag" or self.gmm.covariance_type == "spherical" ) and not self.non_linear_relationships: - r = mkvc( - np.r_[ - [ - np.dot( - self.gmm.precisions_[membership[i]] - * np.eye(len(self.wiresmap.maps)), - r0[i], - ) - for i in range(len(r0)) - ] - ] - ) + r = np.zeros_like(r0) + for i in range(self.gmm.n_components): + selection = membership == i + r[selection] = r0[selection] * self.gmm.precisions_[i].T + r = mkvc(r) + else: if self.non_linear_relationships: r = mkvc( @@ -608,14 +589,11 @@ def deriv(self, m): else: r0 = (self.W * (mkvc(dm))).reshape(dm.shape, order="F") - r = mkvc( - np.r_[ - [ - np.dot(self.gmm.precisions_[membership[i]], r0[i]) - for i in range(len(r0)) - ] - ] - ) + r = np.zeros_like(r0) + for i in range(self.gmm.n_components): + selection = membership == i + r[selection] = (self.gmm.precisions_[i] @ r0[selection].T).T + r = mkvc(r) return 2 * mkvc(mD.T * (self.W.T * r)) else: @@ -842,9 +820,7 @@ def deriv2(self, m, v=None): mDv = np.c_[mDv] r0 = (self.W * (mkvc(mDv))).reshape(mDv.shape, order="F") second_deriv_times_r0 = mkvc( - np.r_[ - [np.dot(self._r_second_deriv[i], r0[i]) for i in range(len(r0))] - ] + np.einsum("ijk,ik->ij", self._r_second_deriv, r0) ) return 2 * mkvc(mD.T * (self.W * second_deriv_times_r0)) else: @@ -1388,11 +1364,3 @@ def reference_model(self, values: np.ndarray | float): for fct in self.objfcts: fct.reference_model = values - - mref = deprecate_property( - reference_model, - "mref", - "reference_model", - "0.19.0", - error=True, - ) diff --git a/simpeg/regularization/regularization_mesh.py b/simpeg/regularization/regularization_mesh.py index dea11bb2f1..38b97db5e3 100755 --- a/simpeg/regularization/regularization_mesh.py +++ b/simpeg/regularization/regularization_mesh.py @@ -1,7 +1,7 @@ import numpy as np import scipy.sparse as sp -from simpeg.utils.code_utils import deprecate_property, validate_active_indices +from simpeg.utils.code_utils import validate_active_indices from .. import props, utils @@ -518,28 +518,6 @@ def cell_gradient_z(self) -> sp.csr_matrix: ) return self._cell_gradient_z - cellDiffx = deprecate_property( - cell_gradient_x, - "cellDiffx", - "cell_gradient_x", - "0.19.0", - error=True, - ) - cellDiffy = deprecate_property( - cell_gradient_y, - "cellDiffy", - "cell_gradient_y", - "0.19.0", - error=True, - ) - cellDiffz = deprecate_property( - cell_gradient_z, - "cellDiffz", - "cell_gradient_z", - "0.19.0", - error=True, - ) - @property def cell_distances_x(self) -> np.ndarray: """Cell center distance array along the x-direction. diff --git a/simpeg/regularization/sparse.py b/simpeg/regularization/sparse.py index a917e7ecbd..14c6aa9f04 100644 --- a/simpeg/regularization/sparse.py +++ b/simpeg/regularization/sparse.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import numpy as np from discretize.base import BaseMesh @@ -11,7 +9,6 @@ Smallness, SmoothnessFirstOrder, ) -from .. import utils from ..utils import ( validate_ndarray_with_shape, validate_float, @@ -577,12 +574,6 @@ class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): """ def __init__(self, mesh, orientation="x", gradient_type="total", **kwargs): - # Raise error if removed arguments were passed - if (key := "gradientType") in kwargs: - raise TypeError( - f"'{key}' argument has been removed. " - "Please use 'gradient_type' instead." - ) self.gradient_type = gradient_type super().__init__(mesh=mesh, orientation=orientation, **kwargs) @@ -693,14 +684,6 @@ def gradient_type(self, value: str): "gradient_type", value, ["total", "components"] ) - gradientType = utils.code_utils.deprecate_property( - gradient_type, - "gradientType", - new_name="gradient_type", - removal_version="0.19.0", - error=True, - ) - class Sparse(WeightedLeastSquares): r"""Sparse norm weighted least squares regularization. @@ -933,13 +916,6 @@ def __init__( f"Value of type {type(mesh)} provided." ) - # Raise error if removed arguments were passed - if (key := "gradientType") in kwargs: - raise TypeError( - f"'{key}' argument has been removed. " - "Please use 'gradient_type' instead." - ) - self._regularization_mesh = mesh if active_cells is not None: self._regularization_mesh.active_cells = active_cells @@ -997,10 +973,6 @@ def gradient_type(self, value: str): self._gradient_type = value - gradientType = utils.code_utils.deprecate_property( - gradient_type, "gradientType", "0.19.0", error=True - ) - @property def norms(self): """Norms for the child regularization classes. diff --git a/simpeg/regularization/vector.py b/simpeg/regularization/vector.py index cc2f628c44..579e57e9bb 100644 --- a/simpeg/regularization/vector.py +++ b/simpeg/regularization/vector.py @@ -1,15 +1,10 @@ -from __future__ import annotations -from typing import TYPE_CHECKING - import scipy.sparse as sp import numpy as np from .base import Smallness from discretize.base import BaseMesh from .base import RegularizationMesh, BaseRegularization from .sparse import Sparse, SparseSmallness, SparseSmoothness - -if TYPE_CHECKING: - from scipy.sparse import csr_matrix +from scipy.sparse import csr_matrix class BaseVectorRegularization(BaseRegularization): diff --git a/simpeg/seismic/straight_ray_tomography/simulation.py b/simpeg/seismic/straight_ray_tomography/simulation.py index 35916d2ab2..1070f1d544 100644 --- a/simpeg/seismic/straight_ray_tomography/simulation.py +++ b/simpeg/seismic/straight_ray_tomography/simulation.py @@ -1,9 +1,10 @@ +import discretize import numpy as np import scipy.sparse as sp import matplotlib.pyplot as plt from ...simulation import LinearSimulation -from ...utils import sub2ind +from ...utils import sub2ind, validate_type from ... import props @@ -77,13 +78,25 @@ def _lineintegral(M, Tx, Rx): class Simulation2DIntegral(LinearSimulation): slowness, slownessMap, slownessDeriv = props.Invertible("Slowness model (1/v)") - def __init__( - self, mesh=None, survey=None, slowness=None, slownessMap=None, **kwargs - ): - super().__init__(mesh=mesh, survey=survey, **kwargs) + def __init__(self, mesh, survey=None, slowness=None, slownessMap=None, **kwargs): + self.mesh = mesh + super().__init__(survey=survey, **kwargs) self.slowness = slowness self.slownessMap = slownessMap + @property + def mesh(self): + return self._mesh + + @mesh.setter + def mesh(self, value): + value = validate_type("mesh", value, discretize.TensorMesh, cast=False) + if value.dim != 2: + raise ValueError( + f"{type(self).__name__} mesh must be 2D, received a {value.dim}D mesh." + ) + self._mesh = value + @property def A(self): if getattr(self, "_A", None) is not None: diff --git a/simpeg/simulation.py b/simpeg/simulation.py index cc751647cd..bbde7df0f7 100644 --- a/simpeg/simulation.py +++ b/simpeg/simulation.py @@ -3,33 +3,27 @@ """ import os -import inspect import numpy as np import warnings -from discretize.base import BaseMesh from discretize import TensorMesh -from discretize.utils import unpack_widths, sdiag +from discretize.utils import unpack_widths, sdiag, mkvc from . import props -from .data import SyntheticData, Data +from .typing import RandomSeed +from .data import SyntheticData from .survey import BaseSurvey from .utils import ( Counter, timeIt, count, - mkvc, validate_ndarray_with_shape, validate_float, validate_type, validate_string, validate_integer, ) - -try: - from pymatsolver import Pardiso as DefaultSolver -except ImportError: - from .utils.solver_utils import SolverLU as DefaultSolver +import uuid __all__ = ["LinearSimulation", "ExponentialSinusoidSimulation"] @@ -53,19 +47,8 @@ class BaseSimulation(props.HasModel): Parameters ---------- - mesh : discretize.base.BaseMesh, optional - Mesh on which the forward problem is discretized. survey : simpeg.survey.BaseSurvey, optional The survey for the simulation. - solver : None or pymatsolver.base.Base, optional - Numerical solver used to solve the forward problem. If ``None``, - an appropriate solver specific to the simulation class is set by default. - solver_opts : dict, optional - Solver-specific parameters. If ``None``, default parameters are used for - the solver set by ``solver``. Otherwise, the ``dict`` must contain appropriate - pairs of keyword arguments and parameter values for the solver. Please visit - `pymatsolver `__ to learn more - about solvers and their parameters. sensitivity_path : str, optional Path to directory where sensitivity file is stored. counter : None or simpeg.utils.Counter @@ -78,49 +61,22 @@ class BaseSimulation(props.HasModel): def __init__( self, - mesh=None, survey=None, - solver=None, - solver_opts=None, sensitivity_path=None, counter=None, verbose=False, **kwargs, ): - self.mesh = mesh self.survey = survey - if solver is None: - solver = DefaultSolver - self.solver = solver - if solver_opts is None: - solver_opts = {} - self.solver_opts = solver_opts if sensitivity_path is None: sensitivity_path = os.path.join(".", "sensitivity") self.sensitivity_path = sensitivity_path self.counter = counter self.verbose = verbose - super().__init__(**kwargs) - - @property - def mesh(self): - """Mesh for the simulation. - - For more on meshes, visit :py:class:`discretize.base.BaseMesh`. + self._uuid = uuid.uuid4() - Returns - ------- - discretize.base.BaseMesh - Mesh on which the forward problem is discretized. - """ - return self._mesh - - @mesh.setter - def mesh(self, value): - if value is not None: - value = validate_type("mesh", value, BaseMesh, cast=False) - self._mesh = value + super().__init__(**kwargs) @property def survey(self): @@ -171,60 +127,6 @@ def sensitivity_path(self): def sensitivity_path(self, value): self._sensitivity_path = validate_string("sensitivity_path", value) - @property - def solver(self): - r"""Numerical solver used in the forward simulation. - - Many forward simulations in SimPEG require solutions to discrete linear - systems of the form: - - .. math:: - \mathbf{A}(\mathbf{m}) \, \mathbf{u} = \mathbf{q} - - where :math:`\mathbf{A}` is an invertible matrix that depends on the - model :math:`\mathbf{m}`. The numerical solver can be set using the - ``solver`` property. In SimPEG, the - `pymatsolver `__ package - is used to create solver objects. Parameters specific to each solver - can be set manually using the ``solver_opts`` property. - - Returns - ------- - pymatsolver.base.Base - Numerical solver used to solve the forward problem. - """ - return self._solver - - @solver.setter - def solver(self, cls): - if cls is not None: - if not inspect.isclass(cls): - raise TypeError(f"solver must be a class, not a {type(cls)}") - if not hasattr(cls, "__mul__"): - raise TypeError("solver must support the multiplication operator, `*`.") - self._solver = cls - - @property - def solver_opts(self): - """Solver-specific parameters. - - The parameters specific to the solver set with the ``solver`` property are set - upon instantiation. The ``solver_opts`` property is used to set solver-specific properties. - This is done by providing a ``dict`` that contains appropriate pairs of keyword arguments - and parameter values. Please visit `pymatsolver `__ - to learn more about solvers and their parameters. - - Returns - ------- - dict - keyword arguments and parameters passed to the solver. - """ - return self._solver_opts - - @solver_opts.setter - def solver_opts(self, value): - self._solver_opts = validate_type("solver_opts", value, dict, cast=False) - @property def verbose(self): """Verbose progress printout. @@ -285,11 +187,13 @@ def dpred(self, m=None, f=None): f = self.fields(m) - data = Data(self.survey) + survey_slices = self.survey.get_all_slices() + dpred = np.full(self.survey.nD, np.nan) for src in self.survey.source_list: for rx in src.receiver_list: - data[src, rx] = rx.eval(src, self.mesh, f) - return mkvc(data) + src_rx_slice = survey_slices[src, rx] + dpred[src_rx_slice] = mkvc(rx.eval(src, self.mesh, f)) + return mkvc(dpred) @timeIt def Jvec(self, m, v, f=None): @@ -465,7 +369,7 @@ def make_synthetic_data( noise_floor=0.0, f=None, add_noise=False, - random_seed=None, + random_seed: RandomSeed | None = None, **kwargs, ): r"""Make synthetic data for the model and Gaussian noise provided. @@ -490,8 +394,10 @@ def make_synthetic_data( forward problem to obtain noiseless data. add_noise : bool Whether to add gaussian noise to the synthetic data or not. - random_seed : int, optional - Random seed to pass to :py:class:`numpy.random.default_rng`. + random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed used for random sampling. It can either be an int or + a predefined Numpy random number generator (see + ``numpy.random.default_rng``). Returns ------- @@ -511,8 +417,8 @@ def make_synthetic_data( dclean = self.dpred(m, f=f) if add_noise is True: - std = np.sqrt((relative_error * np.abs(dclean)) ** 2 + noise_floor**2) random_num_generator = np.random.default_rng(seed=random_seed) + std = np.sqrt((relative_error * np.abs(dclean)) ** 2 + noise_floor**2) noise = random_num_generator.normal(loc=0, scale=std, size=dclean.shape) dobs = dclean + noise else: @@ -537,9 +443,6 @@ class BaseTimeSimulation(BaseSimulation): Parameters ---------- - mesh : discretize.base.BaseMesh, optional - Mesh on which the forward problem is discretized. This is not necessarily - the same as the mesh on which the simulation is defined. t0 : float, optional Initial time, in seconds, for the time-dependent forward simulation. time_steps : (n_steps, ) numpy.ndarray, optional @@ -568,10 +471,10 @@ class BaseTimeSimulation(BaseSimulation): representation. """ - def __init__(self, mesh=None, t0=0.0, time_steps=None, **kwargs): + def __init__(self, t0=0.0, time_steps=None, **kwargs): self.t0 = t0 self.time_steps = time_steps - super().__init__(mesh=mesh, **kwargs) + super().__init__(**kwargs) @property def time_steps(self): @@ -695,11 +598,13 @@ def dpred(self, m=None, f=None): if f is None: f = self.fields(m) - data = Data(self.survey) + survey_slices = self.survey.get_all_slices() + dpred = np.full(self.survey.nD, np.nan) for src in self.survey.source_list: for rx in src.receiver_list: - data[src, rx] = rx.eval(src, self.mesh, self.time_mesh, f) - return data.dobs + src_rx_slice = survey_slices[src, rx] + dpred[src_rx_slice] = mkvc(rx.eval(src, self.mesh, self.time_mesh, f)) + return dpred ############################################################################## @@ -735,9 +640,6 @@ class LinearSimulation(BaseSimulation): Parameters ---------- - mesh : discretize.BaseMesh, optional - Mesh on which the forward problem is discretized. This is not necessarily - the same as the mesh on which the simulation is defined. model_map : simpeg.maps.BaseMap Mapping from the model parameters to vector that the linear operator acts on. G : (n_data, n_param) numpy.ndarray or scipy.sparse.csr_matrx @@ -750,11 +652,10 @@ class LinearSimulation(BaseSimulation): "The model for a linear problem" ) - def __init__(self, mesh=None, linear_model=None, model_map=None, G=None, **kwargs): - super().__init__(mesh=mesh, **kwargs) + def __init__(self, linear_model=None, model_map=None, G=None, **kwargs): + super().__init__(**kwargs) self.linear_model = linear_model self.model_map = model_map - self.solver = None if G is not None: self.G = G @@ -893,6 +794,8 @@ class ExponentialSinusoidSimulation(LinearSimulation): Parameters ---------- + mesh : discretize.TensorMesh + 1D TensorMesh defining the discretization of the model space. n_kernels : int The number of kernel factors for the linear problem; i.e. the number of :math:`j_i \in [j_0, ... , j_n]`. This sets the number of rows @@ -907,7 +810,8 @@ class ExponentialSinusoidSimulation(LinearSimulation): Maximum value for the spread of the kernel factors. """ - def __init__(self, n_kernels=20, p=-0.25, q=0.25, j0=0.0, jn=60.0, **kwargs): + def __init__(self, mesh, n_kernels=20, p=-0.25, q=0.25, j0=0.0, jn=60.0, **kwargs): + self.mesh = mesh self.n_kernels = n_kernels self.p = p self.q = q @@ -915,6 +819,26 @@ def __init__(self, n_kernels=20, p=-0.25, q=0.25, j0=0.0, jn=60.0, **kwargs): self.jn = jn super(ExponentialSinusoidSimulation, self).__init__(**kwargs) + @property + def mesh(self): + """Mesh for the simulation. + + Returns + ------- + discretize.TensorMesh + Mesh on which the forward problem is discretized. + """ + return self._mesh + + @mesh.setter + def mesh(self, value): + value = validate_type("mesh", value, TensorMesh, cast=False) + if value.dim != 1: + raise ValueError( + f"{type(self).__name__} mesh must be 1D, received a {value.dim}D mesh." + ) + self._mesh = value + @property def n_kernels(self): r"""The number of kernel factors for the linear problem. diff --git a/simpeg/survey.py b/simpeg/survey.py index 57b534ea6a..e471ab1b0c 100644 --- a/simpeg/survey.py +++ b/simpeg/survey.py @@ -290,7 +290,7 @@ class BaseSrc: Location of the source """ - def __init__(self, receiver_list=None, location=None, **kwargs): + def __init__(self, receiver_list=None, location=None): if receiver_list is None: receiver_list = [] self.receiver_list = receiver_list @@ -431,7 +431,7 @@ def __init__(self, source_list, counter=None, **kwargs): self.source_list = source_list if counter is not None: - self.counter = counter + self._counter = validate_type("counter", counter, Counter, cast=False) self._uid = uuid.uuid4() super().__init__(**kwargs) @@ -481,21 +481,6 @@ def uid(self): """ return self._uid - @property - def counter(self): - """A SimPEG counter object for counting iterations and operations - - Returns - ------- - simpeg.utils.counter_utils.Counter - A SimPEG counter object - """ - return self._counter - - @counter.setter - def counter(self, new_obj): - self._counter = validate_type("counter", new_obj, Counter, cast=False) - # TODO: this should be private def get_source_indices(self, sources): if not isinstance(sources, list): @@ -554,6 +539,92 @@ def _n_fields(self): """number of fields required for solution""" return sum(src._fields_per_source for src in self.source_list) + def get_slice(self, source, receiver): + """ + Get slice to index a flat array for a given source-receiver pair. + + Use this method to index a data or uncertainty array for + a source-receiver pair of this survey. + + Parameters + ---------- + source : .BaseSrc + Source object. + receiver : .BaseRx + Receiver object. + + Returns + ------- + slice + + Raises + ------ + KeyError + If the given ``source`` or ``receiver`` do not belong to this survey. + + See also + -------- + .get_all_slices + """ + # Create generator for source and receiver pairs + source_receiver_pairs = ( + (src, rx) for src in self.source_list for rx in src.receiver_list + ) + # Get the start and end offsets for the given source and receiver, and + # build the slice + src_rx_slice = None + end_offset = 0 + for src, rx in source_receiver_pairs: + start_offset = end_offset + end_offset += rx.nD + if src is source and rx is receiver: + src_rx_slice = slice(start_offset, end_offset) + break + # Raise error if the source-receiver pair is not in the survey + if src_rx_slice is None: + msg = ( + f"Source '{source}' and receiver '{receiver}' pair " + "is not part of the survey." + ) + raise KeyError(msg) + return src_rx_slice + + def get_all_slices(self): + """ + Get slices to index a flat array for all source-receiver pairs. + + .. warning:: + + Survey objects are mutable objects. If the sources or receivers in + it get modified, slices generated with this method will not match + the arrays linked to the modified survey. + + Returns + ------- + dict[tuple[.BaseSrc, .BaseRx], slice] + Dictionary with flat array slices for every pair of source and + receiver in the survey. The keys are tuples of a single source and + a single receiver, and the values are the corresponding slice for + each one of them. + + See also + -------- + .get_slice + """ + # Create generator for source and receiver pairs + source_receiver_pairs = ( + (src, rx) for src in self.source_list for rx in src.receiver_list + ) + # Get the start and end offsets for all source-receiver pairs, and + # build the slices. + slices = {} + end_offset = 0 + for src, rx in source_receiver_pairs: + start_offset = end_offset + end_offset += rx.nD + slices[(src, rx)] = slice(start_offset, end_offset) + return slices + class BaseTimeSurvey(BaseSurvey): """Base SimPEG survey class for time-dependent simulations.""" diff --git a/simpeg/typing/__init__.py b/simpeg/typing/__init__.py new file mode 100644 index 0000000000..9f68652973 --- /dev/null +++ b/simpeg/typing/__init__.py @@ -0,0 +1,75 @@ +""" +============================= +Typing (:mod:`simpeg.typing`) +============================= + +This module provides additional `PEP 484 `_ +type aliases used in ``simpeg``'s codebase. + +API +--- + +.. autosummary:: + :toctree: generated/ + + RandomSeed + MinimizeCallable + +""" + +import numpy as np +import numpy.typing as npt +from typing import Union, TypeAlias +from collections.abc import Callable +from scipy.sparse.linalg import LinearOperator + +RandomSeed: TypeAlias = Union[ + int, + npt.NDArray[np.int_], + np.random.SeedSequence, + np.random.BitGenerator, + np.random.Generator, +] +""" +A ``typing.Union`` for random seeds and Numpy's random number generators. + +These type of variables can be used throughout ``simpeg`` to control random +states of functions and classes. These variables can either be an integer that +will be used as a ``seed`` to define a Numpy's :class:`numpy.random.Generator`, or +a predefined random number generator. + +Examples +-------- + +>>> import numpy as np +>>> from simpeg.typing import RandomSeed +>>> +>>> def my_function(seed: RandomSeed = None): +... rng = np.random.default_rng(seed=seed) +... ... +""" + +MinimizeCallable: TypeAlias = Callable[ + [np.ndarray, bool, bool], + float + | tuple[float, np.ndarray | LinearOperator] + | tuple[float, np.ndarray, LinearOperator], +] +""" +The callable expected for the minimization operations. + +The function's signature should look like:: + + func(x: numpy.ndarray, return_g: bool, return_H: bool) + +It should output up to three values ordered as:: + + f_val : float + gradient : numpy.ndarray + H : LinearOperator + +`f_val` is always returned, `gradient` is returned if `return_g`, and `H_func` is returned if `return_H`. +`f_val` should always be the first value returned, `gradient` will always be the second, and `H_func` will +always be the last. If `return_g == return_H == False`, then only the single argument `f_val` is +returned. +""" diff --git a/simpeg/utils/__init__.py b/simpeg/utils/__init__.py index d5506d510c..d6ecd0393a 100644 --- a/simpeg/utils/__init__.py +++ b/simpeg/utils/__init__.py @@ -11,6 +11,17 @@ documentation for many details on items. +Logger +====== +Function to fetch the SimPEG logger. It can be used to stream messages to the logger, +and to temporarily adjust its configuration (e.g. change log level). + +.. autosummary:: + :toctree: generated/ + + get_logger + + Counter Utility Functions ========================= @@ -67,6 +78,8 @@ :toctree: generated/ surface2inds + get_discrete_topography + shift_to_discrete_topography Model Utility Functions @@ -76,6 +89,7 @@ :toctree: generated/ depth_weighting + distance_weighting model_builder.add_block model_builder.create_2_layer_model model_builder.create_block_in_wholespace @@ -141,10 +155,30 @@ validate_direction validate_active_indices +Solver utilities +---------------- +Functions to get and set the default solver meant to be used in PDE simulations. + +.. autosummary:: + :toctree: generated/ + + get_default_solver + set_default_solver + +Custom warnings +--------------- +List of custom warnings used in SimPEG. + +.. autosummary:: + :toctree: generated/ + + BreakingChangeWarning + PerformanceWarning """ from discretize.utils.interpolation_utils import interpolation_matrix +from .logger import get_logger from .code_utils import ( mem_profile_class, hook, @@ -210,6 +244,8 @@ closest_points_index, extract_core_mesh, surface2inds, + get_discrete_topography, + shift_to_discrete_topography, ) from .curv_utils import ( volume_tetrahedron, @@ -225,7 +261,7 @@ rotation_matrix_from_normals, rotate_points_from_normals, ) -from .model_utils import depth_weighting +from .model_utils import depth_weighting, distance_weighting from .plot_utils import plot2Ddata, plotLayer, plot_1d_layer_model from .io_utils import download from .pgi_utils import ( @@ -235,46 +271,5 @@ GaussianMixtureWithNonlinearRelationships, GaussianMixtureWithNonlinearRelationshipsWithPrior, ) - -# Deprecated imports -interpmat = deprecate_function( - interpolation_matrix, "interpmat", removal_version="0.19.0", error=True -) - -from .code_utils import ( - memProfileWrapper, - setKwargs, - printTitles, - printLine, - checkStoppers, - printStoppers, - printDone, - callHooks, - dependentProperty, - asArray_N_x_Dim, -) -from .mat_utils import ( - sdInv, - getSubArray, - inv3X3BlockDiagonal, - inv2X2BlockDiagonal, - makePropertyTensor, - invPropertyTensor, - diagEst, - uniqueRows, -) -from .mesh_utils import ( - meshTensor, - closestPoints, - ExtractCoreMesh, -) -from .curv_utils import ( - volTetra, - faceInfo, - indexCube, - exampleLrmGrid, -) -from .coord_utils import ( - rotatePointsFromNormals, - rotationMatrixFromNormals, -) +from .solver_utils import get_default_solver, set_default_solver +from .warnings import BreakingChangeWarning, PerformanceWarning diff --git a/simpeg/utils/code_utils.py b/simpeg/utils/code_utils.py index 8c2014b216..ceb8ac68e7 100644 --- a/simpeg/utils/code_utils.py +++ b/simpeg/utils/code_utils.py @@ -1,10 +1,11 @@ -from __future__ import annotations import types + import numpy as np from functools import wraps import warnings from discretize.utils import as_array_n_by_dim # noqa: F401 +from discretize.utils import requires as module_requires # scooby is a soft dependency for simpeg try: @@ -20,6 +21,12 @@ def __init__(self, additional, core, optional, ncol, text_width, sort): ) +try: + import memory_profiler +except ImportError: + memory_profiler = False + + def requires(var): """Wrap a function to require a specfic attribute. @@ -80,7 +87,7 @@ def requiresVarWrapper(self, *args, **kwargs): return requiresVar -@requires("memory_profiler") +@module_requires({"memory_profiler": memory_profiler}) def mem_profile_class(input_class, *args): """Creates a new class from the target class with memory profiled methods. @@ -250,9 +257,13 @@ def print_line(obj, printers, pad=""): """ values = "" for printer in printers: - values += ("{{:^{0:d}}}".format(printer["width"])).format( - printer["format"] % printer["value"](obj) - ) + value = printer["value"](obj) + format_string = f"^{printer['width']}s" + if value is not None: + formatted_val = printer["format"](value) + else: + formatted_val = "" + values += f"{formatted_val:{format_string}}" print(pad + values) @@ -309,12 +320,12 @@ def print_stoppers(obj, stoppers, pad="", stop="STOP!", done="DONE!"): done : str, default: "DONE!" String for statement when stopping criterian not encountered """ - print(pad + "{0!s}{1!s}{2!s}".format("-" * 25, stop, "-" * 25)) + print(pad + "-" * 25 + stop + "-" * 25) for stopper in stoppers: l = stopper["left"](obj) r = stopper["right"](obj) print(pad + stopper["str"] % (l <= r, l, r)) - print(pad + "{0!s}{1!s}{2!s}".format("-" * 25, done, "-" * 25)) + print(pad + "-" * 25 + done + "-" * 25) def call_hooks(match, mainFirst=False): @@ -367,17 +378,15 @@ def wrapper(self, *args, **kwargs): return out - extra = """ - If you have things that also need to run in the method {0!s}, you can create a method:: + extra = f""" + If you have things that also need to run in the method {match}, you can create a method:: - def _{1!s}*(self, ... ): + def _{match}*(self, ... ): pass - Where the * can be any string. If present, _{2!s}* will be called at the start of the default {3!s} call. + Where the * can be any string. If present, _{match}* will be called at the start of the default {match} call. You may also completely overwrite this function. - """.format( - match, match, match, match - ) + """ doc = wrapper.__doc__ wrapper.__doc__ = ("" if doc is None else doc) + extra return wrapper @@ -471,11 +480,9 @@ def __init__(self, add_pckg=None, ncol=3, text_width=80, sort=False): "pymatsolver", "numpy", "scipy", - "sklearn", "matplotlib", - "empymod", "geoana", - "pandas", + "libdlf", ] # Optional packages. @@ -484,6 +491,8 @@ def __init__(self, add_pckg=None, ncol=3, text_width=80, sort=False): "pydiso", "numba", "dask", + "sklearn", + "pandas", "sympy", "IPython", "ipywidgets", @@ -510,7 +519,11 @@ def __init__(self, add_pckg=None, ncol=3, text_width=80, sort=False): def deprecate_class( - removal_version=None, new_location=None, future_warn=False, error=False + removal_version=None, + new_location=None, + future_warn=False, + error=False, + replace_docstring=True, ): """Utility function to deprecate a class @@ -557,7 +570,8 @@ def __init__(self, *args, **kwargs): cls.__init__ = __init__ if new_location is not None: parent_name = f"{new_location}.{parent_name}" - cls.__doc__ = f""" This class has been deprecated, see `{parent_name}` for documentation""" + if replace_docstring: + cls.__doc__ = f""" This class has been deprecated, see `{parent_name}` for documentation""" return cls return decorator @@ -922,7 +936,9 @@ def validate_float( return var -def validate_list_of_types(property_name, var, class_type, ensure_unique=False): +def validate_list_of_types( + property_name, var, class_type, ensure_unique=False, min_n=0, max_n=None +): """Validate list of instances of a certain class Parameters @@ -935,6 +951,8 @@ def validate_list_of_types(property_name, var, class_type, ensure_unique=False): Class type(s) that are allowed in the list ensure_unique : bool, optional Checks if all items in the var are unique items. + min_n, max_n : int, optional + Minimum and maximum supported list length. Defaults accept any length list. Returns ------- @@ -948,8 +966,20 @@ def validate_list_of_types(property_name, var, class_type, ensure_unique=False): else: raise TypeError(f"{property_name!r} must be a list of {class_type}") - is_true = [isinstance(x, class_type) for x in var] - if np.all(is_true): + if max_n is not None: + if min_n == max_n and len(var) != max_n: + raise ValueError( + f"{property_name!r} must have exactly {min_n} item{'s' if min_n != 1 else ''}." + ) + elif len(var) > max_n: + raise ValueError( + f"{property_name!r} must have at most {max_n} item{'s' if max_n != 1 else ''}." + ) + if len(var) < min_n: + raise ValueError( + f"{property_name!r} must have at least {min_n} item{'s' if min_n != 1 else ''}." + ) + if all(isinstance(x, class_type) for x in var): if ensure_unique and len(set(var)) != len(var): raise ValueError( f"The {property_name!r} list must be unique. Cannot re-use items" @@ -1028,7 +1058,10 @@ def validate_ndarray_with_shape(property_name, var, shape=None, dtype=float): dtypes = dtype for dtype in dtypes: try: - var = np.asarray(var, dtype=dtype) + if isinstance(var, np.ndarray): + var = var.astype(dtype, casting="safe", copy=False) + else: + var = np.asarray(var, dtype=dtype) bad_type = False break except (TypeError, ValueError) as err: @@ -1102,21 +1135,42 @@ def validate_type(property_name, obj, obj_type, cast=True, strict=False): obj_type Returns the object in the specified type when validated """ + if not isinstance(obj_type, tuple): + obj_type = (obj_type,) + + if len(obj_type) > 1: + type_name = ( + ", ".join(cls.__qualname__ for cls in obj_type[:-1]) + + " or " + + obj_type[-1].__qualname__ + ) + else: + type_name = obj_type[0].__qualname__ + if cast: - try: - obj = obj_type(obj) - except Exception as err: + good_cast = False + err = None + for cls in obj_type: + try: + new_obj = cls(obj) + good_cast = True + except Exception as trial_error: + err = trial_error + if good_cast: + obj = new_obj + break + if not good_cast: raise TypeError( - f"{type(obj).__name__} cannot be converted to type {obj_type.__name__} " + f"{type(obj).__qualname__} cannot be converted to {type_name} " f"required for {property_name}." ) from err - if strict and type(obj) is not obj_type: + if strict and type(obj) not in obj_type: raise TypeError( - f"Object must be exactly a {obj_type.__name__} for {property_name}" + f"{property_name} must be exactly a {type_name}, not {type(obj).__qualname__}" ) if not isinstance(obj, obj_type): raise TypeError( - f"Object must be an instance of {obj_type.__name__} for {property_name}" + f"{property_name} must be an instance of {type_name}, not {type(obj).__qualname__}" ) return obj @@ -1227,38 +1281,3 @@ def validate_active_indices(property_name, index_arr, n_cells): if index_arr.shape != (n_cells,): raise ValueError(f"Input 'active_cells' must have shape {(n_cells,)}") return index_arr - - -############################################################### -# DEPRECATIONS -############################################################### -memProfileWrapper = deprecate_function( - mem_profile_class, "memProfileWrapper", removal_version="0.18.0", error=True -) -setKwargs = deprecate_function( - set_kwargs, "setKwargs", removal_version="0.18.0", error=True -) -printTitles = deprecate_function( - print_titles, "printTitles", removal_version="0.18.0", error=True -) -printLine = deprecate_function( - print_line, "printLine", removal_version="0.18.0", error=True -) -printStoppers = deprecate_function( - print_stoppers, "printStoppers", removal_version="0.18.0", error=True -) -checkStoppers = deprecate_function( - check_stoppers, "checkStoppers", removal_version="0.18.0", error=True -) -printDone = deprecate_function( - print_done, "printDone", removal_version="0.18.0", error=True -) -callHooks = deprecate_function( - call_hooks, "callHooks", removal_version="0.18.0", error=True -) -dependentProperty = deprecate_function( - dependent_property, "dependentProperty", removal_version="0.18.0", error=True -) -asArray_N_x_Dim = deprecate_function( - as_array_n_by_dim, "asArray_N_x_Dim", removal_version="0.19.0", error=True -) diff --git a/simpeg/utils/coord_utils.py b/simpeg/utils/coord_utils.py index e1d17c5dbf..84491b42f7 100644 --- a/simpeg/utils/coord_utils.py +++ b/simpeg/utils/coord_utils.py @@ -2,18 +2,3 @@ rotation_matrix_from_normals, rotate_points_from_normals, ) -from .code_utils import deprecate_function - -# deprecated functions -rotationMatrixFromNormals = deprecate_function( - rotation_matrix_from_normals, - "rotationMatrixFromNormals", - removal_version="0.19.0", - error=True, -) -rotatePointsFromNormals = deprecate_function( - rotate_points_from_normals, - "rotatePointsFromNormals", - removal_version="0.19.0", - error=True, -) diff --git a/simpeg/utils/curv_utils.py b/simpeg/utils/curv_utils.py index 71e764ce60..93b4e393ec 100644 --- a/simpeg/utils/curv_utils.py +++ b/simpeg/utils/curv_utils.py @@ -4,21 +4,3 @@ face_info, example_curvilinear_grid, ) -from .code_utils import deprecate_function - -# deprecated functions -volTetra = deprecate_function( - volume_tetrahedron, "volTetra", removal_version="0.19.0", error=True -) -indexCube = deprecate_function( - index_cube, "indexCube", removal_version="0.19.0", error=True -) -faceInfo = deprecate_function( - face_info, "faceInfo", removal_version="0.19.0", error=True -) -exampleLrmGrid = deprecate_function( - example_curvilinear_grid, - "exampleLrmGrid", - removal_version="0.19.0", - error=True, -) diff --git a/simpeg/utils/logger.py b/simpeg/utils/logger.py new file mode 100644 index 0000000000..16936abeb7 --- /dev/null +++ b/simpeg/utils/logger.py @@ -0,0 +1,38 @@ +""" +Define logger for SimPEG. +""" + +import logging + +__all__ = ["get_logger"] + + +def _create_logger(): + """ + Create logger for SimPEG. + """ + logger = logging.getLogger("SimPEG") + logger.setLevel(logging.INFO) + handler = logging.StreamHandler() + formatter = logging.Formatter("{levelname}: {message}", style="{") + handler.setFormatter(formatter) + logger.addHandler(handler) + return logger + + +LOGGER = _create_logger() + + +def get_logger(): + r""" + Get the default event logger. + + The logger records events and relevant information while setting up simulations and + inversions. By default the logger will stream to stderr and using the INFO level. + + Returns + ------- + logger : :class:`logging.Logger` + The logger object for SimPEG. + """ + return LOGGER diff --git a/simpeg/utils/mat_utils.py b/simpeg/utils/mat_utils.py index 3614a15c2f..67b5d83187 100644 --- a/simpeg/utils/mat_utils.py +++ b/simpeg/utils/mat_utils.py @@ -1,5 +1,5 @@ import numpy as np -from .code_utils import deprecate_function +from ..typing import RandomSeed from discretize.utils import ( # noqa: F401 Zero, Identity, @@ -121,15 +121,15 @@ def unique_rows(M): Indices to project from output array to input array """ - b = np.ascontiguousarray(M).view(np.dtype((np.void, M.dtype.itemsize * M.shape[1]))) - _, unqInd = np.unique(b, return_index=True) - _, invInd = np.unique(b, return_inverse=True) - unqM = M[unqInd] - return unqM, unqInd, invInd + return np.unique(M, return_index=True, return_inverse=True, axis=0) def eigenvalue_by_power_iteration( - combo_objfct, model, n_pw_iter=4, fields_list=None, seed=None + combo_objfct, + model, + n_pw_iter=4, + fields_list=None, + random_seed: RandomSeed | None = None, ): r"""Estimate largest eigenvalue in absolute value using power iteration. @@ -150,8 +150,10 @@ def eigenvalue_by_power_iteration( they will be evaluated within the function. If combo_objfct mixs data misfit and regularization terms, the list should contains simpeg.fields for the data misfit terms and None for the regularization term. - seed : int - Random seed for the initial random guess of eigenvector. + random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed for the initial random guess of eigenvector. It can either + be an int, a predefined Numpy random number generator, or any valid + input to ``numpy.random.default_rng``. Returns ------- @@ -164,24 +166,24 @@ def eigenvalue_by_power_iteration( approximated by the Rayleigh quotient: .. math:: + \lambda_k = \frac{\mathbf{x_k^T A x_k}}{\mathbf{x_k^T x_k}} - where :math:`\mathfb{A}` is our matrix and :math:`\mathfb{x_k}` is computed + where :math:`\mathbf{A}` is our matrix and :math:`\mathbf{x_k}` is computed recursively according to: .. math:: + \mathbf{x_{k+1}} = \frac{\mathbf{A x_k}}{\| \mathbf{Ax_k} \|} The elements of the initial vector :math:`\mathbf{x_0}` are randomly selected from a uniform distribution. """ - - if seed is not None: - np.random.seed(seed) + rng = np.random.default_rng(seed=random_seed) # Initial guess for eigen-vector - x0 = np.random.rand(*model.shape) + x0 = rng.random(size=model.shape) x0 = x0 / np.linalg.norm(x0) # transform to ComboObjectiveFunction if required @@ -352,9 +354,11 @@ def dip_azimuth2cartesian(dip, azm): Parameters ---------- dip : float or 1D numpy.ndarray - Dip angle in degrees. Values in range [0, 90] + Dip angle in degrees. Values in range [-90, 90]. Positive values correspond to + a vector pointing downwards (negative z component). azm : float or 1D numpy.ndarray - Asimuthal angle (strike) in degrees. Defined clockwise from Northing. Values is range [0, 360] + Azimuthal angle (strike) in degrees. Defined clockwise from Northing. + Values is range [0, 360] or [-180, 180]. Returns ------- @@ -362,6 +366,42 @@ def dip_azimuth2cartesian(dip, azm): Numpy array whose columns represent the x, y and z components of the vector(s) in Cartesian coordinates + Examples + -------- + >>> vector = dip_azimuth2cartesian(0, 45) + >>> x, y, z = vector[0].tolist() + >>> x, y, z # doctest: +NUMBER + (0.707, 0.707, 0.0) + + >>> vector = dip_azimuth2cartesian(0, -45) + >>> x, y, z = vector[0].tolist() + >>> x, y, z # doctest: +NUMBER + (-0.707, 0.707, 0.0) + + >>> vector = dip_azimuth2cartesian(60, 0) + >>> x, y, z = vector[0].tolist() + >>> x, y, z # doctest: +NUMBER + (0.0, 0.5, -0.866) + + >>> vector = dip_azimuth2cartesian(-30, 0) + >>> x, y, z = vector[0].tolist() + >>> x, y, z # doctest: +NUMBER + (0.0, 0.866, 0.5) + + >>> vector = dip_azimuth2cartesian(90, 0) + >>> x, y, z = vector[0].tolist() + >>> x, y, z # doctest: +NUMBER + (0.0, 0.0, -1.0) + + >>> vector = dip_azimuth2cartesian(-90, 0) + >>> x, y, z = vector[0].tolist() + >>> x, y, z # doctest: +NUMBER + (0.0, 0.0, 1.0) + + >>> vector = dip_azimuth2cartesian(30, 60) + >>> x, y, z = vector[0].tolist() + >>> x, y, z # doctest: +NUMBER + (0.75, 0.433, -0.5) """ azm = np.asarray(azm) @@ -443,44 +483,3 @@ def define_plane_from_points(xyz1, xyz2, xyz3): d = -(a * xyz1[0] + b * xyz1[1] + c * xyz1[2]) return a, b, c, d - - -################################################ -# DEPRECATED FUNCTIONS -################################################ - - -diagEst = deprecate_function( - estimate_diagonal, "diagEst", removal_version="0.19.0", error=True -) -uniqueRows = deprecate_function( - unique_rows, "uniqueRows", removal_version="0.19.0", error=True -) -sdInv = deprecate_function(sdinv, "sdInv", removal_version="0.19.0", error=True) -getSubArray = deprecate_function( - get_subarray, "getSubArray", removal_version="0.19.0", error=True -) -inv3X3BlockDiagonal = deprecate_function( - inverse_3x3_block_diagonal, - "inv3X3BlockDiagonal", - removal_version="0.19.0", - error=True, -) -inv2X2BlockDiagonal = deprecate_function( - inverse_2x2_block_diagonal, - "inv2X2BlockDiagonal", - removal_version="0.19.0", - error=True, -) -makePropertyTensor = deprecate_function( - make_property_tensor, - "makePropertyTensor", - removal_version="0.19.0", - error=True, -) -invPropertyTensor = deprecate_function( - inverse_property_tensor, - "invPropertyTensor", - removal_version="0.19.0", - error=True, -) diff --git a/simpeg/utils/mesh_utils.py b/simpeg/utils/mesh_utils.py index 1fc3a8d580..42a85bde14 100644 --- a/simpeg/utils/mesh_utils.py +++ b/simpeg/utils/mesh_utils.py @@ -1,24 +1,27 @@ import numpy as np -from .code_utils import deprecate_function +from scipy.spatial import cKDTree + +from discretize import TensorMesh from discretize.utils import ( # noqa: F401 unpack_widths, closest_points_index, extract_core_mesh, + active_from_xyz, ) def surface2inds(vrtx, trgl, mesh, boundaries=True, internal=True): - """Takes a triangulated surface and determine which mesh cells it intersects. + """Takes a triangulated surface and determines which mesh cells it intersects. Parameters ---------- vrtx : (n_nodes, 3) numpy.ndarray of float The location of the vertices of the triangles trgl : (n_triang, 3) numpy.ndarray of int - Each row describes the 3 indices into the `vrtx` array that make up a triangle's - vertices. - mesh : discretize.TensorMesh + Each row describes the 3 indices into the `vrtx` array that make up a + triangle's vertices. + mesh : TensorMesh boundaries : bool, optional internal : bool, optional @@ -97,15 +100,181 @@ def surface2inds(vrtx, trgl, mesh, boundaries=True, internal=True): return insideGrid -################################################ -# DEPRECATED FUNCTIONS -################################################ -meshTensor = deprecate_function( - unpack_widths, "meshTensor", removal_version="0.19.0", error=True -) -closestPoints = deprecate_function( - closest_points_index, "closestPoints", removal_version="0.19.0", error=True -) -ExtractCoreMesh = deprecate_function( - extract_core_mesh, "ExtractCoreMesh", removal_version="0.19.0", error=True -) +def _closest_grid_indices(grid, pts): + """Return indices of closest gridded points for a set of input points. + + Parameters + ---------- + grid : (n, dim) numpy.ndarray + A gridded set of points. + pts : (m, dim) numpy.ndarray + Points being projected to gridded locations. + + Returns + ------- + (n,) numpy.ndarray + Indices of the closest gridded points for all *pts* supplied. + """ + if grid.squeeze().ndim == 1: + grid_inds = np.asarray( + [np.abs(pt - grid).argmin() for pt in pts.tolist()], dtype=int + ) + else: + tree = cKDTree(grid) + _, grid_inds = tree.query(pts) + + return grid_inds + + +def get_discrete_topography(mesh, active_cells, topo_cell_cutoff="top"): + """ + Generate discrete topography locations from mesh and active cells. + + Parameters + ---------- + mesh : TensorMesh or discretize.TreeMesh + A tensor or tree mesh. + active_cells : numpy.ndarray of bool or int + Active cells index; i.e. indices of cells below surface + topo_cell_cutoff : {"top", "center"} + String to specify the cutoff for ground cells and the locations of + the discrete topography. For "top", ground cells lie entirely below + the surface topography and the discrete topography is defined on the + top faces of surface cells. For "center", only the cell centers must + lie below the surface topography and the discrete topography is defined + at the centers of surface cells. + + Returns + ------- + (n, dim) numpy.ndarray + Discrete topography locations; i.e. xy[z]. + """ + if mesh._meshType == "TENSOR": + if mesh.dim == 3: + mesh2D = TensorMesh([mesh.h[0], mesh.h[1]], mesh.x0[:2]) + zc = mesh.cell_centers[:, 2] + ACTIND = active_cells.reshape( + (mesh.vnC[0] * mesh.vnC[1], mesh.vnC[2]), order="F" + ) + ZC = zc.reshape((mesh.vnC[0] * mesh.vnC[1], mesh.vnC[2]), order="F") + topoCC = np.zeros(ZC.shape[0]) + + for i in range(ZC.shape[0]): + if topo_cell_cutoff == "top": + ind = np.argmax(ZC[i, :][ACTIND[i, :]]) + dz = mesh.h[2][ACTIND[i, :]][ind] * 0.5 + elif topo_cell_cutoff == "center": + dz = 0.0 + else: + raise ValueError("'topo_cell_cutoff' must be 'top' or 'center'.") + topoCC[i] = ZC[i, :][ACTIND[i, :]].max() + dz + return np.c_[mesh2D.cell_centers, topoCC] + + elif mesh.dim == 2: + mesh1D = TensorMesh([mesh.h[0]], [mesh.x0[0]]) + yc = mesh.cell_centers[:, 1] + ACTIND = active_cells.reshape((mesh.vnC[0], mesh.vnC[1]), order="F") + YC = yc.reshape((mesh.vnC[0], mesh.vnC[1]), order="F") + topoCC = np.zeros(YC.shape[0]) + for i in range(YC.shape[0]): + ind = np.argmax(YC[i, :][ACTIND[i, :]]) + if topo_cell_cutoff == "top": + dy = mesh.h[1][ACTIND[i, :]][ind] * 0.5 + elif topo_cell_cutoff == "center": + dy = 0.0 + else: + raise ValueError("'topo_cell_cutoff' must be 'top' or 'center'.") + topoCC[i] = YC[i, :][ACTIND[i, :]].max() + dy + return np.c_[mesh1D.cell_centers, topoCC] + + elif mesh._meshType == "TREE": + inds = mesh.get_boundary_cells(active_cells, direction="zu")[0] + + if topo_cell_cutoff == "top": + dz = mesh.h_gridded[inds, -1] * 0.5 + elif topo_cell_cutoff == "center": + dz = 0.0 + return np.c_[mesh.cell_centers[inds, :-1], mesh.cell_centers[inds, -1] + dz] + else: + raise NotImplementedError(f"{type(mesh)} mesh is not supported.") + + +def shift_to_discrete_topography( + mesh, + pts, + active_cells, + topo_cell_cutoff="top", + shift_horizontal=True, + heights=0.0, +): + """ + Shift locations relative to discrete surface topography. + + Parameters + ---------- + mesh : discretize.TensorMesh or discretize.TreeMesh + The mesh (2D or 3D) defining the discrete domain. + pts : (n, dim) numpy.ndarray + The original set of points being shifted relative to the discretize + surface topography. + active_cells : numpy.ndarray of int or bool, optional + Index array for all cells lying below the surface topography. + topo_cell_cutoff : {"top", "center"} + String to specify the cutoff for ground cells and the locations of the discrete + topography. For "top", ground cells lie entirely below the surface topography + and the discrete topography is defined on the top faces of surface cells. + For "center", only the cell centers must lie below the surface topography and + the discrete topography is defined at the centers of surface cells. + The topography is defined using the 'topo' input parameter. + heights : float or (n,) numpy.ndarray, optional + Height(s) relative to the true surface topography. Used to preserve flight + heights or borehole depths. + shift_horizontal : bool, optional + When True, locations are shifted horizontally to lie vertically over cell + centers. When False, the original horizontal locations are preserved. + + Returns + ------- + (n, dim) numpy.ndarray + The set of points shifted relative to the discretize surface topography. + """ + if mesh._meshType != "TENSOR" and mesh._meshType != "TREE": + raise NotImplementedError( + "shift_to_discrete_topography only supported for TensorMesh and TreeMesh'." + ) + + if not isinstance(heights, (int, float)) and len(pts) != len(heights): + raise ValueError( + ( + "If supplied as a `numpy.ndarray`, the number of heights must ", + "equal the number of points.", + ) + ) + + discrete_topography = get_discrete_topography( + mesh, active_cells, topo_cell_cutoff=topo_cell_cutoff + ) + + if pts.ndim == 2 and pts.shape[1] == mesh.dim: + has_elevation = True + horizontal_pts = pts[:, :-1] + else: + has_elevation = False + horizontal_pts = pts.squeeze() # in case (n, 1) array + + topo_inds = _closest_grid_indices(discrete_topography[:, :-1], horizontal_pts) + + if shift_horizontal: + if has_elevation: + cell_inds = mesh.point2index(pts) + out = np.c_[ + mesh.cell_centers[cell_inds, :-1], discrete_topography[topo_inds, -1] + ] + else: + out = discrete_topography[topo_inds, :] + else: + out = np.c_[horizontal_pts, discrete_topography[topo_inds, -1]] + + out[:, -1] += heights + + return out diff --git a/simpeg/utils/model_builder.py b/simpeg/utils/model_builder.py index 285fa976c1..0e31cfdc99 100644 --- a/simpeg/utils/model_builder.py +++ b/simpeg/utils/model_builder.py @@ -1,3 +1,4 @@ +import warnings import numpy as np import scipy.ndimage as ndi import scipy.sparse as sp @@ -5,6 +6,9 @@ from scipy.spatial import Delaunay from discretize.base import BaseMesh +from ..typing import RandomSeed +from ..utils.warnings import BreakingChangeWarning + def add_block(cell_centers, model, p0, p1, prop_value): """Add a homogeneous block to an existing cell centered model @@ -50,7 +54,7 @@ def get_indices_block(p0, p1, cell_centers): Returns ------- - tuple of int + array of int Indices of the cells whose center lie within the specified block """ @@ -59,51 +63,89 @@ def get_indices_block(p0, p1, cell_centers): cell_centers = cell_centers.cell_centers # Validation: p0 and p1 live in the same dimensional space - assert len(p0) == len(p1), "Dimension mismatch. len(p0) != len(p1)" + if len(p0) != len(p1): + msg = ( + "Dimension mismatch between `p0` and `p1`. " + f"`p0` has {len(p0)} elements, while " + f"`p1` has {len(p0)} elements. " + "They should have the same amount of elements." + ) + raise ValueError(msg) # Validation: mesh and points live in the same dimensional space - dimMesh = np.size(cell_centers[0, :]) - assert len(p0) == dimMesh, "Dimension mismatch. len(p0) != dimMesh" + mesh_ndim = len(p0) + if cell_centers.size % mesh_ndim != 0: + msg = ( + "Dimension mismatch between `cell_centers` and dimensions of block " + "corners. " + f"The `cell_centers` have {cell_centers.size} elements that don't match " + f"the expected number of dimensions of the block corners ({len(p0)})." + ) + raise ValueError(msg) - for ii in range(len(p0)): - p0[ii], p1[ii] = np.min([p0[ii], p1[ii]]), np.max([p0[ii], p1[ii]]) + # Redefine the block corners to ensure they are correctly defined as min and max + p0_new = [min(p0[i], p1[i]) for i in range(mesh_ndim)] + p1_new = [max(p0[i], p1[i]) for i in range(mesh_ndim)] - if dimMesh == 1: + if mesh_ndim == 1: # Define the reference points - x1 = p0[0] - x2 = p1[0] + (x1,) = p0_new + (x2,) = p1_new - indX = (x1 <= cell_centers[:, 0]) & (cell_centers[:, 0] <= x2) - ind = np.where(indX) + x_centers = cell_centers + indX = (x1 <= x_centers) & (x_centers <= x2) + (ind,) = np.where(indX) - elif dimMesh == 2: + elif mesh_ndim == 2: # Define the reference points - x1 = p0[0] - y1 = p0[1] + x1, y1 = p0_new + x2, y2 = p1_new - x2 = p1[0] - y2 = p1[1] + x_centers = cell_centers[:, 0] + y_centers = cell_centers[:, 1] + indX = (x1 <= x_centers) & (x_centers <= x2) + indY = (y1 <= y_centers) & (y_centers <= y2) - indX = (x1 <= cell_centers[:, 0]) & (cell_centers[:, 0] <= x2) - indY = (y1 <= cell_centers[:, 1]) & (cell_centers[:, 1] <= y2) + (ind,) = np.where(indX & indY) - ind = np.where(indX & indY) - - elif dimMesh == 3: + elif mesh_ndim == 3: # Define the points - x1 = p0[0] - y1 = p0[1] - z1 = p0[2] - - x2 = p1[0] - y2 = p1[1] - z2 = p1[2] - - indX = (x1 <= cell_centers[:, 0]) & (cell_centers[:, 0] <= x2) - indY = (y1 <= cell_centers[:, 1]) & (cell_centers[:, 1] <= y2) - indZ = (z1 <= cell_centers[:, 2]) & (cell_centers[:, 2] <= z2) - - ind = np.where(indX & indY & indZ) + x1, y1, z1 = p0_new + x2, y2, z2 = p1_new + + x_centers = cell_centers[:, 0] + y_centers = cell_centers[:, 1] + z_centers = cell_centers[:, 2] + indX = (x1 <= x_centers) & (x_centers <= x2) + indY = (y1 <= y_centers) & (y_centers <= y2) + indZ = (z1 <= z_centers) & (z_centers <= z2) + + (ind,) = np.where(indX & indY & indZ) + + # Warn users about the breaking change introduced in the return of this function + msg = ( + "Since SimPEG v0.25.0, the 'get_indices_block' function returns a single array " + "with the cell indices, instead of a tuple with a single element. " + "This means that we don't need to unpack the tuple anymore to access to the " + "cell indices." + "\n" + "If you were using this function as in:" + "\n\n" + " ind = get_indices_block(p0, p1, mesh.cell_centers)[0]" + "\n\n" + "Make sure you update it to:" + "\n\n" + " ind = get_indices_block(p0, p1, mesh.cell_centers)" + "\n\n" + "To hide this warning, add this to your script or notebook:" + "\n" + "\n import warnings" + "\n from simpeg.utils import BreakingChangeWarning" + "\n" + "\n warnings.filterwarnings(action='ignore', category=BreakingChangeWarning)" + "\n" + ) + warnings.warn(msg, BreakingChangeWarning, stacklevel=2) # Return a tuple return ind @@ -218,7 +260,7 @@ def get_indices_sphere(center, radius, cell_centers): Returns ------- - tuple of int + array of int Indices of the cells whose center lie within the specified sphere """ @@ -414,15 +456,26 @@ def create_layers_model(cell_centers, layer_tops, layer_values): return model -def create_random_model(shape, seed=1000, anisotropy=None, its=100, bounds=None): - """Create random model by convolving a kernel with a uniformly distributed random model. +def create_random_model( + shape, + random_seed: RandomSeed | None = 1000, + anisotropy=None, + its=100, + bounds=None, + **kwargs, +): + """ + Create random model by convolving a kernel with a uniformly distributed random model. Parameters ---------- shape : int or tuple of int - Shape of the model. Can define a vector of size (n_cells) or define the dimensions of a tensor - seed : int, optional - If not None, sets the seed for the random uniform model that is convolved with the kernel. + Shape of the model. Can define a vector of size (n_cells) or define the + dimensions of a tensor. + random_seed : None or :class:`~simpeg.typing.RandomSeed`, optional + Random seed for random uniform model that is convolved with the kernel. + It can either be an int, a predefined Numpy random number generator, or + any valid input to ``numpy.random.default_rng``. anisotropy : numpy.ndarray this is the (*3*, *n*) blurring kernel that is used. its : int @@ -447,17 +500,19 @@ def create_random_model(shape, seed=1000, anisotropy=None, its=100, bounds=None) >>> plt.show() """ + if kwargs: + args = ", ".join([f"'{key}'" for key in kwargs]) + raise TypeError(f"Invalid arguments {args}.") + # --- + if bounds is None: bounds = [0, 1] - if seed is not None: - np.random.seed(seed) - print("Using a seed of: ", seed) - - if isinstance(shape, (int, float)): + if isinstance(shape, int): shape = (shape,) # make it a tuple for consistency - mr = np.random.rand(*shape) + rng = np.random.default_rng(seed=random_seed) + mr = rng.random(size=shape) if anisotropy is None: if len(shape) == 1: smth = np.array([1, 10.0, 1], dtype=float) diff --git a/simpeg/utils/model_utils.py b/simpeg/utils/model_utils.py index 91df15da71..dc48a0e17c 100644 --- a/simpeg/utils/model_utils.py +++ b/simpeg/utils/model_utils.py @@ -1,8 +1,27 @@ -from .mat_utils import mkvc +import warnings +from typing import Literal, Optional + +import discretize import numpy as np +import scipy.sparse as sp from scipy.interpolate import griddata from scipy.spatial import cKDTree -import scipy.sparse as sp +from scipy.spatial.distance import cdist + +from .mat_utils import mkvc + +try: + import numba + from numba import njit, prange +except ImportError: + numba = None + + # Define dummy njit decorator + def njit(*args, **kwargs): + return lambda f: f + + # Define dummy prange function + prange = range def surface_layer_index(mesh, topo, index=0): @@ -150,3 +169,174 @@ def depth_weighting( wz = wz[active_cells] return wz / np.nanmax(wz) + + +@njit(parallel=True) +def _distance_weighting_numba( + cell_centers: np.ndarray, + reference_locs: np.ndarray, + threshold: float, + exponent: float = 2.0, +) -> np.ndarray: + r""" + distance weighting kernel in numba. + + If numba is not installed, this will work as a regular for loop. + + Parameters + ---------- + cell_centers : np.ndarray + cell centers of the mesh. + reference_locs : (n, ndim) numpy.ndarray + The coordinate of the reference location, usually the receiver locations, + for the distance weighting. + A 2d array, with multiple reference locations, where each row should + contain the coordinates of a single location point in the following + order: _x_, _y_, _z_ (for 3D meshes) or _x_, _z_ (for 2D meshes). + threshold : float + Threshold parameters used in the distance weighting. + exponent : float, optional + Exponent parameter for distance weighting. + The exponent should match the natural decay power of the potential + field. For example, for gravity acceleration, set it to 2; for magnetic + fields, to 3. + + Returns + ------- + (n_active) numpy.ndarray + Normalized distance weights for the mesh at every active cell as + a 1d-array. + """ + n_active_cells = cell_centers.shape[0] + n_reference_locs = len(reference_locs) + + distance_weights = np.zeros(n_active_cells) + for j in prange(n_active_cells): + cell_center = cell_centers[j] + for i in range(n_reference_locs): + reference_loc = reference_locs[i] + distance = np.sqrt(((cell_center - reference_loc) ** 2).sum()) + distance_weights[j] += (distance + threshold) ** (-2 * exponent) + + distance_weights = np.sqrt(distance_weights) + distance_weights /= np.nanmax(distance_weights) + return distance_weights + + +def distance_weighting( + mesh: discretize.base.BaseMesh, + reference_locs: np.ndarray, + active_cells: Optional[np.ndarray] = None, + exponent: Optional[float] = 2.0, + threshold: Optional[float] = None, + engine: Literal["numba", "scipy"] = "numba", + cdist_opts: Optional[dict] = None, +): + r""" + Construct diagonal elements of a distance weighting matrix + + Builds the model weights following the distance weighting strategy, a method + to generate weights based on the distance between mesh cell centers and some + reference location(s). + Use these weights in regularizations to counteract the natural decay of + potential field data with distance. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + Discretized model space. + reference_locs : (n, ndim) numpy.ndarray + The coordinate of the reference location, usually the receiver locations, + for the distance weighting. + A 2d array, with multiple reference locations, where each row should + contain the coordinates of a single location point in the following + order: _x_, _y_, _z_ (for 3D meshes) or _x_, _z_ (for 2D meshes). + active_cells : (mesh.n_cells) numpy.ndarray of bool, optional + Index vector for the active cells on the mesh. + If ``None``, every cell will be assumed to be active. + exponent : float or None, optional + Exponent parameter for distance weighting. + The exponent should match the natural decay power of the potential + field. For example, for gravity acceleration, set it to 2; for magnetic + fields, to 3. + threshold : float or None, optional + Threshold parameters used in the distance weighting. + If ``None``, it will be set to half of the smallest cell width. + engine: str, 'numba' or 'scipy' + Pick between a ``scipy.spatial.distance.cdist`` computation (memory + intensive) or `for` loop implementation, parallelized with numba if + available. Default to ``"numba"``. + cdist_opts: dict, optional + Only valid with ``engine=="scipy"``. Options to pass to + ``scipy.spatial.distance.cdist``. Default to None. + + Returns + ------- + (n_active) numpy.ndarray + Normalized distance weights for the mesh at every active cell as + a 1d-array. + """ + + active_cells = ( + np.ones(mesh.n_cells, dtype=bool) if active_cells is None else active_cells + ) + + # Default threshold value + if threshold is None: + threshold = 0.5 * mesh.h_gridded.min() + + reference_locs = np.atleast_2d(reference_locs) + cell_centers = mesh.cell_centers[active_cells] + + # address 1D case + if mesh.dim == 1: + cell_centers = cell_centers.reshape(-1, 1) + reference_locs = reference_locs.reshape(-1, 1) + + if reference_locs.shape[1] != mesh.dim: + raise ValueError( + f"Invalid 'reference_locs' with shape '{reference_locs.shape}'. " + "The number of columns of the reference_locs array should match " + f"the dimensions of the mesh ({mesh.dim})." + ) + + if engine == "numba" and cdist_opts is not None: + raise TypeError( + "The `cdist_opts` is valid only when engine is 'scipy'." + "The current engine is 'numba'." + ) + + if engine == "numba": + if numba is None: + warnings.warn( + "Numba is not installed. Distance computations will be slower.", + stacklevel=2, + ) + distance_weights = _distance_weighting_numba( + cell_centers, + reference_locs, + exponent=exponent, + threshold=threshold, + ) + + elif engine == "scipy": + warnings.warn( + "``scipy.spatial.distance.cdist`` computations can be memory intensive. " + "Consider switching to `engine='numba'` " + "if you run into memory overflow issues.", + stacklevel=2, + ) + cdist_opts = cdist_opts or dict() + distance = cdist(cell_centers, reference_locs, **cdist_opts) + + distance_weights = (((distance + threshold) ** exponent) ** -2).sum(axis=1) + + distance_weights = distance_weights**0.5 + distance_weights /= np.nanmax(distance_weights) + + else: + raise ValueError( + f"Invalid engine '{engine}'. Engine should be either 'scipy' or 'numba'." + ) + + return distance_weights diff --git a/simpeg/utils/pgi_utils.py b/simpeg/utils/pgi_utils.py index a11fe9fb56..9fa0d02bef 100644 --- a/simpeg/utils/pgi_utils.py +++ b/simpeg/utils/pgi_utils.py @@ -3,35 +3,53 @@ from mpl_toolkits.axes_grid1.inset_locator import inset_axes from scipy import linalg from scipy.special import logsumexp -from sklearn.mixture import GaussianMixture -from sklearn.cluster import KMeans -from sklearn.utils import check_array -from sklearn.utils.validation import check_is_fitted -from sklearn.mixture._gaussian_mixture import ( - _compute_precision_cholesky, - _compute_log_det_cholesky, - _estimate_gaussian_covariances_full, - _estimate_gaussian_covariances_diag, - _estimate_gaussian_covariances_spherical, - _check_means, - _check_precisions, - _check_shape, -) -from sklearn.mixture._base import check_random_state, ConvergenceWarning import warnings from simpeg.maps import IdentityMap +from discretize.utils.code_utils import requires + +# sklearn is a soft dependency +try: + import sklearn + from sklearn.mixture import GaussianMixture + from sklearn.cluster import KMeans + from sklearn.utils import check_array + from sklearn.utils.validation import check_is_fitted + from sklearn.mixture._gaussian_mixture import ( + _compute_precision_cholesky, + _compute_log_det_cholesky, + _estimate_gaussian_covariances_full, + _estimate_gaussian_covariances_diag, + _estimate_gaussian_covariances_spherical, + _check_means, + _check_precisions, + _check_shape, + ) + from sklearn.mixture._base import check_random_state, ConvergenceWarning + +except ImportError: + GaussianMixture = None + sklearn = False +else: + # Try to import `validate_data` (added in sklearn 1.6). + # We should remove these bits when we set sklearn>=1.6 as the minimum version, and + # just import `validate_data`. + try: + from sklearn.utils.validation import validate_data + except ImportError: + validate_data = None + ############################################################################### # Disclaimer: the following classes built upon the GaussianMixture class # -# from Scikit-Learn. New functionalitie are added, as well as modifications to# -# existing functions, to serve the purposes pursued within SimPEG. # +# from Scikit-Learn. New functionalities are added, as well as modifications # +# to existing functions, to serve the purposes pursued within SimPEG. # # This use is allowed by the Scikit-Learn licensing (BSD-3-Clause License) # -# and we are grateful for their contributions to the open-source community. # # +# and we are grateful for their contributions to the open-source community. # ############################################################################### -class WeightedGaussianMixture(GaussianMixture): +class WeightedGaussianMixture(GaussianMixture if sklearn else object): """ Weighted Gaussian mixture class @@ -65,6 +83,7 @@ class WeightedGaussianMixture(GaussianMixture): Active indexes """ + @requires({"sklearn": sklearn}) def __init__( self, n_components, @@ -158,13 +177,13 @@ def compute_clusters_covariances(self): def order_clusters_GM_weight(self, outputindex=False): """Order clusters by decreasing weights - PARAMETERS + Parameters ---------- outputindex : bool, default: ``True`` If ``True``, return the sorting index - RETURN - ------ + Returns + ------- np.ndarray Sorting index """ @@ -194,18 +213,21 @@ def _check_weights(self, weights, n_components, n_samples): """ [modified from Scikit-Learn.mixture.gaussian_mixture] Check the user provided 'weights'. + Parameters ---------- weights : array-like, shape (n_components,) or (n_samples, n_components) The proportions of components of each mixture. n_components : int Number of components. + n_samples : int or None + Number of samples. Returns ------- - weights : array, shape (n_components,) + weights : (n_components,) or (n_samples, n_components) numpy.ndarray """ - + weights = np.asarray(weights) if len(weights.shape) == 2: weights = check_array( weights, dtype=[np.float64, np.float32], ensure_2d=True @@ -218,7 +240,7 @@ def _check_weights(self, weights, n_components, n_samples): _check_shape(weights, (n_components,), "weights") # check range - if any(np.less(weights, 0.0)) or any(np.greater(weights, 1.0)): + if (weights < 0.0).any() or (weights > 1.0).any(): raise ValueError( "The parameter 'weights' should be in the range " "[0, 1], but got max value %.5f, min value %.5f" @@ -271,6 +293,7 @@ def _initialize_parameters(self, X, random_state): """ [modified from Scikit-Learn.mixture._base] Initialize the model parameters. + Parameters ---------- X : array-like, shape (n_samples, n_features) @@ -303,6 +326,7 @@ def _m_step(self, X, log_resp): """ [modified from Scikit-Learn.mixture.gaussian_mixture] M step. + Parameters ---------- X : array-like, shape (n_samples, n_features) @@ -327,6 +351,7 @@ def _estimate_gaussian_covariances_tied(self, resp, X, nk, means, reg_covar): """ [modified from Scikit-Learn.mixture.gaussian_mixture] Estimate the tied covariance matrix. + Parameters ---------- resp : array-like, shape (n_samples, n_components) @@ -334,6 +359,7 @@ def _estimate_gaussian_covariances_tied(self, resp, X, nk, means, reg_covar): nk : array-like, shape (n_components,) means : array-like, shape (n_components, n_features) reg_covar : float + Returns ------- covariance : array, shape (n_features, n_features) @@ -350,6 +376,7 @@ def _estimate_gaussian_parameters(self, X, mesh, resp, reg_covar, covariance_typ """ [modified from Scikit-Learn.mixture.gaussian_mixture] Estimate the Gaussian distribution parameters. + Parameters ---------- X : array-like, shape (n_samples, n_features) @@ -360,6 +387,7 @@ def _estimate_gaussian_parameters(self, X, mesh, resp, reg_covar, covariance_typ The regularization added to the diagonal of the covariance matrices. covariance_type : {'full', 'tied', 'diag', 'spherical'} The type of precision matrices. + Returns ------- nk : array-like, shape (n_components,) @@ -385,9 +413,11 @@ def _e_step(self, X): """ [modified from Scikit-Learn.mixture.gaussian_mixture] E step. + Parameters ---------- X : array-like, shape (n_samples, n_features) + Returns ------- log_prob_norm : float @@ -426,6 +456,7 @@ def _estimate_log_gaussian_prob_with_sensW( """ [New function, modified from Scikit-Learn.mixture.gaussian_mixture._estimate_log_gaussian_prob] Estimate the log Gaussian probability with depth or sensitivity weighting. + Parameters ---------- X : array-like, shape (n_samples, n_features) @@ -438,6 +469,7 @@ def _estimate_log_gaussian_prob_with_sensW( 'diag' : shape of (n_components, n_features) 'spherical' : shape of (n_components,) covariance_type : {'full', 'tied', 'diag', 'spherical'} + Returns ------- log_prob : array, shape (n_samples, n_components) @@ -484,9 +516,11 @@ def _estimate_weighted_log_prob_with_sensW(self, X, sensW): """ [New function, modified from Scikit-Learn.mixture.gaussian_mixture._estimate_weighted_log_prob] Estimate the weighted log-probabilities, log P(X | Z) + log weights. + Parameters ---------- X : array-like, shape (n_samples, n_features) + Returns ------- weighted_log_prob : array, shape (n_samples, n_component) @@ -515,7 +549,13 @@ def score_samples_with_sensW(self, X, sensW): Log probabilities of each data point in X. """ check_is_fitted(self) - X = self._validate_data(X, reset=False) + # TODO: Ditch self._validate_data when setting sklearn>=1.6 as the minimum + # required version. + X = ( + validate_data(self, X, reset=False) + if validate_data is not None + else self._validate_data(X, reset=False) + ) return logsumexp(self._estimate_weighted_log_prob_with_sensW(X, sensW), axis=1) @@ -828,6 +868,7 @@ class GaussianMixtureWithPrior(WeightedGaussianMixture): Shape is (index of the fixed cell, lithology index) fixed_membership: """ + @requires({"sklearn": sklearn}) def __init__( self, gmmref, @@ -1099,7 +1140,15 @@ def fit_predict(self, X, y=None, debug=False): if self.verbose: print("modified from scikit-learn") - X = self._validate_data(X, dtype=[np.float64, np.float32], ensure_min_samples=2) + # TODO: Ditch self._validate_data when setting sklearn>=1.6 as the minimum + # required version. + X = ( + validate_data(self, X, dtype=[np.float64, np.float32], ensure_min_samples=2) + if validate_data is not None + else self._validate_data( + self, X, dtype=[np.float64, np.float32], ensure_min_samples=2 + ) + ) if X.shape[0] < self.n_components: raise ValueError( "Expected n_samples >= n_components " @@ -1157,7 +1206,7 @@ def fit_predict(self, X, y=None, debug=False): self.converged_ = True break - self._print_verbose_msg_init_end(lower_bound) + self._custom_print_verbose_msg_init_end(lower_bound) if lower_bound > max_lower_bound or max_lower_bound == -np.inf: max_lower_bound = lower_bound @@ -1180,6 +1229,22 @@ def fit_predict(self, X, y=None, debug=False): return self + def _custom_print_verbose_msg_init_end(self, ll): + """ + Wrapper for the upstream _print_verbose_msg_init_end + + This method was created to provide support of older versions + (scikit-learn<1.5.0) of this private method. + """ + try: + self._print_verbose_msg_init_end(ll, init_has_converged=True) + except TypeError as exception: + # In scikit-learn<1.5.0, the method has a single argument + match = "got an unexpected keyword argument 'init_has_converged'" + if match not in str(exception): + raise + self._print_verbose_msg_init_end(ll) + class GaussianMixtureWithNonlinearRelationships(WeightedGaussianMixture): """Gaussian mixture class for non-linear relationships. @@ -1207,6 +1272,7 @@ class GaussianMixtureWithNonlinearRelationships(WeightedGaussianMixture): List of mapping describing a nonlinear relationships between physical properties; one per cluster/unit. """ + @requires({"sklearn": sklearn}) def __init__( self, mesh, @@ -1254,6 +1320,7 @@ def _initialize(self, X, resp): """ [modified from Scikit-Learn.mixture.gaussian_mixture] Initialization of the Gaussian mixture parameters. + Parameters ---------- X : array-like, shape (n_samples, n_features) @@ -1295,6 +1362,7 @@ def _estimate_log_gaussian_prob( """ [modified from Scikit-Learn.mixture.gaussian_mixture] Estimate the log Gaussian probability. + Parameters ---------- X : array-like, shape (n_samples, n_features) @@ -1306,6 +1374,7 @@ def _estimate_log_gaussian_prob( 'diag' : shape of (n_components, n_features) 'spherical' : shape of (n_components,) covariance_type : {'full', 'tied', 'diag', 'spherical'} + Returns ------- log_prob : array, shape (n_samples, n_components) @@ -1514,6 +1583,7 @@ class GaussianMixtureWithNonlinearRelationshipsWithPrior(GaussianMixtureWithPrio """ + @requires({"sklearn": sklearn}) def __init__( self, gmmref, diff --git a/simpeg/utils/plot_utils.py b/simpeg/utils/plot_utils.py index fc50246fd8..1c96afd170 100644 --- a/simpeg/utils/plot_utils.py +++ b/simpeg/utils/plot_utils.py @@ -255,7 +255,7 @@ def hillshade(array, azimuth, angle_altitude): Y, hillshade(DATA, shade_azimuth, shade_angle_altitude), shade_ncontour, - **shadeOpts + **shadeOpts, ) if dataloc: @@ -279,7 +279,7 @@ def plot_1d_layer_model( plot_elevation=False, show_layers=False, vlim=None, - **kwargs + **kwargs, ): """ Plot the vertical profile for a 1D layered Earth model. diff --git a/simpeg/utils/solver_utils.py b/simpeg/utils/solver_utils.py index e5b4f343c4..70af2dae68 100644 --- a/simpeg/utils/solver_utils.py +++ b/simpeg/utils/solver_utils.py @@ -1,326 +1,112 @@ -import numpy as np -from scipy.sparse import linalg -from .mat_utils import mkvc +from pymatsolver import ( + AvailableSolvers, + Solver, + SolverLU, + SolverCG, + SolverBiCG, + Diagonal, + Pardiso, + Mumps, + wrap_direct, + wrap_iterative, +) +from pymatsolver.solvers import Base +from .code_utils import deprecate_function import warnings -import inspect - - -def _checkAccuracy(A, b, X, accuracyTol): - nrm = np.linalg.norm(mkvc(A * X - b), np.inf) - nrm_b = np.linalg.norm(mkvc(b), np.inf) - if nrm_b > 0: - nrm /= nrm_b - if nrm > accuracyTol: - msg = "### SolverWarning ###: Accuracy on solve is above tolerance: {0:e} > {1:e}".format( - nrm, accuracyTol - ) - print(msg) - warnings.warn(msg, RuntimeWarning, stacklevel=2) - - -def SolverWrapD(fun, factorize=True, checkAccuracy=True, accuracyTol=1e-6, name=None): - """Wraps a direct Solver.) +from typing import Type + +__all__ = [ + "Solver", + "SolverLU", + "SolverCG", + "SolverBiCG", + "Diagonal", + "Pardiso", + "Mumps", + "wrap_direct", + "wrap_iterative", + "get_default_solver", + "set_default_solver", + "SolverWrapD", + "SolverWrapI", + "SolverDiag", +] + +# The default direct solver priority is: +# Pardiso (optional, but available on intel systems) +# Mumps (optional, but available for all systems) +# Scipy's SuperLU (available for all scipy systems) +if AvailableSolvers["Pardiso"]: + _DEFAULT_SOLVER = Pardiso +elif AvailableSolvers["Mumps"]: + _DEFAULT_SOLVER = Mumps +else: + _DEFAULT_SOLVER = SolverLU + + +def get_default_solver(warn=False) -> Type[Base]: + """Return the default solver used by simpeg. Parameters ---------- - fun : callable - A function handle that accepts a sparse matrix input. - factorize : bool, default: ``True`` - If True, `fun` returns a solver object that has `solve` and - `factorize` methods. - checkAccuracy : bool, default: ``True`` - If ``True``, verify the accuracy of the solve - accuracyTol : float, default: 1e-6 - Minimum accuracy of the solve - name : str, optional - A name for the function - - Returns - ------- - Solver - A new solver class created from a direct solver `fun`. - - Examples - -------- - A solver that does not have a factorize method. - - >>> from simpeg.utils.solver_utils import SolverWrapD - >>> import scipy.sparse as sp - >>> SpSolver = SolverWrapD(sp.linalg.spsolve, factorize=False) - >>> A = sp.diags([1, -1], [0, 1], shape=(10, 10)) - >>> b = np.arange(10) - >>> Ainv = SpSolver(A) - >>> x_solve = Ainv * b - >>> A @ x_solve - array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.]) - - Or one that has a factorize method (which can be re-used on multiple solves) - - >>> SolverLU = SolverWrapD(sp.linalg.splu, factorize=True) - >>> A = sp.diags([1, -1], [0, 1], shape=(10, 10)) - >>> b = np.arange(10) - >>> Ainv = SolverLU(A) - >>> x_solve = Ainv * b - >>> A @ x_solve - array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.]) - """ - - def __init__(self, A, **kwargs): - self.A = A.tocsc() - - self.checkAccuracy = kwargs.pop("checkAccuracy", checkAccuracy) - self.accuracyTol = kwargs.pop("accuracyTol", accuracyTol) - - func_params = inspect.signature(fun).parameters - # First test if function excepts **kwargs, - # in which case we do not need to cull the kwargs - do_cull = True - for param_name in func_params: - param = func_params[param_name] - if param.kind == inspect.Parameter.VAR_KEYWORD: - do_cull = False - if do_cull: - # build a dictionary of valid kwargs - culled_args = {} - for item in kwargs: - if item in func_params: - culled_args[item] = kwargs[item] - else: - warnings.warn( - f"{item} is not a valid keyword for {fun.__name__} and will be ignored", - stacklevel=2, - ) - kwargs = culled_args - - self.kwargs = kwargs - - if factorize: - self.solver = fun(self.A, **kwargs) - - def __mul__(self, b): - if not isinstance(b, np.ndarray): - raise TypeError("Can only multiply by a numpy array.") - - if len(b.shape) == 1 or b.shape[1] == 1: - b = b.flatten() - # Just one RHS - - if b.dtype is np.dtype("O"): - b = b.astype(type(b[0])) - - if factorize: - X = self.solver.solve(b, **self.kwargs) - else: - X = fun(self.A, b, **self.kwargs) - else: # Multiple RHSs - if b.dtype is np.dtype("O"): - b = b.astype(type(b[0, 0])) - - X = np.empty_like(b) - - for i in range(b.shape[1]): - if factorize: - X[:, i] = self.solver.solve(b[:, i]) - else: - X[:, i] = fun(self.A, b[:, i], **self.kwargs) - - if self.checkAccuracy: - _checkAccuracy(self.A, b, X, self.accuracyTol) - return X - - def __matmul__(self, other): - return self * other - - def clean(self): - if factorize and hasattr(self.solver, "clean"): - return self.solver.clean() - - return type( - name if name is not None else fun.__name__, - (object,), - { - "__init__": __init__, - "clean": clean, - "__mul__": __mul__, - "__matmul__": __matmul__, - }, - ) - + warn : bool, optional -def SolverWrapI(fun, checkAccuracy=True, accuracyTol=1e-5, name=None): - """Wraps an iterative Solver. + .. deprecated:: 0.25.0 - Parameters - ---------- - fun : function - A function handle that accepts two arguments, a sparse matrix and a rhs array. - checkAccuracy : bool, default: ``True`` - If ``True``, verify the accuracy of the solve - accuracyTol : float, default: 1e-5 - Minimum accuracy of the solve - name : str, optional - A name for the function + Argument ``warn`` is deprecated and will be removed in + SimPEG v0.26.0. Returns ------- - Solver - A new solver class created from the function. - - Examples - -------- - - >>> import scipy.sparse as sp - >>> from simpeg.utils.solver_utils import SolverWrapI - - >>> SolverCG = SolverWrapI(sp.linalg.cg) - >>> A = sp.diags([-1, 2, -1], [-1, 0, 1], shape=(10, 10)) - >>> b = np.arange(10) - >>> Ainv = SolverCG(A) - >>> x_solve = Ainv * b - >>> A @ x_solve - array([3.55271368e-15, 1.00000000e+00, 2.00000000e+00, 3.00000000e+00, - 4.00000000e+00, 5.00000000e+00, 6.00000000e+00, 7.00000000e+00, - 8.00000000e+00, 9.00000000e+00]) + solver + The default solver class used by simpeg's simulations. """ + if warn: + warnings.warn( + "The `warn` argument has been deprecated and will be " + "removed in SimPEG v0.26.0.", + FutureWarning, + stacklevel=2, + ) + return _DEFAULT_SOLVER - def __init__(self, A, **kwargs): - self.A = A - - self.checkAccuracy = kwargs.pop("checkAccuracy", checkAccuracy) - self.accuracyTol = kwargs.pop("accuracyTol", accuracyTol) - - func_params = inspect.signature(fun).parameters - # First test if function excepts **kwargs, - # in which case we do not need to cull the kwargs - do_cull = True - for param_name in func_params: - param = func_params[param_name] - if param.kind == inspect.Parameter.VAR_KEYWORD: - do_cull = False - if do_cull: - # build a dictionary of valid kwargs - culled_args = {} - for item in kwargs: - if item in func_params: - culled_args[item] = kwargs[item] - else: - warnings.warn( - f"{item} is not a valid keyword for {fun.__name__} and will be ignored", - stacklevel=2, - ) - kwargs = culled_args - - self.kwargs = kwargs - - def __mul__(self, b): - if not isinstance(b, np.ndarray): - raise TypeError("Can only multiply by a numpy array.") - - if len(b.shape) == 1 or b.shape[1] == 1: - b = b.flatten() - # Just one RHS - out = fun(self.A, b, **self.kwargs) - if isinstance(out, tuple) and len(out) == 2: - # We are dealing with scipy output with an info! - X = out[0] - self.info = out[1] - else: - X = out - else: # Multiple RHSs - X = np.empty_like(b) - for i in range(b.shape[1]): - out = fun(self.A, b[:, i], **self.kwargs) - if isinstance(out, tuple) and len(out) == 2: - # We are dealing with scipy output with an info! - X[:, i] = out[0] - self.info = out[1] - else: - X[:, i] = out - - if self.checkAccuracy: - _checkAccuracy(self.A, b, X, self.accuracyTol) - return X - - def __matmul__(self, other): - return self * other - - def clean(self): - pass - - return type( - name if name is not None else fun.__name__, - (object,), - { - "__init__": __init__, - "clean": clean, - "__mul__": __mul__, - "__matmul__": __matmul__, - }, - ) - - -Solver = SolverWrapD(linalg.spsolve, factorize=False, name="Solver") -SolverLU = SolverWrapD(linalg.splu, factorize=True, name="SolverLU") -SolverCG = SolverWrapI(linalg.cg, name="SolverCG") -SolverBiCG = SolverWrapI(linalg.bicgstab, name="SolverBiCG") - - -class SolverDiag(object): - """Solver for a diagonal linear system - This is a simple solver used for diagonal matrices. +def set_default_solver(solver_class: Type[Base]): + """Set the default solver used by simpeg. Parameters ---------- - A : - A diagonal linear system - - Examples - -------- - >>> import scipy.sparse as sp - >>> from simpeg.utils.solver_utils import SolverDiag - >>> A = sp.diags(np.linspace(1, 2, 10)) - >>> b = np.arange(10) - >>> Ainv = SolverDiag(A) - >>> x_solve = Ainv * b - >>> A @ x_solve - array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.]) + solver_class + A ``pymatsolver.solvers.Base`` subclass used to construct an object + that acts os the inverse of a sparse matrix. """ - - def __init__(self, A, **kwargs): - self.A = A - self._diagonal = A.diagonal() - for kwarg in kwargs: - warnings.warn( - f"{kwarg} is not recognized and will be ignored", stacklevel=2 - ) - - def __mul__(self, rhs): - n = self.A.shape[0] - assert rhs.size % n == 0, "Incorrect shape of rhs." - nrhs = rhs.size // n - - if len(rhs.shape) == 1 or rhs.shape[1] == 1: - x = self._solve1(rhs) - else: - x = self._solveM(rhs) - - if nrhs == 1: - return x.flatten() - elif nrhs > 1: - return x.reshape((n, nrhs), order="F") - - def __matmul__(self, other): - return self * other - - def _solve1(self, rhs): - return rhs.flatten() / self._diagonal - - def _solveM(self, rhs): - n = self.A.shape[0] - nrhs = rhs.size // n - return rhs / self._diagonal.repeat(nrhs).reshape((n, nrhs)) - - def clean(self): - """Clean""" - pass + global _DEFAULT_SOLVER + if not issubclass(solver_class, Base): + raise TypeError( + "Default solver must be a subclass of pymatsolver.solvers.Base." + ) + _DEFAULT_SOLVER = solver_class + + +# should likely deprecate these classes in favor of the pymatsolver versions. +SolverWrapD = deprecate_function( + wrap_direct, + old_name="SolverWrapD", + removal_version="0.24.0", + new_location="pymatsolver", + error=True, +) +SolverWrapI = deprecate_function( + wrap_iterative, + old_name="SolverWrapI", + removal_version="0.24.0", + new_location="pymatsolver", + error=True, +) +SolverDiag = deprecate_function( + Diagonal, + old_name="SolverDiag", + removal_version="0.24.0", + new_location="pymatsolver", + error=True, +) diff --git a/simpeg/utils/warnings.py b/simpeg/utils/warnings.py new file mode 100644 index 0000000000..cea674e8f7 --- /dev/null +++ b/simpeg/utils/warnings.py @@ -0,0 +1,17 @@ +""" +Custom warnings that can be used across SimPEG. +""" + +__all__ = ["BreakingChangeWarning", "PerformanceWarning"] + + +class BreakingChangeWarning(Warning): + """ + Warning to let users know about a breaking change that was introduced. + """ + + +class PerformanceWarning(Warning): + """ + Warning raised when there is a possible performance impact. + """ diff --git a/tests/base/props/test_has_model.py b/tests/base/props/test_has_model.py new file mode 100644 index 0000000000..d794f3d05d --- /dev/null +++ b/tests/base/props/test_has_model.py @@ -0,0 +1,117 @@ +import re + +import pytest +import numpy as np +import numpy.testing as npt +from simpeg import props, maps + + +@pytest.fixture(scope="module") +def mock_model_class(): + class MockModel(props.HasModel): + prop, prop_map, _prop_deriv = props.Invertible("test physical property") + other, other_map, _other_deriv = props.Invertible("another physical property") + + def __init__( + self, prop=None, prop_map=None, other=None, other_map=None, **kwargs + ): + self.prop = prop + self.prop_map = prop_map + self.other = other + self.other_map = other_map + super().__init__(**kwargs) + + @property + def prop_dependent_property(self): + if (thing := getattr(self, "_prop_dependent_property", None)) is None: + thing = 2 * self.prop + self._prop_dependent_property = thing + return thing + + @property + def _delete_on_model_update(self): + if self.prop_map is not None: + return ["_prop_dependent_property"] + return [] + + return MockModel + + +@pytest.fixture() +def modeler(mock_model_class): + prop_map = maps.ExpMap() + modeler = mock_model_class(prop_map=prop_map) + return modeler + + +def test_clear_on_model_change(modeler): + x = np.ones(10) + modeler.model = x + npt.assert_array_equal(np.exp(x), modeler.prop) + + item_1 = modeler.prop_dependent_property + assert item_1 is not None + + x2 = x + 1 + modeler.model = x2 + assert getattr(modeler, "_prop_dependent_property", None) is None + + item_2 = modeler.prop_dependent_property + assert item_2 is not None + assert item_1 is not item_2 + + +def test_no_clear_on_model_reassign(modeler): + x = np.ones(10) + + modeler.model = x + npt.assert_array_equal(np.exp(x), modeler.prop) + + item_1 = modeler.prop_dependent_property + assert item_1 is not None + + modeler.model = x.copy() + assert getattr(modeler, "_prop_dependent_property", None) is not None + + item_2 = modeler.prop_dependent_property + + assert item_1 is item_2 + + +def test_map_clearing(modeler): + modeler.prop = np.ones(10) + assert modeler.prop_map is None + + +def test_no_clear_without_mapping(modeler): + modeler.prop_map = None + modeler.other_map = maps.ExpMap() + modeler.prop = np.ones(10) + item1 = modeler.prop_dependent_property + assert item1 is not None + + modeler.model = np.zeros(10) + + assert getattr(modeler, "_prop_dependent_property", None) is not None + assert modeler.prop_dependent_property is item1 + + +def test_model_needed(modeler): + assert modeler.needs_model + + +def test_no_model_needed(modeler): + modeler.prop_map = None + assert not modeler.needs_model + + +def test_deletion_removal(modeler): + msg = re.escape("HasModel.deleteTheseOnModelUpdate has been removed") + with pytest.raises(NotImplementedError, match=msg): + modeler.deleteTheseOnModelUpdate + + +def test_clean_on_model_update_deprecation(modeler): + msg = "clean_on_model_update has been deprecated due to repeated functionality.*" + with pytest.warns(FutureWarning, match=msg): + modeler.clean_on_model_update diff --git a/tests/base/test_Props.py b/tests/base/props/test_prop_relationships.py similarity index 99% rename from tests/base/test_Props.py rename to tests/base/props/test_prop_relationships.py index bb7de0602d..53c2948788 100644 --- a/tests/base/test_Props.py +++ b/tests/base/props/test_prop_relationships.py @@ -106,7 +106,7 @@ def __init__( AMap=None, gamma=4.74, gammaMap=None, - **kwargs + **kwargs, ): super().__init__(**kwargs) self.Ks = Ks diff --git a/tests/base/regularizations/test_cross_gradient.py b/tests/base/regularizations/test_cross_gradient.py index 4b5741a7cb..65f21ea4b7 100644 --- a/tests/base/regularizations/test_cross_gradient.py +++ b/tests/base/regularizations/test_cross_gradient.py @@ -40,10 +40,9 @@ def test_order_approximate_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = True - self.assertTrue(cross_grad.test()) + self.assertTrue(cross_grad.test(random_seed=10)) def test_order_full_hessian(self): """ @@ -51,11 +50,10 @@ def test_order_full_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = False - self.assertTrue(cross_grad._test_deriv()) - self.assertTrue(cross_grad._test_deriv2(expectedOrder=2)) + self.assertTrue(cross_grad._test_deriv(random_seed=10)) + self.assertTrue(cross_grad._test_deriv2(random_seed=10, expectedOrder=2)) def test_deriv2_no_arg(self): np.random.seed(10) @@ -135,10 +133,9 @@ def test_order_approximate_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = True - self.assertTrue(cross_grad.test()) + self.assertTrue(cross_grad.test(random_seed=10)) def test_order_full_hessian(self): """ @@ -146,11 +143,10 @@ def test_order_full_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = False - self.assertTrue(cross_grad._test_deriv()) - self.assertTrue(cross_grad._test_deriv2(expectedOrder=2)) + self.assertTrue(cross_grad._test_deriv(random_seed=10)) + self.assertTrue(cross_grad._test_deriv2(random_seed=10, expectedOrder=2)) def test_deriv2_no_arg(self): np.random.seed(10) @@ -214,10 +210,9 @@ def test_order_approximate_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = True - self.assertTrue(cross_grad.test()) + self.assertTrue(cross_grad.test(random_seed=10)) def test_order_full_hessian(self): """ @@ -225,11 +220,10 @@ def test_order_full_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = False - self.assertTrue(cross_grad._test_deriv()) - self.assertTrue(cross_grad._test_deriv2(expectedOrder=2)) + self.assertTrue(cross_grad._test_deriv(random_seed=10)) + self.assertTrue(cross_grad._test_deriv2(random_seed=10, expectedOrder=2)) def test_deriv2_no_arg(self): np.random.seed(10) @@ -282,10 +276,9 @@ def test_order_approximate_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = True - self.assertTrue(cross_grad.test()) + self.assertTrue(cross_grad.test(random_seed=10)) def test_order_full_hessian(self): """ @@ -293,11 +286,10 @@ def test_order_full_hessian(self): Test deriv and deriv2 matrix of cross-gradient with approx_hessian=True """ - np.random.seed(10) cross_grad = self.cross_grad cross_grad.approx_hessian = False - self.assertTrue(cross_grad._test_deriv()) - self.assertTrue(cross_grad._test_deriv2(expectedOrder=2)) + self.assertTrue(cross_grad._test_deriv(random_seed=10)) + self.assertTrue(cross_grad._test_deriv2(random_seed=10, expectedOrder=2)) def test_deriv2_no_arg(self): np.random.seed(10) diff --git a/tests/base/regularizations/test_full_gradient.py b/tests/base/regularizations/test_full_gradient.py index ae3b51e27f..4653793919 100644 --- a/tests/base/regularizations/test_full_gradient.py +++ b/tests/base/regularizations/test_full_gradient.py @@ -141,7 +141,7 @@ def test_first_derivatives(dim, alphas, reg_dirs): def func(x): return reg(x), reg.deriv(x) - check_derivative(func, np.ones(mesh.n_cells), plotIt=False) + check_derivative(func, np.ones(mesh.n_cells), plotIt=False, random_seed=7641) @pytest.mark.parametrize( @@ -156,7 +156,7 @@ def test_second_derivatives(dim, alphas, reg_dirs): def func(x): return reg.deriv(x), lambda v: reg.deriv2(x, v) - check_derivative(func, np.ones(mesh.n_cells), plotIt=False) + check_derivative(func, np.ones(mesh.n_cells), plotIt=False, random_seed=5876231) @pytest.mark.parametrize("with_active_cells", [True, False]) diff --git a/tests/base/regularizations/test_jtv.py b/tests/base/regularizations/test_jtv.py index 29239f36d7..e30d570c5e 100644 --- a/tests/base/regularizations/test_jtv.py +++ b/tests/base/regularizations/test_jtv.py @@ -46,7 +46,7 @@ def test_order_full_hessian(self): """ jtv = self.jtv self.assertTrue(jtv._test_deriv(self.x0)) - self.assertTrue(jtv._test_deriv2(self.x0, expectedOrder=2)) + self.assertTrue(jtv._test_deriv2(self.x0, random_seed=42, expectedOrder=2)) def test_deriv2_no_arg(self): m = self.x0 @@ -96,7 +96,7 @@ def test_order_full_hessian(self): """ jtv = self.jtv self.assertTrue(jtv._test_deriv(self.x0)) - self.assertTrue(jtv._test_deriv2(self.x0, expectedOrder=2)) + self.assertTrue(jtv._test_deriv2(self.x0, random_seed=42, expectedOrder=2)) def test_deriv2_no_arg(self): m = self.x0 @@ -143,7 +143,7 @@ def test_order_full_hessian(self): """ jtv = self.jtv self.assertTrue(jtv._test_deriv(self.x0)) - self.assertTrue(jtv._test_deriv2(self.x0, expectedOrder=2)) + self.assertTrue(jtv._test_deriv2(self.x0, random_seed=42, expectedOrder=2)) def test_deriv2_no_arg(self): m = self.x0 @@ -192,7 +192,7 @@ def test_order_full_hessian(self): """ jtv = self.jtv self.assertTrue(jtv._test_deriv(self.x0)) - self.assertTrue(jtv._test_deriv2(self.x0, expectedOrder=2)) + self.assertTrue(jtv._test_deriv2(self.x0, random_seed=42, expectedOrder=2)) def test_deriv2_no_arg(self): m = self.x0 diff --git a/tests/base/regularizations/test_mappings.py b/tests/base/regularizations/test_mappings.py new file mode 100644 index 0000000000..cc23000ab3 --- /dev/null +++ b/tests/base/regularizations/test_mappings.py @@ -0,0 +1,345 @@ +""" +Test mapping functions in regularizations. +""" + +import numpy as np +import pytest +from discretize import TensorMesh +from scipy.sparse import diags + +from simpeg.maps import IdentityMap, LinearMap, LogMap +from simpeg.regularization import Smallness, SmoothnessFirstOrder, SmoothnessSecondOrder + + +@pytest.fixture +def tensor_mesh(): + hx = [(2.0, 10)] + h = [hx, hx, hx] + return TensorMesh(h) + + +@pytest.fixture +def active_cells(tensor_mesh): + return np.ones(tensor_mesh.n_cells, dtype=bool) + + +@pytest.fixture +def model(tensor_mesh): + n = tensor_mesh.n_cells + return np.linspace(1.0, 51.0, n) + + +@pytest.fixture +def reference_model(tensor_mesh): + return np.random.default_rng(seed=5959).uniform(size=tensor_mesh.n_cells) + + +class TestMappingInSmallness: + """ + Test mapping in Smallness regularization. + """ + + def test_default_mapping(self, tensor_mesh, active_cells, model, reference_model): + """ + Test regularization using the default (identity) mapping. + """ + reg = Smallness( + mesh=tensor_mesh, active_cells=active_cells, reference_model=reference_model + ) + assert isinstance(reg.mapping, IdentityMap) + volume_weights = tensor_mesh.cell_volumes + expected = np.sum(volume_weights * (model - reference_model) ** 2) + np.testing.assert_allclose(reg(model), expected) + expected_gradient = 2 * volume_weights * (model - reference_model) + np.testing.assert_allclose(reg.deriv(model), expected_gradient) + + def test_linear_mapping(self, tensor_mesh, active_cells, model, reference_model): + """ + Test regularization using a linear mapping. + """ + n_active_cells = active_cells.sum() + a = np.full(n_active_cells, 3.5) + a_matrix = diags(a) + linear_mapping = LinearMap(a_matrix, b=None) + reg = Smallness( + mesh=tensor_mesh, + active_cells=active_cells, + mapping=linear_mapping, + reference_model=reference_model, + ) + assert reg.mapping is linear_mapping + volume_weights = tensor_mesh.cell_volumes + expected = np.sum(volume_weights * (a * model - a * reference_model) ** 2) + np.testing.assert_allclose(reg(model), expected) + expected_gradient = 2 * a * volume_weights * (a * model - a * reference_model) + np.testing.assert_allclose(reg.deriv(model), expected_gradient) + + def test_nonlinear_mapping(self, tensor_mesh, active_cells, model, reference_model): + """ + Test regularization using a non-linear mapping. + """ + log_mapping = LogMap() + reg = Smallness( + mesh=tensor_mesh, + active_cells=active_cells, + mapping=log_mapping, + reference_model=reference_model, + ) + assert reg.mapping is log_mapping + volume_weights = tensor_mesh.cell_volumes + expected = np.sum( + volume_weights * (np.log(model) - np.log(reference_model)) ** 2 + ) + np.testing.assert_allclose(reg(model), expected) + expected_gradient = ( + 2 * (1 / model) * volume_weights * (np.log(model) - np.log(reference_model)) + ) + np.testing.assert_allclose(reg.deriv(model), expected_gradient) + + +@pytest.mark.parametrize( + "use_reference_model", [True, False], ids=["m_ref", "no m_ref"] +) +class TestMappingInSmoothnessFirstOrder: + """ + Test mapping in SmoothnessFirstOrder regularization. + """ + + @pytest.mark.parametrize("orientation", ["x", "y", "z"]) + def test_default_mapping( + self, + tensor_mesh, + active_cells, + model, + reference_model, + use_reference_model, + orientation, + ): + """ + Test regularization using the default (identity) mapping. + """ + reg = SmoothnessFirstOrder( + mesh=tensor_mesh, + active_cells=active_cells, + orientation=orientation, + reference_model=reference_model, + reference_model_in_smooth=use_reference_model, + ) + + # Test call + gradients = getattr(reg.regularization_mesh, f"cell_gradient_{orientation}") + model_diff = model - reference_model if use_reference_model else model + r = reg.W @ gradients @ model_diff + expected = r.T @ r + np.testing.assert_allclose(reg(model), expected) + + # Test deriv + expected_gradient = 2 * gradients.T @ reg.W.T @ reg.W @ gradients @ model_diff + np.testing.assert_allclose(reg.deriv(model), expected_gradient, atol=1e-10) + + @pytest.mark.parametrize("orientation", ["x", "y", "z"]) + def test_linear_mapping( + self, + tensor_mesh, + active_cells, + model, + reference_model, + use_reference_model, + orientation, + ): + """ + Test regularization using a linear mapping. + """ + n_active_cells = active_cells.sum() + a = np.full(n_active_cells, 3.5) + a_matrix = diags(a) + linear_mapping = LinearMap(a_matrix, b=None) + reg = SmoothnessFirstOrder( + mesh=tensor_mesh, + active_cells=active_cells, + orientation=orientation, + reference_model=reference_model, + reference_model_in_smooth=use_reference_model, + mapping=linear_mapping, + ) + assert reg.mapping is linear_mapping + + # Test call + gradients = getattr(reg.regularization_mesh, f"cell_gradient_{orientation}") + model_diff = ( + a * model - a * reference_model if use_reference_model else a * model + ) + r = reg.W @ gradients @ model_diff + expected = r.T @ r + np.testing.assert_allclose(reg(model), expected) + + # Test deriv + f_m_deriv = gradients @ a_matrix + expected_gradient = 2 * f_m_deriv.T @ reg.W.T @ reg.W @ gradients @ model_diff + np.testing.assert_allclose(reg.deriv(model), expected_gradient, atol=1e-10) + + @pytest.mark.parametrize("orientation", ["x", "y", "z"]) + def test_nonlinear_mapping( + self, + tensor_mesh, + active_cells, + model, + reference_model, + use_reference_model, + orientation, + ): + """ + Test regularization using a non-linear mapping. + """ + log_mapping = LogMap() + reg = SmoothnessFirstOrder( + mesh=tensor_mesh, + active_cells=active_cells, + orientation=orientation, + reference_model=reference_model, + reference_model_in_smooth=use_reference_model, + mapping=log_mapping, + ) + assert reg.mapping is log_mapping + + # Test call + gradients = getattr(reg.regularization_mesh, f"cell_gradient_{orientation}") + model_diff = ( + np.log(model) - np.log(reference_model) + if use_reference_model + else np.log(model) + ) + r = reg.W @ gradients @ model_diff + expected = r.T @ r + np.testing.assert_allclose(reg(model), expected) + + # Test deriv + f_m_deriv = gradients @ diags(1 / model) + expected_gradient = 2 * f_m_deriv.T @ reg.W.T @ reg.W @ gradients @ model_diff + np.testing.assert_allclose(reg.deriv(model), expected_gradient, atol=1e-10) + + +@pytest.mark.parametrize( + "use_reference_model", [True, False], ids=["m_ref", "no m_ref"] +) +class TestMappingInSmoothnessSecondOrder: + """ + Test mapping in SmoothnessSecondOrder regularization. + """ + + @pytest.mark.parametrize("orientation", ["x", "y", "z"]) + def test_default_mapping( + self, + tensor_mesh, + active_cells, + model, + reference_model, + use_reference_model, + orientation, + ): + """ + Test regularization using the default (identity) mapping. + """ + reg = SmoothnessSecondOrder( + mesh=tensor_mesh, + active_cells=active_cells, + orientation=orientation, + reference_model=reference_model, + reference_model_in_smooth=use_reference_model, + ) + + # Test call + gradients = getattr(reg.regularization_mesh, f"cell_gradient_{orientation}") + model_diff = model - reference_model if use_reference_model else model + l_matrix = gradients.T @ gradients # 2nd order derivative matrix + r = reg.W @ l_matrix @ model_diff + expected = r.T @ r + np.testing.assert_allclose(reg(model), expected) + + # Test deriv + f_m_deriv = l_matrix + expected_gradient = 2 * f_m_deriv.T @ reg.W.T @ reg.W @ l_matrix @ model_diff + np.testing.assert_allclose(reg.deriv(model), expected_gradient, atol=1e-10) + + @pytest.mark.parametrize("orientation", ["x", "y", "z"]) + def test_linear_mapping( + self, + tensor_mesh, + active_cells, + model, + reference_model, + use_reference_model, + orientation, + ): + """ + Test regularization using a linear mapping. + """ + n_active_cells = active_cells.sum() + a = np.full(n_active_cells, 3.5) + a_matrix = diags(a) + linear_mapping = LinearMap(a_matrix, b=None) + reg = SmoothnessSecondOrder( + mesh=tensor_mesh, + active_cells=active_cells, + orientation=orientation, + reference_model=reference_model, + reference_model_in_smooth=use_reference_model, + mapping=linear_mapping, + ) + assert reg.mapping is linear_mapping + + # Test call + gradients = getattr(reg.regularization_mesh, f"cell_gradient_{orientation}") + model_diff = ( + a * model - a * reference_model if use_reference_model else a * model + ) + l_matrix = gradients.T @ gradients # 2nd order derivative matrix + r = reg.W @ l_matrix @ model_diff + expected = r.T @ r + np.testing.assert_allclose(reg(model), expected) + + # Test deriv + f_m_deriv = l_matrix @ a_matrix + expected_gradient = 2 * f_m_deriv.T @ reg.W.T @ reg.W @ l_matrix @ model_diff + np.testing.assert_allclose(reg.deriv(model), expected_gradient, atol=1e-10) + + @pytest.mark.parametrize("orientation", ["x", "y", "z"]) + def test_nonlinear_mapping( + self, + tensor_mesh, + active_cells, + model, + reference_model, + use_reference_model, + orientation, + ): + """ + Test regularization using a non-linear mapping. + """ + log_mapping = LogMap() + reg = SmoothnessSecondOrder( + mesh=tensor_mesh, + active_cells=active_cells, + orientation=orientation, + reference_model=reference_model, + reference_model_in_smooth=use_reference_model, + mapping=log_mapping, + ) + assert reg.mapping is log_mapping + + # Test call + gradients = getattr(reg.regularization_mesh, f"cell_gradient_{orientation}") + model_diff = ( + np.log(model) - np.log(reference_model) + if use_reference_model + else np.log(model) + ) + l_matrix = gradients.T @ gradients # 2nd order derivative matrix + r = reg.W @ l_matrix @ model_diff + expected = r.T @ r + np.testing.assert_allclose(reg(model), expected) + + # Test deriv + f_m_deriv = l_matrix @ diags(1 / model) + expected_gradient = 2 * f_m_deriv.T @ reg.W.T @ reg.W @ l_matrix @ model_diff + np.testing.assert_allclose(reg.deriv(model), expected_gradient, atol=1e-10) diff --git a/tests/base/regularizations/test_pgi_regularization.py b/tests/base/regularizations/test_pgi_regularization.py index b1f08e905f..98bfa5fb69 100644 --- a/tests/base/regularizations/test_pgi_regularization.py +++ b/tests/base/regularizations/test_pgi_regularization.py @@ -3,12 +3,14 @@ import discretize import numpy as np -from pymatsolver import SolverLU from scipy.stats import multivariate_normal from simpeg import regularization from simpeg.maps import Wires from simpeg.utils import WeightedGaussianMixture, mkvc +from simpeg.utils import get_default_solver + +Solver = get_default_solver() class TestPGI(unittest.TestCase): @@ -104,7 +106,7 @@ def test_full_covariances(self): self.assertTrue(passed_deriv1) print("1st derivatives for PGI & Full Cov. are ok.") - Hinv = SolverLU(reg.deriv2(self.model)) + Hinv = Solver(reg.deriv2(self.model)) p = Hinv * deriv direction2 = np.c_[self.wires * p] passed_derivative = np.allclose( @@ -209,7 +211,7 @@ def test_tied_covariances(self): self.assertTrue(passed_deriv1) print("1st derivatives for PGI & tied Cov. are ok.") - Hinv = SolverLU(reg.deriv2(self.model)) + Hinv = Solver(reg.deriv2(self.model)) p = Hinv * deriv direction2 = np.c_[self.wires * p] passed_derivative = np.allclose( @@ -312,7 +314,7 @@ def test_diag_covariances(self): self.assertTrue(passed_deriv1) print("1st derivatives for PGI & diag Cov. are ok.") - Hinv = SolverLU(reg.deriv2(self.model)) + Hinv = Solver(reg.deriv2(self.model)) p = Hinv * deriv direction2 = np.c_[self.wires * p] passed_derivative = np.allclose( @@ -415,7 +417,7 @@ def test_spherical_covariances(self): self.assertTrue(passed_deriv1) print("1st derivatives for PGI & spherical Cov. are ok.") - Hinv = SolverLU(reg.deriv2(self.model)) + Hinv = Solver(reg.deriv2(self.model)) p = Hinv * deriv direction2 = np.c_[self.wires * p] passed_derivative = np.allclose( @@ -470,18 +472,84 @@ def test_spherical_covariances(self): plt.show() -def test_removed_mref(): - """Test if PGI raises error when accessing removed mref property.""" - h = [[(2, 2)], [(2, 2)], [(2, 2)]] - mesh = discretize.TensorMesh(h) - n_components = 1 - gmm = WeightedGaussianMixture(mesh=mesh, n_components=n_components) - samples = np.random.default_rng(seed=42).normal(size=(mesh.n_cells, 2)) - gmm.fit(samples) - pgi = regularization.PGI(mesh=mesh, gmmref=gmm) - message = "mref has been removed, please use reference_model." - with pytest.raises(NotImplementedError, match=message): - pgi.mref +class TestCheckWeights: + """Test the ``WeightedGaussianMixture._check_weights`` method.""" + + VALID_ARGS = { + "1d-array": (np.array([0.5, 0.2, 0.3]), 3, None), + "2d-array": (np.array([[0.5, 0.2, 0.3], [0.25, 0.70, 0.05]]), 3, 2), + "1d-list": ([0.5, 0.2, 0.3], 3, None), + "2d-list": ([[0.5, 0.2, 0.3], [0.25, 0.70, 0.05]], 3, 2), + } + INVALID_SHAPE = { + "1d-array": (np.array([0.5, 0.2, 0.3]), 5, None), + "2d-array": (np.array([[0.5, 0.2, 0.3], [0.25, 0.70, 0.05]]), 5, 13), + "1d-list": ([0.5, 0.2, 0.3], 5, None), + "2d-list": ([[0.5, 0.2, 0.3], [0.25, 0.70, 0.05]], 5, 13), + } + INVALID_RANGE = { + "1d-greater": (np.array([10.5, 0.2, 0.3]), 3, None), + "1d-lower": (np.array([-1.0, 0.2, 0.3]), 3, None), + "2d-greater": (np.array([[0.5, 0.2, 0.3], [10.25, 0.70, 0.05]]), 3, 2), + "2d-lower": (np.array([[0.5, 0.2, 0.3], [0.25, -0.70, 0.05]]), 3, 2), + } + INVALID_NORM = { + "1d-lower": (np.array([0.001, 0.2, 0.3]), 3, None), + "1d-greater": (np.array([0.99, 0.2, 0.3]), 3, None), + "2d-lower": (np.array([[0.001, 0.2, 0.3], [0.25, 0.70, 0.05]]), 3, 2), + "2d-greater": (np.array([[0.99, 0.2, 0.3], [0.25, 0.70, 0.05]]), 3, 2), + } + + @pytest.fixture + def mesh(self): + mesh = discretize.TensorMesh([2, 2, 2]) + return mesh + + @pytest.mark.parametrize("args", VALID_ARGS.values(), ids=VALID_ARGS.keys()) + def test_valid_arguments(self, mesh, args): + """ + Check if method doesn't fail if arguments are valid. + """ + weights, n_components, n_samples = args + WeightedGaussianMixture(n_components=1, mesh=mesh)._check_weights( + weights, n_components, n_samples + ) + + @pytest.mark.parametrize("args", INVALID_SHAPE.values(), ids=INVALID_SHAPE.keys()) + def test_invalid_shape(self, mesh, args): + """ + Check if method raise error upon weights with invalid shape. + """ + weights, n_components, n_samples = args + msg = "The parameter 'weights' should have the shape of" + with pytest.raises(ValueError, match=msg): + WeightedGaussianMixture(n_components=1, mesh=mesh)._check_weights( + weights, n_components, n_samples + ) + + @pytest.mark.parametrize("args", INVALID_RANGE.values(), ids=INVALID_RANGE.keys()) + def test_invalid_range(self, mesh, args): + """ + Check if method raise error upon weights with invalid range. + """ + weights, n_components, n_samples = args + msg = r"The parameter 'weights' should be in the range \[0, 1\]" + with pytest.raises(ValueError, match=msg): + WeightedGaussianMixture(n_components=1, mesh=mesh)._check_weights( + weights, n_components, n_samples + ) + + @pytest.mark.parametrize("args", INVALID_NORM.values(), ids=INVALID_NORM.keys()) + def test_non_normalized(self, mesh, args): + """ + Check if method raise error upon non-normalized weights. + """ + weights, n_components, n_samples = args + msg = r"The parameter 'weights' should be normalized" + with pytest.raises(ValueError, match=msg): + WeightedGaussianMixture(n_components=1, mesh=mesh)._check_weights( + weights, n_components, n_samples + ) if __name__ == "__main__": diff --git a/tests/base/regularizations/test_regularization.py b/tests/base/regularizations/test_regularization.py index 257dca9dba..cf0c0a33d5 100644 --- a/tests/base/regularizations/test_regularization.py +++ b/tests/base/regularizations/test_regularization.py @@ -89,15 +89,16 @@ def test_regularization(self): print("--- Checking {} --- \n".format(reg.__class__.__name__)) + rng = np.random.default_rng(seed=639) if mapping.nP != "*": - m = np.random.rand(mapping.nP) + m = rng.random(mapping.nP) else: - m = np.random.rand(mesh.nC) + m = rng.random(mesh.nC) mref = np.ones_like(m) * np.mean(m) reg.reference_model = mref # test derivs - passed = reg.test(m, eps=TOL) + passed = reg.test(m, eps=TOL, random_seed=rng) self.assertTrue(passed) def test_regularization_ActiveCells(self): @@ -139,13 +140,14 @@ def test_regularization_ActiveCells(self): reg = r( mesh, active_cells=active_cells, mapping=maps.IdentityMap(nP=nP) ) - m = np.random.rand(mesh.nC)[active_cells] + rng = np.random.default_rng(seed=532) + m = rng.random(mesh.nC)[active_cells] mref = np.ones_like(m) * np.mean(m) reg.reference_model = mref print("--- Checking {} ---\n".format(reg.__class__.__name__)) - passed = reg.test(m, eps=TOL) + passed = reg.test(m, eps=TOL, random_seed=rng) self.assertTrue(passed) if testRegMesh: @@ -239,7 +241,8 @@ def test_property_mirroring(self): def test_addition(self): mesh = discretize.TensorMesh([8, 7, 6]) - m = np.random.rand(mesh.nC) + rng = np.random.default_rng(seed=523) + m = rng.random(mesh.nC) reg1 = regularization.WeightedLeastSquares(mesh) reg2 = regularization.WeightedLeastSquares(mesh) @@ -247,21 +250,22 @@ def test_addition(self): reg_a = reg1 + reg2 self.assertTrue(len(reg_a) == 2) self.assertTrue(reg1(m) + reg2(m) == reg_a(m)) - reg_a.test(eps=TOL) + reg_a.test(eps=TOL, random_seed=rng) reg_b = 2 * reg1 + reg2 self.assertTrue(len(reg_b) == 2) self.assertTrue(2 * reg1(m) + reg2(m) == reg_b(m)) - reg_b.test(eps=TOL) + reg_b.test(eps=TOL, random_seed=rng) reg_c = reg1 + reg2 / 2 self.assertTrue(len(reg_c) == 2) self.assertTrue(reg1(m) + 0.5 * reg2(m) == reg_c(m)) - reg_c.test(eps=TOL) + reg_c.test(eps=TOL, random_seed=rng) def test_mappings(self): mesh = discretize.TensorMesh([8, 7, 6]) - m = np.random.rand(2 * mesh.nC) + rng = np.random.default_rng(seed=123) + m = rng.random(2 * mesh.nC) wires = maps.Wires(("sigma", mesh.nC), ("mu", mesh.nC)) @@ -276,9 +280,9 @@ def test_mappings(self): self.assertTrue(reg3.nP == 2 * mesh.nC) self.assertTrue(reg3(m) == reg1(m) + reg2(m)) - reg1.test(eps=TOL) - reg2.test(eps=TOL) - reg3.test(eps=TOL) + reg1.test(eps=TOL, random_seed=rng) + reg2.test(eps=TOL, random_seed=rng) + reg3.test(eps=TOL, random_seed=rng) def test_mref_is_zero(self): mesh = discretize.TensorMesh([10, 5, 8]) @@ -469,7 +473,7 @@ def test_nC_residual(self): ) mapping = maps.ExpMap(mesh) * maps.SurjectVertical1D(mesh) * actMap - regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) + regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].active_cells]]) reg = regularization.WeightedLeastSquares(regMesh) self.assertTrue(reg._nC_residual == regMesh.nC) @@ -577,7 +581,8 @@ def test_sparse_properties(self): def test_vector_amplitude(self): n_comp = 4 mesh = discretize.TensorMesh([8, 7]) - model = np.random.randn(mesh.nC, n_comp) + rng = np.random.default_rng(5412) + model = rng.random((mesh.nC, n_comp)) with pytest.raises(TypeError, match="'regularization_mesh' must be of type"): regularization.VectorAmplitude("abc") @@ -593,7 +598,7 @@ def test_vector_amplitude(self): reg.objfcts[0].f_m(model.flatten(order="F")), np.linalg.norm(model, axis=1) ) - reg.test(model.flatten(order="F")) + reg.test(model.flatten(order="F"), random_seed=rng) def test_WeightedLeastSquares(): @@ -615,6 +620,7 @@ def test_WeightedLeastSquares(): def test_cross_ref_reg(dim): mesh = discretize.TensorMesh([3, 4, 5][:dim]) actives = mesh.cell_centers[:, -1] < 0.6 + rng = np.random.default_rng(6634) n_active = actives.sum() ref_dir = dim * [1] @@ -627,8 +633,8 @@ def test_cross_ref_reg(dim): assert cross_reg._nC_residual == dim * n_active # give it some cell weights, and some cell vector weights to do something with - cell_weights = np.random.rand(n_active) - cell_vec_weights = np.random.rand(n_active, dim) + cell_weights = rng.random(n_active) + cell_vec_weights = rng.random((n_active, dim)) cross_reg.set_weights(cell_weights=cell_weights) cross_reg.set_weights(vec_weights=cell_vec_weights) @@ -637,8 +643,8 @@ def test_cross_ref_reg(dim): else: assert cross_reg.W.shape == (n_active, n_active) - m = np.random.rand(dim * n_active) - cross_reg.test(m) + m = rng.random(dim * n_active) + cross_reg.test(m, random_seed=rng) def test_cross_reg_reg_errors(): @@ -785,25 +791,18 @@ def mesh(self, request): ) def test_mref_property(self, mesh, regularization_class): """Test mref property.""" - msg = "mref has been removed, please use reference_model." reg = regularization_class(mesh) - with pytest.raises(NotImplementedError, match=msg): - reg.mref + assert not hasattr(reg, "mref") def test_regmesh_property(self, mesh): """Test regmesh property.""" - msg = "regmesh has been removed, please use regularization_mesh." reg = BaseRegularization(mesh) - with pytest.raises(NotImplementedError, match=msg): - reg.regmesh + assert not hasattr(reg, "regmesh") @pytest.mark.parametrize("regularization_class", (Sparse, SparseSmoothness)) def test_gradient_type(self, mesh, regularization_class): """Test gradientType argument.""" - msg = ( - "'gradientType' argument has been removed. " - "Please use 'gradient_type' instead." - ) + msg = "got an unexpected keyword argument" with pytest.raises(TypeError, match=msg): regularization_class(mesh, gradientType="total") @@ -814,10 +813,7 @@ def test_gradient_type(self, mesh, regularization_class): def test_ind_active(self, mesh, regularization_class): """Test if error is raised when passing the indActive argument.""" active_cells = np.ones(len(mesh), dtype=bool) - msg = ( - "'indActive' argument has been removed. " - "Please use 'active_cells' instead." - ) + msg = "got an unexpected keyword argument" with pytest.raises(TypeError, match=msg): regularization_class(mesh, indActive=active_cells) @@ -829,9 +825,7 @@ def test_ind_active_property(self, mesh, regularization_class): """Test if error is raised when trying to access the indActive property.""" active_cells = np.ones(len(mesh), dtype=bool) reg = regularization_class(mesh, active_cells=active_cells) - msg = "indActive has been removed, please use active_cells." - with pytest.raises(NotImplementedError, match=msg): - reg.indActive + assert not hasattr(reg, "indActive") @pytest.mark.parametrize( "regularization_class", @@ -840,7 +834,7 @@ def test_ind_active_property(self, mesh, regularization_class): def test_cell_weights_argument(self, mesh, regularization_class): """Test if error is raised when passing the cell_weights argument.""" weights = np.ones(len(mesh)) - msg = "'cell_weights' argument has been removed. Please use 'weights' instead." + msg = "got an unexpected keyword argument" with pytest.raises(TypeError, match=msg): regularization_class(mesh, cell_weights=weights) @@ -850,54 +844,8 @@ def test_cell_weights_argument(self, mesh, regularization_class): def test_cell_weights_property(self, mesh, regularization_class): """Test if error is raised when trying to access the cell_weights property.""" weights = {"weights": np.ones(len(mesh))} - msg = ( - "'cell_weights' has been removed. " - "Please access weights using the `set_weights`, `get_weights`, and " - "`remove_weights` methods." - ) reg = regularization_class(mesh, weights=weights) - with pytest.raises(AttributeError, match=msg): - reg.cell_weights - - @pytest.mark.parametrize( - "regularization_class", (BaseRegularization, WeightedLeastSquares) - ) - def test_cell_weights_setter(self, mesh, regularization_class): - """Test if error is raised when trying to set the cell_weights property.""" - msg = ( - "'cell_weights' has been removed. " - "Please access weights using the `set_weights`, `get_weights`, and " - "`remove_weights` methods." - ) - reg = regularization_class(mesh) - with pytest.raises(AttributeError, match=msg): - reg.cell_weights = "dummy variable" - - -class TestRemovedRegularizations: - """ - Test if errors are raised after creating removed regularization classes. - """ - - @pytest.mark.parametrize( - "regularization_class", - ( - regularization.PGIwithNonlinearRelationshipsSmallness, - regularization.PGIwithRelationships, - regularization.Simple, - regularization.SimpleSmall, - regularization.SimpleSmoothDeriv, - regularization.Small, - regularization.SmoothDeriv, - regularization.SmoothDeriv2, - regularization.Tikhonov, - ), - ) - def test_removed_class(self, regularization_class): - class_name = regularization_class.__name__ - msg = f"{class_name} has been removed, please use." - with pytest.raises(NotImplementedError, match=msg): - regularization_class() + assert not hasattr(reg, "cell_weights") @pytest.mark.parametrize( diff --git a/tests/base/test_Fields.py b/tests/base/test_Fields.py index b9450bfc5f..0f06c586bd 100644 --- a/tests/base/test_Fields.py +++ b/tests/base/test_Fields.py @@ -4,14 +4,9 @@ from simpeg import survey, simulation, utils, fields, data import numpy as np -import sys np.random.seed(32) - -if sys.version_info < (3,): - zero_types = [0, 0.0, np.r_[0], long(0)] -else: - zero_types = [0, 0.0, np.r_[0]] +zero_types = [0, 0.0, np.r_[0]] class FieldsTest(unittest.TestCase): @@ -40,7 +35,10 @@ def setUp(self): source_list = [Src0, Src1, Src2, Src3, Src4] mysurvey = survey.BaseSurvey(source_list=source_list) - sim = simulation.BaseSimulation(mesh=mesh, survey=mysurvey) + sim = simulation.BaseSimulation(survey=mysurvey) + # insert a mesh into the simulation (required for the Fields objects) + # This should likely be moved to a BasePDESimulation test. + sim.mesh = mesh self.D = data.Data(mysurvey) self.F = fields.Fields( sim, @@ -49,7 +47,6 @@ def setUp(self): ) self.Src0 = Src0 self.Src1 = Src1 - self.mesh = mesh self.XYZ = XYZ self.simulation = sim @@ -164,7 +161,10 @@ def setUp(self): source_list = [Src0, Src1, Src2, Src3, Src4] mysurvey = survey.BaseSurvey(source_list=source_list) - sim = simulation.BaseSimulation(mesh=mesh, survey=mysurvey) + sim = simulation.BaseSimulation(survey=mysurvey) + # insert a mesh into the simulation (required for the Fields objects) + # This should likely be moved to a BasePDESimulation test. + sim.mesh = mesh self.F = fields.Fields( sim, knownFields={"e": "E"}, @@ -172,7 +172,6 @@ def setUp(self): ) self.Src0 = Src0 self.Src1 = Src1 - self.mesh = mesh self.XYZ = XYZ self.simulation = sim @@ -255,14 +254,16 @@ def setUp(self): source_list = [Src0, Src1, Src2, Src3, Src4] mysurvey = survey.BaseSurvey(source_list=source_list) sim = simulation.BaseTimeSimulation( - mesh, time_steps=[(10.0, 3), (20.0, 2)], survey=mysurvey + time_steps=[(10.0, 3), (20.0, 2)], survey=mysurvey ) + # insert a mesh into the simulation (required for the Fields objects) + # This should likely be moved to a BasePDESimulation test. + sim.mesh = mesh self.F = fields.TimeFields( simulation=sim, knownFields={"phi": "CC", "e": "E", "b": "F"} ) self.Src0 = Src0 self.Src1 = Src1 - self.mesh = mesh self.XYZ = XYZ def test_contains(self): @@ -380,8 +381,11 @@ def setUp(self): source_list = [Src0, Src1, Src2, Src3, Src4] mysurvey = survey.BaseSurvey(source_list=source_list) sim = simulation.BaseTimeSimulation( - mesh, time_steps=[(10.0, 3), (20.0, 2)], survey=mysurvey + time_steps=[(10.0, 3), (20.0, 2)], survey=mysurvey ) + # insert a mesh into the simulation (required for the Fields objects) + # This should likely be moved to a BasePDESimulation test. + sim.mesh = mesh def alias(b, srcInd, timeInd): return self.F.mesh.edge_curl.T * b + timeInd @@ -391,7 +395,6 @@ def alias(b, srcInd, timeInd): ) self.Src0 = Src0 self.Src1 = Src1 - self.mesh = mesh self.XYZ = XYZ self.simulation = sim diff --git a/tests/base/test_Solver.py b/tests/base/test_Solver.py deleted file mode 100644 index adb753f6c8..0000000000 --- a/tests/base/test_Solver.py +++ /dev/null @@ -1,77 +0,0 @@ -import unittest - -from simpeg import Solver, SolverDiag, SolverCG, SolverLU -from discretize import TensorMesh -from simpeg.utils import sdiag -import numpy as np - -TOLD = 1e-10 -TOLI = 1e-3 -numRHS = 5 - -np.random.seed(77) - - -def dotest(MYSOLVER, multi=False, A=None, **solverOpts): - if A is None: - h1 = np.ones(10) * 100.0 - h2 = np.ones(10) * 100.0 - h3 = np.ones(10) * 100.0 - - h = [h1, h2, h3] - - M = TensorMesh(h) - - D = M.face_divergence - G = -M.face_divergence.T - Msig = M.get_face_inner_product() - A = D * Msig * G - A[-1, -1] *= ( - 1 / M.cell_volumes[-1] - ) # remove the constant null space from the matrix - else: - M = TensorMesh([A.shape[0]]) - - Ainv = MYSOLVER(A, **solverOpts) - if multi: - e = np.ones(M.nC) - else: - e = np.ones((M.nC, numRHS)) - rhs = A * e - x = Ainv * rhs - Ainv.clean() - return np.linalg.norm(e - x, np.inf) - - -class TestSolver(unittest.TestCase): - def test_direct_spsolve_1(self): - self.assertLess(dotest(Solver, False), TOLD) - - def test_direct_spsolve_M(self): - self.assertLess(dotest(Solver, True), TOLD) - - def test_direct_splu_1(self): - self.assertLess(dotest(SolverLU, False), TOLD) - - def test_direct_splu_M(self): - self.assertLess(dotest(SolverLU, True), TOLD) - - def test_iterative_diag_1(self): - self.assertLess( - dotest(SolverDiag, False, A=sdiag(np.random.rand(10) + 1.0)), TOLI - ) - - def test_iterative_diag_M(self): - self.assertLess( - dotest(SolverDiag, True, A=sdiag(np.random.rand(10) + 1.0)), TOLI - ) - - def test_iterative_cg_1(self): - self.assertLess(dotest(SolverCG, False), TOLI) - - def test_iterative_cg_M(self): - self.assertLess(dotest(SolverCG, True), TOLI) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/base/test_mass_matrices.py b/tests/base/test_base_pde_sim.py similarity index 90% rename from tests/base/test_mass_matrices.py rename to tests/base/test_base_pde_sim.py index 90dc04ea75..e9406d1339 100644 --- a/tests/base/test_mass_matrices.py +++ b/tests/base/test_base_pde_sim.py @@ -1,5 +1,9 @@ +import re + +import pymatsolver from simpeg.base import with_property_mass_matrices, BasePDESimulation from simpeg import props, maps +from simpeg.utils import PerformanceWarning import unittest import discretize import numpy as np @@ -9,6 +13,8 @@ import scipy.sparse as sp import pytest +from simpeg.utils import get_default_solver + # define a very simple class... @with_property_mass_matrices("sigma") @@ -28,11 +34,11 @@ def __init__( self.muMap = muMap @property - def deleteTheseOnModelUpdate(self): + def _delete_on_model_update(self): """ matrices to be deleted if the model for conductivity/resistivity is updated """ - toDelete = super().deleteTheseOnModelUpdate + toDelete = super()._delete_on_model_update if self.sigmaMap is not None or self.rhoMap is not None: toDelete = toDelete + self._clear_on_sigma_update return toDelete @@ -493,7 +499,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=8672354) def test_Mn_deriv(self): u = np.random.randn(self.mesh.n_nodes) @@ -510,7 +516,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=523876) def test_Me_deriv(self): u = np.random.randn(self.mesh.n_edges) @@ -527,7 +533,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=9875163) def test_Me_diagonal_anisotropy_deriv(self): u = np.random.randn(self.mesh.n_edges) @@ -544,7 +550,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=1658372) def test_Me_full_anisotropy_deriv(self): u = np.random.randn(self.mesh.n_edges) @@ -561,7 +567,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=9867234) def test_Mf_deriv(self): u = np.random.randn(self.mesh.n_faces) @@ -578,7 +584,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=10523687) def test_Mf_diagonal_anisotropy_deriv(self): u = np.random.randn(self.mesh.n_faces) @@ -595,7 +601,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=19876354) def test_Mf_full_anisotropy_deriv(self): u = np.random.randn(self.mesh.n_faces) @@ -612,7 +618,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=102309487) def test_MccI_deriv(self): u = np.random.randn(self.mesh.n_cells) @@ -629,7 +635,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=89726354) def test_MnI_deriv(self): u = np.random.randn(self.mesh.n_nodes) @@ -646,7 +652,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=12503698) def test_MeI_deriv(self): u = np.random.randn(self.mesh.n_edges) @@ -663,7 +669,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=5674129834) def test_MfI_deriv(self): u = np.random.randn(self.mesh.n_faces) @@ -680,7 +686,7 @@ def Jvec(v): return d, Jvec - assert check_derivative(f, x0=x0, num=3, plotIt=False) + assert check_derivative(f, x0=x0, num=3, plotIt=False, random_seed=532349) def test_Mcc_adjoint(self): n_items = self.mesh.n_cells @@ -806,3 +812,48 @@ def test_bad_derivative_stash(): with pytest.raises(TypeError): sim.MeSigmaDeriv(u, v) + + +def test_solver_defaults(caplog, info_logging): + mesh = discretize.TensorMesh([2, 2, 2]) + sim = BasePDESimulation(mesh) + # Check that logging.info was created + assert "Setting the default solver" in caplog.text + # Test if default solver was properly set + assert sim.solver is get_default_solver() + + +@pytest.mark.parametrize("solver_class", [pymatsolver.SolverLU, pymatsolver.Solver]) +def test_performance_warning_on_solver(solver_class): + """ + Test PerformanceWarning when setting an inefficient solver. + """ + mesh = discretize.TensorMesh([2, 2, 2]) + regex = re.escape( + f"The 'pymatsolver.{solver_class.__name__}' solver might lead to high " + "computation times." + ) + with pytest.warns(PerformanceWarning, match=regex): + BasePDESimulation(mesh, solver=solver_class) + + +def test_bad_solver(): + mesh = discretize.TensorMesh([2, 2, 2]) + msg = re.escape("BasePDESimulation.solver must be a class") + with pytest.raises(TypeError, match=msg): + BasePDESimulation(mesh, solver="f") + + msg = re.escape("str is not a subclass of pymatsolver.base.BaseSolver") + with pytest.raises(TypeError, match=msg): + BasePDESimulation(mesh, solver=str) + + +def test_mesh_required(): + with pytest.raises(TypeError): + BasePDESimulation() + + +def test_bad_mesh(): + with pytest.raises(TypeError): + # should error on anything besides a discretize.base.BaseMesh + BasePDESimulation(np.array([1, 2, 3])) diff --git a/tests/base/test_correspondance.py b/tests/base/test_correspondance.py index e4cd6cac3f..8e92fdb848 100644 --- a/tests/base/test_correspondance.py +++ b/tests/base/test_correspondance.py @@ -43,8 +43,8 @@ def test_order_full_hessian(self): """ corr = self.corr - self.assertTrue(corr._test_deriv()) - self.assertTrue(corr._test_deriv2(expectedOrder=2)) + self.assertTrue(corr._test_deriv(random_seed=10)) + self.assertTrue(corr._test_deriv2(random_seed=10, expectedOrder=2)) def test_deriv2_no_arg(self): m = np.random.randn(2 * len(self.mesh)) diff --git a/tests/base/test_data_misfit.py b/tests/base/test_data_misfit.py index 2e23131da5..76b0905e51 100644 --- a/tests/base/test_data_misfit.py +++ b/tests/base/test_data_misfit.py @@ -1,3 +1,5 @@ +import re +import pytest import unittest import numpy as np @@ -5,8 +7,7 @@ from simpeg import maps from simpeg import data_misfit, simulation, survey - -np.random.seed(17) +from simpeg import Data class DataMisfitTest(unittest.TestCase): @@ -23,7 +24,7 @@ def setUp(self): mesh=mesh, survey=survey.BaseSurvey([source]), model_map=maps.ExpMap(mesh) ) - synthetic_data = sim.make_synthetic_data(model) + synthetic_data = sim.make_synthetic_data(model, random_seed=17) self.relative = 0.01 self.noise_floor = 1e-8 @@ -68,7 +69,74 @@ def test_setting_W(self): def test_DataMisfitOrder(self): self.data.relative_error = self.relative self.data.noise_floor = self.noise_floor - self.dmis.test(x=self.model) + self.dmis.test(x=self.model, random_seed=17) + + def test_real_valued(self): + # Change model + new_model = self.model + 1 + + # Misfit to data + misfit_original = self.dmis(new_model) + + # Test pseudo-complex, with 0 imaginary part; misfit must be the same + d_pseudo = Data(self.sim.survey, dobs=self.data.dobs + 0j * self.data.dobs) + d_pseudo.relative_error = self.relative + d_pseudo.noise_floor = self.noise_floor + dmis_pseudo = data_misfit.L2DataMisfit(simulation=self.sim, data=d_pseudo) + misfit_pseudo = dmis_pseudo(new_model) + # assert_array_equal with strict also checks dtype + np.testing.assert_array_equal(misfit_original, misfit_pseudo, strict=True) + + # Test actually complex; misfit must be different + data_imag = self.sim.make_synthetic_data(self.model, random_seed=17) + d_complex = Data(self.sim.survey, dobs=self.data.dobs + 1j * data_imag.dobs) + d_complex.relative_error = self.relative + d_complex.noise_floor = self.noise_floor + dmis_complex = data_misfit.L2DataMisfit(simulation=self.sim, data=d_complex) + misfit_complex = dmis_complex(new_model) + assert misfit_original != misfit_complex + assert misfit_complex.dtype == np.float64 + + +class MockSimulation(simulation.BaseSimulation): + """ + Mock simulation class that returns nans or infs in the dpred array. + """ + + def __init__(self, invalid_value=np.nan): + self.invalid_value = invalid_value + super().__init__() + + def dpred(self, m=None, f=None): + a = np.arange(4, dtype=np.float64) + a[1] = self.invalid_value + return a + + +class TestNanOrInfInResidual: + """Test errors if the simulation return dpred with nans or infs.""" + + @pytest.fixture + def n_data(self): + return 4 + + @pytest.fixture + def sample_survey(self, n_data): + receivers = survey.BaseRx(np.zeros(n_data)[:, np.newaxis]) + source = survey.BaseSrc([receivers]) + return survey.BaseSurvey([source]) + + @pytest.mark.parametrize("invalid_value", [np.nan, np.inf]) + def test_error(self, sample_survey, invalid_value): + mock_simulation = MockSimulation(invalid_value) + data = Data(sample_survey) + dmisfit = data_misfit.BaseDataMisfit(data, mock_simulation) + msg = re.escape( + "The `MockSimulation.dpred()` method returned an array that contains " + "`nan`s and/or `inf`s." + ) + with pytest.raises(ValueError, match=msg): + dmisfit.residual(m=None) if __name__ == "__main__": diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index 474fba332a..387f74d6c3 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -1,27 +1,53 @@ +import re +from collections import namedtuple +from datetime import datetime +import pathlib import unittest +import warnings +from statistics import harmonic_mean + import pytest import numpy as np import discretize +import simpeg from simpeg import ( maps, directives, regularization, - data_misfit, optimization, inversion, inverse_problem, simulation, ) +from simpeg.data_misfit import L2DataMisfit +from simpeg.potential_fields import gravity from simpeg.potential_fields import magnetics as mag import shutil +from simpeg.regularization.base import Smallness +from simpeg.regularization.sparse import Sparse + class directivesValidation(unittest.TestCase): + + def test_error_irls_and_beta_scheduling(self): + """ + Test if validation error when ``UpdateIRLS`` and ``BetaSchedule`` are present. + """ + directives_list = directives.DirectiveList( + directives.UpdateIRLS(), + directives.BetaSchedule(coolingFactor=2, coolingRate=1), + ) + msg = "Beta scheduling is handled by the" + with pytest.raises(AssertionError, match=msg): + directives_list.validate() + def test_validation_pass(self): betaest = directives.BetaEstimate_ByEig() - IRLS = directives.Update_IRLS(f_min_change=1e-4, minGNiter=3, beta_tol=1e-2) + IRLS = directives.UpdateIRLS() + update_Jacobi = directives.UpdatePreconditioner() dList = [betaest, IRLS, update_Jacobi] directiveList = directives.DirectiveList(*dList) @@ -31,8 +57,9 @@ def test_validation_pass(self): def test_validation_fail(self): betaest = directives.BetaEstimate_ByEig() - IRLS = directives.Update_IRLS(f_min_change=1e-4, minGNiter=3, beta_tol=1e-2) + IRLS = directives.UpdateIRLS() update_Jacobi = directives.UpdatePreconditioner() + dList = [betaest, update_Jacobi, IRLS] directiveList = directives.DirectiveList(*dList) @@ -51,7 +78,7 @@ def test_validation_initial_beta_fail(self): def test_validation_warning(self): betaest = directives.BetaEstimate_ByEig() - IRLS = directives.Update_IRLS(f_min_change=1e-4, minGNiter=3, beta_tol=1e-2) + IRLS = directives.UpdateIRLS() dList = [betaest, IRLS] directiveList = directives.DirectiveList(*dList) @@ -59,6 +86,16 @@ def test_validation_warning(self): self.assertTrue(directiveList.validate()) +def test_directive_list_iterable(): + directs = [ + directives.UpdateIRLS(), + directives.BetaSchedule(coolingFactor=2, coolingRate=1), + ] + directives_list = directives.DirectiveList(*directs) + for item1, item2 in zip(directives_list, directs): + assert item1 is item2 + + class ValidationInInversion(unittest.TestCase): def setUp(self): mesh = discretize.TensorMesh([4, 4, 4]) @@ -83,13 +120,13 @@ def setUp(self): m = np.random.rand(mesh.nC) - data = sim.make_synthetic_data(m, add_noise=True) - dmis = data_misfit.L2DataMisfit(data=data, simulation=sim) + data = sim.make_synthetic_data(m, add_noise=True, random_seed=19) + dmis = L2DataMisfit(data=data, simulation=sim) dmis.W = 1.0 / data.relative_error # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=2, lower=-10.0, upper=10.0, maxIterCG=2 + maxIter=2, lower=-10.0, upper=10.0, cg_maxiter=2 ) self.model = m @@ -109,21 +146,20 @@ def test_validation_in_inversion(self): betaest = directives.BetaEstimate_ByEig() # Here is where the norms are applied - IRLS = directives.Update_IRLS(f_min_change=1e-4, minGNiter=3, beta_tol=1e-2) - + IRLS = directives.UpdateIRLS(f_min_change=1e-4) update_Jacobi = directives.UpdatePreconditioner() sensitivity_weights = directives.UpdateSensitivityWeights() with self.assertRaises(AssertionError): # validation should happen and this will fail # (IRLS needs to be before update_Jacobi) - inv = inversion.BaseInversion( + inversion.BaseInversion( invProb, directiveList=[betaest, update_Jacobi, IRLS] ) with self.assertRaises(AssertionError): # validation should happen and this will fail # (sensitivity_weights needs to be before betaest) - inv = inversion.BaseInversion( + inversion.BaseInversion( invProb, directiveList=[betaest, sensitivity_weights] ) @@ -258,7 +294,136 @@ def test_sensitivity_weighting_amplitude_minimum(self): # self.test_sensitivity_weighting_subroutine(test_weights, test_directive) - print("SENSITIVITY WEIGHTING BY AMPLIUTDE AND MAX ALUE TEST PASSED") + print("SENSITIVITY WEIGHTING BY AMPLITUDE AND MAX VALUE TEST PASSED") + + def test_irls_directive(self): + input_norms = [0.0, 1.0, 1.0, 1.0] + reg = regularization.Sparse(self.mesh) + reg.norms = input_norms + projection = maps.Projection(self.mesh.n_cells, np.arange(self.mesh.n_cells)) + + other_reg = regularization.WeightedLeastSquares(self.mesh) + + invProb = inverse_problem.BaseInvProblem(self.dmis, reg + other_reg, self.opt) + + beta_schedule = directives.BetaSchedule(coolingFactor=3) + + # Here is where the norms are applied + irls_directive = directives.UpdateIRLS( + cooling_factor=3, + chifact_start=100.0, + chifact_target=1.0, + irls_cooling_factor=1.2, + f_min_change=np.inf, + max_irls_iterations=20, + misfit_tolerance=1e-0, + percentile=100, + verbose=True, + ) + + assert irls_directive.cooling_factor == 3 + assert irls_directive.metrics is not None + + # TODO Move these assertion test to the 'test_validation_in_inversion' after update + with self.assertRaises(AssertionError): + inversion.BaseInversion( + invProb, directiveList=[beta_schedule, irls_directive] + ) + + with self.assertRaises(AssertionError): + inversion.BaseInversion( + invProb, directiveList=[beta_schedule, irls_directive] + ) + + spherical_weights = directives.SphericalUnitsWeights(projection, [reg]) + with self.assertRaises(AssertionError): + inversion.BaseInversion( + invProb, directiveList=[irls_directive, spherical_weights] + ) + + update_Jacobi = directives.UpdatePreconditioner() + with self.assertRaises(AssertionError): + inversion.BaseInversion( + invProb, directiveList=[update_Jacobi, irls_directive] + ) + + invProb.phi_d = 1.0 + self.opt.iter = 3 + invProb.model = np.random.randn(reg.regularization_mesh.nC) + inv = inversion.BaseInversion(invProb, directiveList=[irls_directive]) + + irls_directive.initialize() + assert irls_directive.metrics.input_norms == [input_norms, None] + assert reg.norms == [2.0, 2.0, 2.0, 2.0] + + irls_directive.inversion = inv + irls_directive.endIter() + + assert irls_directive.metrics.start_irls_iter == self.opt.iter + assert len(reg.objfcts[0]._weights) == 2 # With irls weights + assert len(other_reg.objfcts[0]._weights) == 1 # No irls + irls_directive.metrics.irls_iteration_count += 1 + irls_directive.endIter() + + assert self.opt.stopNextIteration + + # Test stopping criteria based on max_irls_iter + irls_directive.max_irls_iterations = 2 + assert irls_directive.stopping_criteria() + + expected_target = self.dmis.nD + # Test beta re-adjustment down + invProb.phi_d = 4.0 + irls_directive.misfit_tolerance = 0.1 + irls_directive.adjust_cooling_schedule() + + ratio = invProb.phi_d / expected_target + expected_factor = harmonic_mean([4 / 3, ratio]) + np.testing.assert_allclose(irls_directive.cooling_factor, expected_factor) + + # Test beta re-adjustment up + invProb.phi_d = 1 / 2 + ratio = invProb.phi_d / expected_target + expected_factor = harmonic_mean([1 / 2, ratio]) + + irls_directive.adjust_cooling_schedule() + np.testing.assert_allclose(irls_directive.cooling_factor, expected_factor) + + # Test beta no-adjustment + irls_directive.cooling_factor = ( + 2.0 # set this to something not 1 to make sure it changes to 1. + ) + + invProb.phi_d = expected_target * ( + 1 + irls_directive.misfit_tolerance * 0.5 + ) # something within the relative tolerance + irls_directive.adjust_cooling_schedule() + assert irls_directive.cooling_factor == 1 + + def test_spherical_weights(self): + reg = regularization.Sparse(self.mesh) + projection = maps.Projection(self.mesh.n_cells, np.arange(self.mesh.n_cells)) + for obj in reg.objfcts[1:]: + obj.units = "radian" + + with pytest.raises(TypeError, match="Attribute 'amplitude' must be of type"): + directives.SphericalUnitsWeights(reg, [reg]) + + with pytest.raises(TypeError, match="Attribute 'angles' must be a list of"): + directives.SphericalUnitsWeights(projection, ["abc"]) + + spherical_weights = directives.SphericalUnitsWeights(projection, [reg]) + + inv_prob = inverse_problem.BaseInvProblem(self.dmis, reg, self.opt) + model = np.abs(np.random.randn(reg.regularization_mesh.nC)) + inv_prob.model = model + inv = inversion.BaseInversion(inv_prob, directiveList=[spherical_weights]) + spherical_weights.inversion = inv + + spherical_weights.initialize() + + assert "angle_scale" not in reg.objfcts[0]._weights + assert reg.objfcts[1]._weights["angle_scale"].max() == model.max() / np.pi def tearDown(self): # Clean up the working directory @@ -276,8 +441,10 @@ def test_save_output_dict(RegClass): sim = simulation.ExponentialSinusoidSimulation( mesh=mesh, model_map=maps.IdentityMap() ) - data = sim.make_synthetic_data(np.ones(mesh.n_cells), add_noise=True) - dmis = data_misfit.L2DataMisfit(data, sim) + data = sim.make_synthetic_data( + np.ones(mesh.n_cells), add_noise=True, random_seed=20 + ) + dmis = L2DataMisfit(data, sim) opt = optimization.InexactGaussNewton(maxIter=1) @@ -306,53 +473,6 @@ def test_save_output_dict(RegClass): assert "x SparseSmoothness.norm" in out_dict -class TestDeprecatedArguments: - """ - Test if directives raise errors after passing deprecated arguments. - """ - - def test_debug(self): - """ - Test if InversionDirective raises error after passing 'debug'. - """ - msg = "'debug' property has been removed. Please use 'verbose'." - with pytest.raises(TypeError, match=msg): - directives.InversionDirective(debug=True) - - -class TestUpdateSensitivityWeightsRemovedArgs: - """ - Test if `UpdateSensitivityWeights` raises errors after passing removed arguments. - """ - - def test_every_iter(self): - """ - Test if `UpdateSensitivityWeights` raises error after passing `everyIter`. - """ - msg = "'everyIter' property has been removed. Please use 'every_iteration'." - with pytest.raises(TypeError, match=msg): - directives.UpdateSensitivityWeights(everyIter=True) - - def test_threshold(self): - """ - Test if `UpdateSensitivityWeights` raises error after passing `threshold`. - """ - msg = "'threshold' property has been removed. Please use 'threshold_value'." - with pytest.raises(TypeError, match=msg): - directives.UpdateSensitivityWeights(threshold=True) - - def test_normalization(self): - """ - Test if `UpdateSensitivityWeights` raises error after passing `normalization`. - """ - msg = ( - "'normalization' property has been removed. " - "Please define normalization using 'normalization_method'." - ) - with pytest.raises(TypeError, match=msg): - directives.UpdateSensitivityWeights(normalization=True) - - class TestUpdateSensitivityNormalization: """ Test the `normalization` property and setter in `UpdateSensitivityWeights` @@ -397,5 +517,787 @@ def test_normalization_method_setter_invalid(self, normalization_method): d_temp.normalization_method = normalization_method +class TestRandomSeedProperty: + """ + Test ``random_seed`` setter methods of directives. + """ + + directive_classes = ( + directives.AlphasSmoothEstimate_ByEig, + directives.BetaEstimate_ByEig, + directives.BetaEstimateMaxDerivative, + directives.ScalingMultipleDataMisfits_ByEig, + ) + + @pytest.mark.parametrize("directive_class", directive_classes) + @pytest.mark.parametrize( + "random_seed", + (42, np.random.default_rng(seed=1), np.array([1, 2])), + ids=("int", "rng", "array"), + ) + def test_valid_seed(self, directive_class, random_seed): + "Test if seed setter works as expected on valid seed arguments." + directive = directive_class(random_seed=random_seed) + assert directive.random_seed is random_seed + + @pytest.mark.parametrize("directive_class", directive_classes) + @pytest.mark.parametrize("random_seed", (42.1, np.array([1.0, 2.0]))) + def test_invalid_seed(self, directive_class, random_seed): + "Test if seed setter works as expected on valid seed arguments." + msg = "Unable to initialize the random number generator with " + with pytest.raises(TypeError, match=msg): + directive_class(random_seed=random_seed) + + +class TestBetaEstimatorArguments: + """ + Test if arguments are assigned in beta estimator directives. + These tests catch the bug described and fixed in #1460. + """ + + def test_beta_estimate_by_eig(self): + """Test on directives.BetaEstimate_ByEig.""" + beta0_ratio = 3.0 + n_pw_iter = 3 + random_seed = 42 + directive = directives.BetaEstimate_ByEig( + beta0_ratio=beta0_ratio, n_pw_iter=n_pw_iter, random_seed=random_seed + ) + assert directive.beta0_ratio == beta0_ratio + assert directive.n_pw_iter == n_pw_iter + assert directive.random_seed == random_seed + + def test_beta_estimate_max_derivative(self): + """Test on directives.BetaEstimateMaxDerivative.""" + beta0_ratio = 3.0 + random_seed = 42 + directive = directives.BetaEstimateMaxDerivative( + beta0_ratio=beta0_ratio, random_seed=random_seed + ) + assert directive.beta0_ratio == beta0_ratio + assert directive.random_seed == random_seed + + +class TestRemovedSeedProperty: + """ + Test removal of seed property. + """ + + CLASSES = ( + directives.AlphasSmoothEstimate_ByEig, + directives.BetaEstimate_ByEig, + directives.BetaEstimateMaxDerivative, + directives.ScalingMultipleDataMisfits_ByEig, + ) + + def get_message_removed_error(self, old_name, new_name, version="v0.24.0"): + msg = ( + f"'{old_name}' has been removed in " + f" SimPEG {version}, please use '{new_name}' instead." + ) + return msg + + @pytest.mark.parametrize("directive", CLASSES) + def test_error_argument(self, directive): + """ + Test if error is raised after passing ``seed`` to the constructor. + """ + msg = self.get_message_removed_error("seed", "random_seed") + with pytest.raises(TypeError, match=msg): + directive(seed=42135) + + @pytest.mark.parametrize("directive", CLASSES) + def test_error_accessing_property(self, directive): + """ + Test error when trying to access the ``seed`` property. + """ + directive_obj = directive(random_seed=42) + msg = "seed has been removed, please use random_seed" + with pytest.raises(NotImplementedError, match=msg): + directive_obj.seed + + +class TestUpdateIRLS: + """ + Additional tests to UpdateIRLS directive. + """ + + @pytest.fixture + def mesh(self): + """Sample tensor mesh.""" + return discretize.TensorMesh([4, 4, 4]) + + @pytest.fixture + def data_misfit(self, mesh): + rx = mag.Point(np.vstack([[0.25, 0.25, 0.25], [-0.25, -0.25, 0.25]])) + igrf = mag.UniformBackgroundField( + receiver_list=[rx], amplitude=5000, inclination=90, declination=0 + ) + survey = mag.Survey(igrf) + sim = mag.Simulation3DIntegral( + mesh, survey=survey, chiMap=maps.IdentityMap(mesh) + ) + model = np.random.default_rng(seed=42).normal(size=mesh.n_cells) + data = sim.make_synthetic_data(model, add_noise=True) + dmisfit = L2DataMisfit(data=data, simulation=sim) + return dmisfit + + def test_end_iter_irls_threshold(self, mesh, data_misfit): + """ + Test if irls_threshold is modified in every regularization term after + the IRLS process started. + """ + # Define a regularization combo with sparse and non-sparse terms + irls_threshold = 4.5 + sparse_regularization = Sparse( + mesh, norms=[1, 1, 1, 1], irls_threshold=irls_threshold + ) + non_sparse_regularization = Smallness(mesh) + reg = 0.1 * sparse_regularization + 0.5 * non_sparse_regularization + # Define inversion + opt = optimization.ProjectedGNCG() + opt.iter = 0 # manually set iter to zero + inv_prob = inverse_problem.BaseInvProblem(data_misfit, reg, opt) + inv_prob.phi_d = np.nan # manually set value for phi_d + inv_prob.model = np.zeros(mesh.n_cells) # manually set the model + # Define inversion + inv = inversion.BaseInversion(inv_prob) + irls_cooling_factor = 1.2 + update_irls = directives.UpdateIRLS( + irls_cooling_factor=irls_cooling_factor, + inversion=inv, + dmisfit=data_misfit, + reg=reg, + ) + # Modify metrics to kick in the IRLS process + update_irls.metrics.start_irls_iter = 0 + # Check irls_threshold of the objective function terms after running endIter + update_irls.endIter() + for obj_fun in sparse_regularization.objfcts: + assert obj_fun.irls_threshold == irls_threshold / irls_cooling_factor + # The irls_threshold for the sparse_regularization should not be changed + assert sparse_regularization.irls_threshold == irls_threshold + + +class DummySaveEveryIteration(directives.SaveEveryIteration): + """ + Dummy non-abstract class to test SaveEveryIteration. + """ + + @property + def file_abs_path(self) -> pathlib.Path: + """ + Simple implementation of abstract property file_abs_path. + """ + return self.directory / self.name + + +class MockOpt: + """Mock Opt object.""" + + def __init__(self, xc=None, maxIter=100): + if xc is None: + xc = np.random.default_rng(seed=42).uniform(size=23) + self.xc = xc + self.maxIter = maxIter + + +class MockInvProb: + """Mock InvProb object.""" + + def __init__(self, opt): + self.opt = opt + + +class MockInversion: + """Mock Inversion object.""" + + def __init__(self, xc=None, maxIter=100): + opt = MockOpt(xc=xc, maxIter=maxIter) + inv_prob = MockInvProb(opt) + self.invProb = inv_prob + + +class TestSaveEveryIteration: + """Test the SaveEveryIteration directive.""" + + @pytest.mark.parametrize("directory", ["dummy/path", "../dummy/path"]) + def test_directory(self, directory): + """Test the directory property.""" + directive = DummySaveEveryIteration(directory=directory) + assert directive.directory == pathlib.Path(directory).resolve() + + def test_no_directory(self): + """Test if the directory property is None when on_disk is False""" + directive = DummySaveEveryIteration(directory="blah", on_disk=False) + assert directive._directory is None + + # accessing the directive property should raise error when on_disk is False + msg = re.escape("directory' is only available") + with pytest.raises(AttributeError, match=msg): + directive.directory + + # using the directive setter should raise error when on_disk is False + + @pytest.mark.parametrize("directory", ["dummy/path", "../dummy/path"]) + def test_directory_setter(self, directory): + """Test the directory setter.""" + directive = DummySaveEveryIteration() + directive.directory = directory + assert directive.directory == pathlib.Path(directory).resolve() + + def test_directory_setter_error_none(self): + """Test error when trying to set directory=None if on_disk is True.""" + directive = DummySaveEveryIteration() + msg = re.escape("Directory is not optional if 'on_disk==True'") + with pytest.raises(ValueError, match=msg): + directive.directory = None + + def test_name(self): + """Test the name property.""" + name = "blah" + directive = DummySaveEveryIteration(name=name) + assert directive.name == name + + def test_name_setter(self): + """Test the name setter.""" + directive = DummySaveEveryIteration() + name = "blah" + directive.name = name + assert directive.name == name + + def test_mkdir(self, tmp_path): + """Test _mkdir_and_check_output_file.""" + directory = tmp_path / "blah" + directive = DummySaveEveryIteration(directory=directory) + directive._mkdir_and_check_output_file() + assert directory.exists() + fname = directory / directive.name + assert not fname.exists() + + @pytest.mark.parametrize( + "should_exist", [True, False], ids=["should_exist", "should_not_exist"] + ) + def test_check_output_file_exists(self, tmp_path, should_exist): + """Test _mkdir_and_check_output_file when file exists.""" + directory = tmp_path / "blah" + directory.mkdir(parents=True) + directive = DummySaveEveryIteration(directory=directory) + fname = directive.file_abs_path + fname.touch() + assert fname.exists() + + if should_exist: + # No warning should be raised if exists and should exist + with warnings.catch_warnings(): + warnings.simplefilter("error") + directive._mkdir_and_check_output_file(should_exist=should_exist) + else: + # Warning should be raised if exists and should not exist + with pytest.warns(UserWarning, match="Overwriting file"): + directive._mkdir_and_check_output_file(should_exist=should_exist) + + @pytest.mark.parametrize( + "should_exist", [True, False], ids=["should_exist", "should_not_exist"] + ) + def test_check_output_file_doesnt_exist(self, tmp_path, should_exist): + """Test _mkdir_and_check_output_file when file doesn't exist.""" + directory = tmp_path / "blah" + directory.mkdir(parents=True) + directive = DummySaveEveryIteration(directory=directory) + fname = directive.file_abs_path + + if should_exist: + # Warning should be raised if doesn't exist and should exist + with pytest.warns( + UserWarning, match=re.escape(f"File {fname} was not found") + ): + directive._mkdir_and_check_output_file(should_exist=should_exist) + else: + # No warning should be raised if doesn't exist and should not exist + with warnings.catch_warnings(): + warnings.simplefilter("error") + directive._mkdir_and_check_output_file(should_exist=should_exist) + + @pytest.mark.parametrize("opt", [True, False], ids=["with-opt", "without-opt"]) + def test_initialize(self, opt): + """ + Test the initialize method. + """ + directive = DummySaveEveryIteration() + if opt: + directive.inversion = MockInversion(maxIter=10000) + + expected_start_time = datetime.now().strftime("%Y-%m-%d-%H-%M") + directive.initialize() + assert directive._start_time == expected_start_time + + if opt: + # maxIter was set to 10000, so the _iter_format should be "05d" + assert directive._iter_format == "05d" + + def test_time_iter_no_opt(self): + directive = DummySaveEveryIteration(name="dummy") + time_name = directive._time_file_name.name + assert directive._time_iter_file_name.name == time_name + "_###" + + def test_deprecated_fileName(self): + directive = DummySaveEveryIteration(name="dummy") + + with pytest.warns(FutureWarning, match=r"'fileName' has been deprecated .*"): + f_name = directive.fileName + + assert f_name == "dummy" + + +class TestSaveModelEveryIteration: + """Test the SaveModelEveryIteration directive.""" + + def test_on_disk(self): + """ + Test on_disk is always True. + """ + directive = directives.SaveModelEveryIteration() + assert directive.on_disk + + def test_on_disk_argument(self): + """ + Test warning after passing on_disk as argument. + """ + msg = re.escape("The 'on_disk' argument is ignored") + with pytest.warns(UserWarning, match=msg): + directive = directives.SaveModelEveryIteration(on_disk=False) + assert directive.on_disk + + def test_on_disk_setter(self): + """ + Test error after trying to modify value of on_disk. + """ + directive = directives.SaveModelEveryIteration() + msg = re.escape("Cannot modify value of 'on_disk'") + with pytest.raises(AttributeError, match=msg): + directive.on_disk = False + + def test_end_iter(self, tmp_path): + """ + Test if endIter saves the model to a file. + """ + directory = tmp_path / "dummy_dir" + directive = directives.SaveModelEveryIteration(directory=directory) + + # Add a mock inversion to the directive + mock_inversion = MockInversion() + directive.inversion = mock_inversion + + # Initialize and call endIter + directive.initialize() + directive.endIter() + + # Check if file exists + assert directory.exists() + assert directive.file_abs_path.exists() + array = np.load(directive.file_abs_path) + + np.testing.assert_equal(array, mock_inversion.invProb.opt.xc) + + +class BaseTestOutputDirective: + """ + Base class to test directives that need a full inversion. + """ + + def get_inversion_problem(self): + """ + Simple gravity inversion problem to test the directive. + """ + # Mesh + # ---- + h = [(1.0, 6)] + mesh = discretize.TensorMesh([h, h, h], origin="CCN") + + # Survey + # ------ + x = np.linspace(-2.0, 2.0, 5) + xx, yy = np.meshgrid(x, x) + zz = 1.0 * np.ones_like(xx) + receiver_locations = np.vstack([c.ravel() for c in (xx, yy, zz)]).T + receivers = gravity.Point(locations=receiver_locations, components="gz") + source_field = gravity.SourceField([receivers]) + survey = gravity.Survey(source_field) + + # Simulation + # ---------- + mapping = simpeg.maps.IdentityMap(mesh=mesh) + simulation = gravity.Simulation3DIntegral( + mesh=mesh, survey=survey, rhoMap=mapping, engine="choclo" + ) + + # Synthetic data + # -------------- + model = np.zeros(mesh.n_cells) + model = simpeg.utils.model_builder.add_block( + mesh.cell_centers, + model, + p0=[-1.0, -1.0, -2.0], + p1=[1.0, 1.0, -1.0], + prop_value=200, + ) + synthetic_data = simulation.make_synthetic_data( + model, + relative_error=0.1, + random_seed=4, + add_noise=True, + ) + + # Inversion problem + # ----------------- + data_misfit = simpeg.data_misfit.L2DataMisfit( + data=synthetic_data, simulation=simulation + ) + regularization = simpeg.regularization.WeightedLeastSquares(mesh) + optimizer = optimization.ProjectedGNCG() + inv_prob = simpeg.inverse_problem.BaseInvProblem( + data_misfit, regularization, optimizer + ) + + return inv_prob + + def get_directives(self, save_output_directive: directives.SaveEveryIteration): + """ + Get list of directives to use in the sample gravity inversion. + + Include the save_output_directive passed as argument in the list. + """ + sensitivity_weights = simpeg.directives.UpdateSensitivityWeights( + every_iteration=False + ) + update_jacobi = simpeg.directives.UpdatePreconditioner( + update_every_iteration=True + ) + starting_beta = simpeg.directives.BetaEstimate_ByEig(beta0_ratio=10) + beta_schedule = simpeg.directives.BetaSchedule(coolingFactor=2.0, coolingRate=1) + target_misfit = simpeg.directives.TargetMisfit(chifact=1.0) + + directives_list = [ + sensitivity_weights, + starting_beta, + update_jacobi, + beta_schedule, + save_output_directive, + target_misfit, + ] + return directives_list + + +class TestSaveOutputEveryIteration(BaseTestOutputDirective): + """ + Test the SaveOutputEveryIteration directive. + """ + + @pytest.mark.parametrize("on_disk", [True, False]) + def test_initialize(self, tmp_path, on_disk): + """Test the initialize method.""" + directory = tmp_path / "dummy" + directive = directives.SaveOutputEveryIteration( + on_disk=on_disk, directory=directory + ) + directive.initialize() + + # Check directory was created + if on_disk: + assert directory.exists() + + # Check that the file was created + assert directive.file_abs_path is not None + assert directive.file_abs_path.exists() + + # Check header in file + with directive.file_abs_path.open(mode="r") as f: + lines = f.readlines() + assert len(lines) == 1 + assert "beta" in lines[0] + assert "phi_d" in lines[0] + assert "phi_m" in lines[0] + else: + assert directive.file_abs_path is None + + assert directive.beta == [] + assert directive.phi_d == [] + assert directive.phi_m == [] + assert directive.phi_m_smooth_z == [] + assert directive.phi == [] + + @pytest.mark.parametrize( + ("on_disk", "test_load_results"), + [ + pytest.param( + True, + True, + marks=pytest.mark.xfail( + reason="bug in load_results", raises=AttributeError + ), + ), + (True, False), + (False, None), + ], + ids=["on_disk-test_load_results", "on_disk", "not_on_disk"], + ) + def test_end_iter(self, tmp_path, on_disk, test_load_results): + """Test the endIter method.""" + inv_prob = self.get_inversion_problem() + + directory = tmp_path / "dummy" + directive = directives.SaveOutputEveryIteration( + directory=directory, on_disk=on_disk + ) + directives_list = self.get_directives(directive) + inversion = simpeg.inversion.BaseInversion(inv_prob, directives_list) + + initial_model = np.zeros(inv_prob.dmisfit.nP) + inversion.run(initial_model) + + # Check that lists are not empty + lists = [ + "beta", + "phi_d", + "phi_m", + "phi_m_small", + "phi_m_smooth_x", + "phi_m_smooth_y", + "phi_m_smooth_z", + "phi", + ] + for attribute in lists: + assert getattr(directive, attribute) + + # Just exit the test if on_disk is False + if not on_disk: + return + + # Check that the file was created if on_disk + assert directive.file_abs_path is not None + assert directive.file_abs_path.exists() + + # Check content of file + with directive.file_abs_path.open(mode="r") as f: + lines = f.readlines() + assert "beta" in lines[0] + assert "phi_d" in lines[0] + assert "phi_m" in lines[0] + assert len(lines) > 1 + + # Test load_results + if test_load_results: + original_values = {attr: getattr(directive, attr) for attr in lists} + for attribute in lists: + # Clean the lists + setattr(directive, attribute, []) + + # Load results and check if they are the same as the original ones + directive.load_results() + + # Check that the original values were recovered + for attribute in lists: + np.testing.assert_equal( + getattr(directive, attribute), + original_values[attribute], + ) + + def test_load_results_error(self, tmp_path): + """ + Test error when no file_name is passed to load_results. + """ + directory = tmp_path / "dummy" + directive = directives.SaveOutputEveryIteration( + directory=directory, on_disk=False + ) + msg = re.escape("'file_name' is a required argument") + with pytest.raises(TypeError, match=msg): + directive.load_results() + + +class TestSaveOutputDictEveryIteration(BaseTestOutputDirective): + """ + Test the SaveOutputDictEveryIteration directive. + """ + + def test_initialize(self): + """Test the initialize method.""" + directive = directives.SaveOutputDictEveryIteration() + directive.initialize() + + # Check outDict was created and is empty + assert hasattr(directive, "outDict") + assert not directive.outDict + + @pytest.mark.parametrize("on_disk", [True, False], ids=["on_disk", "not_on_disk"]) + def test_end_iter(self, tmp_path, on_disk): + """Test the endIter method.""" + inv_prob = self.get_inversion_problem() + + directory = tmp_path / "dummy" + directive = directives.SaveOutputDictEveryIteration( + directory=directory, on_disk=on_disk + ) + directives_list = self.get_directives(directive) + inversion = simpeg.inversion.BaseInversion(inv_prob, directives_list) + + initial_model = np.zeros(inv_prob.dmisfit.nP) + inversion.run(initial_model) + + # Check if the outDict is not empty + assert directive.outDict + fields = [ + "iter", + "beta", + "phi_d", + "phi_m", + "f", + "m", + "dpred", + ] + for iteration in directive.outDict: + for field in fields: + assert field in directive.outDict[iteration] + + # Check if output files were created + if on_disk: + assert directory.exists() + assert directory.is_dir() + files = list(directory.iterdir()) + assert len(files) == len(directive.outDict) + + def test_deprecated(self): + with pytest.warns(FutureWarning, match=".*saveOnDisk has been deprecated.*"): + directive = directives.SaveOutputDictEveryIteration(saveOnDisk=True) + + assert directive.on_disk + + @pytest.mark.parametrize("on_disk", [True, False], ids=["on_disk", "not_on_disk"]) + def test_file_abs_path_optional(self, on_disk): + directive = directives.SaveOutputDictEveryIteration(on_disk=on_disk) + if on_disk: + assert directive.file_abs_path is not None + else: + assert directive.file_abs_path is None + + +class MockJointInvProb: + + def __init__(self): + self.opt = namedtuple("Opt", "f iter cg_count")(0.1, 10, 200) + self.betas = [1, 2, 3] + self.phi_d_list = [0.1, 0.2, 0.3] + self.phi_m_list = [0.2, 0.3, 0.4] + self.lambd = 1e5 + self.phi_sim = 10 + + +class TestSimMeasureSaveOutputEveryIteration: + + @pytest.mark.parametrize("on_disk", [True, False]) + def test_initialize(self, tmp_path, on_disk): + """Test the initialize method.""" + directory = tmp_path / "dummy" + directive = directives.SimilarityMeasureSaveOutputEveryIteration( + on_disk=on_disk, directory=directory + ) + directive.initialize() + + # Check directory was created + if on_disk: + assert directory.exists() + + # Check that the file was created + assert directive.file_abs_path is not None + assert directive.file_abs_path.exists() + + # Check header in file + with directive.file_abs_path.open(mode="r") as f: + lines = f.readlines() + assert len(lines) == 1 + assert "betas" in lines[0] + assert "joint_phi_d" in lines[0] + assert "joint_phi_m" in lines[0] + assert "phi_sim" in lines[0] + else: + assert directive.file_abs_path is None + + assert directive.betas == [] + assert directive.lambd == [] + assert directive.phi_d == [] + assert directive.phi_m == [] + assert directive.phi_sim == [] + assert directive.phi == [] + + @pytest.mark.parametrize("on_disk", [True, False]) + def test_end_iter(self, tmp_path, on_disk): + directory = tmp_path / "dummy" + directive = directives.SimilarityMeasureSaveOutputEveryIteration( + on_disk=on_disk, directory=directory + ) + directive.initialize() + + joint_problem = MockJointInvProb() + joint_inv = namedtuple("JointInversion", "invProb")(joint_problem) + directive.inversion = joint_inv + + assert directive.invProb is joint_inv.invProb + + directive.endIter() + + assert directive.betas == [joint_problem.betas] + assert directive.phi_d == [joint_problem.phi_d_list] + assert directive.phi_m == [joint_problem.phi_m_list] + assert directive.lambd == [joint_problem.lambd] + assert directive.phi_sim == [joint_problem.phi_sim] + assert directive.phi == [joint_problem.opt.f] + + if on_disk: + out_file = directive.file_abs_path + assert out_file.exists() + + n_lines = 0 + with open(out_file) as f: + while f.readline(): + n_lines += 1 + assert n_lines == 2 # header plus one line + + @pytest.mark.xfail( + reason="np.loadtxt will not work to read in log file that has nested lists." + ) + @pytest.mark.parametrize("pass_file_name", [True, False]) + def test_load_results(self, tmp_path, pass_file_name): + directory = tmp_path / "dummy" + directive = directives.SimilarityMeasureSaveOutputEveryIteration( + directory=directory, on_disk=True + ) + directive.initialize() + + joint_problem = MockJointInvProb() + joint_inv = namedtuple("JointInversion", "invProb")(joint_problem) + directive.inversion = joint_inv + + directive.endIter() + + if pass_file_name: + log_file = directive.file_abs_path + directive.load_results(log_file) + else: + directive.load_results() + + assert directive.betas == [joint_problem.betas] + assert directive.phi_d == [joint_problem.phi_d_list] + assert directive.phi_m == [joint_problem.phi_m_list] + assert directive.lambd == [joint_problem.lambd] + assert directive.phi_sim == [joint_problem.phi_sim] + assert directive.phi == [joint_problem.opt.f] + + def test_load_results_error(self): + directive = directives.SimilarityMeasureSaveOutputEveryIteration(on_disk=False) + with pytest.raises(TypeError, match=r"'file_name' is a required argument.*"): + directive.load_results() + + if __name__ == "__main__": unittest.main() diff --git a/tests/base/test_directives_deprecations.py b/tests/base/test_directives_deprecations.py new file mode 100644 index 0000000000..0c930b6b1d --- /dev/null +++ b/tests/base/test_directives_deprecations.py @@ -0,0 +1,18 @@ +""" +Test deprecation of public directives submodules. +""" + +import pytest +import importlib + +REGEX = r"The `simpeg\.directives\.[a-z_]+` submodule has been deprecated, " +DEPRECATED_SUBMODULES = ("directives", "pgi_directives", "sim_directives") + + +@pytest.mark.parametrize("submodule", DEPRECATED_SUBMODULES) +def test_deprecations(submodule): + """ + Test FutureWarning when trying to import the deprecated modules. + """ + with pytest.warns(FutureWarning, match=REGEX): + importlib.import_module(f"simpeg.directives.{submodule}") diff --git a/tests/base/test_inversion.py b/tests/base/test_inversion.py new file mode 100644 index 0000000000..6e19703e9f --- /dev/null +++ b/tests/base/test_inversion.py @@ -0,0 +1,69 @@ +import discretize +import pytest +import numpy as np + +import simpeg.optimization as smp_opt +import simpeg.inversion as smp_inv +import simpeg.directives as smp_drcs +import simpeg.simulation + +from simpeg.inverse_problem import BaseInvProblem + +SIMPEG_OPTIMIZERS = [ + smp_opt.ProjectedGradient, + smp_opt.BFGS, + smp_opt.InexactGaussNewton, + smp_opt.SteepestDescent, + smp_opt.ProjectedGNCG, +] + + +@pytest.fixture(params=SIMPEG_OPTIMIZERS) +def inversion(request): + opt = request.param(maxIter=0) + + mesh = discretize.TensorMesh([10]) + n_d = 5 + sim = simpeg.simulation.ExponentialSinusoidSimulation( + mesh=mesh, + n_kernels=n_d, + model_map=simpeg.maps.IdentityMap(mesh), + ) + m0 = np.zeros(mesh.n_cells) + data = sim.make_synthetic_data( + m0, add_noise=True, relative_error=0, noise_floor=0.1, seed=0 + ) + dmis = simpeg.data_misfit.L2DataMisfit(data, sim) + reg = simpeg.regularization.Smallness(mesh) + + prob = BaseInvProblem(dmis, reg, opt) + return smp_inv.BaseInversion(prob) + + +@pytest.mark.parametrize("dlist", [[], [smp_drcs.UpdatePreconditioner()]]) +def test_bfgs_init_logic(inversion, dlist, caplog, info_logging): + dlist = smp_drcs.DirectiveList(*dlist, inversion=inversion) + inversion.directiveList = dlist + + inv_prb = inversion.invProb + + # Always defaults to trying to initialize bfgs with reg.deriv2 + assert inv_prb.init_bfgs + + m0 = np.zeros(10) + inversion.run(m0) + captured = caplog.text + + if isinstance(inv_prb.opt, smp_opt.InexactGaussNewton) and any( + isinstance(dr, smp_drcs.UpdatePreconditioner) for dr in dlist + ): + assert not inv_prb.init_bfgs + assert "bfgsH0" not in captured + elif isinstance(inv_prb.opt, (smp_opt.BFGS, smp_opt.InexactGaussNewton)): + assert inv_prb.init_bfgs + assert "bfgsH0" in captured + else: + assert inv_prb.init_bfgs # defaults to True even if opt would not use it. + assert ( + "bfgsH0" not in captured + ) # But shouldn't say anything if it doesn't use it. diff --git a/tests/base/test_joint.py b/tests/base/test_joint.py index f856239edc..5ea1467ad9 100644 --- a/tests/base/test_joint.py +++ b/tests/base/test_joint.py @@ -53,8 +53,9 @@ def setUp(self): mesh=mesh, survey=survey1, rhoMap=maps.ExpMap(mesh) ) - dobs0 = simulation0.make_synthetic_data(model) - dobs1 = simulation1.make_synthetic_data(model) + rng = np.random.default_rng(seed=42) + dobs0 = simulation0.make_synthetic_data(model, random_seed=rng) + dobs1 = simulation1.make_synthetic_data(model, random_seed=rng) self.mesh = mesh self.model = model @@ -72,9 +73,9 @@ def setUp(self): self.dmiscombo = self.dmis0 + self.dmis1 def test_multiDataMisfit(self): - self.dmis0.test() - self.dmis1.test() - self.dmiscombo.test(x=self.model) + self.dmis0.test(random_seed=42) + self.dmis1.test(random_seed=42) + self.dmiscombo.test(x=self.model, random_seed=42) def test_inv(self): reg = regularization.WeightedLeastSquares(self.mesh) @@ -97,7 +98,7 @@ def test_inv_mref_setting(self): reg2 = regularization.WeightedLeastSquares(self.mesh) reg = reg1 + reg2 opt = optimization.ProjectedGNCG( - maxIter=30, lower=-10, upper=10, maxIterLS=20, maxIterCG=50, tolCG=1e-4 + maxIter=30, lower=-10, upper=10, maxIterLS=20, cg_maxiter=50, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(self.dmiscombo, reg, opt) directives_list = [ diff --git a/tests/base/test_maps.py b/tests/base/test_maps.py index 4957f8db28..c697ad820a 100644 --- a/tests/base/test_maps.py +++ b/tests/base/test_maps.py @@ -1,3 +1,4 @@ +from copy import deepcopy import numpy as np import unittest import discretize @@ -8,6 +9,13 @@ from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz import inspect +from simpeg.maps._parametric import ( + BaseParametric, + ParametricLayer, + ParametricBlock, + ParametricEllipsoid, +) + TOL = 1e-14 np.random.seed(121) @@ -24,6 +32,7 @@ MAPS_TO_EXCLUDE_2D = [ "ComboMap", "ActiveCells", + "EffectiveSusceptibilityMap", "InjectActiveCells", "LogMap", "LinearMap", @@ -51,6 +60,7 @@ MAPS_TO_EXCLUDE_3D = [ "ComboMap", "ActiveCells", + "EffectiveSusceptibilityMap", "InjectActiveCells", "LogMap", "LinearMap", @@ -129,19 +139,19 @@ def setUp(self): def test_transforms2D(self): for M in self.maps2test2D: - self.assertTrue(M(self.mesh2).test()) + self.assertTrue(M(self.mesh2).test(random_seed=42)) def test_transforms2Dvec(self): for M in self.maps2test2D: - self.assertTrue(M(self.mesh2).test()) + self.assertTrue(M(self.mesh2).test(random_seed=42)) def test_transforms3D(self): for M in self.maps2test3D: - self.assertTrue(M(self.mesh3).test()) + self.assertTrue(M(self.mesh3).test(random_seed=42)) def test_transforms3Dvec(self): for M in self.maps2test3D: - self.assertTrue(M(self.mesh3).test()) + self.assertTrue(M(self.mesh3).test(random_seed=42)) def test_invtransforms2D(self): for M in self.maps2test2D: @@ -184,71 +194,38 @@ def test_invtransforms3D(self): def test_ParametricCasingAndLayer(self): mapping = maps.ParametricCasingAndLayer(self.meshCyl) m = np.r_[-2.0, 1.0, 6.0, 2.0, -0.1, 0.2, 0.5, 0.2, -0.2, 0.2] - self.assertTrue(mapping.test(m)) + self.assertTrue(mapping.test(m=m, random_seed=42)) def test_ParametricBlock2D(self): mesh = discretize.TensorMesh([np.ones(30), np.ones(20)], x0=np.array([-15, -5])) mapping = maps.ParametricBlock(mesh) # val_background,val_block, block_x0, block_dx, block_y0, block_dy m = np.r_[-2.0, 1.0, -5, 10, 5, 4] - self.assertTrue(mapping.test(m)) + self.assertTrue(mapping.test(m=m, random_seed=42)) def test_transforms_logMap_reciprocalMap(self): - # Note that log/reciprocal maps can be kinda finicky, so we are being - # explicit about the random seed. - - v2 = np.r_[ - 0.40077291, 0.1441044, 0.58452314, 0.96323738, 0.01198519, 0.79754415 - ] - dv2 = np.r_[ - 0.80653921, 0.13132446, 0.4901117, 0.03358737, 0.65473762, 0.44252488 - ] - v3 = np.r_[ - 0.96084865, - 0.34385186, - 0.39430044, - 0.81671285, - 0.65929109, - 0.2235217, - 0.87897526, - 0.5784033, - 0.96876393, - 0.63535864, - 0.84130763, - 0.22123854, - ] - dv3 = np.r_[ - 0.96827838, - 0.26072111, - 0.45090749, - 0.10573893, - 0.65276365, - 0.15646586, - 0.51679682, - 0.23071984, - 0.95106218, - 0.14201845, - 0.25093564, - 0.3732866, - ] mapping = maps.LogMap(self.mesh2) - self.assertTrue(mapping.test(v2, dx=dv2)) + self.assertTrue(mapping.test(random_seed=42)) mapping = maps.LogMap(self.mesh3) - self.assertTrue(mapping.test(v3, dx=dv3)) + self.assertTrue(mapping.test(random_seed=42)) mapping = maps.ReciprocalMap(self.mesh2) - self.assertTrue(mapping.test(v2, dx=dv2)) + self.assertTrue(mapping.test(random_seed=42)) mapping = maps.ReciprocalMap(self.mesh3) - self.assertTrue(mapping.test(v3, dx=dv3)) + self.assertTrue(mapping.test(random_seed=42)) + + def test_EffectiveSusceptibilityMap(self): + mapping = maps.EffectiveSusceptibilityMap(50000.0, mesh=self.mesh3) + self.assertTrue(mapping.test(random_seed=42)) def test_Mesh2MeshMap(self): mapping = maps.Mesh2Mesh([self.mesh22, self.mesh2]) - self.assertTrue(mapping.test()) + self.assertTrue(mapping.test(random_seed=42)) def test_Mesh2MeshMapVec(self): mapping = maps.Mesh2Mesh([self.mesh22, self.mesh2]) - self.assertTrue(mapping.test()) + self.assertTrue(mapping.test(random_seed=42)) def test_mapMultiplication(self): M = discretize.TensorMesh([2, 3]) @@ -335,7 +312,7 @@ def test_map2Dto3D_x(self): ]: # m2to3 = maps.Surject2Dto3D(M3, normal='X') m = np.arange(m2to3.nP) - self.assertTrue(m2to3.test()) + self.assertTrue(m2to3.test(random_seed=42)) self.assertTrue( np.all(utils.mkvc((m2to3 * m).reshape(M3.vnC, order="F")[0, :, :]) == m) ) @@ -350,7 +327,7 @@ def test_map2Dto3D_y(self): ]: # m2to3 = maps.Surject2Dto3D(M3, normal='Y') m = np.arange(m2to3.nP) - self.assertTrue(m2to3.test()) + self.assertTrue(m2to3.test(random_seed=42)) self.assertTrue( np.all(utils.mkvc((m2to3 * m).reshape(M3.vnC, order="F")[:, 0, :]) == m) ) @@ -365,7 +342,7 @@ def test_map2Dto3D_z(self): ]: # m2to3 = maps.Surject2Dto3D(M3, normal='Z') m = np.arange(m2to3.nP) - self.assertTrue(m2to3.test()) + self.assertTrue(m2to3.test(random_seed=42)) self.assertTrue( np.all(utils.mkvc((m2to3 * m).reshape(M3.vnC, order="F")[:, :, 0]) == m) ) @@ -373,20 +350,23 @@ def test_map2Dto3D_z(self): def test_ParametricPolyMap(self): M2 = discretize.TensorMesh([np.ones(10), np.ones(10)], "CN") mParamPoly = maps.ParametricPolyMap(M2, 2, logSigma=True, normal="Y") - self.assertTrue(mParamPoly.test(m=np.r_[1.0, 1.0, 0.0, 0.0, 0.0])) + self.assertTrue( + mParamPoly.test(m=np.r_[1.0, 1.0, 0.0, 0.0, 0.0], random_seed=42) + ) def test_ParametricSplineMap(self): M2 = discretize.TensorMesh([np.ones(10), np.ones(10)], "CN") x = M2.cell_centers_x mParamSpline = maps.ParametricSplineMap(M2, x, normal="Y", order=1) - self.assertTrue(mParamSpline.test()) + self.assertTrue(mParamSpline.test(random_seed=42)) def test_parametric_block(self): M1 = discretize.TensorMesh([np.ones(10)], "C") block = maps.ParametricBlock(M1) self.assertTrue( block.test( - m=np.hstack([np.random.rand(2), np.r_[M1.x0, 2 * M1.h[0].min()]]) + m=np.hstack([np.random.rand(2), np.r_[M1.x0, 2 * M1.h[0].min()]]), + random_seed=42, ) ) @@ -400,7 +380,8 @@ def test_parametric_block(self): np.r_[M2.x0[0], 2 * M2.h[0].min()], np.r_[M2.x0[1], 4 * M2.h[1].min()], ] - ) + ), + random_seed=42, ) ) @@ -415,7 +396,8 @@ def test_parametric_block(self): np.r_[M3.x0[1], 4 * M3.h[1].min()], np.r_[M3.x0[2], 5 * M3.h[2].min()], ] - ) + ), + random_seed=42, ) ) @@ -430,7 +412,8 @@ def test_parametric_ellipsoid(self): np.r_[M2.x0[0], 2 * M2.h[0].min()], np.r_[M2.x0[1], 4 * M2.h[1].min()], ] - ) + ), + random_seed=42, ) ) @@ -445,7 +428,8 @@ def test_parametric_ellipsoid(self): np.r_[M3.x0[1], 4 * M3.h[1].min()], np.r_[M3.x0[2], 5 * M3.h[2].min()], ] - ) + ), + random_seed=42, ) ) @@ -471,8 +455,8 @@ def test_sum(self): self.assertTrue(np.all(summap0 * m0 == summap1 * m0)) - self.assertTrue(summap0.test(m0)) - self.assertTrue(summap1.test(m0)) + self.assertTrue(summap0.test(m=m0, random_seed=42)) + self.assertTrue(summap1.test(m=m0, random_seed=42)) def test_surject_units(self): M2 = discretize.TensorMesh([np.ones(10), np.ones(20)], "CC") @@ -486,7 +470,7 @@ def test_surject_units(self): self.assertTrue(np.all(m1[unit1] == 0)) self.assertTrue(np.all(m1[unit2] == 1)) - self.assertTrue(surject_units.test(m0)) + self.assertTrue(surject_units.test(m=m0, random_seed=42)) def test_Projection(self): nP = 10 @@ -508,7 +492,7 @@ def test_Projection(self): maps.Projection(nP, np.r_[10]) * m mapping = maps.Projection(nP, np.r_[1, 2, 6, 1, 3, 5, 4, 9, 9, 8, 0]) - mapping.test() + mapping.test(random_seed=42) def test_Tile(self): """ @@ -634,16 +618,14 @@ class TestSCEMT(unittest.TestCase): def test_sphericalInclusions(self): mesh = discretize.TensorMesh([4, 5, 3]) mapping = maps.SelfConsistentEffectiveMedium(mesh, sigma0=1e-1, sigma1=1.0) - m = np.random.default_rng(seed=0).random(mesh.n_cells) - mapping.test(m=m, dx=0.05 * np.ones(mesh.n_cells), num=3) + mapping.test(num=3, random_seed=42) def test_spheroidalInclusions(self): mesh = discretize.TensorMesh([4, 3, 2]) mapping = maps.SelfConsistentEffectiveMedium( mesh, sigma0=1e-1, sigma1=1.0, alpha0=0.8, alpha1=0.9, rel_tol=1e-8 ) - m = np.abs(np.random.rand(mesh.nC)) - mapping.test(m=m, dx=0.05 * np.ones(mesh.n_cells), num=3) + mapping.test(num=3, random_seed=42) @pytest.mark.parametrize( @@ -690,7 +672,7 @@ def test_LinearMapDerivs(A, b): y1 = mapping.deriv(m) @ v y2 = mapping.deriv(m, v=v) np.testing.assert_equal(y1, y2) - mapping.test() + mapping.test(random_seed=42) def test_LinearMap_errors(): @@ -772,5 +754,362 @@ def test_linearity(): assert all(not m.is_linear for m in non_linear_maps) +class RemovedIndActive: + """Base class to test removed ``actInd`` and ``indActive`` arguments in maps.""" + + @pytest.fixture + def mesh(self): + """Sample mesh.""" + return discretize.TensorMesh([np.ones(10), np.ones(10)], "CN") + + @pytest.fixture + def active_cells(self, mesh): + """Sample active cells for the mesh.""" + active_cells = np.ones(mesh.n_cells, dtype=bool) + active_cells[0] = False + return active_cells + + def get_message_removed_error(self, old_name, new_name, version="v0.24.0"): + msg = ( + f"'{old_name}' was removed in " + f"SimPEG {version}, please use '{new_name}' instead." + ) + return msg + + +class TestParametricPolyMap(RemovedIndActive): + """Test removed ``actInd`` in ParametricPolyMap.""" + + def test_error_argument(self, mesh, active_cells): + """ + Test if error is raised after passing ``actInd`` to the constructor. + """ + msg = "Unsupported keyword argument actInd" + with pytest.raises(TypeError, match=msg): + maps.ParametricPolyMap(mesh, 2, actInd=active_cells) + + def test_error_accessing_property(self, mesh, active_cells): + """ + Test error when trying to access the ``actInd`` property. + """ + mapping = maps.ParametricPolyMap(mesh, 2, active_cells=active_cells) + msg = "actInd has been removed, please use active_cells" + with pytest.raises(NotImplementedError, match=msg): + mapping.actInd + + def test_error_setter(self, mesh, active_cells): + """ + Test error when trying to set the ``actInd`` property. + """ + mapping = maps.ParametricPolyMap(mesh, 2, active_cells=active_cells) + msg = "actInd has been removed, please use active_cells" + with pytest.raises(NotImplementedError, match=msg): + mapping.actInd = active_cells + + +class TestMesh2Mesh(RemovedIndActive): + """Test removed ``indActive`` in ``Mesh2Mesh``.""" + + @pytest.fixture + def meshes(self, mesh): + return [mesh, deepcopy(mesh)] + + def test_error_argument(self, meshes, active_cells): + """ + Test if error is raised after passing ``indActive`` to the constructor. + """ + msg = self.get_message_removed_error("indActive", "active_cells") + with pytest.raises(TypeError, match=msg): + maps.Mesh2Mesh(meshes, indActive=active_cells) + + def test_error_accessing_property(self, meshes, active_cells): + """ + Test error when trying to access the ``indActive`` property. + """ + mapping = maps.Mesh2Mesh(meshes, active_cells=active_cells) + msg = "indActive has been removed, please use active_cells" + with pytest.raises(NotImplementedError, match=msg): + mapping.indActive + + def test_warning_setter(self, meshes, active_cells): + """ + Test warning when trying to set the ``indActive`` property. + """ + mapping = maps.Mesh2Mesh(meshes, active_cells=active_cells) + msg = "indActive has been removed, please use active_cells" + with pytest.raises(NotImplementedError, match=msg): + mapping.indActive = active_cells + + +class TestInjectActiveCells(RemovedIndActive): + """Test removed ``indActive`` and ``valInactive`` in ``InjectActiveCells``.""" + + def test_indactive_error_argument(self, mesh, active_cells): + """ + Test if error is raised after passing ``indActive`` to the constructor. + """ + msg = self.get_message_removed_error("indActive", "active_cells") + with pytest.raises(TypeError, match=msg): + maps.InjectActiveCells(mesh, indActive=active_cells) + + def test_indactive_error_accessing_property(self, mesh, active_cells): + """ + Test error when trying to access the ``indActive`` property. + """ + mapping = maps.InjectActiveCells(mesh, active_cells=active_cells) + msg = "indActive has been removed, please use active_cells" + with pytest.raises(NotImplementedError, match=msg): + mapping.indActive + + def test_indactive_error_setter(self, mesh, active_cells): + """ + Test error when trying to set the ``indActive`` property. + """ + mapping = maps.InjectActiveCells(mesh, active_cells=active_cells) + msg = "indActive has been removed, please use active_cells" + with pytest.raises(NotImplementedError, match=msg): + mapping.indActive = active_cells + + @pytest.mark.parametrize("value_inactive", (3.14, np.array([1]))) + def test_valinactive_error_argument(self, mesh, active_cells, value_inactive): + """ + Test if error is raised after passing ``valInactive`` to the constructor. + """ + msg = self.get_message_removed_error("valInactive", "value_inactive") + with pytest.raises(TypeError, match=msg): + maps.InjectActiveCells( + mesh, active_cells=active_cells, valInactive=value_inactive + ) + + def test_valinactive_error_accessing_property(self, mesh, active_cells): + """ + Test error when trying to access the ``valInactive`` property. + """ + mapping = maps.InjectActiveCells( + mesh, active_cells=active_cells, value_inactive=3.14 + ) + msg = "valInactive has been removed, please use value_inactive" + with pytest.raises(NotImplementedError, match=msg): + mapping.valInactive + + def test_valinactive_error_setter(self, mesh, active_cells): + """ + Test error when trying to set the ``valInactive`` property. + """ + mapping = maps.InjectActiveCells( + mesh, active_cells=active_cells, value_inactive=3.14 + ) + msg = "valInactive has been removed, please use value_inactive" + with pytest.raises(NotImplementedError, match=msg): + mapping.valInactive = 4.5 + + +class TestParametric(RemovedIndActive): + """Test removed ``indActive`` in parametric mappings.""" + + CLASSES = (BaseParametric, ParametricLayer, ParametricBlock, ParametricEllipsoid) + + @pytest.mark.parametrize("map_class", CLASSES) + def test_indactive_error_argument(self, mesh, active_cells, map_class): + """ + Test if error is raised after passing ``indActive`` to the constructor. + """ + msg = self.get_message_removed_error("indActive", "active_cells") + with pytest.raises(TypeError, match=msg): + map_class(mesh, indActive=active_cells) + + @pytest.mark.parametrize("map_class", CLASSES) + def test_indactive_error_accessing_property(self, mesh, active_cells, map_class): + """ + Test error when trying to access the ``indActive`` property. + """ + mapping = map_class(mesh, active_cells=active_cells) + msg = "indActive has been removed, please use active_cells" + with pytest.raises(NotImplementedError, match=msg): + mapping.indActive + + @pytest.mark.parametrize("map_class", CLASSES) + def test_indactive_error_setter(self, mesh, active_cells, map_class): + """ + Test error when trying to set the ``indActive`` property. + """ + mapping = map_class(mesh, active_cells=active_cells) + msg = "indActive has been removed, please use active_cells" + with pytest.raises(NotImplementedError, match=msg): + mapping.indActive = active_cells + + +class TestParametricDeriv: + """ + Test the ``deriv`` method of parametric maps. + + + Test if ``map.deriv(m) @ v`` is equivalent to ``map.deriv(m, v=v)``. + """ + + @pytest.fixture + def mesh_2d(self): + """Sample mesh.""" + h = 10 + return discretize.TensorMesh([h, h], "CC") + + @pytest.fixture + def mesh_3d(self): + """Sample mesh.""" + h = 10 + return discretize.TensorMesh([h, h, h], "CCN") + + @pytest.fixture + def cyl_mesh(self): + """Sample cylindrical mesh.""" + return discretize.CylindricalMesh([4, 6, 5]) + + @pytest.mark.parametrize( + "map_class", + [ + maps.ParametricBlock, + maps.ParametricBlockInLayer, + maps.ParametricEllipsoid, + maps.ParametricLayer, + maps.ParametricPolyMap, + ], + ) + def test_deriv_mesh_3d(self, mesh_3d, map_class): + """ + Test maps on a 3d mesh. + """ + kwargs = {} + if map_class is maps.ParametricPolyMap: + kwargs["order"] = [1, 1] + mapping = map_class(mesh_3d, **kwargs) + model_size = mapping.shape[1] + rng = np.random.default_rng(seed=48) + model = rng.uniform(size=model_size) + v = rng.uniform(size=model_size) + derivative = mapping.deriv(model) + np.testing.assert_allclose(derivative @ v, mapping.deriv(model, v=v)) + + def test_deriv_mesh_2d(self, mesh_2d): + """ + Test maps on a 2d mesh. + """ + mapping = maps.ParametricCircleMap(mesh_2d) + model_size = mapping.shape[1] + rng = np.random.default_rng(seed=48) + model = rng.uniform(size=model_size) + v = rng.uniform(size=model_size) + derivative = mapping.deriv(model) + np.testing.assert_allclose(derivative @ v, mapping.deriv(model, v=v)) + + def test_deriv_cyl_mesh(self, cyl_mesh): + """ + Test maps on a cylindrical mesh. + """ + mapping = maps.ParametricCasingAndLayer(cyl_mesh) + model_size = mapping.shape[1] + rng = np.random.default_rng(seed=48) + model = rng.uniform(size=model_size) + v = rng.uniform(size=model_size) + derivative = mapping.deriv(model) + np.testing.assert_allclose(derivative @ v, mapping.deriv(model, v=v)) + + +def test_deriv_SelfConsistentEffectiveMedium(): + """ + Test deriv method of ``SelfConsistentEffectiveMedium``. + """ + h = 10 + mesh = discretize.TensorMesh([h, h, h], "CCN") + mapping = maps.SelfConsistentEffectiveMedium(mesh, sigma0=1, sigma1=2) + model_size = mapping.shape[1] + rng = np.random.default_rng(seed=48) + model = rng.uniform(size=model_size) + v = rng.uniform(size=model_size) + derivative = mapping.deriv(model) + np.testing.assert_allclose(derivative @ v, mapping.deriv(model, v=v), rtol=1e-6) + + +class TestComplexMapDerivative: + """ + Test deriv method of ComplexMap. + """ + + @pytest.fixture + def mesh(self): + return discretize.TensorMesh([4]) + + @pytest.fixture + def active_cells(self, mesh): + return mesh.cell_centers < 0.5 + + def test_deriv(self, mesh, active_cells): + """ + Test the deriv method. + + Since the mapping is linear, the derivative matrix times a vector should return + the same as evaluating the mapping on the same vector. + """ + n_cells = mesh.n_cells + n_active_cells = np.sum(active_cells) + mapping = maps.ComplexMap(nP=n_active_cells * 2) + m = np.random.default_rng(seed=12).uniform(size=n_cells) + derivative = mapping.deriv(m) + expected = mapping * m + np.testing.assert_allclose(expected, derivative @ m) + + def test_deriv_with_vector(self, mesh, active_cells): + """ + Test the deriv method with a ``v`` argument. + + Since the mapping is linear, the derivative matrix times a vector should return + the same as evaluating the mapping on the same vector. + """ + n_cells = mesh.n_cells + n_active_cells = np.sum(active_cells) + mapping = maps.ComplexMap(nP=n_active_cells * 2) + rng = np.random.default_rng(seed=12) + m = rng.uniform(size=n_cells) + v = rng.uniform(size=n_cells) + derivative = mapping.deriv(m, v=v) + expected = mapping * v + np.testing.assert_allclose(expected, derivative) + + def test_deriv_within_combo(self, mesh, active_cells): + """ + Test the deriv method when being called within a ``ComboMap``. + """ + n_cells = mesh.n_cells + n_active_cells = np.sum(active_cells) + inject_map = maps.InjectActiveCells( + mesh, active_cells=active_cells, value_inactive=0 + ) + complex_map = maps.ComplexMap(nP=n_active_cells * 2) + mapping = inject_map * complex_map + rng = np.random.default_rng(seed=12) + m = rng.uniform(size=n_cells) + expected = mapping * m + derivative = mapping.deriv(m) + np.testing.assert_allclose(expected, derivative @ m) + + def test_deriv_within_combo_with_vector(self, mesh, active_cells): + """ + Test the deriv method when being called within a ``ComboMap`` and ``v`` as an + array. + """ + n_cells = mesh.n_cells + n_active_cells = np.sum(active_cells) + inject_map = maps.InjectActiveCells( + mesh, active_cells=active_cells, value_inactive=0 + ) + complex_map = maps.ComplexMap(nP=n_active_cells * 2) + mapping = inject_map * complex_map + rng = np.random.default_rng(seed=12) + m = rng.uniform(size=n_cells) + v = rng.uniform(size=n_cells) + derivative = mapping.deriv(m, v=v) + expected = mapping * v + np.testing.assert_allclose(expected, derivative) + + if __name__ == "__main__": unittest.main() diff --git a/tests/base/test_model_utils.py b/tests/base/test_model_utils.py index 97802fbf2d..c77d71fe6f 100644 --- a/tests/base/test_model_utils.py +++ b/tests/base/test_model_utils.py @@ -1,16 +1,11 @@ -import pytest -import unittest - import numpy as np - +import pytest from discretize import TensorMesh -from simpeg import ( - utils, -) +from simpeg import utils -class DepthWeightingTest(unittest.TestCase): +class TestDepthWeighting: def test_depth_weighting_3D(self): # Mesh dh = 5.0 @@ -19,19 +14,20 @@ def test_depth_weighting_3D(self): hz = [(dh, 15)] mesh = TensorMesh([hx, hy, hz], "CCN") - actv = np.random.randint(0, 2, mesh.n_cells) == 1 + rng = np.random.default_rng(seed=42) + actv = rng.integers(low=0, high=2, size=mesh.n_cells, dtype=bool) - r_loc = 0.1 # Depth weighting + r_loc = 0.1 wz = utils.depth_weighting( mesh, r_loc, active_cells=actv, exponent=5, threshold=0 ) - reference_locs = ( - np.random.rand(1000, 3) * (mesh.nodes.max(axis=0) - mesh.nodes.min(axis=0)) - + mesh.origin + # Define reference locs at random locations + reference_locs = rng.uniform( + low=mesh.nodes.min(axis=0), high=mesh.nodes.max(axis=0), size=(1000, 3) ) - reference_locs[:, -1] = r_loc + reference_locs[:, -1] = r_loc # set them all at the same elevation wz2 = utils.depth_weighting( mesh, reference_locs, active_cells=actv, exponent=5, threshold=0 @@ -47,8 +43,8 @@ def test_depth_weighting_3D(self): np.testing.assert_allclose(wz, wz2) - with self.assertRaises(ValueError): - wz2 = utils.depth_weighting(mesh, np.random.rand(10, 3, 3)) + with pytest.raises(ValueError): + utils.depth_weighting(mesh, rng.random(size=(10, 3, 3))) def test_depth_weighting_2D(self): # Mesh @@ -57,7 +53,8 @@ def test_depth_weighting_2D(self): hz = [(dh, 15)] mesh = TensorMesh([hx, hz], "CN") - actv = np.random.randint(0, 2, mesh.n_cells) == 1 + rng = np.random.default_rng(seed=42) + actv = rng.integers(low=0, high=2, size=mesh.n_cells, dtype=bool) r_loc = 0.1 # Depth weighting @@ -65,11 +62,11 @@ def test_depth_weighting_2D(self): mesh, r_loc, active_cells=actv, exponent=5, threshold=0 ) - reference_locs = ( - np.random.rand(1000, 2) * (mesh.nodes.max(axis=0) - mesh.nodes.min(axis=0)) - + mesh.origin + # Define reference locs at random locations + reference_locs = rng.uniform( + low=mesh.nodes.min(axis=0), high=mesh.nodes.max(axis=0), size=(1000, 2) ) - reference_locs[:, -1] = r_loc + reference_locs[:, -1] = r_loc # set them all at the same elevation wz2 = utils.depth_weighting( mesh, reference_locs, active_cells=actv, exponent=5, threshold=0 @@ -77,6 +74,109 @@ def test_depth_weighting_2D(self): np.testing.assert_allclose(wz, wz2) +class TestDistancehWeighting: + def test_distance_weighting_3D(self): + # Mesh + dh = 5.0 + hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] + hy = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] + hz = [(dh, 15)] + mesh = TensorMesh([hx, hy, hz], "CCN") + + rng = np.random.default_rng(seed=42) + actv = rng.integers(low=0, high=2, size=mesh.n_cells, dtype=bool) + + # Define reference locs at random locations + reference_locs = rng.uniform( + low=mesh.nodes.min(axis=0), high=mesh.nodes.max(axis=0), size=(1000, 3) + ) + + # distance weighting + with pytest.warns(): + wz_scipy = utils.distance_weighting( + mesh, reference_locs, active_cells=actv, exponent=3, engine="scipy" + ) + wz_numba = utils.distance_weighting( + mesh, reference_locs, active_cells=actv, exponent=3, engine="numba" + ) + np.testing.assert_allclose(wz_scipy, wz_numba) + + with pytest.raises(ValueError): + utils.distance_weighting( + mesh, reference_locs, active_cells=actv, exponent=3, engine="test" + ) + + def test_distance_weighting_2D(self): + # Mesh + dh = 5.0 + hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] + hz = [(dh, 15)] + mesh = TensorMesh([hx, hz], "CN") + + rng = np.random.default_rng(seed=42) + actv = rng.integers(low=0, high=2, size=mesh.n_cells, dtype=bool) + + # Define reference locs at random locations + reference_locs = rng.uniform( + low=mesh.nodes.min(axis=0), high=mesh.nodes.max(axis=0), size=(1000, 2) + ) + + # distance weighting + with pytest.warns(): + wz_scipy = utils.distance_weighting( + mesh, reference_locs, active_cells=actv, exponent=3, engine="scipy" + ) + wz_numba = utils.distance_weighting( + mesh, reference_locs, active_cells=actv, exponent=3, engine="numba" + ) + np.testing.assert_allclose(wz_scipy, wz_numba) + + def test_distance_weighting_1D(self): + # Mesh + dh = 5.0 + hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] + mesh = TensorMesh([hx], "C") + + rng = np.random.default_rng(seed=42) + actv = rng.integers(low=0, high=2, size=mesh.n_cells, dtype=bool) + + # Define reference locs at random locations + reference_locs = rng.uniform( + low=mesh.nodes.min(axis=0), high=mesh.nodes.max(axis=0), size=(1000, 1) + ) + + # distance weighting + with pytest.warns(): + wz_scipy = utils.distance_weighting( + mesh, reference_locs, active_cells=actv, exponent=3, engine="scipy" + ) + wz_numba = utils.distance_weighting( + mesh, reference_locs, active_cells=actv, exponent=3, engine="numba" + ) + np.testing.assert_allclose(wz_scipy, wz_numba) + + @pytest.mark.parametrize("ndim", (2, 3)) + def test_invalid_reference_locs(self, ndim): + """ + Test if errors are raised when invalid reference_locs are passed. + """ + hx = [5.0, 10] + h = [hx] * ndim + origin = "CCN" if ndim == 3 else "CC" + reference_locs = [1.0, 2.0] if ndim == 3 else [1.0] + mesh = TensorMesh(h, origin) + with pytest.raises(ValueError): + utils.distance_weighting(mesh, reference_locs) + + def test_numba_and_cdist_opts_error(self): + """Test error when passing numba and cdist_opts.""" + hx = [5.0, 10] + mesh = TensorMesh([hx, hx, hx]) + msg = "The `cdist_opts` is valid only when engine is 'scipy'." + with pytest.raises(TypeError, match=msg): + utils.distance_weighting(mesh, [1.0, 2.0, 3.0], cdist_opts={"foo": "bar"}) + + @pytest.fixture def mesh(): """Sample mesh.""" @@ -95,7 +195,3 @@ def test_removed_indactive(mesh): msg = "'indActive' argument has been removed. " "Please use 'active_cells' instead." with pytest.raises(TypeError, match=msg): utils.depth_weighting(mesh, 0, indActive=active_cells) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/base/test_objective_function.py b/tests/base/test_objective_function.py index 060dcf907c..24650f64b4 100644 --- a/tests/base/test_objective_function.py +++ b/tests/base/test_objective_function.py @@ -35,7 +35,7 @@ def deriv2(self, m, v=None): class TestBaseObjFct(unittest.TestCase): def test_derivs(self): objfct = objective_function.L2ObjectiveFunction() - self.assertTrue(objfct.test(eps=1e-9)) + self.assertTrue(objfct.test(eps=1e-9, random_seed=42)) def test_deriv2(self): nP = 100 @@ -57,7 +57,7 @@ def test_scalarmul(self): objfct_c = objfct_a + objfct_b self.assertTrue(scalar * objfct_a(m) == objfct_b(m)) - self.assertTrue(objfct_b.test()) + self.assertTrue(objfct_b.test(random_seed=42)) self.assertTrue(objfct_c(m) == objfct_a(m) + objfct_b(m)) self.assertTrue(len(objfct_c.objfcts) == 2) @@ -70,7 +70,7 @@ def test_sum(self): objfct = objective_function.L2ObjectiveFunction( W=sp.eye(nP) ) + scalar * objective_function.L2ObjectiveFunction(W=sp.eye(nP)) - self.assertTrue(objfct.test(eps=1e-9)) + self.assertTrue(objfct.test(eps=1e-9, random_seed=42)) self.assertTrue(np.all(objfct.multipliers == np.r_[1.0, scalar])) @@ -84,7 +84,7 @@ def test_2sum(self): + alpha1 * objective_function.L2ObjectiveFunction() ) phi2 = objective_function.L2ObjectiveFunction() + alpha2 * phi1 - self.assertTrue(phi2.test(eps=EPS)) + self.assertTrue(phi2.test(eps=EPS, random_seed=42)) self.assertTrue(len(phi1.multipliers) == 2) self.assertTrue(len(phi2.multipliers) == 2) @@ -126,7 +126,7 @@ def test_3sum(self): self.assertTrue(len(phi.objfcts) == 3) - self.assertTrue(phi.test()) + self.assertTrue(phi.test(random_seed=42)) def test_sum_fail(self): nP1 = 10 @@ -166,7 +166,7 @@ def test_ZeroObjFct(self): + utils.Zero() * objective_function.L2ObjectiveFunction() ) self.assertTrue(len(phi.objfcts) == 1) - self.assertTrue(phi.test()) + self.assertTrue(phi.test(random_seed=42)) def test_updateMultipliers(self): nP = 10 @@ -257,9 +257,10 @@ def test_Maps(self): self.assertTrue(objfct3(m) == objfct1(m) + objfct2(m)) - objfct1.test() - objfct2.test() - objfct3.test() + seed = 42 + objfct1.test(random_seed=seed) + objfct2.test(random_seed=seed) + objfct3.test(random_seed=seed) def test_ComboW(self): nP = 15 diff --git a/tests/base/test_optimizers.py b/tests/base/test_optimizers.py index 45ef588f25..f8c80bb3c8 100644 --- a/tests/base/test_optimizers.py +++ b/tests/base/test_optimizers.py @@ -1,67 +1,505 @@ -import unittest +import re +import pytest + from simpeg.utils import sdiag import numpy as np +import numpy.testing as npt import scipy.sparse as sp from simpeg import optimization from discretize.tests import get_quadratic, rosenbrock TOL = 1e-2 +OPTIMIZERS = [ + optimization.GaussNewton, + optimization.InexactGaussNewton, + optimization.BFGS, + optimization.ProjectedGradient, + optimization.SteepestDescent, + optimization.ProjectedGNCG, +] + +OPT_KWARGS = { + optimization.GaussNewton: {}, + optimization.InexactGaussNewton: dict(cg_rtol=1e-6, cg_maxiter=100), + optimization.BFGS: dict(maxIter=100, tolG=1e-2, maxIterLS=20), + optimization.ProjectedGradient: dict(maxIter=100, cg_rtol=1e-6, cg_maxiter=100), + optimization.SteepestDescent: dict(maxIter=10000, tolG=1e-5, tolX=1e-8, eps=1e-8), + optimization.ProjectedGNCG: dict(cg_rtol=1e-6, cg_maxiter=100), +} + + +@pytest.mark.parametrize("optimizer", OPTIMIZERS) +@pytest.mark.parametrize( + ("func", "x_true", "x0"), + [ + (rosenbrock, np.array([1.0, 1.0]), np.array([0, 0])), + ( + get_quadratic(sp.identity(2).tocsr(), np.array([-5, 5])), + np.array([5, -5]), + np.zeros(2), + ), + ], + ids=["rosenbrock", "quadratic"], +) +class TestUnboundOptimizers: + + def test_minimizer(self, optimizer, func, x_true, x0): + opt = optimizer(**OPT_KWARGS[optimizer]) + xopt = opt.minimize(func, x0) + npt.assert_allclose(xopt, x_true, rtol=TOL) + + +@pytest.mark.parametrize("optimizer", OPTIMIZERS) +class TestNanInit: + + def test_nan(self, optimizer): + opt = optimizer(maxIter=0) + with pytest.raises(ValueError, match=re.escape("x0 has a nan.")): + opt.minimize(rosenbrock, np.array([np.nan, 0.0])) + + def test_no_nan(self, optimizer): + opt = optimizer(maxIter=0) + opt.minimize(rosenbrock, np.array([0.0, 0.0])) + + +def test_NewtonRoot(): + def fun(x, return_g=True): + if return_g: + return np.sin(x), sdiag(np.cos(x)) + return np.sin(x) + + x = np.array([np.pi - 0.3, np.pi + 0.1, 0]) + xopt = optimization.NewtonRoot(comments=False).root(fun, x) + x_true = np.array([np.pi, np.pi, 0]) + npt.assert_allclose(xopt, x_true, rtol=0, atol=TOL) + + +@pytest.mark.parametrize( + "optimizer", filter(lambda x: issubclass(x, optimization.Bounded), OPTIMIZERS) +) +@pytest.mark.parametrize( + ("lower", "upper", "x_true", "x0"), + [ + (-2, 2, np.array([2.0, -2.0]), np.zeros(2)), + (-2, 8, np.array([5, -2]), np.zeros(2)), + (-8, 2, np.array([2, -5]), np.zeros(2)), + ], + ids=["both active", "lower active", "upper active"], +) +class TestBoundedOptimizers: + def test_minimizer(self, optimizer, lower, upper, x_true, x0): + func = get_quadratic(sp.identity(2).tocsr(), np.array([-5, 5])) + opt = optimizer(lower=lower, upper=upper) + xopt = opt.minimize(func, x0) + npt.assert_allclose(xopt, x_true, rtol=TOL) + + +@pytest.mark.parametrize( + ("x0", "bounded"), + [(np.array([8, 2]), False), (np.array([4, 0]), True)], + ids=["active not bound", "active and bound"], +) +def test_projected_gncg_active_not_bound_branch(x0, bounded): + # tests designed to test the branches of the + # projected gncg when a point is in the active set but not in the binding set. + func = get_quadratic(sp.identity(2).tocsr(), np.array([-5, 5])) + opt = optimization.ProjectedGNCG(upper=8, lower=0) + _, g = func(x0, return_g=True, return_H=False) + + opt.g = g + active = opt.activeSet(x0) + bound = opt.bindingSet(x0) + + # assert that the initial point is what we intend to hit the correct branch + # in the minimizer. + assert not np.any(active & ~bound) is bounded + + xopt = opt.minimize(func, x0) + x_true = np.array([5, 0]) + npt.assert_allclose(xopt, x_true, rtol=TOL) + + +@pytest.mark.parametrize("lower", [None, 0.0, np.zeros(10)]) +@pytest.mark.parametrize("upper", [None, 1.0, np.ones(10)]) +class TestBounded: + + def test_project(self, lower, upper): + x = np.linspace(-9.5, 8.2, 10) + bnd = optimization.Bounded(lower=lower, upper=upper) + + x_proj = bnd.projection(x) + if lower is not None: + assert x_proj.min() == 0.0 + else: + assert x_proj.min() == x.min() + + if upper is not None: + assert x_proj.max() == 1.0 + else: + assert x_proj.max() == x.max() + + def test_active_set(self, lower, upper): + x = np.linspace(-9.5, 8.2, 10) + bnd = optimization.Bounded(lower=lower, upper=upper) + + active_set = bnd.activeSet(x) + + if lower is not None: + assert all(active_set[x <= lower]) + else: + assert not any(active_set[x <= 0]) + + if upper is not None: + assert all(active_set[x >= upper]) + else: + assert not any(active_set[x >= 1]) + + def test_inactive_set(self, lower, upper): + x = np.linspace(-9.5, 8.2, 10) + bnd = optimization.Bounded(lower=lower, upper=upper) + + inactive_set = bnd.inactiveSet(x) + + if lower is not None: + assert not any(inactive_set[x <= lower]) + else: + assert all(inactive_set[x <= 0]) + + if upper is not None: + assert not any(inactive_set[x >= upper]) + else: + assert all(inactive_set[x >= 1]) + + def test_binding_set(self, lower, upper): + x = np.linspace(-9.5, 8.2, 10) + g = (np.ones(5)[:, None] * np.array([-1, 1])).reshape(-1) + assert len(x) == len(g) + assert g[0] == -1 and g[1] == 1 and g[2] == -1 # and so on + bnd = optimization.Bounded(lower=lower, upper=upper) + bnd.g = g + + bnd_set = bnd.bindingSet(x) + + if lower is not None: + assert all(bnd_set[(x <= lower) & (g >= 0)]) + else: + assert not any(bnd_set[(x <= 0) & (g >= 0)]) + + if upper is not None: + assert all(bnd_set[(x >= upper) & (g <= 0)]) + else: + assert not any(bnd_set[(x >= 1) & (g <= 0)]) + + +def test_bounded_kwargs_only(): + with pytest.raises( + TypeError, + match=re.escape( + "Bounded.__init__() takes 1 positional argument but 2 were given" + ), + ): + optimization.Bounded(None) + + +@pytest.mark.parametrize( + ("lower", "upper"), + [ + (np.zeros(11), None), + (None, np.ones(11)), + (np.zeros(11), np.ones(10)), + (np.zeros(10), np.ones(11)), + (np.zeros(11), np.ones(11)), + ], + ids=["only_lower", "only_upper", "bad_lower", "bad_upper", "both_bad"], +) +@pytest.mark.parametrize( + "opt_class", [optimization.ProjectedGradient, optimization.ProjectedGNCG] +) +def test_bad_bounds(lower, upper, opt_class): + m = np.linspace(-9.5, 8.2, 10) + opt = opt_class(lower=lower, upper=upper) + with pytest.raises(RuntimeError, match="Initial model is not projectable"): + opt.startup(m) + + +class TestInexactCGParams: + + def test_defaults(self): + cg_pars = optimization.InexactCG() + assert cg_pars.cg_atol == 0.0 + assert cg_pars.cg_rtol == 1e-1 + assert cg_pars.cg_maxiter == 5 + + def test_init(self): + cg_pars = optimization.InexactCG(cg_rtol=1e-3, cg_atol=1e-5, cg_maxiter=10) + assert cg_pars.cg_atol == 1e-5 + assert cg_pars.cg_rtol == 1e-3 + assert cg_pars.cg_maxiter == 10 + + def test_kwargs_only(self): + with pytest.raises( + TypeError, + match=re.escape( + "InexactCG.__init__() takes 1 positional argument but 2 were given" + ), + ): + optimization.InexactCG(1e-3) + + def test_deprecated(self): + with pytest.warns(FutureWarning, match=".*tolCG has been deprecated.*"): + cg_pars = optimization.InexactCG(tolCG=1e-3) + assert cg_pars.cg_atol == 0.0 + assert cg_pars.cg_rtol == 1e-3 + + with pytest.warns(FutureWarning, match=".*maxIterCG has been deprecated.*"): + cg_pars = optimization.InexactCG(maxIterCG=3) + assert cg_pars.cg_atol == 0.0 + assert cg_pars.cg_rtol == 1e-1 + assert cg_pars.cg_maxiter == 3 + + +class TestProjectedGradient: + + def test_defaults(self): + opt = optimization.ProjectedGradient() + assert opt.cg_rtol == 1e-1 + assert opt.cg_atol == 0.0 + assert opt.cg_maxiter == 5 + assert np.isneginf(opt.lower) + assert np.isposinf(opt.upper) + + def test_init(self): + opt = optimization.ProjectedGradient( + cg_rtol=1e-3, cg_atol=1e-5, cg_maxiter=10, lower=0.0, upper=1.0 + ) + assert opt.cg_rtol == 1e-3 + assert opt.cg_atol == 1e-5 + assert opt.cg_maxiter == 10 + assert opt.lower == 0.0 + assert opt.upper == 1.0 + + def test_kwargs_only(self): + with pytest.raises( + TypeError, + match=re.escape( + "ProjectedGradient.__init__() takes 1 positional argument but 2 were given" + ), + ): + optimization.ProjectedGradient(10) + + @pytest.mark.parametrize("on_init", [True, False], ids=["init", "attribute setter"]) + def test_deprecated_tolCG(self, on_init): + match = ".*tolCG has been deprecated.*cg_rtol.*" + if on_init: + with pytest.warns(FutureWarning, match=match): + opt = optimization.ProjectedGradient(tolCG=1e-3) + else: + opt = optimization.ProjectedGradient() + with pytest.warns(FutureWarning, match=match): + opt.tolCG = 1e-3 + + with pytest.warns(FutureWarning, match=match): + assert opt.tolCG == 1e-3 + assert opt.cg_atol == 0.0 + assert opt.cg_rtol == 1e-3 + + # test setting new changes old + opt.cg_rtol = 1e-4 + + with pytest.warns(FutureWarning, match=match): + assert opt.tolCG == 1e-4 + + @pytest.mark.parametrize("on_init", [True, False], ids=["init", "attribute setter"]) + def test_deprecated_maxIterCG(self, on_init): + + match = ".*maxIterCG has been deprecated.*" + if on_init: + with pytest.warns(FutureWarning, match=match): + opt = optimization.ProjectedGradient(maxIterCG=3) + else: + opt = optimization.ProjectedGradient() + with pytest.warns(FutureWarning, match=match): + opt.maxIterCG = 3 + + with pytest.warns(FutureWarning, match=match): + assert opt.maxIterCG == 3 + + assert opt.cg_maxiter == 3 + + # test setting new changes old + opt.cg_maxiter = 8 + with pytest.warns(FutureWarning, match=match): + assert opt.maxIterCG == 8 + + +class TestInexactGaussNewton: + + def test_defaults(self): + opt = optimization.InexactGaussNewton() + assert opt.cg_rtol == 1e-1 + assert opt.cg_atol == 0.0 + assert opt.cg_maxiter == 5 + + def test_init(self): + opt = optimization.InexactGaussNewton(cg_rtol=1e-3, cg_atol=1e-5, cg_maxiter=10) + assert opt.cg_rtol == 1e-3 + assert opt.cg_atol == 1e-5 + assert opt.cg_maxiter == 10 + + def test_kwargs_only(self): + with pytest.raises( + TypeError, + match=re.escape( + "InexactGaussNewton.__init__() takes 1 positional argument but 2 were given" + ), + ): + optimization.InexactGaussNewton(10) + + @pytest.mark.parametrize("on_init", [True, False], ids=["init", "attribute setter"]) + def test_deprecated_tolCG(self, on_init): + match = ".*tolCG has been deprecated.*cg_rtol.*" + if on_init: + with pytest.warns(FutureWarning, match=match): + opt = optimization.InexactGaussNewton(tolCG=1e-3) + else: + opt = optimization.InexactGaussNewton() + with pytest.warns(FutureWarning, match=match): + opt.tolCG = 1e-3 + + with pytest.warns(FutureWarning, match=match): + assert opt.tolCG == 1e-3 + assert opt.cg_atol == 0.0 + assert opt.cg_rtol == 1e-3 + + # test setting new changes old + opt.cg_rtol = 1e-4 + + with pytest.warns(FutureWarning, match=match): + assert opt.tolCG == 1e-4 + + @pytest.mark.parametrize("on_init", [True, False], ids=["init", "attribute setter"]) + def test_deprecated_maxIterCG(self, on_init): + + match = ".*maxIterCG has been deprecated.*" + if on_init: + with pytest.warns(FutureWarning, match=match): + opt = optimization.InexactGaussNewton(maxIterCG=3) + else: + opt = optimization.InexactGaussNewton() + with pytest.warns(FutureWarning, match=match): + opt.maxIterCG = 3 + + with pytest.warns(FutureWarning, match=match): + assert opt.maxIterCG == 3 + + assert opt.cg_maxiter == 3 + + # test setting new changes old + opt.cg_maxiter = 8 + with pytest.warns(FutureWarning, match=match): + assert opt.maxIterCG == 8 + + +class TestProjectedGNCG: + + @pytest.mark.parametrize("cg_tol_defaults", ["atol", "rtol", "both"]) + def test_defaults(self, cg_tol_defaults): + # testing setting the new default value of rtol if only atol is passed + if cg_tol_defaults == "rtol": + opt = optimization.ProjectedGNCG(cg_atol=1e-5) + assert opt.cg_atol == 1e-5 + assert opt.cg_rtol == 1e-3 + # testing setting the new default value of atol if only rtol is passed + elif cg_tol_defaults == "atol": + opt = optimization.ProjectedGNCG(cg_rtol=1e-4) + assert opt.cg_atol == 0.0 + assert opt.cg_rtol == 1e-4 + # test the old defaults + else: + with pytest.warns( + FutureWarning, match="The defaults for ProjectedGNCG will change.*" + ): + opt = optimization.ProjectedGNCG() + assert opt.cg_rtol == 0.0 + assert opt.cg_atol == 1e-3 + assert opt.cg_maxiter == 5 + assert np.isneginf(opt.lower) + assert np.isposinf(opt.upper) + + def test_init(self): + opt = optimization.ProjectedGNCG( + cg_rtol=1e-3, cg_atol=1e-5, cg_maxiter=10, lower=0.0, upper=1.0 + ) + assert opt.cg_rtol == 1e-3 + assert opt.cg_atol == 1e-5 + assert opt.cg_maxiter == 10 + assert opt.lower == 0.0 + assert opt.upper == 1.0 + + def test_kwargs_only(self): + with pytest.raises( + TypeError, + match=re.escape( + "ProjectedGNCG.__init__() takes 1 positional argument but 2 were given" + ), + ): + optimization.ProjectedGNCG(10) + + @pytest.mark.parametrize("on_init", [True, False], ids=["init", "attribute setter"]) + def test_deprecated_tolCG(self, on_init): + if on_init: + with pytest.warns( + FutureWarning, match=".*tolCG has been deprecated.*cg_atol.*" + ): + opt = optimization.ProjectedGNCG(tolCG=1e-5) + else: + opt = optimization.ProjectedGNCG() + with pytest.warns( + FutureWarning, match=".*tolCG has been deprecated.*cg_atol.*" + ): + opt.tolCG = 1e-5 + + with pytest.warns(FutureWarning, match=".*tolCG has been deprecated.*"): + assert opt.tolCG == 1e-5 + + assert opt.cg_atol == 1e-5 + assert opt.cg_rtol == 0.0 + + # test setting new changes old + opt.cg_atol = 1e-4 + + with pytest.warns(FutureWarning, match=".*tolCG has been deprecated.*"): + assert opt.tolCG == 1e-4 + + @pytest.mark.parametrize("on_init", [True, False], ids=["init", "attribute setter"]) + @pytest.mark.parametrize( + ("old_name", "new_name", "val1", "val2"), + [ + ("maxIterCG", "cg_maxiter", 3, 8), + ("stepActiveSet", "step_active_set", True, False), + ("stepOffBoundsFact", "active_set_grad_scale", 1.2, 1.4), + ], + ids=["maxIterCG", "stepActiveSet", "stepOffBoundsFact"], + ) + def test_deprecated_maxIterCG(self, on_init, old_name, new_name, val1, val2): + + match = f".*{old_name} has been deprecated.*" + if on_init: + with pytest.warns(FutureWarning, match=match): + opt = optimization.ProjectedGNCG(**{old_name: val1}) + else: + opt = optimization.ProjectedGNCG() + with pytest.warns(FutureWarning, match=match): + setattr(opt, old_name, val1) + opt.maxIterCG = 3 + + with pytest.warns(FutureWarning, match=match): + assert getattr(opt, old_name) == val1 + + assert getattr(opt, old_name) == val1 + + setattr(opt, new_name, val2) -class TestOptimizers(unittest.TestCase): - def setUp(self): - self.A = sp.identity(2).tocsr() - self.b = np.array([-5, -5]) - - def test_GN_rosenbrock(self): - GN = optimization.GaussNewton() - xopt = GN.minimize(rosenbrock, np.array([0, 0])) - x_true = np.array([1.0, 1.0]) - print("xopt: ", xopt) - print("x_true: ", x_true) - self.assertTrue(np.linalg.norm(xopt - x_true, 2) < TOL, True) - - def test_GN_quadratic(self): - GN = optimization.GaussNewton() - xopt = GN.minimize(get_quadratic(self.A, self.b), np.array([0, 0])) - x_true = np.array([5.0, 5.0]) - print("xopt: ", xopt) - print("x_true: ", x_true) - self.assertTrue(np.linalg.norm(xopt - x_true, 2) < TOL, True) - - def test_ProjGradient_quadraticBounded(self): - PG = optimization.ProjectedGradient(debug=True) - PG.lower, PG.upper = -2, 2 - xopt = PG.minimize(get_quadratic(self.A, self.b), np.array([0, 0])) - x_true = np.array([2.0, 2.0]) - print("xopt: ", xopt) - print("x_true: ", x_true) - self.assertTrue(np.linalg.norm(xopt - x_true, 2) < TOL, True) - - def test_ProjGradient_quadratic1Bound(self): - myB = np.array([-5, 1]) - PG = optimization.ProjectedGradient() - PG.lower, PG.upper = -2, 2 - xopt = PG.minimize(get_quadratic(self.A, myB), np.array([0, 0])) - x_true = np.array([2.0, -1.0]) - print("xopt: ", xopt) - print("x_true: ", x_true) - self.assertTrue(np.linalg.norm(xopt - x_true, 2) < TOL, True) - - def test_NewtonRoot(self): - def fun(x, return_g=True): - if return_g: - return np.sin(x), sdiag(np.cos(x)) - return np.sin(x) - - x = np.array([np.pi - 0.3, np.pi + 0.1, 0]) - xopt = optimization.NewtonRoot(comments=False).root(fun, x) - x_true = np.array([np.pi, np.pi, 0]) - print("Newton Root Finding") - print("xopt: ", xopt) - print("x_true: ", x_true) - self.assertTrue(np.linalg.norm(xopt - x_true, 2) < TOL, True) - - -if __name__ == "__main__": - unittest.main() + with pytest.warns(FutureWarning, match=match): + assert getattr(opt, old_name) == val2 diff --git a/tests/base/test_problem.py b/tests/base/test_problem.py index a6afd10479..e39608e774 100644 --- a/tests/base/test_problem.py +++ b/tests/base/test_problem.py @@ -1,13 +1,11 @@ import unittest -import discretize from simpeg import simulation import numpy as np class TestTimeSimulation(unittest.TestCase): def setUp(self): - mesh = discretize.TensorMesh([10, 10]) - self.sim = simulation.BaseTimeSimulation(mesh) + self.sim = simulation.BaseTimeSimulation() def test_timeProblem_setTimeSteps(self): self.sim.time_steps = [(1e-6, 3), 1e-5, (1e-4, 2)] diff --git a/tests/base/test_simulation.py b/tests/base/test_simulation.py index 4f49d4c9fa..12578951db 100644 --- a/tests/base/test_simulation.py +++ b/tests/base/test_simulation.py @@ -1,6 +1,9 @@ +import re import unittest import numpy as np import discretize +import pytest + from simpeg import maps, simulation @@ -25,10 +28,47 @@ def test_make_synthetic_data(self): assert np.all(data.relative_error == 0.05 * np.ones_like(dclean)) +def test_exp_sim_mesh_required(): + msg = ".*missing 1 required positional argument: 'mesh'" + with pytest.raises(TypeError, match=msg): + simulation.ExponentialSinusoidSimulation() + + +def test_exp_sim_bad_mesh_type(): + mesh = discretize.TreeMesh([16, 16]) + msg = "mesh must be an instance of TensorMesh, not TreeMesh" + with pytest.raises(TypeError, match=msg): + simulation.ExponentialSinusoidSimulation(mesh) + + +def test_exp_sim_bad_mesh_dim(): + mesh = discretize.TensorMesh([5, 5]) + msg = "ExponentialSinusoidSimulation mesh must be 1D, received a 2D mesh." + with pytest.raises(ValueError, match=msg): + simulation.ExponentialSinusoidSimulation(mesh) + + +@pytest.mark.parametrize( + "base_class", + [ + simulation.BaseSimulation, + simulation.BaseTimeSimulation, + simulation.LinearSimulation, + ], +) +def test_base_no_mesh(base_class): + # this current message is... not a good one + # # but is what is thrown when an invalid arugment is passed. + msg = re.escape( + "object.__init__() takes exactly one argument (the instance to initialize)" + ) + with pytest.raises(TypeError, match=msg): + base_class(mesh=0) + + class TestTimeSimulation(unittest.TestCase): def setUp(self): - mesh = discretize.TensorMesh([10, 10]) - self.sim = simulation.BaseTimeSimulation(mesh=mesh) + self.sim = simulation.BaseTimeSimulation() def test_time_simulation_time_steps(self): self.sim.time_steps = [(1e-6, 3), 1e-5, (1e-4, 2)] diff --git a/tests/base/test_survey.py b/tests/base/test_survey.py new file mode 100644 index 0000000000..83ddc908c2 --- /dev/null +++ b/tests/base/test_survey.py @@ -0,0 +1,34 @@ +""" +Tests for BaseSurvey class. +""" + +import pytest +import numpy as np + +from simpeg.utils import Counter +from simpeg.survey import BaseSurvey, BaseRx, BaseSrc + + +class TestCounterValidation: + + @pytest.fixture + def sample_source(self): + locations = np.array([1.0, 2.0, 3.0]) + receiver = BaseRx(locations=locations) + source = BaseSrc(receiver_list=[receiver]) + return source + + def test_valid_counter(self, sample_source): + """No error should be raise after passing a valid Counter object to Survey.""" + counter = Counter() + BaseSurvey(source_list=[sample_source], counter=counter) + + def test_invalid_counter(self, sample_source): + """Test error upon invalid Counter.""" + + class InvalidCounter: + pass + + invalid_counter = InvalidCounter() + with pytest.raises(TypeError): + BaseSurvey(source_list=[sample_source], counter=invalid_counter) diff --git a/tests/base/test_survey_data.py b/tests/base/test_survey_data.py index 9b06649e07..6b7da90a68 100644 --- a/tests/base/test_survey_data.py +++ b/tests/base/test_survey_data.py @@ -1,7 +1,11 @@ +import re +import pytest import unittest import numpy as np from simpeg import survey, utils, data +from simpeg.survey import BaseRx, BaseSrc, BaseSurvey + np.random.seed(100) @@ -26,35 +30,6 @@ def setUp(self): mysurvey = survey.BaseSurvey(source_list=source_list) self.D = data.Data(mysurvey) - def test_data(self): - V = [] - for src in self.D.survey.source_list: - for rx in src.receiver_list: - v = np.random.rand(rx.nD) - V += [v] - index = self.D.index_dictionary[src][rx] - self.D.dobs[index] = v - V = np.concatenate(V) - self.assertTrue(np.all(V == self.D.dobs)) - - D2 = data.Data(self.D.survey, V) - self.assertTrue(np.all(D2.dobs == self.D.dobs)) - - def test_standard_dev(self): - V = [] - for src in self.D.survey.source_list: - for rx in src.receiver_list: - v = np.random.rand(rx.nD) - V += [v] - index = self.D.index_dictionary[src][rx] - self.D.relative_error[index] = v - self.assertTrue(np.all(v == self.D.relative_error[index])) - V = np.concatenate(V) - self.assertTrue(np.all(V == self.D.relative_error)) - - D2 = data.Data(self.D.survey, relative_error=V) - self.assertTrue(np.all(D2.relative_error == self.D.relative_error)) - def test_uniqueSrcs(self): srcs = self.D.survey.source_list srcs += [srcs[0]] @@ -72,5 +47,171 @@ def test_sourceIndex(self): ) +class BaseFixtures: + @pytest.fixture + def sample_survey(self): + """Create sample Survey object.""" + x = np.linspace(5, 10, 3) + coordinates = utils.ndgrid(x, x, np.r_[0.0]) + source_location = np.r_[0, 0, 0.0] + receivers = [survey.BaseRx(coordinates) for i in range(4)] + sources = [survey.BaseSrc([rx], location=source_location) for rx in receivers] + sources.append(survey.BaseSrc(receivers, location=source_location)) + return survey.BaseSurvey(source_list=sources) + + @pytest.fixture + def sample_data(self, sample_survey): + """Create sample Data object.""" + return data.Data(sample_survey) + + +class TestDataIndexing(BaseFixtures): + """Test indexing of Data object.""" + + def get_source_receiver_pairs(self, survey): + """Return generator for each source-receiver pair in the survey.""" + source_receiver_pairs = ( + (src, rx) for src in survey.source_list for rx in src.receiver_list + ) + return source_receiver_pairs + + def test_getitem(self, sample_data): + """Test the ``Data.__getitem__`` method.""" + # Assign dobs to the data object + dobs = np.random.default_rng(seed=42).uniform(size=sample_data.survey.nD) + sample_data.dobs = dobs + + # Iterate over source-receiver pairs + survey_slices = sample_data.survey.get_all_slices() + for src, rx in self.get_source_receiver_pairs(sample_data.survey): + # Check if the __getitem__ returns the correct slice of the dobs + expected = dobs[survey_slices[src, rx]] + np.testing.assert_allclose(sample_data[src, rx], expected) + + def test_setitem(self, sample_data): + """Test the ``Data.__setitem__`` method.""" + # Assign dobs to the data object + rng = np.random.default_rng(seed=42) + dobs = rng.uniform(size=sample_data.survey.nD) + sample_data.dobs = dobs + + # Override the dobs array for each source-receiver pair + dobs_new = [] + for src, rx in self.get_source_receiver_pairs(sample_data.survey): + _dobs_new_piece = dobs = rng.uniform(size=rx.nD) + sample_data[src, rx] = _dobs_new_piece + dobs_new.append(_dobs_new_piece) + + # Check that the dobs in the data matches the new one + dobs_new = np.hstack(dobs_new) + np.testing.assert_allclose(dobs_new, sample_data.dobs) + + +class TestSurveySlice: + """ + Test BaseSurvey's slices for flat arrays. + """ + + def build_receiver(self, n_locs: int): + locs = np.ones(n_locs)[:, np.newaxis] + return BaseRx(locs) + + @pytest.mark.parametrize( + "all_slices", [True, False], ids=["all_slices", "single_slice"] + ) + def test_single_source(self, all_slices): + """ + Test slicing a survey with a single source. + """ + n_locs = (4, 7) + receivers = [self.build_receiver(n_locs=i) for i in n_locs] + source = BaseSrc(receivers) + test_survey = BaseSurvey([source]) + if all_slices: + expected = { + (source, receivers[0]): slice(0, 4), + (source, receivers[1]): slice(4, 4 + 7), + } + slices = test_survey.get_all_slices() + assert slices == expected + else: + assert test_survey.get_slice(source, receivers[0]) == slice(0, 4) + assert test_survey.get_slice(source, receivers[1]) == slice(4, 4 + 7) + + @pytest.mark.parametrize( + "all_slices", [True, False], ids=["all_slices", "single_slices"] + ) + def test_multiple_sources_shared_receivers(self, all_slices): + """ + Test slicing a survey with multiple sources and shared receivers. + """ + n_locs = (4, 7) + receivers = [self.build_receiver(n_locs=i) for i in n_locs] + sources = [BaseSrc(receivers), BaseSrc(receivers)] + test_survey = BaseSurvey(sources) + if all_slices: + expected = { + (sources[0], receivers[0]): slice(0, 4), + (sources[0], receivers[1]): slice(4, 4 + 7), + (sources[1], receivers[0]): slice(11, 11 + 4), + (sources[1], receivers[1]): slice(15, 15 + 7), + } + slices = test_survey.get_all_slices() + assert slices == expected + else: + assert test_survey.get_slice(sources[0], receivers[0]) == slice(0, 4) + assert test_survey.get_slice(sources[0], receivers[1]) == slice(4, 4 + 7) + assert test_survey.get_slice(sources[1], receivers[0]) == slice(11, 11 + 4) + assert test_survey.get_slice(sources[1], receivers[1]) == slice(15, 15 + 7) + + @pytest.mark.parametrize( + "all_slices", [True, False], ids=["all_slices", "single_slices"] + ) + def test_multiple_sources(self, all_slices): + """ + Test slicing a survey with multiple sources. + """ + receivers_a = [self.build_receiver(n_locs=i) for i in (4, 7)] + receivers_b = [self.build_receiver(n_locs=i) for i in (8, 3)] + srcs = [BaseSrc(receivers_a), BaseSrc(receivers_b)] + test_survey = BaseSurvey(srcs) + if all_slices: + expected = { + (srcs[0], receivers_a[0]): slice(0, 4), + (srcs[0], receivers_a[1]): slice(4, 4 + 7), + (srcs[1], receivers_b[0]): slice(11, 11 + 8), + (srcs[1], receivers_b[1]): slice(19, 19 + 3), + } + slices = test_survey.get_all_slices() + assert slices == expected + else: + assert test_survey.get_slice(srcs[0], receivers_a[0]) == slice(0, 4) + assert test_survey.get_slice(srcs[0], receivers_a[1]) == slice(4, 4 + 7) + assert test_survey.get_slice(srcs[1], receivers_b[0]) == slice(11, 11 + 8) + assert test_survey.get_slice(srcs[1], receivers_b[1]) == slice(19, 19 + 3) + + @pytest.mark.parametrize("missing", ["source", "receiver", "both"]) + def test_missing_source_receiver(self, missing): + """ + Test error on missing source-receiver pair. + """ + # Generate a survey + receivers_a = [self.build_receiver(n_locs=i) for i in (4, 7)] + receivers_b = [self.build_receiver(n_locs=i) for i in (8, 3)] + sources = [BaseSrc(receivers_a), BaseSrc(receivers_b)] + test_survey = BaseSurvey(sources) + # Try to slice with missing source-receiver pair + src, rx = sources[0], receivers_a[1] + if missing in ("source", "both"): + src = BaseSrc() # new src not in the survey + if missing in ("receiver", "both"): + rx = self.build_receiver(1) # new rx not in the survey + msg = re.escape( + f"Source '{src}' and receiver '{rx}' pair " "is not part of the survey." + ) + with pytest.raises(KeyError, match=msg): + test_survey.get_slice(src, rx) + + if __name__ == "__main__": unittest.main() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000..6947cdffd6 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,30 @@ +import pytest +from simpeg.utils import get_logger +import logging + + +@pytest.fixture(scope="session", autouse=True) +def quiet_logger_for_tests(request): + logger = get_logger() + + init_level = logger.level + # default solver log is issued at the INFO level. + # set the logger to the higher WARNING level to + # ignore the default solver messages. + logger.setLevel(logging.WARNING) + + yield + + logger.setLevel(init_level) + + +@pytest.fixture() +def info_logging(): + # provide a fixture to temporarily set the logging level to info + logger = get_logger() + init_level = logger.level + logger.setLevel(logging.INFO) + + yield + + logger.setLevel(init_level) diff --git a/tests/dask/test_DC_jvecjtvecadj_dask.py b/tests/dask/test_DC_jvecjtvecadj_dask.py index a2f32c2e16..7440cadd34 100644 --- a/tests/dask/test_DC_jvecjtvecadj_dask.py +++ b/tests/dask/test_DC_jvecjtvecadj_dask.py @@ -15,8 +15,6 @@ from simpeg.electromagnetics import resistivity as dc import shutil -np.random.seed(40) - TOL = 1e-5 FLR = 1e-20 # "zero", so if residual below this --> pass regardless of order @@ -45,13 +43,13 @@ def setUp(self): ) mSynth = np.ones(mesh.nC) - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -71,6 +69,7 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=54, ) self.assertTrue(passed) @@ -87,7 +86,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=6 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=6, + random_seed=1234, ) self.assertTrue(passed) @@ -123,13 +126,13 @@ def setUp(self): ) mSynth = np.ones(mesh.nC) - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -149,6 +152,7 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=883, ) self.assertTrue(passed) @@ -165,7 +169,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=78523, ) self.assertTrue(passed) diff --git a/tests/dask/test_IP_jvecjtvecadj_dask.py b/tests/dask/test_IP_jvecjtvecadj_dask.py index 73ac660054..6e23a40107 100644 --- a/tests/dask/test_IP_jvecjtvecadj_dask.py +++ b/tests/dask/test_IP_jvecjtvecadj_dask.py @@ -73,8 +73,12 @@ def setUp(self): # Find cells that lie below surface topography ind_active = ds.utils.active_from_xyz(mesh, topo_2d) # Shift electrodes to the surface of discretized topography - dc_data.survey.drape_electrodes_on_topography(mesh, ind_active, option="top") - ip_data.survey.drape_electrodes_on_topography(mesh, ind_active, option="top") + dc_data.survey.drape_electrodes_on_topography( + mesh, ind_active, option="top", shift_horizontal=False + ) + ip_data.survey.drape_electrodes_on_topography( + mesh, ind_active, option="top", shift_horizontal=False + ) # Define conductivity model in S/m (or resistivity model in Ohm m) air_conductivity = 1e-8 @@ -97,7 +101,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=5, maxIter=1, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=5 + maxIterLS=5, maxIter=1, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=5 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -117,6 +121,7 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=66346, ) self.assertTrue(passed) @@ -133,7 +138,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=41, ) self.assertTrue(passed) @@ -162,12 +171,12 @@ def setUp(self): mesh=mesh, survey=survey, sigma=sigma, etaMap=maps.IdentityMap(mesh) ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -187,6 +196,7 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=41, ) self.assertTrue(passed) @@ -203,7 +213,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=41, ) self.assertTrue(passed) @@ -232,12 +246,12 @@ def setUp(self): mesh=mesh, survey=survey, sigma=sigma, etaMap=maps.IdentityMap(mesh) ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -256,6 +270,7 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=41, ) self.assertTrue(passed) @@ -272,7 +287,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=41, ) self.assertTrue(passed) @@ -305,12 +324,12 @@ def setUp(self): storeJ=True, ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -329,6 +348,7 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=41, ) self.assertTrue(passed) @@ -345,7 +365,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=41, ) self.assertTrue(passed) @@ -385,12 +409,12 @@ def setUp(self): storeJ=True, ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -409,6 +433,7 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=41, ) self.assertTrue(passed) @@ -425,7 +450,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=41, ) self.assertTrue(passed) diff --git a/tests/dask/test_grav_inversion_linear.py b/tests/dask/test_grav_inversion_linear.py index df68680167..3b3f97ff02 100644 --- a/tests/dask/test_grav_inversion_linear.py +++ b/tests/dask/test_grav_inversion_linear.py @@ -77,7 +77,7 @@ def setUp(self): self.mesh, survey=survey, rhoMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="ram", chunk_format="row", ) @@ -86,7 +86,11 @@ def setUp(self): # computing sensitivities to ram is best using dask processes with dask.config.set(scheduler="processes"): data = sim.make_synthetic_data( - self.model, relative_error=0.0, noise_floor=0.0005, add_noise=True + self.model, + relative_error=0.0, + noise_floor=0.0005, + add_noise=True, + random_seed=42, ) # Create a regularization reg = regularization.Sparse(self.mesh, active_cells=actv, mapping=idenMap) @@ -98,12 +102,17 @@ def setUp(self): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-4 + maxIter=100, + lower=-1.0, + upper=1.0, + maxIterLS=20, + cg_maxiter=10, + cg_rtol=1e-4, ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e2) # Here is where the norms are applied - IRLS = directives.Update_IRLS(max_irls_iterations=20, chifact_start=2.0) + IRLS = directives.UpdateIRLS() update_Jacobi = directives.UpdatePreconditioner() sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) self.inv = inversion.BaseInversion( diff --git a/tests/dask/test_mag_MVI_Octree.py b/tests/dask/test_mag_MVI_Octree.py index cc3984f2ab..5516589dd5 100644 --- a/tests/dask/test_mag_MVI_Octree.py +++ b/tests/dask/test_mag_MVI_Octree.py @@ -75,15 +75,15 @@ def setUp(self): # Convert the inclination declination to vector in Cartesian M_xyz = utils.mat_utils.dip_azimuth2cartesian(M[0], M[1]) - # Get the indicies of the magnetized block - ind = utils.model_builder.get_indices_block( + # Get the indices of the magnetized block + indices = utils.model_builder.get_indices_block( np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, - )[0] + ) # Assign magnetization values - model[ind, :] = np.kron(np.ones((ind.shape[0], 1)), M_xyz * 0.05) + model[indices, :] = np.kron(np.ones((indices.size, 1)), M_xyz * 0.05) # Remove air cells self.model = model[actv, :] @@ -100,7 +100,7 @@ def setUp(self): survey=survey, model_type="vector", chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="disk", chunk_format="auto", ) @@ -108,7 +108,11 @@ def setUp(self): # Compute some data and add some random noise data = sim.make_synthetic_data( - utils.mkvc(self.model), relative_error=0.0, noise_floor=5.0, add_noise=True + utils.mkvc(self.model), + relative_error=0.0, + noise_floor=5.0, + add_noise=True, + random_seed=40, ) # This Mapping connects the regularizations for the three-component @@ -135,7 +139,7 @@ def setUp(self): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=10, lower=-10, upper=10.0, maxIterLS=5, maxIterCG=5, tolCG=1e-4 + maxIter=10, lower=-10, upper=10.0, maxIterLS=5, cg_maxiter=5, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) @@ -146,8 +150,8 @@ def setUp(self): # Here is where the norms are applied # Use pick a treshold parameter empirically based on the distribution of # model parameters - IRLS = directives.Update_IRLS( - f_min_change=1e-3, max_irls_iterations=0, beta_tol=5e-1 + IRLS = directives.UpdateIRLS( + f_min_change=1e-3, max_irls_iterations=0, misfit_tolerance=5e-1 ) # Pre-conditioner @@ -199,23 +203,23 @@ def setUp(self): lower=Lbound, upper=Ubound, maxIterLS=5, - maxIterCG=5, - tolCG=1e-3, - stepOffBoundsFact=1e-3, + cg_maxiter=5, + cg_rtol=1e-3, + active_set_grad_scale=1e-3, ) opt.approxHinv = None invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=beta) # Here is where the norms are applied - IRLS = directives.Update_IRLS( + IRLS = directives.UpdateIRLS( f_min_change=1e-4, max_irls_iterations=5, - minGNiter=1, - beta_tol=0.5, - coolingRate=1, - coolEps_q=True, - sphericalDomain=True, + misfit_tolerance=0.5, + ) + + spherical_scale = directives.SphericalUnitsWeights( + amplitude=wires.amp, angles=[reg_t, reg_p] ) # Special directive specific to the mag amplitude problem. The sensitivity @@ -226,7 +230,13 @@ def setUp(self): self.inv = inversion.BaseInversion( invProb, - directiveList=[ProjSpherical, IRLS, sensitivity_weights, update_Jacobi], + directiveList=[ + spherical_scale, + ProjSpherical, + IRLS, + sensitivity_weights, + update_Jacobi, + ], ) def test_mag_inverse(self): diff --git a/tests/dask/test_mag_inversion_linear_Octree.py b/tests/dask/test_mag_inversion_linear_Octree.py index 0bf5c4b6e0..e01da350cd 100644 --- a/tests/dask/test_mag_inversion_linear_Octree.py +++ b/tests/dask/test_mag_inversion_linear_Octree.py @@ -107,13 +107,17 @@ def setUp(self): self.mesh, survey=survey, chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="ram", chunk_format="equal", ) self.sim = sim data = sim.make_synthetic_data( - self.model, relative_error=0.0, noise_floor=1.0, add_noise=True + self.model, + relative_error=0.0, + noise_floor=1.0, + add_noise=True, + random_seed=40, ) # Create a regularization @@ -135,9 +139,9 @@ def setUp(self): lower=0.0, upper=10.0, maxIterLS=5, - maxIterCG=10, - tolCG=1e-4, - stepOffBoundsFact=1e-4, + cg_maxiter=10, + cg_rtol=1e-3, + active_set_grad_scale=1e-4, ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e6) @@ -145,7 +149,7 @@ def setUp(self): # Here is where the norms are applied # Use pick a treshold parameter empirically based on the distribution of # model parameters - IRLS = directives.Update_IRLS() + IRLS = directives.UpdateIRLS() update_Jacobi = directives.UpdatePreconditioner() sensitivity_weights = directives.UpdateSensitivityWeights() self.inv = inversion.BaseInversion( diff --git a/tests/dask/test_mag_nonLinear_Amplitude.py b/tests/dask/test_mag_nonLinear_Amplitude.py index eb1775c032..8da94479b1 100644 --- a/tests/dask/test_mag_nonLinear_Amplitude.py +++ b/tests/dask/test_mag_nonLinear_Amplitude.py @@ -83,7 +83,7 @@ def setUp(self): np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, - )[0] + ) # Assign magnetization value, inducing field strength will # be applied in by the :class:`simpeg.PF.Magnetics` problem @@ -101,7 +101,7 @@ def setUp(self): survey=survey, mesh=mesh, chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="forward_only", ) simulation.M = M_xyz @@ -135,7 +135,7 @@ def setUp(self): mesh=mesh, survey=survey, chiMap=idenMap, - ind_active=surf, + active_cells=surf, store_sensitivities="ram", ) simulation.model = mstart @@ -152,8 +152,8 @@ def setUp(self): lower=-np.inf, upper=np.inf, maxIterLS=5, - maxIterCG=5, - tolCG=1e-3, + cg_maxiter=5, + cg_rtol=1e-3, ) # Define misfit function (obs-calc) @@ -167,8 +167,8 @@ def setUp(self): # Target misfit to stop the inversion, # try to fit as much as possible of the signal, we don't want to lose anything - IRLS = directives.Update_IRLS( - f_min_change=1e-3, minGNiter=1, beta_tol=1e-1, max_irls_iterations=5 + IRLS = directives.UpdateIRLS( + f_min_change=1e-3, misfit_tolerance=1e-1, max_irls_iterations=5 ) update_Jacobi = directives.UpdatePreconditioner() # Put all the parts together @@ -201,7 +201,7 @@ def setUp(self): mesh=mesh, survey=surveyAmp, chiMap=idenMap, - ind_active=surf, + active_cells=surf, is_amplitude_data=True, store_sensitivities="forward_only", ) @@ -228,7 +228,7 @@ def setUp(self): survey=surveyAmp, mesh=mesh, chiMap=idenMap, - ind_active=actv, + active_cells=actv, is_amplitude_data=True, ) @@ -244,7 +244,7 @@ def setUp(self): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=10, lower=0.0, upper=1.0, maxIterLS=5, maxIterCG=5, tolCG=1e-3 + maxIter=10, lower=0.0, upper=1.0, maxIterLS=5, cg_maxiter=5, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) @@ -253,13 +253,7 @@ def setUp(self): betaest = directives.BetaEstimate_ByEig(beta0_ratio=1) # Specify the sparse norms - IRLS = directives.Update_IRLS( - max_irls_iterations=5, - f_min_change=1e-3, - minGNiter=1, - coolingRate=1, - beta_search=False, - ) + IRLS = directives.UpdateIRLS(max_irls_iterations=5, f_min_change=1e-3) # Special directive specific to the mag amplitude problem. The sensitivity # weights are update between each iteration. @@ -268,7 +262,13 @@ def setUp(self): # Put all together self.inv = inversion.BaseInversion( - invProb, directiveList=[update_SensWeight, betaest, IRLS, update_Jacobi] + invProb, + directiveList=[ + update_SensWeight, + betaest, + IRLS, + update_Jacobi, + ], ) self.mstart = mstart diff --git a/tests/em/em1d/test_EM1D_FD_fwd.py b/tests/em/em1d/test_EM1D_FD_fwd.py index 8a62a1398f..88a7498d5a 100644 --- a/tests/em/em1d/test_EM1D_FD_fwd.py +++ b/tests/em/em1d/test_EM1D_FD_fwd.py @@ -9,6 +9,7 @@ vertical_magnetic_field_horizontal_loop as mag_field, ) import empymod +import pytest class EM1D_FD_test_failures(unittest.TestCase): @@ -548,5 +549,62 @@ def solution(res): self.assertLess(err, 1e-4) +@pytest.mark.parametrize( + "rx_class", + [fdem.receivers.PointMagneticField, fdem.receivers.PointMagneticFieldSecondary], +) +@pytest.mark.parametrize("n_locs1", [1, 4]) +@pytest.mark.parametrize("n_locs2", [1, 4]) +@pytest.mark.parametrize("orientation", ["x", "y", "z"]) +@pytest.mark.parametrize("component", ["real", "imag", "both"]) +def test_rx_loc_shapes(rx_class, n_locs1, n_locs2, orientation, component): + offsets = np.full(n_locs1, 100.0) + rx1_locs = np.pad(offsets[:, None], ((0, 0), (0, 2)), constant_values=0) + offsets = np.full(n_locs2, 100.0) + rx2_locs = np.pad(offsets[:, None], ((0, 0), (0, 2)), constant_values=0) + + rx_list = [ + rx_class(rx1_locs, orientation=orientation, component=component), + rx_class(rx2_locs, orientation=orientation, component=component), + ] + n_d = n_locs1 + n_locs2 + if component == "both": + n_d *= 2 + + src = fdem.sources.MagDipole(rx_list, frequency=0.1) + srv = fdem.Survey(src) + + sim = fdem.Simulation1DLayered(survey=srv, sigma=[1]) + d = sim.dpred(None) + + # assert the shape is correct + assert d.shape == (n_d,) + + # every value should be the same... + d1 = d[srv.get_slice(src, rx_list[0])] + d2 = d[srv.get_slice(src, rx_list[1])] + + if component == "both": + d1 = d1[::2] + 1j * d1[1::2] + d2 = d2[::2] + 1j * d2[1::2] + d = np.r_[d1, d2] + np.testing.assert_allclose(d, d[0], rtol=1e-12) + + sim.sigmaMap = maps.IdentityMap(nP=1) + # make sure forming J works + J = sim.getJ(np.ones(1)) + assert J.shape == (n_d, 1) + + # and all of its values are the same too: + j1 = J[srv.get_slice(src, rx_list[0]), 0] + j2 = J[srv.get_slice(src, rx_list[1]), 0] + + if component == "both": + j1 = j1[::2] + 1j * j1[1::2] + j2 = j2[::2] + 1j * j2[1::2] + J = np.r_[j1, j2] + np.testing.assert_allclose(J, J[0], rtol=1e-12) + + if __name__ == "__main__": unittest.main() diff --git a/tests/em/em1d/test_EM1D_FD_getJ.py b/tests/em/em1d/test_EM1D_FD_getJ.py new file mode 100644 index 0000000000..cf18744e2f --- /dev/null +++ b/tests/em/em1d/test_EM1D_FD_getJ.py @@ -0,0 +1,107 @@ +""" +Test the getJ method of FDEM 1D simulation. +""" + +import numpy as np +import simpeg.electromagnetics.frequency_domain as fdem +from simpeg import maps + + +def create_simulation_and_conductivities(identity_mapping: bool): + # Create Survey + # ------------- + # Source properties + frequencies = np.r_[382, 1822, 7970, 35920, 130100] # frequencies in Hz + source_location = np.array([0.0, 0.0, 30.0]) # (3, ) numpy.array_like + source_orientation = "z" # "x", "y" or "z" + moment = 1.0 # dipole moment in Am^2 + + # Receiver properties + receiver_locations = np.array([10.0, 0.0, 30.0]) # or (N, 3) numpy.ndarray + receiver_orientation = "z" # "x", "y" or "z" + data_type = "ppm" # "secondary", "total" or "ppm" + + source_list = [] # create empty list for source objects + + # loop over all sources + for freq in frequencies: + # Define receivers that measure real and imaginary component + # magnetic field data in ppm. + receiver_list = [] + receiver_list.append( + fdem.receivers.PointMagneticFieldSecondary( + receiver_locations, + orientation=receiver_orientation, + data_type=data_type, + component="real", + ) + ) + receiver_list.append( + fdem.receivers.PointMagneticFieldSecondary( + receiver_locations, + orientation=receiver_orientation, + data_type=data_type, + component="imag", + ) + ) + + # Define a magnetic dipole source at each frequency + source_list.append( + fdem.sources.MagDipole( + receiver_list=receiver_list, + frequency=freq, + location=source_location, + orientation=source_orientation, + moment=moment, + ) + ) + + # Define the survey + survey = fdem.survey.Survey(source_list) + + # Defining a 1D Layered Earth Model + # --------------------------------- + # Define layer thicknesses (m) + thicknesses = np.array([20.0, 40.0]) + + # Define layer conductivities (S/m) + conductivities = np.r_[0.1, 1.0, 0.1] + + # Define a mapping + n_layers = len(conductivities) + model_mapping = ( + maps.IdentityMap(nP=n_layers) if identity_mapping else maps.ExpMap(nP=n_layers) + ) + + # Define the Forward Simulation, Predict Data and Plot + # ---------------------------------------------------- + simulation = fdem.Simulation1DLayered( + survey=survey, + thicknesses=thicknesses, + sigmaMap=model_mapping, + ) + + return simulation, conductivities + + +def test_getJ(): + """ + Test if getJ returns different J matrices after passing different maps. + """ + dpreds, jacobians = [], [] + + # Compute dpred and J using an identity map and an exp map + for identity_mapping in (True, False): + simulation, conductivities = create_simulation_and_conductivities( + identity_mapping + ) + model = conductivities if identity_mapping else np.log(conductivities) + dpreds.append(simulation.dpred(model)) + jac = simulation.getJ(model) + jacobians.append(jac) + + # The two dpreds should be equal + assert np.allclose(*dpreds) + + # The two J matrices should not be equal + assert not np.allclose(*jacobians, atol=0.0) diff --git a/tests/em/em1d/test_EM1D_FD_jac_layers.py b/tests/em/em1d/test_EM1D_FD_jac_layers.py index 432b647a64..0364ae3992 100644 --- a/tests/em/em1d/test_EM1D_FD_jac_layers.py +++ b/tests/em/em1d/test_EM1D_FD_jac_layers.py @@ -118,7 +118,7 @@ def derChk(m): dm = m_1D * 0.5 passed = tests.check_derivative( - derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=9186724 ) self.assertTrue(passed) if passed: @@ -164,7 +164,9 @@ def misfit(m, dobs): def derChk(m): return misfit(m, dobs) - passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=2345 + ) self.assertTrue(passed) if passed: print("EM1DFM MagDipole Jtvec test works") @@ -279,7 +281,7 @@ def derChk(m): dm = m_1D * 0.5 passed = tests.check_derivative( - derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=664 ) self.assertTrue(passed) if passed: @@ -325,7 +327,9 @@ def misfit(m, dobs): def derChk(m): return misfit(m, dobs) - passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=42 + ) self.assertTrue(passed) if passed: print("EM1DFM Circular Loop Jtvec test works") @@ -420,7 +424,7 @@ def derChk(m): return [fwdfun(m), lambda mx: jacfun(m, mx)] passed = tests.check_derivative( - derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=1123 ) self.assertTrue(passed) if passed: @@ -463,7 +467,9 @@ def misfit(m, dobs): def derChk(m): return misfit(m, dobs) - passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=124 + ) self.assertTrue(passed) if passed: print("EM1DFM Piecewise Linear Loop Jtvec test works") diff --git a/tests/em/em1d/test_EM1D_FD_jac_layers2.py b/tests/em/em1d/test_EM1D_FD_jac_layers2.py new file mode 100644 index 0000000000..dbbe2154b8 --- /dev/null +++ b/tests/em/em1d/test_EM1D_FD_jac_layers2.py @@ -0,0 +1,490 @@ +from simpeg import maps +from discretize import tests, TensorMesh +import simpeg.electromagnetics.frequency_domain as fdem +import numpy as np +from scipy.constants import mu_0 +from scipy.sparse import diags + + +class TestEM1D_FD_Jacobian_MagDipole: + + # Tests 2nd order convergence of Jvec and Jtvec for magnetic dipole sources. + # - All src and rx orientations + # - All rx components + # - Span many frequencies + # - Tests derivatives wrt sigma, mu, thicknesses and h + def setup_class(self): + # Layers and topography + nearthick = np.logspace(-1, 1, 5) + deepthick = np.logspace(1, 2, 10) + thicknesses = np.r_[nearthick, deepthick] + topo = np.r_[0.0, 0.0, 100.0] + + # Survey Geometry + height = 1e-5 + src_location = np.array([0.0, 0.0, 100.0 + height]) + rx_location = np.array([5.0, 5.0, 100.0 + height]) + frequencies = np.logspace(1, 8, 9) + orientations = ["x", "y", "z"] + components = ["real", "imag", "both"] + + # Define sources and receivers + source_list = [] + for f in frequencies: + for tx_orientation in orientations: + receiver_list = [] + + for rx_orientation in orientations: + for comp in components: + receiver_list.append( + fdem.receivers.PointMagneticFieldSecondary( + rx_location, orientation=rx_orientation, component=comp + ) + ) + + source_list.append( + fdem.sources.MagDipole( + receiver_list, + frequency=f, + location=src_location, + orientation=tx_orientation, + ) + ) + + # Survey + survey = fdem.Survey(source_list) + + self.topo = topo + self.survey = survey + self.showIt = False + self.height = height + self.frequencies = frequencies + self.thicknesses = thicknesses + self.nlayers = len(thicknesses) + 1 + + wire_map = maps.Wires( + ("mu", self.nlayers), + ("sigma", self.nlayers), + ("h", 1), + ("thicknesses", self.nlayers - 1), + ) + self.sigma_map = maps.ExpMap(nP=self.nlayers) * wire_map.sigma + self.mu_map = maps.ExpMap(nP=self.nlayers) * wire_map.mu + self.thicknesses_map = maps.ExpMap(nP=self.nlayers - 1) * wire_map.thicknesses + nP = len(source_list) + surject_mesh = TensorMesh([np.ones(nP)]) + self.h_map = maps.SurjectFull(surject_mesh) * maps.ExpMap(nP=1) * wire_map.h + + sim = fdem.Simulation1DLayered( + survey=self.survey, + sigmaMap=self.sigma_map, + muMap=self.mu_map, + thicknessesMap=self.thicknesses_map, + hMap=self.h_map, + topo=self.topo, + ) + + self.sim = sim + + def test_EM1DFDJvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 2 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[3] = mu_blk + + # General model + m_1D = np.r_[ + np.log(mu), np.log(sig), np.log(self.height), np.log(self.thicknesses) + ] + + def fwdfun(m): + resp = self.sim.dpred(m) + return resp + # return Hz + + def jacfun(m, dm): + Jvec = self.sim.Jvec(m, dm) + return Jvec + + def derChk(m): + return [fwdfun(m), lambda mx: jacfun(m, mx)] + + dm = m_1D * 0.5 + + passed = tests.check_derivative( + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=9186724 + ) + assert passed + + def test_EM1DFDJtvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 2 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[3] = mu_blk + + # General model + m_true = np.r_[ + np.log(mu), np.log(sig), np.log(self.height), np.log(self.thicknesses) + ] + + dobs = self.sim.dpred(m_true) + + m_ini = np.r_[ + np.log(np.ones(self.nlayers) * 1.5 * mu_half), + np.log(np.ones(self.nlayers) * sigma_half), + np.log(0.5 * self.height), + np.log(self.thicknesses) * 0.9, + ] + resp_ini = self.sim.dpred(m_ini) + dr = resp_ini - dobs + + def misfit(m, dobs): + dpred = self.sim.dpred(m) + misfit = np.linalg.norm(dpred - dobs) ** 2 + dmisfit = 2.0 * self.sim.Jtvec( + m, dr + ) # derivative of ||dpred - dobs||^2 gives factor of 2 + return misfit, dmisfit + + def derChk(m): + return misfit(m, dobs) + + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=2345 + ) + assert passed + + def test_jtjdiag(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 2 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[3] = mu_blk + + # General model + model = np.r_[ + np.log(mu), np.log(sig), np.log(self.height), np.log(self.thicknesses) + ] + + rng = np.random.default_rng(seed=42) + weights_matrix = diags(rng.random(size=self.sim.survey.nD)) + jtj_diag = self.sim.getJtJdiag(model, W=weights_matrix) + + J = self.sim.getJ(model) + expected = np.diag(J.T @ weights_matrix.T @ weights_matrix @ J) + np.testing.assert_allclose(expected, jtj_diag) + + +class TestEM1D_FD_Jacobian_CircularLoop: + # Tests 2nd order convergence of Jvec and Jtvec for horizontal loop sources. + # - All rx orientations + # - All rx components + # - Span many frequencies + # - Tests derivatives wrt sigma, mu, thicknesses and h + def setup_class(self): + nearthick = np.logspace(-1, 1, 5) + deepthick = np.logspace(1, 2, 10) + thicknesses = np.r_[nearthick, deepthick] + topo = np.r_[0.0, 0.0, 100.0] + height = 1e-5 + + src_location = np.array([0.0, 0.0, 100.0 + height]) + rx_location = np.array([0.0, 0.0, 100.0 + height]) + frequencies = np.logspace(1, 8, 9) + orientations = ["x", "y", "z"] + components = ["real", "imag", "both"] + I = 1.0 + a = 10.0 + + # Define sources and receivers + source_list = [] + for f in frequencies: + receiver_list = [] + + for rx_orientation in orientations: + for comp in components: + receiver_list.append( + fdem.receivers.PointMagneticFieldSecondary( + rx_location, orientation=rx_orientation, component=comp + ) + ) + + source_list.append( + fdem.sources.CircularLoop( + receiver_list, f, src_location, radius=a, current=I + ) + ) + + # Survey + survey = fdem.Survey(source_list) + + self.topo = topo + self.survey = survey + self.showIt = False + self.height = height + self.frequencies = frequencies + self.thicknesses = thicknesses + self.nlayers = len(thicknesses) + 1 + + nP = len(source_list) + + wire_map = maps.Wires( + ("sigma", self.nlayers), + ("mu", self.nlayers), + ("thicknesses", self.nlayers - 1), + ("h", 1), + ) + self.sigma_map = maps.ExpMap(nP=self.nlayers) * wire_map.sigma + self.mu_map = maps.ExpMap(nP=self.nlayers) * wire_map.mu + self.thicknesses_map = maps.ExpMap(nP=self.nlayers - 1) * wire_map.thicknesses + surject_mesh = TensorMesh([np.ones(nP)]) + self.h_map = maps.SurjectFull(surject_mesh) * maps.ExpMap(nP=1) * wire_map.h + + sim = fdem.Simulation1DLayered( + survey=self.survey, + sigmaMap=self.sigma_map, + muMap=self.mu_map, + thicknessesMap=self.thicknesses_map, + hMap=self.h_map, + topo=self.topo, + ) + + self.sim = sim + + def test_EM1DFDJvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 2 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[3] = mu_blk + + # General model + m_1D = np.r_[ + np.log(sig), np.log(mu), np.log(self.thicknesses), np.log(self.height) + ] + + def fwdfun(m): + resp = self.sim.dpred(m) + return resp + # return Hz + + def jacfun(m, dm): + Jvec = self.sim.Jvec(m, dm) + return Jvec + + def derChk(m): + return [fwdfun(m), lambda mx: jacfun(m, mx)] + + dm = m_1D * 0.5 + + passed = tests.check_derivative( + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=664 + ) + assert passed + + def test_EM1DFDJtvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 2 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[3] = mu_blk + + # General model + m_true = np.r_[ + np.log(sig), np.log(mu), np.log(self.thicknesses), np.log(self.height) + ] + + dobs = self.sim.dpred(m_true) + + m_ini = np.r_[ + np.log(np.ones(self.nlayers) * sigma_half), + np.log(np.ones(self.nlayers) * 1.5 * mu_half), + np.log(self.thicknesses) * 0.9, + np.log(0.5 * self.height), + ] + resp_ini = self.sim.dpred(m_ini) + dr = resp_ini - dobs + + def misfit(m, dobs): + dpred = self.sim.dpred(m) + misfit = np.linalg.norm(dpred - dobs) ** 2 + dmisfit = 2 * self.sim.Jtvec( + m, dr + ) # derivative of ||dpred - dobs||^2 gives factor of 2 + return misfit, dmisfit + + def derChk(m): + return misfit(m, dobs) + + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=42 + ) + assert passed + + +class TestEM1D_FD_Jacobian_LineCurrent: + # Tests 2nd order convergence of Jvec and Jtvec for piecewise linear loop. + # - All rx orientations + # - All rx components + # - Span many frequencies + # - Tests derivatives wrt sigma, mu, thicknesses and h + def setup_class(self): + x_path = np.array([-2, -2, 2, 2, -2]) + y_path = np.array([-1, 1, 1, -1, -1]) + frequencies = np.logspace(0, 4) + + wire_paths = np.c_[x_path, y_path, np.ones(5) * 0.5] + source_list = [] + receiver_list = [] + receiver_location = np.array([9.28, 0.0, 0.45]) + orientations = ["x", "y", "z"] + components = ["real", "imag", "both"] + + # Define sources and receivers + source_list = [] + for f in frequencies: + receiver_list = [] + + for rx_orientation in orientations: + for comp in components: + receiver_list.append( + fdem.receivers.PointMagneticFieldSecondary( + receiver_location, + orientation=rx_orientation, + component=comp, + ) + ) + + source_list.append(fdem.sources.LineCurrent(receiver_list, f, wire_paths)) + + # Survey + survey = fdem.Survey(source_list) + self.thicknesses = np.array([20.0, 40.0]) + + self.nlayers = len(self.thicknesses) + 1 + wire_map = maps.Wires( + ("sigma", self.nlayers), + ("mu", self.nlayers), + ("thicknesses", self.nlayers - 1), + ) + self.sigma_map = maps.ExpMap(nP=self.nlayers) * wire_map.sigma + self.mu_map = maps.ExpMap(nP=self.nlayers) * wire_map.mu + self.thicknesses_map = maps.ExpMap(nP=self.nlayers - 1) * wire_map.thicknesses + + sim = fdem.Simulation1DLayered( + survey=survey, + sigmaMap=self.sigma_map, + muMap=self.mu_map, + thicknessesMap=self.thicknesses_map, + ) + + self.sim = sim + + def test_EM1DFDJvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[1] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 1.1 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[1] = mu_blk + + # General model + m_1D = np.r_[np.log(sig), np.log(mu), np.log(self.thicknesses)] + + def fwdfun(m): + resp = self.sim.dpred(m) + return resp + # return Hz + + def jacfun(m, dm): + Jvec = self.sim.Jvec(m, dm) + return Jvec + + dm = m_1D * 0.5 + + def derChk(m): + return [fwdfun(m), lambda mx: jacfun(m, mx)] + + passed = tests.check_derivative( + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=1123 + ) + assert passed + + def test_EM1DFDJtvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[1] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 1.1 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[1] = mu_blk + + # General model + m_true = np.r_[np.log(sig), np.log(mu), np.log(self.thicknesses)] + + dobs = self.sim.dpred(m_true) + + m_ini = np.r_[ + np.log(np.ones(self.nlayers) * sigma_half), + np.log(np.ones(self.nlayers) * mu_half), + np.log(self.thicknesses) * 0.9, + ] + resp_ini = self.sim.dpred(m_ini) + dr = resp_ini - dobs + + def misfit(m, dobs): + dpred = self.sim.dpred(m) + misfit = np.linalg.norm(dpred - dobs) ** 2 + dmisfit = 2 * self.sim.Jtvec( + m, dr + ) # derivative of ||dpred - dobs||^2 gives factor of 2 + return misfit, dmisfit + + def derChk(m): + return misfit(m, dobs) + + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=124 + ) + assert passed diff --git a/tests/em/em1d/test_EM1D_FD_jac_layers3.py b/tests/em/em1d/test_EM1D_FD_jac_layers3.py new file mode 100644 index 0000000000..bc49ac3432 --- /dev/null +++ b/tests/em/em1d/test_EM1D_FD_jac_layers3.py @@ -0,0 +1,461 @@ +from simpeg import maps +from discretize import tests, TensorMesh +import simpeg.electromagnetics.frequency_domain as fdem +import numpy as np +from scipy.constants import mu_0 +from scipy.sparse import diags + + +class TestEM1D_FD_Jacobian_MagDipole: + # Tests 2nd order convergence of Jvec and Jtvec for magnetic dipole sources. + # - All src and rx orientations + # - All rx components + # - Span many frequencies + # - Tests derivatives wrt sigma, mu, thicknesses and h + def setup_class(self): + # Layers and topography + nearthick = np.logspace(-1, 1, 5) + deepthick = np.logspace(1, 2, 10) + thicknesses = np.r_[nearthick, deepthick] + topo = np.r_[0.0, 0.0, 100.0] + + # Survey Geometry + height = 1e-5 + src_location = np.array([0.0, 0.0, 100.0 + height]) + rx_location = np.array([5.0, 5.0, 100.0 + height]) + frequencies = np.logspace(1, 8, 9) + orientations = ["x", "y", "z"] + components = ["real", "imag", "both"] + + # Define sources and receivers + source_list = [] + for f in frequencies: + for tx_orientation in orientations: + receiver_list = [] + + for rx_orientation in orientations: + for comp in components: + receiver_list.append( + fdem.receivers.PointMagneticFieldSecondary( + rx_location, orientation=rx_orientation, component=comp + ) + ) + + source_list.append( + fdem.sources.MagDipole( + receiver_list, + frequency=f, + location=src_location, + orientation=tx_orientation, + ) + ) + + # Survey + survey = fdem.Survey(source_list) + + self.topo = topo + self.survey = survey + self.showIt = False + self.height = height + self.frequencies = frequencies + self.thicknesses = thicknesses + self.nlayers = len(thicknesses) + 1 + + wire_map = maps.Wires( + ("sigma", self.nlayers), + ("h", 1), + ("thicknesses", self.nlayers - 1), + ) + self.sigma_map = maps.ExpMap(nP=self.nlayers) * wire_map.sigma + self.thicknesses_map = maps.ExpMap(nP=self.nlayers - 1) * wire_map.thicknesses + nP = len(source_list) + surject_mesh = TensorMesh([np.ones(nP)]) + self.h_map = maps.SurjectFull(surject_mesh) * maps.ExpMap(nP=1) * wire_map.h + + sim = fdem.Simulation1DLayered( + survey=self.survey, + sigmaMap=self.sigma_map, + thicknessesMap=self.thicknesses_map, + hMap=self.h_map, + topo=self.topo, + ) + + self.sim = sim + + def test_EM1DFDJvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # General model + m_1D = np.r_[np.log(sig), np.log(self.height), np.log(self.thicknesses)] + + def fwdfun(m): + resp = self.sim.dpred(m) + return resp + # return Hz + + def jacfun(m, dm): + Jvec = self.sim.Jvec(m, dm) + return Jvec + + def derChk(m): + return [fwdfun(m), lambda mx: jacfun(m, mx)] + + dm = m_1D * 0.5 + + passed = tests.check_derivative( + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=9186724 + ) + assert passed + + def test_EM1DFDJtvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # General model + m_true = np.r_[np.log(sig), np.log(self.height), np.log(self.thicknesses)] + + dobs = self.sim.dpred(m_true) + + m_ini = np.r_[ + np.log(np.ones(self.nlayers) * sigma_half), + np.log(0.5 * self.height), + np.log(self.thicknesses) * 0.9, + ] + resp_ini = self.sim.dpred(m_ini) + dr = resp_ini - dobs + + def misfit(m, dobs): + dpred = self.sim.dpred(m) + misfit = np.linalg.norm(dpred - dobs) ** 2 + dmisfit = 2.0 * self.sim.Jtvec( + m, dr + ) # derivative of ||dpred - dobs||^2 gives factor of 2 + return misfit, dmisfit + + def derChk(m): + return misfit(m, dobs) + + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=2345 + ) + assert passed + + def test_jtjdiag(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # General model + model = np.r_[np.log(sig), np.log(self.height), np.log(self.thicknesses)] + + rng = np.random.default_rng(seed=42) + weights_matrix = diags(rng.random(size=self.sim.survey.nD)) + jtj_diag = self.sim.getJtJdiag(model, W=weights_matrix) + + J = self.sim.getJ(model) + expected = np.diag(J.T @ weights_matrix.T @ weights_matrix @ J) + np.testing.assert_allclose(expected, jtj_diag) + + +class TestEM1D_FD_Jacobian_CircularLoop: + # Tests 2nd order convergence of Jvec and Jtvec for horizontal loop sources. + # - All rx orientations + # - All rx components + # - Span many frequencies + # - Tests derivatives wrt sigma, mu, thicknesses and h + def setup_class(self): + nearthick = np.logspace(-1, 1, 5) + deepthick = np.logspace(1, 2, 10) + thicknesses = np.r_[nearthick, deepthick] + topo = np.r_[0.0, 0.0, 100.0] + height = 1e-5 + + src_location = np.array([0.0, 0.0, 100.0 + height]) + rx_location = np.array([0.0, 0.0, 100.0 + height]) + frequencies = np.logspace(1, 8, 9) + orientations = ["x", "y", "z"] + components = ["real", "imag", "both"] + I = 1.0 + a = 10.0 + + # Define sources and receivers + source_list = [] + for f in frequencies: + receiver_list = [] + + for rx_orientation in orientations: + for comp in components: + receiver_list.append( + fdem.receivers.PointMagneticFieldSecondary( + rx_location, orientation=rx_orientation, component=comp + ) + ) + + source_list.append( + fdem.sources.CircularLoop( + receiver_list, f, src_location, radius=a, current=I + ) + ) + + # Survey + survey = fdem.Survey(source_list) + + self.topo = topo + self.survey = survey + self.showIt = False + self.height = height + self.frequencies = frequencies + self.thicknesses = thicknesses + self.nlayers = len(thicknesses) + 1 + + nP = len(source_list) + + wire_map = maps.Wires( + ("sigma", self.nlayers), + ("mu", self.nlayers), + ("thicknesses", self.nlayers - 1), + ("h", 1), + ) + self.sigma_map = maps.ExpMap(nP=self.nlayers) * wire_map.sigma + self.mu_map = maps.ExpMap(nP=self.nlayers) * wire_map.mu + self.thicknesses_map = maps.ExpMap(nP=self.nlayers - 1) * wire_map.thicknesses + surject_mesh = TensorMesh([np.ones(nP)]) + self.h_map = maps.SurjectFull(surject_mesh) * maps.ExpMap(nP=1) * wire_map.h + + sim = fdem.Simulation1DLayered( + survey=self.survey, + sigmaMap=self.sigma_map, + muMap=self.mu_map, + thicknessesMap=self.thicknesses_map, + hMap=self.h_map, + topo=self.topo, + ) + + self.sim = sim + + def test_EM1DFDJvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 2 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[3] = mu_blk + + # General model + m_1D = np.r_[ + np.log(sig), np.log(mu), np.log(self.thicknesses), np.log(self.height) + ] + + def fwdfun(m): + resp = self.sim.dpred(m) + return resp + # return Hz + + def jacfun(m, dm): + Jvec = self.sim.Jvec(m, dm) + return Jvec + + def derChk(m): + return [fwdfun(m), lambda mx: jacfun(m, mx)] + + dm = m_1D * 0.5 + + passed = tests.check_derivative( + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=664 + ) + assert passed + + def test_EM1DFDJtvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[3] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 2 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[3] = mu_blk + + # General model + m_true = np.r_[ + np.log(sig), np.log(mu), np.log(self.thicknesses), np.log(self.height) + ] + + dobs = self.sim.dpred(m_true) + + m_ini = np.r_[ + np.log(np.ones(self.nlayers) * sigma_half), + np.log(np.ones(self.nlayers) * 1.5 * mu_half), + np.log(self.thicknesses) * 0.9, + np.log(0.5 * self.height), + ] + resp_ini = self.sim.dpred(m_ini) + dr = resp_ini - dobs + + def misfit(m, dobs): + dpred = self.sim.dpred(m) + misfit = np.linalg.norm(dpred - dobs) ** 2 + dmisfit = 2 * self.sim.Jtvec( + m, dr + ) # derivative of ||dpred - dobs||^2 gives factor of 2 + return misfit, dmisfit + + def derChk(m): + return misfit(m, dobs) + + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=42 + ) + assert passed + + +class TestEM1D_FD_Jacobian_LineCurrent: + # Tests 2nd order convergence of Jvec and Jtvec for piecewise linear loop. + # - All rx orientations + # - All rx components + # - Span many frequencies + # - Tests derivatives wrt sigma, mu, thicknesses and h + def setup_class(self): + x_path = np.array([-2, -2, 2, 2, -2]) + y_path = np.array([-1, 1, 1, -1, -1]) + frequencies = np.logspace(0, 4) + + wire_paths = np.c_[x_path, y_path, np.ones(5) * 0.5] + source_list = [] + receiver_list = [] + receiver_location = np.array([9.28, 0.0, 0.45]) + orientations = ["x", "y", "z"] + components = ["real", "imag", "both"] + + # Define sources and receivers + source_list = [] + for f in frequencies: + receiver_list = [] + + for rx_orientation in orientations: + for comp in components: + receiver_list.append( + fdem.receivers.PointMagneticFieldSecondary( + receiver_location, + orientation=rx_orientation, + component=comp, + ) + ) + + source_list.append(fdem.sources.LineCurrent(receiver_list, f, wire_paths)) + + # Survey + survey = fdem.Survey(source_list) + self.thicknesses = np.array([20.0, 40.0]) + + self.nlayers = len(self.thicknesses) + 1 + wire_map = maps.Wires( + ("sigma", self.nlayers), + ("mu", self.nlayers), + ("thicknesses", self.nlayers - 1), + ) + self.sigma_map = maps.ExpMap(nP=self.nlayers) * wire_map.sigma + self.mu_map = maps.ExpMap(nP=self.nlayers) * wire_map.mu + self.thicknesses_map = maps.ExpMap(nP=self.nlayers - 1) * wire_map.thicknesses + + sim = fdem.Simulation1DLayered( + survey=survey, + sigmaMap=self.sigma_map, + muMap=self.mu_map, + thicknessesMap=self.thicknesses_map, + ) + + self.sim = sim + + def test_EM1DFDJvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[1] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 1.1 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[1] = mu_blk + + # General model + m_1D = np.r_[np.log(sig), np.log(mu), np.log(self.thicknesses)] + + def fwdfun(m): + resp = self.sim.dpred(m) + return resp + # return Hz + + def jacfun(m, dm): + Jvec = self.sim.Jvec(m, dm) + return Jvec + + dm = m_1D * 0.5 + + def derChk(m): + return [fwdfun(m), lambda mx: jacfun(m, mx)] + + passed = tests.check_derivative( + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=1123 + ) + assert passed + + def test_EM1DFDJtvec_Layers(self): + # Conductivity + sigma_half = 0.01 + sigma_blk = 0.1 + sig = np.ones(self.nlayers) * sigma_half + sig[1] = sigma_blk + + # Permeability + mu_half = mu_0 + mu_blk = 1.1 * mu_0 + mu = np.ones(self.nlayers) * mu_half + mu[1] = mu_blk + + # General model + m_true = np.r_[np.log(sig), np.log(mu), np.log(self.thicknesses)] + + dobs = self.sim.dpred(m_true) + + m_ini = np.r_[ + np.log(np.ones(self.nlayers) * sigma_half), + np.log(np.ones(self.nlayers) * mu_half), + np.log(self.thicknesses) * 0.9, + ] + resp_ini = self.sim.dpred(m_ini) + dr = resp_ini - dobs + + def misfit(m, dobs): + dpred = self.sim.dpred(m) + misfit = np.linalg.norm(dpred - dobs) ** 2 + dmisfit = 2 * self.sim.Jtvec( + m, dr + ) # derivative of ||dpred - dobs||^2 gives factor of 2 + return misfit, dmisfit + + def derChk(m): + return misfit(m, dobs) + + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=124 + ) + assert passed diff --git a/tests/em/em1d/test_EM1D_TD_general_fwd.py b/tests/em/em1d/test_EM1D_TD_general_fwd.py index 1d4daf5c6d..962b4e8b45 100644 --- a/tests/em/em1d/test_EM1D_TD_general_fwd.py +++ b/tests/em/em1d/test_EM1D_TD_general_fwd.py @@ -276,5 +276,14 @@ def test_em1dtd_mag_dipole_bzdt(self): np.testing.assert_allclose(self.bzdt, empymod_solution, rtol=1e-2) +def test_backwards_compatible_filter_key(): + + srv = tdem.Survey([]) + sim = tdem.Simulation1DLayered(survey=srv) + sim.time_filter = "key_81_CosSin_2009" + + assert sim.time_filter == "key_81_2009" + + if __name__ == "__main__": unittest.main() diff --git a/tests/em/em1d/test_EM1D_TD_general_jac_layers.py b/tests/em/em1d/test_EM1D_TD_general_jac_layers.py index 6cce1655aa..cc9cfddda9 100644 --- a/tests/em/em1d/test_EM1D_TD_general_jac_layers.py +++ b/tests/em/em1d/test_EM1D_TD_general_jac_layers.py @@ -3,6 +3,7 @@ from discretize import tests import numpy as np import simpeg.electromagnetics.time_domain as tdem +import pytest class EM1D_TD_general_Jac_layers_ProblemTests(unittest.TestCase): @@ -85,7 +86,7 @@ def derChk(m): return [fwdfun(m), lambda mx: jacfun(m, mx)] passed = tests.check_derivative( - derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=9187235 ) self.assertTrue(passed) @@ -118,7 +119,9 @@ def misfit(m, dobs): def derChk(m): return misfit(m, dobs) - passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-26) + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-26, random_seed=42 + ) self.assertTrue(passed) @@ -265,7 +268,7 @@ def derChk(m): return [fwdfun(m), lambda mx: jacfun(m, mx)] passed = tests.check_derivative( - derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=42 ) self.assertTrue(passed) @@ -297,9 +300,66 @@ def misfit(m, dobs): def derChk(m): return misfit(m, dobs) - passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-26) + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-26, random_seed=42 + ) self.assertTrue(passed) +@pytest.mark.parametrize( + "rx_class", + [ + tdem.receivers.PointMagneticField, + tdem.receivers.PointMagneticFluxDensity, + tdem.receivers.PointMagneticFluxTimeDerivative, + ], +) +@pytest.mark.parametrize("n_locs1", [1, 4]) +@pytest.mark.parametrize("n_locs2", [1, 4]) +@pytest.mark.parametrize("orientation", ["x", "y", "z"]) +@pytest.mark.parametrize( + "waveform", [tdem.sources.StepOffWaveform(), tdem.sources.RampOffWaveform(1e-6)] +) +@pytest.mark.parametrize("comparison", ["dpred", "J"]) +def test_rx_loc_shapes(rx_class, n_locs1, n_locs2, orientation, waveform, comparison): + offsets = np.full(n_locs1, 100.0) + rx1_locs = np.pad(offsets[:, None], ((0, 0), (0, 2)), constant_values=0) + offsets = np.full(n_locs2, 100.0) + rx2_locs = np.pad(offsets[:, None], ((0, 0), (0, 2)), constant_values=0) + + times = [1e-5, 1e-4] + rx_list = [ + rx_class(rx1_locs, times=times, orientation=orientation), + rx_class(rx2_locs, times=times, orientation=orientation), + ] + n_d = (n_locs1 + n_locs2) * len(times) + + src = tdem.sources.MagDipole(rx_list, waveform=waveform) + srv = tdem.Survey(src) + + sim = tdem.Simulation1DLayered(survey=srv, sigma=[1]) + if comparison == "dpred": + d = sim.dpred(None) + else: + sim.sigmaMap = maps.IdentityMap(nP=1) + J = sim.getJ(np.ones(1)) + d = J[:, 0] + + # assert the shape is correct + assert d.shape == (n_d,) + + # every pair of values should be the same... + d1 = d[srv.get_slice(src, rx_list[0])] + d2 = d[srv.get_slice(src, rx_list[1])] + + # get the data into an n_loc * n_times shape + d = np.r_[ + d1.reshape(2, -1).T, + d2.reshape(2, -1).T, + ] + d_compare = d[0] * np.ones_like(d) + np.testing.assert_equal(d, d_compare) + + if __name__ == "__main__": unittest.main() diff --git a/tests/em/em1d/test_EM1D_TD_getJ.py b/tests/em/em1d/test_EM1D_TD_getJ.py new file mode 100644 index 0000000000..42273fbedf --- /dev/null +++ b/tests/em/em1d/test_EM1D_TD_getJ.py @@ -0,0 +1,161 @@ +""" +Test the getJ method of FDEM 1D simulation. +""" + +import pytest +import numpy as np +import simpeg.electromagnetics.time_domain as tdem +from simpeg import maps +from scipy.sparse import diags + + +def create_simulation_and_conductivities(identity_mapping: bool): + # Create Survey + # ------------- + # Source properties + source_location = np.array([0.0, 0.0, 20.0]) + source_orientation = "z" # "x", "y" or "z" + source_current = 1.0 # maximum on-time current + source_radius = 6.0 # source loop radius + + # Receiver properties + receiver_location = np.array([0.0, 0.0, 20.0]) + receiver_orientation = "z" # "x", "y" or "z" + times = np.logspace(-5, -2, 31) # time channels (s) + + # Define receiver list. In our case, we have only a single receiver for each source. + # When simulating the response for multiple component and/or field orientations, + # the list consists of multiple receiver objects. + receiver_list = [] + receiver_list.append( + tdem.receivers.PointMagneticFluxDensity( + receiver_location, times, orientation=receiver_orientation + ) + ) + + # Define the source waveform. Here we define a unit step-off. The definition + # of other waveform types is covered in a separate tutorial. + waveform = tdem.sources.StepOffWaveform() + + # Define source list. In our case, we have only a single source. + source_list = [ + tdem.sources.CircularLoop( + receiver_list=receiver_list, + location=source_location, + orientation=source_orientation, + waveform=waveform, + current=source_current, + radius=source_radius, + ) + ] + + # Define the survey + survey = tdem.Survey(source_list) + + # Defining a 1D Layered Earth Model + # --------------------------------- + # Physical properties + background_conductivity = 1e-1 + layer_conductivity = 1e0 + + # Layer thicknesses + thicknesses = np.array([40.0, 40.0]) + n_layer = len(thicknesses) + 1 + + # Conductivities + conductivities = background_conductivity * np.ones(n_layer) + conductivities[1] = layer_conductivity + + # Define a mapping + model_mapping = ( + maps.IdentityMap(nP=n_layer) if identity_mapping else maps.ExpMap(nP=n_layer) + ) + + # Define the Forward Simulation, Predict Data and Plot + # ---------------------------------------------------- + simulation = tdem.Simulation1DLayered( + survey=survey, + thicknesses=thicknesses, + sigmaMap=model_mapping, + ) + + return simulation, conductivities + + +def test_getJ(): + """ + Test if getJ returns different J matrices after passing different maps. + """ + dpreds, jacobians = [], [] + + # Compute dpred and J using an identity map and an exp map + for identity_mapping in (True, False): + simulation, conductivities = create_simulation_and_conductivities( + identity_mapping + ) + model = conductivities if identity_mapping else np.log(conductivities) + dpreds.append(simulation.dpred(model)) + jac = simulation.getJ(model) + jacobians.append(jac) + + # The two dpreds should be equal + assert np.allclose(*dpreds) + + # The two J matrices should not be equal + assert not np.allclose(*jacobians, atol=0.0) + + +@pytest.mark.parametrize("mapping", ["identity", "expmap"]) +def test_JtJdiag(mapping): + """ + Test the getJtJdiag method of the simulation. + """ + identity_mapping = mapping == "identity" + simulation, conductivities = create_simulation_and_conductivities(identity_mapping) + + model = conductivities if identity_mapping else np.log(conductivities) + rng = np.random.default_rng(seed=42) + weights_matrix = diags(rng.random(size=simulation.survey.nD)) + jtj_diag = simulation.getJtJdiag(model, W=weights_matrix) + + J = simulation.getJ(model) + expected = np.diag(J.T @ weights_matrix.T @ weights_matrix @ J) + np.testing.assert_allclose(expected, jtj_diag) + + +@pytest.mark.parametrize("mapping", ["identity", "expmap"]) +def test_Jvec(mapping): + """ + Test the Jvec method of the simulation. + """ + identity_mapping = mapping == "identity" + simulation, conductivities = create_simulation_and_conductivities(identity_mapping) + + model = conductivities if identity_mapping else np.log(conductivities) + rng = np.random.default_rng(seed=42) + vector = rng.random(size=model.size) + jvec = simulation.Jvec(model, vector) + + J = simulation.getJ(model) + expected = J @ vector + + np.testing.assert_allclose(expected, jvec) + + +@pytest.mark.parametrize("mapping", ["identity", "expmap"]) +def test_Jtvec(mapping): + """ + Test the Jtvec method of the simulation. + """ + identity_mapping = mapping == "identity" + simulation, conductivities = create_simulation_and_conductivities(identity_mapping) + + model = conductivities if identity_mapping else np.log(conductivities) + rng = np.random.default_rng(seed=42) + vector = rng.random(size=simulation.survey.nD) + jtvec = simulation.Jtvec(model, vector) + + J = simulation.getJ(model) + expected = J.T @ vector + + np.testing.assert_allclose(expected, jtvec) diff --git a/tests/em/em1d/test_EM1D_TD_off_jac_layers.py b/tests/em/em1d/test_EM1D_TD_off_jac_layers.py index c985b8e758..1288b544b6 100644 --- a/tests/em/em1d/test_EM1D_TD_off_jac_layers.py +++ b/tests/em/em1d/test_EM1D_TD_off_jac_layers.py @@ -116,7 +116,7 @@ def derChk(m): return [fwdfun(m), lambda mx: jacfun(m, mx)] passed = tests.check_derivative( - derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=51234 ) self.assertTrue(passed) if passed: @@ -160,7 +160,9 @@ def misfit(m, dobs): def derChk(m): return misfit(m, dobs) - passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=52 + ) self.assertTrue(passed) if passed: print("EM1DTM MagDipole Jtvec test works") @@ -276,7 +278,7 @@ def derChk(m): return [fwdfun(m), lambda mx: jacfun(m, mx)] passed = tests.check_derivative( - derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 + derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15, random_seed=523 ) self.assertTrue(passed) if passed: @@ -320,7 +322,9 @@ def misfit(m, dobs): def derChk(m): return misfit(m, dobs) - passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) + passed = tests.check_derivative( + derChk, m_ini, num=4, plotIt=False, eps=1e-27, random_seed=98234 + ) self.assertTrue(passed) if passed: print("EM1DTM Circular Loop Jtvec test works") diff --git a/tests/em/em1d/test_utils.py b/tests/em/em1d/test_utils.py new file mode 100644 index 0000000000..bbeb4b2174 --- /dev/null +++ b/tests/em/em1d/test_utils.py @@ -0,0 +1,37 @@ +from collections import namedtuple + +import pytest +import libdlf +import numpy as np +import numpy.testing as npt +from empymod.transform import get_dlf_points + +from simpeg.electromagnetics.utils.em1d_utils import get_splined_dlf_points + +FILTERS = [f"hankel.{filt}" for filt in libdlf.hankel.__all__] + [ + f"fourier.{filt}" for filt in libdlf.fourier.__all__ +] + + +@pytest.mark.parametrize("filt", FILTERS) +@pytest.mark.parametrize("n_points", [1, 5, 10]) +def test_splined_dlf(filt, n_points): + f_type, f_name = filt.split(".") + f_module = getattr(libdlf, f_type) + filt = getattr(f_module, f_name) + base, *vals = filt() + if len(vals) == 2: + v0, v1 = vals + else: + v0 = v1 = vals[0] + factor = np.around([base[1] / base[0]], 15) + filt_type = namedtuple("Filter", "base v0 v1 factor") + filt = filt_type(base, v0, v1, factor) + + r_s = np.logspace(-1, 1, n_points) + + out1, out2 = get_splined_dlf_points(filt, r_s.min(), r_s.max()) + test1, test2 = get_dlf_points(filt, r_s, -1) + + npt.assert_allclose(out1, test1[0]) + npt.assert_allclose(out2, test2) diff --git a/tests/em/fdem/forward/test_FDEM_analytics.py b/tests/em/fdem/forward/test_FDEM_analytics.py index afda665b43..ebadf7a7ea 100644 --- a/tests/em/fdem/forward/test_FDEM_analytics.py +++ b/tests/em/fdem/forward/test_FDEM_analytics.py @@ -5,7 +5,7 @@ import numpy as np import scipy.sparse as sp from scipy.constants import mu_0 -from simpeg import SolverLU, utils +from simpeg import utils from simpeg.electromagnetics import analytics from simpeg.electromagnetics import frequency_domain as fdem @@ -59,14 +59,6 @@ def setUp(self): sigma[mesh.gridCC[:, 2] > 0] = 1e-8 prb = fdem.Simulation3DMagneticFluxDensity(mesh, survey=survey, sigma=sigma) - - try: - from pymatsolver import Pardiso - - prb.solver = Pardiso - except ImportError: - prb.solver = SolverLU - self.prb = prb self.mesh = mesh self.sig = sig diff --git a/tests/em/fdem/forward/test_FDEM_casing.py b/tests/em/fdem/forward/test_FDEM_casing.py index f1aa75a736..92d8b85153 100644 --- a/tests/em/fdem/forward/test_FDEM_casing.py +++ b/tests/em/fdem/forward/test_FDEM_casing.py @@ -12,9 +12,10 @@ sigma = np.r_[10.0, 5.5e6, 1e-1] mu = mu_0 * np.r_[1.0, 100.0, 1.0] srcloc = np.r_[0.0, 0.0, 0.0] -xobs = np.random.rand(n) + 10.0 +rng = np.random.default_rng(seed=42) +xobs = rng.uniform(size=n) + 10.0 yobs = np.zeros(n) -zobs = np.random.randn(n) +zobs = rng.normal(size=n) def CasingMagDipoleDeriv_r(x): @@ -63,15 +64,29 @@ def CasingMagDipole2Deriv_z_z(z): class Casing_DerivTest(unittest.TestCase): def test_derivs(self): + rng = np.random.default_rng(seed=42) + + tests.check_derivative( + CasingMagDipoleDeriv_r, + np.ones(n) * 10 + rng.normal(size=n), + plotIt=False, + random_seed=rng, + ) + tests.check_derivative( - CasingMagDipoleDeriv_r, np.ones(n) * 10 + np.random.randn(n), plotIt=False + CasingMagDipoleDeriv_z, rng.normal(size=n), plotIt=False, random_seed=rng ) - tests.check_derivative(CasingMagDipoleDeriv_z, np.random.randn(n), plotIt=False) + tests.check_derivative( CasingMagDipole2Deriv_z_r, - np.ones(n) * 10 + np.random.randn(n), + np.ones(n) * 10 + rng.normal(size=n), plotIt=False, + random_seed=rng, ) + tests.check_derivative( - CasingMagDipole2Deriv_z_z, np.random.randn(n), plotIt=False + CasingMagDipole2Deriv_z_z, + rng.normal(size=n), + plotIt=False, + random_seed=rng, ) diff --git a/tests/em/fdem/forward/test_FDEM_dipolar_sources.py b/tests/em/fdem/forward/test_FDEM_dipolar_sources.py new file mode 100644 index 0000000000..fe3fe48bbb --- /dev/null +++ b/tests/em/fdem/forward/test_FDEM_dipolar_sources.py @@ -0,0 +1,111 @@ +from scipy.constants import mu_0 +import numpy as np +import pytest + +from discretize import TensorMesh +from geoana.em.static import MagneticDipoleWholeSpace +import simpeg.electromagnetics.frequency_domain as fdem +from simpeg import maps + +from simpeg.utils.solver_utils import get_default_solver + +Solver = get_default_solver() + +TOL = 2e-2 # relative tolerance + +# Defining transmitter locations +source_location = np.r_[0, 0, 0] + + +def create_survey(source_type="MagDipole", mu=mu_0, orientation="Z"): + + freq = 10 + + # Must define the transmitter properties and associated receivers + source_list = [ + getattr(fdem.sources, source_type)( + [], + location=source_location, + frequency=freq, + moment=1.0, + orientation=orientation, + mu=mu, + ) + ] + + survey = fdem.Survey(source_list) + return survey + + +def create_mesh_model(): + cell_size = 20 + n_core = 10 + padding_factor = 1.3 + n_padding = 10 + + h = [ + (cell_size, n_padding, -padding_factor), + (cell_size, n_core), + (cell_size, n_padding, padding_factor), + ] + mesh = TensorMesh([h, h, h], origin="CCC") + + # Conductivity in S/m + air_conductivity = 1e-8 + background_conductivity = 1e-1 + + model = air_conductivity * np.ones(mesh.n_cells) + model[mesh.cell_centers[:, 2] < 0] = background_conductivity + + return mesh, model + + +@pytest.mark.parametrize("simulation_type", ["e", "b", "h", "j"]) +@pytest.mark.parametrize("field_test", ["bPrimary", "hPrimary"]) +@pytest.mark.parametrize("mur", [1, 50]) +def test_dipolar_fields(simulation_type, field_test, mur, orientation="Z"): + + mesh, model = create_mesh_model() + survey = create_survey("MagDipole", mu=mur * mu_0, orientation="Z") + + if simulation_type in ["e", "b"]: + grid = mesh.faces + projection = mesh.project_face_vector + if simulation_type == "e": + sim = fdem.simulation.Simulation3DElectricField( + mesh, survey=survey, sigmaMap=maps.IdentityMap(), solver=Solver + ) + elif simulation_type == "b": + sim = fdem.simulation.Simulation3DMagneticFluxDensity( + mesh, survey=survey, sigmaMap=maps.IdentityMap(), solver=Solver + ) + + elif simulation_type in ["h", "j"]: + grid = mesh.edges + projection = mesh.project_edge_vector + if simulation_type == "h": + sim = fdem.simulation.Simulation3DMagneticField( + mesh, survey=survey, sigmaMap=maps.IdentityMap(), solver=Solver + ) + elif simulation_type == "j": + sim = fdem.simulation.Simulation3DCurrentDensity( + mesh, survey=survey, sigmaMap=maps.IdentityMap(), solver=Solver + ) + + # get numeric solution + src = survey.source_list[0] + numeric = getattr(src, field_test)(sim) + + # get analytic + dipole = MagneticDipoleWholeSpace(orientation=orientation, mu=mur * mu_0) + + if field_test == "bPrimary": + analytic = projection(dipole.magnetic_flux_density(grid)) + elif field_test == "hPrimary": + analytic = projection(dipole.magnetic_field(grid)) + + # Check that the rms is below a tolerance + diff = analytic - numeric + rms = np.sqrt(np.mean(diff**2)) + maxabs = np.max(np.abs(analytic)) + assert rms < maxabs * TOL diff --git a/tests/em/fdem/forward/test_FDEM_primsec.py b/tests/em/fdem/forward/test_FDEM_primsec.py index c66b688bd0..76c09f0c99 100644 --- a/tests/em/fdem/forward/test_FDEM_primsec.py +++ b/tests/em/fdem/forward/test_FDEM_primsec.py @@ -5,7 +5,6 @@ from simpeg import maps, tests, utils from simpeg.electromagnetics import frequency_domain as fdem -from pymatsolver import Pardiso as Solver import numpy as np import unittest @@ -16,8 +15,6 @@ TOL_JT = 1e-10 FLR = 1e-20 # "zero", so if residual below this --> pass regardless of order -np.random.seed(2016) - # To test the primary secondary-source, we look at make sure doing primary # secondary for a simple model gives comprable results to just solving a 3D # problem @@ -128,15 +125,16 @@ def fun(x): lambda x: self.secondarySimulation.Jvec(x0, x, f=self.fields_primsec), ] - return tests.check_derivative(fun, x0, num=2, plotIt=False) + return tests.check_derivative(fun, x0, num=2, plotIt=False, random_seed=515) def AdjointTest(self): print("\nTesting adjoint") m = model f = self.fields_primsec - v = np.random.rand(self.secondarySurvey.nD) - w = np.random.rand(self.secondarySimulation.sigmaMap.nP) + rng = np.random.default_rng(seed=2016) + v = rng.uniform(size=self.secondarySurvey.nD) + w = rng.uniform(size=self.secondarySimulation.sigmaMap.nP) vJw = v.dot(self.secondarySimulation.Jvec(m, w, f)) wJtv = w.dot(self.secondarySimulation.Jtvec(m, v, f)) @@ -169,7 +167,6 @@ def setUpClass(self): self.primarySimulation = fdem.Simulation3DMagneticFluxDensity( meshp, sigmaMap=primaryMapping ) - self.primarySimulation.solver = Solver primarySrc = fdem.Src.MagDipole(self.rxlist, frequency=freq, location=src_loc) self.primarySurvey = fdem.Survey([primarySrc]) @@ -185,7 +182,6 @@ def setUpClass(self): self.secondarySimulation = fdem.Simulation3DMagneticFluxDensity( meshs, survey=self.secondarySurvey, sigmaMap=mapping ) - self.secondarySimulation.solver = Solver # Full 3D problem to compare with self.survey3D = fdem.Survey([primarySrc]) @@ -193,7 +189,6 @@ def setUpClass(self): self.simulation3D = fdem.Simulation3DMagneticFluxDensity( meshs, survey=self.survey3D, sigmaMap=mapping ) - self.simulation3D.solver = Solver # solve and store fields print(" solving primary - secondary") @@ -236,7 +231,6 @@ def setUpClass(self): self.primarySimulation = fdem.Simulation3DCurrentDensity( meshp, sigmaMap=primaryMapping ) - self.primarySimulation.solver = Solver s_e = np.zeros(meshp.nF) inds = meshp.nFx + meshp.closest_points_index(src_loc, grid_loc="Fz") s_e[inds] = 1.0 / csz @@ -260,7 +254,6 @@ def setUpClass(self): survey=self.secondarySurvey, sigmaMap=mapping, ) - self.secondarySimulation.solver = Solver # Full 3D problem to compare with @@ -276,7 +269,6 @@ def setUpClass(self): self.simulation3D = fdem.Simulation3DElectricField( meshs, survey=self.survey3D, sigmaMap=mapping ) - self.simulation3D.solver = Solver self.simulation3D.model = model # solve and store fields diff --git a/tests/em/fdem/forward/test_FDEM_sources.py b/tests/em/fdem/forward/test_FDEM_sources.py index 7ba8de8170..07fa24bb62 100644 --- a/tests/em/fdem/forward/test_FDEM_sources.py +++ b/tests/em/fdem/forward/test_FDEM_sources.py @@ -376,24 +376,6 @@ def test_CircularLoop_bPrimaryMu50_h(self): assert self.bPrimaryTest(src, "j") -def test_removal_circular_loop_n(): - """ - Test if passing the N argument to CircularLoop raises an error - """ - msg = "'N' property has been removed. Please use 'n_turns'." - with pytest.raises(TypeError, match=msg): - fdem.sources.CircularLoop( - [], - frequency=1e-3, - radius=np.sqrt(1 / np.pi), - location=[0, 0, 0], - orientation="Z", - mu=mu_0, - current=0.5, - N=2, - ) - - def test_line_current_failures(): rx_locs = [[0.5, 0.5, 0]] tx_locs = [[0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0], [0, 0, 0]] @@ -402,3 +384,37 @@ def test_line_current_failures(): ) with pytest.raises(ValueError): fdem.sources.LineCurrent([rx], 10, tx_locs) + + +class TestBugFixDuplicatedCurrent: + """Test that the duplicated current in LinearCurrent.Mejs has been removed.""" + + sigma = 1e-2 + + def compute_e_field(self, current): + """ + Calculate electric field on a mesh with a line current as the source. + """ + hx = [(20.0, 10)] + mesh = discretize.TensorMesh([hx, hx, hx], origin="CCC") + line_path = np.array([[-50, 0, -50], [50, 0, -50]]) + source = fdem.sources.LineCurrent( + location=line_path, current=current, frequency=1.0, receiver_list=[] + ) + survey = fdem.Survey([source]) + simulation = fdem.Simulation3DElectricField( + mesh=mesh, survey=survey, sigma=self.sigma + ) + fields = simulation.fields() + return fields[source, "e"] + + def test_fields_linear_on_current(self): + """ + Test that the electric fields are linear on the current. + + When the current is multiplied twice, the fields are not linear. + """ + current_1, current_2 = 2.5, 10.3 + expected = self.compute_e_field(current_1 + current_2) + actual = self.compute_e_field(current_1) + self.compute_e_field(current_2) + np.testing.assert_allclose(expected, actual, atol=1e-14) diff --git a/tests/em/fdem/forward/test_fields_crosscheck.py b/tests/em/fdem/forward/test_fields_crosscheck.py new file mode 100644 index 0000000000..627407b792 --- /dev/null +++ b/tests/em/fdem/forward/test_fields_crosscheck.py @@ -0,0 +1,142 @@ +import numpy as np +import pytest + +import discretize + +from simpeg import maps + +from simpeg.electromagnetics import frequency_domain as fdem +from simpeg.utils.solver_utils import get_default_solver + +SOLVER = get_default_solver() + +# relative tolerances +RELTOL = 1e-2 +MINTOL = 1e-20 # minimum tolerance we test, anything below this is "ZERO" + +FREQUENCY = 5e-1 +SIMULATION_TYPES = ["e", "b", "h", "j"] +FIELDS_TEST = ["e", "b", "h", "j", "charge", "charge_density"] + +VERBOSE = True + + +def get_fdem_simulation(mesh, fdem_type, frequency): + mapping = maps.ExpMap(mesh) + + source_list = [ + fdem.sources.MagDipole([], frequency=frequency, location=np.r_[0.0, 0.0, 10.0]) + ] + survey = fdem.Survey(source_list) + + if fdem_type == "e": + sim = fdem.Simulation3DElectricField( + mesh, survey=survey, sigmaMap=mapping, solver=SOLVER + ) + + elif fdem_type == "b": + sim = fdem.Simulation3DMagneticFluxDensity( + mesh, survey=survey, sigmaMap=mapping, solver=SOLVER + ) + + elif fdem_type == "j": + sim = fdem.Simulation3DCurrentDensity( + mesh, survey=survey, sigmaMap=mapping, solver=SOLVER + ) + + elif fdem_type == "h": + sim = fdem.Simulation3DMagneticField( + mesh, survey=survey, sigmaMap=mapping, solver=SOLVER + ) + + return sim + + +class TestFieldsCrosscheck: + + @property + def mesh(self): + if getattr(self, "_mesh", None) is None: + cs = 10.0 + ncx, ncy, ncz = 4, 4, 4 + npad = 4 + pf = 1.3 + hx = [(cs, npad, -pf), (cs, ncx), (cs, npad, pf)] + hy = [(cs, npad, -pf), (cs, ncy), (cs, npad, pf)] + hz = [(cs, npad, -pf), (cs, ncz), (cs, npad, pf)] + self._mesh = discretize.TensorMesh([hx, hy, hz], ["C", "C", "C"]) + return self._mesh + + @property + def model(self): + if getattr(self, "_model", None) is None: + sigma_background = 10 + sigma_target = 1e-2 + sigma_air = 1e-8 + + target_width = 40 + target_depth = -20 + + inds_target = ( + (self.mesh.cell_centers[:, 0] > -target_width / 2) + & (self.mesh.cell_centers[:, 0] < target_width / 2) + & (self.mesh.cell_centers[:, 1] > -target_width / 2) + & (self.mesh.cell_centers[:, 1] < target_width / 2) + & (self.mesh.cell_centers[:, 2] > -target_width / 2 + target_depth) + & (self.mesh.cell_centers[:, 2] < target_width / 2 + target_depth) + ) + + sigma_model = sigma_background * np.ones(self.mesh.n_cells) + sigma_model[self.mesh.cell_centers[:, 2] > 0] = sigma_air + + sigma_model[inds_target] = sigma_target + + self._model = np.log(sigma_model) + return self._model + + @property + def simulation_dict(self): + if getattr(self, "_simulation_dict", None) is None: + self._simulation_dict = { + key: get_fdem_simulation(self.mesh, key, FREQUENCY) + for key in SIMULATION_TYPES + } + return self._simulation_dict + + @property + def fields_dict(self): + if getattr(self, "_fields_dict", None) is None: + self._fields_dict = { + key: sim.fields(self.model) for key, sim in self.simulation_dict.items() + } + return self._fields_dict + + def compare_fields(self, field1, field2, relative_tolerance, verbose=False): + norm_diff = np.linalg.norm(field1 - field2) + abs_tol = np.max( + [ + relative_tolerance + * (np.linalg.norm(field1) + np.linalg.norm(field2)) + / 2, + MINTOL, + ] + ) + test = norm_diff < abs_tol + + if verbose is True: + print(f"||diff||: {norm_diff:1.2e} < TOL: {abs_tol:1.2e} ? {test}") + + return test + + @pytest.mark.parametrize("sim_pairs", [("e", "b"), ("h", "j")], ids=["eb", "hj"]) + @pytest.mark.parametrize("field_test", FIELDS_TEST) + def test_fields_cross_check_EBHJ( + self, sim_pairs, field_test, relative_tolerance=RELTOL, verbose=VERBOSE + ): + field1 = self.fields_dict[sim_pairs[0]][:, field_test] + field2 = self.fields_dict[sim_pairs[1]][:, field_test] + + if verbose is True: + print(f"Testing simulations {sim_pairs} for field {field_test}") + + assert self.compare_fields(field1, field2, relative_tolerance, verbose) diff --git a/tests/em/fdem/forward/test_permittivity.py b/tests/em/fdem/forward/test_permittivity.py index 2ef4abd272..aaaff90b74 100644 --- a/tests/em/fdem/forward/test_permittivity.py +++ b/tests/em/fdem/forward/test_permittivity.py @@ -6,7 +6,6 @@ import geoana import discretize from simpeg.electromagnetics import frequency_domain as fdem -from pymatsolver import Pardiso # set up the mesh @@ -83,7 +82,6 @@ def print_comparison( forward_only=True, sigma=conductivity, permittivity=epsilon, - solver=Pardiso, ), lambda survey, epsilon: fdem.Simulation3DMagneticFluxDensity( mesh, @@ -91,7 +89,6 @@ def print_comparison( forward_only=True, sigma=conductivity, permittivity=epsilon, - solver=Pardiso, ), ], ) @@ -138,7 +135,6 @@ def test_mag_dipole(epsilon, frequency, simulation): forward_only=True, sigma=conductivity, permittivity=epsilon, - solver=Pardiso, ), lambda survey, epsilon: fdem.Simulation3DMagneticField( mesh, @@ -146,7 +142,6 @@ def test_mag_dipole(epsilon, frequency, simulation): forward_only=True, sigma=conductivity, permittivity=epsilon, - solver=Pardiso, ), ], ) @@ -223,7 +218,6 @@ def test_cross_check_e_dipole(epsilon_r, frequency): forward_only=True, sigma=sigma, permittivity=rel_permittivity * epsilon_0, - solver=Pardiso, ) # H-formulation @@ -240,7 +234,6 @@ def test_cross_check_e_dipole(epsilon_r, frequency): forward_only=True, sigma=sigma, permittivity=rel_permittivity * epsilon_0, - solver=Pardiso, ) # compute fields @@ -315,7 +308,6 @@ def test_cross_check_b_dipole(epsilon_r, frequency): forward_only=True, sigma=sigma, permittivity=rel_permittivity * epsilon_0, - solver=Pardiso, ) # E-formulation @@ -330,7 +322,6 @@ def test_cross_check_b_dipole(epsilon_r, frequency): forward_only=True, sigma=sigma, permittivity=rel_permittivity * epsilon_0, - solver=Pardiso, ) # compute fields diff --git a/tests/em/fdem/forward/test_properties.py b/tests/em/fdem/forward/test_properties.py index 2b20b8c900..c583907f7e 100644 --- a/tests/em/fdem/forward/test_properties.py +++ b/tests/em/fdem/forward/test_properties.py @@ -44,12 +44,13 @@ def test_source_properties_validation(): # LineCurrent with pytest.raises(TypeError): fdem.sources.LineCurrent([], frequency, location=["a", "b", "c"]) + rng = np.random.default_rng(seed=42) + random_locations = rng.normal(size=(5, 3, 2)) with pytest.raises(ValueError): - fdem.sources.LineCurrent([], frequency, location=np.random.rand(5, 3, 2)) + fdem.sources.LineCurrent([], frequency, location=random_locations) + random_locations = rng.normal(size=(5, 3)) with pytest.raises(ValueError): - fdem.sources.LineCurrent( - [], frequency, location=np.random.rand(5, 3), current=0.0 - ) + fdem.sources.LineCurrent([], frequency, location=random_locations, current=0.0) def test_bad_source_type(): diff --git a/tests/em/fdem/inverse/adjoint/test_FDEM_adjointEB.py b/tests/em/fdem/inverse/adjoint/test_FDEM_adjointEB.py index 618d133b5b..260227a860 100644 --- a/tests/em/fdem/inverse/adjoint/test_FDEM_adjointEB.py +++ b/tests/em/fdem/inverse/adjoint/test_FDEM_adjointEB.py @@ -26,17 +26,18 @@ def adjointTest(fdemType, comp): m = np.log(np.ones(prb.sigmaMap.nP) * CONDUCTIVITY) mu = np.ones(prb.mesh.nC) * MU + rng = np.random.default_rng(seed=42) if addrandoms is True: - m = m + np.random.randn(prb.sigmaMap.nP) * np.log(CONDUCTIVITY) * 1e-1 - mu = mu + np.random.randn(prb.mesh.nC) * MU * 1e-1 + m = m + rng.normal(size=prb.sigmaMap.nP) * np.log(CONDUCTIVITY) * 1e-1 + mu = mu + rng.normal(size=prb.mesh.nC) * MU * 1e-1 survey = prb.survey # prb.PropMap.PropModel.mu = mu # prb.PropMap.PropModel.mui = 1./mu u = prb.fields(m) - v = np.random.rand(survey.nD) - w = np.random.rand(prb.mesh.nC) + v = rng.uniform(size=survey.nD) + w = rng.uniform(size=prb.mesh.nC) vJw = v.dot(prb.Jvec(m, w, u)) wJtv = w.dot(prb.Jtvec(m, v, u)) diff --git a/tests/em/fdem/inverse/adjoint/test_FDEM_adjointHJ.py b/tests/em/fdem/inverse/adjoint/test_FDEM_adjointHJ.py index 4713ff4611..bb0dcac83b 100644 --- a/tests/em/fdem/inverse/adjoint/test_FDEM_adjointHJ.py +++ b/tests/em/fdem/inverse/adjoint/test_FDEM_adjointHJ.py @@ -26,15 +26,16 @@ def adjointTest(fdemType, comp, src): m = np.log(np.ones(prb.sigmaMap.nP) * CONDUCTIVITY) mu = np.ones(prb.mesh.nC) * MU + rng = np.random.default_rng(seed=42) if addrandoms is True: - m = m + np.random.randn(prb.sigmaMap.nP) * np.log(CONDUCTIVITY) * 1e-1 - mu = mu + np.random.randn(prb.mesh.nC) * MU * 1e-1 + m = m + rng.normal(size=prb.sigmaMap.nP) * np.log(CONDUCTIVITY) * 1e-1 + mu = mu + rng.normal(size=prb.mesh.nC) * MU * 1e-1 survey = prb.survey u = prb.fields(m) - v = np.random.rand(survey.nD) - w = np.random.rand(prb.mesh.nC) + v = rng.uniform(size=survey.nD) + w = rng.uniform(size=prb.mesh.nC) vJw = v.dot(prb.Jvec(m, w, u)) wJtv = w.dot(prb.Jtvec(m, v, u)) diff --git a/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py b/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py index 7434ade3e1..2aae768832 100644 --- a/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py +++ b/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py @@ -29,20 +29,20 @@ def derivTest(fdemType, comp, src): prb = getFDEMProblem(fdemType, comp, SrcType, freq) - # prb.solverOpts = dict(check_accuracy=True) print(f"{fdemType} formulation {src} - {comp}") x0 = np.log(np.ones(prb.sigmaMap.nP) * CONDUCTIVITY) - # mu = np.log(np.ones(prb.mesh.nC)*MU) if addrandoms is True: - x0 = x0 + np.random.randn(prb.sigmaMap.nP) * np.log(CONDUCTIVITY) * 1e-1 - # mu = mu + np.random.randn(prb.sigmaMap.nP)*MU*1e-1 + rng = np.random.default_rng(seed=42) + x0 = x0 + rng.normal(size=prb.sigmaMap.nP) * np.log(CONDUCTIVITY) * 1e-1 def fun(x): return prb.dpred(x), lambda x: prb.Jvec(x0, x) - return tests.check_derivative(fun, x0, num=2, plotIt=False, eps=FLR) + return tests.check_derivative( + fun, x0, num=2, plotIt=False, eps=FLR, random_seed=6521 + ) class FDEM_DerivTests(unittest.TestCase): diff --git a/tests/em/fdem/muinverse/test_muinverse.py b/tests/em/fdem/muinverse/test_muinverse.py index 50864fa9e3..9e45560342 100644 --- a/tests/em/fdem/muinverse/test_muinverse.py +++ b/tests/em/fdem/muinverse/test_muinverse.py @@ -18,8 +18,9 @@ def setupMeshModel(): hz = [(cs, npad, -1.3), (cs, nc), (cs, npad, 1.3)] mesh = discretize.CylindricalMesh([hx, 1.0, hz], "0CC") - muMod = 1 + MuMax * np.random.randn(mesh.nC) - sigmaMod = np.random.randn(mesh.nC) + rng = np.random.default_rng(seed=2016) + muMod = 1 + MuMax * rng.normal(size=mesh.nC) + sigmaMod = rng.normal(size=mesh.nC) return mesh, muMod, sigmaMod @@ -149,7 +150,8 @@ def test_mats_cleared(self): MfMuiIDeriv_zero = self.simulation.MfMuiIDeriv(utils.Zero()) MeMuDeriv_zero = self.simulation.MeMuDeriv(utils.Zero()) - m1 = np.random.rand(self.mesh.nC) + rng = np.random.default_rng(seed=2016) + m1 = rng.uniform(size=self.mesh.nC) self.simulation.model = m1 self.assertTrue(getattr(self, "_MeMu", None) is None) @@ -168,7 +170,6 @@ def JvecTest( self.setUpProb(prbtype, sigmaInInversion, invertMui) print("Testing Jvec {}".format(prbtype)) - np.random.seed(3321) mod = self.m0 def fun(x): @@ -177,9 +178,12 @@ def fun(x): lambda x: self.simulation.Jvec(mod, x), ) - dx = np.random.rand(*mod.shape) * (mod.max() - mod.min()) * 0.01 + rng = np.random.default_rng(seed=3321) + dx = rng.uniform(size=mod.shape) * (mod.max() - mod.min()) * 0.01 - return tests.check_derivative(fun, mod, dx=dx, num=3, plotIt=False) + return tests.check_derivative( + fun, mod, dx=dx, num=4, plotIt=False, random_seed=55 + ) def JtvecTest( self, prbtype="ElectricField", sigmaInInversion=False, invertMui=False @@ -187,9 +191,9 @@ def JtvecTest( self.setUpProb(prbtype, sigmaInInversion, invertMui) print("Testing Jvec {}".format(prbtype)) - np.random.seed(31345) - u = np.random.rand(self.simulation.muMap.nP) - v = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=3321) + u = rng.uniform(size=self.simulation.muMap.nP) + v = rng.uniform(size=self.survey.nD) self.simulation.model = self.m0 diff --git a/tests/em/nsem/forward/test_1D_finite_volume.py b/tests/em/nsem/forward/test_1D_finite_volume.py index 4af4dae02f..07b8ecd8b5 100644 --- a/tests/em/nsem/forward/test_1D_finite_volume.py +++ b/tests/em/nsem/forward/test_1D_finite_volume.py @@ -2,7 +2,6 @@ from discretize import TensorMesh from simpeg.electromagnetics import natural_source as nsem from simpeg import maps -from pymatsolver import Pardiso import unittest @@ -27,18 +26,14 @@ def setUp(self): self.frequencies = np.logspace(-2, 1, 30) rx_list = [ - nsem.receivers.PointNaturalSource( + nsem.receivers.Impedance( [[0]], orientation="xy", component="apparent_resistivity" ), - nsem.receivers.PointNaturalSource( - [[0]], orientation="xy", component="phase" - ), - nsem.receivers.PointNaturalSource( + nsem.receivers.Impedance([[0]], orientation="xy", component="phase"), + nsem.receivers.Impedance( [[0]], orientation="yx", component="apparent_resistivity" ), - nsem.receivers.PointNaturalSource( - [[0]], orientation="yx", component="phase" - ), + nsem.receivers.Impedance([[0]], orientation="yx", component="phase"), ] # simulation src_list = [ @@ -50,14 +45,12 @@ def get_simulation(self, formulation="e"): if formulation == "e": return nsem.simulation.Simulation1DElectricField( mesh=self.mesh, - solver=Pardiso, survey=self.survey, sigmaMap=maps.IdentityMap(), ) elif formulation == "h": return nsem.simulation.Simulation1DMagneticField( mesh=self.mesh, - solver=Pardiso, survey=self.survey, sigmaMap=maps.IdentityMap(), ) diff --git a/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py b/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py index a2432f9135..7aa36f6144 100644 --- a/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py +++ b/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py @@ -16,6 +16,7 @@ def appResPhs(freq, z): return app_res, app_phs zList = [] + survey_slices = nsemdata.survey.get_all_slices() for src in nsemdata.survey.source_list: zc = [src.frequency] for rx in src.receiver_list: @@ -23,7 +24,8 @@ def appResPhs(freq, z): m = 1j else: m = 1 - zc.append(m * nsemdata[src, rx]) + src_rx_slice = survey_slices[src, rx] + zc.append(m * nsemdata.dobs[src_rx_slice]) zList.append(zc) return [ appResPhs(zList[i][0], np.sum(zList[i][1:3])) for i in np.arange(len(zList)) @@ -32,9 +34,10 @@ def appResPhs(freq, z): def calculateAnalyticSolution(source_list, mesh, model): surveyAna = nsem.Survey(source_list) - data1D = nsem.Data(surveyAna) + survey_slices = surveyAna.get_all_slices() + data1D = np.full(surveyAna.nD, np.nan) for src in surveyAna.source_list: - elev = src.receiver_list[0].locations[0] + elev = src.receiver_list[0].locations_e[0] anaEd, anaEu, anaHd, anaHu = nsem.utils.analytic_1d.getEHfields( mesh, model, src.frequency, elev ) @@ -45,7 +48,9 @@ def calculateAnalyticSolution(source_list, mesh, model): # anaH = (anaHtemp/anaEtemp[-1])#.conj() anaZ = anaE / anaH for rx in src.receiver_list: - data1D[src, rx] = getattr(anaZ, rx.component) + src_rx_slice = survey_slices[src, rx] + data1D[src_rx_slice] = getattr(anaZ, rx.component) + data1D = nsem.Data(surveyAna, data1D) return data1D diff --git a/tests/em/nsem/forward/test_Problem3D_VsAnalyticSolution.py b/tests/em/nsem/forward/test_Problem3D_VsAnalyticSolution.py index 5086c62d43..5300d49273 100644 --- a/tests/em/nsem/forward/test_Problem3D_VsAnalyticSolution.py +++ b/tests/em/nsem/forward/test_Problem3D_VsAnalyticSolution.py @@ -5,8 +5,6 @@ from simpeg.electromagnetics import natural_source as nsem -np.random.seed(1100) - TOLr = 1 TOLp = 2 FLR = 1e-20 # "zero", so if residual below this --> pass regardless of order diff --git a/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py b/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py index 16395302a5..be624bb19f 100644 --- a/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py +++ b/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py @@ -1,16 +1,22 @@ import unittest +import warnings + from simpeg.electromagnetics import natural_source as nsem from simpeg import maps import numpy as np from scipy.constants import mu_0 +ns_rx = nsem.receivers + +import pytest + def create_survey(freq): receivers_list = [ - nsem.receivers.PointNaturalSource(component="real"), - nsem.receivers.PointNaturalSource(component="imag"), - nsem.receivers.PointNaturalSource(component="app_res"), - nsem.receivers.PointNaturalSource(component="phase"), + nsem.receivers.Impedance([[]], component="real"), + nsem.receivers.Impedance([[]], component="imag"), + nsem.receivers.Impedance([[]], component="app_res"), + nsem.receivers.Impedance([[]], component="phase"), ] source_list = [nsem.sources.Planewave(receivers_list, f) for f in freq] @@ -24,7 +30,7 @@ def true_solution(freq, sigma_half): -np.sqrt(np.pi * freq * mu_0 / sigma_half), -np.sqrt(np.pi * freq * mu_0 / sigma_half), 1 / sigma_half, - 45.0, + -135.0, ] return soln @@ -61,5 +67,31 @@ def test_4(self): np.testing.assert_allclose(*compute_simulation(100.0, 1.0)) +@pytest.mark.parametrize( + "rx_class", + [ + ns_rx.Impedance, + ns_rx.Admittance, + ns_rx.Tipper, + ns_rx.ApparentConductivity, + ], +) +def test_incorrect_rx_types(rx_class): + loc = np.zeros((1, 3)) + rx = rx_class(loc) + source = nsem.sources.Planewave(rx, frequency=10) + survey = nsem.Survey(source) + # make sure that only these exact classes do not issue warnings. + if rx_class is ns_rx.Impedance: + with warnings.catch_warnings(): + warnings.simplefilter("error") + nsem.Simulation1DRecursive(survey=survey) + else: + with pytest.raises( + NotImplementedError, match="Simulation1DRecursive does not support .*" + ): + nsem.Simulation1DRecursive(survey=survey) + + if __name__ == "__main__": unittest.main() diff --git a/tests/em/nsem/forward/test_Simulation2D_vs_Analytic_pytest.py b/tests/em/nsem/forward/test_Simulation2D_vs_Analytic_pytest.py new file mode 100644 index 0000000000..96833bf431 --- /dev/null +++ b/tests/em/nsem/forward/test_Simulation2D_vs_Analytic_pytest.py @@ -0,0 +1,170 @@ +import pytest +from scipy.constants import mu_0 +import numpy as np +from discretize import TensorMesh +from simpeg.electromagnetics import natural_source as nsem +from simpeg.utils import model_builder +from simpeg import maps + +REL_TOLERANCE = 0.05 +ABS_TOLERANCE = 1e-13 + + +@pytest.fixture +def mesh(): + # Mesh for testing + return TensorMesh( + [ + [(40.0, 10, -1.4), (40.0, 50), (40.0, 10, 1.4)], + [(40.0, 10, -1.4), (40.0, 50), (40.0, 10, 1.4)], + ], + "CC", + ) + + +@pytest.fixture +def mapping(mesh): + return maps.IdentityMap(mesh) + + +def get_model(mesh, model_type): + # Model used for testing + model = 1e-8 * np.ones(mesh.nC) + model[mesh.cell_centers[:, 1] < 0.0] = 1e-2 + + if model_type == "layer": + model[mesh.cell_centers[:, 1] < -500.0] = 1e-1 + elif model_type == "block": + ind_block = model_builder.get_block_indices( + mesh.cell_centers, + np.array([-500, -800]), + np.array([500, -400]), + ) + model[ind_block] = 1e-1 + + return model + + +@pytest.fixture +def locations(): + # Receiver locations + elevation = 0.0 + rx_x = np.arange(-350, 350, 200) + return np.c_[rx_x, elevation + np.zeros_like(rx_x)] + + +@pytest.fixture +def frequencies(): + # Frequencies being evaluated + return [1e1, 2e1] + + +def get_survey(locations, frequencies, survey_type, component, orientation): + source_list = [] + + for f in frequencies: + # MT data types (Zxy, Zyx) + if survey_type == "impedance": + rx_list = [ + nsem.receivers.Impedance( + locations_e=locations, + locations_h=locations, + orientation=orientation, + component=component, + ) + ] + + # ZTEM data types (Tzx, Tzy) + elif survey_type == "tipper": + rx_list = [ + nsem.receivers.Tipper( + locations_h=locations, + locations_base=locations, + orientation=orientation, + component=component, + ) + ] + + source_list.append(nsem.sources.Planewave(rx_list, f)) + + return nsem.survey.Survey(source_list) + + +def get_analytic_halfspace_solution(sigma, f, survey_type, component, orientation): + # MT data types (Zxy, Zyx) + if survey_type == "impedance": + if component in ["real", "imag"]: + ampl = np.sqrt(np.pi * f * mu_0 / sigma) + if orientation == "xy": + return -ampl + else: + return ampl + elif component == "app_res": + return 1 / sigma + elif component == "phase": + if orientation == "xy": + return -135.0 + else: + return 45 + + # ZTEM data types (Tzx, Tzy) + elif survey_type == "tipper": + return 0.0 + + +# Validate impedances, tippers and admittances against analytic +# solution for a halfspace. + +CASES_LIST_HALFSPACE = [ + ("impedance", "real", "xy"), + ("impedance", "real", "yx"), + ("impedance", "imag", "xy"), + ("impedance", "imag", "yx"), + ("impedance", "app_res", "xy"), + ("impedance", "app_res", "yx"), + ("impedance", "phase", "xy"), + ("impedance", "phase", "yx"), + # ("tipper", "real", "zx"), + # ("tipper", "real", "zy"), + # ("tipper", "imag", "zx"), + # ("tipper", "imag", "zy"), +] + + +@pytest.mark.parametrize("survey_type, component, orientation", CASES_LIST_HALFSPACE) +def test_analytic_halfspace_solution( + survey_type, component, orientation, frequencies, locations, mesh, mapping +): + # Numerical solution + survey = get_survey(locations, frequencies, survey_type, component, orientation) + model_hs = get_model(mesh, "halfspace") # 1e-2 halfspace + if orientation in ["xy", "zx"]: + sim = nsem.simulation.Simulation2DElectricField( + mesh, survey=survey, sigmaMap=mapping + ) + elif orientation in ["yx", "zy"]: + sim = nsem.simulation.Simulation2DMagneticField( + mesh, survey=survey, sigmaMap=mapping + ) + + numeric_solution = sim.dpred(model_hs) + + # Analytic solution + sigma_hs = 1e-2 + n_locations = np.shape(locations)[0] + analytic_solution = np.hstack( + [ + get_analytic_halfspace_solution( + sigma_hs, f, survey_type, component, orientation + ) + for f in frequencies + ] + ) + analytic_solution = np.repeat(analytic_solution, n_locations) + + # # Error + err = np.abs( + (numeric_solution - analytic_solution) / (analytic_solution + ABS_TOLERANCE) + ) + + assert np.all(err < REL_TOLERANCE) diff --git a/tests/em/nsem/forward/test_Simulation3D_vs_Analytic_pytest.py b/tests/em/nsem/forward/test_Simulation3D_vs_Analytic_pytest.py new file mode 100644 index 0000000000..0bef1fd56c --- /dev/null +++ b/tests/em/nsem/forward/test_Simulation3D_vs_Analytic_pytest.py @@ -0,0 +1,192 @@ +import pytest +from scipy.constants import mu_0 +import numpy as np +from discretize import TensorMesh +from simpeg.electromagnetics import natural_source as nsem +from simpeg.utils import model_builder, mkvc +from simpeg import maps + +REL_TOLERANCE = 0.05 +ABS_TOLERANCE = 1e-13 + + +@pytest.fixture +def mesh(): + # Mesh for testing + return TensorMesh( + [ + [(200, 6, -1.5), (200.0, 4), (200, 6, 1.5)], + [(200, 6, -1.5), (200.0, 4), (200, 6, 1.5)], + [(200, 8, -1.5), (200.0, 8), (200, 8, 1.5)], + ], + "CCC", + ) + + +@pytest.fixture +def mapping(mesh): + return maps.IdentityMap(mesh) + + +def get_model(mesh, model_type): + # Model used for testing + model = 1e-8 * np.ones(mesh.nC) + model[mesh.cell_centers[:, 2] < 0.0] = 1e-2 + + if model_type == "layer": + model[mesh.cell_centers[:, 2] < -3000.0] = 1e-1 + elif model_type == "block": + ind_block = model_builder.get_block_indices( + mesh.cell_centers, + np.array([-1000, -1000, -1500]), + np.array([1000, 1000, -1000]), + ) + model[ind_block] = 1e-1 + + return model + + +@pytest.fixture +def locations(): + # Receiver locations + elevation = 0.0 + rx_x, rx_y = np.meshgrid(np.arange(-350, 350, 200), np.arange(-350, 350, 200)) + return np.hstack( + (mkvc(rx_x, 2), mkvc(rx_y, 2), elevation + np.zeros((np.prod(rx_x.shape), 1))) + ) + + +@pytest.fixture +def frequencies(): + # Frequencies being evaluated + return [1e-1, 2e-1] + + +def get_survey(locations, frequencies, survey_type, component): + source_list = [] + + for f in frequencies: + # MT data types (Zxx, Zxy, Zyx, Zyy) + if survey_type == "impedance": + if component == "phase": + orientations = ["xy", "yx"] # off-diagonal only!!! + else: + orientations = ["xx", "xy", "yx", "yy"] + rx_list = [ + nsem.receivers.Impedance( + locations_e=locations, + locations_h=locations, + orientation=ij, + component=component, + ) + for ij in orientations + ] + + # ZTEM data types (Txx, Tyx, Tzx, Txy, Tyy, Tzy) + elif survey_type == "tipper": + rx_list = [ + nsem.receivers.Tipper( + locations_h=locations, + locations_base=locations, + orientation=ij, + component=component, + ) + for ij in ["xx", "yx", "zx", "xy", "yy", "zy"] + ] + + # Admittance data types (Yxx, Yyx, Yzx, Yxy, Yyy, Yzy) + elif survey_type == "admittance": + rx_list = [ + nsem.receivers.Admittance( + locations_e=locations, + locations_h=locations, + orientation=ij, + component=component, + ) + for ij in ["xx", "yx", "zx", "xy", "yy", "zy"] + ] + + elif survey_type == "apparent_conductivity": + rx_list = [nsem.receivers.ApparentConductivity(locations)] + + source_list.append(nsem.sources.PlanewaveXYPrimary(rx_list, f)) + + return nsem.survey.Survey(source_list) + + +def get_analytic_halfspace_solution(sigma, f, survey_type, component): + # MT data types (Zxx, Zxy, Zyx, Zyy) + if survey_type == "impedance": + if component in ["real", "imag"]: + ampl = np.sqrt(np.pi * f * mu_0 / sigma) + return np.r_[0.0, -ampl, ampl, 0.0] + elif component == "app_res": + return np.r_[0.0, 1 / sigma, 1 / sigma, 0.0] + elif component == "phase": + return np.r_[-135.0, 45.0] # off-diagonal only! + + # ZTEM data types (Txx, Tyx, Tzx, Txy, Tyy, Tzy) + elif survey_type == "tipper": + if component == "real": + return np.r_[1.0, 0.0, 0.0, 0.0, 1.0, 0.0] + else: + return np.r_[0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + + # Admittance data types (Yxx, Yyx, Yzx, Yxy, Yyy, Yzy) + elif survey_type == "admittance": + ampl = 0.5 * np.sqrt(sigma / (np.pi * f * mu_0)) + if component == "real": + return np.r_[0.0, -ampl, 0.0, ampl, 0.0, 0.0] + else: + return np.r_[0.0, ampl, 0.0, -ampl, 0.0, 0.0] + + # MobileMT data type (app_cond) + elif survey_type == "apparent_conductivity": + return sigma + + +# Validate impedances, tippers and admittances against analytic +# solution for a halfspace. + +CASES_LIST_HALFSPACE = [ + ("impedance", "real"), + ("impedance", "imag"), + ("impedance", "app_res"), + ("impedance", "phase"), + ("tipper", "real"), + ("tipper", "imag"), + ("admittance", "real"), + ("admittance", "imag"), + ("apparent_conductivity", None), +] + + +@pytest.mark.parametrize("survey_type, component", CASES_LIST_HALFSPACE) +def test_analytic_halfspace_solution( + survey_type, component, frequencies, locations, mesh, mapping +): + # Numerical solution + survey = get_survey(locations, frequencies, survey_type, component) + model_hs = get_model(mesh, "halfspace") # 1e-2 halfspace + sim = nsem.simulation.Simulation3DPrimarySecondary( + mesh, survey=survey, sigmaPrimary=model_hs, sigmaMap=mapping + ) + numeric_solution = sim.dpred(model_hs) + + # Analytic solution + sigma_hs = 1e-2 + n_locations = np.shape(locations)[0] + analytic_solution = np.hstack( + [ + get_analytic_halfspace_solution(sigma_hs, f, survey_type, component) + for f in frequencies + ] + ) + analytic_solution = np.repeat(analytic_solution, n_locations) + + # # Error + err = np.abs( + (numeric_solution - analytic_solution) / (analytic_solution + ABS_TOLERANCE) + ) + + assert np.all(err < REL_TOLERANCE) diff --git a/tests/em/nsem/forward/test_getJ_not_implemented.py b/tests/em/nsem/forward/test_getJ_not_implemented.py new file mode 100644 index 0000000000..0da2a3ee95 --- /dev/null +++ b/tests/em/nsem/forward/test_getJ_not_implemented.py @@ -0,0 +1,50 @@ +""" +Test NotImplementedError on getJ for NSEM 1D finite volume simulations. +""" + +import pytest +import numpy as np +import discretize +from simpeg import maps +from simpeg.electromagnetics import natural_source as nsem + + +@pytest.fixture +def mesh(): + csz = 100 + nc = 300 + npad = 30 + pf = 1.2 + mesh = discretize.TensorMesh([[(csz, npad, -pf), (csz, nc), (csz, npad)]], "N") + mesh.x0 = np.r_[-mesh.h[0][:-npad].sum()] + return mesh + + +@pytest.fixture +def survey(): + frequencies = np.logspace(-2, 1, 30) + receiver = nsem.receivers.Impedance( + [[0]], orientation="xy", component="apparent_resistivity" + ) + sources = [nsem.sources.Planewave([receiver], frequency=f) for f in frequencies] + survey = nsem.survey.Survey(sources) + return survey + + +@pytest.mark.parametrize( + "simulation_class", [nsem.Simulation1DElectricField, nsem.Simulation1DMagneticField] +) +def test_getJ_not_implemented(mesh, survey, simulation_class): + """ + Test NotImplementedError on getJ for NSEM 1D simulations. + """ + mapping = maps.IdentityMap() + simulation = simulation_class( + mesh=mesh, + survey=survey, + sigmaMap=mapping, + ) + model = np.ones(survey.nD) + msg = "The getJ method hasn't been implemented" + with pytest.raises(NotImplementedError, match=msg): + simulation.getJ(model) diff --git a/tests/em/nsem/forward/test_receiver_eval.py b/tests/em/nsem/forward/test_receiver_eval.py new file mode 100644 index 0000000000..1f953198e9 --- /dev/null +++ b/tests/em/nsem/forward/test_receiver_eval.py @@ -0,0 +1,36 @@ +""" +Test receiver's ``eval`` method. +""" + +import numpy as np +import pytest +from simpeg.electromagnetics import natural_source as nsem +from simpeg.electromagnetics.natural_source.utils.test_utils import setup1DSurvey +from simpeg.utils.solver_utils import get_default_solver + + +@pytest.mark.parametrize("orientation", ["xx", "yy"]) +def test_zero_value(orientation): + """ + Test if ``Impedance.eval()`` returns an array of zeros on 1D problem + when orientation is ``"xx"`` or ``"yy"``. + + Test bugfix introduced in #1692. + """ + survey, sigma, _, mesh = setup1DSurvey(sigmaHalf=1e-2, rx_orientation=orientation) + + # Define simulation and precompute fields + solver = get_default_solver() + simulation = nsem.Simulation1DPrimarySecondary( + mesh, sigmaPrimary=sigma, sigma=sigma, survey=survey, solver=solver + ) + fields = simulation.fields() + + # Check if calling eval on each receiver returns the expected result + sources_and_receivers = ( + (src, rx) for src in survey.source_list for rx in src.receiver_list + ) + for source, receiver in sources_and_receivers: + result = receiver.eval(source, mesh, fields) + np.testing.assert_allclose(result, 0) + assert result.shape == (receiver.nD, 1) diff --git a/tests/em/nsem/inversion/test_BC_Sims.py b/tests/em/nsem/inversion/test_BC_Sims.py index 10edb4ce3d..84eecaacf8 100644 --- a/tests/em/nsem/inversion/test_BC_Sims.py +++ b/tests/em/nsem/inversion/test_BC_Sims.py @@ -6,7 +6,6 @@ from simpeg.electromagnetics import natural_source as nsem from simpeg import maps from discretize import TensorMesh, TreeMesh, CylindricalMesh -from pymatsolver import Pardiso def check_deriv(sim, test_mod, **kwargs): @@ -25,8 +24,9 @@ def J_func(v): def check_adjoint(sim, test_mod): - u = np.random.rand(len(test_mod)) - v = np.random.rand(sim.survey.nD) + rng = np.random.default_rng(seed=42) + u = rng.uniform(size=len(test_mod)) + v = rng.uniform(size=sim.survey.nD) f = sim.fields(test_mod) Ju = sim.Jvec(test_mod, u, f=f) @@ -53,18 +53,18 @@ def create_simulation_1d(sim_type, deriv_type): frequencies = np.logspace(-2, 1, 30) rx_list = [ - nsem.receivers.PointNaturalSource([[0]], orientation="xy", component="real"), - nsem.receivers.PointNaturalSource([[0]], orientation="xy", component="imag"), - nsem.receivers.PointNaturalSource( + nsem.receivers.Impedance([[0]], orientation="xy", component="real"), + nsem.receivers.Impedance([[0]], orientation="xy", component="imag"), + nsem.receivers.Impedance( [[0]], orientation="xy", component="apparent_resistivity" ), - nsem.receivers.PointNaturalSource([[0]], orientation="xy", component="phase"), - nsem.receivers.PointNaturalSource([[0]], orientation="yx", component="real"), - nsem.receivers.PointNaturalSource([[0]], orientation="yx", component="imag"), - nsem.receivers.PointNaturalSource( + nsem.receivers.Impedance([[0]], orientation="xy", component="phase"), + nsem.receivers.Impedance([[0]], orientation="yx", component="real"), + nsem.receivers.Impedance([[0]], orientation="yx", component="imag"), + nsem.receivers.Impedance( [[0]], orientation="yx", component="apparent_resistivity" ), - nsem.receivers.PointNaturalSource([[0]], orientation="yx", component="phase"), + nsem.receivers.Impedance([[0]], orientation="yx", component="phase"), ] src_list = [nsem.sources.Planewave(rx_list, frequency=f) for f in frequencies] survey = nsem.Survey(src_list) @@ -88,14 +88,12 @@ def create_simulation_1d(sim_type, deriv_type): mesh, survey=survey, **sim_kwargs, - solver=Pardiso, ) else: sim = nsem.simulation.Simulation1DMagneticField( mesh, survey=survey, **sim_kwargs, - solver=Pardiso, ) return sim, test_mod @@ -171,18 +169,12 @@ def create_simulation_2d(sim_type, deriv_type, mesh_type, fixed_boundary=False): sim_kwargs["h_bc"] = h_bc rx_list = [ - nsem.receivers.PointNaturalSource( - rx_locs, orientation="xy", component="real" - ), - nsem.receivers.PointNaturalSource( - rx_locs, orientation="xy", component="imag" - ), - nsem.receivers.PointNaturalSource( + nsem.receivers.Impedance(rx_locs, orientation="xy", component="real"), + nsem.receivers.Impedance(rx_locs, orientation="xy", component="imag"), + nsem.receivers.Impedance( rx_locs, orientation="xy", component="apparent_resistivity" ), - nsem.receivers.PointNaturalSource( - rx_locs, orientation="xy", component="phase" - ), + nsem.receivers.Impedance(rx_locs, orientation="xy", component="phase"), ] src_list = [nsem.sources.Planewave(rx_list, frequency=f) for f in frequencies] survey = nsem.Survey(src_list) @@ -191,7 +183,6 @@ def create_simulation_2d(sim_type, deriv_type, mesh_type, fixed_boundary=False): mesh, survey=survey, **sim_kwargs, - solver=Pardiso, ) else: if fixed_boundary: @@ -222,18 +213,12 @@ def create_simulation_2d(sim_type, deriv_type, mesh_type, fixed_boundary=False): sim_kwargs["e_bc"] = e_bc rx_list = [ - nsem.receivers.PointNaturalSource( - rx_locs, orientation="yx", component="real" - ), - nsem.receivers.PointNaturalSource( - rx_locs, orientation="yx", component="imag" - ), - nsem.receivers.PointNaturalSource( + nsem.receivers.Impedance(rx_locs, orientation="yx", component="real"), + nsem.receivers.Impedance(rx_locs, orientation="yx", component="imag"), + nsem.receivers.Impedance( rx_locs, orientation="yx", component="apparent_resistivity" ), - nsem.receivers.PointNaturalSource( - rx_locs, orientation="yx", component="phase" - ), + nsem.receivers.Impedance(rx_locs, orientation="yx", component="phase"), ] src_list = [nsem.sources.Planewave(rx_list, frequency=f) for f in frequencies] survey = nsem.Survey(src_list) @@ -242,7 +227,6 @@ def create_simulation_2d(sim_type, deriv_type, mesh_type, fixed_boundary=False): mesh, survey=survey, **sim_kwargs, - solver=Pardiso, ) return sim, test_mod @@ -259,19 +243,19 @@ def test_errors(self): def test_e_sigma_deriv(self): sim, test_mod = create_simulation_1d("e", "sigma") - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=235) def test_h_sigma_deriv(self): sim, test_mod = create_simulation_1d("h", "sigma") - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=5212) def test_e_mu_deriv(self): sim, test_mod = create_simulation_1d("e", "mu") - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=63246) def test_h_mu_deriv(self): sim, test_mod = create_simulation_1d("h", "mu") - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=124) def test_e_sigma_adjoint(self): sim, test_mod = create_simulation_1d("e", "sigma") @@ -295,10 +279,10 @@ def test_errors(self): rx_locs = np.c_[np.linspace(-8000, 8000, 3), np.zeros(3)] mesh_1d = TensorMesh([5]) mesh_2d = TensorMesh([5, 5]) - r_xy = nsem.receivers.PointNaturalSource( + r_xy = nsem.receivers.Impedance( rx_locs, orientation="xy", component="apparent_resistivity" ) - r_yx = nsem.receivers.PointNaturalSource( + r_yx = nsem.receivers.Impedance( rx_locs, orientation="yx", component="apparent_resistivity" ) survey_xy = nsem.Survey([nsem.sources.Planewave([r_xy], frequency=10)]) @@ -332,13 +316,15 @@ def test_errors(self): nsem.simulation.Simulation2DMagneticField( mesh_2d, survey=survey_yx, e_bc=100 ) + + random_array = np.random.default_rng(seed=42).uniform(size=20) with self.assertRaises(TypeError): nsem.simulation.Simulation2DElectricField( - mesh_2d, survey=survey_xy, h_bc=np.random.rand(20) + mesh_2d, survey=survey_xy, h_bc=random_array ) with self.assertRaises(TypeError): nsem.simulation.Simulation2DMagneticField( - mesh_2d, survey=survey_yx, e_bc=np.random.rand(20) + mesh_2d, survey=survey_yx, e_bc=random_array ) # Check fixed boundary condition Key Error @@ -353,8 +339,9 @@ def test_errors(self): # Check fixed boundary condition length error bc = {} + rng = np.random.default_rng(seed=42) for freq in survey_xy.frequencies: - bc[freq] = np.random.rand(mesh_2d.boundary_edges.shape[0] + 3) + bc[freq] = rng.uniform(size=mesh_2d.boundary_edges.shape[0] + 3) with self.assertRaises(ValueError): nsem.simulation.Simulation2DElectricField( mesh_2d, survey=survey_xy, h_bc=bc @@ -366,19 +353,19 @@ def test_errors(self): def test_e_sigma_deriv(self): sim, test_mod = create_simulation_2d("e", "sigma", "TensorMesh") - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=125) def test_h_sigma_deriv(self): sim, test_mod = create_simulation_2d("h", "sigma", "TensorMesh") - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=7425) def test_e_mu_deriv(self): sim, test_mod = create_simulation_2d("e", "mu", "TensorMesh") - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=236423) def test_h_mu_deriv(self): sim, test_mod = create_simulation_2d("h", "mu", "TensorMesh") - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=34632) def test_e_sigma_adjoint(self): sim, test_mod = create_simulation_2d("e", "sigma", "TensorMesh") @@ -408,13 +395,13 @@ def test_e_sigma_deriv_fixed(self): sim, test_mod = create_simulation_2d( "e", "sigma", "TensorMesh", fixed_boundary=True ) - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=2634) def test_h_sigma_deriv_fixed(self): sim, test_mod = create_simulation_2d( "h", "sigma", "TensorMesh", fixed_boundary=True ) - assert check_deriv(sim, test_mod, num=3) + assert check_deriv(sim, test_mod, num=3, random_seed=3651326) def test_e_sigma_adjoint_fixed(self): sim, test_mod = create_simulation_2d( diff --git a/tests/em/nsem/inversion/test_NSEM_2D_jvecjtvecadj_pytest.py b/tests/em/nsem/inversion/test_NSEM_2D_jvecjtvecadj_pytest.py new file mode 100644 index 0000000000..e93aee6089 --- /dev/null +++ b/tests/em/nsem/inversion/test_NSEM_2D_jvecjtvecadj_pytest.py @@ -0,0 +1,232 @@ +import pytest +import numpy as np +from discretize import TensorMesh, tests +from simpeg import ( + maps, + data_misfit, +) +from simpeg.utils import model_builder +from simpeg.electromagnetics import natural_source as nsem + +ADJ_RTOL = 1e-5 + + +@pytest.fixture +def mesh(): + return TensorMesh( + [ + [(40.0, 10, -1.4), (40.0, 50), (40.0, 10, 1.4)], + [(40.0, 10, -1.4), (40.0, 50), (40.0, 10, 1.4)], + ], + "CC", + ) + + +@pytest.fixture +def active_cells(mesh): + return mesh.cell_centers[:, 1] < 0.0 + + +@pytest.fixture +def mapping(mesh, active_cells): + return maps.InjectActiveCells(mesh, active_cells, 1e-8) * maps.ExpMap( + nP=np.sum(active_cells) + ) + + +@pytest.fixture +def sigma_hs(mesh, active_cells): + sigma_hs = 1e-8 * np.ones(mesh.nC) + sigma_hs[active_cells] = 1e1 + return sigma_hs + + +@pytest.fixture +def locations(): + # Receiver locations + elevation = 0.0 + rx_x = np.arange(-350, 350, 200) + return np.c_[rx_x, elevation + np.zeros_like(rx_x)] + + +@pytest.fixture +def frequencies(): + # Frequencies being evaluated + return [1e-1, 2e-1] + + +def get_survey(survey_type, orientation, components, locations, frequencies): + + if not isinstance(components, list): + components = [components] + + source_list = [] + + for f in frequencies: + + # MT data types (Zxy or Zyx) + if survey_type == "impedance": + rx_list = [ + nsem.receivers.Impedance( + locations, + orientation=orientation, + component=comp, + ) + for comp in components + ] + + # ZTEM data types (Tzx or Tzy) + elif survey_type == "tipper": + rx_list = [ + nsem.receivers.Tipper( + locations_h=locations, + locations_base=np.zeros_like(locations), + orientation=orientation, + component=comp, + ) + for comp in components + ] + + # Admittance data types (Yxy or Yyx) + elif survey_type == "admittance": + rx_list = [ + nsem.receivers.Admittance( + locations, + orientation=orientation, + component=comp, + ) + for comp in components + ] + + source_list.append(nsem.sources.Planewave(rx_list, f)) + + return nsem.survey.Survey(source_list) + + +CASES_LIST = [ + ("impedance", "xy", ["real", "imag"]), + ("impedance", "yx", ["real", "imag"]), + ("impedance", "xy", ["app_res"]), + ("impedance", "yx", ["app_res"]), + ("impedance", "xy", ["phase"]), + ("impedance", "yx", ["phase"]), +] + + +@pytest.mark.parametrize("survey_type, orientation, components", CASES_LIST) +class TestDerivatives: + def get_setup_objects( + self, + survey_type, + orientation, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ): + survey = get_survey( + survey_type, orientation, components, locations, frequencies + ) + + # Define the simulation + if orientation in ["xy", "zy"]: + sim = nsem.simulation.Simulation2DElectricField( + mesh, survey=survey, sigmaMap=mapping + ) + elif orientation in ["yx", "zx"]: + sim = nsem.simulation.Simulation2DMagneticField( + mesh, survey=survey, sigmaMap=mapping + ) + + n_active = np.sum(active_cells) + + rng = np.random.default_rng(4412) + # Model + m0 = np.log(1e1) * np.ones(n_active) + ind = model_builder.get_indices_block( + np.r_[-200.0, -600.0], + np.r_[200.0, -200.0], + mesh.cell_centers[active_cells, :], + ) + m0[ind] = np.log(1e0) + m0 += 0.01 * rng.uniform(low=-1, high=1, size=n_active) + + # Define data and misfit + data = sim.make_synthetic_data(m0, add_noise=True) + dmis = data_misfit.L2DataMisfit(simulation=sim, data=data) + + return m0, dmis + + def test_misfit( + self, + survey_type, + orientation, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ): + m0, dmis = self.get_setup_objects( + survey_type, + orientation, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ) + sim = dmis.simulation + + passed = tests.check_derivative( + lambda m: (sim.dpred(m), lambda mx: sim.Jvec(m, mx)), + m0, + plotIt=False, + num=2, + random_seed=42, + ) + + assert passed + + def test_adjoint( + self, + survey_type, + orientation, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ): + m0, dmis = self.get_setup_objects( + survey_type, + orientation, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ) + sim = dmis.simulation + n_data = sim.survey.nD + + f = sim.fields(m0) + tests.assert_isadjoint( + lambda u: sim.Jvec(m0, u, f=f), + lambda v: sim.Jtvec(m0, v, f=f), + m0.shape, + (n_data,), + rtol=ADJ_RTOL, + random_seed=44, + ) diff --git a/tests/em/nsem/inversion/test_NSEM_3D_jvecjtvecadj_pytest.py b/tests/em/nsem/inversion/test_NSEM_3D_jvecjtvecadj_pytest.py new file mode 100644 index 0000000000..457499fdc7 --- /dev/null +++ b/tests/em/nsem/inversion/test_NSEM_3D_jvecjtvecadj_pytest.py @@ -0,0 +1,252 @@ +import pytest +import numpy as np +from discretize import TensorMesh, tests +from simpeg import ( + maps, + data_misfit, +) +from simpeg.utils import mkvc, model_builder +from simpeg.electromagnetics import natural_source as nsem + +ADJ_RTOL = 1e-10 + + +@pytest.fixture +def mesh(): + return TensorMesh( + [ + [(200, 6, -1.5), (200.0, 4), (200, 6, 1.5)], + [(200, 6, -1.5), (200.0, 4), (200, 6, 1.5)], + [(200, 8, -1.5), (200.0, 8), (200, 8, 1.5)], + ], + "CCC", + ) + + +@pytest.fixture +def active_cells(mesh): + return mesh.cell_centers[:, 2] < 0.0 + + +@pytest.fixture +def mapping(mesh, active_cells): + return maps.InjectActiveCells(mesh, active_cells, 1e-8) * maps.ExpMap( + nP=np.sum(active_cells) + ) + + +@pytest.fixture +def sigma_hs(mesh, active_cells): + sigma_hs = 1e-8 * np.ones(mesh.nC) + sigma_hs[active_cells] = 1e1 + return sigma_hs + + +@pytest.fixture +def locations(): + # Receiver locations + elevation = 0.0 + rx_x, rx_y = np.meshgrid(np.arange(-350, 350, 200), np.arange(-350, 350, 200)) + return np.hstack( + (mkvc(rx_x, 2), mkvc(rx_y, 2), elevation + np.zeros((np.prod(rx_x.shape), 1))) + ) + + +@pytest.fixture +def frequencies(): + # Frequencies being evaluated + return [1e-1, 2e-1] + + +def get_survey(survey_type, orientations, components, locations, frequencies): + if not isinstance(orientations, list): + orientations = [orientations] + + if not isinstance(components, list): + components = [components] + + source_list = [] + + for f in frequencies: + rx_list = [] + + # MT data types (Zxx, Zxy, Zyx, Zyy) + if survey_type == "impedance": + for orient in orientations: + rx_list.extend( + [ + nsem.receivers.Impedance( + locations, + orientation=orient, + component=comp, + ) + for comp in components + ] + ) + + # ZTEM data types (Txx, Tyx, Tzx, Txy, Tyy, Tzy) + elif survey_type == "tipper": + for orient in orientations: + rx_list.extend( + [ + nsem.receivers.Tipper( + locations_h=locations, + locations_base=np.zeros_like(locations), + orientation=orient, + component=comp, + ) + for comp in components + ] + ) + + # Admittance data types (Yxx, Yyx, Yzx, Yxy, Yyy, Yzy) + elif survey_type == "admittance": + for orient in orientations: + rx_list.extend( + [ + nsem.receivers.Admittance( + locations, + orientation=orient, + component=comp, + ) + for comp in components + ] + ) + + # MobileMT is app_cond + elif survey_type == "apparent_conductivity": + rx_list.extend([nsem.receivers.ApparentConductivity(locations)]) + + source_list.append(nsem.sources.PlanewaveXYPrimary(rx_list, f)) + + return nsem.survey.Survey(source_list) + + +CASES_LIST = [ + ("impedance", ["xy", "yx"], ["real", "imag"]), + ("impedance", ["xx", "yy"], ["real", "imag"]), + ("impedance", ["xy", "yx"], ["app_res"]), + ("impedance", ["xx", "yy"], ["app_res"]), + ("impedance", ["xy", "yx"], ["phase"]), + ("tipper", ["zx", "zy"], ["real", "imag"]), + ("tipper", ["xx", "yy"], ["real", "imag"]), + ("tipper", ["xy", "yx"], ["real", "imag"]), + ("admittance", ["xy", "yx"], ["real", "imag"]), + ("admittance", ["xx", "yy"], ["real", "imag"]), + ("admittance", ["zx", "zy"], ["real", "imag"]), + ("apparent_conductivity", None, None), +] + + +@pytest.mark.parametrize("survey_type, orientations, components", CASES_LIST) +class TestDerivatives: + def get_setup_objects( + self, + survey_type, + orientations, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ): + survey = get_survey( + survey_type, orientations, components, locations, frequencies + ) + + # Define the simulation + sim = nsem.simulation.Simulation3DPrimarySecondary( + mesh, survey=survey, sigmaMap=mapping, sigmaPrimary=sigma_hs + ) + + n_active = np.sum(active_cells) + rng = np.random.default_rng(4412) + + # Model + m0 = np.log(1e1) * np.ones(n_active) + ind = model_builder.get_indices_block( + np.r_[-200.0, -200.0, -600.0], + np.r_[200.0, 200.0, -200.0], + mesh.cell_centers[active_cells, :], + ) + m0[ind] = np.log(1e0) + m0 += 0.01 * rng.uniform(low=-1, high=1, size=n_active) + + # Define data and misfit + data = sim.make_synthetic_data(m0, add_noise=True) + dmis = data_misfit.L2DataMisfit(simulation=sim, data=data) + + return m0, dmis + + def test_misfit( + self, + survey_type, + orientations, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ): + m0, dmis = self.get_setup_objects( + survey_type, + orientations, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ) + sim = dmis.simulation + + passed = tests.check_derivative( + lambda m: (sim.dpred(m), lambda mx: sim.Jvec(m, mx)), + m0, + plotIt=False, + num=3, + random_seed=412, + ) + + assert passed + + def test_adjoint( + self, + survey_type, + orientations, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ): + m0, dmis = self.get_setup_objects( + survey_type, + orientations, + components, + locations, + frequencies, + mesh, + active_cells, + mapping, + sigma_hs, + ) + sim = dmis.simulation + n_data = sim.survey.nD + + f = sim.fields(m0) + tests.assert_isadjoint( + lambda u: sim.Jvec(m0, u, f=f), + lambda v: sim.Jtvec(m0, v, f=f), + m0.shape, + (n_data,), + rtol=ADJ_RTOL, + random_seed=32, + ) diff --git a/tests/em/nsem/inversion/test_Problem1D_Adjoint.py b/tests/em/nsem/inversion/test_Problem1D_Adjoint.py index d3a9aaaafc..28449e0395 100644 --- a/tests/em/nsem/inversion/test_Problem1D_Adjoint.py +++ b/tests/em/nsem/inversion/test_Problem1D_Adjoint.py @@ -18,10 +18,10 @@ def JvecAdjointTest_1D(sigmaHalf, formulation="PrimSec"): # Define a receiver for each data type as a list receivers_list = [ - nsem.receivers.PointNaturalSource(component="real"), - nsem.receivers.PointNaturalSource(component="imag"), - nsem.receivers.PointNaturalSource(component="app_res"), - nsem.receivers.PointNaturalSource(component="phase"), + nsem.receivers.Impedance([[]], component="real"), + nsem.receivers.Impedance([[]], component="imag"), + nsem.receivers.Impedance([[]], component="app_res"), + nsem.receivers.Impedance([[]], component="phase"), ] # Use a list to define the planewave source at each frequency and assign receivers @@ -50,9 +50,9 @@ def JvecAdjointTest_1D(sigmaHalf, formulation="PrimSec"): m = np.r_[sigma_model, layer_thicknesses] u = simulation.fields(m) - np.random.seed(1983) - v = np.random.rand(survey.nD) - w = np.random.rand(len(m)) + rng = np.random.default_rng(seed=1983) + v = rng.uniform(size=survey.nD) + w = rng.uniform(size=len(m)) vJw = v.dot(simulation.Jvec(m, w, u)) wJtv = w.dot(simulation.Jtvec(m, v, u)) @@ -80,14 +80,10 @@ def JvecAdjointTest(sigmaHalf, formulation="PrimSec"): m = sigma u = problem.fields(m) - np.random.seed(1983) - v = np.random.rand( - survey.nD, - ) + rng = np.random.default_rng(seed=1983) + v = rng.uniform(size=survey.nD) # print problem.PropMap.PropModel.nP - w = np.random.rand( - problem.mesh.nC, - ) + w = rng.uniform(size=problem.mesh.nC) vJw = v.ravel().dot(problem.Jvec(m, w, u)) wJtv = w.ravel().dot(problem.Jtvec(m, v, u)) diff --git a/tests/em/nsem/inversion/test_Problem1D_Derivs.py b/tests/em/nsem/inversion/test_Problem1D_Derivs.py index 733e5bda1f..de11c736b4 100644 --- a/tests/em/nsem/inversion/test_Problem1D_Derivs.py +++ b/tests/em/nsem/inversion/test_Problem1D_Derivs.py @@ -16,10 +16,10 @@ def DerivJvecTest_1D(halfspace_value, freq=False, expMap=True): # Define a receiver for each data type as a list receivers_list = [ - nsem.receivers.PointNaturalSource(component="real"), - nsem.receivers.PointNaturalSource(component="imag"), - nsem.receivers.PointNaturalSource(component="app_res"), - nsem.receivers.PointNaturalSource(component="phase"), + nsem.receivers.Impedance([[]], component="real"), + nsem.receivers.Impedance([[]], component="imag"), + nsem.receivers.Impedance([[]], component="app_res"), + nsem.receivers.Impedance([[]], component="phase"), ] # Use a list to define the planewave source at each frequency and assign receivers @@ -46,12 +46,13 @@ def DerivJvecTest_1D(halfspace_value, freq=False, expMap=True): ) x0 = np.r_[sigma_model, layer_thicknesses] - np.random.seed(1983) def fun(x): return simulation.dpred(x), lambda x: simulation.Jvec(x0, x) - return tests.check_derivative(fun, x0, num=6, plotIt=False, eps=FLR) + return tests.check_derivative( + fun, x0, num=6, plotIt=False, eps=FLR, random_seed=298376 + ) def DerivJvecTest(halfspace_value, freq=False, expMap=True): @@ -69,13 +70,14 @@ def DerivJvecTest(halfspace_value, freq=False, expMap=True): ) x0 = sigBG - np.random.seed(1983) survey = simulation.survey def fun(x): return simulation.dpred(x), lambda x: simulation.Jvec(x0, x) - return tests.check_derivative(fun, x0, num=4, plotIt=False, eps=FLR) + return tests.check_derivative( + fun, x0, num=4, plotIt=False, eps=FLR, random_seed=5553 + ) class NSEM_DerivTests(unittest.TestCase): diff --git a/tests/em/nsem/inversion/test_Problem3D_Adjoint.py b/tests/em/nsem/inversion/test_Problem3D_Adjoint.py index ecafadd8d0..eb3ec464a4 100644 --- a/tests/em/nsem/inversion/test_Problem3D_Adjoint.py +++ b/tests/em/nsem/inversion/test_Problem3D_Adjoint.py @@ -44,14 +44,11 @@ def JvecAdjointTest( ) u = simulation.fields(m) - np.random.seed(1983) - v = np.random.rand( - simulation.survey.nD, - ) + rng = np.random.default_rng(seed=1983) + v = rng.uniform(size=simulation.survey.nD) # print problem.PropMap.PropModel.nP - w = np.random.rand( - len(m), - ) + w = rng.uniform(size=len(m)) + # print(problem.Jvec(m, w, u)) vJw = v.ravel().dot(simulation.Jvec(m, w, u)) wJtv = w.ravel().dot(simulation.Jtvec(m, v, u)) diff --git a/tests/em/nsem/inversion/test_Problem3D_Derivs.py b/tests/em/nsem/inversion/test_Problem3D_Derivs.py index 9d4b332a06..41a99d5ce4 100644 --- a/tests/em/nsem/inversion/test_Problem3D_Derivs.py +++ b/tests/em/nsem/inversion/test_Problem3D_Derivs.py @@ -2,7 +2,7 @@ import pytest import unittest import numpy as np -from simpeg import tests, mkvc +from simpeg import tests from simpeg.electromagnetics import natural_source as nsem from scipy.constants import mu_0 @@ -51,7 +51,7 @@ def test_Jtjdiag_clearing(model_simulation_tuple): def test_Jmatrix(model_simulation_tuple): model, simulation = model_simulation_tuple - rng = np.random.default_rng(4421) + rng = np.random.default_rng(4422) # create random vector vec = rng.standard_normal(simulation.survey.nD) @@ -88,36 +88,9 @@ def DerivJvecTest(inputSetup, comp="All", freq=False, expMap=True): def fun(x): return simulation.dpred(x), lambda x: simulation.Jvec(m, x) - return tests.check_derivative(fun, m, num=3, plotIt=False, eps=FLR) - - -def DerivProjfieldsTest(inputSetup, comp="All", freq=False): - survey, simulation = nsem.utils.test_utils.setupSimpegNSEM_ePrimSec( - inputSetup, comp, freq + return tests.check_derivative( + fun, m, num=3, plotIt=False, eps=FLR, random_seed=1512 ) - print("Derivative test of data projection for eFormulation primary/secondary\n") - # simulation.mapping = Maps.ExpMap(simulation.mesh) - # Initate things for the derivs Test - src = survey.source_list[0] - np.random.seed(1983) - u0x = np.random.randn(survey.mesh.nE) + np.random.randn(survey.mesh.nE) * 1j - u0y = np.random.randn(survey.mesh.nE) + np.random.randn(survey.mesh.nE) * 1j - u0 = np.vstack((mkvc(u0x, 2), mkvc(u0y, 2))) - f0 = simulation.fieldsPair(survey.mesh, survey) - # u0 = np.hstack((mkvc(u0_px,2),mkvc(u0_py,2))) - f0[src, "e_pxSolution"] = u0[: len(u0) / 2] # u0x - f0[src, "e_pySolution"] = u0[len(u0) / 2 : :] # u0y - - def fun(u): - f = simulation.fieldsPair(survey.mesh, survey) - f[src, "e_pxSolution"] = u[: len(u) / 2] - f[src, "e_pySolution"] = u[len(u) / 2 : :] - return ( - rx.eval(src, survey.mesh, f), - lambda t: rx.evalDeriv(src, survey.mesh, f0, mkvc(t, 2)), - ) - - return tests.check_derivative(fun, u0, num=3, plotIt=False, eps=FLR) class NSEM_DerivTests(unittest.TestCase): diff --git a/tests/em/nsem/inversion/test_complex_resistivity.py b/tests/em/nsem/inversion/test_complex_resistivity.py index 45cc6ef6cc..ad3955dfd0 100644 --- a/tests/em/nsem/inversion/test_complex_resistivity.py +++ b/tests/em/nsem/inversion/test_complex_resistivity.py @@ -6,8 +6,8 @@ from discretize.utils import mkvc from simpeg.electromagnetics import natural_source as ns import numpy as np -from pymatsolver import Pardiso as Solver from discretize.utils import volume_average +import pytest TOLr = 5e-2 TOL = 1e-4 @@ -32,7 +32,7 @@ def setUp(self): # create background conductivity model sigma_back = 1e-2 - sigma_background = np.zeros(mesh.nC) * sigma_back + sigma_background = np.ones(mesh.nC) * sigma_back sigma_background[~active] = 1e-8 # create a model to test with @@ -68,7 +68,9 @@ def create_simulation(self, rx_type="apparent_resistivity", rx_orientation="xy") rx_loc[:, 2] = -50 # Make a receiver list - rxList = [ns.Rx.PointNaturalSource(rx_loc, rx_orientation, rx_type)] + rxList = [ + ns.Rx.Impedance(rx_loc, orientation=rx_orientation, component=rx_type) + ] # Source list freqs = [10, 50, 200] @@ -79,7 +81,7 @@ def create_simulation(self, rx_type="apparent_resistivity", rx_orientation="xy") # Set the mapping actMap = maps.InjectActiveCells( - mesh=self.mesh, indActive=self.active, valInactive=np.log(1e-8) + mesh=self.mesh, active_cells=self.active, value_inactive=np.log(1e-8) ) mapping = maps.ExpMap(self.mesh) * actMap # print(survey_ns.source_list) @@ -89,7 +91,6 @@ def create_simulation(self, rx_type="apparent_resistivity", rx_orientation="xy") survey=survey_ns, sigmaPrimary=self.sigma_background, sigmaMap=mapping, - solver=Solver, ) return sim @@ -104,11 +105,11 @@ def create_simulation_rx(self, rx_type="apparent_resistivity", rx_orientation="x # Make a receiver list rxList = [ - ns.Rx.PointNaturalSource( - orientation=rx_orientation, - component=rx_type, + ns.Rx.Impedance( locations_e=rx_loc, locations_h=rx_loc, + orientation=rx_orientation, + component=rx_type, ) ] @@ -121,7 +122,7 @@ def create_simulation_rx(self, rx_type="apparent_resistivity", rx_orientation="x # Set the mapping actMap = maps.InjectActiveCells( - mesh=self.mesh, indActive=self.active, valInactive=np.log(1e-8) + mesh=self.mesh, active_cells=self.active, value_inactive=np.log(1e-8) ) mapping = maps.ExpMap(self.mesh) * actMap # print(survey_ns.source_list) @@ -131,7 +132,6 @@ def create_simulation_rx(self, rx_type="apparent_resistivity", rx_orientation="x survey=survey_ns, sigmaPrimary=self.sigma_background, sigmaMap=mapping, - solver=Solver, ) return sim @@ -147,7 +147,9 @@ def create_simulation_1dprimary_assign_mesh1d( rx_loc[:, 2] = -50 # Make a receiver list - rxList = [ns.Rx.PointNaturalSource(rx_loc, rx_orientation, rx_type)] + rxList = [ + ns.Rx.Impedance(rx_loc, orientation=rx_orientation, component=rx_type) + ] # give background a value x0 = self.mesh.x0 @@ -173,7 +175,7 @@ def create_simulation_1dprimary_assign_mesh1d( # Set the mapping actMap = maps.InjectActiveCells( - mesh=self.mesh, indActive=self.active, valInactive=np.log(1e-8) + mesh=self.mesh, active_cells=self.active, value_inactive=np.log(1e-8) ) mapping = maps.ExpMap(self.mesh) * actMap # print(survey_ns.source_list) @@ -182,7 +184,6 @@ def create_simulation_1dprimary_assign_mesh1d( self.mesh, survey=survey_ns, sigmaMap=mapping, - solver=Solver, ) return sim @@ -198,7 +199,9 @@ def create_simulation_1dprimary_assign( rx_loc[:, 2] = -50 # Make a receiver list - rxList = [ns.Rx.PointNaturalSource(rx_loc, rx_orientation, rx_type)] + rxList = [ + ns.Rx.Impedance(rx_loc, orientation=rx_orientation, component=rx_type) + ] # Source list freqs = [10, 50, 200] @@ -212,7 +215,7 @@ def create_simulation_1dprimary_assign( # Set the mapping actMap = maps.InjectActiveCells( - mesh=self.mesh, indActive=self.active, valInactive=np.log(1e-8) + mesh=self.mesh, active_cells=self.active, value_inactive=np.log(1e-8) ) mapping = maps.ExpMap(self.mesh) * actMap # print(survey_ns.source_list) @@ -221,20 +224,23 @@ def create_simulation_1dprimary_assign( self.mesh, survey=survey_ns, sigmaMap=mapping, - solver=Solver, ) return sim def check_deriv(self, sim): def fun(x): - return sim.dpred(x), lambda x: sim.Jvec(self.model, x) + d = sim.dpred(x) + return d, lambda y: sim.Jvec(x, y) - passed = tests.check_derivative(fun, self.model, num=3, plotIt=False) + passed = tests.check_derivative( + fun, self.model, num=3, plotIt=False, random_seed=1983 + ) self.assertTrue(passed) def check_adjoint(self, sim): - w = np.random.rand(len(self.model)) - v = np.random.rand(sim.survey.nD) + rng = np.random.default_rng(seed=42) + w = rng.uniform(size=len(self.model)) + v = rng.uniform(size=sim.survey.nD) f = sim.fields(self.model) vJw = v.ravel().dot(sim.Jvec(self.model, w, f)) @@ -275,6 +281,7 @@ def test_apparent_resistivity_yx(self): def test_apparent_resistivity_yy(self): self.check_deriv_adjoint("apparent_resistivity", "yy") + @pytest.mark.xfail() def test_phase_xx(self): self.check_deriv_adjoint("phase", "xx") @@ -284,9 +291,34 @@ def test_phase_xy(self): def test_phase_yx(self): self.check_deriv_adjoint("phase", "yx") + @pytest.mark.xfail() def test_phase_yy(self): self.check_deriv_adjoint("phase", "yy") + def test_real_xx(self): + self.check_deriv_adjoint("real", "xx") + + def test_real_xy(self): + self.check_deriv_adjoint("real", "xy") + + def test_real_yx(self): + self.check_deriv_adjoint("real", "yx") + + def test_real_yy(self): + self.check_deriv_adjoint("real", "yy") + + def test_imag_xx(self): + self.check_deriv_adjoint("imag", "xx") + + def test_imag_xy(self): + self.check_deriv_adjoint("imag", "xy") + + def test_imag_yx(self): + self.check_deriv_adjoint("imag", "yx") + + def test_imag_yy(self): + self.check_deriv_adjoint("imag", "yy") + if __name__ == "__main__": unittest.main() diff --git a/tests/em/nsem/survey/test_nsem_data.py b/tests/em/nsem/survey/test_nsem_data.py index 61babc3818..a6ccfe1604 100644 --- a/tests/em/nsem/survey/test_nsem_data.py +++ b/tests/em/nsem/survey/test_nsem_data.py @@ -34,5 +34,5 @@ def test_from_rec_array(self): for src in data_obj.survey.source_list: assert len(src.receiver_list) == 2 # one real, one imaginary component for rx in src.receiver_list: - np.testing.assert_almost_equal(rx.locations, [self.loc]) + np.testing.assert_almost_equal(rx.locations_e, [self.loc]) np.testing.assert_almost_equal(data_obj.dobs, np.array([0.5, 0.0, 0.5, 1.0])) diff --git a/tests/em/static/test_DCIP_io_utils.py b/tests/em/static/test_DCIP_io_utils.py index 12bd6c6beb..eebf780d06 100644 --- a/tests/em/static/test_DCIP_io_utils.py +++ b/tests/em/static/test_DCIP_io_utils.py @@ -39,7 +39,8 @@ def test_dc2d(self): self.station_spacing, ) survey2D = dc.survey.Survey(source_list) - dobs = np.random.rand(survey2D.nD) + rng = np.random.default_rng(seed=42) + dobs = rng.uniform(size=survey2D.nD) dunc = 1e-3 * np.ones(survey2D.nD) data2D = data.Data(survey2D, dobs=dobs, standard_deviation=dunc) @@ -94,7 +95,8 @@ def test_ip2d(self): self.station_spacing, ) survey2D = dc.survey.Survey(source_list) - dobs = np.random.rand(survey2D.nD) + rng = np.random.default_rng(seed=42) + dobs = rng.uniform(size=survey2D.nD) dunc = 1e-3 * np.ones(survey2D.nD) data2D = data.Data(survey2D, dobs=dobs, standard_deviation=dunc) @@ -151,7 +153,8 @@ def test_dcip3d(self): self.station_spacing, ) survey3D = dc.survey.Survey(source_list) - dobs = np.random.rand(survey3D.nD) + rng = np.random.default_rng(seed=42) + dobs = rng.uniform(size=survey3D.nD) dunc = 1e-3 * np.ones(survey3D.nD) data3D = data.Data(survey3D, dobs=dobs, standard_deviation=dunc) diff --git a/tests/em/static/test_DC_1D_jvecjtvecadj.py b/tests/em/static/test_DC_1D_jvecjtvecadj.py index 87ff631371..c55e19aaa9 100644 --- a/tests/em/static/test_DC_1D_jvecjtvecadj.py +++ b/tests/em/static/test_DC_1D_jvecjtvecadj.py @@ -87,9 +87,9 @@ def test_forward_accuracy(data_type, rx_type, tx_type): @pytest.mark.parametrize("deriv_type", ("sigma", "h", "both")) def test_derivative(deriv_type): n_layer = 4 - np.random.seed(40) - log_cond = np.random.rand(n_layer) - log_thick = np.random.rand(n_layer - 1) + rng = np.random.default_rng(seed=40) + log_cond = rng.uniform(size=n_layer) + log_thick = rng.uniform(size=n_layer - 1) if deriv_type != "h": sigma_map = maps.ExpMap() @@ -130,15 +130,15 @@ def J(v): return d, J - assert check_derivative(sim_1d_func, model, plotIt=False, num=4) + assert check_derivative(sim_1d_func, model, plotIt=False, num=4, random_seed=125) @pytest.mark.parametrize("deriv_type", ("sigma", "h", "both")) def test_adjoint(deriv_type): n_layer = 4 - np.random.seed(40) - log_cond = np.random.rand(n_layer) - log_thick = np.random.rand(n_layer - 1) + rng = np.random.default_rng(seed=40) + log_cond = rng.uniform(size=n_layer) + log_thick = rng.uniform(size=n_layer - 1) if deriv_type != "h": sigma_map = maps.ExpMap() @@ -177,7 +177,7 @@ def J(v): def JT(v): return simulation.Jtvec(model, v) - assert_isadjoint(J, JT, len(model), survey.nD) + assert_isadjoint(J, JT, len(model), survey.nD, random_seed=5512) def test_errors(): @@ -219,9 +219,9 @@ def test_errors(): def test_functionality(): n_layer = 4 - np.random.seed(40) - log_cond = np.random.rand(n_layer) - thick = np.random.rand(n_layer - 1) + rng = np.random.default_rng(seed=40) + log_cond = rng.uniform(size=n_layer) + thick = rng.uniform(size=n_layer - 1) sigma_map = maps.ExpMap() model = log_cond diff --git a/tests/em/static/test_DC_2D_analytic.py b/tests/em/static/test_DC_2D_analytic.py index 9edbf3ca0f..0c93b80360 100644 --- a/tests/em/static/test_DC_2D_analytic.py +++ b/tests/em/static/test_DC_2D_analytic.py @@ -1,9 +1,11 @@ +import discretize import numpy as np import unittest +import pytest from discretize import TensorMesh -from simpeg import utils, SolverLU +from simpeg import utils from simpeg.electromagnetics import resistivity as dc from simpeg.electromagnetics import analytics @@ -43,19 +45,11 @@ def setUp(self): self.data_ana = data_ana self.plotIt = False - try: - from pymatsolver import Pardiso - - self.solver = Pardiso - except ImportError: - self.solver = SolverLU - def test_Simulation2DNodal(self, tolerance=0.05): simulation = dc.simulation_2d.Simulation2DNodal( self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) data = simulation.dpred() @@ -71,7 +65,6 @@ def test_Simulation2DCellCentered(self, tolerance=0.05): self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) data = simulation.dpred() @@ -114,19 +107,11 @@ def setUp(self): self.data_ana = data_ana self.plotIt = False - try: - from pymatsolver import Pardiso - - self.solver = Pardiso - except ImportError: - self.solver = SolverLU - def test_Simulation2DNodal(self, tolerance=0.05): simulation = dc.simulation_2d.Simulation2DNodal( self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) data = simulation.dpred() @@ -142,7 +127,6 @@ def test_Simulation2DCellCentered(self, tolerance=0.05): self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) data = simulation.dpred() @@ -187,19 +171,11 @@ def setUp(self): self.data_ana = data_ana self.plotIt = False - try: - from pymatsolver import PardisoSolver - - self.solver = PardisoSolver - except ImportError: - self.solver = SolverLU - def test_Simulation2DNodal(self, tolerance=0.05): simulation = dc.simulation_2d.Simulation2DNodal( self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) data = simulation.dpred() @@ -215,7 +191,6 @@ def test_Simulation2DCellCentered(self, tolerance=0.05): self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) data = simulation.dpred() @@ -259,19 +234,11 @@ def setUp(self): self.sigma = sigma self.data_ana = data_ana - try: - from pymatsolver import PardisoSolver - - self.solver = PardisoSolver - except ImportError: - self.solver = SolverLU - def test_Simulation2DCellCentered(self, tolerance=0.05): simulation = dc.simulation_2d.Simulation2DCellCentered( self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) data = simulation.dpred() @@ -287,7 +254,6 @@ def test_Simulation2DNodal(self, tolerance=0.05): self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) data = simulation.dpred() @@ -326,14 +292,14 @@ def setUp(self): ROI_large_TSE = np.array([200, 0]) ROI_largeInds = utils.model_builder.get_indices_block( ROI_large_BNW, ROI_large_TSE, mesh.gridN - )[0] + ) # print(ROI_largeInds.shape) ROI_small_BNW = np.array([-50, -25]) ROI_small_TSE = np.array([50, 0]) ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, mesh.gridN - )[0] + ) # print(ROI_smallInds.shape) ROI_inds = np.setdiff1d(ROI_largeInds, ROI_smallInds) @@ -345,19 +311,11 @@ def setUp(self): self.plotIt = False self.ROI_inds = ROI_inds - try: - from pymatsolver import PardisoSolver - - self.solver = PardisoSolver - except ImportError: - self.solver = SolverLU - def test_Simulation2DCellCentered(self, tolerance=0.05): simulation = dc.simulation_2d.Simulation2DCellCentered( self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, ) field = simulation.fields() @@ -374,7 +332,6 @@ def test_Simulation2DCellCentered_Dirichlet(self, tolerance=0.05): self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Dirichlet", ) field = simulation.fields() @@ -392,7 +349,6 @@ def test_Simulation2DNodal(self, tolerance=0.05): self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, ) field = simulation.fields() data = field[:, "phi"][:, 0] @@ -437,19 +393,11 @@ def setUp(self): self.sigma_half = sighalf self.plotIt = False - try: - from pymatsolver import Pardiso - - self.solver = Pardiso - except ImportError: - self.solver = SolverLU - def test_Simulation2DNodal(self, tolerance=0.05): simulation = dc.simulation_2d.Simulation2DNodal( self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) with self.assertRaises(KeyError): @@ -468,7 +416,6 @@ def test_Simulation2DCellCentered(self, tolerance=0.05): self.mesh, survey=self.survey, sigma=self.sigma, - solver=self.solver, bc_type="Robin", ) with self.assertRaises(KeyError): @@ -483,5 +430,12 @@ def test_Simulation2DCellCentered(self, tolerance=0.05): self.assertLess(err, tolerance) +def test_bad_mesh_dim(): + mesh = discretize.TensorMesh([3, 3, 3]) + msg = "Simulation2DNodal mesh must be 2D, received a 3D mesh." + with pytest.raises(ValueError, match=msg): + dc.Simulation2DNodal(mesh) + + if __name__ == "__main__": unittest.main() diff --git a/tests/em/static/test_DC_2D_jvecjtvecadj.py b/tests/em/static/test_DC_2D_jvecjtvecadj.py index c25e06711f..83c12bff25 100644 --- a/tests/em/static/test_DC_2D_jvecjtvecadj.py +++ b/tests/em/static/test_DC_2D_jvecjtvecadj.py @@ -13,11 +13,6 @@ ) from simpeg.electromagnetics import resistivity as dc -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - class DCProblem_2DTests(unittest.TestCase): formulation = "Simulation2DCellCentered" @@ -47,18 +42,17 @@ def setUp(self): mesh, rhoMap=maps.IdentityMap(mesh), storeJ=self.storeJ, - solver=Solver, survey=survey, bc_type=self.bc_type, ) mSynth = np.ones(mesh.nC) * 1.0 - data = simulation.make_synthetic_data(mSynth, add_noise=True) + data = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e0) inv = inversion.BaseInversion(invProb) @@ -78,6 +72,7 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=9582376, ) self.assertTrue(passed) @@ -94,7 +89,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=23, ) self.assertTrue(passed) diff --git a/tests/em/static/test_DC_FieldsDipoleFullspace.py b/tests/em/static/test_DC_FieldsDipoleFullspace.py index ac33a2f2b9..a81b8f34c7 100644 --- a/tests/em/static/test_DC_FieldsDipoleFullspace.py +++ b/tests/em/static/test_DC_FieldsDipoleFullspace.py @@ -5,10 +5,6 @@ import numpy as np from simpeg.electromagnetics import resistivity as dc -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver from geoana.em import fdem from scipy.constants import mu_0, epsilon_0 @@ -78,14 +74,14 @@ def setUp(self): ROI_large_TSE = np.array([75, -75, 75]) ROI_largeInds = utils.model_builder.get_indices_block( ROI_large_BNW, ROI_large_TSE, faceGrid - )[0] + ) # print(ROI_largeInds.shape) ROI_small_BNW = np.array([-4, 4, -4]) ROI_small_TSE = np.array([4, -4, 4]) ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, faceGrid - )[0] + ) # print(ROI_smallInds.shape) ROIfaceInds = np.setdiff1d(ROI_largeInds, ROI_smallInds) @@ -103,7 +99,6 @@ def test_Simulation3DCellCentered_Dirichlet(self, tolerance=0.1): simulation = dc.Simulation3DCellCentered( self.mesh, survey=self.survey, sigma=self.sigma, bc_type="Dirichlet" ) - simulation.solver = Solver f = simulation.fields() eNumeric = utils.mkvc(f[self.survey.source_list, "e"]) @@ -136,7 +131,6 @@ def test_Simulation3DCellCentered_Mixed(self, tolerance=0.1): simulation = dc.simulation.Simulation3DCellCentered( self.mesh, survey=self.survey, sigma=self.sigma, bc_type="Mixed" ) - simulation.solver = Solver f = simulation.fields() eNumeric = utils.mkvc(f[self.survey.source_list, "e"]) @@ -165,7 +159,6 @@ def test_Simulation3DCellCentered_Neumann(self, tolerance=0.1): simulation = dc.Simulation3DCellCentered( self.mesh, survey=self.survey, sigma=self.sigma, bc_type="Neumann" ) - simulation.solver = Solver f = simulation.fields() eNumeric = utils.mkvc(f[self.survey.source_list, "e"]) @@ -251,14 +244,14 @@ def setUp(self): ROI_large_TSE = np.array([75, -75, 75]) ROI_largeInds = utils.model_builder.get_indices_block( ROI_large_BNW, ROI_large_TSE, edgeGrid - )[0] + ) # print(ROI_largeInds.shape) ROI_small_BNW = np.array([-4, 4, -4]) ROI_small_TSE = np.array([4, -4, 4]) ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, edgeGrid - )[0] + ) # print(ROI_smallInds.shape) ROIedgeInds = np.setdiff1d(ROI_largeInds, ROI_smallInds) @@ -276,7 +269,6 @@ def test_Simulation3DNodal(self, tolerance=0.1): simulation = dc.simulation.Simulation3DNodal( self.mesh, survey=self.survey, sigma=self.sigma ) - simulation.solver = Solver f = simulation.fields() eNumeric = utils.mkvc(f[self.survey.source_list, "e"]) diff --git a/tests/em/static/test_DC_FieldsMultipoleFullspace.py b/tests/em/static/test_DC_FieldsMultipoleFullspace.py index d5e9b8ebfc..4f9c63ed04 100644 --- a/tests/em/static/test_DC_FieldsMultipoleFullspace.py +++ b/tests/em/static/test_DC_FieldsMultipoleFullspace.py @@ -5,11 +5,6 @@ import numpy as np from simpeg.electromagnetics import resistivity as dc -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - from geoana.em import fdem from scipy.constants import mu_0, epsilon_0 @@ -106,14 +101,14 @@ def setUp(self): ROI_large_TSE = np.array([75, -75, 75]) ROI_largeInds = utils.model_builder.get_indices_block( ROI_large_BNW, ROI_large_TSE, faceGrid - )[0] + ) # print(ROI_largeInds.shape) ROI_small_BNW = np.array([-4, 4, -4]) ROI_small_TSE = np.array([4, -4, 4]) ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, faceGrid - )[0] + ) # print(ROI_smallInds.shape) ROIfaceInds = np.setdiff1d(ROI_largeInds, ROI_smallInds) @@ -131,7 +126,6 @@ def test_Simulation3DCellCentered_Dirichlet(self, tolerance=0.1): simulation = dc.Simulation3DCellCentered( self.mesh, survey=self.survey, sigma=self.sigma, bc_type="Dirichlet" ) - simulation.solver = Solver f = simulation.fields() eNumeric = utils.mkvc(f[self.survey.source_list, "e"]) @@ -157,7 +151,6 @@ def test_Simulation3DCellCentered_Mixed(self, tolerance=0.1): simulation = dc.simulation.Simulation3DCellCentered( self.mesh, survey=self.survey, sigma=self.sigma, bc_type="Mixed" ) - simulation.solver = Solver f = simulation.fields() eNumeric = utils.mkvc(f[self.survey.source_list, "e"]) @@ -175,7 +168,6 @@ def test_Simulation3DCellCentered_Neumann(self, tolerance=0.1): simulation = dc.Simulation3DCellCentered( self.mesh, survey=self.survey, sigma=self.sigma, bc_type="Neumann" ) - simulation.solver = Solver f = simulation.fields() eNumeric = utils.mkvc(f[self.survey.source_list, "e"]) @@ -280,14 +272,14 @@ def setUp(self): ROI_large_TSE = np.array([75, -75, 75]) ROI_largeInds = utils.model_builder.get_indices_block( ROI_large_BNW, ROI_large_TSE, edgeGrid - )[0] + ) # print(ROI_largeInds.shape) ROI_small_BNW = np.array([-4, 4, -4]) ROI_small_TSE = np.array([4, -4, 4]) ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, edgeGrid - )[0] + ) # print(ROI_smallInds.shape) ROIedgeInds = np.setdiff1d(ROI_largeInds, ROI_smallInds) @@ -305,7 +297,6 @@ def test_Simulation3DNodal(self, tolerance=0.1): simulation = dc.simulation.Simulation3DNodal( self.mesh, survey=self.survey, sigma=self.sigma ) - simulation.solver = Solver f = simulation.fields() eNumeric = utils.mkvc(f[self.survey.source_list, "e"]) diff --git a/tests/em/static/test_DC_Utils.py b/tests/em/static/test_DC_Utils.py index bc872fcc85..8055041cdf 100644 --- a/tests/em/static/test_DC_Utils.py +++ b/tests/em/static/test_DC_Utils.py @@ -1,5 +1,6 @@ # import matplotlib # matplotlib.use('Agg') +import pytest import unittest import numpy as np import discretize @@ -11,11 +12,6 @@ import shutil import os -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - class DCUtilsTests_halfspace(unittest.TestCase): def setUp(self): @@ -82,17 +78,16 @@ def test_io_rhoa(self): dim=self.mesh.dim, ) - self.assertEqual(survey_type, survey.survey_type) - # Setup Problem with exponential mapping expmap = maps.ExpMap(self.mesh) problem = dc.Simulation3DCellCentered( self.mesh, sigmaMap=expmap, survey=survey, bc_type="Neumann" ) - problem.solver = Solver # Create synthetic data - dobs = problem.make_synthetic_data(self.model, relative_error=0.0) + dobs = problem.make_synthetic_data( + self.model, relative_error=0.0, random_seed=40 + ) dobs.noise_floor = 1e-5 # Testing IO @@ -345,5 +340,70 @@ def test_convert_to_2d(self): self.assertEqual(survey.locations_a[0, 0], 0) +class TestConvertTo2DInvalidInputs: + """ + Test convert_survey_3d_to_2d_lines after passing invalid inputs. + """ + + @pytest.fixture + def survey_3d(self): + """Sample 3D DC survey.""" + receiver = dc.receivers.Dipole( + locations_m=np.array([[-100, 0, 0]]), + locations_n=np.array([[100, 0, 0]]), + data_type="volt", + ) + source = dc.sources.Dipole( + receiver_list=[receiver], + location_a=np.array([-50, 0, 0]), + location_b=np.array([50, 0, 0]), + ) + survey = dc.Survey(source_list=[source]) + return survey + + @pytest.fixture + def survey_2d(self): + """Sample 2D DC survey.""" + receiver = dc.receivers.Dipole( + locations_m=np.array([[-100, 0]]), + locations_n=np.array([[100, 0]]), + data_type="volt", + ) + source = dc.sources.Dipole( + receiver_list=[receiver], + location_a=np.array([-50, 0]), + location_b=np.array([50, 0]), + ) + survey = dc.Survey(source_list=[source]) + return survey + + def test_invalid_survey(self, survey_2d): + """ + Test if error is raised when passing an invalid survey (2D survey) + """ + line_ids = np.ones(survey_2d.nD) + with pytest.raises(ValueError, match="Invalid 2D 'survey'"): + utils.convert_survey_3d_to_2d_lines(survey_2d, line_ids) + + def test_invalid_line_ids_wrong_dims(self, survey_3d): + """ + Test if error is raised after invalid line_ids with wrong dimensions. + """ + line_ids = np.atleast_2d(np.ones(survey_3d.nD)) + msg = "Invalid 'lineID' array with '2' dimensions. " + with pytest.raises(ValueError, match=msg): + utils.convert_survey_3d_to_2d_lines(survey_3d, line_ids) + + def test_invalid_line_ids_wrong_size(self, survey_3d): + """ + Test if error is raised after an invalid line_ids with wrong size. + """ + size = survey_3d.nD - 1 + line_ids = np.ones(size) + msg = f"Invalid 'lineID' array with '{size}' elements. " + with pytest.raises(ValueError, match=msg): + utils.convert_survey_3d_to_2d_lines(survey_3d, line_ids) + + if __name__ == "__main__": unittest.main() diff --git a/tests/em/static/test_DC_analytic.py b/tests/em/static/test_DC_analytic.py index d26c1a1cca..e247877902 100644 --- a/tests/em/static/test_DC_analytic.py +++ b/tests/em/static/test_DC_analytic.py @@ -6,11 +6,6 @@ from simpeg.electromagnetics import resistivity as dc from simpeg.electromagnetics import analytics -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - class DCProblemAnalyticTests(unittest.TestCase): def setUp(self): @@ -55,7 +50,6 @@ def test_Simulation3DNodal(self, tolerance=0.05): self.mesh, survey=self.survey, sigma=self.sigma, - solver=Solver, bc_type="Neumann", ) data = simulation.dpred() @@ -77,7 +71,6 @@ def test_Simulation3DNodal_Robin(self, tolerance=0.05): self.mesh, survey=self.survey, sigma=self.sigma, - solver=Solver, bc_type="Robin", ) data = simulation.dpred() @@ -93,7 +86,6 @@ def test_Simulation3DCellCentered_Mixed(self, tolerance=0.05): survey=self.survey, sigma=self.sigma, bc_type="Mixed", - solver=Solver, ) data = simulation.dpred() @@ -116,7 +108,6 @@ def test_Simulation3DCellCentered_Neumann(self, tolerance=0.05): survey=self.survey, sigma=self.sigma, bc_type="Neumann", - solver=Solver, ) data = simulation.dpred() err = np.sqrt( @@ -178,7 +169,6 @@ def test_Simulation3DCellCentered_Dirichlet(self, tolerance=0.05): survey=self.survey, sigma=self.sigma, bc_type="Dirichlet", - solver=Solver, ) data = simulation.dpred() @@ -234,7 +224,6 @@ def test_Simulation3DCellCentered_Mixed(self, tolerance=0.05): survey=self.survey, sigma=self.sigma, bc_type="Mixed", - solver=Solver, ) data = simulation.dpred() err = np.sqrt( diff --git a/tests/em/static/test_DC_jvecjtvecadj.py b/tests/em/static/test_DC_jvecjtvecadj.py index 25c08fc8e2..c99cb4e274 100644 --- a/tests/em/static/test_DC_jvecjtvecadj.py +++ b/tests/em/static/test_DC_jvecjtvecadj.py @@ -13,10 +13,8 @@ ) from simpeg.utils import mkvc from simpeg.electromagnetics import resistivity as dc -from pymatsolver import Pardiso import shutil -np.random.seed(40) TOL = 1e-5 FLR = 1e-20 # "zero", so if residual below this --> pass regardless of order @@ -46,13 +44,13 @@ def setUp(self): ) mSynth = np.ones(mesh.nC) - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -72,14 +70,16 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=918367, ) self.assertTrue(passed) def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(mkvc(self.dobs).shape[0]) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=mkvc(self.dobs).shape[0]) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-10 @@ -88,7 +88,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=6 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=6, + random_seed=63, ) self.assertTrue(passed) @@ -129,27 +133,28 @@ def setUp(self): mesh=mesh, survey=self.survey, sigmaMap=self.sigma_map, - solver=Pardiso, bc_type="Dirichlet", ) def test_e_deriv(self): - x0 = -1 + 1e-1 * np.random.rand(self.sigma_map.nP) + rng = np.random.default_rng(seed=42) + x0 = -1 + 1e-1 * rng.uniform(size=self.sigma_map.nP) def fun(x): return self.prob.dpred(x), lambda x: self.prob.Jvec(x0, x) - return tests.check_derivative(fun, x0, num=3, plotIt=False) + return tests.check_derivative(fun, x0, num=3, plotIt=False, random_seed=98253) def test_e_adjoint(self): print("Adjoint Test for e") - m = -1 + 1e-1 * np.random.rand(self.sigma_map.nP) + rng = np.random.default_rng(seed=42) + m = -1 + 1e-1 * rng.uniform(size=self.sigma_map.nP) u = self.prob.fields(m) # u = u[self.survey.source_list,'e'] - v = np.random.rand(self.survey.nD) - w = np.random.rand(self.sigma_map.nP) + v = rng.uniform(size=self.survey.nD) + w = rng.uniform(size=self.sigma_map.nP) vJw = v.dot(self.prob.Jvec(m, w, u)) wJtv = w.dot(self.prob.Jtvec(m, v, u)) @@ -185,13 +190,13 @@ def setUp(self): ) mSynth = np.ones(mesh.nC) - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -211,14 +216,16 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=825, ) self.assertTrue(passed) def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(mkvc(self.dobs).shape[0]) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=mkvc(self.dobs).shape[0]) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -227,7 +234,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=3456845, ) self.assertTrue(passed) @@ -262,7 +273,7 @@ def setUp(self): dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -282,14 +293,16 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=562, ) self.assertTrue(passed) def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(mkvc(self.dobs).shape[0]) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=mkvc(self.dobs).shape[0]) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -298,7 +311,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=1254, ) self.assertTrue(passed) @@ -327,13 +344,13 @@ def setUp(self): ) mSynth = np.ones(mesh.nC) - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -353,14 +370,16 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=98670234, ) self.assertTrue(passed) def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(mkvc(self.dobs).shape[0]) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=mkvc(self.dobs).shape[0]) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-10 @@ -369,7 +388,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=4 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=4, + random_seed=5613789, ) self.assertTrue(passed) @@ -405,13 +428,13 @@ def setUp(self): ) mSynth = np.ones(mesh.nC) - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -431,14 +454,16 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=346, ) self.assertTrue(passed) def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(mkvc(self.dobs).shape[0]) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=mkvc(self.dobs).shape[0]) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -447,7 +472,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=83475902, ) self.assertTrue(passed) @@ -487,13 +516,13 @@ def setUp(self): ) mSynth = np.ones(mesh.nC) - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(simulation=simulation, data=dobs) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -513,14 +542,16 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=908762, ) self.assertTrue(passed) def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(mkvc(self.dobs).shape[0]) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=mkvc(self.dobs).shape[0]) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -529,7 +560,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=63426, ) self.assertTrue(passed) diff --git a/tests/em/static/test_DC_miniaturize.py b/tests/em/static/test_DC_miniaturize.py index 95a2fb67a7..317bbe4dd4 100644 --- a/tests/em/static/test_DC_miniaturize.py +++ b/tests/em/static/test_DC_miniaturize.py @@ -2,7 +2,6 @@ from simpeg.electromagnetics.static.utils.static_utils import generate_dcip_sources_line from simpeg import maps import numpy as np -from pymatsolver import Pardiso import discretize import os @@ -173,7 +172,6 @@ def setUp(self): self.sim1 = dc.Simulation2DNodal( survey=survey, mesh=mesh, - solver=Pardiso, storeJ=False, sigmaMap=maps.IdentityMap(mesh), miniaturize=False, @@ -182,7 +180,6 @@ def setUp(self): self.sim2 = dc.Simulation2DNodal( survey=survey, mesh=mesh, - solver=Pardiso, storeJ=False, sigmaMap=maps.IdentityMap(mesh), miniaturize=True, @@ -198,13 +195,15 @@ def test_dpred(self): self.assertTrue(np.allclose(d1, d2)) def test_Jvec(self): - u = np.random.rand(*self.model.shape) + rng = np.random.default_rng(seed=42) + u = rng.uniform(size=self.model.shape) J1u = self.sim1.Jvec(self.model, u, f=self.f1) J2u = self.sim2.Jvec(self.model, u, f=self.f2) self.assertTrue(np.allclose(J1u, J2u)) def test_Jtvec(self): - v = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.survey.nD) J1tv = self.sim1.Jtvec(self.model, v, f=self.f1) J2tv = self.sim2.Jtvec(self.model, v, f=self.f2) self.assertTrue(np.allclose(J1tv, J2tv)) @@ -269,7 +268,6 @@ def setUp(self): self.sim1 = dc.Simulation3DNodal( survey=survey, mesh=mesh, - solver=Pardiso, storeJ=False, sigmaMap=maps.IdentityMap(mesh), miniaturize=False, @@ -278,7 +276,6 @@ def setUp(self): self.sim2 = dc.Simulation3DNodal( survey=survey, mesh=mesh, - solver=Pardiso, storeJ=False, sigmaMap=maps.IdentityMap(mesh), miniaturize=True, @@ -295,13 +292,15 @@ def test_dpred(self): self.assertTrue(np.allclose(d1, d2)) def test_Jvec(self): - u = np.random.rand(*self.model.shape) + rng = np.random.default_rng(seed=42) + u = rng.uniform(size=self.model.shape) J1u = self.sim1.Jvec(self.model, u, f=self.f1) J2u = self.sim2.Jvec(self.model, u, f=self.f2) self.assertTrue(np.allclose(J1u, J2u)) def test_Jtvec(self): - v = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.survey.nD) J1tv = self.sim1.Jtvec(self.model, v, f=self.f1) J2tv = self.sim2.Jtvec(self.model, v, f=self.f2) self.assertTrue(np.allclose(J1tv, J2tv)) diff --git a/tests/em/static/test_IP_2D_fwd.py b/tests/em/static/test_IP_2D_fwd.py index 93c22b41e9..59c883a231 100644 --- a/tests/em/static/test_IP_2D_fwd.py +++ b/tests/em/static/test_IP_2D_fwd.py @@ -6,11 +6,6 @@ from simpeg.electromagnetics import resistivity as dc from simpeg.electromagnetics import induced_polarization as ip -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - class IPProblemAnalyticTests(unittest.TestCase): def setUp(self): @@ -57,7 +52,6 @@ def test_Simulation2DNodal(self): problemDC = dc.Simulation2DNodal( self.mesh, survey=self.surveyDC, sigmaMap=maps.IdentityMap(self.mesh) ) - problemDC.solver = Solver data0 = problemDC.dpred(self.sigma0) datainf = problemDC.dpred(self.sigmaInf) @@ -69,7 +63,6 @@ def test_Simulation2DNodal(self): sigma=self.sigmaInf, etaMap=maps.IdentityMap(self.mesh), ) - problemIP.solver = Solver data_full = data0 - datainf data = problemIP.dpred(self.eta) @@ -87,7 +80,6 @@ def test_Simulation2DCellCentered(self): problemDC = dc.Simulation2DCellCentered( self.mesh, survey=self.surveyDC, rhoMap=maps.IdentityMap(self.mesh) ) - problemDC.solver = Solver data0 = problemDC.dpred(1.0 / self.sigma0) finf = problemDC.fields(1.0 / self.sigmaInf) datainf = problemDC.dpred(1.0 / self.sigmaInf, f=finf) @@ -100,7 +92,6 @@ def test_Simulation2DCellCentered(self): rho=1.0 / self.sigmaInf, etaMap=maps.IdentityMap(self.mesh), ) - problemIP.solver = Solver data_full = data0 - datainf data = problemIP.dpred(self.eta) err = np.linalg.norm((data - data_full) / data_full) ** 2 / data_full.size @@ -165,7 +156,6 @@ def test_Simulation2DNodal(self): simDC = dc.Simulation2DNodal( self.mesh, sigmaMap=maps.IdentityMap(self.mesh), - solver=Solver, survey=self.survey_dc, ) data0 = simDC.dpred(self.sigma0) @@ -176,7 +166,6 @@ def test_Simulation2DNodal(self): self.mesh, sigma=self.sigmaInf, etaMap=maps.IdentityMap(self.mesh), - solver=Solver, survey=self.survey_ip, ) data = simIP.dpred(self.eta) @@ -185,7 +174,6 @@ def test_Simulation2DNodal(self): self.mesh, sigma=self.sigmaInf, etaMap=maps.IdentityMap(self.mesh), - solver=Solver, survey=self.survey_ip, storeJ=True, ) @@ -209,7 +197,6 @@ def test_Simulation2DCellCentered(self): simDC = dc.Simulation2DCellCentered( self.mesh, sigmaMap=maps.IdentityMap(self.mesh), - solver=Solver, survey=self.survey_dc, ) data0 = simDC.dpred(self.sigma0) @@ -220,7 +207,6 @@ def test_Simulation2DCellCentered(self): self.mesh, sigma=self.sigmaInf, etaMap=maps.IdentityMap(self.mesh), - solver=Solver, survey=self.survey_ip, ) data = simIP.dpred(self.eta) diff --git a/tests/em/static/test_IP_2D_jvecjtvecadj.py b/tests/em/static/test_IP_2D_jvecjtvecadj.py index c95da77982..46ff8fe203 100644 --- a/tests/em/static/test_IP_2D_jvecjtvecadj.py +++ b/tests/em/static/test_IP_2D_jvecjtvecadj.py @@ -14,8 +14,6 @@ from simpeg.electromagnetics import resistivity as dc from simpeg.electromagnetics import induced_polarization as ip -np.random.seed(30) - class IPProblemTestsCC(unittest.TestCase): def setUp(self): @@ -49,12 +47,12 @@ def setUp(self): ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = problem.make_synthetic_data(mSynth, add_noise=True) + dobs = problem.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -73,14 +71,16 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=63426, ) self.assertTrue(passed) def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=30) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=self.survey.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-10 @@ -89,7 +89,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=7861325, ) self.assertTrue(passed) @@ -126,12 +130,12 @@ def setUp(self): ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = problem.make_synthetic_data(mSynth, add_noise=True) + dobs = problem.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -150,14 +154,16 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=87643, ) self.assertTrue(passed) def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=30) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=self.survey.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -166,7 +172,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=25938764, ) self.assertTrue(passed) diff --git a/tests/em/static/test_IP_fwd.py b/tests/em/static/test_IP_fwd.py index 3d722f931c..1ba373f891 100644 --- a/tests/em/static/test_IP_fwd.py +++ b/tests/em/static/test_IP_fwd.py @@ -7,11 +7,6 @@ from simpeg.electromagnetics import resistivity as dc from simpeg.electromagnetics import induced_polarization as ip -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - class IPProblemAnalyticTests(unittest.TestCase): def setUp(self): @@ -55,7 +50,6 @@ def test_Simulation3DNodal(self): simulationdc = dc.simulation.Simulation3DNodal( mesh=self.mesh, survey=self.surveyDC, sigmaMap=maps.IdentityMap(self.mesh) ) - simulationdc.solver = Solver data0 = simulationdc.dpred(self.sigma0) finf = simulationdc.fields(self.sigmaInf) datainf = simulationdc.dpred(self.sigmaInf, f=finf) @@ -68,7 +62,6 @@ def test_Simulation3DNodal(self): Ainv=simulationdc.Ainv, _f=finf, ) - simulationip.solver = Solver data_full = data0 - datainf data = simulationip.dpred(self.eta) err = np.linalg.norm((data - data_full) / data_full) ** 2 / data_full.size @@ -84,7 +77,6 @@ def test_Simulation3DCellCentered(self): simulationdc = dc.simulation.Simulation3DCellCentered( mesh=self.mesh, survey=self.surveyDC, sigmaMap=maps.IdentityMap(self.mesh) ) - simulationdc.solver = Solver data0 = simulationdc.dpred(self.sigma0) finf = simulationdc.fields(self.sigmaInf) datainf = simulationdc.dpred(self.sigmaInf, f=finf) @@ -97,7 +89,6 @@ def test_Simulation3DCellCentered(self): Ainv=simulationdc.Ainv, _f=finf, ) - simulationip.solver = Solver data_full = data0 - datainf data = simulationip.dpred(self.eta) err = np.linalg.norm((data - data_full) / data_full) ** 2 / data_full.size @@ -157,7 +148,6 @@ def test_Simulation3DNodal(self): simulationdc = dc.simulation.Simulation3DNodal( self.mesh, sigmaMap=maps.IdentityMap(self.mesh), - solver=Solver, survey=self.survey_dc, ) data0 = simulationdc.dpred(self.sigma0) @@ -170,7 +160,6 @@ def test_Simulation3DNodal(self): sigma=self.sigmaInf, etaMap=maps.IdentityMap(self.mesh), Ainv=simulationdc.Ainv, - solver=Solver, ) data = simulationip.dpred(self.eta) @@ -180,7 +169,6 @@ def test_Simulation3DNodal(self): sigma=self.sigmaInf, etaMap=maps.IdentityMap(self.mesh), Ainv=simulationdc.Ainv, - solver=Solver, storeJ=True, ) data2 = simulationip_stored.dpred(self.eta) @@ -203,7 +191,6 @@ def test_Simulation3DCellCentered(self): simulationdc = dc.simulation.Simulation3DCellCentered( self.mesh, sigmaMap=maps.IdentityMap(self.mesh), - solver=Solver, survey=self.survey_dc, ) data0 = simulationdc.dpred(self.sigma0) @@ -216,7 +203,6 @@ def test_Simulation3DCellCentered(self): sigma=self.sigmaInf, etaMap=maps.IdentityMap(self.mesh), Ainv=simulationdc.Ainv, - solver=Solver, ) data = simulationip.dpred(self.eta) diff --git a/tests/em/static/test_IP_jvecjtvecadj.py b/tests/em/static/test_IP_jvecjtvecadj.py index 8884b3b385..f3906d5026 100644 --- a/tests/em/static/test_IP_jvecjtvecadj.py +++ b/tests/em/static/test_IP_jvecjtvecadj.py @@ -14,8 +14,6 @@ from simpeg.electromagnetics import induced_polarization as ip import shutil -np.random.seed(30) - class IPProblemTestsCC(unittest.TestCase): def setUp(self): @@ -41,12 +39,12 @@ def setUp(self): mesh=mesh, survey=survey, sigma=sigma, etaMap=maps.IdentityMap(mesh) ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -66,14 +64,16 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=63426, ) self.assertTrue(passed) def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.Survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=30) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=self.survey.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-10 @@ -82,7 +82,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=234623, ) self.assertTrue(passed) @@ -111,12 +115,12 @@ def setUp(self): mesh=mesh, survey=survey, sigma=sigma, etaMap=maps.IdentityMap(mesh) ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -135,14 +139,16 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=63462, ) self.assertTrue(passed) def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.Survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=30) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=self.survey.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -151,7 +157,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=5234, ) self.assertTrue(passed) @@ -184,12 +194,12 @@ def setUp(self): storeJ=True, ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -208,14 +218,16 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=4512, ) self.assertTrue(passed) def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.Survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=30) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=self.survey.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-10 @@ -224,7 +236,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=541, ) self.assertTrue(passed) @@ -264,12 +280,12 @@ def setUp(self): storeJ=True, ) mSynth = np.ones(mesh.nC) * 0.1 - dobs = simulation.make_synthetic_data(mSynth, add_noise=True) + dobs = simulation.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=simulation) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -288,14 +304,16 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=512, ) self.assertTrue(passed) def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.Survey.nSrc) - v = np.random.rand(self.mesh.nC) - w = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=30) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=self.survey.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -304,7 +322,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=87623, ) self.assertTrue(passed) diff --git a/tests/em/static/test_SIP_2D_jvecjtvecadj.py b/tests/em/static/test_SIP_2D_jvecjtvecadj.py index 751aa7ea98..9068da25d4 100644 --- a/tests/em/static/test_SIP_2D_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_2D_jvecjtvecadj.py @@ -14,13 +14,6 @@ import numpy as np from simpeg.electromagnetics import spectral_induced_polarization as sip -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - -np.random.seed(38) - class SIPProblemTestsCC(unittest.TestCase): def setUp(self): @@ -63,17 +56,16 @@ def setUp(self): etaMap=wires.eta, tauiMap=wires.taui, verbose=False, - solver=Solver, survey=survey, ) mSynth = np.r_[eta, 1.0 / tau] problem.model = mSynth - dobs = problem.make_synthetic_data(mSynth, add_noise=True) + dobs = problem.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -93,14 +85,16 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=51, ) self.assertTrue(passed) def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC * 2) - w = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=38) + v = rng.uniform(size=self.mesh.nC * 2) + w = rng.uniform(size=self.survey.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-10 @@ -109,7 +103,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=38, ) self.assertTrue(passed) @@ -155,17 +153,16 @@ def setUp(self): etaMap=wires.eta, tauiMap=wires.taui, verbose=False, - solver=Solver, survey=survey, ) mSynth = np.r_[eta, 1.0 / tau] problem.model = mSynth - dobs = problem.make_synthetic_data(mSynth, add_noise=True) + dobs = problem.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -185,13 +182,15 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=643, ) self.assertTrue(passed) def test_adjoint(self): # Adjoint Test - v = np.random.rand(self.mesh.nC * 2) - w = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=38) + v = rng.uniform(size=self.mesh.nC * 2) + w = rng.uniform(size=self.survey.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -200,7 +199,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=2 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=2, + random_seed=521, ) self.assertTrue(passed) @@ -257,11 +260,10 @@ def setUp(self): tauiMap=actmaptau * wires.taui, cMap=actmapc * wires.c, actinds=~airind, - solver=Solver, survey=survey, ) mSynth = np.r_[eta[~airind], 1.0 / tau[~airind], c[~airind]] - dobs = problem.make_synthetic_data(mSynth, add_noise=True) + dobs = problem.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg_eta = regularization.WeightedLeastSquares( @@ -275,7 +277,7 @@ def setUp(self): ) reg = reg_eta + reg_taui + reg_c opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -295,13 +297,15 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=575, ) self.assertTrue(passed) def test_adjoint(self): # Adjoint Test - v = np.random.rand(self.reg.mapping.nP) - w = np.random.rand(self.survey.nD) + rng = np.random.default_rng(seed=38) + v = rng.uniform(size=self.reg.mapping.nP) + w = rng.uniform(size=self.survey.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -310,7 +314,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=57556, ) self.assertTrue(passed) diff --git a/tests/em/static/test_SIP_jvecjtvecadj.py b/tests/em/static/test_SIP_jvecjtvecadj.py index d9400f6f1e..4929aae0fe 100644 --- a/tests/em/static/test_SIP_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_jvecjtvecadj.py @@ -13,13 +13,6 @@ import numpy as np from simpeg.electromagnetics import spectral_induced_polarization as sip -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - -np.random.seed(38) - class SIPProblemTestsCC(unittest.TestCase): def setUp(self): @@ -71,15 +64,14 @@ def setUp(self): tauiMap=wires.taui, storeJ=False, ) - problem.solver = Solver mSynth = np.r_[eta, 1.0 / tau] problem.model = mSynth - dobs = problem.make_synthetic_data(mSynth, add_noise=True) + dobs = problem.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -99,14 +91,16 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=51, ) self.assertTrue(passed) def test_adjoint(self): # Adjoint Test # u = np.random.rand(self.mesh.nC*self.survey.nSrc) - v = np.random.rand(self.mesh.nC * 2) - w = np.random.rand(self.dobs.shape[0]) + rng = np.random.default_rng(seed=38) + v = rng.uniform(size=self.mesh.nC * 2) + w = rng.uniform(size=self.dobs.shape[0]) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-10 @@ -115,7 +109,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=51, ) self.assertTrue(passed) @@ -168,16 +166,15 @@ def setUp(self): storeJ=False, ) print(survey.nD) - problem.solver = Solver mSynth = np.r_[eta, 1.0 / tau] print(survey.nD) - dobs = problem.make_synthetic_data(mSynth, add_noise=True) + dobs = problem.make_synthetic_data(mSynth, add_noise=True, random_seed=40) print(survey.nD) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg = regularization.WeightedLeastSquares(mesh) opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -197,13 +194,15 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=5432, ) self.assertTrue(passed) def test_adjoint(self): # Adjoint Test - v = np.random.rand(self.mesh.nC * 2) - w = np.random.rand(self.dobs.shape[0]) + rng = np.random.default_rng(seed=38) + v = rng.uniform(size=self.mesh.nC * 2) + w = rng.uniform(size=self.dobs.shape[0]) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -212,7 +211,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=553254, ) self.assertTrue(passed) @@ -277,9 +280,8 @@ def setUp(self): verbose=False, ) - problem.solver = Solver mSynth = np.r_[eta[~airind], 1.0 / tau[~airind], c[~airind]] - dobs = problem.make_synthetic_data(mSynth, add_noise=True) + dobs = problem.make_synthetic_data(mSynth, add_noise=True, random_seed=40) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) reg_eta = regularization.Sparse(mesh, mapping=wires.eta, active_cells=~airind) @@ -287,7 +289,7 @@ def setUp(self): reg_c = regularization.Sparse(mesh, mapping=wires.c, active_cells=~airind) reg = reg_eta + reg_taui + reg_c opt = optimization.InexactGaussNewton( - maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=6 + maxIterLS=20, maxIter=10, tolF=1e-6, tolX=1e-6, tolG=1e-6, cg_maxiter=6 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) inv = inversion.BaseInversion(invProb) @@ -307,13 +309,15 @@ def test_misfit(self): self.m0, plotIt=False, num=3, + random_seed=754, ) self.assertTrue(passed) def test_adjoint(self): # Adjoint Test - v = np.random.rand(self.reg.mapping.nP) - w = np.random.rand(self.dobs.shape[0]) + rng = np.random.default_rng(seed=38) + v = rng.uniform(size=self.reg.mapping.nP) + w = rng.uniform(size=self.dobs.shape[0]) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < 1e-8 @@ -322,7 +326,11 @@ def test_adjoint(self): def test_dataObj(self): passed = tests.check_derivative( - lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + lambda m: [self.dmis(m), self.dmis.deriv(m)], + self.m0, + plotIt=False, + num=3, + random_seed=2234, ) self.assertTrue(passed) diff --git a/tests/em/static/test_SPjvecjtvecadj.py b/tests/em/static/test_SPjvecjtvecadj.py index 8d806a3de3..583d530c6d 100644 --- a/tests/em/static/test_SPjvecjtvecadj.py +++ b/tests/em/static/test_SPjvecjtvecadj.py @@ -43,8 +43,8 @@ def test_forward(): mesh=mesh, survey=dc_survey, sigma=conductivity ) - dc_dpred = sim_dc.make_synthetic_data(None, add_noise=False) - sp_dpred = sim.make_synthetic_data(q, add_noise=False) + dc_dpred = sim_dc.make_synthetic_data(None, add_noise=False, random_seed=40) + sp_dpred = sim.make_synthetic_data(q, add_noise=False, random_seed=40) np.testing.assert_allclose(dc_dpred.dobs, sp_dpred.dobs) @@ -71,8 +71,9 @@ def Jvec(v): return d, Jvec - m0 = np.random.randn(q_map.shape[1]) - check_derivative(func, m0, plotIt=False) + rng = np.random.default_rng(seed=42) + m0 = rng.normal(size=q_map.shape[1]) + check_derivative(func, m0, plotIt=False, random_seed=rng) @pytest.mark.parametrize( @@ -87,7 +88,8 @@ def test_adjoint(q_map): sim.model = None sim.qMap = q_map - model = np.random.rand(q_map.shape[1]) + rng = np.random.default_rng(seed=42) + model = rng.uniform(size=q_map.shape[1]) f = sim.fields(model) def Jvec(v): @@ -96,7 +98,9 @@ def Jvec(v): def Jtvec(v): return sim.Jtvec(model, v, f=f) - assert_isadjoint(Jvec, Jtvec, shape_u=(q_map.shape[1],), shape_v=(survey.nD)) + assert_isadjoint( + Jvec, Jtvec, shape_u=(q_map.shape[1],), shape_v=(survey.nD), random_seed=rng + ) def test_errors(): @@ -110,36 +114,17 @@ def test_clears(): # set qMap as a non-linear map to make sure it adds the correct # items to be cleared on model update sim.qMap = maps.IdentityMap() - assert sim.deleteTheseOnModelUpdate == [] - assert sim.clean_on_model_update == [] + assert sim._delete_on_model_update == [] sim.storeJ = True sim.qMap = maps.ExpMap() - assert sim.deleteTheseOnModelUpdate == ["_Jmatrix", "_gtgdiag"] - assert sim.clean_on_model_update == [] + assert sim._delete_on_model_update == ["_Jmatrix", "_gtgdiag"] def test_deprecations(): """ - Test warning after importing deprecated `spontaneous_potential` module + Test error after importing deprecated `spontaneous_potential` module """ - msg = ( - "The 'spontaneous_potential' module has been renamed to 'self_potential'. " - "Please use the 'self_potential' module instead. " - "The 'spontaneous_potential' module will be removed in SimPEG 0.23." - ) - with pytest.warns(FutureWarning, match=msg): + msg = "The 'spontaneous_potential' module has been moved to 'self_potential'" + with pytest.raises(ImportError, match=msg): import simpeg.electromagnetics.static.spontaneous_potential # noqa: F401 - - -def test_imported_objects_on_deprecated_module(): - """ - Test if the new `self_potential` module and the deprecated `spontaneous - potential` have the same members. - """ - import simpeg.electromagnetics.static.spontaneous_potential as spontaneous - - members_self = set([m for m in dir(sp) if not m.startswith("_")]) - members_spontaneous = set([m for m in dir(spontaneous) if not m.startswith("_")]) - difference = members_self - members_spontaneous - assert not difference diff --git a/tests/em/static/test_dc_sources_interface.py b/tests/em/static/test_dc_sources_interface.py new file mode 100644 index 0000000000..74a85ea5e1 --- /dev/null +++ b/tests/em/static/test_dc_sources_interface.py @@ -0,0 +1,152 @@ +""" +Test interface for some DC sources. +""" + +import pytest +import numpy as np +from simpeg.electromagnetics.static import resistivity as dc + + +class TestDipoleLocations: + r""" + Test the location, location_a and location_b arguments for the Dipole + + Considering that `location`, `location_a`, `location_b` can be None or not + None, then we have 8 different possible combinations. + + + .. code:: + + | location | location_a | location_b | Result | + |----------|------------|------------|--------| + | None | None | None | Error | + | None | None | not None | Error | + | None | not None | None | Error | + | None | not None | not None | Run | + | not None | None | None | Run | + | not None | None | not None | Error | + | not None | not None | None | Error | + | not None | not None | not None | Error | + """ + + @pytest.fixture + def receiver(self): + """Sample DC dipole receiver.""" + receiver = dc.receivers.Dipole( + locations_m=np.array([[-100, 0]]), + locations_n=np.array([[100, 0]]), + data_type="volt", + ) + return receiver + + def test_all_nones(self, receiver): + """ + Test error being raised when passing all location as None + """ + msg = "Found 'location', 'location_a' and 'location_b' as None. " + with pytest.raises(TypeError, match=msg): + dc.sources.Dipole( + receiver_list=[receiver], + location_a=None, + location_b=None, + location=None, + ) + + @pytest.mark.parametrize("electrode", ("a", "b", "both")) + def test_not_nones(self, receiver, electrode): + """ + Test error after location as not None, and location_a and/or location_b + as not None + """ + msg = ( + "Found 'location_a' and/or 'location_b' as not None values. " + "When passing a not None value for 'location', 'location_a' and " + "'location_b' should be set to None." + ) + electrode_a = np.array([-1.0, 0.0]) + electrode_b = np.array([1.0, 0.0]) + if electrode == "a": + kwargs = dict(location_a=electrode_a, location_b=None) + elif electrode == "b": + kwargs = dict(location_a=None, location_b=electrode_b) + else: + kwargs = dict(location_a=electrode_a, location_b=electrode_b) + with pytest.raises(TypeError, match=msg): + dc.sources.Dipole( + receiver_list=[receiver], + location=[electrode_a, electrode_b], + **kwargs, + ) + + @pytest.mark.parametrize("none_electrode", ("a", "b")) + def test_single_location_as_none(self, receiver, none_electrode): + """ + Test error after location is None and one of location_a or location_b + is also None. + """ + msg = ( + f"Invalid 'location_{none_electrode}' set to None. " + "When 'location' is None, both 'location_a' and 'location_b' " + "should be set to a value different than None." + ) + electrode_a = np.array([-1.0, 0.0]) + electrode_b = np.array([1.0, 0.0]) + if none_electrode == "a": + kwargs = dict(location_a=None, location_b=electrode_b) + else: + kwargs = dict(location_a=electrode_a, location_b=None) + with pytest.raises(TypeError, match=msg): + dc.sources.Dipole( + receiver_list=[receiver], + location=None, + **kwargs, + ) + + def test_location_none(self, receiver): + """ + Test if object is correctly initialized with location set to None + """ + electrode_a = np.array([-1.0, 0.0]) + electrode_b = np.array([1.0, 0.0]) + source = dc.sources.Dipole( + receiver_list=[receiver], + location_a=electrode_a, + location_b=electrode_b, + location=None, + ) + assert isinstance(source.location, np.ndarray) + assert len(source.location) == 2 + np.testing.assert_allclose(source.location, [electrode_a, electrode_b]) + + def test_location_not_none(self, receiver): + """ + Test if object is correctly initialized with location is set + """ + electrode_a = np.array([-1.0, 0.0]) + electrode_b = np.array([1.0, 0.0]) + source = dc.sources.Dipole( + receiver_list=[receiver], + location=[electrode_a, electrode_b], + ) + assert isinstance(source.location, np.ndarray) + assert len(source.location) == 2 + np.testing.assert_allclose(source.location, [electrode_a, electrode_b]) + + @pytest.mark.parametrize("length", (0, 1, 3)) + def test_location_invalid_num_elements(self, length, receiver): + """ + Test error after passing location with invalid number of elements + """ + if length == 0: + location = () + elif length == 1: + location = (np.array([1.0, 0.0]),) + else: + location = ( + np.array([1.0, 0.0]), + np.array([1.0, 0.0]), + np.array([1.0, 0.0]), + ) + msg = "location must be a list or tuple of length 2" + with pytest.raises(ValueError, match=msg): + dc.sources.Dipole(receiver_list=[receiver], location=location) diff --git a/tests/em/static/test_dc_survey.py b/tests/em/static/test_dc_survey.py new file mode 100644 index 0000000000..9e5f85c814 --- /dev/null +++ b/tests/em/static/test_dc_survey.py @@ -0,0 +1,144 @@ +""" +Tests for resistivity (DC) survey objects. +""" + +import re +import pytest +import numpy as np + +from discretize import TensorMesh +from simpeg.electromagnetics.static.resistivity import Survey +from simpeg.electromagnetics.static.resistivity import sources +from simpeg.electromagnetics.static.resistivity import receivers + + +class TestRemovedSourceType: + """Tests after removing the source_type argument and property.""" + + def test_error_after_argument(self): + """ + Test error after passing ``source_type`` as argument to the constructor. + """ + msg = "Argument 'survey_type' has been removed" + with pytest.raises(TypeError, match=msg): + Survey(source_list=[], survey_type="dipole-dipole") + + def test_error_removed_property(self): + """ + Test if error is raised when accessing the ``survey_type`` property. + """ + survey = Survey(source_list=[]) + msg = "'survey_type' has been removed." + with pytest.raises(AttributeError, match=msg): + survey.survey_type + with pytest.raises(AttributeError, match=msg): + survey.survey_type = "dipole-dipole" + + +class TestDeprecatedArgsDrapeElectrodes: + """ + Test the deprecated arguments in ``drape_electrodes_on_topography``. + + Deprecated arguments: + + - ``ind_active`` was removed, + - ``option`` was deprecated, + - ``topography`` is not used and was deprecated, + - ``force`` is not used and was deprecated, + - non-empty ``kwargs`` raise error. + """ + + @pytest.fixture + def mesh(self): + return TensorMesh((5, 5, 5)) + + @pytest.fixture + def survey(self): + """Test if warning is raised after passing ``option`` as argument.""" + receivers_list = [ + receivers.Dipole( + locations_m=[[1, 2, 3], [4, 5, 6]], + locations_n=[[7, 8, 9], [10, 11, 12]], + ) + ] + sources_list = [ + sources.Dipole( + receivers_list, location_a=[0.5, 1.5, 2.5], location_b=[4.5, 5.5, 6.5] + ) + ] + return Survey(source_list=sources_list) + + def test_error_ind_active(self, mesh): + """ + Test if error is raised after passing ``ind_active`` as argument. + """ + survey = Survey(source_list=[]) + active_cells = np.ones(mesh.n_cells, dtype=bool) + msg = re.escape("Unsupported keyword argument") + with pytest.raises(TypeError, match=msg): + survey.drape_electrodes_on_topography( + mesh, active_cells, ind_active=active_cells + ) + + def test_deprecated_option(self, mesh, survey): + """Test if warning is raised after passing ``option`` as argument.""" + msg = ( + "Argument ``option`` is deprecated in favor of ``topo_cell_cutoff`` " + "and will be removed in SimPEG v0.27.0." + ) + active_cells = np.ones(mesh.n_cells, dtype=bool) + with pytest.warns(FutureWarning, match=msg): + survey.drape_electrodes_on_topography(mesh, active_cells, option="top") + + def test_deprecated_topography(self, mesh, survey): + """ + Test warning after passing ``topography`` as argument. + """ + active_cells = np.ones(mesh.n_cells, dtype=bool) + msg = re.escape("The `topography` argument is not used") + with pytest.warns(FutureWarning, match=msg): + survey.drape_electrodes_on_topography(mesh, active_cells, topography="blah") + + def test_deprecated_force(self, mesh, survey): + """ + Test warning after passing ``force`` as argument. + """ + active_cells = np.ones(mesh.n_cells, dtype=bool) + msg = re.escape("The `force` argument is not used") + with pytest.warns(FutureWarning, match=msg): + survey.drape_electrodes_on_topography(mesh, active_cells, force="blah") + + @pytest.mark.filterwarnings( + r"ignore:The `force` argument is not used:FutureWarning" + ) + @pytest.mark.filterwarnings( + r"ignore:The `topography` argument is not used:FutureWarning" + ) + def test_non_empty_kwargs(self, mesh): + """ + Test error after passing non empty kwargs. + """ + survey = Survey(source_list=[]) + active_cells = np.ones(mesh.n_cells, dtype=bool) + msg = re.escape("Unsupported keyword argument") + with pytest.raises(TypeError, match=msg): + survey.drape_electrodes_on_topography( + mesh, active_cells, force="blah", topography="blah", other_arg="blah" + ) + + +def test_repr(): + """Test the __repr__ method of the survey.""" + receivers_list = [ + receivers.Dipole( + locations_m=[[1, 2, 3], [4, 5, 6]], locations_n=[[7, 8, 9], [10, 11, 12]] + ) + ] + sources_list = [ + sources.Dipole( + receivers_list, location_a=[0.5, 1.5, 2.5], location_b=[4.5, 5.5, 6.5] + ) + ] + survey = Survey(source_list=sources_list) + expected_repr = "Survey(#sources: 1; #data: 2)" + assert repr(survey) == expected_repr diff --git a/tests/em/static/test_model_assignment.py b/tests/em/static/test_model_assignment.py new file mode 100644 index 0000000000..cc21f59bb8 --- /dev/null +++ b/tests/em/static/test_model_assignment.py @@ -0,0 +1,180 @@ +""" +Test model assignment to simulation classes + +Test if the `getJ` method of a few static EM simulations updates the `model`. +These tests have been added as part of the bugfix in #1361. +""" + +import pytest +import numpy as np + +from discretize import TensorMesh +from simpeg import utils +from simpeg.maps import IdentityMap, Wires +from simpeg.electromagnetics import resistivity as dc +from simpeg.electromagnetics import spectral_induced_polarization as sip +from simpeg.electromagnetics.static.utils import generate_dcip_sources_line + + +class TestDCSimulations: + @pytest.fixture + def mesh_3d(self): + """Sample mesh.""" + cell_size = 0.5 + npad = 2 + hx = [(cell_size, npad, -1.5), (cell_size, 10), (cell_size, npad, 1.5)] + hy = [(cell_size, npad, -1.5), (cell_size, 10), (cell_size, npad, 1.5)] + hz = [(cell_size, npad, -1.5), (cell_size, 10), (cell_size, npad, 1.5)] + mesh = TensorMesh([hx, hy, hz], x0="CCC") + return mesh + + @pytest.fixture + def survey_3d(self, mesh_3d): + """Sample survey.""" + xmin, xmax = mesh_3d.nodes_x.min(), mesh_3d.nodes_x.max() + ymin, ymax = mesh_3d.nodes_y.min(), mesh_3d.nodes_y.max() + x = mesh_3d.nodes_x[(mesh_3d.nodes_x > xmin) & (mesh_3d.nodes_x < xmax)] + y = mesh_3d.nodes_y[(mesh_3d.nodes_y > ymin) & (mesh_3d.nodes_y < ymax)] + + Aloc = np.r_[1.25, 0.0, 0.0] + Bloc = np.r_[-1.25, 0.0, 0.0] + M = utils.ndgrid(x - 1.0, y, np.r_[0.0]) + N = utils.ndgrid(x + 1.0, y, np.r_[0.0]) + rx = dc.receivers.Dipole(M, N) + src = dc.sources.Dipole([rx], Aloc, Bloc) + survey = dc.survey.Survey([src]) + return survey + + @pytest.fixture + def mesh_2d(self): + """Sample mesh.""" + cell_size = 0.5 + width = 10.0 + hx = [ + (cell_size, 10, -1.3), + (cell_size, width / cell_size), + (cell_size, 10, 1.3), + ] + hy = [(cell_size, 3, -1.3), (cell_size, 3, 1.3)] + mesh = TensorMesh([hx, hy], "CN") + return mesh + + @pytest.fixture + def survey_2d(self, mesh_2d): + """Sample survey.""" + survey_end_points = np.array([-5.0, 5.0, 0, 0]) + + source_list = generate_dcip_sources_line( + "dipole-dipole", "volt", "2D", survey_end_points, 0.0, 5, 2.5 + ) + survey = dc.survey.Survey(source_list) + return survey + + @pytest.mark.parametrize( + "simulation_class", + (dc.simulation.Simulation3DNodal, dc.simulation.Simulation3DCellCentered), + ) + @pytest.mark.parametrize("storeJ", [True, False]) + def test_simulation_3d(self, mesh_3d, survey_3d, simulation_class, storeJ): + """ + Test model assignment on the ``getJ`` method of 3d simulations + """ + mapping = IdentityMap(mesh_3d) + simulation = simulation_class( + mesh=mesh_3d, survey=survey_3d, sigmaMap=mapping, storeJ=storeJ + ) + model_1 = np.ones(mesh_3d.nC) * 1e-2 + model_2 = np.ones(mesh_3d.nC) * 1e-1 + # Call `getJ` passing a model and check if it was correctly assigned + j_1 = simulation.getJ(model_1) + assert model_1 is simulation.model + # Call `getJ` passing a different model and check if it was correctly assigned + j_2 = simulation.getJ(model_2) + assert model_2 is simulation.model + # Check if the two Js are different + assert not np.allclose(j_1, j_2) + + @pytest.mark.parametrize( + "simulation_class", + (dc.simulation_2d.Simulation2DNodal, dc.simulation_2d.Simulation2DCellCentered), + ) + @pytest.mark.parametrize("storeJ", [True, False]) + def test_simulation_2d(self, mesh_2d, survey_2d, simulation_class, storeJ): + """ + Test model assignment on the ``getJ`` method of 2d simulations + """ + mapping = IdentityMap(mesh_2d) + simulation = simulation_class( + mesh=mesh_2d, survey=survey_2d, sigmaMap=mapping, storeJ=storeJ + ) + model_1 = np.ones(mesh_2d.nC) * 1e-2 + model_2 = np.ones(mesh_2d.nC) * 1e-1 + # Call `getJ` passing a model and check if it was correctly assigned + j_1 = simulation.getJ(model_1) + assert model_1 is simulation.model + # Call `getJ` passing a different model and check if it was correctly assigned + j_2 = simulation.getJ(model_2) + assert model_2 is simulation.model + # Check if the two Js are different + assert not np.allclose(j_1, j_2) + + +class TestSIPSimulations: + @pytest.fixture + def mesh_3d(self): + """Sample mesh.""" + cs = 25.0 + hx = [(cs, 0, -1.3), (cs, 21), (cs, 0, 1.3)] + hy = [(cs, 0, -1.3), (cs, 21), (cs, 0, 1.3)] + hz = [(cs, 0, -1.3), (cs, 20)] + mesh = TensorMesh([hx, hy, hz], x0="CCN") + return mesh + + @pytest.fixture + def survey_3d(self, mesh_3d): + """Sample survey.""" + x = mesh_3d.cell_centers_x[ + (mesh_3d.cell_centers_x > -155.0) & (mesh_3d.cell_centers_x < 155.0) + ] + y = mesh_3d.cell_centers_y[ + (mesh_3d.cell_centers_y > -155.0) & (mesh_3d.cell_centers_y < 155.0) + ] + Aloc = np.r_[-200.0, 0.0, 0.0] + Bloc = np.r_[200.0, 0.0, 0.0] + M = utils.ndgrid(x - 25.0, y, np.r_[0.0]) + + times = np.arange(10) * 1e-3 + 1e-3 + rx = sip.receivers.Pole(M, times) + src = sip.sources.Dipole([rx], Aloc, Bloc) + survey = sip.Survey([src]) + return survey + + @pytest.mark.xfail( + reason=( + "SIP simulation requires some care to pass this test. " + "See #1361 for more details." + ) + ) + def test_simulation_3d(self, mesh_3d, survey_3d): + """ + Test model assignment on the ``getJ`` method of 3d simulations + """ + wires = Wires(("eta", mesh_3d.nC), ("taui", mesh_3d.nC)) + sigma = np.ones(mesh_3d.nC) * 1e-2 + simulation = sip.Simulation3DNodal( + mesh_3d, + sigma=sigma, + survey=survey_3d, + etaMap=wires.eta, + tauiMap=wires.taui, + ) + model_1 = np.r_[sigma, 1.0 / sigma] + model_2 = np.r_[sigma * 2, 1.0 / sigma] + # Call `getJ` passing a model and check if it was correctly assigned + j_1 = simulation.getJ(model_1) + assert model_1 is simulation.model + # Call `getJ` passing a different model and check if it was correctly assigned + j_2 = simulation.getJ(model_2) + assert model_2 is simulation.model + # Check if the two Js are different + assert not np.allclose(j_1, j_2) diff --git a/tests/em/static/test_sip_survey.py b/tests/em/static/test_sip_survey.py new file mode 100644 index 0000000000..c39523c0de --- /dev/null +++ b/tests/em/static/test_sip_survey.py @@ -0,0 +1,32 @@ +""" +Tests for Spectral IP (SIP) survey objects. +""" + +import pytest + +from simpeg.electromagnetics.static.spectral_induced_polarization import Survey + + +class TestRemovedSourceType: + """ + Tests after removing the source_type argument and property. + """ + + def test_error_after_argument(self): + """ + Test error after passing ``source_type`` as argument to the constructor. + """ + msg = "Argument 'survey_type' has been removed" + with pytest.raises(TypeError, match=msg): + Survey(source_list=[], survey_type="dipole-dipole") + + def test_error_removed_property(self): + """ + Test if error is raised when accessing the ``survey_type`` property. + """ + survey = Survey(source_list=[]) + msg = "'survey_type' has been removed." + with pytest.raises(AttributeError, match=msg): + survey.survey_type + with pytest.raises(AttributeError, match=msg): + survey.survey_type = "dipole-dipole" diff --git a/tests/em/static/test_spectral_ip_mappings.py b/tests/em/static/test_spectral_ip_mappings.py new file mode 100644 index 0000000000..f82379c90c --- /dev/null +++ b/tests/em/static/test_spectral_ip_mappings.py @@ -0,0 +1,41 @@ +""" +Test ``spectral_ip_mappings`` function. +""" + +import pytest +import numpy as np + +import discretize +from simpeg.electromagnetics.static.spectral_induced_polarization import ( + spectral_ip_mappings, +) + + +class TestDeprecatedIndActive: + """Test deprecated ``indActive`` argument ``spectral_ip_mappings``.""" + + OLD_NAME = "indActive" + NEW_NAME = "active_cells" + + @pytest.fixture + def mesh(self): + """Sample mesh.""" + return discretize.TensorMesh([10, 10, 10], "CCN") + + @pytest.fixture + def active_cells(self, mesh): + """Sample active cells for the mesh.""" + active_cells = np.ones(mesh.n_cells, dtype=bool) + active_cells[0] = False + return active_cells + + def test_error_argument(self, mesh, active_cells): + """ + Test if error is raised after passing ``indActive`` as argument. + """ + msg = ( + "'indActive' was removed in SimPEG v0.24.0, " + "please use 'active_cells' instead." + ) + with pytest.raises(TypeError, match=msg): + spectral_ip_mappings(mesh, indActive=active_cells) diff --git a/tests/em/static/test_static_utils.py b/tests/em/static/test_static_utils.py new file mode 100644 index 0000000000..e2aaa1691c --- /dev/null +++ b/tests/em/static/test_static_utils.py @@ -0,0 +1,90 @@ +""" +Test functions in ``static_utils``. +""" + +import re +import pytest +import numpy as np + +import discretize +from simpeg.electromagnetics.static.utils.static_utils import ( + drapeTopotoLoc, + gettopoCC, + closestPointsGrid, +) + + +@pytest.fixture +def mesh(): + """Sample mesh.""" + return discretize.TensorMesh([10, 10, 10], "CCN") + + +@pytest.fixture +def points(): + """Sample points.""" + return np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) + + +@pytest.fixture +def active_cells(mesh): + """Sample active cells for the mesh.""" + active_cells = np.ones(mesh.n_cells, dtype=bool) + active_cells[0] = False + return active_cells + + +class TestDeprecatedIndActive: + """Test deprecated ``ind_active`` argument ``drapeTopotoLoc``.""" + + OLD_NAME = "ind_active" + NEW_NAME = "active_cells" + + @pytest.mark.filterwarnings("ignore:The `drapeTopotoLoc` function is deprecated") + def test_error_argument(self, mesh, points, active_cells): + """ + Test if error is raised after passing ``ind_active`` as argument. + """ + msg = "Unsupported keyword argument ind_active" + with pytest.raises(TypeError, match=msg): + drapeTopotoLoc(mesh, points, ind_active=active_cells) + + +class TestDeprecatedFunctions: + """Test deprecated functions.""" + + def test_drape_topo_warning(self, mesh, points, active_cells): + """ + Test deprecation warning for `drapeTopotoLoc`. + """ + msg = re.escape( + "The `drapeTopotoLoc` function is deprecated, " + "and will be removed in SimPEG v0.27.0. " + "This functionality has been replaced by the " + "'shift_to_discrete_topography' function, which can be imported from" + "simpeg.utils" + ) + with pytest.warns(FutureWarning, match=msg): + drapeTopotoLoc(mesh, points, active_cells=active_cells) + + def test_topo_cells_warning(self, mesh, active_cells): + """ + Test deprecation warning for `drapeTopotoLoc`. + """ + msg = re.escape( + "The `gettopoCC` function is deprecated, " + "and will be removed in SimPEG v0.27.0. " + "This functionality has been replaced by the " + "'get_discrete_topography' function, which can be imported from" + "simpeg.utils" + ) + with pytest.warns(FutureWarning, match=msg): + gettopoCC(mesh, active_cells) + + def test_closest_points(self, mesh, points): + """ + Test deprecation warning for `closestPointsGrid`. + """ + msg = re.escape("The `closestPointsGrid` function is now deprecated.") + with pytest.warns(FutureWarning, match=msg): + closestPointsGrid(mesh.cell_centers, points) diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint.py b/tests/em/tdem/test_TDEM_DerivAdjoint.py index 49d0b4476d..69514dd054 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint.py @@ -5,8 +5,6 @@ from simpeg import maps, tests from simpeg.electromagnetics import time_domain as tdem -from pymatsolver import Pardiso as Solver - plotIt = False testDeriv = True @@ -46,7 +44,6 @@ def get_prob(mesh, mapping, formulation, **kwargs): mesh, sigmaMap=mapping, **kwargs ) prb.time_steps = [(1e-05, 10), (5e-05, 10), (2.5e-4, 10)] - prb.solver = Solver return prb @@ -67,8 +64,9 @@ def setUpClass(self): mapping = get_mapping(mesh) self.survey = get_survey() self.prob = get_prob(mesh, mapping, self.formulation, survey=self.survey) - self.m = np.log(1e-1) * np.ones(self.prob.sigmaMap.nP) + 1e-3 * np.random.randn( - self.prob.sigmaMap.nP + rng = np.random.default_rng(seed=42) + self.m = np.log(1e-1) * np.ones(self.prob.sigmaMap.nP) + 1e-3 * rng.normal( + size=self.prob.sigmaMap.nP ) print("Solving Fields for problem {}".format(self.formulation)) t = time.time() @@ -103,7 +101,6 @@ def set_receiver_list(self, rxcomp): src.receiver_list = rxlist def JvecTest(self, rxcomp): - np.random.seed(10) self.set_receiver_list(rxcomp) def derChk(m): @@ -117,17 +114,19 @@ def derChk(m): prbtype=self.formulation, rxcomp=rxcomp ) ) - tests.check_derivative(derChk, self.m, plotIt=False, num=2, eps=1e-20) + tests.check_derivative( + derChk, self.m, plotIt=False, num=2, eps=1e-20, random_seed=12 + ) def JvecVsJtvecTest(self, rxcomp): - np.random.seed(10) self.set_receiver_list(rxcomp) print( "\nAdjoint Testing Jvec, Jtvec prob {}, {}".format(self.formulation, rxcomp) ) - m = np.random.rand(self.prob.sigmaMap.nP) - d = np.random.randn(self.prob.survey.nD) + rng = np.random.default_rng(seed=42) + m = rng.uniform(size=self.prob.sigmaMap.nP) + d = rng.normal(size=self.prob.survey.nD) V1 = d.dot(self.prob.Jvec(self.m, m, f=self.fields)) V2 = m.dot(self.prob.Jtvec(self.m, d, f=self.fields)) tol = TOL * (np.abs(V1) + np.abs(V2)) / 2.0 @@ -146,8 +145,9 @@ def test_eDeriv_m_adjoint(self): print("\n Testing eDeriv_m Adjoint") - m = np.random.rand(len(self.m)) - e = np.random.randn(prb.mesh.nE) + rng = np.random.default_rng(seed=42) + m = rng.uniform(size=len(self.m)) + e = rng.normal(size=prb.mesh.nE) V1 = e.dot(f._eDeriv_m(1, prb.survey.source_list[0], m)) V2 = m.dot(f._eDeriv_m(1, prb.survey.source_list[0], e, adjoint=True)) tol = TOL * (np.abs(V1) + np.abs(V2)) / 2.0 @@ -162,8 +162,9 @@ def test_eDeriv_u_adjoint(self): prb = self.prob f = self.fields - b = np.random.rand(prb.mesh.nF) - e = np.random.randn(prb.mesh.nE) + rng = np.random.default_rng(seed=42) + b = rng.uniform(size=prb.mesh.nF) + e = rng.normal(size=prb.mesh.nE) V1 = e.dot(f._eDeriv_u(1, prb.survey.source_list[0], b)) V2 = b.dot(f._eDeriv_u(1, prb.survey.source_list[0], e, adjoint=True)) tol = TOL * (np.abs(V1) + np.abs(V2)) / 2.0 diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py index e650dde269..8b175a548b 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py @@ -6,7 +6,6 @@ from simpeg.electromagnetics import time_domain as tdem from simpeg.electromagnetics import utils from scipy.interpolate import interp1d -from pymatsolver import Pardiso as Solver import pytest plotIt = False @@ -47,7 +46,6 @@ def get_prob(mesh, mapping, formulation, **kwargs): prb = getattr(tdem, "Simulation3D{}".format(formulation))( mesh, sigmaMap=mapping, **kwargs ) - prb.solver = Solver return prb @@ -72,14 +70,14 @@ def setUpClass(self): time_steps = [(1e-3, 5), (1e-4, 5), (5e-5, 10), (5e-5, 10), (1e-4, 10)] t_mesh = discretize.TensorMesh([time_steps]) times = t_mesh.nodes_x - np.random.rand(412) + rng = np.random.default_rng(seed=42) self.survey = get_survey(times, self.t0) self.prob = get_prob( mesh, mapping, self.formulation, survey=self.survey, time_steps=time_steps ) self.m = np.log(1e-1) * np.ones(self.prob.sigmaMap.nP) - self.m *= 0.25 * np.random.rand(*self.m.shape) + 1 + self.m *= 0.25 * rng.uniform(size=self.m.shape) + 1 print("Solving Fields for problem {}".format(self.formulation)) t = time.time() @@ -120,7 +118,6 @@ def set_receiver_list(self, rxcomp): src.receiver_list = rxlist def JvecTest(self, rxcomp): - np.random.seed(4) self.set_receiver_list(rxcomp) def derChk(m): @@ -134,17 +131,19 @@ def derChk(m): prbtype=self.formulation, rxcomp=rxcomp ) ) - tests.check_derivative(derChk, self.m, plotIt=False, num=3, eps=1e-20) + tests.check_derivative( + derChk, self.m, plotIt=False, num=3, eps=1e-20, random_seed=5412 + ) def JvecVsJtvecTest(self, rxcomp): - np.random.seed(4) self.set_receiver_list(rxcomp) print( "\nAdjoint Testing Jvec, Jtvec prob {}, {}".format(self.formulation, rxcomp) ) - m = np.random.rand(self.prob.sigmaMap.nP) - d = np.random.randn(self.prob.survey.nD) + rng = np.random.default_rng(seed=4) + m = rng.uniform(size=self.prob.sigmaMap.nP) + d = rng.normal(size=self.prob.survey.nD) V1 = d.dot(self.prob.Jvec(self.m, m, f=self.fields)) V2 = m.dot(self.prob.Jtvec(self.m, d, f=self.fields)) tol = TOL * (np.abs(V1) + np.abs(V2)) / 2.0 @@ -308,7 +307,7 @@ def test_Jvec_adjoint_j_dhdtz(self): # return Av, ADeriv_dm # print('\n Testing ADeriv {}'.format(prbtype)) -# tests.check_derivative(AderivFun, m0, plotIt=False, num=4, eps=EPS) +# tests.check_derivative(AderivFun, m0, plotIt=False, num=4, eps=EPS, random_seed=512) # def A_adjointTest(self, prbtype): # prb, m0, mesh = setUp_TDEM(prbtype) diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py b/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py index fb764d924b..9d6e5d192e 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py @@ -3,7 +3,6 @@ import discretize from simpeg import maps, tests from simpeg.electromagnetics import time_domain as tdem -from pymatsolver import Pardiso as Solver plotIt = False @@ -14,7 +13,6 @@ def setUp_TDEM(prbtype="ElectricField", rxcomp="ElectricFieldx", src_z=0.0): - np.random.seed(10) cs = 5.0 ncx = 8 ncy = 8 @@ -55,12 +53,12 @@ def setUp_TDEM(prbtype="ElectricField", rxcomp="ElectricFieldx", src_z=0.0): time_steps = [(1e-05, 10), (5e-05, 10), (2.5e-4, 10)] - m = np.log(5e-1) * np.ones(mapping.nP) + 1e-3 * np.random.randn(mapping.nP) + rng = np.random.default_rng(seed=42) + m = np.log(5e-1) * np.ones(mapping.nP) + 1e-3 * rng.normal(size=mapping.nP) prb = getattr(tdem, "Simulation3D{}".format(prbtype))( mesh, survey=survey, time_steps=time_steps, sigmaMap=mapping ) - prb.solver = Solver return prb, m, mesh @@ -77,7 +75,10 @@ def derChk(m): return [prb.dpred(m), lambda mx: prb.Jvec(m, mx)] print("test_Jvec_{prbtype}_{rxcomp}".format(prbtype=prbtype, rxcomp=rxcomp)) - tests.check_derivative(derChk, m, plotIt=False, num=2, eps=1e-20) + + tests.check_derivative( + derChk, m, plotIt=False, num=2, eps=1e-20, random_seed=52135 + ) def test_Jvec_e_dbzdt(self): self.JvecTest("ElectricField", "MagneticFluxTimeDerivativez") @@ -107,8 +108,9 @@ def JvecVsJtvecTest( print("\nAdjoint Testing Jvec, Jtvec prob {}, {}".format(prbtype, rxcomp)) prb, m0, mesh = setUp_TDEM(prbtype, rxcomp, src_z) - m = np.random.rand(prb.sigmaMap.nP) - d = np.random.randn(prb.survey.nD) + rng = np.random.default_rng(seed=42) + m = rng.uniform(size=prb.sigmaMap.nP) + d = rng.normal(size=prb.survey.nD) print(m.shape, d.shape, m0.shape) diff --git a/tests/em/tdem/test_TDEM_crosscheck.py b/tests/em/tdem/test_TDEM_crosscheck.py index ec0742b066..905b1115d9 100644 --- a/tests/em/tdem/test_TDEM_crosscheck.py +++ b/tests/em/tdem/test_TDEM_crosscheck.py @@ -3,11 +3,8 @@ from simpeg import maps from simpeg.electromagnetics import time_domain as tdem -from simpeg.electromagnetics import utils import numpy as np -from pymatsolver import Pardiso as Solver - TOL = 1e-4 FLR = 1e-20 @@ -16,7 +13,6 @@ def setUp_TDEM( prbtype="MagneticFluxDensity", rxcomp="bz", waveform="stepoff", src_type=None ): # set a seed so that the same conductivity model is used for all runs - np.random.seed(25) cs = 10.0 ncx = 4 ncy = 4 @@ -39,13 +35,11 @@ def setUp_TDEM( ) mapping = maps.ExpMap(mesh) * maps.SurjectVertical1D(mesh) * activeMap - rxtimes = np.logspace(-4, -3, 20) + rxtimes = np.hstack([np.r_[0], np.logspace(-4, -3, 20)]) if waveform.upper() == "RAW": - out = utils.VTEMFun(prb.times, 0.00595, 0.006, 100) - wavefun = interp1d(prb.times, out) t0 = 0.006 - waveform = tdem.Src.RawWaveform(off_time=t0, waveform_function=wavefun) + waveform = tdem.sources.VTEMWaveform(off_time=t0) time_steps = [(1e-3, 5), (1e-4, 5), (5e-5, 10), (5e-5, 10), (1e-4, 10)] rxtimes = t0 + rxtimes @@ -74,9 +68,11 @@ def setUp_TDEM( prb = getattr(tdem, "Simulation3D{}".format(prbtype))( mesh, survey=survey, time_steps=time_steps, sigmaMap=mapping ) - prb.solver = Solver - m = np.log(1e-1) * np.ones(prb.sigmaMap.nP) + 1e-2 * np.random.rand(prb.sigmaMap.nP) + rng = np.random.default_rng(seed=42) + m = np.log(1e-1) * np.ones(prb.sigmaMap.nP) + 1e-2 * rng.uniform( + size=prb.sigmaMap.nP + ) return prb, m, mesh diff --git a/tests/em/tdem/test_TDEM_dipolar_sources.py b/tests/em/tdem/test_TDEM_dipolar_sources.py new file mode 100644 index 0000000000..9f6bad07f1 --- /dev/null +++ b/tests/em/tdem/test_TDEM_dipolar_sources.py @@ -0,0 +1,102 @@ +from discretize import TensorMesh + +from simpeg import maps +import simpeg.electromagnetics.time_domain as tdem + +import numpy as np + +from simpeg.utils.solver_utils import get_default_solver + +Solver = get_default_solver() + +TOL = 0.06 # relative tolerance + +# Observation times for response (time channels) +n_times = 30 +time_channels = np.logspace(-4, -1, n_times) + +# Defining transmitter locations +source_locations = np.r_[0, 0, 15.5] +receiver_locations = np.atleast_2d(np.r_[0, 0, 15.5]) + + +def create_survey(src_type="MagDipole"): + + bz_receiver = tdem.receivers.PointMagneticFluxDensity( + receiver_locations, time_channels, "z" + ) + dbdtz_receiver = tdem.receivers.PointMagneticFluxTimeDerivative( + receiver_locations, time_channels, "z" + ) + receivers_list = [bz_receiver, dbdtz_receiver] + + source_list = [ + getattr(tdem.sources, src_type)( + receivers_list, + location=source_locations, + waveform=tdem.sources.StepOffWaveform(), + moment=1.0, + orientation="z", + ) + ] + survey = tdem.Survey(source_list) + return survey + + +def test_BH_dipole(): + survey_b = create_survey() + survey_h = create_survey() + + cell_size = 20 + n_core = 10 + padding_factor = 1.3 + n_padding = 15 + + h = [ + (cell_size, n_padding, -padding_factor), + (cell_size, n_core), + (cell_size, n_padding, padding_factor), + ] + mesh = TensorMesh([h, h, h], origin="CCC") + + air_conductivity = 1e-8 + background_conductivity = 1e-1 + + model = air_conductivity * np.ones(mesh.n_cells) + model[mesh.cell_centers[:, 2] < 0] = background_conductivity + + nsteps = 10 + time_steps = [ + (1e-5, nsteps), + (3e-5, nsteps), + (1e-4, nsteps), + (3e-4, nsteps), + (1e-3, nsteps), + (3e-3, nsteps), + (1e-2, nsteps - 4), + ] + + simulation_b = tdem.simulation.Simulation3DMagneticFluxDensity( + mesh, + survey=survey_b, + sigmaMap=maps.IdentityMap(), + solver=Solver, + time_steps=time_steps, + ) + + simulation_h = tdem.simulation.Simulation3DMagneticField( + mesh, + survey=survey_h, + sigmaMap=maps.IdentityMap(), + solver=Solver, + time_steps=time_steps, + ) + + fields_b = simulation_b.fields(model) + dpred_b = simulation_b.dpred(model, f=fields_b) + + fields_h = simulation_h.fields(model) + dpred_h = simulation_h.dpred(model, f=fields_h) + + # Check if the two predicted fields are close enough + np.testing.assert_allclose(dpred_h, dpred_b, rtol=TOL) diff --git a/tests/em/tdem/test_TDEM_forward_Analytic.py b/tests/em/tdem/test_TDEM_forward_Analytic.py index 1c6e85c18e..8eae1b5da3 100644 --- a/tests/em/tdem/test_TDEM_forward_Analytic.py +++ b/tests/em/tdem/test_TDEM_forward_Analytic.py @@ -3,7 +3,6 @@ import discretize import matplotlib.pyplot as plt import numpy as np -from pymatsolver import Pardiso as Solver from scipy.constants import mu_0 from simpeg import maps from simpeg.electromagnetics import analytics @@ -163,7 +162,6 @@ def analytic_wholespace_dipole_comparison( mesh=mesh, survey=survey, sigmaMap=mapping ) - sim.solver = Solver sim.time_steps = [ (1e-06, 40), (5e-06, 40), @@ -267,7 +265,6 @@ def analytic_halfspace_mag_dipole_comparison( sim = tdem.Simulation3DMagneticFluxDensity( mesh, survey=survey, time_steps=time_steps, sigmaMap=mapping ) - sim.solver = Solver sigma = np.ones(mesh.shape_cells[2]) * 1e-8 sigma[active] = sig_half diff --git a/tests/em/tdem/test_TDEM_forward_Analytic_RawWaveform.py b/tests/em/tdem/test_TDEM_forward_Analytic_RawWaveform.py index c5a8e9ba63..5e25069fc0 100644 --- a/tests/em/tdem/test_TDEM_forward_Analytic_RawWaveform.py +++ b/tests/em/tdem/test_TDEM_forward_Analytic_RawWaveform.py @@ -3,7 +3,6 @@ import discretize import matplotlib.pyplot as plt import numpy as np -from pymatsolver import Pardiso as Solver from scipy.constants import mu_0 from scipy.interpolate import interp1d from simpeg import maps @@ -79,7 +78,6 @@ def halfSpaceProblemAnaDiff( prb = tdem.Simulation3DMagneticFluxDensity( mesh, survey=survey, sigmaMap=mapping, time_steps=time_steps ) - prb.solver = Solver sigma = np.ones(mesh.shape_cells[2]) * 1e-8 sigma[active] = sig_half diff --git a/tests/em/tdem/test_TDEM_grounded.py b/tests/em/tdem/test_TDEM_grounded.py index c85a8807cc..dcd4a451c4 100644 --- a/tests/em/tdem/test_TDEM_grounded.py +++ b/tests/em/tdem/test_TDEM_grounded.py @@ -6,7 +6,6 @@ import discretize from simpeg.electromagnetics import time_domain as tdem from simpeg import maps, tests -from pymatsolver import Pardiso class TestGroundedSourceTDEM_j(unittest.TestCase): @@ -76,7 +75,6 @@ def setUpClass(self): time_steps=time_steps, mu=mu, sigmaMap=maps.ExpMap(mesh), - solver=Pardiso, ) survey = tdem.Survey([src]) @@ -93,11 +91,12 @@ def setUpClass(self): print("Testing problem {} \n\n".format(self.prob_type)) def derivtest(self, deriv_fct): - m0 = np.log(self.sigma) + np.random.rand(self.mesh.nC) + rng = np.random.default_rng(seed=42) + m0 = np.log(self.sigma) + rng.uniform(size=self.mesh.nC) self.prob.model = m0 return tests.check_derivative( - deriv_fct, np.log(self.sigma), num=3, plotIt=False + deriv_fct, np.log(self.sigma), num=3, plotIt=False, random_seed=521 ) def test_deriv_phi(self): @@ -131,22 +130,25 @@ def deriv_check(m): self.derivtest(deriv_check) def test_adjoint_phi(self): - v = np.random.rand(self.mesh.nC) - w = np.random.rand(self.mesh.nC) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=self.mesh.nC) a = w.T.dot(self.src._phiInitialDeriv(self.prob, v=v)) b = v.T.dot(self.src._phiInitialDeriv(self.prob, v=w, adjoint=True)) self.assertTrue(np.allclose(a, b)) def test_adjoint_j(self): - v = np.random.rand(self.mesh.nC) - w = np.random.rand(self.mesh.nF) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=self.mesh.nF) a = w.T.dot(self.src.jInitialDeriv(self.prob, v=v)) b = v.T.dot(self.src.jInitialDeriv(self.prob, v=w, adjoint=True)) self.assertTrue(np.allclose(a, b)) def test_adjoint_h(self): - v = np.random.rand(self.mesh.nC) - w = np.random.rand(self.mesh.nE) + rng = np.random.default_rng(seed=42) + v = rng.uniform(size=self.mesh.nC) + w = rng.uniform(size=self.mesh.nE) a = w.T.dot(self.src.hInitialDeriv(self.prob, v=v)) b = v.T.dot(self.src.hInitialDeriv(self.prob, v=w, adjoint=True)) self.assertTrue(np.allclose(a, b)) diff --git a/tests/em/tdem/test_TDEM_inductive_permeable.py b/tests/em/tdem/test_TDEM_inductive_permeable.py index 8bf243554d..b0bd644f34 100644 --- a/tests/em/tdem/test_TDEM_inductive_permeable.py +++ b/tests/em/tdem/test_TDEM_inductive_permeable.py @@ -10,7 +10,6 @@ from simpeg.electromagnetics import time_domain as tdem from simpeg import utils, maps -from pymatsolver import Pardiso plotIt = False TOL = 1e-4 @@ -158,14 +157,12 @@ def populate_target(mur): survey=survey, time_steps=time_steps, sigmaMap=maps.IdentityMap(mesh), - solver=Pardiso, ) prob_late_ontime = tdem.Simulation3DMagneticFluxDensity( mesh=mesh, survey=survey_late_ontime, time_steps=time_steps, sigmaMap=maps.IdentityMap(mesh), - solver=Pardiso, ) fields_dict = {} @@ -232,8 +229,9 @@ def populate_target(mur): assert all(passed) prob.sigma = 1e-4 * np.ones(mesh.nC) - v = utils.mkvc(np.random.rand(mesh.nE)) - w = utils.mkvc(np.random.rand(mesh.nF)) + rng = np.random.default_rng(seed=42) + v = utils.mkvc(rng.uniform(size=mesh.nE)) + w = utils.mkvc(rng.uniform(size=mesh.nF)) assert np.all( mesh.get_edge_inner_product(1e-4 * np.ones(mesh.nC)) * v == prob.MeSigma * v ) @@ -256,8 +254,9 @@ def populate_target(mur): ) prob.rho = 1.0 / 1e-3 * np.ones(mesh.nC) - v = utils.mkvc(np.random.rand(mesh.nE)) - w = utils.mkvc(np.random.rand(mesh.nF)) + rng = np.random.default_rng(seed=42) + v = utils.mkvc(rng.uniform(size=mesh.nE)) + w = utils.mkvc(rng.uniform(size=mesh.nF)) np.testing.assert_allclose( mesh.get_edge_inner_product(1e-3 * np.ones(mesh.nC)) * v, prob.MeSigma * v diff --git a/tests/em/tdem/test_TDEM_sources.py b/tests/em/tdem/test_TDEM_sources.py index 3d4fff3896..5eac6e252f 100644 --- a/tests/em/tdem/test_TDEM_sources.py +++ b/tests/em/tdem/test_TDEM_sources.py @@ -1,12 +1,12 @@ -import pytest import unittest +import re +import pytest import numpy as np import scipy.sparse as sp from discretize.tests import check_derivative from numpy.testing import assert_array_almost_equal from simpeg.electromagnetics.time_domain.sources import ( - CircularLoop, ExponentialWaveform, HalfSineWaveform, PiecewiseLinearWaveform, @@ -38,26 +38,30 @@ def test_waveform_with_custom_off_time(self): assert_array_almost_equal(result, expected) -class TestRampOffWaveform(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.times = np.linspace(start=0, stop=1e-2, num=11) - - def test_waveform_with_whole_offtime(self): - ramp_off = RampOffWaveform(off_time=1e-2) - result = [ramp_off.eval(t) for t in self.times] - expected = np.array([1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0.0]) - assert_array_almost_equal(result, expected) +@pytest.mark.parametrize("ramp_start", [-1e-3, None, 0, 1e-3]) +@pytest.mark.parametrize("ramp_end", [1e-2, 5e-3]) +class TestRampOffWaveform: + times = np.linspace(start=-1e-2, stop=2e-2, num=31) - def test_waveform_with_partial_off_time(self): - ramp_off = RampOffWaveform(off_time=5e-3) + def test_waveform_evaluate(self, ramp_start, ramp_end): + if ramp_start is None: + args = (ramp_end,) + else: + args = (ramp_start, ramp_end) + ramp_off = RampOffWaveform(*args) + if ramp_start is None: + assert ramp_off.ramp_start == 0.0 result = [ramp_off.eval(t) for t in self.times] - expected = np.array([1.0, 0.8, 0.6, 0.4, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + expected = np.interp(self.times, ramp_off.time_nodes, [1, 0]) assert_array_almost_equal(result, expected) - def test_waveform_derivative(self): + def test_waveform_derivative(self, ramp_start, ramp_end): # Test the waveform derivative at points between the time_nodes - wave = RampOffWaveform(off_time=1e-2) + if ramp_start is None: + args = (ramp_end,) + else: + args = (ramp_start, ramp_end) + wave = RampOffWaveform(*args) def f(t): wave_eval = np.array([wave.eval(ti) for ti in t]) @@ -73,7 +77,139 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=5421) + + +@pytest.mark.parametrize("attr", ["ramp_end", "off_time"]) +def test_ramp_off_time_is_ramp_end(attr): + t_off = 0.01 + if attr == "ramp_end": + ramp = RampOffWaveform(t_off) + else: + with pytest.warns( + DeprecationWarning, match="`off_time` keyword arg has been deprecated.*" + ): + ramp = RampOffWaveform(off_time=t_off) + assert ramp.ramp_end == t_off + assert ramp.off_time == t_off + + t2_off = 0.02 + setattr(ramp, attr, t2_off) + assert ramp.ramp_end == t2_off + assert ramp.off_time == t2_off + + +def test_ramp_off_bad_end(): + with pytest.raises( + ValueError, + match=re.escape("'ramp_end' must be a value in the range (0.1, inf]"), + ): + RampOffWaveform(0.1, 0.0) + + +def test_ramp_off_good_args(): + with pytest.warns( + DeprecationWarning, match="`off_time` keyword arg has been deprecated.*" + ): + ramp = RampOffWaveform(off_time=0.1) + assert ramp.ramp_start == 0.0 + assert ramp.ramp_end == 0.1 + + ramp = RampOffWaveform(0.1) + assert ramp.ramp_start == 0.0 + assert ramp.ramp_end == 0.1 + + ramp = RampOffWaveform(ramp_end=0.1) + assert ramp.ramp_start == 0.0 + assert ramp.ramp_end == 0.1 + + ramp = RampOffWaveform(0.1, 0.2) + assert ramp.ramp_start == 0.1 + assert ramp.ramp_end == 0.2 + + ramp = RampOffWaveform(0.1, ramp_end=0.2) + assert ramp.ramp_start == 0.1 + assert ramp.ramp_end == 0.2 + + ramp = RampOffWaveform(ramp_start=0.1, ramp_end=0.2) + assert ramp.ramp_start == 0.1 + assert ramp.ramp_end == 0.2 + + ramp = RampOffWaveform(ramp_end=0.2, ramp_start=0.1) + assert ramp.ramp_start == 0.1 + assert ramp.ramp_end == 0.2 + + with pytest.warns( + DeprecationWarning, match="`off_time` keyword arg has been deprecated.*" + ): + ramp = RampOffWaveform(0.1, off_time=0.2) + assert ramp.ramp_start == 0.1 + assert ramp.ramp_end == 0.2 + + with pytest.warns( + DeprecationWarning, match="`off_time` keyword arg has been deprecated.*" + ): + ramp = RampOffWaveform(ramp_start=0.1, off_time=0.2) + assert ramp.ramp_start == 0.1 + assert ramp.ramp_end == 0.2 + + +def test_ramp_off_bad_args(): + with pytest.raises( + TypeError, + match=re.escape("Can not specify both `off_time` and a `ramp_end` value."), + ): + RampOffWaveform(0.01, 0.2, off_time=0.1) + with pytest.raises( + TypeError, + match=re.escape("Can not specify both `off_time` and a `ramp_end` value."), + ): + RampOffWaveform(ramp_end=0.2, off_time=0.1) + with pytest.raises( + TypeError, + match=re.escape("RampOffWaveform() requires `ramp_end` to be specified."), + ): + RampOffWaveform() + with pytest.raises( + TypeError, + match=re.escape("RampOffWaveform() requires `ramp_end` to be specified."), + ): + RampOffWaveform(ramp_start=0.0) + with pytest.raises( + TypeError, + match=re.escape( + "Must specify one or two positional arguments for the RampOffWaveform." + ), + ): + RampOffWaveform(0.1, 0.2, 0.3) + with pytest.raises( + TypeError, + match=re.escape( + "argument for RampOffWaveform() given by name ('ramp_start') and position (position 0)" + ), + ): + RampOffWaveform(0.1, ramp_start=0.0) + with pytest.raises( + TypeError, + match=re.escape( + "argument for RampOffWaveform() given by name ('ramp_start') and position (position 0)" + ), + ): + RampOffWaveform(0.1, 0.2, ramp_start=0.0) + with pytest.raises( + TypeError, + match=re.escape( + "argument for RampOffWaveform() given by name ('ramp_start') and position (position 0)" + ), + ): + RampOffWaveform(0.1, 0.2, ramp_start=0.1, ramp_end=0.2) + with pytest.raises( + TypeError, + match=re.escape( + "argument for RampOffWaveform() given by name ('ramp_end') and position (position 1)" + ), + ): + RampOffWaveform(0.1, 0.2, ramp_end=0.0) class TestVTEMWaveform(unittest.TestCase): @@ -115,7 +251,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=643) class TestTrapezoidWaveform(unittest.TestCase): @@ -157,7 +293,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=5277) class TestTriangularWaveform(unittest.TestCase): @@ -195,7 +331,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=874) class TestQuarterSineRampOnWaveform(unittest.TestCase): @@ -268,7 +404,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=7564) def test_waveform_without_plateau_derivative(self): # Test the waveform derivative at points between the time_nodes @@ -290,7 +426,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=12) def test_waveform_negative_plateau_derivative(self): # Test the waveform derivative at points between the time_nodes @@ -312,7 +448,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=52) class TestHalfSineWaveform(unittest.TestCase): @@ -372,7 +508,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=5) def test_waveform_without_plateau_derivative(self): # Test the waveform derivative at points between the time_nodes @@ -392,7 +528,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=6) class TestPiecewiseLinearWaveform(unittest.TestCase): @@ -430,7 +566,7 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=11) class TestExponentialWaveform(unittest.TestCase): @@ -520,25 +656,9 @@ def f(t): ) dt = np.min(np.diff(t0)) * 0.5 * np.ones_like(t0) - assert check_derivative(f, t0, dx=dt, plotIt=False) + assert check_derivative(f, t0, dx=dt, plotIt=False, random_seed=5555) def test_simple_source(): waveform = StepOffWaveform() assert waveform.eval(0.0) == 1.0 - - -def test_removal_circular_loop_n(): - """ - Test if passing the N argument to CircularLoop raises an error - """ - msg = "'N' property has been removed. Please use 'n_turns'." - with pytest.raises(TypeError, match=msg): - CircularLoop( - [], - waveform=StepOffWaveform(), - location=np.array([0.0, 0.0, 0.0]), - radius=1.0, - current=0.5, - N=2, - ) diff --git a/tests/em/tdem/test_large_loop.py b/tests/em/tdem/test_large_loop.py new file mode 100644 index 0000000000..88e08904a1 --- /dev/null +++ b/tests/em/tdem/test_large_loop.py @@ -0,0 +1,180 @@ +import numpy as np +import pytest + +import discretize +from simpeg.electromagnetics import time_domain as tdem +from simpeg import maps + +# solver +from simpeg.utils.solver_utils import get_default_solver + + +Solver = get_default_solver() + +# conductivity values +rho_back = 500 +sigma_air = 1e-8 +sigma_back = 1 / rho_back + +current = 2 +REL_TOL = 0.35 + + +def setup_mesh_model(tx_halfwidth=50): + + # design a tensor mesh + cell_size = 20 + padding_factor = 1.5 + + n_cells_x = int(tx_halfwidth * 2 / cell_size) + n_cells_z = int((tx_halfwidth) / cell_size) + 5 + n_padding_x = 11 + n_padding_z = 11 + + hx = [ + (cell_size, n_padding_x, -padding_factor), + (cell_size, n_cells_x), + (cell_size, n_padding_z, padding_factor), + ] + + hz = [ + (cell_size, n_padding_z, -padding_factor), + (cell_size, n_cells_z), + (cell_size, n_padding_z, padding_factor), + ] + + mesh = discretize.TensorMesh([hx, hx, hz], origin="CC0") + mesh.origin = mesh.origin - np.r_[0, 0, mesh.h[2][: n_padding_z + n_cells_z].sum()] + mesh.n_cells + + # define model + model = sigma_air * np.ones(mesh.n_cells) + model[mesh.cell_centers[:, 2] < 0] = sigma_back + + return mesh, model + + +def setup_survey(rx_locs, rx_times, tx_halfwidth=50, tx_z=0.5): + # transmitter + tx_halfwidth = 50 + tx_z = 0.5 # put slightly above the surface + tx_points = np.array( + [ + [-tx_halfwidth, -tx_halfwidth, tx_z], + [tx_halfwidth, -tx_halfwidth, tx_z], + [tx_halfwidth, tx_halfwidth, tx_z], + [-tx_halfwidth, tx_halfwidth, tx_z], + [-tx_halfwidth, -tx_halfwidth, tx_z], # close the loop + ] + ) + + # define survey for 3D simulation + dbdt_receivers = [ + tdem.receivers.PointMagneticFluxTimeDerivative( + locations=rx_locs, times=rx_times, orientation="z" + ) + ] + + b_receivers = [ + tdem.receivers.PointMagneticFluxDensity( + locations=rx_locs, times=rx_times, orientation=orientation + ) + for orientation in ["z"] + ] + + waveform = tdem.sources.StepOffWaveform() + + src = tdem.sources.LineCurrent( + receiver_list=b_receivers + dbdt_receivers, + location=tx_points, + waveform=waveform, + srcType="inductive", + current=current, + ) + + survey = tdem.Survey([src]) + + return survey + + +def setup_simulation(mesh, survey, simulation_type="EB"): + + nsteps = 20 + time_steps = [ + (3e-6, nsteps), + (1e-5, nsteps), + (3e-5, nsteps), + (1e-4, nsteps), + (3e-4, nsteps + 4), + ] + + if simulation_type == "EB": + simulation = tdem.simulation.Simulation3DMagneticFluxDensity( + mesh=mesh, + survey=survey, + time_steps=time_steps, + solver=Solver, + sigmaMap=maps.IdentityMap(mesh), + ) + elif simulation_type == "HJ": + simulation = tdem.simulation.Simulation3DMagneticField( + mesh=mesh, + survey=survey, + time_steps=time_steps, + solver=Solver, + sigmaMap=maps.IdentityMap(mesh), + ) + return simulation + + +@pytest.mark.parametrize("simulation_type", ["EB", "HJ"]) +def test_large_loop(simulation_type): + + tx_halfwidth = 50 + + # receiver times + rx_times = 1e-3 * np.logspace(-1, 1, 30) + + rx_x = np.r_[20] # np.linspace(-100, 100, 10) + rx_y = np.r_[20] # np.linspace(-100, 100, 10) + rx_z = np.r_[0] + + rx_locs = discretize.utils.ndgrid(rx_x, rx_y, rx_z) + + mesh, model = setup_mesh_model(tx_halfwidth=tx_halfwidth) + survey = setup_survey(rx_locs=rx_locs, rx_times=rx_times, tx_halfwidth=tx_halfwidth) + simulation = setup_simulation(mesh, survey, simulation_type) + + fields = simulation.fields(model) + dpred_numeric = simulation.dpred(model, f=fields) + + # define 1D simulation + dbdt_receivers1d = [ + tdem.receivers.PointMagneticFluxTimeDerivative( + locations=rx_locs, times=rx_times, orientation="z" + ) + ] + + b_receivers1d = [ + tdem.receivers.PointMagneticFluxDensity( + locations=rx_locs, times=rx_times, orientation="z" + ) + ] + + waveform = tdem.sources.StepOffWaveform() + src1d = tdem.sources.LineCurrent( + receiver_list=b_receivers1d + dbdt_receivers1d, + location=survey.source_list[0].location, + waveform=waveform, + srcType="inductive", + ) + + survey1d = tdem.Survey([src1d]) + + simulation_1D = tdem.simulation_1d.Simulation1DLayered( + survey=survey1d, sigmaMap=maps.IdentityMap() + ) + + dpred1d = simulation_1D.dpred(sigma_back) * current + + np.testing.assert_allclose(dpred_numeric, dpred1d, rtol=REL_TOL) diff --git a/tests/em/vrm/test_vrm_instantiation.py b/tests/em/vrm/test_vrm_instantiation.py new file mode 100644 index 0000000000..91c6af90e0 --- /dev/null +++ b/tests/em/vrm/test_vrm_instantiation.py @@ -0,0 +1,27 @@ +import discretize +import pytest +import simpeg.electromagnetics.viscous_remanent_magnetization as vrm + + +def test_mesh_required(): + with pytest.raises( + TypeError, match=".*missing 1 required positional argument: 'mesh'" + ): + vrm.Simulation3DLinear() + + +def test_bad_mesh_type(): + mesh = discretize.CylindricalMesh([3, 3, 3]) + with pytest.raises( + TypeError, + match="mesh must be an instance of TensorMesh or TreeMesh, not CylindricalMesh", + ): + vrm.Simulation3DLinear(mesh) + + +def test_bad_mesh_dim(): + mesh = discretize.TensorMesh([3, 3]) + with pytest.raises( + ValueError, match="Simulation3DLinear mesh must be 3D, received a 2D mesh." + ): + vrm.Simulation3DLinear(mesh) diff --git a/tests/em/vrm/test_vrmfwd.py b/tests/em/vrm/test_vrmfwd.py index 5e45cdcb6f..70756dfaa3 100644 --- a/tests/em/vrm/test_vrmfwd.py +++ b/tests/em/vrm/test_vrmfwd.py @@ -1,3 +1,4 @@ +import pytest import unittest import numpy as np import discretize @@ -13,8 +14,6 @@ class VRM_fwd_tests(unittest.TestCase): seed = 518936 def test_predict_dipolar(self): - np.random.seed(self.seed) - h = [0.05, 0.05] meshObj = discretize.TensorMesh((h, h, h), x0="CCC") @@ -26,8 +25,9 @@ def test_predict_dipolar(self): times = np.logspace(-4, -2, 3) waveObj = vrm.waveforms.SquarePulse(delt=0.02) - phi = np.random.uniform(-np.pi, np.pi) - psi = np.random.uniform(-np.pi, np.pi) + rng = np.random.default_rng(seed=self.seed) + phi = rng.uniform(low=-np.pi, high=np.pi) + psi = rng.uniform(low=-np.pi, high=np.pi) R = 2.0 loc_rx = ( R * np.c_[np.sin(phi) * np.cos(psi), np.sin(phi) * np.sin(psi), np.cos(phi)] @@ -43,8 +43,9 @@ def test_predict_dipolar(self): vrm.receivers.Point(loc_rx, times=times, field_type="dhdt", orientation="z") ) - alpha = np.random.uniform(0, np.pi) - beta = np.random.uniform(-np.pi, np.pi) + rng = np.random.default_rng(seed=self.seed) + alpha = rng.uniform(low=0, high=np.pi) + beta = rng.uniform(low=-np.pi, high=np.pi) loc_tx = [0.0, 0.0, 0.0] Src = vrm.sources.CircLoop( receiver_list, loc_tx, 25.0, np.r_[alpha, beta], 1.0, waveObj @@ -94,8 +95,6 @@ def test_sources(self): computed. """ - np.random.seed(self.seed) - h = [0.5, 0.5] meshObj = discretize.TensorMesh((h, h, h), x0="CCC") @@ -107,8 +106,9 @@ def test_sources(self): times = np.logspace(-4, -2, 3) waveObj = vrm.waveforms.SquarePulse(delt=0.02) - phi = np.random.uniform(-np.pi, np.pi) - psi = np.random.uniform(-np.pi, np.pi) + rng = np.random.default_rng(seed=self.seed) + phi = rng.uniform(low=-np.pi, high=np.pi) + psi = rng.uniform(low=-np.pi, high=np.pi) Rrx = 3.0 loc_rx = ( Rrx @@ -125,8 +125,9 @@ def test_sources(self): vrm.receivers.Point(loc_rx, times=times, field_type="dhdt", orientation="z") ) - alpha = np.random.uniform(0, np.pi) - beta = np.random.uniform(-np.pi, np.pi) + rng = np.random.default_rng(seed=self.seed) + alpha = rng.uniform(low=0, high=np.pi) + beta = rng.uniform(low=-np.pi, high=np.pi) Rtx = 4.0 loc_tx = ( Rtx @@ -424,8 +425,6 @@ def test_receiver_types(self): are correct. """ - np.random.seed(self.seed) - h1 = [0.25, 0.25] meshObj_Tensor = discretize.TensorMesh((h1, h1, h1), x0="CCN") @@ -439,8 +438,9 @@ def test_receiver_types(self): times = np.array([1e-3]) waveObj = vrm.waveforms.SquarePulse(delt=0.02) - phi = np.random.uniform(-np.pi, np.pi) - psi = np.random.uniform(-np.pi, np.pi) + rng = np.random.default_rng(seed=self.seed) + phi = rng.uniform(low=-np.pi, high=np.pi) + psi = rng.uniform(low=-np.pi, high=np.pi) R = 5.0 loc_rx = ( R * np.c_[np.sin(phi) * np.cos(psi), np.sin(phi) * np.sin(psi), np.cos(phi)] @@ -524,5 +524,64 @@ def test_receiver_types(self): self.assertTrue(Test) +class TestDeprecatedIndActive: + """Test deprecated ``indActive`` argument in viscous remanent mag simulations.""" + + CLASSES = ( + vrm.BaseVRMSimulation, + vrm.Simulation3DLinear, + vrm.Simulation3DLogUniform, + ) + OLD_NAME = "indActive" + NEW_NAME = "active_cells" + + @pytest.fixture + def mesh(self): + """Sample mesh.""" + return discretize.TensorMesh([10, 10, 10], "CCN") + + @pytest.fixture + def active_cells(self, mesh): + """Sample active cells for the mesh.""" + active_cells = np.ones(mesh.n_cells, dtype=bool) + active_cells[0] = False + return active_cells + + @pytest.mark.parametrize("simulation", CLASSES) + def test_error_argument(self, mesh, active_cells, simulation): + """ + Test if error is raised after passing ``indActive`` to the constructor. + """ + msg = ( + "'indActive' was removed in SimPEG v0.24.0, " + "please use 'active_cells' instead." + ) + with pytest.raises(TypeError, match=msg): + simulation(mesh, indActive=active_cells) + + @pytest.mark.parametrize("simulation", CLASSES) + def test_error_accessing_property(self, mesh, active_cells, simulation): + """ + Test error when trying to access the ``indActive`` property. + """ + sim = simulation(mesh, active_cells=active_cells) + msg = f"{self.OLD_NAME} has been removed, please use {self.NEW_NAME}" + with pytest.raises(NotImplementedError, match=msg): + sim.indActive + + @pytest.mark.parametrize("simulation", CLASSES) + def test_error_setter(self, mesh, active_cells, simulation): + """ + Test error when trying to set the ``indActive`` property. + """ + sim = simulation(mesh, active_cells=active_cells) + # Define new active cells to pass to the setter + new_active_cells = active_cells.copy() + new_active_cells[-4:] = False + msg = f"{self.OLD_NAME} has been removed, please use {self.NEW_NAME}" + with pytest.raises(NotImplementedError, match=msg): + sim.indActive = new_active_cells + + if __name__ == "__main__": unittest.main() diff --git a/tests/em/vrm/test_vrminv.py b/tests/em/vrm/test_vrminv.py index 1b64627490..78f14dd3fc 100644 --- a/tests/em/vrm/test_vrminv.py +++ b/tests/em/vrm/test_vrminv.py @@ -58,7 +58,7 @@ def test_basic_inversion(self): Survey.t_active = np.zeros(Survey.nD, dtype=bool) Survey.set_active_interval(-1e6, 1e6) Problem = vrm.Simulation3DLinear(meshObj, survey=Survey, refinement_factor=2) - dobs = Problem.make_synthetic_data(mod) + dobs = Problem.make_synthetic_data(mod, random_seed=40) Survey.noise_floor = 1e-11 dmis = data_misfit.L2DataMisfit(data=dobs, simulation=Problem) @@ -75,7 +75,7 @@ def test_basic_inversion(self): weights={"weights": W}, ) opt = optimization.ProjectedGNCG( - maxIter=20, lower=0.0, upper=1e-2, maxIterLS=20, tolCG=1e-4 + maxIter=20, lower=0.0, upper=1e-2, maxIterLS=20, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) directives = [BetaSchedule(coolingFactor=2, coolingRate=1), TargetMisfit()] diff --git a/tests/flow/test_Richards.py b/tests/flow/test_Richards.py index 71e9cc31b0..d234cc82f4 100644 --- a/tests/flow/test_Richards.py +++ b/tests/flow/test_Richards.py @@ -1,5 +1,6 @@ import unittest import numpy as np +import pytest from discretize.tests import check_derivative import discretize @@ -8,12 +9,6 @@ from simpeg import utils from simpeg.flow import richards -try: - from pymatsolver import Pardiso as Solver -except Exception: - from simpeg import Solver - - TOL = 1e-8 np.random.seed(0) @@ -39,14 +34,12 @@ def setUp(self): hydraulic_conductivity=k_fun, water_retention=theta_fun, root_finder_tol=1e-6, - debug=False, boundary_conditions=bc, initial_conditions=h, do_newton=False, method="mixed", ) prob.time_steps = time_steps - prob.solver = Solver self.h0 = h self.mesh = mesh @@ -75,6 +68,7 @@ def _dotest_getResidual(self, newton): self.h0, expectedOrder=2 if newton else 1, plotIt=False, + random_seed=142, ) self.assertTrue(passed, True) @@ -101,6 +95,7 @@ def _dotest_sensitivity(self): self.mtrue, num=3, plotIt=False, + random_seed=6184726, ) self.assertTrue(passed, True) @@ -108,7 +103,11 @@ def _dotest_sensitivity_full(self): print("Testing Richards Derivative FULL dim={}".format(self.mesh.dim)) J = self.prob.Jfull(self.mtrue) passed = check_derivative( - lambda m: [self.prob.dpred(m), J], self.mtrue, num=3, plotIt=False + lambda m: [self.prob.dpred(m), J], + self.mtrue, + num=3, + plotIt=False, + random_seed=97861534, ) self.assertTrue(passed, True) @@ -286,5 +285,21 @@ def test_sensitivity_full(self): self._dotest_sensitivity_full() +def test_bad_mesh_type(): + mesh = discretize.CylindricalMesh([3, 3, 3]) + params = richards.empirical.HaverkampParams().celia1990 + k_fun, theta_fun = richards.empirical.haverkamp(mesh, **params) + + msg = "mesh must be an instance of TensorMesh or TreeMesh, not CylindricalMesh" + with pytest.raises(TypeError, match=msg): + richards.SimulationNDCellCentered( + mesh, + hydraulic_conductivity=k_fun, + water_retention=theta_fun, + boundary_conditions=np.array([1.0]), + initial_conditions=np.array([1.0]), + ) + + if __name__ == "__main__": unittest.main() diff --git a/tests/flow/test_Richards_empirical.py b/tests/flow/test_Richards_empirical.py index 4361621e3f..1f5e8f9178 100644 --- a/tests/flow/test_Richards_empirical.py +++ b/tests/flow/test_Richards_empirical.py @@ -1,4 +1,5 @@ import unittest +import pytest import numpy as np @@ -18,7 +19,10 @@ def test_haverkamp_theta_u(self): mesh = discretize.TensorMesh([50]) hav = richards.empirical.Haverkamp_theta(mesh) passed = check_derivative( - lambda u: (hav(u), hav.derivU(u)), np.random.randn(50), plotIt=False + lambda u: (hav(u), hav.derivU(u)), + np.random.randn(50), + plotIt=False, + random_seed=5662, ) self.assertTrue(passed, True) @@ -53,14 +57,17 @@ def fun(m): print("Haverkamp_theta test m deriv: ", name) - passed = check_derivative(fun, x0, plotIt=False) + passed = check_derivative(fun, x0, plotIt=False, random_seed=444) self.assertTrue(passed, True) def test_vangenuchten_theta_u(self): mesh = discretize.TensorMesh([50]) van = richards.empirical.Vangenuchten_theta(mesh) passed = check_derivative( - lambda u: (van(u), van.derivU(u)), np.random.randn(50), plotIt=False + lambda u: (van(u), van.derivU(u)), + np.random.randn(50), + plotIt=False, + random_seed=5777, ) self.assertTrue(passed, True) @@ -95,7 +102,7 @@ def fun(m): print("Vangenuchten_theta test m deriv: ", name) - passed = check_derivative(fun, x0, plotIt=False) + passed = check_derivative(fun, x0, plotIt=False, random_seed=666) self.assertTrue(passed, True) def test_haverkamp_k_u(self): @@ -104,7 +111,10 @@ def test_haverkamp_k_u(self): hav = richards.empirical.Haverkamp_k(mesh) print("Haverkamp_k test u deriv") passed = check_derivative( - lambda u: (hav(u), hav.derivU(u)), np.random.randn(mesh.nC), plotIt=False + lambda u: (hav(u), hav.derivU(u)), + np.random.randn(mesh.nC), + plotIt=False, + random_seed=5662, ) self.assertTrue(passed, True) @@ -152,7 +162,9 @@ def fun(m): print("Haverkamp_k test m deriv: ", name) - passed = check_derivative(fun, np.random.randn(mesh.nC * nM), plotIt=False) + passed = check_derivative( + fun, np.random.randn(mesh.nC * nM), plotIt=False, random_seed=65 + ) self.assertTrue(passed, True) def test_vangenuchten_k_u(self): @@ -162,7 +174,10 @@ def test_vangenuchten_k_u(self): print("Vangenuchten_k test u deriv") passed = check_derivative( - lambda u: (van(u), van.derivU(u)), np.random.randn(mesh.nC), plotIt=False + lambda u: (van(u), van.derivU(u)), + np.random.randn(mesh.nC), + plotIt=False, + random_seed=777, ) self.assertTrue(passed, True) @@ -201,9 +216,27 @@ def fun(m): print("Vangenuchten_k test m deriv: ", name) - passed = check_derivative(fun, x0, plotIt=False) + passed = check_derivative(fun, x0, plotIt=False, random_seed=918724) self.assertTrue(passed, True) +@pytest.mark.parametrize( + "empirical_class", + [ + richards.empirical.NonLinearModel, + richards.empirical.BaseWaterRetention, + richards.empirical.BaseHydraulicConductivity, + richards.empirical.Haverkamp_theta, + richards.empirical.Haverkamp_k, + richards.empirical.Vangenuchten_theta, + richards.empirical.Vangenuchten_k, + ], +) +def test_bad_mesh_type(empirical_class): + msg = "mesh must be an instance of BaseMesh, not ndarray" + with pytest.raises(TypeError, match=msg): + empirical_class(np.array([1, 2, 3])) + + if __name__ == "__main__": unittest.main() diff --git a/tests/meta/test_dask_meta.py b/tests/meta/test_dask_meta.py index 5feb5f6c75..bca049224a 100644 --- a/tests/meta/test_dask_meta.py +++ b/tests/meta/test_dask_meta.py @@ -5,6 +5,7 @@ from discretize import TensorMesh import scipy.sparse as sp import pytest +import time from simpeg.meta import ( MetaSimulation, @@ -289,6 +290,7 @@ def test_dask_meta_errors(cluster): # incompatible length of mappings and simulations lists with pytest.raises(ValueError): DaskMetaSimulation(sims[:-1], mappings, client) + time.sleep(0.1) # sleep for a bit to let the communicator catch up # Bad Simulation type? with pytest.raises(TypeError): @@ -300,16 +302,19 @@ def test_dask_meta_errors(cluster): mappings, client, ) + time.sleep(0.1) # sleep for a bit to let the communicator catch up # mappings have incompatible input lengths: mappings[0] = maps.Projection(mesh.n_cells + 10, np.arange(mesh.n_cells) + 1) with pytest.raises(ValueError): DaskMetaSimulation(sims, mappings, client) + time.sleep(0.1) # sleep for a bit to let the communicator catch up # incompatible mapping and simulation mappings[0] = maps.Projection(mesh.n_cells, [0, 1, 3, 5, 10]) with pytest.raises(ValueError): DaskMetaSimulation(sims, mappings, client) + time.sleep(0.1) # sleep for a bit to let the communicator catch up def test_sum_errors(cluster): @@ -351,6 +356,7 @@ def test_sum_errors(cluster): # Test simulations with different numbers of data. with pytest.raises(ValueError): DaskSumMetaSimulation(sims, mappings, client) + time.sleep(0.1) # sleep for a half second to let the communicator catch up def test_repeat_errors(cluster): @@ -382,12 +388,15 @@ def test_repeat_errors(cluster): mappings[0] = maps.Projection(mesh.n_cells + 1, np.arange(mesh.n_cells) + 1) with pytest.raises(ValueError): DaskRepeatedSimulation(sim, mappings, client) + time.sleep(0.1) # sleep for a half second to let the communicator catch up # incompatible mappings and simulations mappings[0] = maps.Projection(mesh.n_cells, [0, 1, 3, 5, 10]) with pytest.raises(ValueError): DaskRepeatedSimulation(sim, mappings, client) + time.sleep(0.1) # sleep for a half second to let the communicator catch up # Bad Simulation type? with pytest.raises(TypeError): DaskRepeatedSimulation(lambda x: x * 2, mappings, client) + time.sleep(0.1) # sleep for a half second to let the communicator catch up diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py index eaabf64f6f..be2431fb10 100644 --- a/tests/meta/test_multiprocessing_sim.py +++ b/tests/meta/test_multiprocessing_sim.py @@ -1,6 +1,4 @@ import numpy as np -import multiprocessing as mp -import sys from simpeg.potential_fields import gravity from simpeg.electromagnetics.static import resistivity as dc @@ -17,9 +15,6 @@ MultiprocessingRepeatedSimulation, ) -if sys.version_info[0] == 3 and sys.version_info[1] <= 8: - mp.set_start_method("spawn") - def test_meta_correctness(): mesh = TensorMesh([16, 16, 16], origin="CCN") diff --git a/tests/pf/test_base_pf_simulation.py b/tests/pf/test_base_pf_simulation.py new file mode 100644 index 0000000000..94f3a0efe1 --- /dev/null +++ b/tests/pf/test_base_pf_simulation.py @@ -0,0 +1,333 @@ +""" +Test BasePFSimulation class +""" + +import re +import pytest +import numpy as np +from discretize import CylindricalMesh, TensorMesh, TreeMesh + +import simpeg +from simpeg.potential_fields.base import BasePFSimulation +from simpeg.survey import BaseSurvey +from simpeg.potential_fields import gravity, magnetics + + +@pytest.fixture +def mock_simulation_class(): + """ + Mock simulation class as child of BasePFSimulation + """ + + class MockSimulation(BasePFSimulation): + @property + def G(self): + """Define a dummy G property to avoid warnings on tests.""" + pass + + return MockSimulation + + +@pytest.fixture +def tensor_mesh(): + """ + Return sample TensorMesh + """ + h = (3, 3, 3) + return TensorMesh(h) + + +@pytest.fixture +def tree_mesh(): + """ + Return sample TensorMesh + """ + h = (4, 4, 4) + mesh = TreeMesh(h) + mesh.refine_points(points=(0, 0, 0), level=2) + return mesh + + +@pytest.fixture +def mock_survey_class(): + """ + Mock survey class as child of BaseSurvey + """ + + class MockSurvey(BaseSurvey): + pass + + return MockSurvey + + +class TestEngine: + """ + Test the engine property and some of its relations with other attributes + """ + + def test_invalid_engine(self, tensor_mesh, mock_simulation_class): + """ + Test if error is raised after invalid engine + """ + engine = "invalid engine" + msg = rf"'engine' must be in \('geoana', 'choclo'\). Got '{engine}'" + with pytest.raises(ValueError, match=msg): + mock_simulation_class(tensor_mesh, engine=engine) + + def test_invalid_engine_without_choclo( + self, tensor_mesh, mock_simulation_class, monkeypatch + ): + """ + Test error after choosing "choclo" as engine but not being installed + """ + monkeypatch.setattr(simpeg.potential_fields.base, "choclo", None) + engine = "choclo" + msg = "The choclo package couldn't be found." + with pytest.raises(ImportError, match=msg): + mock_simulation_class(tensor_mesh, engine=engine) + + def test_sensitivity_path_as_dir(self, tensor_mesh, mock_simulation_class, tmpdir): + """ + Test error if the sensitivity_path is a dir + + Error should be raised if using ``engine=="choclo"`` and setting + ``store_sensitivities="disk"``. + """ + sensitivity_path = str(tmpdir.mkdir("sensitivities")) + msg = re.escape( + f"The passed sensitivity_path '{sensitivity_path}' is a directory." + ) + with pytest.raises(ValueError, match=msg): + mock_simulation_class( + tensor_mesh, + engine="choclo", + store_sensitivities="disk", + sensitivity_path=sensitivity_path, + ) + + +class TestGetActiveNodes: + """ + Tests _get_active_nodes private method + """ + + def test_no_inactive_cells_tensor(self, tensor_mesh, mock_simulation_class): + """ + Test _get_active_nodes when all cells are active on a tensor mesh + """ + simulation = mock_simulation_class(tensor_mesh) + active_nodes, active_cell_nodes = simulation._get_active_nodes() + np.testing.assert_equal(active_nodes, tensor_mesh.nodes) + np.testing.assert_equal(active_cell_nodes, tensor_mesh.cell_nodes) + + def test_no_inactive_cells_tree(self, tree_mesh, mock_simulation_class): + """ + Test _get_active_nodes when all cells are active on a tree mesh + """ + simulation = mock_simulation_class(tree_mesh) + active_nodes, active_cell_nodes = simulation._get_active_nodes() + np.testing.assert_equal(active_nodes, tree_mesh.total_nodes) + np.testing.assert_equal(active_cell_nodes, tree_mesh.cell_nodes) + + def test_inactive_cells_tensor(self, tensor_mesh, mock_simulation_class): + """ + Test _get_active_nodes with some inactive cells on a tensor mesh + """ + # Define active cells: only the first cell is active + active_cells = np.zeros(tensor_mesh.n_cells, dtype=bool) + active_cells[0] = True + # Initialize simulation + simulation = mock_simulation_class(tensor_mesh, active_cells=active_cells) + # Build expected active_nodes and active_cell_nodes + expected_active_nodes = tensor_mesh.nodes[tensor_mesh[0].nodes] + expected_active_cell_nodes = np.atleast_2d(np.arange(8, dtype=int)) + # Test method + active_nodes, active_cell_nodes = simulation._get_active_nodes() + np.testing.assert_equal(active_nodes, expected_active_nodes) + np.testing.assert_equal(active_cell_nodes, expected_active_cell_nodes) + + def test_inactive_cells_tree(self, tree_mesh, mock_simulation_class): + """ + Test _get_active_nodes with some inactive cells on a tensor mesh + """ + # Define active cells: only the first cell is active + active_cells = np.zeros(tree_mesh.n_cells, dtype=bool) + active_cells[0] = True + + # Initialize simulation + simulation = mock_simulation_class(tree_mesh, active_cells=active_cells) + + # Build expected active_nodes (in the right order for a single cell) + expected_active_nodes = [ + [0, 0, 0], + [0.25, 0, 0], + [0, 0.25, 0], + [0.25, 0.25, 0], + [0, 0, 0.25], + [0.25, 0, 0.25], + [0, 0.25, 0.25], + [0.25, 0.25, 0.25], + ] + + # Run method + active_nodes, active_cell_nodes = simulation._get_active_nodes() + + # Check shape of active nodes and check if all of them are there + assert active_nodes.shape == (8, 3) + for node in expected_active_nodes: + assert node in active_nodes + + # Check shape of active_cell_nodes and check if they are in the right + # order + assert active_cell_nodes.shape == (1, 8) + for node, node_index in zip(expected_active_nodes, active_cell_nodes[0]): + np.testing.assert_equal(node, active_nodes[node_index]) + + +class TestGetComponentsAndReceivers: + """ + Test _get_components_and_receivers private method + """ + + @pytest.fixture + def receiver_locations(self): + receiver_locations = np.array([[0, 0, 0], [1, 1, 1]], dtype=np.float64) + return receiver_locations + + @pytest.fixture + def gravity_survey(self, receiver_locations): + receiver_locations = np.array([[0, 0, 0], [1, 1, 1]], dtype=np.float64) + components = ["gxy", "guv"] + receivers = gravity.receivers.Point( + receiver_locations, + components=components, + ) + # Define the SourceField and the Survey + source_field = gravity.sources.SourceField(receiver_list=[receivers]) + return gravity.Survey(source_field) + + @pytest.fixture + def magnetic_survey(self, receiver_locations): + receiver_locations = np.array([[0, 0, 0], [1, 1, 1]], dtype=np.float64) + components = ["tmi", "bx"] + receivers = magnetics.receivers.Point( + receiver_locations, + components=components, + ) + # Define the SourceField and the Survey + source_field = magnetics.sources.UniformBackgroundField( + receiver_list=[receivers], + amplitude=55_000, + inclination=45.0, + declination=12.0, + ) + return magnetics.Survey(source_field) + + def test_missing_source_field( + self, tensor_mesh, mock_survey_class, mock_simulation_class + ): + """ + Test error after missing survey in simulation + """ + survey = mock_survey_class(source_list=None) + simulation = mock_simulation_class(tensor_mesh, survey=survey) + msg = "The survey '(.*)' has no 'source_field' attribute." + with pytest.raises(AttributeError, match=msg): + # need to iterate over the generator to actually test its code + [item for item in simulation._get_components_and_receivers()] + + def test_components_and_receivers_gravity( + self, tensor_mesh, gravity_survey, mock_simulation_class, receiver_locations + ): + """ + Test method on a gravity survey + """ + simulation = mock_simulation_class(tensor_mesh, survey=gravity_survey) + components_and_receivers = tuple( + items for items in simulation._get_components_and_receivers() + ) + # Check we have a single element in the iterator + assert len(components_and_receivers) == 1 + # Check if components and receiver locations are correct + components, receivers = components_and_receivers[0] + assert components == ["gxy", "guv"] + np.testing.assert_equal(receivers, receiver_locations) + + def test_components_and_receivers_magnetics( + self, tensor_mesh, magnetic_survey, mock_simulation_class, receiver_locations + ): + """ + Test method on a magnetic survey + """ + simulation = mock_simulation_class(tensor_mesh, survey=magnetic_survey) + components_and_receivers = tuple( + items for items in simulation._get_components_and_receivers() + ) + # Check we have a single element in the iterator + assert len(components_and_receivers) == 1 + # Check if components and receiver locations are correct + components, receivers = components_and_receivers[0] + assert components == ["tmi", "bx"] + np.testing.assert_equal(receivers, receiver_locations) + + +class TestInvalidMesh: + """ + Test if errors are raised after invalid mesh are passed to the base simulation. + """ + + @pytest.fixture(params=("tensormesh", "treemesh")) + def mesh_2d(self, request): + """Sample 2D mesh.""" + hx, hy = [(0.1, 8)], [(0.1, 8)] + h = (hx, hy) + if request.param == "tensormesh": + mesh = TensorMesh(h, "CC") + else: + mesh = TreeMesh(h, origin="CC") + mesh.finalize() + return mesh + + @pytest.mark.parametrize("engine", ("choclo", "geoana")) + def test_invalid_mesh_dimensions(self, mesh_2d, mock_simulation_class, engine): + """ + Test error when passing a mesh with invalid dimensions. + """ + msg = re.escape("MockSimulation mesh must be 3D, received a 2D mesh.") + with pytest.raises(ValueError, match=msg): + mock_simulation_class(mesh_2d, engine=engine) + + def test_invalid_mesh_type(self, mock_simulation_class): + """ + Test error when passing an invalid mesh class. + """ + h = (3, 3, 3) + msg = re.escape( + "mesh must be an instance of TensorMesh or TreeMesh, not CylindricalMesh" + ) + with pytest.raises(TypeError, match=msg): + mock_simulation_class(CylindricalMesh(h)) + + +class TestRemovedIndActive: + """ + Test if using the removed ``ind_active`` argument/property raise errors. + """ + + def test_removed_argument(self, tensor_mesh, mock_simulation_class): + """Test if passing ind_active argument raises error.""" + ind_active = np.ones(tensor_mesh.n_cells, dtype=bool) + msg = ( + "'ind_active' has been removed in " + "SimPEG v0.24.0, please use 'active_cells' instead." + ) + with pytest.raises(TypeError, match=msg): + mock_simulation_class(tensor_mesh, ind_active=ind_active) + + def test_removed_property(self, tensor_mesh, mock_simulation_class): + """Test if accessing the ind_active property raises an error.""" + ind_active = np.ones(tensor_mesh.n_cells, dtype=bool) + simulation = mock_simulation_class(tensor_mesh, active_cells=ind_active) + msg = "ind_active has been removed, please use active_cells." + with pytest.raises(NotImplementedError, match=msg): + simulation.ind_active diff --git a/tests/pf/test_equivalent_sources.py b/tests/pf/test_equivalent_sources.py new file mode 100644 index 0000000000..4a726bbfd6 --- /dev/null +++ b/tests/pf/test_equivalent_sources.py @@ -0,0 +1,1142 @@ +import pytest + +from collections.abc import Iterable +import numpy as np +from discretize import TensorMesh +from discretize.utils import mesh_builder_xyz, mkvc +import simpeg +from simpeg.optimization import ProjectedGNCG +from simpeg.potential_fields import gravity, magnetics, base + +GRAVITY_COMPONENTS = ["gx", "gy", "gz", "gxx", "gyy", "gzz", "gxy", "gxz", "gyz", "guv"] +MAGNETIC_COMPONENTS = [ + "tmi", + "bx", + "by", + "bz", + "bxx", + "byy", + "bzz", + "bxy", + "bxz", + "byz", + "tmi_x", + "tmi_y", + "tmi_z", +] + +# Define a pytest.mark.xfail to use for engine parametrizations when the method that is +# being tested is not implemented when using geoana as engine. +XFAIL_GEOANA = pytest.param( + "geoana", + marks=pytest.mark.xfail(reason="not implemented", raises=NotImplementedError), +) + + +def create_grid(x_range, y_range, size): + """Create a 2D horizontal coordinates grid.""" + x_start, x_end = x_range + y_start, y_end = y_range + x, y = np.meshgrid( + np.linspace(x_start, x_end, size), np.linspace(y_start, y_end, size) + ) + return x, y + + +@pytest.fixture() +def mesh_params(): + """Parameters for building the sample meshes.""" + h = [5, 5] + padding_distances = np.ones((2, 2)) * 50 + return h, padding_distances + + +@pytest.fixture() +def tensor_mesh(mesh_params, coordinates): + """Sample 2D tensor mesh to use with equivalent sources.""" + mesh_type = "tensor" + h, padding_distance = mesh_params + mesh = mesh_builder_xyz( + coordinates[:, :2], h, padding_distance=padding_distance, mesh_type=mesh_type + ) + return mesh + + +@pytest.fixture() +def tree_mesh(mesh_params, coordinates): + """Sample 2D tree mesh to use with equivalent sources.""" + mesh_type = "tree" + h, padding_distance = mesh_params + mesh = mesh_builder_xyz( + coordinates[:, :2], h, padding_distance=padding_distance, mesh_type=mesh_type + ) + mesh.refine_points(coordinates[:, :2], padding_cells_by_level=[2, 4]) + return mesh + + +@pytest.fixture(params=["tensor", "tree"]) +def mesh(tensor_mesh, tree_mesh, request): + """Sample 2D mesh to use with equivalent sources.""" + mesh_type = request.param + if mesh_type == "tree": + return tree_mesh + elif mesh_type == "tensor": + return tensor_mesh + else: + raise ValueError(f"Invalid mesh type: '{mesh_type}'") + + +@pytest.fixture +def mesh_top(): + """Top boundary of the mesh.""" + return -20.0 + + +@pytest.fixture +def mesh_bottom(): + """Bottom boundary of the mesh.""" + return -50.0 + + +@pytest.fixture +def coordinates(): + """Synthetic observation points grid.""" + x, y = create_grid(x_range=(-50, 50), y_range=(-50, 50), size=11) + z = np.full_like(x, fill_value=5.0) + return np.c_[mkvc(x), mkvc(y), mkvc(z)] + + +def get_block_model(mesh, phys_property: float | tuple): + """ + Build a block model. + + Parameters + ---------- + mesh : discretize.BaseMesh + Mesh. + phys_property : float or tuple of floats + Pass a tuple of floats if you want to generate a vector model. + + Returns + ------- + model : np.ndarray + """ + if not isinstance(phys_property, Iterable): + model = simpeg.utils.model_builder.add_block( + mesh.cell_centers, + np.zeros(mesh.n_cells), + np.r_[-20, -20], + np.r_[20, 20], + phys_property, + ) + else: + models = tuple( + simpeg.utils.model_builder.add_block( + mesh.cell_centers, + np.zeros(mesh.n_cells), + np.r_[-20, -20], + np.r_[20, 20], + p, + ) + for p in phys_property + ) + model = np.hstack(models) + return model + + +def get_mapping(mesh): + """Get an identity map for the given mesh.""" + return simpeg.maps.IdentityMap(nP=mesh.n_cells) + + +def get_mesh_3d(mesh, top: float, bottom: float): + """ + Build a 3D mesh analogous to the 2D mesh + the top and bottom bounds. + """ + origin = (*mesh.origin, bottom) + h = (*mesh.h, np.array([top - bottom], dtype=np.float64)) + mesh_3d = TensorMesh(h=h, origin=origin) + return mesh_3d + + +@pytest.fixture +def gravity_survey(coordinates): + """ + Sample survey for the gravity equivalent sources. + """ + return build_gravity_survey(coordinates, components="gz") + + +@pytest.fixture +def magnetic_survey(coordinates): + """ + Sample survey for the magnetic equivalent sources. + """ + return build_magnetic_survey(coordinates, components="tmi") + + +def build_gravity_survey(coordinates, components): + """ + Build a gravity survey. + """ + receivers = gravity.Point(coordinates, components=components) + source_field = gravity.SourceField([receivers]) + survey = gravity.Survey(source_field) + return survey + + +def build_magnetic_survey( + coordinates, components, amplitude=51_000.0, inclination=71.0, declination=12.0 +): + """ + Build a magnetic survey. + """ + receivers = magnetics.Point(coordinates, components=components) + source_field = magnetics.UniformBackgroundField( + [receivers], + amplitude=amplitude, + inclination=inclination, + declination=declination, + ) + survey = magnetics.Survey(source_field) + return survey + + +class Test3DMeshError: + """ + Test if error is raised after passing a 3D mesh to equivalent sources. + """ + + @pytest.fixture + def mesh_3d(self): + mesh = TensorMesh([2, 3, 4]) + return mesh + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + def test_error_on_gravity(self, mesh_3d, engine): + """ + Test error is raised after passing a 3D mesh to gravity eq source class. + """ + msg = "SimulationEquivalentSourceLayer mesh must be 2D, received a 3D mesh." + with pytest.raises(ValueError, match=msg): + gravity.SimulationEquivalentSourceLayer( + mesh=mesh_3d, cell_z_top=0.0, cell_z_bottom=-2.0, engine=engine + ) + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + def test_error_on_mag(self, mesh_3d, engine): + """ + Test error is raised after passing a 3D mesh to magnetic eq source class. + """ + msg = "SimulationEquivalentSourceLayer mesh must be 2D, received a 3D mesh." + with pytest.raises(ValueError, match=msg): + magnetics.SimulationEquivalentSourceLayer( + mesh=mesh_3d, cell_z_top=0.0, cell_z_bottom=-2.0, engine=engine + ) + + def test_error_on_base_class(self, mesh_3d): + """ + Test error is raised after passing a 3D mesh to the eq source base class. + """ + msg = "BaseEquivalentSourceLayerSimulation mesh must be 2D, received a 3D mesh." + with pytest.raises(ValueError, match=msg): + base.BaseEquivalentSourceLayerSimulation( + mesh=mesh_3d, cell_z_top=0.0, cell_z_bottom=-2.0 + ) + + +class TestGravityEquivalentSourcesForward: + """ + Test the forward capabilities of the gravity equivalent sources. + """ + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + @pytest.mark.parametrize("store_sensitivities", ("ram", "forward_only")) + def test_forward_vs_simulation( + self, + tensor_mesh, + mesh_bottom, + mesh_top, + gravity_survey, + engine, + store_sensitivities, + ): + """ + Test forward of the eq sources vs. using the integral 3d simulation. + """ + # Build 3D mesh that is analogous to the 2D mesh with bottom and top + mesh_3d = get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) + # Build simulations + mapping = get_mapping(tensor_mesh) + sim_3d = gravity.Simulation3DIntegral( + survey=gravity_survey, mesh=mesh_3d, rhoMap=mapping + ) + eq_sources = gravity.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=gravity_survey, + rhoMap=mapping, + engine=engine, + store_sensitivities=store_sensitivities, + ) + # Compare predictions of both simulations + model = get_block_model(tensor_mesh, 2.67) + np.testing.assert_allclose( + sim_3d.dpred(model), eq_sources.dpred(model), atol=1e-7 + ) + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + @pytest.mark.parametrize("store_sensitivities", ("ram", "forward_only")) + @pytest.mark.parametrize("components", GRAVITY_COMPONENTS + [["gz", "gzz"]]) + def test_forward_vs_simulation_with_components( + self, + coordinates, + tensor_mesh, + mesh_bottom, + mesh_top, + engine, + store_sensitivities, + components, + ): + """ + Test forward vs simulation using different gravity components. + """ + # Build survey + survey = build_gravity_survey(coordinates, components) + # Build 3D mesh that is analogous to the 2D mesh with bottom and top + mesh_3d = get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) + # Build simulations + mapping = get_mapping(tensor_mesh) + sim_3d = gravity.Simulation3DIntegral( + survey=survey, mesh=mesh_3d, rhoMap=mapping + ) + eq_sources = gravity.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=survey, + rhoMap=mapping, + engine=engine, + store_sensitivities=store_sensitivities, + ) + # Compare predictions of both simulations + model = get_block_model(tensor_mesh, 2.67) + np.testing.assert_allclose( + sim_3d.dpred(model), eq_sources.dpred(model), atol=5e-6 + ) + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + def test_forward_vs_simulation_on_disk( + self, + tensor_mesh, + mesh_bottom, + mesh_top, + gravity_survey, + engine, + tmp_path, + ): + """ + Test forward vs simulation storing sensitivities on disk. + """ + # Build 3D mesh that is analogous to the 2D mesh with bottom and top + mesh_3d = get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) + # Define sensitivity_dir + if engine == "geoana": + sensitivity_path = tmp_path / "sensitivities_geoana" + sensitivity_path.mkdir() + elif engine == "choclo": + sensitivity_path = tmp_path / "sensitivities_choclo" + # Build simulations + mapping = get_mapping(tensor_mesh) + sim_3d = gravity.Simulation3DIntegral( + survey=gravity_survey, mesh=mesh_3d, rhoMap=mapping + ) + eq_sources = gravity.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=gravity_survey, + rhoMap=mapping, + engine=engine, + store_sensitivities="disk", + sensitivity_path=str(sensitivity_path), + ) + # Compare predictions of both simulations + model = get_block_model(tensor_mesh, 2.67) + np.testing.assert_allclose(sim_3d.dpred(model), eq_sources.dpred(model)) + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + @pytest.mark.parametrize("store_sensitivities", ("ram", "forward_only")) + def test_forward_vs_simulation_with_active_cells( + self, + tensor_mesh, + mesh_bottom, + mesh_top, + gravity_survey, + engine, + store_sensitivities, + ): + """ + Test forward vs simulation using active cells. + """ + model = get_block_model(tensor_mesh, 2.67) + + # Define some inactive cells inside the block + block_cells_indices = np.indices(model.shape).ravel()[model != 0] + inactive_indices = block_cells_indices[ + : block_cells_indices.size // 2 + ] # mark half of the cells in the block as inactive + active_cells = np.ones_like(model, dtype=bool) + active_cells[inactive_indices] = False + assert not np.all(active_cells) # check we do have inactive cells + + # Keep only values of the model in the active cells + model = model[active_cells] + + # Build 3D mesh that is analogous to the 2D mesh with bottom and top + mesh_3d = get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) + + # Build simulations + mapping = simpeg.maps.IdentityMap(nP=model.size) + sim_3d = gravity.Simulation3DIntegral( + survey=gravity_survey, + mesh=mesh_3d, + rhoMap=mapping, + active_cells=active_cells, + ) + eq_sources = gravity.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=gravity_survey, + rhoMap=mapping, + engine=engine, + store_sensitivities=store_sensitivities, + active_cells=active_cells, + ) + # Compare predictions of both simulations + np.testing.assert_allclose( + sim_3d.dpred(model), eq_sources.dpred(model), atol=1e-7 + ) + + @pytest.mark.parametrize("store_sensitivities", ("ram", "forward_only")) + def test_forward_geoana_choclo( + self, mesh, mesh_bottom, mesh_top, gravity_survey, store_sensitivities + ): + """Compare forwards using geoana and choclo.""" + # Build simulations + mapping = get_mapping(mesh) + kwargs = dict( + mesh=mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=gravity_survey, + rhoMap=mapping, + store_sensitivities=store_sensitivities, + ) + sim_geoana = gravity.SimulationEquivalentSourceLayer(engine="geoana", **kwargs) + sim_choclo = gravity.SimulationEquivalentSourceLayer(engine="choclo", **kwargs) + model = get_block_model(mesh, 2.67) + np.testing.assert_allclose(sim_geoana.dpred(model), sim_choclo.dpred(model)) + + def test_forward_choclo_serial_parallel( + self, mesh, mesh_bottom, mesh_top, gravity_survey + ): + """Test forward using choclo in serial and in parallel.""" + # Build simulations + mapping = get_mapping(mesh) + kwargs = dict( + mesh=mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=gravity_survey, + rhoMap=mapping, + engine="choclo", + ) + sim_parallel = gravity.SimulationEquivalentSourceLayer( + numba_parallel=True, **kwargs + ) + sim_serial = gravity.SimulationEquivalentSourceLayer( + numba_parallel=False, **kwargs + ) + model = get_block_model(mesh, 2.67) + np.testing.assert_allclose(sim_parallel.dpred(model), sim_serial.dpred(model)) + + +@pytest.mark.parametrize("parallel", [True, False], ids=["parallel", "serial"]) +@pytest.mark.parametrize("components", [*GRAVITY_COMPONENTS, ["gz", "gzz"]]) +@pytest.mark.parametrize("engine", ["choclo", XFAIL_GEOANA]) +class TestGravityEquivalentSourcesForwardOnly: + """ + Test gravity equivalent sources methods without building the sensitivity matrix. + """ + + def test_Jvec( + self, + coordinates, + tensor_mesh, + mesh_bottom, + mesh_top, + components, + engine, + parallel, + ): + """ + Test Jvec with "forward_only" vs. J @ v with J stored in ram. + """ + # Build survey + gravity_survey = build_gravity_survey(coordinates, components=components) + # Build simulations + mapping = get_mapping(tensor_mesh) + eqs_ram, eqs_forward_only = ( + gravity.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=gravity_survey, + rhoMap=mapping, + engine=engine, + store_sensitivities=store, + numba_parallel=parallel, + ) + for store in ("ram", "forward_only") + ) + # Compare predictions of both simulations + model = get_block_model(tensor_mesh, 2.67) + vector = np.random.default_rng(seed=42).uniform(size=model.size) + expected = eqs_ram.getJ(model) @ vector + atol = np.max(np.abs(expected)) * 1e-7 + # Test Jvec + np.testing.assert_allclose( + expected, eqs_forward_only.Jvec(model, vector), atol=atol + ) + # Test getJ + np.testing.assert_allclose( + expected, eqs_forward_only.getJ(model) @ vector, atol=atol + ) + + def test_Jtvec( + self, + coordinates, + tensor_mesh, + mesh_bottom, + mesh_top, + components, + engine, + parallel, + ): + """ + Test Jtvec with "forward_only" vs. J.T @ v with J stored in ram. + """ + # Build survey + gravity_survey = build_gravity_survey(coordinates, components=components) + # Build simulations + mapping = get_mapping(tensor_mesh) + eqs_ram, eqs_forward_only = ( + gravity.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=gravity_survey, + rhoMap=mapping, + engine=engine, + store_sensitivities=store, + numba_parallel=parallel, + ) + for store in ("ram", "forward_only") + ) + # Compare predictions of both simulations + model = get_block_model(tensor_mesh, 2.67) + vector = np.random.default_rng(seed=42).uniform(size=gravity_survey.nD) + expected = eqs_ram.getJ(model).T @ vector + atol = np.max(np.abs(expected)) * 1e-7 + # Test Jtvec + np.testing.assert_allclose( + expected, eqs_forward_only.Jtvec(model, vector), atol=atol + ) + # Test getJ + np.testing.assert_allclose( + expected, eqs_forward_only.getJ(model).T @ vector, atol=atol + ) + + def test_getJtJdiag( + self, + coordinates, + tensor_mesh, + mesh_bottom, + mesh_top, + components, + engine, + parallel, + ): + """ + Test the ``getJtJdiag`` method, comparing forward_only with storing J in memory. + """ + # Build survey + gravity_survey = build_gravity_survey(coordinates, components=components) + # Build simulations + mapping = get_mapping(tensor_mesh) + eqs_ram, eqs_forward_only = ( + gravity.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=gravity_survey, + rhoMap=mapping, + engine=engine, + store_sensitivities=store, + numba_parallel=parallel, + ) + for store in ("ram", "forward_only") + ) + # Compare predictions of both simulations + model = get_block_model(tensor_mesh, 2.67) + gtgdiag_ram = eqs_ram.getJtJdiag(model) + gtgdiag_linop = eqs_forward_only.getJtJdiag(model) + atol = np.max(np.abs(gtgdiag_ram)) * 1e-7 + np.testing.assert_allclose(gtgdiag_ram, gtgdiag_linop, atol=atol) + + +class TestMagneticEquivalentSourcesForward: + """ + Test the forward capabilities of the magnetic equivalent sources. + """ + + @pytest.mark.parametrize("engine", ["geoana", "choclo"]) + @pytest.mark.parametrize("store_sensitivities", ["ram", "forward_only"]) + @pytest.mark.parametrize("model_type", ["scalar", "vector"]) + @pytest.mark.parametrize("components", [*MAGNETIC_COMPONENTS, ["tmi", "bx"]]) + def test_forward_vs_simulation( + self, + coordinates, + tensor_mesh, + mesh_bottom, + mesh_top, + engine, + store_sensitivities, + model_type, + components, + ): + """ + Test forward of the eq sources vs. using the integral 3d simulation. + """ + # Build survey + survey = build_magnetic_survey(coordinates, components) + # Build 3D mesh that is analogous to the 2D mesh with bottom and top + mesh_3d = get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) + # Build model and mapping + if model_type == "scalar": + model = get_block_model(tensor_mesh, 0.2e-3) + else: + model = get_block_model(tensor_mesh, (0.2e-3, -0.1e-3, 0.5e-3)) + mapping = simpeg.maps.IdentityMap(nP=model.size) + # Build simulations + sim_3d = magnetics.Simulation3DIntegral( + survey=survey, + mesh=mesh_3d, + chiMap=mapping, + model_type=model_type, + ) + eq_sources = magnetics.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=survey, + chiMap=mapping, + engine=engine, + store_sensitivities=store_sensitivities, + model_type=model_type, + ) + # Compare predictions of both simulations + np.testing.assert_allclose( + sim_3d.dpred(model), eq_sources.dpred(model), atol=1e-7 + ) + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + def test_forward_vs_simulation_on_disk( + self, + tensor_mesh, + mesh_bottom, + mesh_top, + magnetic_survey, + engine, + tmp_path, + ): + """ + Test forward vs simulation storing sensitivities on disk. + """ + # Build 3D mesh that is analogous to the 2D mesh with bottom and top + mesh_3d = get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) + # Define sensitivity_dir + if engine == "geoana": + sensitivity_path = tmp_path / "sensitivities_geoana" + sensitivity_path.mkdir() + elif engine == "choclo": + sensitivity_path = tmp_path / "sensitivities_choclo" + # Build simulations + mapping = get_mapping(tensor_mesh) + sim_3d = magnetics.Simulation3DIntegral( + survey=magnetic_survey, mesh=mesh_3d, chiMap=mapping + ) + eq_sources = magnetics.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=magnetic_survey, + chiMap=mapping, + engine=engine, + store_sensitivities="disk", + sensitivity_path=str(sensitivity_path), + ) + # Compare predictions of both simulations + model = get_block_model(tensor_mesh, 0.2e-3) + np.testing.assert_allclose(sim_3d.dpred(model), eq_sources.dpred(model)) + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + @pytest.mark.parametrize("store_sensitivities", ("ram", "forward_only")) + def test_forward_vs_simulation_with_active_cells( + self, + tensor_mesh, + mesh_bottom, + mesh_top, + magnetic_survey, + engine, + store_sensitivities, + ): + """ + Test forward vs simulation using active cells. + """ + model = get_block_model(tensor_mesh, 0.2e-3) + + # Define some inactive cells inside the block + block_cells_indices = np.indices(model.shape).ravel()[model != 0] + inactive_indices = block_cells_indices[ + : block_cells_indices.size // 2 + ] # mark half of the cells in the block as inactive + active_cells = np.ones_like(model, dtype=bool) + active_cells[inactive_indices] = False + assert not np.all(active_cells) # check we do have inactive cells + + # Keep only values of the model in the active cells + model = model[active_cells] + + # Build 3D mesh that is analogous to the 2D mesh with bottom and top + mesh_3d = get_mesh_3d(tensor_mesh, top=mesh_top, bottom=mesh_bottom) + + # Build simulations + mapping = simpeg.maps.IdentityMap(nP=model.size) + sim_3d = magnetics.Simulation3DIntegral( + survey=magnetic_survey, + mesh=mesh_3d, + chiMap=mapping, + active_cells=active_cells, + ) + eq_sources = magnetics.SimulationEquivalentSourceLayer( + mesh=tensor_mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=magnetic_survey, + chiMap=mapping, + engine=engine, + store_sensitivities=store_sensitivities, + active_cells=active_cells, + ) + # Compare predictions of both simulations + np.testing.assert_allclose( + sim_3d.dpred(model), eq_sources.dpred(model), atol=1e-7 + ) + + @pytest.mark.parametrize("store_sensitivities", ("ram", "forward_only")) + def test_forward_geoana_choclo( + self, mesh, mesh_bottom, mesh_top, magnetic_survey, store_sensitivities + ): + """Compare forwards using geoana and choclo.""" + # Build simulations + mapping = get_mapping(mesh) + kwargs = dict( + mesh=mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=magnetic_survey, + chiMap=mapping, + store_sensitivities=store_sensitivities, + ) + sim_geoana = magnetics.SimulationEquivalentSourceLayer( + engine="geoana", **kwargs + ) + sim_choclo = magnetics.SimulationEquivalentSourceLayer( + engine="choclo", **kwargs + ) + model = get_block_model(mesh, 0.2e-3) + np.testing.assert_allclose(sim_geoana.dpred(model), sim_choclo.dpred(model)) + + @pytest.mark.parametrize("store_sensitivities", ("ram", "forward_only")) + def test_forward_choclo_serial_parallel( + self, mesh, mesh_bottom, mesh_top, magnetic_survey, store_sensitivities + ): + """Test forward using choclo in serial and in parallel.""" + # Build simulations + mapping = get_mapping(mesh) + kwargs = dict( + mesh=mesh, + cell_z_top=mesh_top, + cell_z_bottom=mesh_bottom, + survey=magnetic_survey, + chiMap=mapping, + engine="choclo", + store_sensitivities=store_sensitivities, + ) + sim_parallel = magnetics.SimulationEquivalentSourceLayer( + numba_parallel=True, **kwargs + ) + sim_serial = magnetics.SimulationEquivalentSourceLayer( + numba_parallel=False, **kwargs + ) + model = get_block_model(mesh, 0.2e-3) + np.testing.assert_allclose(sim_parallel.dpred(model), sim_serial.dpred(model)) + + +class BaseFittingEquivalentSources: + """ + Base class to test the fitting of equivalent sources with synthetic data. + """ + + def get_mesh_top_bottom(self, mesh, array=False): + """Build the top and bottom boundaries of the mesh. + + If array is True, the outputs are going to be arrays, otherwise they'll + be floats. + """ + top, bottom = -20.0, -50.0 + if array: + rng = np.random.default_rng(seed=42) + mesh_top = np.full(mesh.n_cells, fill_value=top) + rng.normal( + scale=0.5, size=mesh.n_cells + ) + mesh_bottom = np.full(mesh.n_cells, fill_value=bottom) + rng.normal( + scale=0.5, size=mesh.n_cells + ) + else: + mesh_top, mesh_bottom = top, bottom + return mesh_top, mesh_bottom + + def build_synthetic_data(self, simulation, model): + data = simulation.make_synthetic_data( + model, + relative_error=0.0, + noise_floor=1e-3, + add_noise=True, + random_seed=1, + ) + return data + + def build_inversion(self, mesh, simulation, synthetic_data, max_iterations=20): + """Build inversion problem.""" + # Build data misfit and regularization terms + data_misfit = simpeg.data_misfit.L2DataMisfit( + simulation=simulation, data=synthetic_data + ) + regularization = simpeg.regularization.WeightedLeastSquares(mesh=mesh) + # Choose optimization + optimization = ProjectedGNCG( + maxIter=max_iterations, + maxIterLS=5, + cg_maxiter=20, + cg_rtol=1e-3, + ) + # Build inverse problem + inverse_problem = simpeg.inverse_problem.BaseInvProblem( + data_misfit, regularization, optimization + ) + # Define directives + starting_beta = simpeg.directives.BetaEstimate_ByEig( + beta0_ratio=1e-1, random_seed=42 + ) + beta_schedule = simpeg.directives.BetaSchedule(coolingFactor=3, coolingRate=1) + update_jacobi = simpeg.directives.UpdatePreconditioner() + target_misfit = simpeg.directives.TargetMisfit(chifact=1) + sensitivity_weights = simpeg.directives.UpdateSensitivityWeights( + every_iteration=False + ) + directives = [ + sensitivity_weights, + starting_beta, + beta_schedule, + update_jacobi, + target_misfit, + ] + # Define inversion + inversion = simpeg.inversion.BaseInversion(inverse_problem, directives) + return inversion + + +class TestGravityEquivalentSources(BaseFittingEquivalentSources): + """ + Test fitting gravity equivalent sources with synthetic data. + """ + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + @pytest.mark.parametrize( + "top_bottom_as_array", + (False, True), + ids=("top-bottom-float", "top-bottom-array"), + ) + def test_predictions_on_data_points( + self, + tree_mesh, + gravity_survey, + top_bottom_as_array, + engine, + ): + """ + Test eq sources predictions on the same data points. + + The equivalent sources should be able to reproduce the same data with + which they were trained. + """ + # Get mesh top and bottom + mesh_top, mesh_bottom = self.get_mesh_top_bottom( + tree_mesh, array=top_bottom_as_array + ) + # Build simulation + mapping = get_mapping(tree_mesh) + simulation = gravity.SimulationEquivalentSourceLayer( + tree_mesh, + mesh_top, + mesh_bottom, + survey=gravity_survey, + rhoMap=mapping, + engine=engine, + ) + # Generate synthetic data + model = get_block_model(tree_mesh, 2.67) + synthetic_data = self.build_synthetic_data(simulation, model) + # Build inversion + inversion = self.build_inversion(tree_mesh, simulation, synthetic_data) + # Run inversion + starting_model = np.zeros(tree_mesh.n_cells) + recovered_model = inversion.run(starting_model) + # Predict data + prediction = simulation.dpred(recovered_model) + # Check if prediction is close to the synthetic data + atol, rtol = 0.005, 1e-5 + np.testing.assert_allclose( + prediction, synthetic_data.dobs, atol=atol, rtol=rtol + ) + + +class TestMagneticEquivalentSources(BaseFittingEquivalentSources): + """ + Test fitting magnetic equivalent sources with synthetic data. + """ + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + @pytest.mark.parametrize( + "top_bottom_as_array", + (False, True), + ids=("top-bottom-float", "top-bottom-array"), + ) + def test_predictions_on_data_points( + self, + tree_mesh, + magnetic_survey, + top_bottom_as_array, + engine, + ): + """ + Test eq sources predictions on the same data points. + + The equivalent sources should be able to reproduce the same data with + which they were trained. + """ + # Get mesh top and bottom + mesh_top, mesh_bottom = self.get_mesh_top_bottom( + tree_mesh, array=top_bottom_as_array + ) + # Build simulation + mapping = get_mapping(tree_mesh) + simulation = magnetics.SimulationEquivalentSourceLayer( + tree_mesh, + mesh_top, + mesh_bottom, + survey=magnetic_survey, + chiMap=mapping, + engine=engine, + ) + # Generate synthetic data + model = get_block_model(tree_mesh, 1e-3) + synthetic_data = self.build_synthetic_data(simulation, model) + # Build inversion + inversion = self.build_inversion( + tree_mesh, simulation, synthetic_data, max_iterations=40 + ) + # Run inversion + starting_model = np.zeros(tree_mesh.n_cells) + recovered_model = inversion.run(starting_model) + # Predict data + prediction = simulation.dpred(recovered_model) + # Check if prediction is close to the synthetic data + atol, rtol = 0.005, 1e-5 + np.testing.assert_allclose( + prediction, synthetic_data.dobs, atol=atol, rtol=rtol + ) + + +@pytest.mark.parametrize("parallel", [True, False], ids=["parallel", "serial"]) +@pytest.mark.parametrize("components", [*MAGNETIC_COMPONENTS, ["tmi", "bx"]]) +@pytest.mark.parametrize("engine", ["choclo", XFAIL_GEOANA]) +@pytest.mark.parametrize("model_type", ["scalar", "vector"]) +class TestMagneticEquivalentSourcesForwardOnly: + """ + Test magnetic equivalent sources methods without building the sensitivity matrix. + """ + + def test_Jvec( + self, + coordinates, + tensor_mesh, + mesh_bottom, + mesh_top, + components, + engine, + parallel, + model_type, + ): + """ + Test Jvec with "forward_only" vs. J @ v with J stored in ram. + """ + # Build survey + magnetic_survey = build_magnetic_survey(coordinates, components) + # Define model + model = ( + get_block_model(tensor_mesh, 0.2e-3) + if model_type == "scalar" + else get_block_model(tensor_mesh, (0.2e-3, -0.1e-3, 0.5e-3)) + ) + # Build simulations + mapping = simpeg.maps.IdentityMap(nP=model.size) + eqs_ram, eqs_forward_only = ( + magnetics.SimulationEquivalentSourceLayer( + tensor_mesh, + mesh_top, + mesh_bottom, + survey=magnetic_survey, + chiMap=mapping, + engine=engine, + store_sensitivities=store, + numba_parallel=parallel, + model_type=model_type, + ) + for store in ("ram", "forward_only") + ) + # Compare predictions of both simulations + vector = np.random.default_rng(seed=42).uniform(size=model.size) + expected = eqs_ram.getJ(model) @ vector + atol = np.max(np.abs(expected)) * 1e-7 + # Test Jvec + np.testing.assert_allclose( + expected, eqs_forward_only.Jvec(model, vector), atol=atol + ) + # Test getJ() @ v + jacobian = eqs_forward_only.getJ(model) + np.testing.assert_allclose(expected, jacobian @ vector, atol=atol) + + def test_Jtvec( + self, + coordinates, + tensor_mesh, + mesh_bottom, + mesh_top, + components, + engine, + parallel, + model_type, + ): + """ + Test Jtvec with "forward_only" vs. J.T @ v with J stored in ram. + """ + # Build survey + magnetic_survey = build_magnetic_survey(coordinates, components) + # Define model + model = ( + get_block_model(tensor_mesh, 0.2e-3) + if model_type == "scalar" + else get_block_model(tensor_mesh, (0.2e-3, -0.1e-3, 0.5e-3)) + ) + # Build simulations + mapping = simpeg.maps.IdentityMap(nP=model.size) + eqs_ram, eqs_forward_only = ( + magnetics.SimulationEquivalentSourceLayer( + tensor_mesh, + mesh_top, + mesh_bottom, + survey=magnetic_survey, + chiMap=mapping, + engine=engine, + store_sensitivities=store, + numba_parallel=parallel, + model_type=model_type, + ) + for store in ("ram", "forward_only") + ) + # Compare predictions of both simulations + vector = np.random.default_rng(seed=42).uniform(size=magnetic_survey.nD) + expected = eqs_ram.getJ(model).T @ vector + atol = np.max(np.abs(expected)) * 1e-7 + # Test Jtvec + np.testing.assert_allclose( + expected, eqs_forward_only.Jtvec(model, vector), atol=atol + ) + # Test getJ().T @ v + jacobian = eqs_forward_only.getJ(model) + np.testing.assert_allclose(expected, jacobian.T @ vector, atol=atol) + + def test_getJtJdiag( + self, + coordinates, + tensor_mesh, + mesh_bottom, + mesh_top, + components, + engine, + parallel, + model_type, + ): + """ + Test the ``getJtJdiag`` method, comparing forward_only with storing J in memory. + """ + # Build survey + magnetic_survey = build_magnetic_survey(coordinates, components) + # Define model + model = ( + get_block_model(tensor_mesh, 0.2e-3) + if model_type == "scalar" + else get_block_model(tensor_mesh, (0.2e-3, -0.1e-3, 0.5e-3)) + ) + # Build simulations + mapping = simpeg.maps.IdentityMap(nP=model.size) + eqs_ram, eqs_forward_only = ( + magnetics.SimulationEquivalentSourceLayer( + tensor_mesh, + mesh_top, + mesh_bottom, + survey=magnetic_survey, + chiMap=mapping, + engine=engine, + store_sensitivities=store, + numba_parallel=parallel, + model_type=model_type, + ) + for store in ("ram", "forward_only") + ) + # Compare methods for both simulations + model = ( + get_block_model(tensor_mesh, 0.2e-3) + if model_type == "scalar" + else get_block_model(tensor_mesh, (0.2e-3, -0.1e-3, 0.5e-3)) + ) + gtgdiag_ram = eqs_ram.getJtJdiag(model) + gtgdiag_linop = eqs_forward_only.getJtJdiag(model) + atol = np.max(np.abs(gtgdiag_ram)) * 1e-7 + np.testing.assert_allclose(gtgdiag_ram, gtgdiag_linop, atol=atol) diff --git a/tests/pf/test_forward_Grav_Linear.py b/tests/pf/test_forward_Grav_Linear.py index 40aff12b53..4dfb85fde4 100644 --- a/tests/pf/test_forward_Grav_Linear.py +++ b/tests/pf/test_forward_Grav_Linear.py @@ -1,8 +1,13 @@ +import re + import pytest +from scipy.sparse import diags +from scipy.sparse.linalg import LinearOperator, aslinearoperator import discretize import simpeg from simpeg import maps from simpeg.potential_fields import gravity +from simpeg.utils import model_builder from geoana.gravity import Prism import numpy as np @@ -143,7 +148,7 @@ def test_accelerations_vs_analytic( mesh, survey=survey, rhoMap=idenMap, - ind_active=active_cells, + active_cells=active_cells, store_sensitivities=store_sensitivities, engine=engine, sensitivity_path=str(sensitivity_path), @@ -199,7 +204,7 @@ def test_tensor_vs_analytic( mesh, survey=survey, rhoMap=idenMap, - ind_active=active_cells, + active_cells=active_cells, store_sensitivities=store_sensitivities, engine=engine, sensitivity_path=str(sensitivity_path), @@ -259,7 +264,7 @@ def test_guv_vs_analytic( mesh, survey=survey, rhoMap=idenMap, - ind_active=active_cells, + active_cells=active_cells, store_sensitivities=store_sensitivities, engine=engine, sensitivity_path=str(sensitivity_path), @@ -301,7 +306,7 @@ def test_sensitivity_dtype( simple_mesh, survey=survey, rhoMap=idenMap, - ind_active=active_cells, + active_cells=active_cells, engine=engine, store_sensitivities=store_sensitivities, sensitivity_path=str(sensitivity_path), @@ -329,7 +334,8 @@ def test_invalid_sensitivity_dtype_assignment(self, simple_mesh, invalid_dtype): def test_invalid_engine(self, simple_mesh): """Test if error is raised after invalid engine.""" engine = "invalid engine" - with pytest.raises(ValueError, match=f"Invalid engine '{engine}'"): + msg = rf"'engine' must be in \('geoana', 'choclo'\). Got '{engine}'" + with pytest.raises(ValueError, match=msg): gravity.Simulation3DIntegral(simple_mesh, engine=engine) def test_choclo_and_n_proceesses(self, simple_mesh): @@ -350,7 +356,9 @@ def test_choclo_and_sensitivity_path_as_dir(self, simple_mesh, tmp_path): sensitivity_path = tmp_path / "sensitivity_dummy" sensitivity_path.mkdir() # Check if error is raised - msg = f"The passed sensitivity_path '{str(sensitivity_path)}' is a directory" + msg = re.escape( + f"The passed sensitivity_path '{str(sensitivity_path)}' is a directory" + ) with pytest.raises(ValueError, match=msg): gravity.Simulation3DIntegral( simple_mesh, @@ -405,13 +413,456 @@ def test_choclo_missing(self, simple_mesh, monkeypatch): Check if error is raised when choclo is missing and chosen as engine. """ # Monkeypatch choclo in simpeg.potential_fields.base - monkeypatch.setattr(simpeg.potential_fields.gravity.simulation, "choclo", None) + monkeypatch.setattr(simpeg.potential_fields.base, "choclo", None) # Check if error is raised msg = "The choclo package couldn't be found." with pytest.raises(ImportError, match=msg): gravity.Simulation3DIntegral(simple_mesh, engine="choclo") +class BaseFixtures: + """ + Base test class with some fixtures. + """ + + @pytest.fixture + def survey(self): + # Observation points + x = np.linspace(-20.0, 20.0, 4) + x, y = np.meshgrid(x, x) + z = 5.0 * np.ones_like(x) + coordinates = np.vstack((x.ravel(), y.ravel(), z.ravel())).T + receivers = gravity.receivers.Point(coordinates, components="gz") + source_field = gravity.sources.SourceField(receiver_list=[receivers]) + survey = gravity.survey.Survey(source_field) + return survey + + @pytest.fixture + def mesh(self): + # Mesh + dh = 5.0 + hx = [(dh, 4)] + mesh = discretize.TensorMesh([hx, hx, hx], "CCN") + return mesh + + @pytest.fixture + def densities(self, mesh): + # Define densities + densities = 1e-10 * np.ones(mesh.n_cells) + ind_sphere = model_builder.get_indices_sphere( + np.r_[0.0, 0.0, -20.0], 10.0, mesh.cell_centers + ) + densities[ind_sphere] = 0.2 + return densities + + +class TestJacobianGravity(BaseFixtures): + """ + Test methods related to Jacobian matrix in gravity simulation. + """ + + atol_ratio = 1e-7 + + @pytest.fixture(params=["identity_map", "exp_map"]) + def mapping(self, mesh, request): + mapping = ( + maps.IdentityMap(nP=mesh.n_cells) + if request.param == "identity_map" + else maps.ExpMap(nP=mesh.n_cells) + ) + return mapping + + @pytest.mark.parametrize("engine", ["choclo", "geoana"]) + def test_getJ_as_array(self, survey, mesh, densities, mapping, engine): + """ + Test the getJ method when J is an array in memory. + """ + simulation = gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities="ram", + engine=engine, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = densities if is_identity_map else np.log(densities) + + jac = simulation.getJ(model) + assert isinstance(jac, np.ndarray) + # With an identity mapping, the jacobian should be the same as G. + # With an exp mapping, the jacobian should be G @ the mapping derivative. + expected_jac = ( + simulation.G if is_identity_map else simulation.G @ mapping.deriv(model) + ) + np.testing.assert_allclose(jac, expected_jac) + + @pytest.mark.parametrize("transpose", [False, True], ids=["J @ m", "J.T @ v"]) + def test_getJ_as_linear_operator(self, survey, mesh, densities, mapping, transpose): + """ + Test the getJ method when J is a linear operator. + """ + simulation = gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities="forward_only", + engine="choclo", + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = densities if is_identity_map else np.log(densities) + + jac = simulation.getJ(model) + assert isinstance(jac, LinearOperator) + + if transpose: + vector = np.random.default_rng(seed=42).uniform(size=survey.nD) + result = jac.T @ vector + expected_result = mapping.deriv(model).T @ (simulation.G.T @ vector) + else: + result = jac @ model + expected_result = simulation.G @ (mapping.deriv(model).diagonal() * model) + np.testing.assert_allclose(result, expected_result) + + def test_getJ_as_linear_operator_not_implemented( + self, survey, mesh, densities, mapping + ): + """ + Test getJ raises NotImplementedError when forward only with geoana. + """ + simulation = gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities="forward_only", + engine="geoana", + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = densities if is_identity_map else np.log(densities) + + msg = re.escape( + "Accessing matrix G with " + 'store_sensitivities="forward_only" and engine="geoana" ' + "hasn't been implemented yet." + ) + with pytest.raises(NotImplementedError, match=msg): + simulation.getJ(model) + + @pytest.mark.parametrize( + ("engine", "store_sensitivities"), + [ + ("choclo", "ram"), + ("choclo", "forward_only"), + ("geoana", "ram"), + pytest.param( + "geoana", + "forward_only", + marks=pytest.mark.xfail( + raises=NotImplementedError, reason="not implemented" + ), + ), + ], + ) + def test_Jvec(self, survey, mesh, densities, mapping, engine, store_sensitivities): + """ + Test the Jvec method. + """ + simulation = gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities=store_sensitivities, + engine=engine, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = densities if is_identity_map else np.log(densities) + + vector = np.random.default_rng(seed=42).uniform(size=densities.size) + result = simulation.Jvec(model, vector) + + expected_jac = ( + simulation.G + if is_identity_map + else simulation.G @ aslinearoperator(mapping.deriv(model)) + ) + expected = expected_jac @ vector + + atol = np.max(np.abs(expected)) * self.atol_ratio + np.testing.assert_allclose(result, expected, atol=atol) + + @pytest.mark.parametrize( + ("engine", "store_sensitivities"), + [ + ("choclo", "ram"), + ("choclo", "forward_only"), + ("geoana", "ram"), + pytest.param( + "geoana", + "forward_only", + marks=pytest.mark.xfail( + raises=NotImplementedError, reason="not implemented" + ), + ), + ], + ) + def test_Jtvec(self, survey, mesh, densities, mapping, engine, store_sensitivities): + """ + Test the Jtvec method. + """ + simulation = gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities=store_sensitivities, + engine=engine, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = densities if is_identity_map else np.log(densities) + + vector = np.random.default_rng(seed=42).uniform(size=survey.nD) + result = simulation.Jtvec(model, vector) + + expected_jac = ( + simulation.G + if is_identity_map + else simulation.G @ aslinearoperator(mapping.deriv(model)) + ) + expected = expected_jac.T @ vector + + atol = np.max(np.abs(result)) * self.atol_ratio + np.testing.assert_allclose(result, expected, atol=atol) + + @pytest.mark.parametrize( + "engine", + [ + "choclo", + pytest.param( + "geoana", + marks=pytest.mark.xfail( + raises=NotImplementedError, reason="not implemented" + ), + ), + ], + ) + @pytest.mark.parametrize("method", ["Jvec", "Jtvec"]) + def test_array_vs_linear_operator( + self, survey, mesh, densities, mapping, engine, method + ): + """ + Test methods when using "ram" and "forward_only". + + They should give the same results. + """ + simulation_lo, simulation_ram = ( + gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities=store, + engine=engine, + ) + for store in ("forward_only", "ram") + ) + match method: + case "Jvec": + vector_size = densities.size + case "Jtvec": + vector_size = survey.nD + case _: + raise ValueError(f"Invalid method '{method}'") + + is_identity_map = type(mapping) is maps.IdentityMap + model = densities if is_identity_map else np.log(densities) + + vector = np.random.default_rng(seed=42).uniform(size=vector_size) + result_lo = getattr(simulation_lo, method)(model, vector) + result_ram = getattr(simulation_ram, method)(model, vector) + atol = np.max(np.abs(result_ram)) * self.atol_ratio + np.testing.assert_allclose(result_lo, result_ram, atol=atol) + + @pytest.mark.parametrize("engine", ["choclo", "geoana"]) + @pytest.mark.parametrize("weights", [True, False]) + def test_getJtJdiag(self, survey, mesh, densities, mapping, engine, weights): + """ + Test the ``getJtJdiag`` method with G as an array in memory. + """ + simulation = gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities="ram", + engine=engine, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = densities if is_identity_map else np.log(densities) + + kwargs = {} + if weights: + w_matrix = diags(np.random.default_rng(seed=42).uniform(size=survey.nD)) + kwargs = {"W": w_matrix} + jtj_diag = simulation.getJtJdiag(model, **kwargs) + + expected_jac = ( + simulation.G if is_identity_map else simulation.G @ mapping.deriv(model) + ) + if weights: + expected = np.diag(expected_jac.T @ w_matrix.T @ w_matrix @ expected_jac) + else: + expected = np.diag(expected_jac.T @ expected_jac) + + atol = np.max(np.abs(jtj_diag)) * self.atol_ratio + np.testing.assert_allclose(jtj_diag, expected, atol=atol) + + @pytest.mark.parametrize( + "engine", + [ + "choclo", + pytest.param( + "geoana", + marks=pytest.mark.xfail( + raises=NotImplementedError, reason="not implemented" + ), + ), + ], + ) + @pytest.mark.parametrize("weights", [True, False]) + def test_getJtJdiag_forward_only( + self, survey, mesh, densities, mapping, engine, weights + ): + """ + Test the ``getJtJdiag`` method without building G. + """ + simulation, simulation_ram = ( + gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities=store, + engine=engine, + ) + for store in ("forward_only", "ram") + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = densities if is_identity_map else np.log(densities) + + kwargs = {} + if weights: + weights = np.random.default_rng(seed=42).uniform(size=survey.nD) + kwargs = {"W": diags(np.sqrt(weights))} + jtj_diag = simulation.getJtJdiag(model, **kwargs) + jtj_diag_ram = simulation_ram.getJtJdiag(model, **kwargs) + + atol = np.max(np.abs(jtj_diag)) * self.atol_ratio + np.testing.assert_allclose(jtj_diag, jtj_diag_ram, atol=atol) + + @pytest.mark.parametrize("engine", ("choclo", "geoana")) + def test_getJtJdiag_caching(self, survey, mesh, densities, mapping, engine): + """ + Test the caching behaviour of the ``getJtJdiag`` method. + """ + simulation = gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities="ram", + engine=engine, + ) + # Get diagonal of J.T @ J without any weight + is_identity_map = type(mapping) is maps.IdentityMap + model = densities if is_identity_map else np.log(densities) + + jtj_diagonal_1 = simulation.getJtJdiag(model) + assert hasattr(simulation, "_gtg_diagonal") + assert hasattr(simulation, "_weights_sha256") + gtg_diagonal_1 = simulation._gtg_diagonal + weights_sha256_1 = simulation._weights_sha256 + + # Compute it again and make sure we get the same result + np.testing.assert_allclose(jtj_diagonal_1, simulation.getJtJdiag(model)) + + # Get a new diagonal with weights + weights_matrix = diags( + np.random.default_rng(seed=42).uniform(size=simulation.survey.nD) + ) + jtj_diagonal_2 = simulation.getJtJdiag(model, W=weights_matrix) + assert hasattr(simulation, "_gtg_diagonal") + assert hasattr(simulation, "_weights_sha256") + gtg_diagonal_2 = simulation._gtg_diagonal + weights_sha256_2 = simulation._weights_sha256 + + # The two results should be different + assert not np.array_equal(jtj_diagonal_1, jtj_diagonal_2) + assert not np.array_equal(gtg_diagonal_1, gtg_diagonal_2) + assert weights_sha256_1.digest() != weights_sha256_2.digest() + + +class TestGLinearOperator(BaseFixtures): + """ + Test G as a linear operator. + """ + + @pytest.fixture + def mapping(self, mesh): + return maps.IdentityMap(nP=mesh.n_cells) + + def test_not_implemented(self, survey, mesh, mapping): + """ + Test NotImplementedError when using geoana as engine. + """ + simulation = gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities="forward_only", + engine="geoana", + ) + msg = re.escape( + "Accessing matrix G with " + 'store_sensitivities="forward_only" and engine="geoana" ' + "hasn't been implemented yet." + ) + with pytest.raises(NotImplementedError, match=msg): + simulation.G + + @pytest.mark.parametrize("parallel", [True, False]) + def test_G_dot_m(self, survey, mesh, mapping, densities, parallel): + """Test G @ m.""" + simulation, simulation_ram = ( + gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities=store, + engine="choclo", + numba_parallel=parallel, + ) + for store in ("forward_only", "ram") + ) + assert isinstance(simulation.G, LinearOperator) + assert isinstance(simulation_ram.G, np.ndarray) + np.testing.assert_allclose( + simulation.G @ densities, simulation_ram.G @ densities + ) + + @pytest.mark.parametrize("parallel", [True, False]) + def test_G_t_dot_v(self, survey, mesh, mapping, parallel): + """Test G.T @ v.""" + simulation, simulation_ram = ( + gravity.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + rhoMap=mapping, + store_sensitivities=store, + engine="choclo", + numba_parallel=parallel, + ) + for store in ("forward_only", "ram") + ) + assert isinstance(simulation.G, LinearOperator) + assert isinstance(simulation_ram.G, np.ndarray) + vector = np.random.default_rng(seed=42).uniform(size=survey.nD) + np.testing.assert_allclose(simulation.G.T @ vector, simulation_ram.G.T @ vector) + + class TestConversionFactor: """Test _get_conversion_factor function.""" @@ -436,34 +887,3 @@ def test_invalid_conversion_factor(self): component = "invalid-component" with pytest.raises(ValueError, match=f"Invalid component '{component}'"): gravity.simulation._get_conversion_factor(component) - - -class TestInvalidMeshChoclo: - @pytest.fixture(params=("tensormesh", "treemesh")) - def mesh(self, request): - """Sample 2D mesh.""" - hx, hy = [(0.1, 8)], [(0.1, 8)] - h = (hx, hy) - if request.param == "tensormesh": - mesh = discretize.TensorMesh(h, "CC") - else: - mesh = discretize.TreeMesh(h, origin="CC") - mesh.finalize() - return mesh - - def test_invalid_mesh_with_choclo(self, mesh): - """ - Test if simulation raises error when passing an invalid mesh and using choclo - """ - # Build survey - receivers_locations = np.array([[0, 0, 0]]) - receivers = gravity.Point(receivers_locations) - sources = gravity.SourceField([receivers]) - survey = gravity.Survey(sources) - # Check if error is raised - msg = ( - "Invalid mesh with 2 dimensions. " - "Only 3D meshes are supported when using 'choclo' as engine." - ) - with pytest.raises(ValueError, match=msg): - gravity.Simulation3DIntegral(mesh, survey, engine="choclo") diff --git a/tests/pf/test_forward_Mag_Linear.py b/tests/pf/test_forward_Mag_Linear.py index 21f0570189..45a523cb97 100644 --- a/tests/pf/test_forward_Mag_Linear.py +++ b/tests/pf/test_forward_Mag_Linear.py @@ -1,517 +1,1754 @@ -import pytest -import unittest +from __future__ import annotations + +import re import discretize import numpy as np +import pytest +from scipy.sparse import diags from geoana.em.static import MagneticPrism from scipy.constants import mu_0 +from scipy.sparse.linalg import LinearOperator, aslinearoperator +import simpeg from simpeg import maps, utils +from simpeg.utils import model_builder from simpeg.potential_fields import magnetics as mag -def test_ana_mag_forward(): - nx = 5 - ny = 5 - - h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) - chi1 = 0.01 - chi2 = 0.02 - - # Define a mesh - cs = 0.2 - hxind = [(cs, 41)] - hyind = [(cs, 41)] - hzind = [(cs, 41)] - mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") - - # create a model of two blocks, 1 inside the other - block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) - block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) - - def get_block_inds(grid, block): - return np.where( - (grid[:, 0] > block[0, 0]) - & (grid[:, 0] < block[0, 1]) - & (grid[:, 1] > block[1, 0]) - & (grid[:, 1] < block[1, 1]) - & (grid[:, 2] > block[2, 0]) - & (grid[:, 2] < block[2, 1]) - ) - - block1_inds = get_block_inds(mesh.cell_centers, block1) - block2_inds = get_block_inds(mesh.cell_centers, block2) - - model = np.zeros(mesh.n_cells) - model[block1_inds] = chi1 - model[block2_inds] = chi2 - - active_cells = model != 0.0 - model_reduced = model[active_cells] - - # Create reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells))) - - # Create plane of observations - xr = np.linspace(-20, 20, nx) - yr = np.linspace(-20, 20, ny) - X, Y = np.meshgrid(xr, yr) - Z = np.ones_like(X) * 3.0 - locXyz = np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] - components = ["bx", "by", "bz", "tmi"] - - rxLoc = mag.Point(locXyz, components=components) - srcField = mag.UniformBackgroundField( - receiver_list=[rxLoc], - amplitude=h0_amplitude, - inclination=h0_inclination, - declination=h0_declination, +def get_block_inds(grid: np.ndarray, block: np.ndarray) -> np.ndarray: + """ + Get the indices for a block + + Parameters + ---------- + grid : np.ndarray + (n, 3) array of xyz locations + block : np.ndarray + (3, 2) array of (xmin, xmax), (ymin, ymax), (zmin, zmax) dimensions of + the block. + + Returns + ------- + np.ndarray + boolean array of indices corresponding to the block + """ + + return np.where( + (grid[:, 0] > block[0, 0]) + & (grid[:, 0] < block[0, 1]) + & (grid[:, 1] > block[1, 0]) + & (grid[:, 1] < block[1, 1]) + & (grid[:, 2] > block[2, 0]) + & (grid[:, 2] < block[2, 1]) ) - survey = mag.Survey(srcField) - # Creat reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells))) - sim = mag.Simulation3DIntegral( - mesh, - survey=survey, - chiMap=idenMap, - ind_active=active_cells, - store_sensitivities="forward_only", - n_processes=None, - ) - - data = sim.dpred(model_reduced) - d_x = data[0::4] - d_y = data[1::4] - d_z = data[2::4] - d_t = data[3::4] - - tmi = sim.tmi_projection - d_t2 = d_x * tmi[0] + d_y * tmi[1] + d_z * tmi[2] - np.testing.assert_allclose(d_t, d_t2) # double check internal projection - - # Compute analytical response from magnetic prism - prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) - prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -chi1 * b0 / mu_0) - prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) - - d = ( - prism_1.magnetic_flux_density(locXyz) - + prism_2.magnetic_flux_density(locXyz) - + prism_3.magnetic_flux_density(locXyz) +def create_block_model( + mesh: discretize.TensorMesh, + blocks: tuple[np.ndarray, ...], + block_params: tuple[float, ...] | tuple[np.ndarray, ...], +) -> tuple[np.ndarray, np.ndarray]: + """ + Create a magnetic model from a sequence of blocks + + Parameters + ---------- + mesh : discretize.TensorMesh + TensorMesh object to put the model on + blocks : Tuple[np.ndarray, ...] + Tuple of block definitions (each element is (3, 2) array of + (xmin, xmax), (ymin, ymax), (zmin, zmax) dimensions of the block) + block_params : Tuple[float, ...] + Tuple of parameters to assign for each block. Must be the same length + as ``blocks``. + + Returns + ------- + Tuple[np.ndarray, np.ndarray] + Tuple of the magnetic model and active_cells (a boolean array) + + Raises + ------ + ValueError + if ``blocks`` and ``block_params`` have incompatible dimensions + """ + if len(blocks) != len(block_params): + raise ValueError( + "'blocks' and 'block_params' must have the same number of elements" + ) + model = np.zeros((mesh.n_cells, np.atleast_1d(block_params[0]).shape[0])) + for block, params in zip(blocks, block_params): + block_ind = get_block_inds(mesh.cell_centers, block) + model[block_ind] = params + active_cells = np.any(np.abs(model) > 0, axis=1) + return model.squeeze(), active_cells + + +def create_mag_survey( + components: list[str], + receiver_locations: np.ndarray, + inducing_field_params: tuple[float, float, float], +) -> mag.Survey: + """ + create a magnetic Survey + + Parameters + ---------- + components : List[str] + List of components to model + receiver_locations : np.ndarray + (n, 3) array of xyz receiver locations + inducing_field_params : Tuple[float, float, float] + amplitude, inclination, and declination of the inducing field + + Returns + ------- + mag.Survey + a magnetic Survey instance + """ + receivers = mag.Point(receiver_locations, components=components) + strenght, inclination, declination = inducing_field_params + source_field = mag.UniformBackgroundField( + receiver_list=[receivers], + amplitude=strenght, + inclination=inclination, + declination=declination, ) - - np.testing.assert_allclose(d_x, d[:, 0]) - np.testing.assert_allclose(d_y, d[:, 1]) - np.testing.assert_allclose(d_z, d[:, 2]) - np.testing.assert_allclose(d_t, d @ tmi) - - -def test_ana_mag_tmi_grad_forward(): - nx = 61 - ny = 61 - - H0 = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-H0[1], H0[2], H0[0]) - chi1 = 0.01 - chi2 = 0.02 - - # Define a mesh - cs = 0.2 - hxind = [(cs, 41)] - hyind = [(cs, 41)] - hzind = [(cs, 41)] - mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") - - # create a model of two blocks, 1 inside the other - block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) - block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) - - def get_block_inds(grid, block): - return np.where( - (grid[:, 0] > block[0, 0]) - & (grid[:, 0] < block[0, 1]) - & (grid[:, 1] > block[1, 0]) - & (grid[:, 1] < block[1, 1]) - & (grid[:, 2] > block[2, 0]) - & (grid[:, 2] < block[2, 1]) - ) - - block1_inds = get_block_inds(mesh.cell_centers, block1) - block2_inds = get_block_inds(mesh.cell_centers, block2) - - model = np.zeros(mesh.n_cells) - model[block1_inds] = chi1 - model[block2_inds] = chi2 - - active_cells = model != 0.0 - model_reduced = model[active_cells] - - # Create reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells))) - - # Create plane of observations - xr = np.linspace(-20, 20, nx) - dxr = xr[1] - xr[0] - yr = np.linspace(-20, 20, ny) - dyr = yr[1] - yr[0] - X, Y = np.meshgrid(xr, yr) - Z = np.ones_like(X) * 3.0 - locXyz = np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] - components = ["tmi", "tmi_x", "tmi_y", "tmi_z"] - - rxLoc = mag.Point(locXyz, components=components) - srcField = mag.UniformBackgroundField( - [rxLoc], amplitude=H0[0], inclination=H0[1], declination=H0[2] + return mag.Survey(source_field) + + +def get_shifted_locations( + receiver_locations: np.ndarray, delta: float, direction: str +) -> tuple[np.ndarray, np.ndarray]: + """ + Shift the locations of receivers along a particular direction. + """ + if direction == "x": + index = 0 + elif direction == "y": + index = 1 + elif direction == "z": + index = 2 + else: + raise ValueError(f"Invalid direction '{direction}'") + plus, minus = receiver_locations.copy(), receiver_locations.copy() + plus[:, index] += delta / 2.0 + minus[:, index] -= delta / 2.0 + return plus, minus + + +class TestsMagSimulation: + """ + Test mag simulation against the analytic solutions single prisms + """ + + @pytest.fixture + def mag_mesh(self) -> discretize.TensorMesh: + """ + a small tensor mesh for testing magnetic simulations + + Returns + ------- + discretize.TensorMesh + the tensor mesh for testing + """ + # Define a mesh + cs = 0.2 + hxind = [(cs, 41)] + hyind = [(cs, 41)] + hzind = [(cs, 41)] + mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") + return mesh + + @pytest.fixture + def two_blocks(self) -> tuple[np.ndarray, np.ndarray]: + """ + The parameters defining two blocks. + + The boundaries of the prism should match nodes in the mesh, otherwise + these blocks won't be exactly represented in the mesh model. + + Returns + ------- + Tuple[np.ndarray, np.ndarray] + Tuple of (3, 2) arrays of (xmin, xmax), (ymin, ymax), (zmin, zmax) + dimensions of each block. + """ + block1 = np.array([[-2.5, 0.5], [-3.1, 1.3], [-3.7, 1.5]]) + block2 = np.array([[0.7, 1.9], [-0.7, 2.7], [-1.7, 0.7]]) + return block1, block2 + + @pytest.fixture + def receiver_locations(self) -> np.ndarray: + """ + a grid of receivers for testing + + Returns + ------- + np.ndarray + (n, 3) array of receiver locations + """ + # Create plane of observations + nx, ny = 5, 5 + xr = np.linspace(-20, 20, nx) + yr = np.linspace(-20, 20, ny) + X, Y = np.meshgrid(xr, yr) + Z = np.ones_like(X) * 3.0 + return np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] + + @pytest.fixture + def inducing_field( + self, + ) -> tuple[tuple[float, float, float], tuple[float, float, float]]: + """ + inducing field + + Return inducing field as amplitude and angles and as vector components. + + Returns + ------- + tuple[tuple[float, float, float], tuple[float, float, float]] + (amplitude, inclination, declination), (b_x, b_y, b_z) + """ + h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) + b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) + return (h0_amplitude, h0_inclination, h0_declination), b0 + + @pytest.mark.parametrize( + "engine,parallel_kwargs", + [ + ("geoana", {"n_processes": None}), + ("geoana", {"n_processes": 1}), + ("choclo", {"numba_parallel": False}), + ("choclo", {"numba_parallel": True}), + ], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], ) - survey = mag.Survey(srcField) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_magnetic_field_and_tmi_w_susceptibility( + self, + engine, + parallel_kwargs, + store_sensitivities, + tmp_path, + mag_mesh, + two_blocks, + receiver_locations, + inducing_field, + ): + """ + Test forwarding the magnetic field and tmi (with susceptibility as model) + """ + inducing_field_params, b0 = inducing_field + + chi1 = 0.01 + chi2 = 0.02 + model, active_cells = create_block_model(mag_mesh, two_blocks, (chi1, chi2)) + model_reduced = model[active_cells] + # Create reduced identity map for Linear Problem + identity_map = maps.IdentityMap(nP=int(sum(active_cells))) + + survey = create_mag_survey( + components=["bx", "by", "bz", "tmi"], + receiver_locations=receiver_locations, + inducing_field_params=inducing_field_params, + ) - # Creat reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells))) + sim = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=identity_map, + active_cells=active_cells, + sensitivity_path=str(tmp_path / f"{engine}"), + store_sensitivities=store_sensitivities, + engine=engine, + **parallel_kwargs, + ) - sim = mag.Simulation3DIntegral( - mesh, - survey=survey, - chiMap=idenMap, - ind_active=active_cells, - store_sensitivities="forward_only", - n_processes=None, + data = sim.dpred(model_reduced) + d_x = data[0::4] + d_y = data[1::4] + d_z = data[2::4] + d_t = data[3::4] + + # Compute analytical response from magnetic prism + block1, block2 = two_blocks + prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) + prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) + + d = prism_1.magnetic_flux_density( + receiver_locations + ) + prism_2.magnetic_flux_density(receiver_locations) + + # TMI projection + tmi = sim.tmi_projection + d_t2 = d_x * tmi[0] + d_y * tmi[1] + d_z * tmi[2] + + # Check results + rtol, atol = 2e-6, 1e-6 + np.testing.assert_allclose( + d_t, d_t2, rtol=rtol, atol=atol + ) # double check internal projection + np.testing.assert_allclose(d_x, d[:, 0], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_y, d[:, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_z, d[:, 2], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_t, d @ tmi, rtol=rtol, atol=atol) + + @pytest.mark.parametrize( + "engine, parallel_kwargs", + [ + ("geoana", {"n_processes": None}), + ("geoana", {"n_processes": 1}), + ("choclo", {"numba_parallel": False}), + ("choclo", {"numba_parallel": True}), + ], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], ) - - data = sim.dpred(model_reduced) - tmi = data[0::4] - d_x = data[1::4] - d_y = data[2::4] - d_z = data[3::4] - - # Compute analytical response from magnetic prism - prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) - prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -chi1 * b0 / mu_0) - prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) - - d = ( - prism_1.magnetic_field_gradient(locXyz) - + prism_2.magnetic_field_gradient(locXyz) - + prism_3.magnetic_field_gradient(locXyz) - ) * mu_0 - tmi_x = (d[:, 0, 0] * b0[0] + d[:, 0, 1] * b0[1] + d[:, 0, 2] * b0[2]) / H0[0] - tmi_y = (d[:, 1, 0] * b0[0] + d[:, 1, 1] * b0[1] + d[:, 1, 2] * b0[2]) / H0[0] - tmi_z = (d[:, 2, 0] * b0[0] + d[:, 2, 1] * b0[1] + d[:, 2, 2] * b0[2]) / H0[0] - np.testing.assert_allclose(d_x, tmi_x, rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_y, tmi_y, rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_z, tmi_z, rtol=1e-10, atol=1e-12) - - # finite difference test y-grad - np.testing.assert_allclose( - np.diff(tmi.reshape(nx, ny, order="F")[:, ::2], axis=1) / (2 * dyr), - tmi_y.reshape(nx, ny, order="F")[:, 1::2], - atol=1.0, - rtol=1e-1, + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_magnetic_gradiometry_w_susceptibility( + self, + engine, + parallel_kwargs, + store_sensitivities, + tmp_path, + mag_mesh, + two_blocks, + receiver_locations, + inducing_field, + ): + """ + Test magnetic gradiometry components (with susceptibility as model) + """ + inducing_field_params, b0 = inducing_field + chi1 = 0.01 + chi2 = 0.02 + model, active_cells = create_block_model(mag_mesh, two_blocks, (chi1, chi2)) + model_reduced = model[active_cells] + # Create reduced identity map for Linear Problem + identity_map = maps.IdentityMap(nP=int(sum(active_cells))) + + survey = create_mag_survey( + components=["bxx", "bxy", "bxz", "byy", "byz", "bzz"], + receiver_locations=receiver_locations, + inducing_field_params=inducing_field_params, + ) + sim = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=identity_map, + active_cells=active_cells, + sensitivity_path=str(tmp_path / f"{engine}"), + store_sensitivities=store_sensitivities, + engine=engine, + **parallel_kwargs, + ) + data = sim.dpred(model_reduced) + d_xx = data[0::6] + d_xy = data[1::6] + d_xz = data[2::6] + d_yy = data[3::6] + d_yz = data[4::6] + d_zz = data[5::6] + + # Compute analytical response from magnetic prism + block1, block2 = two_blocks + prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) + prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) + + d = ( + prism_1.magnetic_field_gradient(receiver_locations) + + prism_2.magnetic_field_gradient(receiver_locations) + ) * mu_0 + + # Check results + rtol, atol = 5e-7, 1e-6 + np.testing.assert_allclose(d_xx, d[..., 0, 0], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_xy, d[..., 0, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_xz, d[..., 0, 2], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_yy, d[..., 1, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_yz, d[..., 1, 2], rtol=rtol, atol=atol) + np.testing.assert_allclose(d_zz, d[..., 2, 2], rtol=rtol, atol=atol) + + @pytest.mark.parametrize( + "engine, parallel_kwargs", + [ + ("geoana", {"n_processes": None}), + ("geoana", {"n_processes": 1}), + ("choclo", {"numba_parallel": False}), + ("choclo", {"numba_parallel": True}), + ], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], ) - # finite difference test x-grad - np.testing.assert_allclose( - np.diff(tmi.reshape(nx, ny, order="F")[::2, :], axis=0) / (2 * dxr), - tmi_x.reshape(nx, ny, order="F")[1::2, :], - atol=1.0, - rtol=1e-1, + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_tmi_derivatives_w_susceptibility( + self, + engine, + parallel_kwargs, + store_sensitivities, + tmp_path, + mag_mesh, + two_blocks, + receiver_locations, + inducing_field, + ): + """ + Test TMI derivatives (with susceptibility as model) + """ + (h0_amplitude, h0_inclination, h0_declination), b0 = inducing_field + chi1 = 0.01 + chi2 = 0.02 + model, active_cells = create_block_model(mag_mesh, two_blocks, (chi1, chi2)) + model_reduced = model[active_cells] + # Create reduced identity map for Linear Problem + identity_map = maps.IdentityMap(nP=int(sum(active_cells))) + + components = ["tmi_x", "tmi_y", "tmi_z"] + survey = create_mag_survey( + components=components, + receiver_locations=receiver_locations, + inducing_field_params=(h0_amplitude, h0_inclination, h0_declination), + ) + sim = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=identity_map, + active_cells=active_cells, + sensitivity_path=str(tmp_path / f"{engine}"), + store_sensitivities=store_sensitivities, + engine=engine, + **parallel_kwargs, + ) + data = sim.dpred(model_reduced).reshape(-1, len(components)) + tmi_x = data[:, 0] + tmi_y = data[:, 1] + tmi_z = data[:, 2] + + # Compute analytical response from magnetic prism + block1, block2 = two_blocks + prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) + prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) + + d = ( + prism_1.magnetic_field_gradient(receiver_locations) + + prism_2.magnetic_field_gradient(receiver_locations) + ) * mu_0 + + # Check results + rtol, atol = 5e-7, 1e-6 + expected_tmi_x = ( + d[:, 0, 0] * b0[0] + d[:, 0, 1] * b0[1] + d[:, 0, 2] * b0[2] + ) / h0_amplitude + expected_tmi_y = ( + d[:, 1, 0] * b0[0] + d[:, 1, 1] * b0[1] + d[:, 1, 2] * b0[2] + ) / h0_amplitude + expected_tmi_z = ( + d[:, 2, 0] * b0[0] + d[:, 2, 1] * b0[1] + d[:, 2, 2] * b0[2] + ) / h0_amplitude + np.testing.assert_allclose(tmi_x, expected_tmi_x, rtol=rtol, atol=atol) + np.testing.assert_allclose(tmi_y, expected_tmi_y, rtol=rtol, atol=atol) + np.testing.assert_allclose(tmi_z, expected_tmi_z, rtol=rtol, atol=atol) + + @pytest.mark.parametrize( + "engine, parallel_kwargs", + [ + ("geoana", {"n_processes": None}), + ("geoana", {"n_processes": 1}), + ("choclo", {"numba_parallel": False}), + ("choclo", {"numba_parallel": True}), + ], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], ) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_magnetic_vector_and_tmi_w_magnetization( + self, + engine, + parallel_kwargs, + store_sensitivities, + tmp_path, + mag_mesh, + two_blocks, + receiver_locations, + inducing_field, + ): + """ + Test magnetic vector and TMI (using magnetization vectors as model) + """ + inducing_field_params, b0 = inducing_field + M1 = (utils.mat_utils.dip_azimuth2cartesian(45, -40) * 0.05).squeeze() + M2 = (utils.mat_utils.dip_azimuth2cartesian(120, 32) * 0.1).squeeze() + + model, active_cells = create_block_model(mag_mesh, two_blocks, (M1, M2)) + model_reduced = model[active_cells].reshape(-1, order="F") + # Create reduced identity map for Linear Problem + identity_map = maps.IdentityMap(nP=int(sum(active_cells)) * 3) + + survey = create_mag_survey( + components=["bx", "by", "bz", "tmi"], + receiver_locations=receiver_locations, + inducing_field_params=inducing_field_params, + ) + sim = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=identity_map, + active_cells=active_cells, + sensitivity_path=str(tmp_path / f"{engine}"), + store_sensitivities=store_sensitivities, + model_type="vector", + engine=engine, + **parallel_kwargs, + ) -def test_ana_mag_grad_forward(): - nx = 5 - ny = 5 - - h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) - chi1 = 0.01 - chi2 = 0.02 - - # Define a mesh - cs = 0.2 - hxind = [(cs, 41)] - hyind = [(cs, 41)] - hzind = [(cs, 41)] - mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") - - # create a model of two blocks, 1 inside the other - block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) - block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) - - def get_block_inds(grid, block): - return np.where( - (grid[:, 0] > block[0, 0]) - & (grid[:, 0] < block[0, 1]) - & (grid[:, 1] > block[1, 0]) - & (grid[:, 1] < block[1, 1]) - & (grid[:, 2] > block[2, 0]) - & (grid[:, 2] < block[2, 1]) - ) - - block1_inds = get_block_inds(mesh.cell_centers, block1) - block2_inds = get_block_inds(mesh.cell_centers, block2) - - model = np.zeros(mesh.n_cells) - model[block1_inds] = chi1 - model[block2_inds] = chi2 - - active_cells = model != 0.0 - model_reduced = model[active_cells] - - # Create reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells))) - - # Create plane of observations - xr = np.linspace(-20, 20, nx) - yr = np.linspace(-20, 20, ny) - X, Y = np.meshgrid(xr, yr) - Z = np.ones_like(X) * 3.0 - locXyz = np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] - components = ["bxx", "bxy", "bxz", "byy", "byz", "bzz"] - - rxLoc = mag.Point(locXyz, components=components) - srcField = mag.UniformBackgroundField( - [rxLoc], - amplitude=h0_amplitude, - inclination=h0_inclination, - declination=h0_declination, - ) - survey = mag.Survey(srcField) - - # Creat reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells))) + data = sim.dpred(model_reduced).reshape(-1, 4) - sim = mag.Simulation3DIntegral( - mesh, - survey=survey, - chiMap=idenMap, - ind_active=active_cells, - store_sensitivities="forward_only", - n_processes=None, - ) + # Compute analytical response from magnetic prism + block1, block2 = two_blocks + prism_1 = MagneticPrism( + block1[:, 0], block1[:, 1], M1 * np.linalg.norm(b0) / mu_0 + ) + prism_2 = MagneticPrism( + block2[:, 0], block2[:, 1], M2 * np.linalg.norm(b0) / mu_0 + ) - data = sim.dpred(model_reduced) - d_xx = data[0::6] - d_xy = data[1::6] - d_xz = data[2::6] - d_yy = data[3::6] - d_yz = data[4::6] - d_zz = data[5::6] - - # Compute analytical response from magnetic prism - prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], chi1 * b0 / mu_0) - prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -chi1 * b0 / mu_0) - prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) - - d = ( - prism_1.magnetic_field_gradient(locXyz) - + prism_2.magnetic_field_gradient(locXyz) - + prism_3.magnetic_field_gradient(locXyz) - ) * mu_0 - - np.testing.assert_allclose(d_xx, d[..., 0, 0], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_xy, d[..., 0, 1], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_xz, d[..., 0, 2], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_yy, d[..., 1, 1], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_yz, d[..., 1, 2], rtol=1e-10, atol=1e-12) - np.testing.assert_allclose(d_zz, d[..., 2, 2], rtol=1e-10, atol=1e-12) - - -def test_ana_mag_vec_forward(): - nx = 5 - ny = 5 - - h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) - - M1 = utils.mat_utils.dip_azimuth2cartesian(45, -40) * 0.05 - M2 = utils.mat_utils.dip_azimuth2cartesian(120, 32) * 0.1 - - # Define a mesh - cs = 0.2 - hxind = [(cs, 41)] - hyind = [(cs, 41)] - hzind = [(cs, 41)] - mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") - - # create a model of two blocks, 1 inside the other - block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) - block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) - - def get_block_inds(grid, block): - return np.where( - (grid[:, 0] > block[0, 0]) - & (grid[:, 0] < block[0, 1]) - & (grid[:, 1] > block[1, 0]) - & (grid[:, 1] < block[1, 1]) - & (grid[:, 2] > block[2, 0]) - & (grid[:, 2] < block[2, 1]) - ) - - block1_inds = get_block_inds(mesh.cell_centers, block1) - block2_inds = get_block_inds(mesh.cell_centers, block2) - - model = np.zeros((mesh.n_cells, 3)) - model[block1_inds] = M1 - model[block2_inds] = M2 - - active_cells = np.any(model != 0.0, axis=1) - model_reduced = model[active_cells].reshape(-1, order="F") - - # Create plane of observations - xr = np.linspace(-20, 20, nx) - yr = np.linspace(-20, 20, ny) - X, Y = np.meshgrid(xr, yr) - Z = np.ones_like(X) * 3.0 - locXyz = np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] - components = ["bx", "by", "bz", "tmi"] - - rxLoc = mag.Point(locXyz, components=components) - srcField = mag.UniformBackgroundField( - receiver_list=[rxLoc], - amplitude=h0_amplitude, - inclination=h0_inclination, - declination=h0_declination, + d = prism_1.magnetic_flux_density( + receiver_locations + ) + prism_2.magnetic_flux_density(receiver_locations) + tmi = sim.tmi_projection + + # Check results + rtol, atol = 5e-7, 1e-6 + np.testing.assert_allclose(data[:, 0], d[:, 0], rtol=rtol, atol=atol) + np.testing.assert_allclose(data[:, 1], d[:, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(data[:, 2], d[:, 2], rtol=rtol, atol=atol) + np.testing.assert_allclose(data[:, 3], d @ tmi, rtol=rtol, atol=atol) + + @pytest.mark.parametrize( + "engine, parallel_kwargs", + [ + ("geoana", {"n_processes": None}), + ("geoana", {"n_processes": 1}), + ("choclo", {"numba_parallel": False}), + ("choclo", {"numba_parallel": True}), + ], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], ) - survey = mag.Survey(srcField) - - # Create reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells)) * 3) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_tmi_derivatives_w_magnetization( + self, + engine, + parallel_kwargs, + store_sensitivities, + tmp_path, + mag_mesh, + two_blocks, + receiver_locations, + inducing_field, + ): + """ + Test TMI derivatives (using magnetization vectors as model) + """ + (h0_amplitude, h0_inclination, h0_declination), b0 = inducing_field + M1 = (utils.mat_utils.dip_azimuth2cartesian(45, -40) * 0.05).squeeze() + M2 = (utils.mat_utils.dip_azimuth2cartesian(120, 32) * 0.1).squeeze() + + model, active_cells = create_block_model(mag_mesh, two_blocks, (M1, M2)) + model_reduced = model[active_cells].reshape(-1, order="F") + # Create reduced identity map for Linear Problem + identity_map = maps.IdentityMap(nP=int(sum(active_cells)) * 3) + + components = ["tmi_x", "tmi_y", "tmi_z"] + survey = create_mag_survey( + components=components, + receiver_locations=receiver_locations, + inducing_field_params=(h0_amplitude, h0_inclination, h0_declination), + ) - sim = mag.Simulation3DIntegral( - mesh, - survey=survey, - chiMap=idenMap, - ind_active=active_cells, - store_sensitivities="forward_only", - model_type="vector", - n_processes=None, - ) + sim = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=identity_map, + active_cells=active_cells, + sensitivity_path=str(tmp_path / f"{engine}"), + store_sensitivities=store_sensitivities, + model_type="vector", + engine=engine, + **parallel_kwargs, + ) - data = sim.dpred(model_reduced).reshape(-1, 4) + data = sim.dpred(model_reduced).reshape(-1, len(components)) + tmi_x = data[:, 0] + tmi_y = data[:, 1] + tmi_z = data[:, 2] - # Compute analytical response from magnetic prism - prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], M1 * np.linalg.norm(b0) / mu_0) - prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -M1 * np.linalg.norm(b0) / mu_0) - prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], M2 * np.linalg.norm(b0) / mu_0) + # Compute analytical response from magnetic prism + block1, block2 = two_blocks + prism_1 = MagneticPrism( + block1[:, 0], block1[:, 1], M1 * np.linalg.norm(b0) / mu_0 + ) + prism_2 = MagneticPrism( + block2[:, 0], block2[:, 1], M2 * np.linalg.norm(b0) / mu_0 + ) - d = ( - prism_1.magnetic_flux_density(locXyz) - + prism_2.magnetic_flux_density(locXyz) - + prism_3.magnetic_flux_density(locXyz) + d = ( + prism_1.magnetic_field_gradient(receiver_locations) + + prism_2.magnetic_field_gradient(receiver_locations) + ) * mu_0 + + # Check results + rtol, atol = 5e-7, 1e-6 + expected_tmi_x = ( + d[:, 0, 0] * b0[0] + d[:, 0, 1] * b0[1] + d[:, 0, 2] * b0[2] + ) / h0_amplitude + expected_tmi_y = ( + d[:, 1, 0] * b0[0] + d[:, 1, 1] * b0[1] + d[:, 1, 2] * b0[2] + ) / h0_amplitude + expected_tmi_z = ( + d[:, 2, 0] * b0[0] + d[:, 2, 1] * b0[1] + d[:, 2, 2] * b0[2] + ) / h0_amplitude + np.testing.assert_allclose(tmi_x, expected_tmi_x, rtol=rtol, atol=atol) + np.testing.assert_allclose(tmi_y, expected_tmi_y, rtol=rtol, atol=atol) + np.testing.assert_allclose(tmi_z, expected_tmi_z, rtol=rtol, atol=atol) + + @pytest.mark.parametrize( + "engine, parallel_kwargs", + [ + ("geoana", {"n_processes": None}), + ("geoana", {"n_processes": 1}), + ("choclo", {"numba_parallel": False}), + ("choclo", {"numba_parallel": True}), + ], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], ) - tmi = sim.tmi_projection - - np.testing.assert_allclose(data[:, 0], d[:, 0]) - np.testing.assert_allclose(data[:, 1], d[:, 1]) - np.testing.assert_allclose(data[:, 2], d[:, 2]) - np.testing.assert_allclose(data[:, 3], d @ tmi) - - -def test_ana_mag_amp_forward(): - nx = 5 - ny = 5 - - h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-h0_inclination, h0_declination, h0_amplitude) - - M1 = utils.mat_utils.dip_azimuth2cartesian(45, -40) * 0.05 - M2 = utils.mat_utils.dip_azimuth2cartesian(120, 32) * 0.1 - - # Define a mesh - cs = 0.2 - hxind = [(cs, 41)] - hyind = [(cs, 41)] - hzind = [(cs, 41)] - mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") - - # create a model of two blocks, 1 inside the other - block1 = np.array([[-1.5, 1.5], [-1.5, 1.5], [-1.5, 1.5]]) - block2 = np.array([[-0.7, 0.7], [-0.7, 0.7], [-0.7, 0.7]]) - - def get_block_inds(grid, block): - return np.where( - (grid[:, 0] > block[0, 0]) - & (grid[:, 0] < block[0, 1]) - & (grid[:, 1] > block[1, 0]) - & (grid[:, 1] < block[1, 1]) - & (grid[:, 2] > block[2, 0]) - & (grid[:, 2] < block[2, 1]) - ) - - block1_inds = get_block_inds(mesh.cell_centers, block1) - block2_inds = get_block_inds(mesh.cell_centers, block2) - - model = np.zeros((mesh.n_cells, 3)) - model[block1_inds] = M1 - model[block2_inds] = M2 - - active_cells = np.any(model != 0.0, axis=1) - model_reduced = model[active_cells].reshape(-1, order="F") - - # Create plane of observations - xr = np.linspace(-20, 20, nx) - yr = np.linspace(-20, 20, ny) - X, Y = np.meshgrid(xr, yr) - Z = np.ones_like(X) * 3.0 - locXyz = np.c_[X.reshape(-1), Y.reshape(-1), Z.reshape(-1)] - components = ["bx", "by", "bz"] - - rxLoc = mag.Point(locXyz, components=components) - srcField = mag.UniformBackgroundField( - receiver_list=[rxLoc], - amplitude=h0_amplitude, - inclination=h0_inclination, - declination=h0_declination, - ) - survey = mag.Survey(srcField) - - # Create reduced identity map for Linear Problem - idenMap = maps.IdentityMap(nP=int(sum(active_cells)) * 3) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_magnetic_field_amplitude_w_magnetization( + self, + engine, + parallel_kwargs, + store_sensitivities, + tmp_path, + mag_mesh, + two_blocks, + receiver_locations, + inducing_field, + ): + """ + Test magnetic field amplitude (using magnetization vectors as model) + """ + inducing_field_params, b0 = inducing_field + M1 = (utils.mat_utils.dip_azimuth2cartesian(45, -40) * 0.05).squeeze() + M2 = (utils.mat_utils.dip_azimuth2cartesian(120, 32) * 0.1).squeeze() + + model, active_cells = create_block_model(mag_mesh, two_blocks, (M1, M2)) + model_reduced = model[active_cells].reshape(-1, order="F") + # Create reduced identity map for Linear Problem + identity_map = maps.IdentityMap(nP=int(sum(active_cells)) * 3) + + survey = create_mag_survey( + components=["bx", "by", "bz"], + receiver_locations=receiver_locations, + inducing_field_params=inducing_field_params, + ) - sim = mag.Simulation3DIntegral( - mesh, - survey=survey, - chiMap=idenMap, - ind_active=active_cells, - store_sensitivities="forward_only", - model_type="vector", - is_amplitude_data=True, - n_processes=None, - ) + sim = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=identity_map, + active_cells=active_cells, + sensitivity_path=str(tmp_path / f"{engine}"), + store_sensitivities=store_sensitivities, + model_type="vector", + is_amplitude_data=True, + engine=engine, + **parallel_kwargs, + ) - data = sim.dpred(model_reduced) + data = sim.dpred(model_reduced) - # Compute analytical response from magnetic prism - prism_1 = MagneticPrism(block1[:, 0], block1[:, 1], M1 * np.linalg.norm(b0) / mu_0) - prism_2 = MagneticPrism(block2[:, 0], block2[:, 1], -M1 * np.linalg.norm(b0) / mu_0) - prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], M2 * np.linalg.norm(b0) / mu_0) + # Compute analytical response from magnetic prism + block1, block2 = two_blocks + prism_1 = MagneticPrism( + block1[:, 0], block1[:, 1], M1 * np.linalg.norm(b0) / mu_0 + ) + prism_2 = MagneticPrism( + block2[:, 0], block2[:, 1], M2 * np.linalg.norm(b0) / mu_0 + ) - d = ( - prism_1.magnetic_flux_density(locXyz) - + prism_2.magnetic_flux_density(locXyz) - + prism_3.magnetic_flux_density(locXyz) - ) - d_amp = np.linalg.norm(d, axis=1) + d = prism_1.magnetic_flux_density( + receiver_locations + ) + prism_2.magnetic_flux_density(receiver_locations) + d_amp = np.linalg.norm(d, axis=1) + + # Check results + rtol, atol = 5e-7, 1e-6 + np.testing.assert_allclose(data, d_amp, rtol=rtol, atol=atol) + + @pytest.mark.parametrize("engine", ("geoana", "choclo")) + @pytest.mark.parametrize("direction", ("x", "y", "z")) + def test_tmi_derivatives_finite_diff( + self, + engine, + direction, + mag_mesh, + two_blocks, + receiver_locations, + inducing_field, + ): + """ + Test tmi derivatives against finite differences. + + Use float64 elements in the sensitivity matrix to avoid numerical + instabilities due to small values of delta. + """ + # Get inducing field and two blocks model + inducing_field_params, b0 = inducing_field + chi1, chi2 = 0.01, 0.02 + model, active_cells = create_block_model(mag_mesh, two_blocks, (chi1, chi2)) + model_reduced = model[active_cells] + identity_map = maps.IdentityMap(nP=int(sum(active_cells))) + # Create survey to compute tmi derivative through analytic solution + survey = create_mag_survey( + components=f"tmi_{direction}", + receiver_locations=receiver_locations, + inducing_field_params=inducing_field_params, + ) + kwargs = dict( + chiMap=identity_map, + active_cells=active_cells, + engine=engine, + sensitivity_dtype=np.float64, + ) + simulation = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + **kwargs, + ) + # Create shifted surveys to compute tmi derivatives through finite differences + delta = 1e-6 + shifted_surveys = [ + create_mag_survey( + components="tmi", + receiver_locations=shifted_locations, + inducing_field_params=inducing_field_params, + ) + for shifted_locations in get_shifted_locations( + receiver_locations, delta, direction + ) + ] + simulations_tmi = [ + mag.Simulation3DIntegral(mag_mesh, survey=shifted_survey, **kwargs) + for shifted_survey in shifted_surveys + ] + # Compute tmi derivatives + tmi_derivative = simulation.dpred(model_reduced) + # Compute tmi derivatives with finite differences + tmis = [sim.dpred(model_reduced) for sim in simulations_tmi] + tmi_derivative_finite_diff = (tmis[0] - tmis[1]) / delta + # Compare results + rtol, atol = 1e-6, 5e-6 + np.testing.assert_allclose( + tmi_derivative, tmi_derivative_finite_diff, rtol=rtol, atol=atol + ) - np.testing.assert_allclose(data, d_amp) + @pytest.mark.parametrize("engine", ("choclo", "geoana")) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_sensitivity_dtype( + self, + engine, + store_sensitivities, + mag_mesh, + receiver_locations, + tmp_path, + ): + """Test sensitivity_dtype.""" + # Create survey + receivers = mag.Point(receiver_locations, components="tmi") + sources = mag.UniformBackgroundField( + [receivers], amplitude=50_000, inclination=45, declination=10 + ) + survey = mag.Survey(sources) + # Create reduced identity map for Linear Problem + active_cells = np.ones(mag_mesh.n_cells, dtype=bool) + idenMap = maps.IdentityMap(nP=mag_mesh.n_cells) + # Create simulation + sensitivity_path = tmp_path + if engine == "choclo": + sensitivity_path /= "dummy" + simulation = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=idenMap, + active_cells=active_cells, + engine=engine, + store_sensitivities=store_sensitivities, + sensitivity_path=str(sensitivity_path), + ) + # sensitivity_dtype should be float64 when running forward only, + # but float32 in other cases + if store_sensitivities == "forward_only": + assert simulation.sensitivity_dtype is np.float64 + else: + assert simulation.sensitivity_dtype is np.float32 + + @pytest.mark.parametrize("invalid_dtype", (float, np.float16)) + def test_invalid_sensitivity_dtype_assignment(self, mag_mesh, invalid_dtype): + """ + Test invalid sensitivity_dtype assignment + """ + simulation = mag.Simulation3DIntegral(mag_mesh) + # Check if error is raised + msg = "sensitivity_dtype must be either np.float32 or np.float64." + with pytest.raises(TypeError, match=msg): + simulation.sensitivity_dtype = invalid_dtype + + def test_invalid_engine(self, mag_mesh): + """Test if error is raised after invalid engine.""" + engine = "invalid engine" + msg = rf"'engine' must be in \('geoana', 'choclo'\). Got '{engine}'" + with pytest.raises(ValueError, match=msg): + mag.Simulation3DIntegral(mag_mesh, engine=engine) + + def test_choclo_and_n_proceesses(self, mag_mesh): + """Check if warning is raised after passing n_processes with choclo engine.""" + msg = "The 'n_processes' will be ignored when selecting 'choclo'" + with pytest.warns(UserWarning, match=msg): + simulation = mag.Simulation3DIntegral( + mag_mesh, engine="choclo", n_processes=2 + ) + # Check if n_processes was overwritten and set to None + assert simulation.n_processes is None + + def test_choclo_and_sensitivity_path_as_dir(self, mag_mesh, tmp_path): + """ + Check if error is raised when sensitivity_path is a dir with choclo engine. + """ + # Create a sensitivity_path directory + sensitivity_path = tmp_path / "sensitivity_dummy" + sensitivity_path.mkdir() + # Check if error is raised + msg = re.escape( + f"The passed sensitivity_path '{str(sensitivity_path)}' is a directory" + ) + with pytest.raises(ValueError, match=msg): + mag.Simulation3DIntegral( + mag_mesh, + store_sensitivities="disk", + sensitivity_path=str(sensitivity_path), + engine="choclo", + ) + + def test_sensitivities_on_disk(self, mag_mesh, receiver_locations, tmp_path): + """ + Test if sensitivity matrix is correctly being stored in disk when asked + """ + # Build survey + survey = create_mag_survey( + components=["tmi"], + receiver_locations=receiver_locations, + inducing_field_params=(50000.0, 20.0, 45.0), + ) + # Build simulation + sensitivities_path = tmp_path / "sensitivities" + simulation = mag.Simulation3DIntegral( + mesh=mag_mesh, + survey=survey, + store_sensitivities="disk", + sensitivity_path=str(sensitivities_path), + engine="choclo", + ) + simulation.G + # Check if sensitivity matrix was stored in disk and is a memmap + assert sensitivities_path.is_file() + assert type(simulation.G) is np.memmap + + def test_sensitivities_on_ram(self, mag_mesh, receiver_locations): + """ + Test if sensitivity matrix is correctly being allocated in memory when asked + """ + # Build survey + survey = create_mag_survey( + components=["tmi"], + receiver_locations=receiver_locations, + inducing_field_params=(50000.0, 20.0, 45.0), + ) + # Build simulation + simulation = mag.Simulation3DIntegral( + mesh=mag_mesh, + survey=survey, + store_sensitivities="ram", + engine="choclo", + ) + simulation.G + # Check if sensitivity matrix is a Numpy array (stored in memory) + assert type(simulation.G) is np.ndarray + + def test_choclo_missing(self, mag_mesh, monkeypatch): + """ + Check if error is raised when choclo is missing and chosen as engine. + """ + # Monkeypatch choclo in simpeg.potential_fields.base + monkeypatch.setattr(simpeg.potential_fields.base, "choclo", None) + # Check if error is raised + msg = "The choclo package couldn't be found." + with pytest.raises(ImportError, match=msg): + mag.Simulation3DIntegral(mag_mesh, engine="choclo") def test_removed_modeltype(): - """Test if accesing removed modelType property raises error.""" + """Test if accessing removed modelType property raises error.""" h = [[(2, 2)], [(2, 2)], [(2, 2)]] mesh = discretize.TensorMesh(h) receiver_location = np.array([[0, 0, 100]]) receiver = mag.Point(receiver_location, components="tmi") - background_field = mag.UniformBackgroundField(receiver_list=[receiver]) + background_field = mag.UniformBackgroundField( + receiver_list=[receiver], amplitude=50_000, inclination=90, declination=0 + ) survey = mag.Survey(background_field) mapping = maps.IdentityMap(mesh, nP=mesh.n_cells) sim = mag.Simulation3DIntegral(mesh, survey=survey, chiMap=mapping) - message = "modelType has been removed, please use model_type." - with pytest.raises(NotImplementedError, match=message): + message = "has no attribute 'modelType'" + with pytest.raises(AttributeError, match=message): sim.modelType -if __name__ == "__main__": - unittest.main() +class BaseFixtures: + """ + Base test class with some fixtures. + + Requires that any child class implements a ``scalar_model`` boolean fixture. + It can be a standalone fixture, or it can be a class parametrization. + """ + + def build_survey(self, *, components): + # Observation points + x = np.linspace(-20.0, 20.0, 4) + x, y = np.meshgrid(x, x) + z = 5.0 * np.ones_like(x) + coordinates = np.vstack((x.ravel(), y.ravel(), z.ravel())).T + receivers = mag.receivers.Point(coordinates, components=components) + source_field = mag.UniformBackgroundField( + receiver_list=[receivers], + amplitude=55_000, + inclination=12, + declination=-35, + ) + survey = mag.survey.Survey(source_field) + return survey + + @pytest.fixture( + params=[ + "tmi", + ["bx", "by", "bz"], + ["tmi", "bx"], + ["tmi_x", "tmi_y", "tmi_z"], + ], + ids=["tmi", "mag_components", "tmi_and_mag", "tmi_derivs"], + ) + def survey(self, request): + """ + Return sample magnetic survey. + """ + return self.build_survey(components=request.param) + + @pytest.fixture + def mesh(self): + # Mesh + dh = 5.0 + hx = [(dh, 4)] + mesh = discretize.TensorMesh([hx, hx, hx], "CCN") + return mesh + + @pytest.fixture + def susceptibilities(self, mesh, scalar_model: bool): + """Create sample susceptibilities.""" + susceptibilities = 1e-10 * np.ones( + mesh.n_cells if scalar_model else 3 * mesh.n_cells + ) + ind_sphere = model_builder.get_indices_sphere( + np.r_[0.0, 0.0, -20.0], 10.0, mesh.cell_centers + ) + if scalar_model: + susceptibilities[ind_sphere] = 0.2 + else: + susceptibilities[: mesh.n_cells][ind_sphere] = 0.2 + susceptibilities[mesh.n_cells : 2 * mesh.n_cells][ind_sphere] = 0.3 + susceptibilities[2 * mesh.n_cells : 3 * mesh.n_cells][ind_sphere] = 0.5 + return susceptibilities + + +@pytest.mark.parametrize( + "scalar_model", [True, False], ids=["scalar_model", "vector_model"] +) +class TestnD(BaseFixtures): + """ + Test the ``nD`` property. + """ + + @pytest.fixture + def survey_b_norm(self): + return self.build_survey(components=["bx", "by", "bz"]) + + @pytest.fixture + def mapping(self, mesh, scalar_model): + nparams = mesh.n_cells if scalar_model else 3 * mesh.n_cells + return maps.IdentityMap(nP=nparams) + + def test_nD(self, mesh, survey, mapping, susceptibilities, scalar_model): + """ + Test nD on tmi data. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities="ram", + engine="choclo", + model_type=model_type, + ) + receivers = survey.source_field.receiver_list + n_data = sum(rx.locations.shape[0] * len(rx.components) for rx in receivers) + assert simulation.nD == n_data + dpred = simulation.dpred(susceptibilities) + assert dpred.size == simulation.nD + + def test_nD_amplitude_data( + self, mesh, survey_b_norm, mapping, susceptibilities, scalar_model + ): + """ + Test nD on amplitude data. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey_b_norm, + mesh=mesh, + chiMap=mapping, + store_sensitivities="ram", + engine="choclo", + model_type=model_type, + is_amplitude_data=True, + ) + receivers = survey_b_norm.source_field.receiver_list + n_data = ( + sum(rx.locations.shape[0] * len(rx.components) for rx in receivers) // 3 + ) + assert simulation.nD == n_data + dpred = simulation.dpred(susceptibilities) + assert dpred.size == simulation.nD + + +@pytest.mark.parametrize( + "scalar_model", [True, False], ids=["scalar_model", "vector_model"] +) +class TestGLinearOperator(BaseFixtures): + """ + Test G as a linear operator. + """ + + @pytest.fixture + def mapping(self, mesh, scalar_model): + nparams = mesh.n_cells if scalar_model else 3 * mesh.n_cells + return maps.IdentityMap(nP=nparams) + + @pytest.mark.parametrize("parallel", [True, False], ids=["parallel", "serial"]) + def test_G_dot_m( + self, survey, mesh, mapping, susceptibilities, scalar_model, parallel + ): + """Test G @ m.""" + model_type = "scalar" if scalar_model else "vector" + simulation, simulation_ram = ( + mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities=store, + engine="choclo", + numba_parallel=parallel, + model_type=model_type, + ) + for store in ("forward_only", "ram") + ) + assert isinstance(simulation.G, LinearOperator) + assert isinstance(simulation_ram.G, np.ndarray) + + expected = simulation_ram.G @ susceptibilities + + atol = np.max(np.abs(expected)) * 1e-8 + np.testing.assert_allclose(simulation.G @ susceptibilities, expected, atol=atol) + + @pytest.mark.parametrize("parallel", [True, False], ids=["parallel", "serial"]) + def test_G_t_dot_v(self, survey, mesh, mapping, scalar_model, parallel): + """Test G.T @ v.""" + model_type = "scalar" if scalar_model else "vector" + simulation, simulation_ram = ( + mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities=store, + engine="choclo", + numba_parallel=parallel, + model_type=model_type, + ) + for store in ("forward_only", "ram") + ) + assert isinstance(simulation.G, LinearOperator) + assert isinstance(simulation_ram.G, np.ndarray) + + vector = np.random.default_rng(seed=42).uniform(size=survey.nD) + expected = simulation_ram.G.T @ vector + + atol = np.max(np.abs(expected)) * 1e-7 + np.testing.assert_allclose(simulation.G.T @ vector, expected, atol=atol) + + def test_not_implemented(self, survey, mesh, mapping, scalar_model): + """ + Test NotImplementedError when forward_only and geoana as engine. + """ + engine, store = "geoana", "forward_only" + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities=store, + engine=engine, + model_type=model_type, + ) + msg = re.escape( + "Accessing matrix G with " + 'store_sensitivities="forward_only" and engine="geoana" ' + "hasn't been implemented yet." + ) + with pytest.raises(NotImplementedError, match=msg): + simulation.G + + +@pytest.mark.parametrize( + "scalar_model", [True, False], ids=["scalar_model", "vector_model"] +) +class TestJacobian(BaseFixtures): + """ + Test methods related to Jacobian matrix in magnetic simulation. + """ + + atol_ratio = 1e-7 + + @pytest.fixture(params=["identity_map", "exp_map"]) + def mapping(self, mesh, scalar_model: bool, request): + nparams = mesh.n_cells if scalar_model else 3 * mesh.n_cells + mapping = ( + maps.IdentityMap(nP=nparams) + if request.param == "identity_map" + else maps.ExpMap(nP=nparams) + ) + return mapping + + @pytest.mark.parametrize("engine", ["choclo", "geoana"]) + def test_getJ_as_array( + self, survey, mesh, mapping, susceptibilities, scalar_model, engine + ): + """ + Test the getJ method when J is an array in memory. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities="ram", + engine=engine, + model_type=model_type, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + + jac = simulation.getJ(model) + assert isinstance(jac, np.ndarray) + # With an identity mapping, the jacobian should be the same as G. + # With an exp mapping, the jacobian should be G @ the mapping derivative. + expected_jac = ( + simulation.G if is_identity_map else simulation.G @ mapping.deriv(model) + ) + np.testing.assert_allclose(jac, expected_jac) + + @pytest.mark.parametrize( + "engine", + [ + "choclo", + pytest.param( + "geoana", + marks=pytest.mark.xfail( + reason="not implemented", raises=NotImplementedError + ), + ), + ], + ) + @pytest.mark.parametrize("transpose", [False, True], ids=["J @ m", "J.T @ v"]) + def test_getJ_as_linear_operator( + self, survey, mesh, mapping, susceptibilities, scalar_model, engine, transpose + ): + """ + Test the getJ method when J is a linear operator. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities="forward_only", + engine=engine, + model_type=model_type, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + + jac = simulation.getJ(model) + assert isinstance(jac, LinearOperator) + if transpose: + vector = np.random.default_rng(seed=42).uniform(size=survey.nD) + result = jac.T @ vector + expected_result = mapping.deriv(model).T @ (simulation.G.T @ vector) + else: + result = jac @ model + expected_result = simulation.G @ (mapping.deriv(model).diagonal() * model) + np.testing.assert_allclose(result, expected_result) + + @pytest.mark.parametrize("engine", ["choclo", "geoana"]) + def test_getJ_not_implemented( + self, survey, mesh, mapping, susceptibilities, scalar_model, engine + ): + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities="ram", + engine=engine, + model_type=model_type, + is_amplitude_data=True, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + with pytest.raises(NotImplementedError): + simulation.getJ(model) + + @pytest.mark.parametrize( + ("engine", "store_sensitivities"), + [ + ("choclo", "ram"), + ("choclo", "forward_only"), + ("geoana", "ram"), + pytest.param( + "geoana", + "forward_only", + marks=pytest.mark.xfail( + reason="not implemented", raises=NotImplementedError + ), + ), + ], + ) + def test_Jvec( + self, + survey, + mesh, + mapping, + susceptibilities, + scalar_model, + engine, + store_sensitivities, + ): + """ + Test the Jvec method. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities=store_sensitivities, + engine=engine, + model_type=model_type, + sensitivity_dtype=np.float64, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + + vector = np.random.default_rng(seed=42).uniform(size=susceptibilities.size) + result = simulation.Jvec(model, vector) + + expected_jac = ( + simulation.G + if is_identity_map + else simulation.G @ aslinearoperator(mapping.deriv(model)) + ) + expected = expected_jac @ vector + + atol = np.max(np.abs(expected)) * self.atol_ratio + np.testing.assert_allclose(result, expected, atol=atol) + + @pytest.mark.parametrize( + ("engine", "store_sensitivities"), + [ + ("choclo", "ram"), + ("choclo", "forward_only"), + ("geoana", "ram"), + pytest.param( + "geoana", + "forward_only", + marks=pytest.mark.xfail( + reason="not implemented", raises=NotImplementedError + ), + ), + ], + ) + def test_Jtvec( + self, + survey, + mesh, + mapping, + susceptibilities, + scalar_model, + engine, + store_sensitivities, + ): + """ + Test the Jtvec method. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities=store_sensitivities, + engine=engine, + model_type=model_type, + sensitivity_dtype=np.float64, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + + vector = np.random.default_rng(seed=42).uniform(size=survey.nD) + result = simulation.Jtvec(model, vector) + + expected_jac = ( + simulation.G + if is_identity_map + else simulation.G @ aslinearoperator(mapping.deriv(model)) + ) + expected = expected_jac.T @ vector + + atol = np.max(np.abs(result)) * self.atol_ratio + np.testing.assert_allclose(result, expected, atol=atol) + + @pytest.mark.parametrize( + "engine", + [ + "choclo", + pytest.param( + "geoana", + marks=pytest.mark.xfail( + reason="not implemented", raises=NotImplementedError + ), + ), + ], + ) + @pytest.mark.parametrize("method", ["Jvec", "Jtvec"]) + def test_array_vs_linear_operator( + self, survey, mesh, mapping, susceptibilities, scalar_model, engine, method + ): + """ + Test methods when using "ram" and "forward_only". + + They should give the same results. + """ + model_type = "scalar" if scalar_model else "vector" + simulation_lo, simulation_ram = ( + mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities=store, + engine=engine, + model_type=model_type, + sensitivity_dtype=np.float64, + ) + for store in ("forward_only", "ram") + ) + match method: + case "Jvec": + vector_size = susceptibilities.size + case "Jtvec": + vector_size = survey.nD + case _: # pragma: no cover + raise ValueError(f"Invalid method '{method}'") # pragma: no cover + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + + vector = np.random.default_rng(seed=42).uniform(size=vector_size) + result_lo = getattr(simulation_lo, method)(model, vector) + result_ram = getattr(simulation_ram, method)(model, vector) + atol = np.max(np.abs(result_ram)) * self.atol_ratio + np.testing.assert_allclose(result_lo, result_ram, atol=atol) + + @pytest.mark.parametrize("engine", ["choclo", "geoana"]) + @pytest.mark.parametrize("weights", [True, False]) + def test_getJtJdiag( + self, survey, mesh, mapping, susceptibilities, scalar_model, engine, weights + ): + """ + Test the ``getJtJdiag`` method with G as an array in memory. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities="ram", + engine=engine, + model_type=model_type, + sensitivity_dtype=np.float64, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + + kwargs = {} + if weights: + w_matrix = diags(np.random.default_rng(seed=42).uniform(size=survey.nD)) + kwargs = {"W": w_matrix} + jtj_diag = simulation.getJtJdiag(model, **kwargs) + + expected_jac = ( + simulation.G if is_identity_map else simulation.G @ mapping.deriv(model) + ) + if weights: + expected = np.diag(expected_jac.T @ w_matrix.T @ w_matrix @ expected_jac) + else: + expected = np.diag(expected_jac.T @ expected_jac) + + atol = np.max(np.abs(jtj_diag)) * self.atol_ratio + np.testing.assert_allclose(jtj_diag, expected, atol=atol) + + @pytest.mark.parametrize( + ("engine", "is_amplitude_data"), + [("geoana", True), ("geoana", False), ("choclo", True)], + ids=("geoana-amplitude_data", "geoana-regular_data", "choclo-amplitude_data"), + ) + def test_getJtJdiag_not_implemented( + self, + survey, + mesh, + mapping, + susceptibilities, + scalar_model, + engine, + is_amplitude_data, + ): + """ + Test NotImplementedErrors on ``getJtJdiag``. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities="forward_only", + engine=engine, + is_amplitude_data=is_amplitude_data, + model_type=model_type, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + with pytest.raises(NotImplementedError): + simulation.getJtJdiag(model) + + @pytest.mark.parametrize("parallel", [True, False], ids=("parallel", "serial")) + def test_getJtJdiag_forward_only( + self, survey, mesh, mapping, susceptibilities, scalar_model, parallel + ): + """ + Test the ``getJtJdiag`` method with ``"forward_only"`` and choclo. + """ + model_type = "scalar" if scalar_model else "vector" + simulation, simulation_ram = ( + mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities=store, + engine="choclo", + numba_parallel=parallel, + model_type=model_type, + ) + for store in ("forward_only", "ram") + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + + expected = simulation_ram.getJtJdiag(model) + result = simulation.getJtJdiag(model) + + atol = np.max(np.abs(expected)) * 1e-8 + np.testing.assert_allclose(result, expected, atol=atol) + + @pytest.mark.parametrize("engine", ["choclo", "geoana"]) + def test_getJtJdiag_caching( + self, survey, mesh, mapping, susceptibilities, scalar_model, engine + ): + """ + Test the caching behaviour of the ``getJtJdiag`` method. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities="ram", + engine=engine, + model_type=model_type, + ) + + # Get diagonal of J.T @ J without any weight + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + jtj_diagonal_1 = simulation.getJtJdiag(model) + assert hasattr(simulation, "_gtg_diagonal") + assert hasattr(simulation, "_weights_sha256") + gtg_diagonal_1 = simulation._gtg_diagonal + weights_sha256_1 = simulation._weights_sha256 + + # Compute it again and make sure we get the same result + np.testing.assert_allclose(jtj_diagonal_1, simulation.getJtJdiag(model)) + + # Get a new diagonal with weights + weights_matrix = diags( + np.random.default_rng(seed=42).uniform(size=simulation.survey.nD) + ) + jtj_diagonal_2 = simulation.getJtJdiag(model, W=weights_matrix) + assert hasattr(simulation, "_gtg_diagonal") + assert hasattr(simulation, "_weights_sha256") + gtg_diagonal_2 = simulation._gtg_diagonal + weights_sha256_2 = simulation._weights_sha256 + + # The two results should be different + assert not np.array_equal(jtj_diagonal_1, jtj_diagonal_2) + assert not np.array_equal(gtg_diagonal_1, gtg_diagonal_2) + assert weights_sha256_1.digest() != weights_sha256_2.digest() + + +@pytest.mark.parametrize( + "scalar_model", [True, False], ids=["scalar_model", "vector_model"] +) +class TestJacobianAmplitudeData(BaseFixtures): + """ + Test Jacobian related methods with ``is_amplitude_data``. + """ + + atol_ratio = 1e-7 + + @pytest.fixture + def survey(self): + """ + Sample survey with fixed components bx, by, bz. + + These components are assumed when working with ``is_amplitude_data=True``. + """ + # Observation points + x = np.linspace(-20.0, 20.0, 4) + x, y = np.meshgrid(x, x) + z = 5.0 * np.ones_like(x) + coordinates = np.vstack((x.ravel(), y.ravel(), z.ravel())).T + receivers = mag.receivers.Point(coordinates, components=["bx", "by", "bz"]) + source_field = mag.UniformBackgroundField( + receiver_list=[receivers], + amplitude=55_000, + inclination=12, + declination=-35, + ) + survey = mag.survey.Survey(source_field) + return survey + + @pytest.fixture(params=["identity_map", "exp_map"]) + def mapping(self, mesh, scalar_model: bool, request): + nparams = mesh.n_cells if scalar_model else 3 * mesh.n_cells + mapping = ( + maps.IdentityMap(nP=nparams) + if request.param == "identity_map" + else maps.ExpMap(nP=nparams) + ) + return mapping + + @pytest.mark.parametrize("engine", ["choclo", "geoana"]) + def test_getJ_not_implemented( + self, survey, mesh, mapping, susceptibilities, scalar_model, engine + ): + """ + Test the getJ method when J is an array in memory. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities="ram", + engine=engine, + model_type=model_type, + is_amplitude_data=True, + sensitivity_dtype=np.float64, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + with pytest.raises(NotImplementedError): + simulation.getJ(model) + + @pytest.mark.parametrize( + ("engine", "store_sensitivities"), + [ + ("choclo", "ram"), + ("choclo", "forward_only"), + ("geoana", "ram"), + pytest.param( + "geoana", + "forward_only", + marks=pytest.mark.xfail( + reason="not implemented", raises=NotImplementedError + ), + ), + ], + ) + def test_Jvec( + self, + survey, + mesh, + mapping, + susceptibilities, + scalar_model, + engine, + store_sensitivities, + ): + r""" + Test the Jvec method. + + Test the Jvec method through an alternative implementation. + Define a :math:`f(\chi)` forward model function that returns the norm of the + magnetic field given the susceptibility values of :math:`\chi`: + + .. math:: + + f(\chi) + = \lvert \mathbf{B} \rvert + = \sqrt{B_x^2(\chi) + B_y^2(\chi) + B_z^2(\chi)} + + The gradient of :math:`f(\chi)` (jacobian matrix :math:`\mathbf{J}`) can be + written as: + + .. math:: + + \mathbf{J} = \mathbf{J}_x + \mathbf{J}_y + \mathbf{J}_z + + where: + + .. math:: + + \mathbf{J}_x = + \frac{1}{\lvert \mathbf{B} \rvert} + B_x(\chi) + \frac{\partial B_x}{\partial \chi} + \frac{\partial \chi}{\partial \mathbf{m}} + + and :math:`\mathbf{m}` is the model. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities=store_sensitivities, + engine=engine, + model_type=model_type, + is_amplitude_data=True, + sensitivity_dtype=np.float64, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + vector = np.random.default_rng(seed=42).uniform(size=susceptibilities.size) + result = simulation.Jvec(model, vector) + + magnetic_field = simulation.G @ susceptibilities + bx, by, bz = (magnetic_field[i::3] for i in (0, 1, 2)) + inv_amplitude = 1 / np.sqrt(bx**2 + by**2 + bz**2) + + g_dot_chideriv_v = ( + simulation.G @ aslinearoperator(mapping.deriv(model)) @ vector + ) + jac_x_dot_v = diags(inv_amplitude) @ diags(bx) @ g_dot_chideriv_v[0::3] + jac_y_dot_v = diags(inv_amplitude) @ diags(by) @ g_dot_chideriv_v[1::3] + jac_z_dot_v = diags(inv_amplitude) @ diags(bz) @ g_dot_chideriv_v[2::3] + expected = jac_x_dot_v + jac_y_dot_v + jac_z_dot_v + + atol = np.max(np.abs(expected)) * self.atol_ratio + np.testing.assert_allclose(result, expected, atol=atol) + + @pytest.mark.parametrize( + ("engine", "store_sensitivities"), + [ + ("choclo", "ram"), + ("choclo", "forward_only"), + ("geoana", "ram"), + pytest.param( + "geoana", + "forward_only", + marks=pytest.mark.xfail( + reason="not implemented", raises=NotImplementedError + ), + ), + ], + ) + def test_Jtvec( + self, + survey, + mesh, + mapping, + susceptibilities, + scalar_model, + engine, + store_sensitivities, + ): + """ + Test the Jtvec method. + + Test it similarly to Jvec, but computing the transpose of the matrices. + """ + model_type = "scalar" if scalar_model else "vector" + simulation = mag.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chiMap=mapping, + store_sensitivities=store_sensitivities, + engine=engine, + model_type=model_type, + is_amplitude_data=True, + sensitivity_dtype=np.float64, + ) + is_identity_map = type(mapping) is maps.IdentityMap + model = susceptibilities if is_identity_map else np.log(susceptibilities) + + # Need to set size as survey.nD / 3 because there's a bug in simulation.nD. + vector = np.random.default_rng(seed=42).uniform(size=survey.nD // 3) + result = simulation.Jtvec(model, vector) + + magnetic_field = simulation.G @ susceptibilities + bx, by, bz = (magnetic_field[i::3] for i in (0, 1, 2)) + inv_amplitude = 1 / np.sqrt(bx**2 + by**2 + bz**2) + v = np.array( + ( + bx * inv_amplitude * vector, + by * inv_amplitude * vector, + bz * inv_amplitude * vector, + ) + ).T.ravel() # interleave the values for bx, by, bz + expected = mapping.deriv(model).T @ (simulation.G.T @ v) + + atol = np.max(np.abs(result)) * self.atol_ratio + np.testing.assert_allclose(result, expected, atol=atol) diff --git a/tests/pf/test_forward_PFproblem.py b/tests/pf/test_forward_PFproblem.py deleted file mode 100644 index 660af07d3f..0000000000 --- a/tests/pf/test_forward_PFproblem.py +++ /dev/null @@ -1,88 +0,0 @@ -import unittest -import discretize -from simpeg import utils, maps -from simpeg.utils.model_builder import get_indices_sphere -from simpeg.potential_fields import magnetics as mag -import numpy as np -from pymatsolver import Pardiso - - -class MagFwdProblemTests(unittest.TestCase): - def setUp(self): - Inc = 45.0 - Dec = 45.0 - Btot = 51000 - - self.b0 = mag.analytics.IDTtoxyz(-Inc, Dec, Btot) - - cs = 25.0 - hxind = [(cs, 5, -1.3), (cs / 2.0, 41), (cs, 5, 1.3)] - hyind = [(cs, 5, -1.3), (cs / 2.0, 41), (cs, 5, 1.3)] - hzind = [(cs, 5, -1.3), (cs / 2.0, 40), (cs, 5, 1.3)] - M = discretize.TensorMesh([hxind, hyind, hzind], "CCC") - - chibkg = 0.0 - self.chiblk = 0.01 - chi = np.ones(M.nC) * chibkg - - self.rad = 100 - self.sphere_center = [0.0, 0.0, 0.0] - sph_ind = get_indices_sphere(self.sphere_center, self.rad, M.gridCC) - chi[sph_ind] = self.chiblk - - xr = np.linspace(-300, 300, 41) - yr = np.linspace(-300, 300, 41) - X, Y = np.meshgrid(xr, yr) - Z = np.ones((xr.size, yr.size)) * 150 - components = ["bx", "by", "bz"] - self.xr = xr - self.yr = yr - self.rxLoc = np.c_[utils.mkvc(X), utils.mkvc(Y), utils.mkvc(Z)] - receivers = mag.Point(self.rxLoc, components=components) - srcField = mag.UniformBackgroundField( - receiver_list=[receivers], - amplitude=Btot, - inclination=Inc, - declination=Dec, - ) - - self.survey = mag.Survey(srcField) - - self.sim = mag.simulation.Simulation3DDifferential( - M, - survey=self.survey, - muMap=maps.ChiMap(M), - solver=Pardiso, - ) - self.M = M - self.chi = chi - - def test_ana_forward(self): - u = self.sim.fields(self.chi) - dpred = self.sim.projectFields(u) - - bxa, bya, bza = mag.analytics.MagSphereAnaFunA( - self.rxLoc[:, 0], - self.rxLoc[:, 1], - self.rxLoc[:, 2], - self.rad, - *self.sphere_center, - self.chiblk, - self.b0, - "secondary" - ) - - n_obs, n_comp = self.rxLoc.shape[0], len(self.survey.components) - dx, dy, dz = dpred.reshape(n_comp, n_obs) - - err_x = np.linalg.norm(dx - bxa) / np.linalg.norm(bxa) - err_y = np.linalg.norm(dy - bya) / np.linalg.norm(bya) - err_z = np.linalg.norm(dz - bza) / np.linalg.norm(bza) - - self.assertLess(err_x, 0.08) - self.assertLess(err_y, 0.08) - self.assertLess(err_z, 0.08) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/pf/test_forward_mag_differential.py b/tests/pf/test_forward_mag_differential.py new file mode 100644 index 0000000000..f6571073fe --- /dev/null +++ b/tests/pf/test_forward_mag_differential.py @@ -0,0 +1,414 @@ +import re +import pytest +import discretize +import simpeg.potential_fields as PF +from simpeg import utils, maps +from discretize.utils import mkvc, refine_tree_xyz +import numpy as np +from tests.utils.ellipsoid import ProlateEllipsoid + + +@pytest.fixture +def mesh(): + + dhx, dhy, dhz = 50.0, 50.0, 50.0 # minimum cell width (base mesh cell width) + nbcx = 512 # number of base mesh cells in x + nbcy = 512 + nbcz = 512 + + # Define base mesh (domain and finest discretization) + hx = dhx * np.ones(nbcx) + hy = dhy * np.ones(nbcy) + hz = dhz * np.ones(nbcz) + _mesh = discretize.TreeMesh([hx, hy, hz], x0="CCC") + + xp, yp, zp = np.meshgrid([-1400.0, 1400.0], [-1400.0, 1400.0], [-1000.0, 200.0]) + xy = np.c_[mkvc(xp), mkvc(yp), mkvc(zp)] + _mesh = refine_tree_xyz( + _mesh, + xy, + method="box", + finalize=False, + octree_levels=[1, 1, 1, 1], + ) + _mesh.finalize() + + return _mesh + + +def get_survey(components=("bx", "by", "bz")): + ccx = np.linspace(-1400, 1400, num=57) + ccy = np.linspace(-1400, 1400, num=57) + ccx, ccy = np.meshgrid(ccx, ccy) + ccz = 50.0 * np.ones_like(ccx) + rxLoc = PF.magnetics.receivers.Point( + np.c_[utils.mkvc(ccy.T), utils.mkvc(ccx.T), utils.mkvc(ccz.T)], + components=components, + ) + inducing_field = [55000.0, 60.0, 90.0] + srcField = PF.magnetics.sources.UniformBackgroundField( + [rxLoc], inducing_field[0], inducing_field[1], inducing_field[2] + ) + _survey = PF.magnetics.survey.Survey(srcField) + + return _survey + + +@pytest.mark.parametrize("model_type", ("mu_rem", "mu", "rem")) +def test_forward(model_type, mesh): + """ + Test against the analytic solution for an ellipse with + uniform intrinsic remanence and susceptibility in a + uniform ambient geomagnetic field + """ + tol = 0.1 + + survey = get_survey() + + amplitude = survey.source_field.amplitude + inclination = survey.source_field.inclination + declination = survey.source_field.declination + inducing_field = [amplitude, inclination, declination] + + if model_type == "mu_rem": + susceptibility = 5 + MrX = 150000 + MrY = 150000 + MrZ = 150000 + if model_type == "mu": + susceptibility = 5 + MrX = 0 + MrY = 0 + MrZ = 0 + if model_type == "rem": + susceptibility = 0 + MrX = 150000 + MrY = 150000 + MrZ = 150000 + + center = np.array([00, 0, -400.0]) + axes = [600.0, 200.0] + strike_dip_rake = [0, 0, 90] + + ellipsoid = ProlateEllipsoid( + center, + axes, + strike_dip_rake, + susceptibility=susceptibility, + Mr=(MrX, MrY, MrZ), + inducing_field=inducing_field, + ) + ind_ellipsoid = ellipsoid.get_indices(mesh.cell_centers) + + sus_model = np.zeros(mesh.n_cells) + sus_model[ind_ellipsoid] = susceptibility + mu_model = maps.ChiMap() * sus_model + + Rx = np.zeros(mesh.n_cells) + Ry = np.zeros(mesh.n_cells) + Rz = np.zeros(mesh.n_cells) + + Rx[ind_ellipsoid] = MrX + Ry[ind_ellipsoid] = MrY + Rz[ind_ellipsoid] = MrZ + + u0_Mr_model = mkvc(np.array([Rx, Ry, Rz]).T) + + if model_type == "mu": + u0_Mr_model = None + if model_type == "rem": + mu_model = None + + simulation = PF.magnetics.simulation.Simulation3DDifferential( + survey=survey, mesh=mesh, mu=mu_model, rem=u0_Mr_model, solver_dtype=np.float32 + ) + + dpred_numeric = simulation.dpred() + dpred_analytic = mkvc(ellipsoid.anomalous_bfield(survey.receiver_locations)) + + assert np.allclose( + dpred_numeric, + dpred_analytic, + rtol=0.1, + atol=0.05 * np.max(np.abs(dpred_analytic)), + ) + + err = np.linalg.norm(dpred_numeric - dpred_analytic) / np.linalg.norm( + dpred_analytic + ) + + print( + "\n||dpred_analytic-dpred_numeric||/||dpred_analytic|| = " + + "{:.{}f}".format(err, 2) + + ", tol = " + + str(tol) + ) + + assert err < tol + + u0_M_analytic = ellipsoid.Magnetization() + u0_M_numeric = mesh.average_face_to_cell_vector * simulation.magnetic_polarization() + u0_M_numeric = u0_M_numeric.reshape((mesh.n_cells, 3), order="F") + u0_M_numeric = np.mean(u0_M_numeric[ind_ellipsoid, :], axis=0) + + assert np.allclose( + u0_M_numeric, + u0_M_analytic, + rtol=0.1, + atol=0.01 * np.max(np.abs(u0_M_analytic)), + ) + + +def test_exact_tmi(mesh): + """ + Test against the analytic solution for an ellipse with + uniform intrinsic remanence and susceptibility in a + uniform ambient geomagnetic field + """ + tol = 1e-8 + + survey = get_survey(components=["bx", "by", "bz", "tmi"]) + + amplitude = survey.source_field.amplitude + inclination = survey.source_field.inclination + declination = survey.source_field.declination + inducing_field = [amplitude, inclination, declination] + + susceptibility = 5 + MrX = 150000 + MrY = 150000 + MrZ = 150000 + + center = np.array([00, 0, -400.0]) + axes = [600.0, 200.0] + strike_dip_rake = [0, 0, 90] + + ellipsoid = ProlateEllipsoid( + center, + axes, + strike_dip_rake, + susceptibility=susceptibility, + Mr=(MrX, MrY, MrZ), + inducing_field=inducing_field, + ) + ind_ellipsoid = ellipsoid.get_indices(mesh.cell_centers) + + sus_model = np.zeros(mesh.n_cells) + sus_model[ind_ellipsoid] = susceptibility + mu_model = maps.ChiMap() * sus_model + + Rx = np.zeros(mesh.n_cells) + Ry = np.zeros(mesh.n_cells) + Rz = np.zeros(mesh.n_cells) + + Rx[ind_ellipsoid] = MrX + Ry[ind_ellipsoid] = MrY + Rz[ind_ellipsoid] = MrZ + + u0_Mr_model = mkvc(np.array([Rx, Ry, Rz]).T) + + simulation = PF.magnetics.simulation.Simulation3DDifferential( + survey=survey, + mesh=mesh, + mu=mu_model, + rem=u0_Mr_model, + ) + + dpred_numeric = simulation.dpred() + + dpred_fields = np.reshape(dpred_numeric[: survey.nRx * 4], (4, survey.nRx)).T + + B0 = survey.source_field.b0 + + TMI_exact_analytic = np.linalg.norm(dpred_fields[:, :3] + B0, axis=1) - amplitude + dpred_TMI_exact = dpred_fields[:, 3] + + TMI_exact_err = np.max(np.abs(dpred_TMI_exact - TMI_exact_analytic)) + + assert TMI_exact_err < tol + print( + "max(TMI_exact_err) = " + + "{:.{}e}".format(TMI_exact_err, 2) + + ", tol = " + + str(tol) + ) + + +def test_differential_magnetization_against_integral(mesh): + + survey = get_survey() + + amplitude = survey.source_field.amplitude + inclination = survey.source_field.inclination + declination = survey.source_field.declination + inducing_field = [amplitude, inclination, declination] + + MrX = 150000 + MrY = 150000 + MrZ = 150000 + + center = np.array([00, 0, -400.0]) + axes = [600.0, 200.0] + strike_dip_rake = [0, 0, 90] + + ellipsoid = ProlateEllipsoid( + center, + axes, + strike_dip_rake, + Mr=np.array([MrX, MrY, MrZ]), + inducing_field=inducing_field, + ) + ind_ellipsoid = ellipsoid.get_indices(mesh.cell_centers) + + Rx = np.zeros(mesh.n_cells) + Ry = np.zeros(mesh.n_cells) + Rz = np.zeros(mesh.n_cells) + + Rx[ind_ellipsoid] = MrX + Ry[ind_ellipsoid] = MrY + Rz[ind_ellipsoid] = MrZ + + u0_Mr_model = mkvc(np.array([Rx, Ry, Rz]).T) + eff_sus_model = (u0_Mr_model / amplitude)[ + np.hstack((ind_ellipsoid, ind_ellipsoid, ind_ellipsoid)) + ] + + simulation_differential = PF.magnetics.simulation.Simulation3DDifferential( + survey=survey, + mesh=mesh, + rem=u0_Mr_model, + ) + + simulation_integral = PF.magnetics.simulation.Simulation3DIntegral( + survey=survey, + mesh=mesh, + chi=eff_sus_model, + model_type="vector", + store_sensitivities="forward_only", + active_cells=ind_ellipsoid, + ) + + dpred_numeric_differential = simulation_differential.dpred() + dni = simulation_integral.dpred() + dpred_numeric_integral = np.hstack((dni[0::3], dni[1::3], dni[2::3])) + dpred_analytic = mkvc(ellipsoid.anomalous_bfield(survey.receiver_locations)) + + diff_numeric = np.linalg.norm( + dpred_numeric_differential - dpred_numeric_integral + ) / np.linalg.norm(dpred_numeric_integral) + diff_differential = np.linalg.norm( + dpred_numeric_differential - dpred_analytic + ) / np.linalg.norm(dpred_analytic) + diff_integral = np.linalg.norm( + dpred_numeric_integral - dpred_analytic + ) / np.linalg.norm(dpred_analytic) + + # Check both discretized solutions are closer to each other than to the analytic + assert diff_numeric < diff_differential + assert diff_numeric < diff_integral + + print( + "\n||dpred_integral-dpred_pde||/||dpred_integral|| = " + + "{:.{}f}".format(diff_numeric, 2) + ) + print( + "||dpred_integral-dpred_analytic||/||dpred_analytic|| = " + + "{:.{}f}".format(diff_integral, 2) + ) + print( + "||dpred_pde-dpred_analytic||/||dpred_analytic|| = " + + "{:.{}f}".format(diff_differential, 2) + ) + + +def test_invalid_solver_dtype(mesh): + """ + Test error upon invalid `solver_dtype`. + """ + survey = get_survey() + invalid_dtype = np.int64 + msg = re.escape( + f"Invalid `solver_dtype` '{invalid_dtype}'. " + "It must be np.float32 or np.float64." + ) + with pytest.raises(ValueError, match=msg): + PF.magnetics.simulation.Simulation3DDifferential( + survey=survey, mesh=mesh, solver_dtype=invalid_dtype + ) + + +@pytest.mark.parametrize( + "components", + ["bx", "by", "bz", "tmi", ["bx", "by", "bz"]], + ids=["bx", "by", "bz", "tmi", "b_field"], +) +class TestGetJ: + + @pytest.fixture + def mesh_small(self): + """ + Define a small mesh that would generate a J matrix small enough to fit in memory + """ + h = [(10.0, 8)] + mesh = discretize.TreeMesh([h, h, h], x0="CCC", diagonal_balance=True) + mesh.refine_points((0, 0, 0), level=-1) + mesh.finalize() + return mesh + + @pytest.fixture + def survey_small(self, components): + """ + Define a small survey. + """ + x = np.linspace(-20, 20, 11) + x, y = tuple(c.ravel() for c in np.meshgrid(x, x)) + z = np.ones_like(x) + locations = np.vstack((x, y, z)).T + receiver = PF.magnetics.receivers.Point( + locations, + components=components, + ) + inducing_field = (55_000, -71, 12) + source = PF.magnetics.sources.UniformBackgroundField( + [receiver], *inducing_field + ) + survey = PF.magnetics.survey.Survey(source) + return survey + + def test_getJ_vs_Jvec(self, mesh_small, survey_small): + """ + Test the getJ method against Jvec. + """ + rng = np.random.default_rng(seed=41) + model = rng.uniform(0, 1e-1, size=mesh_small.n_cells) + mapping = maps.IdentityMap(nP=model.size) + simulation = PF.magnetics.simulation.Simulation3DDifferential( + survey=survey_small, + mesh=mesh_small, + muMap=mapping, + storeJ=False, # explicitly not storing J + ) + + vector = rng.uniform(0, 1e-1, size=mesh_small.n_cells) + result = simulation.getJ(model) @ vector + expected = simulation.Jvec(model, vector) + np.testing.assert_allclose(result, expected) + + def test_getJ_vs_Jtvec(self, mesh_small, survey_small): + """ + Test the getJ method against Jtvec. + """ + rng = np.random.default_rng(seed=41) + model = rng.uniform(0, 1e-1, size=mesh_small.n_cells) + mapping = maps.IdentityMap(nP=model.size) + simulation = PF.magnetics.simulation.Simulation3DDifferential( + survey=survey_small, + mesh=mesh_small, + muMap=mapping, + storeJ=False, # explicitly not storing J + ) + + vector = rng.uniform(0, 1e-1, size=survey_small.nD) + result = simulation.getJ(model).T @ vector + expected = simulation.Jtvec(model, vector) + np.testing.assert_allclose(result, expected) diff --git a/tests/pf/test_grav_inversion_linear.py b/tests/pf/test_grav_inversion_linear.py index 3657c4668d..a561436fcd 100644 --- a/tests/pf/test_grav_inversion_linear.py +++ b/tests/pf/test_grav_inversion_linear.py @@ -77,7 +77,7 @@ def test_gravity_inversion_linear(engine): mesh, survey=survey, rhoMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="ram", engine=engine, **kwargs, @@ -98,13 +98,13 @@ def test_gravity_inversion_linear(engine): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 + maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, cg_maxiter=10, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) # Here is where the norms are applied starting_beta = directives.BetaEstimateMaxDerivative(10.0) - IRLS = directives.Update_IRLS() + IRLS = directives.UpdateIRLS() update_Jacobi = directives.UpdatePreconditioner() sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) inv = inversion.BaseInversion( diff --git a/tests/pf/test_mag_MVI_Octree.py b/tests/pf/test_mag_MVI_Octree.py index 5fc3fc68c2..667e6d05fa 100644 --- a/tests/pf/test_mag_MVI_Octree.py +++ b/tests/pf/test_mag_MVI_Octree.py @@ -19,7 +19,6 @@ class MVIProblemTest(unittest.TestCase): def setUp(self): - np.random.seed(0) h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # The magnetization is set along a different @@ -75,14 +74,14 @@ def setUp(self): M_xyz = utils.mat_utils.dip_azimuth2cartesian(M[0], M[1]) # Get the indicies of the magnetized block - ind = utils.model_builder.get_indices_block( + indices = utils.model_builder.get_indices_block( np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, - )[0] + ) # Assign magnetization values - model[ind, :] = np.kron(np.ones((ind.shape[0], 1)), M_xyz * 0.05) + model[indices, :] = np.kron(np.ones((indices.size, 1)), M_xyz * 0.05) # Remove air cells self.model = model[actv, :] @@ -99,14 +98,18 @@ def setUp(self): survey=survey, model_type="vector", chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="disk", ) self.sim = sim # Compute some data and add some random noise data = sim.make_synthetic_data( - utils.mkvc(self.model), relative_error=0.0, noise_floor=5.0, add_noise=True + utils.mkvc(self.model), + relative_error=0.0, + noise_floor=5.0, + add_noise=True, + random_seed=0, ) # This Mapping connects the regularizations for the three-component @@ -132,7 +135,7 @@ def setUp(self): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=10, lower=-10, upper=10.0, maxIterLS=5, maxIterCG=5, tolCG=1e-4 + maxIter=10, lower=-10, upper=10.0, maxIterLS=5, cg_maxiter=5, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) @@ -143,8 +146,8 @@ def setUp(self): # Here is where the norms are applied # Use pick a treshold parameter empirically based on the distribution of # model parameters - IRLS = directives.Update_IRLS( - f_min_change=1e-3, max_irls_iterations=0, beta_tol=5e-1 + IRLS = directives.UpdateIRLS( + f_min_change=1e-3, max_irls_iterations=0, misfit_tolerance=5e-1 ) # Pre-conditioner @@ -195,23 +198,22 @@ def setUp(self): lower=Lbound, upper=Ubound, maxIterLS=5, - maxIterCG=5, - tolCG=1e-3, - stepOffBoundsFact=1e-3, + cg_maxiter=5, + cg_rtol=1e-3, + active_set_grad_scale=1e-3, ) opt.approxHinv = None invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=beta) # Here is where the norms are applied - IRLS = directives.Update_IRLS( + IRLS = directives.UpdateIRLS( f_min_change=1e-4, max_irls_iterations=5, - minGNiter=1, - beta_tol=0.5, - coolingRate=1, - coolEps_q=True, - sphericalDomain=True, + misfit_tolerance=0.5, + ) + spherical_scale = directives.SphericalUnitsWeights( + amplitude=wires.amp, angles=[reg_t, reg_p] ) # Special directive specific to the mag amplitude problem. The sensitivity @@ -222,7 +224,13 @@ def setUp(self): self.inv = inversion.BaseInversion( invProb, - directiveList=[ProjSpherical, IRLS, sensitivity_weights, update_Jacobi], + directiveList=[ + spherical_scale, + ProjSpherical, + IRLS, + sensitivity_weights, + update_Jacobi, + ], ) def test_mag_inverse(self): diff --git a/tests/pf/test_mag_differential_functionality.py b/tests/pf/test_mag_differential_functionality.py new file mode 100644 index 0000000000..f682b39fe6 --- /dev/null +++ b/tests/pf/test_mag_differential_functionality.py @@ -0,0 +1,177 @@ +import pytest +import discretize +import simpeg.potential_fields as PF +from simpeg import utils, maps +from discretize.utils import mkvc, refine_tree_xyz +import numpy as np +from tests.utils.ellipsoid import ProlateEllipsoid + + +@pytest.fixture +def mesh(): + + dhx, dhy, dhz = 75.0, 75.0, 75.0 # minimum cell width (base mesh cell width) + nbcx = 512 # number of base mesh cells in x + nbcy = 512 + nbcz = 512 + + # Define base mesh (domain and finest discretization) + hx = dhx * np.ones(nbcx) + hy = dhy * np.ones(nbcy) + hz = dhz * np.ones(nbcz) + _mesh = discretize.TreeMesh([hx, hy, hz], x0="CCC") + + xp, yp, zp = np.meshgrid([-1400.0, 1400.0], [-1400.0, 1400.0], [-1000.0, 200.0]) + xy = np.c_[mkvc(xp), mkvc(yp), mkvc(zp)] + _mesh = refine_tree_xyz( + _mesh, + xy, + method="box", + finalize=False, + octree_levels=[1, 1, 1, 1], + ) + _mesh.finalize() + + return _mesh + + +def test_recievers(mesh): + """ + Test that multiple point recievers with different components work. + """ + + ccx = np.linspace(-1400, 1400, num=57) + ccy = np.linspace(-1400, 1400, num=57) + ccx, ccy = np.meshgrid(ccx, ccy) + ccz = 50.0 * np.ones_like(ccx) + components_1 = ["bx", "by", "bz", "tmi"] + components_2 = ["by", "tmi"] + rxLoc_1 = PF.magnetics.receivers.Point( + np.c_[utils.mkvc(ccy.T), utils.mkvc(ccx.T), utils.mkvc(ccz.T)], + components=components_1, + ) + rxLoc_2 = PF.magnetics.receivers.Point( + np.c_[utils.mkvc(ccy.T), utils.mkvc(ccx.T), utils.mkvc(ccz.T + 20)], + components=components_2, + ) + inducing_field = [55000.0, 60.0, 90.0] + + srcField_1 = PF.magnetics.sources.UniformBackgroundField( + [rxLoc_1], inducing_field[0], inducing_field[1], inducing_field[2] + ) + survey_1 = PF.magnetics.survey.Survey(srcField_1) + + srcField_2 = PF.magnetics.sources.UniformBackgroundField( + [rxLoc_2], inducing_field[0], inducing_field[1], inducing_field[2] + ) + survey_2 = PF.magnetics.survey.Survey(srcField_2) + + srcField_all = PF.magnetics.sources.UniformBackgroundField( + [rxLoc_1, rxLoc_2], inducing_field[0], inducing_field[1], inducing_field[2] + ) + survey_all = PF.magnetics.survey.Survey(srcField_all) + + amplitude = survey_1.source_field.amplitude + inclination = survey_1.source_field.inclination + declination = survey_1.source_field.declination + inducing_field = [amplitude, inclination, declination] + + susceptibility = 5 + MrX = 150000 + MrY = 150000 + MrZ = 150000 + + center = np.array([00, 0, -400.0]) + axes = [600.0, 200.0] + strike_dip_rake = [0, 0, 90] + + ellipsoid = ProlateEllipsoid( + center, + axes, + strike_dip_rake, + susceptibility=susceptibility, + Mr=(MrX, MrY, MrZ), + inducing_field=inducing_field, + ) + + ind_ellipsoid = ellipsoid.get_indices(mesh.cell_centers) + + sus_model = np.zeros(mesh.n_cells) + sus_model[ind_ellipsoid] = susceptibility + + Rx = np.zeros(mesh.n_cells) + Ry = np.zeros(mesh.n_cells) + Rz = np.zeros(mesh.n_cells) + + Rx[ind_ellipsoid] = MrX / 55000 + Ry[ind_ellipsoid] = MrY / 55000 + Rz[ind_ellipsoid] = MrZ / 55000 + + EsusRem = mkvc(np.array([Rx, Ry, Rz]).T) + + chimap = maps.ChiMap(mesh) + eff_sus_map = maps.EffectiveSusceptibilityMap( + nP=mesh.n_cells * 3, ambient_field_magnitude=survey_1.source_field.amplitude + ) + + wire_map = maps.Wires(("mu", mesh.n_cells), ("rem", mesh.n_cells * 3)) + mu_map = chimap * wire_map.mu + rem_map = eff_sus_map * wire_map.rem + m = np.r_[sus_model, EsusRem] + + simulation_1 = PF.magnetics.simulation.Simulation3DDifferential( + mesh=mesh, + survey=survey_1, + muMap=mu_map, + remMap=rem_map, + ) + + simulation_2 = PF.magnetics.simulation.Simulation3DDifferential( + survey=survey_2, + mesh=mesh, + muMap=mu_map, + remMap=rem_map, + ) + + simulation_all = PF.magnetics.simulation.Simulation3DDifferential( + survey=survey_all, + mesh=mesh, + muMap=mu_map, + remMap=rem_map, + ) + dpred_numeric_all = simulation_all.dpred(m) + dpred_numeric_1 = simulation_1.dpred(m) + dpred_numeric_2 = simulation_2.dpred(m) + dpred_stack = np.hstack((dpred_numeric_1, dpred_numeric_2)) + + rvec = np.random.randn(mesh.n_cells * 4) * 0.001 + + jv_all = simulation_all.Jvec(m, v=rvec) + jv_1 = simulation_1.Jvec(m, v=rvec) + jv_2 = simulation_2.Jvec(m, v=rvec) + jv_stack = np.hstack((jv_1, jv_2)) + + assert np.allclose(dpred_numeric_all, dpred_stack, atol=1e-8) + assert np.allclose(jv_all, jv_stack, atol=1e-8) + + +def test_unsupported_components(mesh): + """ + Test error when survey has unsupported components. + """ + supported_components = ["tmi", "bx", "by", "bz"] + unsupported_components = ["bxx", "byy", "bzz"] + receivers = [ + PF.magnetics.Point( + np.array([[0, 0, 0], [1, 2, 3]]), + components=components, + ) + for components in (*supported_components, *unsupported_components) + ] + inducing_field = [55000.0, 60.0, 90.0] + source = PF.magnetics.sources.UniformBackgroundField(receivers, *inducing_field) + survey = PF.magnetics.survey.Survey(source) + + msg = "Found unsupported magnetic components " + with pytest.raises(NotImplementedError, match=msg): + PF.magnetics.simulation.Simulation3DDifferential(survey=survey, mesh=mesh) diff --git a/tests/pf/test_mag_differential_jvecjtvec.py b/tests/pf/test_mag_differential_jvecjtvec.py new file mode 100644 index 0000000000..91daa643d0 --- /dev/null +++ b/tests/pf/test_mag_differential_jvecjtvec.py @@ -0,0 +1,190 @@ +from discretize.tests import check_derivative, assert_isadjoint +import numpy as np +import pytest +from simpeg import maps, utils +from discretize.utils import mkvc, refine_tree_xyz +import discretize +import simpeg.potential_fields as PF + + +@pytest.fixture +def mesh(): + dhx, dhy, dhz = 400.0, 400.0, 400.0 # minimum cell width (base mesh cell width) + nbcx = 512 # number of base mesh cells in x + nbcy = 512 + nbcz = 512 + + # Define base mesh (domain and finest discretization) + hx = dhx * np.ones(nbcx) + hy = dhy * np.ones(nbcy) + hz = dhz * np.ones(nbcz) + _mesh = discretize.TreeMesh([hx, hy, hz], x0="CCC") + + xp, yp, zp = np.meshgrid([-1400.0, 1400.0], [-1400.0, 1400.0], [-1000.0, 200.0]) + xy = np.c_[mkvc(xp), mkvc(yp), mkvc(zp)] + _mesh = refine_tree_xyz( + _mesh, + xy, + method="box", + finalize=False, + octree_levels=[1, 1, 1, 1], + ) + _mesh.finalize() + return _mesh + + +@pytest.fixture +def survey(): + ccx = np.linspace(-1400, 1400, num=57) + ccy = np.copy(ccx) + + ccx, ccy = np.meshgrid(ccx, ccy) + + ccz = 50.0 * np.ones_like(ccx) + + components = ["bx", "by", "bz", "tmi"] + rxLoc = PF.magnetics.receivers.Point( + np.c_[utils.mkvc(ccy.T), utils.mkvc(ccx.T), utils.mkvc(ccz.T)], + components=components, + ) + inducing_field = [55000.0, 60.0, 90.0] + srcField = PF.magnetics.sources.UniformBackgroundField( + [rxLoc], inducing_field[0], inducing_field[1], inducing_field[2] + ) + _survey = PF.magnetics.survey.Survey(srcField) + + return _survey + + +@pytest.mark.parametrize( + "deriv_type", ("mu", "rem", "mu_fix_rem", "rem_fix_mu", "both") +) +def test_derivative(deriv_type, mesh, survey): + np.random.seed(40) + + chimap = maps.ChiMap(mesh) + eff_sus_map = maps.EffectiveSusceptibilityMap( + ambient_field_magnitude=survey.source_field.amplitude, nP=mesh.n_cells * 3 + ) + + sus_model = np.abs(np.random.randn(mesh.n_cells)) + mu_model = chimap * sus_model + + Rx = np.random.randn(mesh.n_cells) + Ry = np.random.randn(mesh.n_cells) + Rz = np.random.randn(mesh.n_cells) + EsusRem = mkvc(np.array([Rx, Ry, Rz]).T) + + u0_Mr_model = eff_sus_map * EsusRem + + if deriv_type == "mu": + mu_map = chimap + mu = None + rem_map = None + rem = None + m = sus_model + if deriv_type == "rem": + mu_map = None + mu = None + rem_map = eff_sus_map + rem = None + m = EsusRem + if deriv_type == "mu_fix_rem": + mu_map = chimap + mu = None + rem_map = None + rem = u0_Mr_model + m = sus_model + if deriv_type == "rem_fix_mu": + mu_map = None + mu = mu_model + rem_map = eff_sus_map + rem = None + m = EsusRem + if deriv_type == "both": + wire_map = maps.Wires(("mu", mesh.n_cells), ("rem", mesh.n_cells * 3)) + mu_map = chimap * wire_map.mu + rem_map = eff_sus_map * wire_map.rem + m = np.r_[sus_model, EsusRem] + mu = None + rem = None + + simulation = PF.magnetics.simulation.Simulation3DDifferential( + survey=survey, mesh=mesh, mu=mu, rem=rem, muMap=mu_map, remMap=rem_map + ) + + def sim_func(m): + d = simulation.dpred(m) + + def J(v): + return simulation.Jvec(m, v) + + return d, J + + assert check_derivative(sim_func, m, plotIt=False, num=6, eps=1e-8, random_seed=40) + + +@pytest.mark.parametrize( + "deriv_type", ("mu", "rem", "mu_fix_rem", "rem_fix_mu", "both") +) +def test_adjoint(deriv_type, mesh, survey): + np.random.seed(40) + + chimap = maps.ChiMap(mesh) + eff_sus_map = maps.EffectiveSusceptibilityMap( + ambient_field_magnitude=survey.source_field.amplitude, nP=mesh.n_cells * 3 + ) + + sus_model = np.abs(np.random.randn(mesh.n_cells)) + mu_model = chimap * sus_model + + Rx = np.random.randn(mesh.n_cells) + Ry = np.random.randn(mesh.n_cells) + Rz = np.random.randn(mesh.n_cells) + EsusRem = mkvc(np.array([Rx, Ry, Rz]).T) + + u0_Mr_model = eff_sus_map * EsusRem + + if deriv_type == "mu": + mu_map = chimap + mu = None + rem_map = None + rem = None + m = sus_model + if deriv_type == "rem": + mu_map = None + mu = None + rem_map = eff_sus_map + rem = None + m = EsusRem + if deriv_type == "mu_fix_rem": + mu_map = chimap + mu = None + rem_map = None + rem = u0_Mr_model + m = sus_model + if deriv_type == "rem_fix_mu": + mu_map = None + mu = mu_model + rem_map = eff_sus_map + rem = None + m = EsusRem + if deriv_type == "both": + wire_map = maps.Wires(("mu", mesh.n_cells), ("rem", mesh.n_cells * 3)) + mu_map = chimap * wire_map.mu + rem_map = eff_sus_map * wire_map.rem + m = np.r_[sus_model, EsusRem] + mu = None + rem = None + + simulation = PF.magnetics.simulation.Simulation3DDifferential( + survey=survey, mesh=mesh, mu=mu, rem=rem, muMap=mu_map, remMap=rem_map + ) + + def J(v): + return simulation.Jvec(m, v) + + def JT(v): + return simulation.Jtvec(m, v) + + assert_isadjoint(J, JT, len(m), survey.nD, random_seed=40) diff --git a/tests/pf/test_mag_inversion_linear.py b/tests/pf/test_mag_inversion_linear.py index 2e766c7478..c5cb052ecc 100644 --- a/tests/pf/test_mag_inversion_linear.py +++ b/tests/pf/test_mag_inversion_linear.py @@ -1,6 +1,8 @@ import unittest import discretize from discretize.utils import active_from_xyz +import pytest +import matplotlib.pyplot as plt from simpeg import ( utils, maps, @@ -20,8 +22,6 @@ class MagInvLinProblemTest(unittest.TestCase): def setUp(self): - np.random.seed(0) - # Define the inducing field parameter h0_amplitude, h0_inclination, h0_declination = (50000, 90, 0) @@ -85,7 +85,7 @@ def setUp(self): self.mesh, survey=survey, chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="disk", n_processes=None, ) @@ -95,7 +95,7 @@ def setUp(self): data = sim.make_synthetic_data( self.model, relative_error=0.0, - noise_floor=1.0, + noise_floor=2.0, add_noise=True, random_seed=2, ) @@ -110,18 +110,24 @@ def setUp(self): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=100, lower=0.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 + maxIter=100, lower=0.0, upper=1.0, maxIterLS=20, cg_maxiter=10, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) betaest = directives.BetaEstimate_ByEig() # Here is where the norms are applied - IRLS = directives.Update_IRLS(f_min_change=1e-4, minGNiter=1) + IRLS = directives.UpdateIRLS(f_min_change=1e-4) update_Jacobi = directives.UpdatePreconditioner() sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) self.inv = inversion.BaseInversion( - invProb, directiveList=[IRLS, sensitivity_weights, betaest, update_Jacobi] + invProb, + directiveList=[ + IRLS, + sensitivity_weights, + betaest, + update_Jacobi, + ], ) def test_mag_inverse(self): @@ -129,19 +135,30 @@ def test_mag_inverse(self): mrec = self.inv.run(self.model) residual = np.linalg.norm(mrec - self.model) / np.linalg.norm(self.model) - # plt.figure() - # ax = plt.subplot(1, 2, 1) - # midx = int(self.mesh.shape_cells[0]/2) - # self.mesh.plot_slice(self.actvMap*mrec, ax=ax, normal='Y', ind=midx, - # grid=True, clim=(0, 0.02)) + self.assertTrue(residual < 0.05) - # ax = plt.subplot(1, 2, 2) - # midx = int(self.mesh.shape_cells[0]/2) - # self.mesh.plot_slice(self.actvMap*self.model, ax=ax, normal='Y', ind=midx, - # grid=True, clim=(0, 0.02)) - # plt.show() + @pytest.mark.skip(reason="For validation only.") + def test_plot_results(self): + self.sim.store_sensitivities = "ram" + mrec = self.inv.run(self.model) + plt.figure() + ax = plt.subplot(1, 2, 1) + midx = int(self.mesh.shape_cells[0] / 2) + self.mesh.plot_slice( + self.actvMap * mrec, ax=ax, normal="Y", ind=midx, grid=True, clim=(0, 0.02) + ) - self.assertTrue(residual < 0.05) + ax = plt.subplot(1, 2, 2) + midx = int(self.mesh.shape_cells[0] / 2) + self.mesh.plot_slice( + self.actvMap * self.model, + ax=ax, + normal="Y", + ind=midx, + grid=True, + clim=(0, 0.02), + ) + plt.show() def tearDown(self): # Clean up the working directory diff --git a/tests/pf/test_mag_inversion_linear_Octree.py b/tests/pf/test_mag_inversion_linear_Octree.py index a78e714e2e..46e483c8d8 100644 --- a/tests/pf/test_mag_inversion_linear_Octree.py +++ b/tests/pf/test_mag_inversion_linear_Octree.py @@ -1,7 +1,8 @@ import shutil import unittest import numpy as np - +import pytest +import matplotlib.pyplot as plt from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz from simpeg import ( directives, @@ -18,8 +19,6 @@ class MagInvLinProblemTest(unittest.TestCase): def setUp(self): - np.random.seed(0) - # First we need to define the direction of the inducing field # As a simple case, we pick a vertical inducing field of magnitude # 50,000nT. @@ -105,13 +104,17 @@ def setUp(self): self.mesh, survey=survey, chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="ram", n_processes=None, ) self.sim = sim data = sim.make_synthetic_data( - self.model, relative_error=0.0, noise_floor=1.0, add_noise=True + self.model, + relative_error=0.0, + noise_floor=1.0, + add_noise=True, + random_seed=0, ) # Create a regularization @@ -128,13 +131,13 @@ def setUp(self): lower=0.0, upper=10.0, maxIterLS=5, - maxIterCG=20, - tolCG=1e-4, - stepOffBoundsFact=1e-4, + cg_maxiter=20, + cg_rtol=1e-3, + active_set_grad_scale=1e-4, ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e6) - IRLS = directives.Update_IRLS() + IRLS = directives.UpdateIRLS() update_Jacobi = directives.UpdatePreconditioner() sensitivity_weights = directives.UpdateSensitivityWeights() self.inv = inversion.BaseInversion( @@ -145,16 +148,19 @@ def test_mag_inverse(self): # Run the inversion mrec = self.inv.run(self.model * 1e-4) residual = np.linalg.norm(mrec - self.model) / np.linalg.norm(self.model) + self.assertLess(residual, 0.5) - # import matplotlib.pyplot as plt - # plt.figure() - # ax = plt.subplot(1, 2, 1) - # self.mesh.plot_slice(self.actvMap*mrec, ax=ax, normal="Y", grid=True) - # ax = plt.subplot(1, 2, 2) - # self.mesh.plot_slice(self.actvMap*self.model, ax=ax, normal="Y", grid=True) - # plt.show() + @pytest.mark.skip(reason="For validation only.") + def test_plot_results(self): + self.sim.store_sensitivities = "ram" + mrec = self.inv.run(self.model * 1e-4) - self.assertLess(residual, 0.5) + plt.figure() + ax = plt.subplot(1, 2, 1) + self.mesh.plot_slice(self.actvMap * mrec, ax=ax, normal="Y", grid=True) + ax = plt.subplot(1, 2, 2) + self.mesh.plot_slice(self.actvMap * self.model, ax=ax, normal="Y", grid=True) + plt.show() def tearDown(self): # Clean up the working directory diff --git a/tests/pf/test_mag_nonLinear_Amplitude.py b/tests/pf/test_mag_nonLinear_Amplitude.py index d4f11f6ce8..80033d516b 100644 --- a/tests/pf/test_mag_nonLinear_Amplitude.py +++ b/tests/pf/test_mag_nonLinear_Amplitude.py @@ -1,4 +1,6 @@ import numpy as np +import pytest +import matplotlib.pyplot as plt from simpeg import ( data, data_misfit, @@ -82,7 +84,7 @@ def setUp(self): np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, - )[0] + ) # Assign magnetization value, inducing field strength will # be applied in by the :class:`simpeg.PF.Magnetics` problem @@ -100,7 +102,7 @@ def setUp(self): survey=survey, mesh=mesh, chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="forward_only", ) simulation.M = M_xyz @@ -134,7 +136,7 @@ def setUp(self): mesh=mesh, survey=survey, chiMap=idenMap, - ind_active=surf, + active_cells=surf, store_sensitivities="ram", ) simulation.model = mstart @@ -151,8 +153,8 @@ def setUp(self): lower=-np.inf, upper=np.inf, maxIterLS=5, - maxIterCG=5, - tolCG=1e-3, + cg_maxiter=5, + cg_rtol=1e-3, ) # Define misfit function (obs-calc) @@ -166,9 +168,10 @@ def setUp(self): # Target misfit to stop the inversion, # try to fit as much as possible of the signal, we don't want to lose anything - IRLS = directives.Update_IRLS( - f_min_change=1e-3, minGNiter=1, beta_tol=1e-1, max_irls_iterations=5 + IRLS = directives.UpdateIRLS( + f_min_change=1e-3, misfit_tolerance=1e-1, max_irls_iterations=5 ) + update_Jacobi = directives.UpdatePreconditioner() # Put all the parts together inv = inversion.BaseInversion( @@ -200,7 +203,7 @@ def setUp(self): mesh=mesh, survey=surveyAmp, chiMap=idenMap, - ind_active=surf, + active_cells=surf, is_amplitude_data=True, store_sensitivities="forward_only", ) @@ -227,7 +230,7 @@ def setUp(self): survey=surveyAmp, mesh=mesh, chiMap=idenMap, - ind_active=actv, + active_cells=actv, is_amplitude_data=True, ) @@ -243,7 +246,7 @@ def setUp(self): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=10, lower=0.0, upper=1.0, maxIterLS=5, maxIterCG=5, tolCG=1e-3 + maxIter=10, lower=0.0, upper=1.0, maxIterLS=5, cg_maxiter=5, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) @@ -252,14 +255,10 @@ def setUp(self): betaest = directives.BetaEstimate_ByEig(beta0_ratio=1) # Specify the sparse norms - IRLS = directives.Update_IRLS( + IRLS = directives.UpdateIRLS( max_irls_iterations=5, f_min_change=1e-3, - minGNiter=1, - coolingRate=1, - beta_search=False, ) - # Special directive specific to the mag amplitude problem. The sensitivity # weights are update between each iteration. update_SensWeight = directives.UpdateSensitivityWeights() @@ -267,7 +266,13 @@ def setUp(self): # Put all together self.inv = inversion.BaseInversion( - invProb, directiveList=[update_SensWeight, betaest, IRLS, update_Jacobi] + invProb, + directiveList=[ + update_SensWeight, + betaest, + IRLS, + update_Jacobi, + ], ) self.mstart = mstart @@ -279,37 +284,47 @@ def test_mag_inverse(self): mrec_Amp = self.inv.run(self.mstart) residual = np.linalg.norm(mrec_Amp - self.model) / np.linalg.norm(self.model) - # print(residual) - # import matplotlib.pyplot as plt - - # # Plot the amplitude model - # plt.figure() - # ax = plt.subplot(2, 1, 1) - # im = self.mesh.plot_slice(self.actvPlot*self.model, - # ax=ax, normal='Y', ind=66, - # pcolor_opts={"vmin":0., "vmax":0.01} - # ) - # plt.colorbar(im[0]) - # ax.set_xlim([-200, 200]) - # ax.set_ylim([-100, 75]) - # ax.set_xlabel('x') - # ax.set_ylabel('y') - # plt.gca().set_aspect('equal', adjustable='box') - - # ax = plt.subplot(2, 1, 2) - # im = self.mesh.plot_slice(self.actvPlot*mrec_Amp, ax=ax, normal='Y', ind=66, - # pcolor_opts={"vmin":0., "vmax":0.01} - # ) - # plt.colorbar(im[0]) - # ax.set_xlim([-200, 200]) - # ax.set_ylim([-100, 75]) - # ax.set_xlabel('x') - # ax.set_ylabel('y') - # plt.gca().set_aspect('equal', adjustable='box') - - # plt.show() self.assertTrue(residual < 1.0) + @pytest.mark.skip(reason="For validation only.") + def test_plot_results(self): + self.sim.store_sensitivities = "ram" + mrec = self.inv.run(self.model) + + # Plot the amplitude model + plt.figure() + ax = plt.subplot(2, 1, 1) + im = self.mesh.plot_slice( + self.actvPlot * self.model, + ax=ax, + normal="Y", + ind=66, + pcolor_opts={"vmin": 0.0, "vmax": 0.01}, + ) + plt.colorbar(im[0]) + ax.set_xlim([-200, 200]) + ax.set_ylim([-100, 75]) + ax.set_xlabel("x") + ax.set_ylabel("y") + plt.gca().set_aspect("equal", adjustable="box") + + ax = plt.subplot(2, 1, 2) + im = self.mesh.plot_slice( + self.actvPlot * mrec, + ax=ax, + normal="Y", + ind=66, + pcolor_opts={"vmin": 0.0, "vmax": 0.01}, + ) + plt.colorbar(im[0]) + ax.set_xlim([-200, 200]) + ax.set_ylim([-100, 75]) + ax.set_xlabel("x") + ax.set_ylabel("y") + plt.gca().set_aspect("equal", adjustable="box") + + plt.show() + def tearDown(self): # Clean up the working directory if self.sim.store_sensitivities == "disk": diff --git a/tests/pf/test_mag_uniform_background_field.py b/tests/pf/test_mag_uniform_background_field.py index 785be3355e..3f8164aa1c 100644 --- a/tests/pf/test_mag_uniform_background_field.py +++ b/tests/pf/test_mag_uniform_background_field.py @@ -3,7 +3,8 @@ """ import pytest -from simpeg.potential_fields.magnetics import UniformBackgroundField, SourceField +import numpy as np +from simpeg.potential_fields.magnetics import UniformBackgroundField, Point def test_invalid_parameters_argument(): @@ -11,19 +12,49 @@ def test_invalid_parameters_argument(): Test if error is raised after passing 'parameters' as argument """ parameters = (1, 35, 60) - msg = ( - "'parameters' property has been removed." - "Please pass the amplitude, inclination and declination" - " through their own arguments." - ) + msg = r"__init__\(\) got an unexpected keyword argument 'parameters'" with pytest.raises(TypeError, match=msg): UniformBackgroundField(parameters=parameters) -def test_deprecated_source_field(): +@pytest.mark.parametrize("receiver_as_list", (True, False)) +def test_invalid_receiver_type(receiver_as_list): """ - Test if instantiating a magnetics.source.SourceField object raises an error + Test if error is raised after passing invalid type of receivers """ - msg = "SourceField has been removed, please use UniformBackgroundField." - with pytest.raises(NotImplementedError, match=msg): - SourceField() + receiver_invalid = np.array([[1.0, 1.0, 1.0]]) + if receiver_as_list: + receiver_valid = Point(locations=np.array([[0.0, 0.0, 0.0]]), components="tmi") + receiver_list = [receiver_valid, receiver_invalid] + else: + receiver_list = receiver_invalid + msg = f"'receiver_list' must be a list of {Point}" + with pytest.raises(TypeError, match=msg): + UniformBackgroundField( + receiver_list=receiver_list, + amplitude=55_000, + inclination=45, + declination=30, + ) + + +@pytest.mark.parametrize( + "receiver_list", + (None, [Point(locations=np.array([[0.0, 0.0, 0.0]]), components="tmi")]), + ids=("None", "Point"), +) +def test_value_b0(receiver_list): + """ + Test UniformBackgroundField.b0 value + """ + amplitude = 55_000 + inclination = 45 + declination = 10 + expected_b0 = (6753.3292182935065, 38300.03321760104, -38890.87296526011) + uniform_background_field = UniformBackgroundField( + receiver_list=receiver_list, + amplitude=amplitude, + inclination=inclination, + declination=declination, + ) + np.testing.assert_allclose(uniform_background_field.b0, expected_b0) diff --git a/tests/pf/test_mag_vector_amplitude.py b/tests/pf/test_mag_vector_amplitude.py index 65cdde4ddc..87ca2adf7c 100644 --- a/tests/pf/test_mag_vector_amplitude.py +++ b/tests/pf/test_mag_vector_amplitude.py @@ -1,4 +1,6 @@ import unittest +import pytest +import matplotlib.pyplot as plt from simpeg import ( directives, maps, @@ -19,7 +21,6 @@ class MVIProblemTest(unittest.TestCase): def setUp(self): - np.random.seed(0) h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # The magnetization is set along a different @@ -74,15 +75,15 @@ def setUp(self): # Convert the inclination declination to vector in Cartesian M_xyz = utils.mat_utils.dip_azimuth2cartesian(M[0], M[1]) - # Get the indicies of the magnetized block - ind = utils.model_builder.get_indices_block( + # Get the indices of the magnetized block + indices = utils.model_builder.get_indices_block( np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, - )[0] + ) # Assign magnetization values - model[ind, :] = np.kron(np.ones((ind.shape[0], 1)), M_xyz * 0.05) + model[indices, :] = np.kron(np.ones((indices.size, 1)), M_xyz * 0.05) # Remove air cells self.model = model[actv, :] @@ -99,14 +100,18 @@ def setUp(self): survey=survey, model_type="vector", chiMap=idenMap, - ind_active=actv, + active_cells=actv, store_sensitivities="disk", ) self.sim = sim # Compute some data and add some random noise data = sim.make_synthetic_data( - utils.mkvc(self.model), relative_error=0.0, noise_floor=5.0, add_noise=True + utils.mkvc(self.model), + relative_error=0.0, + noise_floor=5.0, + add_noise=True, + random_seed=0, ) reg = regularization.VectorAmplitude( @@ -124,7 +129,7 @@ def setUp(self): # Add directives to the inversion opt = optimization.ProjectedGNCG( - maxIter=10, lower=-10, upper=10.0, maxIterLS=5, maxIterCG=5, tolCG=1e-4 + maxIter=10, lower=-10, upper=10.0, maxIterLS=5, cg_maxiter=5, cg_rtol=1e-3 ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) @@ -135,8 +140,8 @@ def setUp(self): # Here is where the norms are applied # Use pick a treshold parameter empirically based on the distribution of # model parameters - IRLS = directives.Update_IRLS( - f_min_change=1e-3, max_irls_iterations=10, beta_tol=5e-1 + IRLS = directives.UpdateIRLS( + f_min_change=1e-3, max_irls_iterations=10, misfit_tolerance=5e-1 ) # Pre-conditioner @@ -156,28 +161,30 @@ def test_vector_amplitude_inverse(self): nC = int(mrec.shape[0] / 3) vec_xyz = mrec.reshape((nC, 3), order="F") residual = np.linalg.norm(vec_xyz - self.model) / np.linalg.norm(self.model) + self.assertLess(residual, 1) - # import matplotlib.pyplot as plt - # ax = plt.subplot() - # self.mesh.plot_slice( - # self.actvMap * mrec.reshape((-1, 3), order="F"), - # v_type="CCv", - # view="vec", - # ax=ax, - # normal="Y", - # grid=True, - # quiver_opts={ - # "pivot": "mid", - # "scale": 8 * np.abs(mrec).max(), - # "scale_units": "inches", - # }, - # ) - # plt.gca().set_aspect("equal", adjustable="box") - # - # plt.show() + @pytest.mark.skip(reason="For validation only.") + def test_plot_results(self): + self.sim.store_sensitivities = "ram" + mrec = self.inv.run(self.mstart) - self.assertLess(residual, 1) - # self.assertTrue(residual < 0.05) + ax = plt.subplot() + self.mesh.plot_slice( + self.actvMap * mrec.reshape((-1, 3), order="F"), + v_type="CCv", + view="vec", + ax=ax, + normal="Y", + grid=True, + quiver_opts={ + "pivot": "mid", + "scale": 8 * np.abs(mrec).max(), + "scale_units": "inches", + }, + ) + plt.gca().set_aspect("equal", adjustable="box") + + plt.show() def tearDown(self): # Clean up the working directory diff --git a/tests/pf/test_pf_quadtree_inversion_linear.py b/tests/pf/test_pf_quadtree_inversion_linear.py index c6c7f64d8f..ec1c71ec89 100644 --- a/tests/pf/test_pf_quadtree_inversion_linear.py +++ b/tests/pf/test_pf_quadtree_inversion_linear.py @@ -209,7 +209,7 @@ def create_gravity_sim_active(self, block_value=1.0, noise_floor=0.01): survey=grav_survey, rhoMap=self.idenMap_active, store_sensitivities="ram", - ind_active=self.active_cells, + active_cells=self.active_cells, ) # Already defined @@ -243,7 +243,7 @@ def create_magnetics_sim_active(self, block_value=1.0, noise_floor=0.01): survey=mag_survey, chiMap=self.idenMap_active, store_sensitivities="ram", - ind_active=self.active_cells, + active_cells=self.active_cells, ) # Already defined @@ -284,18 +284,17 @@ def create_inversion(self, sim, data, beta=1e3, all_active=True): lower=-1.0, upper=1.0, maxIterLS=5, - maxIterCG=20, - tolCG=1e-4, + cg_maxiter=20, + cg_rtol=1e-3, ) invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=beta) # Build directives - IRLS = directives.Update_IRLS( + IRLS = directives.UpdateIRLS( f_min_change=1e-3, max_irls_iterations=30, - beta_tol=1e-1, - beta_search=False, + misfit_tolerance=1e-1, ) sensitivity_weights = directives.UpdateSensitivityWeights() update_Jacobi = directives.UpdatePreconditioner() @@ -421,7 +420,7 @@ def create_xyz_points_flat(x_range, y_range, spacing, altitude=0.0): grav_survey = gravity.Survey(grav_srcField) self.assertRaises( - AttributeError, + ValueError, gravity.SimulationEquivalentSourceLayer, mesh3D, 0.0, @@ -458,7 +457,7 @@ def create_xyz_points_flat(x_range, y_range, spacing, altitude=0.0): -5.0 * np.ones(self.mesh.nC), survey=grav_survey, rhoMap=subset_idenMap, - ind_active=ind_active, + active_cells=ind_active, ) print("Z_TOP OR Z_BOTTOM LENGTH MATCHING NACTIVE-CELLS ERROR TEST PASSED.") diff --git a/tests/pf/test_pf_survey.py b/tests/pf/test_pf_survey.py new file mode 100644 index 0000000000..0cefa8dd88 --- /dev/null +++ b/tests/pf/test_pf_survey.py @@ -0,0 +1,91 @@ +import functools +import numpy as np +import pytest +from simpeg.potential_fields import gravity as grav +from simpeg.potential_fields import magnetics as mag +from simpeg.data import Data + + +@pytest.fixture(params=["gravity", "magnetics"]) +def survey(request): + rx_locs = np.random.rand(20, 3) + if request.param == "gravity": + rx1_components = ["gx", "gz"] + rx2_components = "gzz" + mod = grav + Source = functools.partial(grav.SourceField) + else: # request.param == "magnetics": + rx1_components = ["bx", "by"] + rx2_components = "tmi" + + mod = mag + Source = functools.partial( + mag.UniformBackgroundField, amplitude=50_000, inclination=90, declination=0 + ) + + rx1 = mod.Point(rx_locs, components=rx1_components) + rx2 = mod.Point(rx_locs, components=rx2_components) + src = Source(receiver_list=[rx1, rx2]) + return mod.Survey(src) + + +def test_survey_counts(survey): + src = survey.source_field + rx1, rx2 = src.receiver_list + + assert rx1.nD == 40 + assert rx2.nD == 20 + assert src.nD == 60 + assert survey.nRx == 40 + np.testing.assert_equal(src.vnD, [40, 20]) + assert survey.nD == 60 + np.testing.assert_equal(survey.vnD, [40, 20]) + + +def test_survey_indexing(survey): + src = survey.source_field + rx1, rx2 = src.receiver_list + d1 = -10 * np.arange(rx1.nD) + d2 = 10 + np.arange(rx2.nD) + data_vec = np.r_[d1, d2] + + data = Data(survey=survey, dobs=data_vec) + + np.testing.assert_equal(data[src, rx1], d1) + np.testing.assert_equal(data[src, rx2], d2) + + +@pytest.mark.parametrize("survey_cls", [grav.Survey, mag.Survey]) +def test_source_list_kwarg(survey_cls): + # cannot pass anything to source list for these classes. + with pytest.raises(TypeError, match=r"source_list is not a valid argument to .*"): + survey_cls("placeholder", source_list=None) + + +@pytest.mark.parametrize( + "survey_cls, source_cls", + [ + (grav.Survey, grav.SourceField), + ( + mag.Survey, + functools.partial( + mag.UniformBackgroundField, + amplitude=50_000, + inclination=90, + declination=0, + ), + ), + ], +) +def test_setting_sourcefield(survey_cls, source_cls): + src1 = source_cls(receiver_list=[]) + survey = survey_cls(source_field=src1) + assert survey.source_field is src1 + assert survey.source_list[0] is src1 + + src2 = source_cls(receiver_list=[]) + survey.source_field = src2 + assert survey.source_field is not src1 + assert survey.source_field is src2 + assert survey.source_list[0] is not src1 + assert survey.source_list[0] is src2 diff --git a/tests/pf/test_sensitivity_PFproblem.py b/tests/pf/test_sensitivity_PFproblem.py index eb39c97485..c7f00c03c7 100644 --- a/tests/pf/test_sensitivity_PFproblem.py +++ b/tests/pf/test_sensitivity_PFproblem.py @@ -4,7 +4,6 @@ # #from simpegPF import BaseMag # #import matplotlib.pyplot as plt # import discretize -# from pymatsolver import Pardiso # #import simpeg.PF as PF # from simpeg import maps, utils # from simpeg.potential_fields import magnetics as mag @@ -49,7 +48,6 @@ # M, # survey=self.survey, # muMap=maps.ChiMap(M), -# solver=Pardiso, # ) # dpre = self.sim.dpred(chi) # @@ -79,7 +77,7 @@ # # d_mu = mu*0.8 # derChk = lambda m: [MfmuI(m), lambda mx: dMfmuI(self.chi, mx)] -# passed = Tests.check_derivative(derChk, mu, num=4, dx = d_mu, plotIt=False) +# passed = Tests.check_derivative(derChk, mu, num=4, dx = d_mu, plotIt=False, random_seed=0) # # self.assertTrue(passed) # @@ -121,7 +119,7 @@ # # d_chi = self.chi*0.8 # derChk = lambda m: [Cm_A(m), lambda mx: dCdm_A(self.chi, mx)] -# passed = Tests.check_derivative(derChk, self.chi, num=4, dx = d_chi, plotIt=False) +# passed = Tests.check_derivative(derChk, self.chi, num=4, dx = d_chi, plotIt=False, random_seed=0) # self.assertTrue(passed) # # @@ -169,7 +167,7 @@ # # d_chi = self.chi*0.8 # derChk = lambda m: [Cm_RHS(m), lambda mx: dCdm_RHS(self.chi, mx)] -# passed = Tests.check_derivative(derChk, self.chi, num=4, dx = d_chi, plotIt=False) +# passed = Tests.check_derivative(derChk, self.chi, num=4, dx = d_chi, plotIt=False, random_seed=0) # self.assertTrue(passed) # # @@ -218,7 +216,7 @@ # # # derChk = lambda m: [ufun(m), lambda mx: dudm(self.chi, mx)] # # # TODO: I am not sure why the order get worse as step decreases .. --; -# # passed = Tests.check_derivative(derChk, self.chi, num=2, dx = d_chi, plotIt=False) +# # passed = Tests.check_derivative(derChk, self.chi, num=2, dx = d_chi, plotIt=False, random_seed=0) # # self.assertTrue(passed) # # @@ -270,7 +268,7 @@ # # # derChk = lambda m: [Bfun(m), lambda mx: dBdm(self.chi, mx)] # # # TODO: I am not sure why the order get worse as step decreases .. --; -# # passed = Tests.check_derivative(derChk, self.chi, num=2, dx = d_chi, plotIt=False) +# # passed = Tests.check_derivative(derChk, self.chi, num=2, dx = d_chi, plotIt=False, random_seed=0) # # self.assertTrue(passed) # # @@ -284,7 +282,7 @@ # # derChk = lambda m: (self.survey.dpred(m), lambda v: self.prob.Jvec(m, v)) # # TODO: I am not sure why the order get worse as step decreases .. --; -# passed = Tests.check_derivative(derChk, self.chi, num=2, dx = d_chi, plotIt=False) +# passed = Tests.check_derivative(derChk, self.chi, num=2, dx = d_chi, plotIt=False, random_seed=0) # self.assertTrue(passed) # # def test_Jtvec(self): @@ -300,7 +298,7 @@ # return misfit, dmisfit # # # TODO: I am not sure why the order get worse as step decreases .. --; -# passed = Tests.check_derivative(misfit, self.chi, num=4, plotIt=False) +# passed = Tests.check_derivative(misfit, self.chi, num=4, plotIt=False, random_seed=0) # self.assertTrue(passed) # # if __name__ == '__main__': diff --git a/tests/pf/test_survey_counting.py b/tests/pf/test_survey_counting.py deleted file mode 100644 index 84aa334561..0000000000 --- a/tests/pf/test_survey_counting.py +++ /dev/null @@ -1,39 +0,0 @@ -import numpy as np -from simpeg.potential_fields import gravity as grav -from simpeg.potential_fields import magnetics as mag - - -def test_gravity_survey(): - rx_locs = np.random.rand(20, 3) - rx_components = ["gx", "gz"] - - rx1 = grav.Point(rx_locs, components=rx_components) - rx2 = grav.Point(rx_locs, components="gzz") - src = grav.SourceField([rx1, rx2]) - survey = grav.Survey(src) - - assert rx1.nD == 40 - assert rx2.nD == 20 - assert src.nD == 60 - assert survey.nRx == 40 - np.testing.assert_equal(src.vnD, [40, 20]) - assert survey.nD == 60 - np.testing.assert_equal(survey.vnD, [40, 20]) - - -def test_magnetics_survey(): - rx_locs = np.random.rand(20, 3) - rx_components = ["bx", "by", "bz"] - - rx1 = mag.Point(rx_locs, components=rx_components) - rx2 = mag.Point(rx_locs, components="tmi") - src = mag.UniformBackgroundField([rx1, rx2]) - survey = mag.Survey(src) - - assert rx1.nD == 60 - assert rx2.nD == 20 - assert src.nD == 80 - np.testing.assert_equal(src.vnD, [60, 20]) - assert survey.nRx == 40 - assert survey.nD == 80 - np.testing.assert_equal(survey.vnD, [60, 20]) diff --git a/tests/seis/test_tomo.py b/tests/seis/test_tomo.py index e7125d70fa..d9aca46768 100644 --- a/tests/seis/test_tomo.py +++ b/tests/seis/test_tomo.py @@ -1,9 +1,14 @@ +import re + import numpy as np import unittest import discretize +import pytest + from simpeg.seismic import straight_ray_tomography as tomo from simpeg import tests, maps, utils +from simpeg.seismic.straight_ray_tomography.simulation import Simulation2DIntegral TOL = 1e-5 FLR = 1e-14 @@ -35,7 +40,29 @@ def test_deriv(self): def fun(x): return self.problem.dpred(x), lambda x: self.problem.Jvec(s, x) - return tests.check_derivative(fun, s, num=4, plotIt=False, eps=FLR) + return tests.check_derivative( + fun, s, num=4, plotIt=False, eps=FLR, random_seed=664 + ) + + +def test_required_mesh_arg(): + msg = ".*missing 1 required positional argument: 'mesh'" + with pytest.raises(TypeError, match=msg): + Simulation2DIntegral() + + +def test_bad_mesh_type(): + mesh = discretize.CylindricalMesh([3, 3, 3]) + msg = "mesh must be an instance of TensorMesh, not CylindricalMesh" + with pytest.raises(TypeError, match=msg): + Simulation2DIntegral(mesh) + + +def test_bad_mesh_dim(): + mesh = discretize.TensorMesh([3, 3, 3]) + msg = re.escape("Simulation2DIntegral mesh must be 2D, received a 3D mesh.") + with pytest.raises(ValueError, match=msg): + Simulation2DIntegral(mesh) if __name__ == "__main__": diff --git a/tests/utils/ellipsoid.py b/tests/utils/ellipsoid.py new file mode 100644 index 0000000000..7b97dfb210 --- /dev/null +++ b/tests/utils/ellipsoid.py @@ -0,0 +1,553 @@ +from simpeg import utils +import numpy as np + +""" +The code for forward modelling magnetic field of ellipsoids present in this +file is based on the implementation made by Diego Takahashi Tomazella and +Vanderlei C. Oliveira Jr., which is available in +https://github.com/pinga-lab/magnetic-ellipsoid, and has been released under +the BSD 3-Clause license: + +> Copyright (c) Diego Takahashi Tomazella and Vanderlei C. Oliveira Jr. +> All rights reserved. +> +> Redistribution and use in source and binary forms, with or without +> modification, are permitted provided that the following conditions are met: +> +> * Redistributions of source code must retain the above copyright notice, this +> list of conditions and the following disclaimer. +> * Redistributions in binary form must reproduce the above copyright notice, +> this list of conditions and the following disclaimer in the documentation +> and/or other materials provided with the distribution. +> * Neither the names of the copyright holders nor the names of any +> contributors may be used to endorse or promote products derived from this +> software without specific prior written permission. +> +> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +> AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +> ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +> LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +> CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +> SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +> INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +> CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +> ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +> POSSIBILITY OF SUCH DAMAGE. +""" + + +class ProlateEllipsoid: + r"""Class for magnetostatic solution for a permeable and remanently + magnetized prolate ellipsoid in a uniform magnetostatic field + based on: https://github.com/pinga-lab/magnetic-ellipsoid + + The ``ProlateEllipse`` class is used to analytically compute the external and internal + secondary magnetic flux density + + Parameters + ---------- + center : (3) array_like, optional + center of ellipsoid (m). + axis : (2) array_like, optional + major and both minor axes of ellipsoid (m). + strike_dip_rake : (3) array_like, optional + strike, dip, and rake of ellipsoid, defined in paper (degrees) + Sets property V (rotation matrix) + susceptibility : float + susceptibility of ellipsoid (SI). + Mr : (3) array_like, optional + Intrinsic remanent magnetic polarization (\mu_0 M) of ellipsoid. + If susceptibility = 0,equivalent to total resultant magnetization. (nT) + inducing_field : (3) array_like, optional + Ambient Geomagnetic Field. (strength(nT),inclination (degrees), declination (degrees) + """ + + def __init__( + self, + center=(0, 0, 0), + axes=(100.1, 100), + strike_dip_rake=(0, 0, 0), + susceptibility=0.0, + Mr=(0, 0, 0), + inducing_field=(50000, 0, 90), + **kwargs, + ): + self.center = self.__redefine_coords(center) + self.axes = axes + self.susceptibility = susceptibility + self.V = strike_dip_rake + Mr = np.array(Mr) + self.Mr = Mr.T + self.B_0 = inducing_field + + @property + def center(self): + """Center of the sphere + + Returns + ------- + (3) numpy.ndarray of float + Center of the sphere. Default = np.r_[0,0,0] + """ + return self._center + + @center.setter + def center(self, vec): + + try: + vec = np.atleast_1d(vec).astype(float) + except (TypeError, AttributeError, ValueError): + raise TypeError(f"location must be array_like, got {type(vec)}") + + if len(vec) != 3: + raise ValueError( + f"location must be array_like with shape (3,), got {len(vec)}" + ) + + self._center = vec + + @property + def axes(self): + """The major axis and shared minor axes of the prolate ellipsoid + + Returns + ------- + (2) numpy.ndarray of float + Center of the sphere. Default = np.r_[100.1,100] + """ + return self._axes + + @axes.setter + def axes(self, vec): + + try: + vec = np.atleast_1d(vec).astype(float) + except (TypeError, AttributeError, ValueError): + raise TypeError(f"location must be array_like, got {type(vec)}") + + if len(vec) != 2: + raise ValueError( + "location must be array_like with shape (2,), got {len(vec)}" + ) + + if vec[0] <= vec[1]: + raise ValueError( + "The major axis of the ellipsoid must be greater then the minor axes" + ) + + if np.any(np.less(vec, 0)): + raise ValueError("The axes must be positive") + axes = np.zeros(3) + axes[:2] = vec + axes[2] = vec[1] + self._axes = axes + + @property + def V(self): + """Rotation Matrix of Ellipsoid + + Returns + ------- + (3,3) numpy.ndarray of float + Rotation Matrix of Ellipsoid + """ + return self._V + + @V.setter + def V(self, vec): + + try: + vec = np.atleast_1d(vec).astype(float) + except (TypeError, AttributeError, ValueError): + raise TypeError(f"strike_dip_rake must be array_like, got {type(vec)}") + + if len(vec) != 3: + raise ValueError( + f"strike_dip_rake must be array_like with shape (3,), got {len(vec)}" + ) + + self._V = self.__rotation_matrix(np.radians(vec)) + + @property + def susceptibility(self): + """Magnetic susceptibility (SI) + + Returns + ------- + float + Magnetic Susceptibility (SI) + """ + return self._susceptibility + + @susceptibility.setter + def susceptibility(self, item): + item = float(item) + if item < 0.0: + raise ValueError("Susceptibility must be positive") + self._susceptibility = item + + @property + def Mr(self): + r"""The remanent polarization (\mu0 M), (nT) + + Returns + ------- + (3) numpy.ndarray of float + Remanent Polarization (nT) + """ + return self._Mr + + @Mr.setter + def Mr(self, vec): + + try: + vec = np.atleast_1d(vec).astype(float) + except (TypeError, AttributeError, ValueError): + raise TypeError(f"location must be array_like, got {type(vec)}") + + if len(vec) != 3: + raise ValueError( + f"location must be array_like with shape (3,), got {len(vec)}" + ) + self._Mr = self.__redefine_coords(vec) + + @property + def B_0(self): + """Amplitude of the inducing field (nT). + + Returns + ------- + (3) numpy.ndarray of float + Amplitude of the primary current density. Default = np.r_[1,0,0] + """ + return self._B_0 + + @B_0.setter + def B_0(self, vec): + + try: + vec = np.atleast_1d(vec).astype(float) + except (TypeError, AttributeError, ValueError): + raise TypeError(f"primary_field must be array_like, got {type(vec)}") + + if len(vec) != 3: + raise ValueError( + f"primary_field must be array_like with shape (3,), got {len(vec)}" + ) + + mag = utils.mat_utils.dip_azimuth2cartesian( + vec[1], + vec[2], + ) + + B_0 = np.array([mag[:, 0] * vec[0], mag[:, 1] * vec[0], mag[:, 2] * vec[0]])[ + :, 0 + ] + + B_0 = self.__redefine_coords(B_0) + + self._B_0 = B_0 + + def get_indices(self, xyz): + """Returns Boolean of provided points internal to ellipse + + Parameters + ---------- + xyz : (..., 3) numpy.ndarray + Locations to evaluate at in units m. + + Returns + ------- + ind: Boolean array, True if internal to ellipse + + """ + + V = self.V + a = self.axes[0] + b = self.axes[1] + c = self.axes[1] + A = np.identity(3) + A[0, 0] = a**-2 + A[1, 1] = b**-2 + A[2, 2] = c**-2 + A = V @ A @ V.T + center = self.center + + t1 = xyz[:, 1] - center[0] + t2 = xyz[:, 0] - center[1] + t3 = -xyz[:, 2] - center[2] + + r_m_rc = np.array([t1, t2, t3]) + b = A @ r_m_rc + + values = np.sum(r_m_rc * b, axis=0) + + ind = values < 1 + + return ind + + def Magnetization(self): + """Returns the resultant magnetization of the ellipsoid as a function + of susceptibility and remanent magnetization + + Parameters + ---------- + + Returns + ------- + M: (3) numpy.ndarray of float + + """ + + V = self.V + + K = self.susceptibility * np.identity(3) # /(4*np.pi) + + N1 = self.__depolarization_prolate() + + I = np.identity(3) + + inv = np.linalg.inv(I + K @ N1) + + M = V @ inv @ V.T @ (K @ self.B_0.T + self.Mr.T) + + M = self.__redefine_coords(M.T) + + return M + + def anomalous_bfield(self, xyz): + """Returns the internal and external secondary magnetic field B_s + + Parameters + ---------- + xyz : (..., 3) numpy.ndarray + Locations to evaluate at in units m. + + Returns + ------- + B_s : (..., 3) np.ndarray + Units of nT + + """ + a = self.axes[0] + b = self.axes[1] + axes_array = np.array([a, b, b]) + + internal_indices = self.get_indices(xyz) + xyz = self.__redefine_coords(xyz) + xyz_m_center = xyz - self.center + + body_axis_coords = (self.V.T @ xyz_m_center.T).T + + x1 = body_axis_coords[:, 0] + x2 = body_axis_coords[:, 1] + x3 = body_axis_coords[:, 2] + + xyz = [x1, x2, x3] + + M = self.__redefine_coords(self.Magnetization()) + + lam = self.__get_lam(x1, x2, x3) + + dlam = self.__d_lam(x1, x2, x3, lam) + + R = np.sqrt((a**2 + lam) * (b**2 + lam) * (b**2 + lam)) + + h = [] + for i in range(len(axes_array)): + h.append(-1 / ((axes_array[i] ** 2 + lam) * R)) + + g = self.__g(lam) + + N2 = self.__N2(h, g, dlam, xyz) + + B_s = self.V @ N2 @ self.V.T @ M + + N1 = self.__depolarization_prolate() + + M_norotate = self.Magnetization() + + B_s = self.__redefine_coords(B_s) + + B_s[internal_indices, :] = M_norotate - N1 @ M_norotate + + return B_s + + def TMI(self, xyz): + """Returns the internal and external exact TMI data + + Parameters + ---------- + xyz : (..., 3) numpy.ndarray + Locations to evaluate at in units m. + + Returns + ------- + TMI : (...,) np.ndarray + Units of nT + + """ + + B_0 = self.__redefine_coords(self.B_0) + + B = self.anomalous_bfield(xyz) + + TMI = np.linalg.norm(B_0 + B, axis=1) - np.linalg.norm(self.B_0) + + return TMI + + def TMI_approx(self, xyz): + """Returns the internal and external approximate TMI data + + Parameters + ---------- + xyz : (..., 3) numpy.ndarray + Locations to evaluate at in units m. + + Returns + ------- + TMI_approx : (...,) np.ndarray + Units of nT + + """ + + B = self.anomalous_bfield(xyz) + B0 = self.__redefine_coords(self.B_0) + + TMI_approx = (B @ B0.T) / np.linalg.norm(B0) + + return TMI_approx + + def __redefine_coords(self, coords): + coords_copy = np.copy(coords) + if len(np.shape(coords)) == 1: + + temp = np.copy(coords[0]) + coords_copy[0] = coords[1] + coords_copy[1] = temp + coords_copy[2] *= -1 + else: + temp = np.copy(coords[:, 0]) + coords_copy[:, 0] = coords[:, 1] + coords_copy[:, 1] = temp + coords_copy[:, 2] *= -1 + + return coords_copy + + def __rotation_matrix(self, strike_dip_rake): + strike = strike_dip_rake[0] + dip = strike_dip_rake[1] + rake = strike_dip_rake[2] + + def R1(theta): + return np.array( + [ + [1, 0, 0], + [0, np.cos(theta), np.sin(theta)], + [0, -np.sin(theta), np.cos(theta)], + ] + ) + + def R2(theta): + return np.array( + [ + [np.cos(theta), 0, -np.sin(theta)], + [0, 1, 0], + [np.sin(theta), 0, np.cos(theta)], + ] + ) + + def R3(theta): + return np.array( + [ + [np.cos(theta), np.sin(theta), 0], + [-np.sin(theta), np.cos(theta), 0], + [0, 0, 1], + ] + ) + + V = R1(np.pi / 2) @ R2(strike) @ R1(np.pi / 2 - dip) @ R3(rake) + + return V + + def __depolarization_prolate(self): + a = self.axes[0] + b = self.axes[1] + + m = a / b + + t11 = 1 / (m**2 - 1) + t22 = m / (m**2 - 1) ** 0.5 + t33 = np.log(m + (m**2 - 1) ** 0.5) + + n11 = t11 * (t22 * t33 - 1) + n22 = 0.5 * (1 - n11) + n33 = n22 + + N1 = np.zeros((3, 3)) + N1[0, 0] = n11 + N1[1, 1] = n22 + N1[2, 2] = n33 + + return N1 + + def __N2(self, h, g, dlam, xyz): + size = np.shape(g[0])[0] + N2 = np.zeros((size, 3, 3)) + abc_2 = self.axes[0] * self.axes[1] * self.axes[2] / 2 + for i in range(3): + for j in range(3): + if i == j: + N2[:, i, j] = -abc_2 * (dlam[i] * h[i] * xyz[i] + g[i]) + else: + N2[:, i, j] = -abc_2 * (dlam[i] * h[j] * xyz[j]) + + return N2 + + def __get_lam(self, x1, x2, x3): + a = self.axes[0] + b = self.axes[1] + p1 = a**2 + b**2 - x1**2 - x2**2 - x3**2 + p0 = a**2 * b**2 - b**2 * x1**2 - a**2 * (x2**2 + x3**2) + lam = (-p1 + np.sqrt(p1**2 - 4 * p0)) / 2 + + return lam + + def __d_lam(self, x1, x2, x3, lam): + + dlam = [] + xyz = [x1, x2, x3] + + den = ( + (x1 / (self.axes[0] ** 2 + lam)) ** 2 + + (x2 / (self.axes[1] ** 2 + lam)) ** 2 + + (x3 / (self.axes[1] ** 2 + lam)) ** 2 + ) + + for i in range(3): + num = (2 * xyz[i]) / (self.axes[i] ** 2 + lam) + dlam.append(num / den) + + return dlam + + def __g(self, lam): + a = self.axes[0] + b = self.axes[1] + a2lam = a**2 + lam + b2lam = b**2 + lam + a2mb2 = a**2 - b**2 + + gmul = 1 / (a2mb2**1.5) + g1t1 = np.log((a2mb2**0.5 + a2lam**0.5) / b2lam**0.5) + g1t2 = (a2mb2 / a2lam) ** 0.5 + + g2t2 = (a2mb2 * a2lam) ** 0.5 / b2lam + + g1 = 2 * gmul * (g1t1 - g1t2) + g2 = gmul * (g2t2 - g1t1) + g3 = g2 + + g = [g1, g2, g3] + + return g diff --git a/tests/utils/test_default_solver.py b/tests/utils/test_default_solver.py new file mode 100644 index 0000000000..4c86b47333 --- /dev/null +++ b/tests/utils/test_default_solver.py @@ -0,0 +1,50 @@ +import re +import warnings +import pytest +from pymatsolver import SolverCG + +from simpeg.utils import get_default_solver, set_default_solver + + +@pytest.fixture(autouse=True) +def reset_default_solver(): + # This should get automatically used + initial_default = get_default_solver() + yield + set_default_solver(initial_default) + + +def test_default_setting(): + set_default_solver(SolverCG) + new_default = get_default_solver() + assert new_default == SolverCG + + +def test_default_error(): + class Temp: + pass + + initial_default = get_default_solver() + + regex = re.escape("Default solver must be a subclass of pymatsolver.solvers.Base.") + with pytest.raises(TypeError, match=regex): + set_default_solver(Temp) + + after_default = get_default_solver() + + # make sure we didn't accidentally set the default. + assert initial_default is after_default + + +def test_deprecation_warning(): + """Test deprecation warning for the warn argument.""" + regex = re.escape("The `warn` argument has been deprecated and will be removed in") + with pytest.warns(FutureWarning, match=regex): + get_default_solver(warn=True) + + +def test_no_deprecation_warning(): + """Test if no deprecation warning is issued with default parameters.""" + with warnings.catch_warnings(): + warnings.simplefilter("error") # raise error if warning was raised + get_default_solver() diff --git a/tests/utils/test_gmm_utils.py b/tests/utils/test_gmm_utils.py index dbef02a7f5..f6e6c93d18 100644 --- a/tests/utils/test_gmm_utils.py +++ b/tests/utils/test_gmm_utils.py @@ -1,3 +1,4 @@ +import pytest import numpy as np import unittest import discretize @@ -277,5 +278,71 @@ def test_MAP_estimate_multi_component_multidimensions(self): ) +class MockGMMLatest(GaussianMixtureWithPrior): + """ + Mock of ``GaussianMixtureWithPrior`` with a ``_print_verbose_msg_init_end`` + method with two positional arguments (scikit-learn==1.5.0). + """ + + def _print_verbose_msg_init_end(self, ll, init_has_converged): + """Override upstream method just for test purposes.""" + return None + + +class MockGMMOlder(GaussianMixtureWithPrior): + """ + Mock of ``GaussianMixtureWithPrior`` with a ``_print_verbose_msg_init_end`` + method with a single positional argument (scikit-learn<1.5.0). + """ + + def _print_verbose_msg_init_end(self, ll): + """Override upstream method just for test purposes.""" + return None + + +class TestCustomPrintMethod: + """ + Test the ``GaussianMixtureWithPrior._print_verbose_msg_init_end`` method + with different signatures of the upstream ``_print_verbose_msg_init_end`` + private method. + """ + + @pytest.fixture + def mesh(self): + """Sample mesh""" + mesh = discretize.TensorMesh([8, 7, 6]) + return mesh + + @pytest.fixture + def model(self, mesh): + """Sample model.""" + model = np.ones(mesh.n_cells, dtype=np.float64) + return model + + @pytest.fixture + def gmmref(self, mesh, model): + """Sample GMM""" + active_cells = np.ones(mesh.n_cells, dtype=bool) + gmmref = WeightedGaussianMixture( + mesh=mesh, + actv=active_cells, + n_components=1, + covariance_type="full", + max_iter=1000, + n_init=10, + tol=1e-8, + warm_start=True, + ) + gmmref.fit(model.reshape(-1, 1)) + return gmmref + + @pytest.mark.parametrize("gmm_class", (MockGMMLatest, MockGMMOlder)) + def test_custom_print_verbose_method(self, gmmref, gmm_class): + """Test custom method against older and latest signature of the upstream one.""" + gmm = gmm_class(gmmref=gmmref) + # Run the custom private method: it should not raise any error + gmm._custom_print_verbose_msg_init_end(3) + + if __name__ == "__main__": unittest.main() diff --git a/tests/utils/test_io_utils.py b/tests/utils/test_io_utils.py index 9e9a07f206..ef6fc2d473 100644 --- a/tests/utils/test_io_utils.py +++ b/tests/utils/test_io_utils.py @@ -256,7 +256,9 @@ def setUp(self): self.std = std rx2 = magnetics.receivers.Point(xyz, components="tmi") - src_bad = magnetics.sources.UniformBackgroundField([rx, rx2]) + src_bad = magnetics.sources.UniformBackgroundField( + receiver_list=[rx, rx2], amplitude=50_000, inclination=90, declination=0 + ) survey_bad = magnetics.survey.Survey(src_bad) self.survey_bad = survey_bad @@ -398,8 +400,8 @@ def setUp(self): pp_sources.append(dc.sources.Pole(pp_receivers, a_loc)) dpdp_sources.append(dc.sources.Dipole(dpdp_receivers, a_loc, b_loc)) - self.pp_survey = dc.survey.Survey(pp_sources, survey_type="pole-pole") - self.dpdp_survey = dc.survey.Survey(dpdp_sources, survey_type="dipole-dipole") + self.pp_survey = dc.survey.Survey(pp_sources) + self.dpdp_survey = dc.survey.Survey(dpdp_sources) # Define data and uncertainties. In this case nD = 6 n_data = len(xa) * len(xm) diff --git a/tests/utils/test_mat_utils.py b/tests/utils/test_mat_utils.py index 3c8e1db1fd..f7e30402f0 100644 --- a/tests/utils/test_mat_utils.py +++ b/tests/utils/test_mat_utils.py @@ -1,7 +1,9 @@ +import pytest import unittest import numpy as np from scipy.sparse.linalg import eigsh from discretize import TensorMesh +from simpeg.objective_function import BaseObjectiveFunction from simpeg import simulation, data_misfit from simpeg.maps import IdentityMap from simpeg.regularization import WeightedLeastSquares @@ -51,6 +53,7 @@ def g(k): relative_error=relative_error, noise_floor=noise_floor, add_noise=True, + random_seed=40, ) dmis = data_misfit.L2DataMisfit(simulation=sim, data=data_obj) self.dmis = dmis @@ -79,7 +82,7 @@ def test_dm_eigenvalue_by_power_iteration(self): field = self.dmis.simulation.fields(self.true_model) max_eigenvalue_numpy, _ = eigsh(dmis_matrix, k=1) max_eigenvalue_directive = eigenvalue_by_power_iteration( - self.dmis, self.true_model, fields_list=field, n_pw_iter=30 + self.dmis, self.true_model, fields_list=field, n_pw_iter=30, random_seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) @@ -92,7 +95,7 @@ def test_dm_eigenvalue_by_power_iteration(self): dmiscombo_matrix = 2 * self.G.T.dot(WtW.dot(self.G)) max_eigenvalue_numpy, _ = eigsh(dmiscombo_matrix, k=1) max_eigenvalue_directive = eigenvalue_by_power_iteration( - self.dmiscombo, self.true_model, n_pw_iter=30 + self.dmiscombo, self.true_model, n_pw_iter=30, random_seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) @@ -102,7 +105,7 @@ def test_reg_eigenvalue_by_power_iteration(self): reg_maxtrix = self.reg.deriv2(self.true_model) max_eigenvalue_numpy, _ = eigsh(reg_maxtrix, k=1) max_eigenvalue_directive = eigenvalue_by_power_iteration( - self.reg, self.true_model, n_pw_iter=100 + self.reg, self.true_model, n_pw_iter=100, random_seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) @@ -114,12 +117,40 @@ def test_combo_eigenvalue_by_power_iteration(self): combo_matrix = dmis_matrix + self.beta * reg_maxtrix max_eigenvalue_numpy, _ = eigsh(combo_matrix, k=1) max_eigenvalue_directive = eigenvalue_by_power_iteration( - self.mixcombo, self.true_model, n_pw_iter=100 + self.mixcombo, self.true_model, n_pw_iter=100, random_seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) print("Eigenvalue Utils for a mixed ComboObjectiveFunction is validated.") +class TestRemovedSeed: + """Test removed ``seed`` argument.""" + + @pytest.fixture + def mock_objfun(self): + """ + Mock objective function class as child of ``BaseObjectiveFunction`` + """ + + class MockObjectiveFunction(BaseObjectiveFunction): + + def deriv2(self, m, v=None, **kwargs): + return np.ones(self.nP) + + return MockObjectiveFunction + + def test_error_argument(self, mock_objfun): + """ + Test if error is raised after passing ``seed``. + """ + msg = "got an unexpected keyword argument 'seed'" + n_params = 5 + combo = mock_objfun(nP=n_params) + 3.0 * mock_objfun(nP=n_params) + model = np.ones(n_params) + with pytest.raises(TypeError, match=msg): + eigenvalue_by_power_iteration(combo_objfct=combo, model=model, seed=42) + + if __name__ == "__main__": unittest.main() diff --git a/tests/utils/test_mesh_utils.py b/tests/utils/test_mesh_utils.py new file mode 100644 index 0000000000..71c7d08a27 --- /dev/null +++ b/tests/utils/test_mesh_utils.py @@ -0,0 +1,118 @@ +import pytest +import numpy as np +from discretize import TensorMesh, TreeMesh, SimplexMesh +from discretize.utils import example_simplex_mesh +from simpeg.utils import shift_to_discrete_topography, get_discrete_topography + +DH = 1.0 +N = 16 + + +def get_mesh(mesh_type, dim): + """Generate test mesh.""" + h = dim * [[(DH, N)]] + origin = dim * "C" + if mesh_type == "tensor": + return TensorMesh(h, origin) + elif mesh_type == "tree": + tree_mesh = TreeMesh(h, origin) + tree_mesh.refine(-1) + return tree_mesh + else: + points, simplices = example_simplex_mesh(dim * [N]) + points = N * points - N / 2 + return SimplexMesh(points, simplices) + + +def get_points(dim): + """Test points.""" + if dim == 2: + return np.array([[1.1, 3.0], [-3.9, -2.0]]) + else: + return np.array([[1.1, -3.6, 3.0], [-3.9, 4.4, -2.0]]) + + +def get_active_cells(mesh): + """Test active cells for the mesh.""" + active_cells = np.zeros(mesh.n_cells, dtype=bool) + if mesh.dim == 1: + active_cells[mesh.cell_centers < 0.0] = True + else: + active_cells[mesh.cell_centers[:, -1] < 0.0] = True + return active_cells + + +CASES_LIST_SUCCESS = [ + ("tensor", 2, "center", False, 0.0), + ("tensor", 3, "top", False, np.r_[1.25, 1.25]), + ("tree", 2, "top", False, np.r_[1.25, 1.25]), + ("tree", 3, "center", False, 0.0), + ("tensor", 2, "center", True, 0.0), + ("tensor", 3, "top", True, np.r_[1.25, 1.25]), + ("tree", 2, "top", True, np.r_[1.25, 1.25]), + ("tree", 3, "center", True, 0.0), +] + + +@pytest.mark.parametrize( + "mesh_type, dim, option, shift_horizontal, heights", CASES_LIST_SUCCESS +) +def test_function_success(mesh_type, dim, option, shift_horizontal, heights): + """Test cases that run properly.""" + mesh = get_mesh(mesh_type, dim) + active_cells = get_active_cells(mesh) + pts = get_points(dim) + + pts_shifted = shift_to_discrete_topography( + mesh, + pts, + active_cells, + topo_cell_cutoff=option, + shift_horizontal=shift_horizontal, + heights=heights, + ) + + if isinstance(heights, (int, float)): + heights = np.array([heights]) + + correct_elevations = heights.copy() + if option == "center": + correct_elevations -= 0.5 * DH + + if shift_horizontal: + correct_locations = np.round(pts) + 0.5 * DH + else: + correct_locations = pts.copy() + correct_locations[:, -1] = correct_elevations + + np.testing.assert_allclose(correct_locations, pts_shifted) + + +def test_mesh_type_error(): + """Throw unsupported mesh type error.""" + mesh_type = "simplex" + dim = 2 + + mesh = get_mesh(mesh_type, dim) + active_cells = get_active_cells(mesh) + pts = get_points(dim) + + with pytest.raises(NotImplementedError): + shift_to_discrete_topography(mesh, pts, active_cells) + + with pytest.raises(NotImplementedError): + get_discrete_topography(mesh, active_cells) + + +def test_size_errors(): + """Throw array size mismatch error.""" + mesh_type = "tensor" + dim = 3 + + mesh = get_mesh(mesh_type, dim) + active_cells = get_active_cells(mesh) + pts = get_points(dim) + heights = np.r_[1.0, 2.0, 3.0] + + with pytest.raises(ValueError): + shift_to_discrete_topography(mesh, pts, active_cells, heights=heights) diff --git a/tests/utils/test_model_builder.py b/tests/utils/test_model_builder.py new file mode 100644 index 0000000000..aa9f823b0a --- /dev/null +++ b/tests/utils/test_model_builder.py @@ -0,0 +1,102 @@ +""" +Test functions in model_builder. +""" + +import re +import pytest +import numpy as np +import discretize +from simpeg.utils.model_builder import create_random_model, get_indices_block + + +class TestRemovalSeedProperty: + """ + Test removed seed property. + """ + + @pytest.fixture + def shape(self): + return (5, 5) + + def test_error_argument(self, shape): + """ + Test if error is raised after passing ``seed`` as argument. + """ + msg = "Invalid arguments 'seed'" + seed = 42135 + with pytest.raises(TypeError, match=msg): + create_random_model(shape, seed=seed) + + def test_error_invalid_kwarg(self, shape): + """ + Test error after passing invalid kwargs to the function. + """ + kwargs = {"foo": 1, "bar": 2} + msg = "Invalid arguments 'foo', 'bar'." + with pytest.raises(TypeError, match=msg): + create_random_model(shape, **kwargs) + + +class TestGetIndicesBlock: + """ + Test the ``get_indices_block`` function. + """ + + block_cells_per_dim = 2 + + def get_mesh_and_block_corners(self, ndims): + + p0_template = [-4, 2, -10] + if ndims == 1: + origin = "C" + p0 = np.array(p0_template[:1]) + elif ndims == 2: + origin = "CC" + p0 = np.array(p0_template[:2]) + else: + origin = "CCN" + p0 = np.array(p0_template) + + cell_size = 2.0 + hx = [(cell_size, 10)] + h = [hx for _ in range(ndims)] + mesh = discretize.TensorMesh(h, origin=origin) + + p1 = np.array([c + self.block_cells_per_dim * cell_size for c in p0]) + return (mesh, p0, p1) + + @pytest.mark.parametrize("ndim", [1, 2, 3]) + def test_get_indices_block(self, ndim): + """Test the funciton returns the right indices.""" + mesh, p0, p1 = self.get_mesh_and_block_corners(ndim) + indices = get_indices_block(p0, p1, mesh.cell_centers) + assert len(indices) == self.block_cells_per_dim**ndim + + def test_invalid_p0_p1(self): + # Dummy mesh (not really used) + hx = [(2.0, 10)] + mesh = discretize.TensorMesh([hx]) + + # Define block corners with different amount of elements + p0 = np.array([1.0, 2.0, 3.0, 4.0]) + p1 = np.array([10.0, 11.0, 12.0]) + + msg = re.escape("Dimension mismatch between `p0` and `p1`.") + with pytest.raises(ValueError, match=msg): + get_indices_block(p0, p1, mesh.cell_centers) + + def test_invalid_mesh_dimensions(self): + # Define a 2d mesh + hx = [(2.0, 10)] + mesh = discretize.TensorMesh([hx, hx]) + + # Define block corners in 3d + p0 = np.array([1.0, 2.0, 3.0]) + p1 = np.array([2.0, 3.0, 4.0]) + + msg = re.escape( + "Dimension mismatch between `cell_centers` and dimensions of " + "block corners." + ) + with pytest.raises(ValueError, match=msg): + get_indices_block(p0, p1, mesh.cell_centers) diff --git a/tests/utils/test_report.py b/tests/utils/test_report.py index 828d06ec3f..2f38f361c5 100644 --- a/tests/utils/test_report.py +++ b/tests/utils/test_report.py @@ -15,11 +15,9 @@ def test_version_defaults(self): "pymatsolver", "numpy", "scipy", - "sklearn", "matplotlib", - "empymod", "geoana", - "pandas", + "libdlf", ], # Optional packages. optional=[ @@ -27,6 +25,8 @@ def test_version_defaults(self): "pydiso", "numba", "dask", + "sklearn", + "pandas", "sympy", "IPython", "ipywidgets", diff --git a/tests/utils/test_solverwrap.py b/tests/utils/test_solverwrap.py deleted file mode 100644 index 126f4466ee..0000000000 --- a/tests/utils/test_solverwrap.py +++ /dev/null @@ -1,64 +0,0 @@ -import unittest -from simpeg.utils.solver_utils import Solver, SolverLU, SolverCG, SolverBiCG, SolverDiag -import scipy.sparse as sp -import numpy as np - - -class TestSolve(unittest.TestCase): - def setUp(self): - # Create a random matrix - n = 400 - A = sp.random(n, n, density=0.25) - - self.n = n - self.A = 0.5 * (A + A.T) + n * sp.eye(n) - - def test_Solver(self): - x = np.random.rand(self.n) - b = self.A @ x - - with self.assertWarns(Warning): - Ainv = Solver(self.A, bad_kwarg=312) - x2 = Ainv @ b - np.testing.assert_almost_equal(x, x2) - - def test_SolverLU(self): - x = np.random.rand(self.n) - b = self.A @ x - - with self.assertWarns(Warning): - Ainv = SolverLU(self.A, bad_kwarg=312) - x2 = Ainv @ b - np.testing.assert_almost_equal(x, x2) - - def test_SolverCG(self): - x = np.random.rand(self.n) - b = self.A @ x - - with self.assertWarns(Warning): - Ainv = SolverCG(self.A, bad_kwarg=312) - x2 = Ainv @ b - np.testing.assert_almost_equal(x, x2, decimal=4) - - def test_SolverBiCG(self): - x = np.random.rand(self.n) - b = self.A @ x - - with self.assertWarns(Warning): - Ainv = SolverBiCG(self.A, bad_kwarg=312) - x2 = Ainv @ b - np.testing.assert_almost_equal(x, x2, decimal=4) - - def test_SolverDiag(self): - x = np.random.rand(self.n) - A = sp.diags(np.random.randn(self.n)) - b = A @ x - - with self.assertWarns(Warning): - Ainv = SolverDiag(A, bad_kwarg=312) - x2 = Ainv @ b - np.testing.assert_almost_equal(x, x2) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/base/test_validators.py b/tests/utils/test_validators.py similarity index 87% rename from tests/base/test_validators.py rename to tests/utils/test_validators.py index 2d2df0eb87..ad86256ab7 100644 --- a/tests/base/test_validators.py +++ b/tests/utils/test_validators.py @@ -150,6 +150,18 @@ def test_list_validation(): "ListProperty", ["Hello", "Hello", "Hello"], str, ensure_unique=True ) + # list is not long enough: + with pytest.raises(ValueError, match=r"'ListProperty' must have at least.*"): + validate_list_of_types("ListProperty", [1, 2, 3, 4], int, min_n=5) + + # list is too long: + with pytest.raises(ValueError, match=r"'ListProperty' must have at most.*"): + validate_list_of_types("ListProperty", [1, 2, 3, 4], int, max_n=2) + + # item is not an exact length + with pytest.raises(ValueError, match=r"'ListProperty' must have exactly.*"): + validate_list_of_types("ListProperty", [1, 2, 3, 4], int, min_n=3, max_n=3) + def test_location_validation(): # simple valid location @@ -223,6 +235,12 @@ def test_ndarray_validation(): assert np.issubdtype(out.dtype, complex) np.testing.assert_equal(out, np.array([3.0j, 4.0j, 5.0j])) + out = validate_ndarray_with_shape( + "array_prop", np.array([3j, 4j, 5j]), dtype=(float, complex) + ) + assert np.issubdtype(out.dtype, complex) + np.testing.assert_equal(out, np.array([3.0j, 4.0j, 5.0j])) + # Valid any shaped arrays assert validate_ndarray_with_shape( "NDarrayProperty", np.random.rand(3, 3, 3), ("*", "*", "*"), float @@ -301,31 +319,66 @@ def test_ndarray_validation(): ) -def test_type_validation(): +def test_type_validation_casting(): # should try to cast to type assert type(validate_type("type_prop", 4.0, int)) == int + # should be okay if only able to cast to one of the possible types + assert type(validate_type("type_prop", "four", (float, str))) == str + + # should error if unable to cast to single type + with pytest.raises(TypeError): + validate_type("type_prop", "four", float) + # isinstance without casting should pass through assert type(validate_type("type_prop", 4.0, object, cast=False)) == float - # should return object without casting if isinstance - assert type(validate_type("type_prop", True, int, cast=False)) == bool + # should error if unable to cast to multiple types + with pytest.raises(TypeError): + validate_type("type_prop", "four", (float, int, complex)) + + +def test_type_validation_strict(): # strict type checking - assert type(validate_type("type_prop", 4.0, float, strict=True)) == float + assert ( + type(validate_type("type_prop", 4.0, float, cast=False, strict=True)) == float + ) - # should error if unable to cast to type - with pytest.raises(TypeError): - validate_type("type_prop", "four", float) + # should ok if strict and is exact of one of the classes + assert ( + type( + validate_type( + "type_prop", True, (int, float, bool), cast=False, strict=True + ) + ) + == bool + ) # should error if strict and not an exact same class with pytest.raises(TypeError): validate_type("type_prop", True, int, cast=False, strict=True) + # should error if strict and not any class + with pytest.raises(TypeError): + validate_type("type_prop", True, (int, float, complex), cast=False, strict=True) + + +def test_type_validation_subclasses(): + + # should return object without casting if isinstance + assert type(validate_type("type_prop", True, int, cast=False)) == bool + + # ok if object without casting isinstance of one of multiple classes + assert type(validate_type("type_prop", True, (int, float, str), cast=False)) == bool + # should error if not strict and not subclass with pytest.raises(TypeError): validate_type("type_prop", True, float, cast=False) + with pytest.raises(TypeError): + validate_type("type_prop", True, (float, complex, str), cast=False) + def test_callable_validation(): def func(x): diff --git a/tutorials/01-models_mapping/plot_1_tensor_models.py b/tutorials/01-models_mapping/plot_1_tensor_models.py index ebcf496e95..d2a06ca020 100644 --- a/tutorials/01-models_mapping/plot_1_tensor_models.py +++ b/tutorials/01-models_mapping/plot_1_tensor_models.py @@ -263,7 +263,9 @@ def make_example_mesh(): # Define the model on subsurface cells model = np.r_[background_value, block_value, xc, dx, yc, dy, zc, dz] -parametric_map = maps.ParametricBlock(mesh, indActive=ind_active, epsilon=1e-10, p=5.0) +parametric_map = maps.ParametricBlock( + mesh, active_cells=ind_active, epsilon=1e-10, p=5.0 +) # Define a single mapping from model to mesh model_map = active_map * parametric_map diff --git a/tutorials/01-models_mapping/plot_2_cyl_models.py b/tutorials/01-models_mapping/plot_2_cyl_models.py index cb118b43f9..89f1ab42d4 100644 --- a/tutorials/01-models_mapping/plot_2_cyl_models.py +++ b/tutorials/01-models_mapping/plot_2_cyl_models.py @@ -161,7 +161,9 @@ def make_example_mesh(): model = np.r_[ background_value, pipe_value, rc, dr, 0.0, 1.0, zc, dz ] # add dummy values for phi -parametric_map = maps.ParametricBlock(mesh, indActive=ind_active, epsilon=1e-10, p=8.0) +parametric_map = maps.ParametricBlock( + mesh, active_cells=ind_active, epsilon=1e-10, p=8.0 +) # Define a single mapping from model to mesh model_map = active_map * parametric_map diff --git a/tutorials/01-models_mapping/plot_3_tree_models.py b/tutorials/01-models_mapping/plot_3_tree_models.py index 2a8549aa6a..252e3ec95d 100644 --- a/tutorials/01-models_mapping/plot_3_tree_models.py +++ b/tutorials/01-models_mapping/plot_3_tree_models.py @@ -279,7 +279,9 @@ def refine_box(mesh): # Define the model on subsurface cells model = np.r_[background_value, block_value, xc, dx, yc, dy, zc, dz] -parametric_map = maps.ParametricBlock(mesh, indActive=ind_active, epsilon=1e-10, p=5.0) +parametric_map = maps.ParametricBlock( + mesh, active_cells=ind_active, epsilon=1e-10, p=5.0 +) # Define a single mapping from model to mesh model_map = active_map * parametric_map diff --git a/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py b/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py index 4715d3937b..b8d8c6317e 100644 --- a/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py +++ b/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py @@ -155,7 +155,7 @@ def g(k): # Define how the optimization problem is solved. opt = optimization.ProjectedGNCG( - maxIter=100, lower=-2.0, upper=2.0, maxIterLS=20, maxIterCG=30, tolCG=1e-4 + maxIter=100, lower=-2.0, upper=2.0, maxIterLS=20, cg_maxiter=30, cg_rtol=1e-3 ) # Here we define the inverse problem that is to be solved @@ -174,7 +174,7 @@ def g(k): sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) # Reach target misfit for L2 solution, then use IRLS until model stops changing. -IRLS = directives.Update_IRLS(max_irls_iterations=40, minGNiter=1, f_min_change=1e-4) +IRLS = directives.UpdateIRLS(max_irls_iterations=40, f_min_change=1e-4) # Defining a starting value for the trade-off parameter (beta) between the data # misfit and the regularization. @@ -187,7 +187,13 @@ def g(k): saveDict = directives.SaveOutputEveryIteration(save_txt=False) # Define the directives as a list -directives_list = [sensitivity_weights, IRLS, starting_beta, update_Jacobi, saveDict] +directives_list = [ + sensitivity_weights, + IRLS, + starting_beta, + update_Jacobi, + saveDict, +] ##################################################################### @@ -233,9 +239,13 @@ def g(k): twin = ax.twinx() twin.plot(saveDict.phi_m, "k--", lw=2) -ax.plot(np.r_[IRLS.iterStart, IRLS.iterStart], np.r_[0, np.max(saveDict.phi_d)], "k:") +ax.plot( + np.r_[IRLS.metrics.start_irls_iter, IRLS.metrics.start_irls_iter], + np.r_[0, np.max(saveDict.phi_d)], + "k:", +) ax.text( - IRLS.iterStart, + IRLS.metrics.start_irls_iter, 0.0, "IRLS Start", va="bottom", diff --git a/tutorials/03-gravity/plot_1a_gravity_anomaly.py b/tutorials/03-gravity/plot_1a_gravity_anomaly.py index d2d8aacfd6..8665fc8189 100644 --- a/tutorials/03-gravity/plot_1a_gravity_anomaly.py +++ b/tutorials/03-gravity/plot_1a_gravity_anomaly.py @@ -2,254 +2,13 @@ Forward Simulation of Gravity Anomaly Data on a Tensor Mesh =========================================================== -Here we use the module *simpeg.potential_fields.gravity* to predict gravity -anomaly data for a synthetic density contrast model. The simulation is -carried out on a tensor mesh. For this tutorial, we focus on the following: +.. important:: - - How to create gravity surveys - - How to predict gravity anomaly data for a density contrast model - - How to include surface topography - - The units of the density contrast model and resulting data + This tutorial has been moved to `User Tutorials + `_. + Checkout the `3D Forward Simulation of Gravity Anomaly Data + `_ tutorial. -""" - -######################################################################### -# Import Modules -# -------------- -# - -import numpy as np -from scipy.interpolate import LinearNDInterpolator -import matplotlib as mpl -import matplotlib.pyplot as plt -import os - -from discretize import TensorMesh -from discretize.utils import mkvc, active_from_xyz - -from simpeg.utils import plot2Ddata, model_builder -from simpeg import maps -from simpeg.potential_fields import gravity - -save_output = False - -# sphinx_gallery_thumbnail_number = 2 - -############################################# -# Defining Topography -# ------------------- -# -# Surface topography is defined as an (N, 3) numpy array. We create it here but -# the topography could also be loaded from a file. -# - -[x_topo, y_topo] = np.meshgrid(np.linspace(-200, 200, 41), np.linspace(-200, 200, 41)) -z_topo = -15 * np.exp(-(x_topo**2 + y_topo**2) / 80**2) -x_topo, y_topo, z_topo = mkvc(x_topo), mkvc(y_topo), mkvc(z_topo) -topo_xyz = np.c_[x_topo, y_topo, z_topo] - - -############################################# -# Defining the Survey -# ------------------- -# -# Here, we define survey that will be used for the forward simulation. Gravity -# surveys are simple to create. The user only needs an (N, 3) array to define -# the xyz locations of the observation locations, and a list of field components -# which are to be measured. -# - -# Define the observation locations as an (N, 3) numpy array or load them. -x = np.linspace(-80.0, 80.0, 17) -y = np.linspace(-80.0, 80.0, 17) -x, y = np.meshgrid(x, y) -x, y = mkvc(x.T), mkvc(y.T) -fun_interp = LinearNDInterpolator(np.c_[x_topo, y_topo], z_topo) -z = fun_interp(np.c_[x, y]) + 5.0 -receiver_locations = np.c_[x, y, z] - -# Define the component(s) of the field we want to simulate as strings within -# a list. Here we simulate only the vertical component of gravity anomaly. -components = ["gz"] - -# Use the observation locations and components to define the receivers. To -# simulate data, the receivers must be defined as a list. -receiver_list = gravity.receivers.Point(receiver_locations, components=components) - -receiver_list = [receiver_list] - -# Defining the source field. -source_field = gravity.sources.SourceField(receiver_list=receiver_list) - -# Defining the survey -survey = gravity.survey.Survey(source_field) - - -############################################# -# Defining a Tensor Mesh -# ---------------------- -# -# Here, we create the tensor mesh that will be used to predict gravity anomaly -# data. -# - -dh = 5.0 -hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hy = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hz = [(dh, 5, -1.3), (dh, 15)] -mesh = TensorMesh([hx, hy, hz], "CCN") - -######################################################## -# Density Contrast Model and Mapping on Tensor Mesh -# ------------------------------------------------- -# -# Here, we create the density contrast model that will be used to predict -# gravity anomaly data and the mapping from the model to the mesh. The model -# consists of a less dense block and a more dense sphere. -# - -# Define density contrast values for each unit in g/cc -background_density = 0.0 -block_density = -0.2 -sphere_density = 0.2 - -# Find the indices for the active mesh cells (e.g. cells below surface) -ind_active = active_from_xyz(mesh, topo_xyz) - -# Define mapping from model to active cells. The model consists of a value for -# each cell below the Earth's surface. -nC = int(ind_active.sum()) -model_map = maps.IdentityMap(nP=nC) -# Define model. Models in SimPEG are vector arrays. -model = background_density * np.ones(nC) - -# You could find the indicies of specific cells within the model and change their -# value to add structures. -ind_block = ( - (mesh.gridCC[ind_active, 0] > -50.0) - & (mesh.gridCC[ind_active, 0] < -20.0) - & (mesh.gridCC[ind_active, 1] > -15.0) - & (mesh.gridCC[ind_active, 1] < 15.0) - & (mesh.gridCC[ind_active, 2] > -50.0) - & (mesh.gridCC[ind_active, 2] < -30.0) -) -model[ind_block] = block_density - -# You can also use SimPEG utilities to add structures to the model more concisely -ind_sphere = model_builder.get_indices_sphere( - np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC -) -ind_sphere = ind_sphere[ind_active] -model[ind_sphere] = sphere_density - -# Plot Density Contrast Model -fig = plt.figure(figsize=(9, 4)) -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) - -ax1 = fig.add_axes([0.1, 0.12, 0.73, 0.78]) -mesh.plot_slice( - plotting_map * model, - normal="Y", - ax=ax1, - ind=int(mesh.shape_cells[1] / 2), - grid=True, - clim=(np.min(model), np.max(model)), - pcolor_opts={"cmap": "viridis"}, -) -ax1.set_title("Model slice at y = 0 m") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") - -ax2 = fig.add_axes([0.85, 0.12, 0.05, 0.78]) -norm = mpl.colors.Normalize(vmin=np.min(model), vmax=np.max(model)) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.viridis -) -cbar.set_label("$g/cm^3$", rotation=270, labelpad=15, size=12) - -plt.show() - - -####################################################### -# Simulation: Gravity Anomaly Data on Tensor Mesh -# ----------------------------------------------- -# -# Here we demonstrate how to predict gravity anomaly data using the integral -# formulation. -# - -############################################################################### -# Define the forward simulation. By setting the ``store_sensitivities`` keyword -# argument to ``"forward_only"``, we simulate the data without storing the -# sensitivities. -# - -simulation = gravity.simulation.Simulation3DIntegral( - survey=survey, - mesh=mesh, - rhoMap=model_map, - ind_active=ind_active, - store_sensitivities="forward_only", - engine="choclo", -) - -############################################################################### -# .. tip:: -# -# Since SimPEG v0.21.0 we can use `Choclo -# `_ as the engine for running the gravity -# simulations, which results in faster and more memory efficient runs. Just -# pass ``engine="choclo"`` when constructing the simulation. -# - -############################################################################### -# Compute predicted data for some model -# SimPEG uses right handed coordinate where Z is positive upward. -# This causes gravity signals look "inconsistent" with density values in visualization. - -dpred = simulation.dpred(model) - -# Plot -fig = plt.figure(figsize=(7, 5)) - -ax1 = fig.add_axes([0.1, 0.1, 0.75, 0.85]) -plot2Ddata(receiver_list[0].locations, dpred, ax=ax1, contourOpts={"cmap": "bwr"}) -ax1.set_title("Gravity Anomaly (Z-component)") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("y (m)") - -ax2 = fig.add_axes([0.82, 0.1, 0.03, 0.85]) -norm = mpl.colors.Normalize(vmin=-np.max(np.abs(dpred)), vmax=np.max(np.abs(dpred))) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.bwr, format="%.1e" -) -cbar.set_label("$mgal$", rotation=270, labelpad=15, size=12) - -plt.show() - - -####################################################### -# Optional: Exporting Results -# --------------------------- -# -# Write the data, topography and true model -# - -if save_output: - dir_path = os.path.dirname(__file__).split(os.path.sep) - dir_path.extend(["outputs"]) - dir_path = os.path.sep.join(dir_path) + os.path.sep - - if not os.path.exists(dir_path): - os.mkdir(dir_path) - - fname = dir_path + "gravity_topo.txt" - np.savetxt(fname, np.c_[topo_xyz], fmt="%.4e") - - np.random.seed(737) - maximum_anomaly = np.max(np.abs(dpred)) - noise = 0.01 * maximum_anomaly * np.random.randn(len(dpred)) - fname = dir_path + "gravity_data.obs" - np.savetxt(fname, np.c_[receiver_locations, dpred + noise], fmt="%.4e") +""" diff --git a/tutorials/03-gravity/plot_1b_gravity_gradiometry.py b/tutorials/03-gravity/plot_1b_gravity_gradiometry.py index 84e4227cf3..1c26443b9d 100644 --- a/tutorials/03-gravity/plot_1b_gravity_gradiometry.py +++ b/tutorials/03-gravity/plot_1b_gravity_gradiometry.py @@ -2,287 +2,12 @@ Forward Simulation of Gradiometry Data on a Tree Mesh ===================================================== -Here we use the module *simpeg.potential_fields.gravity* to predict gravity -gradiometry data for a synthetic density contrast model. The simulation is -carried out on a tree mesh. For this tutorial, we focus on the following: +.. important:: - - How to define the survey when we want multiple field components - - How to predict gravity gradiometry data for a density contrast model - - How to construct tree meshes based on topography and survey geometry - - The units of the density contrast model and resulting data + This tutorial has been moved to `User Tutorials + `_. + Checkout the `3D Forward Simulation of Gravity Gradiometry Data + `_ tutorial. """ - -######################################################################### -# Import Modules -# -------------- -# - -import numpy as np -from scipy.interpolate import LinearNDInterpolator -import matplotlib as mpl -import matplotlib.pyplot as plt - -from discretize import TreeMesh -from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz -from simpeg.utils import plot2Ddata, model_builder -from simpeg import maps -from simpeg.potential_fields import gravity - -# sphinx_gallery_thumbnail_number = 2 - -############################################# -# Defining Topography -# ------------------- -# -# Surface topography is defined as an (N, 3) numpy array. We create it here but -# the topography could also be loaded from a file. -# - -[x_topo, y_topo] = np.meshgrid(np.linspace(-200, 200, 41), np.linspace(-200, 200, 41)) -z_topo = -15 * np.exp(-(x_topo**2 + y_topo**2) / 80**2) -x_topo, y_topo, z_topo = mkvc(x_topo), mkvc(y_topo), mkvc(z_topo) -xyz_topo = np.c_[x_topo, y_topo, z_topo] - - -############################################# -# Defining the Survey -# ------------------- -# -# Here, we define survey that will be used for the forward simulation. Gravity -# surveys are simple to create. The user only needs an (N, 3) array to define -# the xyz locations of the observation locations, and a list of field components -# which are to be measured. -# - -# Define the observation locations as an (N, 3) numpy array or load them -x = np.linspace(-80.0, 80.0, 17) -y = np.linspace(-80.0, 80.0, 17) -x, y = np.meshgrid(x, y) -x, y = mkvc(x.T), mkvc(y.T) -fun_interp = LinearNDInterpolator(np.c_[x_topo, y_topo], z_topo) -z = fun_interp(np.c_[x, y]) + 5 -receiver_locations = np.c_[x, y, z] - -# Define the component(s) of the field we want to simulate as strings within -# a list. Here we measure the x, y and z components of the gravity anomaly at -# each observation location. -components = ["gxz", "gyz", "gzz"] - -# Use the observation locations and components to define the receivers. To -# simulate data, the receivers must be defined as a list. -receiver_list = gravity.receivers.Point(receiver_locations, components=components) - -receiver_list = [receiver_list] - -# Defining the source field. -source_field = gravity.sources.SourceField(receiver_list=receiver_list) - -# Defining the survey -survey = gravity.survey.Survey(source_field) - - -########################################################## -# Defining an OcTree Mesh -# ----------------------- -# -# Here, we create the OcTree mesh that will be used in the forward simulation. -# - -dx = 5 # minimum cell width (base mesh cell width) in x -dy = 5 # minimum cell width (base mesh cell width) in y -dz = 5 # minimum cell width (base mesh cell width) in z - -x_length = 240.0 # domain width in x -y_length = 240.0 # domain width in y -z_length = 120.0 # domain width in z - -# Compute number of base mesh cells required in x and y -nbcx = 2 ** int(np.round(np.log(x_length / dx) / np.log(2.0))) -nbcy = 2 ** int(np.round(np.log(y_length / dy) / np.log(2.0))) -nbcz = 2 ** int(np.round(np.log(z_length / dz) / np.log(2.0))) - -# Define the base mesh -hx = [(dx, nbcx)] -hy = [(dy, nbcy)] -hz = [(dz, nbcz)] -mesh = TreeMesh([hx, hy, hz], x0="CCN") - -# Refine based on surface topography -mesh = refine_tree_xyz( - mesh, xyz_topo, octree_levels=[2, 2], method="surface", finalize=False -) - -# Refine box based on region of interest -xp, yp, zp = np.meshgrid([-100.0, 100.0], [-100.0, 100.0], [-80.0, 0.0]) -xyz = np.c_[mkvc(xp), mkvc(yp), mkvc(zp)] - -mesh = refine_tree_xyz(mesh, xyz, octree_levels=[2, 2], method="box", finalize=False) - -mesh.finalize() - -####################################################### -# Density Contrast Model and Mapping on OcTree Mesh -# ------------------------------------------------- -# -# Here, we create the density contrast model that will be used to simulate gravity -# gradiometry data and the mapping from the model to the mesh. The model -# consists of a less dense block and a more dense sphere. -# - -# Define density contrast values for each unit in g/cc -background_density = 0.0 -block_density = -0.1 -sphere_density = 0.1 - -# Find the indecies for the active mesh cells (e.g. cells below surface) -ind_active = active_from_xyz(mesh, xyz_topo) - -# Define mapping from model to active cells. The model consists of a value for -# each cell below the Earth's surface. -nC = int(ind_active.sum()) -model_map = maps.IdentityMap(nP=nC) # model will be value of active cells - -# Define model. Models in SimPEG are vector arrays. -model = background_density * np.ones(nC) - -# You could find the indicies of specific cells within the model and change their -# value to add structures. -ind_block = ( - (mesh.gridCC[ind_active, 0] > -50.0) - & (mesh.gridCC[ind_active, 0] < -20.0) - & (mesh.gridCC[ind_active, 1] > -15.0) - & (mesh.gridCC[ind_active, 1] < 15.0) - & (mesh.gridCC[ind_active, 2] > -50.0) - & (mesh.gridCC[ind_active, 2] < -30.0) -) -model[ind_block] = block_density - -# You can also use SimPEG utilities to add structures to the model more concisely -ind_sphere = model_builder.get_indices_sphere( - np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC -) -ind_sphere = ind_sphere[ind_active] -model[ind_sphere] = sphere_density - -# Plot Density Contrast Model -fig = plt.figure(figsize=(9, 4)) -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) - -ax1 = fig.add_axes([0.1, 0.12, 0.73, 0.78]) -mesh.plot_slice( - plotting_map * model, - normal="Y", - ax=ax1, - ind=int(mesh.h[1].size / 2), - grid=True, - clim=(np.min(model), np.max(model)), - pcolor_opts={"cmap": "viridis"}, -) -ax1.set_title("Model slice at y = 0 m") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") - -ax2 = fig.add_axes([0.85, 0.12, 0.05, 0.78]) -norm = mpl.colors.Normalize(vmin=np.min(model), vmax=np.max(model)) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.viridis -) -cbar.set_label("$g/cm^3$", rotation=270, labelpad=15, size=12) - -plt.show() - -############################################################## -# Simulation: Gravity Gradiometry Data on an OcTree Mesh -# ------------------------------------------------------ -# -# Here we demonstrate how to predict gravity anomaly data using the integral -# formulation. -# - -############################################################################### -# Define the forward simulation. By setting the ``store_sensitivities`` keyword -# argument to ``"forward_only"``, we simulate the data without storing the -# sensitivities -# - -simulation = gravity.simulation.Simulation3DIntegral( - survey=survey, - mesh=mesh, - rhoMap=model_map, - ind_active=ind_active, - store_sensitivities="forward_only", - engine="choclo", -) - -############################################################################### -# .. tip:: -# -# Since SimPEG v0.21.0 we can use `Choclo -# `_ as the engine for running the gravity -# simulations, which results in faster and more memory efficient runs. Just -# pass ``engine="choclo"`` when constructing the simulation. -# - -############################################################################### -# Compute predicted data for some model - -dpred = simulation.dpred(model) -n_data = len(dpred) - -# Plot -fig = plt.figure(figsize=(10, 3)) -n_locations = receiver_locations.shape[0] -v_max = np.max(np.abs(dpred)) - -ax1 = fig.add_axes([0.1, 0.15, 0.25, 0.78]) -cplot1 = plot2Ddata( - receiver_locations, - dpred[0:n_data:3], - ax=ax1, - ncontour=30, - clim=(-v_max, v_max), - contourOpts={"cmap": "bwr"}, -) -cplot1[0].set_clim((-v_max, v_max)) -ax1.set_title(r"$\partial g /\partial x$") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("y (m)") - -ax2 = fig.add_axes([0.36, 0.15, 0.25, 0.78]) -cplot2 = plot2Ddata( - receiver_locations, - dpred[1:n_data:3], - ax=ax2, - ncontour=30, - clim=(-v_max, v_max), - contourOpts={"cmap": "bwr"}, -) -cplot2[0].set_clim((-v_max, v_max)) -ax2.set_title(r"$\partial g /\partial y$") -ax2.set_xlabel("x (m)") -ax2.set_yticks([]) - -ax3 = fig.add_axes([0.62, 0.15, 0.25, 0.78]) -cplot3 = plot2Ddata( - receiver_locations, - dpred[2:n_data:3], - ax=ax3, - ncontour=30, - clim=(-v_max, v_max), - contourOpts={"cmap": "bwr"}, -) -cplot3[0].set_clim((-v_max, v_max)) -ax3.set_title(r"$\partial g /\partial z$") -ax3.set_xlabel("x (m)") -ax3.set_yticks([]) - -ax4 = fig.add_axes([0.89, 0.13, 0.02, 0.79]) -norm = mpl.colors.Normalize(vmin=-v_max, vmax=v_max) -cbar = mpl.colorbar.ColorbarBase( - ax4, norm=norm, orientation="vertical", cmap=mpl.cm.bwr -) -cbar.set_label("Eotvos", rotation=270, labelpad=15, size=12) - -plt.show() diff --git a/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py b/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py index 89d043d65d..3f3b9cda85 100644 --- a/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py +++ b/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py @@ -2,444 +2,12 @@ Least-Squares Inversion of Gravity Anomaly Data =============================================== -Here we invert gravity anomaly data to recover a density contrast model. We -formulate the inverse problem as a least-squares optimization problem. For -this tutorial, we focus on the following: +.. important:: - - Defining the survey from xyz formatted data - - Generating a mesh based on survey geometry - - Including surface topography - - Defining the inverse problem (data misfit, regularization, optimization) - - Specifying directives for the inversion - - Plotting the recovered model and data misfit - -Although we consider gravity anomaly data in this tutorial, the same approach -can be used to invert gradiometry and other types of geophysical data. + This tutorial has been moved to `User Tutorials + `_. + Checkout the `3D Inversion of Gravity Anomaly Data + `_ tutorial. """ - -######################################################################### -# Import modules -# -------------- -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -import tarfile - -from discretize import TensorMesh -from discretize.utils import active_from_xyz -from simpeg.utils import plot2Ddata, model_builder -from simpeg.potential_fields import gravity -from simpeg import ( - maps, - data, - data_misfit, - inverse_problem, - regularization, - optimization, - directives, - inversion, - utils, -) - -# sphinx_gallery_thumbnail_number = 3 - -############################################# -# Define File Names -# ----------------- -# -# File paths for assets we are loading. To set up the inversion, we require -# topography and field observations. The true model defined on the whole mesh -# is loaded to compare with the inversion result. These files are stored as a -# tar-file on our google cloud bucket: -# "https://storage.googleapis.com/simpeg/doc-assets/gravity.tar.gz" -# - -# storage bucket where we have the data -data_source = "https://storage.googleapis.com/simpeg/doc-assets/gravity.tar.gz" - -# download the data -downloaded_data = utils.download(data_source, overwrite=True) - -# unzip the tarfile -tar = tarfile.open(downloaded_data, "r") -tar.extractall() -tar.close() - -# path to the directory containing our data -dir_path = downloaded_data.split(".")[0] + os.path.sep - -# files to work with -topo_filename = dir_path + "gravity_topo.txt" -data_filename = dir_path + "gravity_data.obs" - - -############################################# -# Load Data and Plot -# ------------------ -# -# Here we load and plot synthetic gravity anomaly data. Topography is generally -# defined as an (N, 3) array. Gravity data is generally defined with 4 columns: -# x, y, z and data. -# - -# Load topography -xyz_topo = np.loadtxt(str(topo_filename)) - -# Load field data -dobs = np.loadtxt(str(data_filename)) - -# Define receiver locations and observed data -receiver_locations = dobs[:, 0:3] -dobs = dobs[:, -1] - -# Plot -mpl.rcParams.update({"font.size": 12}) -fig = plt.figure(figsize=(7, 5)) - -ax1 = fig.add_axes([0.1, 0.1, 0.73, 0.85]) -plot2Ddata(receiver_locations, dobs, ax=ax1, contourOpts={"cmap": "bwr"}) -ax1.set_title("Gravity Anomaly") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("y (m)") - -ax2 = fig.add_axes([0.8, 0.1, 0.03, 0.85]) -norm = mpl.colors.Normalize(vmin=-np.max(np.abs(dobs)), vmax=np.max(np.abs(dobs))) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.bwr, format="%.1e" -) -cbar.set_label("$mgal$", rotation=270, labelpad=15, size=12) - -plt.show() - -############################################# -# Assign Uncertainties -# -------------------- -# -# Inversion with SimPEG requires that we define the standard deviation of our data. -# This represents our estimate of the noise in our data. For a gravity inversion, -# a constant floor value is generally applied to all data. For this tutorial, -# the standard deviation on each datum will be 1% of the maximum observed -# gravity anomaly value. -# - -maximum_anomaly = np.max(np.abs(dobs)) - -uncertainties = 0.01 * maximum_anomaly * np.ones(np.shape(dobs)) - -############################################# -# Defining the Survey -# ------------------- -# -# Here, we define the survey that will be used for this tutorial. Gravity -# surveys are simple to create. The user only needs an (N, 3) array to define -# the xyz locations of the observation locations. From this, the user can -# define the receivers and the source field. -# - -# Define the receivers. The data consists of vertical gravity anomaly measurements. -# The set of receivers must be defined as a list. -receiver_list = gravity.receivers.Point(receiver_locations, components="gz") - -receiver_list = [receiver_list] - -# Define the source field -source_field = gravity.sources.SourceField(receiver_list=receiver_list) - -# Define the survey -survey = gravity.survey.Survey(source_field) - -############################################# -# Defining the Data -# ----------------- -# -# Here is where we define the data that is inverted. The data is defined by -# the survey, the observation values and the standard deviation. -# - -data_object = data.Data(survey, dobs=dobs, standard_deviation=uncertainties) - - -############################################# -# Defining a Tensor Mesh -# ---------------------- -# -# Here, we create the tensor mesh that will be used to invert gravity anomaly -# data. If desired, we could define an OcTree mesh. -# - -dh = 5.0 -hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hy = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hz = [(dh, 5, -1.3), (dh, 15)] -mesh = TensorMesh([hx, hy, hz], "CCN") - -######################################################## -# Starting/Reference Model and Mapping on Tensor Mesh -# --------------------------------------------------- -# -# Here, we create starting and/or reference models for the inversion as -# well as the mapping from the model space to the active cells. Starting and -# reference models can be a constant background value or contain a-priori -# structures. -# - -# Find the indices of the active cells in forward model (ones below surface) -ind_active = active_from_xyz(mesh, xyz_topo) - -# Define mapping from model to active cells -nC = int(ind_active.sum()) -model_map = maps.IdentityMap(nP=nC) # model consists of a value for each active cell - -# Define and plot starting model -starting_model = np.zeros(nC) - - -############################################## -# Define the Physics -# ------------------ -# -# Here, we define the physics of the gravity problem by using the simulation -# class. -# -# .. tip:: -# -# Since SimPEG v0.21.0 we can use `Choclo -# `_ as the engine for running the gravity -# simulations, which results in faster and more memory efficient runs. Just -# pass ``engine="choclo"`` when constructing the simulation. -# - -simulation = gravity.simulation.Simulation3DIntegral( - survey=survey, - mesh=mesh, - rhoMap=model_map, - ind_active=ind_active, - engine="choclo", -) - - -####################################################################### -# Define the Inverse Problem -# -------------------------- -# -# The inverse problem is defined by 3 things: -# -# 1) Data Misfit: a measure of how well our recovered model explains the field data -# 2) Regularization: constraints placed on the recovered model and a priori information -# 3) Optimization: the numerical approach used to solve the inverse problem -# - -# Define the data misfit. Here the data misfit is the L2 norm of the weighted -# residual between the observed data and the data predicted for a given model. -# Within the data misfit, the residual between predicted and observed data are -# normalized by the data's standard deviation. -dmis = data_misfit.L2DataMisfit(data=data_object, simulation=simulation) - -# Define the regularization (model objective function). -reg = regularization.WeightedLeastSquares( - mesh, active_cells=ind_active, mapping=model_map -) - -# Define how the optimization problem is solved. Here we will use a projected -# Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.ProjectedGNCG( - maxIter=10, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 -) - -# Here we define the inverse problem that is to be solved -inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) - -####################################################################### -# Define Inversion Directives -# --------------------------- -# -# Here we define any directiveas that are carried out during the inversion. This -# includes the cooling schedule for the trade-off parameter (beta), stopping -# criteria for the inversion and saving inversion results at each iteration. -# - -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e1) - -# Defining the fractional decrease in beta and the number of Gauss-Newton solves -# for each beta value. -beta_schedule = directives.BetaSchedule(coolingFactor=5, coolingRate=1) - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# Updating the preconditionner if it is model dependent. -update_jacobi = directives.UpdatePreconditioner() - -# Setting a stopping criteria for the inversion. -target_misfit = directives.TargetMisfit(chifact=1) - -# Add sensitivity weights -sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) - -# The directives are defined as a list. -directives_list = [ - sensitivity_weights, - starting_beta, - beta_schedule, - save_iteration, - update_jacobi, - target_misfit, -] - -##################################################################### -# Running the Inversion -# --------------------- -# -# To define the inversion object, we need to define the inversion problem and -# the set of directives. We can then run the inversion. -# - -# Here we combine the inverse problem and the set of directives -inv = inversion.BaseInversion(inv_prob, directives_list) - -# Run inversion -recovered_model = inv.run(starting_model) - - -############################################################ -# Recreate True Model -# ------------------- -# - -# Define density contrast values for each unit in g/cc -background_density = 0.0 -block_density = -0.2 -sphere_density = 0.2 - -# Define model. Models in SimPEG are vector arrays. -true_model = background_density * np.ones(nC) - -# You could find the indicies of specific cells within the model and change their -# value to add structures. -ind_block = ( - (mesh.gridCC[ind_active, 0] > -50.0) - & (mesh.gridCC[ind_active, 0] < -20.0) - & (mesh.gridCC[ind_active, 1] > -15.0) - & (mesh.gridCC[ind_active, 1] < 15.0) - & (mesh.gridCC[ind_active, 2] > -50.0) - & (mesh.gridCC[ind_active, 2] < -30.0) -) -true_model[ind_block] = block_density - -# You can also use SimPEG utilities to add structures to the model more concisely -ind_sphere = model_builder.get_indices_sphere( - np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC -) -ind_sphere = ind_sphere[ind_active] -true_model[ind_sphere] = sphere_density - - -############################################################ -# Plotting True Model and Recovered Model -# --------------------------------------- -# - -# Plot True Model -fig = plt.figure(figsize=(9, 4)) -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) - -ax1 = fig.add_axes([0.1, 0.1, 0.73, 0.8]) -mesh.plot_slice( - plotting_map * true_model, - normal="Y", - ax=ax1, - ind=int(mesh.shape_cells[1] / 2), - grid=True, - clim=(np.min(true_model), np.max(true_model)), - pcolor_opts={"cmap": "viridis"}, -) -ax1.set_title("Model slice at y = 0 m") - - -ax2 = fig.add_axes([0.85, 0.1, 0.05, 0.8]) -norm = mpl.colors.Normalize(vmin=np.min(true_model), vmax=np.max(true_model)) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.viridis, format="%.1e" -) -cbar.set_label("$g/cm^3$", rotation=270, labelpad=15, size=12) - -plt.show() - -# Plot Recovered Model -fig = plt.figure(figsize=(9, 4)) -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) - -ax1 = fig.add_axes([0.1, 0.1, 0.73, 0.8]) -mesh.plot_slice( - plotting_map * recovered_model, - normal="Y", - ax=ax1, - ind=int(mesh.shape_cells[1] / 2), - grid=True, - clim=(np.min(recovered_model), np.max(recovered_model)), - pcolor_opts={"cmap": "viridis"}, -) -ax1.set_title("Model slice at y = 0 m") - -ax2 = fig.add_axes([0.85, 0.1, 0.05, 0.8]) -norm = mpl.colors.Normalize(vmin=np.min(recovered_model), vmax=np.max(recovered_model)) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.viridis -) -cbar.set_label("$g/cm^3$", rotation=270, labelpad=15, size=12) - -plt.show() - -################################################################### -# Plotting Predicted Data and Normalized Misfit -# --------------------------------------------- -# - -# Predicted data with final recovered model -# SimPEG uses right handed coordinate where Z is positive upward. -# This causes gravity signals look "inconsistent" with density values in visualization. -dpred = inv_prob.dpred - -# Observed data | Predicted data | Normalized data misfit -data_array = np.c_[dobs, dpred, (dobs - dpred) / uncertainties] - -fig = plt.figure(figsize=(17, 4)) -plot_title = ["Observed", "Predicted", "Normalized Misfit"] -plot_units = ["mgal", "mgal", ""] - -ax1 = 3 * [None] -ax2 = 3 * [None] -norm = 3 * [None] -cbar = 3 * [None] -cplot = 3 * [None] -v_lim = [np.max(np.abs(dobs)), np.max(np.abs(dobs)), np.max(np.abs(data_array[:, 2]))] - -for ii in range(0, 3): - ax1[ii] = fig.add_axes([0.33 * ii + 0.03, 0.11, 0.23, 0.84]) - cplot[ii] = plot2Ddata( - receiver_list[0].locations, - data_array[:, ii], - ax=ax1[ii], - ncontour=30, - clim=(-v_lim[ii], v_lim[ii]), - contourOpts={"cmap": "bwr"}, - ) - ax1[ii].set_title(plot_title[ii]) - ax1[ii].set_xlabel("x (m)") - ax1[ii].set_ylabel("y (m)") - - ax2[ii] = fig.add_axes([0.33 * ii + 0.25, 0.11, 0.01, 0.85]) - norm[ii] = mpl.colors.Normalize(vmin=-v_lim[ii], vmax=v_lim[ii]) - cbar[ii] = mpl.colorbar.ColorbarBase( - ax2[ii], norm=norm[ii], orientation="vertical", cmap=mpl.cm.bwr - ) - cbar[ii].set_label(plot_units[ii], rotation=270, labelpad=15, size=12) - -plt.show() diff --git a/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py b/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py index 7bfd6da31f..9b4808a4cf 100644 --- a/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py +++ b/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py @@ -2,447 +2,16 @@ Sparse Norm Inversion of Gravity Anomaly Data ============================================= -Here we invert gravity anomaly data to recover a density contrast model. We formulate the inverse problem as an iteratively -re-weighted least-squares (IRLS) optimization problem. For this tutorial, we -focus on the following: +.. important:: - - Defining the survey from xyz formatted data - - Generating a mesh based on survey geometry - - Including surface topography - - Defining the inverse problem (data misfit, regularization, optimization) - - Specifying directives for the inversion - - Setting sparse and blocky norms - - Plotting the recovered model and data misfit - -Although we consider gravity anomaly data in this tutorial, the same approach -can be used to invert gradiometry and other types of geophysical data. + This tutorial has been moved to `User Tutorials + `_. + Checkout the + `Iteratively Re-weighted Least-Squares (IRLS) Inversion on a Tree Mesh + `_ + section in the + `3D Inversion of Gravity Anomaly Data + `_ tutorial. """ - -######################################################################### -# Import modules -# -------------- -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -import tarfile - -from discretize import TensorMesh -from discretize.utils import active_from_xyz -from simpeg.utils import plot2Ddata, model_builder -from simpeg.potential_fields import gravity -from simpeg import ( - maps, - data, - data_misfit, - inverse_problem, - regularization, - optimization, - directives, - inversion, - utils, -) - -# sphinx_gallery_thumbnail_number = 3 - -############################################# -# Define File Names -# ----------------- -# -# File paths for assets we are loading. To set up the inversion, we require -# topography and field observations. The true model defined on the whole mesh -# is loaded to compare with the inversion result. These files are stored as a -# tar-file on our google cloud bucket: -# "https://storage.googleapis.com/simpeg/doc-assets/gravity.tar.gz" -# - -# storage bucket where we have the data -data_source = "https://storage.googleapis.com/simpeg/doc-assets/gravity.tar.gz" - -# download the data -downloaded_data = utils.download(data_source, overwrite=True) - -# unzip the tarfile -tar = tarfile.open(downloaded_data, "r") -tar.extractall() -tar.close() - -# path to the directory containing our data -dir_path = downloaded_data.split(".")[0] + os.path.sep - -# files to work with -topo_filename = dir_path + "gravity_topo.txt" -data_filename = dir_path + "gravity_data.obs" -model_filename = dir_path + "true_model.txt" - - -############################################# -# Load Data and Plot -# ------------------ -# -# Here we load and plot synthetic gravity anomaly data. Topography is generally -# defined as an (N, 3) array. Gravity data is generally defined with 4 columns: -# x, y, z and data. -# - -# Load topography -xyz_topo = np.loadtxt(str(topo_filename)) - -# Load field data -dobs = np.loadtxt(str(data_filename)) - -# Define receiver locations and observed data -receiver_locations = dobs[:, 0:3] -dobs = dobs[:, -1] - -# Plot -mpl.rcParams.update({"font.size": 12}) -fig = plt.figure(figsize=(7, 5)) - -ax1 = fig.add_axes([0.1, 0.1, 0.73, 0.85]) -plot2Ddata(receiver_locations, dobs, ax=ax1, contourOpts={"cmap": "bwr"}) -ax1.set_title("Gravity Anomaly") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("y (m)") - -ax2 = fig.add_axes([0.8, 0.1, 0.03, 0.85]) -norm = mpl.colors.Normalize(vmin=-np.max(np.abs(dobs)), vmax=np.max(np.abs(dobs))) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.bwr, format="%.1e" -) -cbar.set_label("$mgal$", rotation=270, labelpad=15, size=12) - -plt.show() - -############################################# -# Assign Uncertainties -# -------------------- -# -# Inversion with SimPEG requires that we define standard deviation on our data. -# This represents our estimate of the noise in our data. For gravity inversion, -# a constant floor value is generally applied to all data. For this tutorial, -# the standard deviation on each datum will be 1% of the maximum observed -# gravity anomaly value. -# - -maximum_anomaly = np.max(np.abs(dobs)) - -uncertainties = 0.01 * maximum_anomaly * np.ones(np.shape(dobs)) - -############################################# -# Defining the Survey -# ------------------- -# -# Here, we define survey that will be used for this tutorial. Gravity -# surveys are simple to create. The user only needs an (N, 3) array to define -# the xyz locations of the observation locations. From this, the user can -# define the receivers and the source field. -# - -# Define the receivers. The data consist of vertical gravity anomaly measurements. -# The set of receivers must be defined as a list. -receiver_list = gravity.receivers.Point(receiver_locations, components="gz") - -receiver_list = [receiver_list] - -# Define the source field -source_field = gravity.sources.SourceField(receiver_list=receiver_list) - -# Define the survey -survey = gravity.survey.Survey(source_field) - -############################################# -# Defining the Data -# ----------------- -# -# Here is where we define the data that are inverted. The data are defined by -# the survey, the observation values and the standard deviation. -# - -data_object = data.Data(survey, dobs=dobs, standard_deviation=uncertainties) - - -############################################# -# Defining a Tensor Mesh -# ---------------------- -# -# Here, we create the tensor mesh that will be used to invert gravity anomaly -# data. If desired, we could define an OcTree mesh. -# - -dh = 5.0 -hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hy = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hz = [(dh, 5, -1.3), (dh, 15)] -mesh = TensorMesh([hx, hy, hz], "CCN") - -######################################################## -# Starting/Reference Model and Mapping on Tensor Mesh -# --------------------------------------------------- -# -# Here, we create starting and/or reference models for the inversion as -# well as the mapping from the model space to the active cells. Starting and -# reference models can be a constant background value or contain a-priori -# structures. -# - -# Find the indices of the active cells in forward model (ones below surface) -ind_active = active_from_xyz(mesh, xyz_topo) - -# Define mapping from model to active cells -nC = int(ind_active.sum()) -model_map = maps.IdentityMap(nP=nC) # model consists of a value for each active cell - -# Define and plot starting model -starting_model = np.zeros(nC) - - -############################################## -# Define the Physics -# ------------------ -# -# Here, we define the physics of the gravity problem by using the simulation -# class. -# -# .. tip:: -# -# Since SimPEG v0.21.0 we can use `Choclo -# `_ as the engine for running the gravity -# simulations, which results in faster and more memory efficient runs. Just -# pass ``engine="choclo"`` when constructing the simulation. -# - -simulation = gravity.simulation.Simulation3DIntegral( - survey=survey, - mesh=mesh, - rhoMap=model_map, - ind_active=ind_active, - engine="choclo", -) - - -####################################################################### -# Define the Inverse Problem -# -------------------------- -# -# The inverse problem is defined by 3 things: -# -# 1) Data Misfit: a measure of how well our recovered model explains the field data -# 2) Regularization: constraints placed on the recovered model and a priori information -# 3) Optimization: the numerical approach used to solve the inverse problem -# - -# Define the data misfit. Here the data misfit is the L2 norm of the weighted -# residual between the observed data and the data predicted for a given model. -# Within the data misfit, the residual between predicted and observed data are -# normalized by the data's standard deviation. -dmis = data_misfit.L2DataMisfit(data=data_object, simulation=simulation) -dmis.W = utils.sdiag(1 / uncertainties) - -# Define the regularization (model objective function). -reg = regularization.Sparse(mesh, active_cells=ind_active, mapping=model_map) -reg.norms = [0, 2, 2, 2] - -# Define how the optimization problem is solved. Here we will use a projected -# Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.ProjectedGNCG( - maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 -) - -# Here we define the inverse problem that is to be solved -inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) - -####################################################################### -# Define Inversion Directives -# --------------------------- -# -# Here we define any directiveas that are carried out during the inversion. This -# includes the cooling schedule for the trade-off parameter (beta), stopping -# criteria for the inversion and saving inversion results at each iteration. -# - -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e0) - -# Defines the directives for the IRLS regularization. This includes setting -# the cooling schedule for the trade-off parameter. -update_IRLS = directives.Update_IRLS( - f_min_change=1e-4, - max_irls_iterations=30, - coolEpsFact=1.5, - beta_tol=1e-2, -) - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# Updating the preconditionner if it is model dependent. -update_jacobi = directives.UpdatePreconditioner() - -# Add sensitivity weights -sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) - -# The directives are defined as a list. -directives_list = [ - update_IRLS, - sensitivity_weights, - starting_beta, - save_iteration, - update_jacobi, -] - -##################################################################### -# Running the Inversion -# --------------------- -# -# To define the inversion object, we need to define the inversion problem and -# the set of directives. We can then run the inversion. -# - -# Here we combine the inverse problem and the set of directives -inv = inversion.BaseInversion(inv_prob, directives_list) - -# Run inversion -recovered_model = inv.run(starting_model) - - -############################################################ -# Recreate True Model -# ------------------- -# - -# Define density contrast values for each unit in g/cc -background_density = 0.0 -block_density = -0.2 -sphere_density = 0.2 - -# Define model. Models in SimPEG are vector arrays. -true_model = background_density * np.ones(nC) - -# You could find the indicies of specific cells within the model and change their -# value to add structures. -ind_block = ( - (mesh.gridCC[ind_active, 0] > -50.0) - & (mesh.gridCC[ind_active, 0] < -20.0) - & (mesh.gridCC[ind_active, 1] > -15.0) - & (mesh.gridCC[ind_active, 1] < 15.0) - & (mesh.gridCC[ind_active, 2] > -50.0) - & (mesh.gridCC[ind_active, 2] < -30.0) -) -true_model[ind_block] = block_density - -# You can also use SimPEG utilities to add structures to the model more concisely -ind_sphere = model_builder.get_indices_sphere( - np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC -) -ind_sphere = ind_sphere[ind_active] -true_model[ind_sphere] = sphere_density - - -############################################################ -# Plotting True Model and Recovered Model -# --------------------------------------- -# - -# Plot True Model -fig = plt.figure(figsize=(9, 4)) -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) - -ax1 = fig.add_axes([0.1, 0.1, 0.73, 0.8]) -mesh.plot_slice( - plotting_map * true_model, - normal="Y", - ax=ax1, - ind=int(mesh.shape_cells[1] / 2), - grid=True, - clim=(np.min(true_model), np.max(true_model)), - pcolor_opts={"cmap": "viridis"}, -) -ax1.set_title("Model slice at y = 0 m") - - -ax2 = fig.add_axes([0.85, 0.1, 0.05, 0.8]) -norm = mpl.colors.Normalize(vmin=np.min(true_model), vmax=np.max(true_model)) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.viridis, format="%.1e" -) -cbar.set_label("$g/cm^3$", rotation=270, labelpad=15, size=12) - -plt.show() - -# Plot Recovered Model -fig = plt.figure(figsize=(9, 4)) -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) - -ax1 = fig.add_axes([0.1, 0.1, 0.73, 0.8]) -mesh.plot_slice( - plotting_map * recovered_model, - normal="Y", - ax=ax1, - ind=int(mesh.shape_cells[1] / 2), - grid=True, - clim=(np.min(recovered_model), np.max(recovered_model)), - pcolor_opts={"cmap": "viridis"}, -) -ax1.set_title("Model slice at y = 0 m") - -ax2 = fig.add_axes([0.85, 0.1, 0.05, 0.8]) -norm = mpl.colors.Normalize(vmin=np.min(recovered_model), vmax=np.max(recovered_model)) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.viridis -) -cbar.set_label("$g/cm^3$", rotation=270, labelpad=15, size=12) - -plt.show() - -################################################################### -# Plotting Predicted Data and Normalized Misfit -# --------------------------------------------- -# - -# Predicted data with final recovered model -# SimPEG uses right handed coordinate where Z is positive upward. -# This causes gravity signals look "inconsistent" with density values in visualization. -dpred = inv_prob.dpred - -# Observed data | Predicted data | Normalized data misfit -data_array = np.c_[dobs, dpred, (dobs - dpred) / uncertainties] - -fig = plt.figure(figsize=(17, 4)) -plot_title = ["Observed", "Predicted", "Normalized Misfit"] -plot_units = ["mgal", "mgal", ""] - -ax1 = 3 * [None] -ax2 = 3 * [None] -norm = 3 * [None] -cbar = 3 * [None] -cplot = 3 * [None] -v_lim = [np.max(np.abs(dobs)), np.max(np.abs(dobs)), np.max(np.abs(data_array[:, 2]))] - -for ii in range(0, 3): - ax1[ii] = fig.add_axes([0.33 * ii + 0.03, 0.11, 0.23, 0.84]) - cplot[ii] = plot2Ddata( - receiver_list[0].locations, - data_array[:, ii], - ax=ax1[ii], - ncontour=30, - clim=(-v_lim[ii], v_lim[ii]), - contourOpts={"cmap": "bwr"}, - ) - ax1[ii].set_title(plot_title[ii]) - ax1[ii].set_xlabel("x (m)") - ax1[ii].set_ylabel("y (m)") - - ax2[ii] = fig.add_axes([0.33 * ii + 0.25, 0.11, 0.01, 0.85]) - norm[ii] = mpl.colors.Normalize(vmin=-v_lim[ii], vmax=v_lim[ii]) - cbar[ii] = mpl.colorbar.ColorbarBase( - ax2[ii], norm=norm[ii], orientation="vertical", cmap=mpl.cm.bwr - ) - cbar[ii].set_label(plot_units[ii], rotation=270, labelpad=15, size=12) - -plt.show() diff --git a/tutorials/03-gravity/plot_inv_1c_gravity_anomaly_irls_compare_weighting.py b/tutorials/03-gravity/plot_inv_1c_gravity_anomaly_irls_compare_weighting.py new file mode 100644 index 0000000000..8e61754a98 --- /dev/null +++ b/tutorials/03-gravity/plot_inv_1c_gravity_anomaly_irls_compare_weighting.py @@ -0,0 +1,13 @@ +""" +Compare weighting strategy with Inversion of surface Gravity Anomaly Data +========================================================================= + +.. important:: + + This tutorial has been moved to `User Tutorials + `_. + + Checkout the `Compare weighting strategy with Inversion of surface Gravity Anomaly + Data `_ tutorial. + +""" diff --git a/tutorials/04-magnetics/plot_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_2a_magnetics_induced.py index d5b90274fd..ead80fc2a9 100644 --- a/tutorials/04-magnetics/plot_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_2a_magnetics_induced.py @@ -2,234 +2,12 @@ Forward Simulation of Total Magnetic Intensity Data =================================================== -Here we use the module *simpeg.potential_fields.magnetics* to predict magnetic -data for a magnetic susceptibility model. We simulate the data on a tensor mesh. -For this tutorial, we focus on the following: +.. important:: - - How to define the survey - - How to predict magnetic data for a susceptibility model - - How to include surface topography - - The units of the physical property model and resulting data + This tutorial has been moved to `User Tutorials + `_. + Checkout the `3D Forward Simulation of TMI Data + `_ tutorial. """ - -######################################################################### -# Import Modules -# -------------- -# - -import numpy as np -from scipy.interpolate import LinearNDInterpolator -import matplotlib as mpl -import matplotlib.pyplot as plt -import os - -from discretize import TensorMesh -from discretize.utils import mkvc, active_from_xyz -from simpeg.utils import plot2Ddata, model_builder -from simpeg import maps -from simpeg.potential_fields import magnetics - -write_output = False - -# sphinx_gallery_thumbnail_number = 2 - - -############################################# -# Topography -# ---------- -# -# Surface topography is defined as an (N, 3) numpy array. We create it here but -# topography could also be loaded from a file. -# - -[x_topo, y_topo] = np.meshgrid(np.linspace(-200, 200, 41), np.linspace(-200, 200, 41)) -z_topo = -15 * np.exp(-(x_topo**2 + y_topo**2) / 80**2) -x_topo, y_topo, z_topo = mkvc(x_topo), mkvc(y_topo), mkvc(z_topo) -xyz_topo = np.c_[x_topo, y_topo, z_topo] - -############################################# -# Defining the Survey -# ------------------- -# -# Here, we define survey that will be used for the simulation. Magnetic -# surveys are simple to create. The user only needs an (N, 3) array to define -# the xyz locations of the observation locations, the list of field components -# which are to be modeled and the properties of the Earth's field. -# - -# Define the observation locations as an (N, 3) numpy array or load them. -x = np.linspace(-80.0, 80.0, 17) -y = np.linspace(-80.0, 80.0, 17) -x, y = np.meshgrid(x, y) -x, y = mkvc(x.T), mkvc(y.T) -fun_interp = LinearNDInterpolator(np.c_[x_topo, y_topo], z_topo) -z = fun_interp(np.c_[x, y]) + 10 # Flight height 10 m above surface. -receiver_locations = np.c_[x, y, z] - -# Define the component(s) of the field we want to simulate as a list of strings. -# Here we simulation total magnetic intensity data. -components = ["tmi"] - -# Use the observation locations and components to define the receivers. To -# simulate data, the receivers must be defined as a list. -receiver_list = magnetics.receivers.Point(receiver_locations, components=components) - -receiver_list = [receiver_list] - -# Define the inducing field H0 = (intensity [nT], inclination [deg], declination [deg]) -inclination = 90 -declination = 0 -strength = 50000 - -source_field = magnetics.sources.UniformBackgroundField( - receiver_list=receiver_list, - amplitude=strength, - inclination=inclination, - declination=declination, -) - -# Define the survey -survey = magnetics.survey.Survey(source_field) - - -############################################# -# Defining a Tensor Mesh -# ---------------------- -# -# Here, we create the tensor mesh that will be used for the forward simulation. -# - -dh = 5.0 -hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hy = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hz = [(dh, 5, -1.3), (dh, 15)] -mesh = TensorMesh([hx, hy, hz], "CCN") - - -############################################# -# Defining a Susceptibility Model -# ------------------------------- -# -# Here, we create the model that will be used to predict magnetic data -# and the mapping from the model to the mesh. The model -# consists of a susceptible sphere in a less susceptible host. -# - -# Define susceptibility values for each unit in SI -background_susceptibility = 0.0001 -sphere_susceptibility = 0.01 - -# Find cells that are active in the forward modeling (cells below surface) -ind_active = active_from_xyz(mesh, xyz_topo) - -# Define mapping from model to active cells -nC = int(ind_active.sum()) -model_map = maps.IdentityMap(nP=nC) # model is a vlue for each active cell - -# Define model. Models in SimPEG are vector arrays -model = background_susceptibility * np.ones(ind_active.sum()) -ind_sphere = model_builder.get_indices_sphere( - np.r_[0.0, 0.0, -45.0], 15.0, mesh.cell_centers -) -ind_sphere = ind_sphere[ind_active] -model[ind_sphere] = sphere_susceptibility - -# Plot Model -fig = plt.figure(figsize=(9, 4)) - -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) -ax1 = fig.add_axes([0.1, 0.12, 0.73, 0.78]) -mesh.plot_slice( - plotting_map * model, - normal="Y", - ax=ax1, - ind=int(mesh.shape_cells[1] / 2), - grid=True, - clim=(np.min(model), np.max(model)), -) -ax1.set_title("Model slice at y = 0 m") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") - -ax2 = fig.add_axes([0.85, 0.12, 0.05, 0.78]) -norm = mpl.colors.Normalize(vmin=np.min(model), vmax=np.max(model)) -cbar = mpl.colorbar.ColorbarBase(ax2, norm=norm, orientation="vertical") -cbar.set_label("Magnetic Susceptibility (SI)", rotation=270, labelpad=15, size=12) - -plt.show() - - -################################################################### -# Simulation: TMI Data for a Susceptibility Model -# ----------------------------------------------- -# -# Here we demonstrate how to predict magnetic data for a magnetic -# susceptibility model using the integral formulation. -# - -# Define the forward simulation. By setting the 'store_sensitivities' keyword -# argument to "forward_only", we simulate the data without storing the sensitivities -simulation = magnetics.simulation.Simulation3DIntegral( - survey=survey, - mesh=mesh, - model_type="scalar", - chiMap=model_map, - ind_active=ind_active, - store_sensitivities="forward_only", -) - -# Compute predicted data for a susceptibility model -dpred = simulation.dpred(model) - -# Plot -fig = plt.figure(figsize=(6, 5)) -v_max = np.max(np.abs(dpred)) - -ax1 = fig.add_axes([0.1, 0.1, 0.8, 0.85]) -plot2Ddata( - receiver_list[0].locations, - dpred, - ax=ax1, - ncontour=30, - clim=(-v_max, v_max), - contourOpts={"cmap": "bwr"}, -) -ax1.set_title("TMI Anomaly") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("y (m)") - -ax2 = fig.add_axes([0.87, 0.1, 0.03, 0.85]) -norm = mpl.colors.Normalize(vmin=-np.max(np.abs(dpred)), vmax=np.max(np.abs(dpred))) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.bwr -) -cbar.set_label("$nT$", rotation=270, labelpad=15, size=12) - -plt.show() - - -####################################################### -# Optional: Export Data -# --------------------- -# -# Write the data and topography -# - -if write_output: - dir_path = os.path.dirname(__file__).split(os.path.sep) - dir_path.extend(["outputs"]) - dir_path = os.path.sep.join(dir_path) + os.path.sep - - if not os.path.exists(dir_path): - os.mkdir(dir_path) - - fname = dir_path + "magnetics_topo.txt" - np.savetxt(fname, np.c_[xyz_topo], fmt="%.4e") - - np.random.seed(211) - maximum_anomaly = np.max(np.abs(dpred)) - noise = 0.02 * maximum_anomaly * np.random.randn(len(dpred)) - fname = dir_path + "magnetics_data.obs" - np.savetxt(fname, np.c_[receiver_locations, dpred + noise], fmt="%.4e") diff --git a/tutorials/04-magnetics/plot_2b_magnetics_mvi.py b/tutorials/04-magnetics/plot_2b_magnetics_mvi.py index 0007708f83..95cbad69b4 100644 --- a/tutorials/04-magnetics/plot_2b_magnetics_mvi.py +++ b/tutorials/04-magnetics/plot_2b_magnetics_mvi.py @@ -2,294 +2,12 @@ Forward Simulation of Gradiometry Data for Magnetic Vector Models ================================================================= -Here we use the module *simpeg.potential_fields.magnetics* to predict magnetic -gradiometry data for magnetic vector models. The simulation is performed on a -Tree mesh. For this tutorial, we focus on the following: +.. important:: - - How to define the survey when we want to measured multiple field components - - How to predict magnetic data in the case of remanence - - How to include surface topography - - How to construct tree meshes based on topography and survey geometry - - The units of the physical property model and resulting data + This tutorial has been moved to `User Tutorials + `_. + Checkout the `3D Forward Simulation of Magnetic Gradiometry Data for Magnetic Vector + Models `_ tutorial. """ - -######################################################################### -# Import Modules -# -------------- -# - -import numpy as np -from scipy.interpolate import LinearNDInterpolator -import matplotlib as mpl -import matplotlib.pyplot as plt - -from discretize import TreeMesh -from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz -from simpeg.utils import plot2Ddata, model_builder, mat_utils -from simpeg import maps -from simpeg.potential_fields import magnetics - -# sphinx_gallery_thumbnail_number = 2 - - -############################################# -# Topography -# ---------- -# -# Here we define surface topography as an (N, 3) numpy array. Topography could -# also be loaded from a file. -# - -[x_topo, y_topo] = np.meshgrid(np.linspace(-200, 200, 41), np.linspace(-200, 200, 41)) -z_topo = -15 * np.exp(-(x_topo**2 + y_topo**2) / 80**2) -x_topo, y_topo, z_topo = mkvc(x_topo), mkvc(y_topo), mkvc(z_topo) -xyz_topo = np.c_[x_topo, y_topo, z_topo] - -############################################# -# Defining the Survey -# ------------------- -# -# Here, we define survey that will be used for the simulation. Magnetic -# surveys are simple to create. The user only needs an (N, 3) array to define -# the xyz locations of the observation locations, the list of field components -# which are to be modeled and the properties of the Earth's field. -# - -# Define the observation locations as an (N, 3) numpy array or load them. -x = np.linspace(-80.0, 80.0, 17) -y = np.linspace(-80.0, 80.0, 17) -x, y = np.meshgrid(x, y) -x, y = mkvc(x.T), mkvc(y.T) -fun_interp = LinearNDInterpolator(np.c_[x_topo, y_topo], z_topo) -z = fun_interp(np.c_[x, y]) + 10 # Flight height 10 m above surface. -receiver_locations = np.c_[x, y, z] - -# Define the component(s) of the field we want to simulate as strings within -# a list. Here we measure the x, y and z derivatives of the Bz anomaly at -# each observation location. -components = ["bxz", "byz", "bzz"] - -# Use the observation locations and components to define the receivers. To -# simulate data, the receivers must be defined as a list. -receiver_list = magnetics.receivers.Point(receiver_locations, components=components) - -receiver_list = [receiver_list] - -# Define the inducing field H0 = (intensity [nT], inclination [deg], declination [deg]) -field_inclination = 60 -field_declination = 30 -field_strength = 50000 - -source_field = magnetics.sources.UniformBackgroundField( - receiver_list=receiver_list, - amplitude=field_strength, - inclination=field_inclination, - declination=field_declination, -) - -# Define the survey -survey = magnetics.survey.Survey(source_field) - - -########################################################## -# Defining an OcTree Mesh -# ----------------------- -# -# Here, we create the OcTree mesh that will be used to predict magnetic -# gradiometry data for the forward simuulation. -# - -dx = 5 # minimum cell width (base mesh cell width) in x -dy = 5 # minimum cell width (base mesh cell width) in y -dz = 5 # minimum cell width (base mesh cell width) in z - -x_length = 240.0 # domain width in x -y_length = 240.0 # domain width in y -z_length = 120.0 # domain width in y - -# Compute number of base mesh cells required in x and y -nbcx = 2 ** int(np.round(np.log(x_length / dx) / np.log(2.0))) -nbcy = 2 ** int(np.round(np.log(y_length / dy) / np.log(2.0))) -nbcz = 2 ** int(np.round(np.log(z_length / dz) / np.log(2.0))) - -# Define the base mesh -hx = [(dx, nbcx)] -hy = [(dy, nbcy)] -hz = [(dz, nbcz)] -mesh = TreeMesh([hx, hy, hz], x0="CCN") - -# Refine based on surface topography -mesh = refine_tree_xyz( - mesh, xyz_topo, octree_levels=[2, 2], method="surface", finalize=False -) - -# Refine box base on region of interest -xp, yp, zp = np.meshgrid([-100.0, 100.0], [-100.0, 100.0], [-80.0, 0.0]) -xyz = np.c_[mkvc(xp), mkvc(yp), mkvc(zp)] - -mesh = refine_tree_xyz(mesh, xyz, octree_levels=[2, 2], method="box", finalize=False) - -mesh.finalize() - -########################################################## -# Create Magnetic Vector Intensity Model (MVI) -# -------------------------------------------- -# -# Magnetic vector models are defined by three-component effective -# susceptibilities. To create a magnetic vector -# model, we must -# -# 1) Define the magnetic susceptibility for each cell. Then multiply by the -# unit vector direction of the inducing field. (induced contribution) -# 2) Define the remanent magnetization vector for each cell and normalize -# by the magnitude of the Earth's field (remanent contribution) -# 3) Sum the induced and remanent contributions -# 4) Define as a vector np.r_[chi_1, chi_2, chi_3] -# -# - -# Define susceptibility values for each unit in SI -background_susceptibility = 0.0001 -sphere_susceptibility = 0.01 - -# Find cells active in the forward modeling (cells below surface) -ind_active = active_from_xyz(mesh, xyz_topo) - -# Define mapping from model to active cells -nC = int(ind_active.sum()) -model_map = maps.IdentityMap(nP=3 * nC) # model has 3 parameters for each cell - -# Define susceptibility for each cell -susceptibility_model = background_susceptibility * np.ones(ind_active.sum()) -ind_sphere = model_builder.get_indices_sphere(np.r_[0.0, 0.0, -45.0], 15.0, mesh.gridCC) -ind_sphere = ind_sphere[ind_active] -susceptibility_model[ind_sphere] = sphere_susceptibility - -# Compute the unit direction of the inducing field in Cartesian coordinates -field_direction = mat_utils.dip_azimuth2cartesian(field_inclination, field_declination) - -# Multiply susceptibility model to obtain the x, y, z components of the -# effective susceptibility contribution from induced magnetization. -susceptibility_model = np.outer(susceptibility_model, field_direction) - -# Define the effective susceptibility contribution for remanent magnetization to have a -# magnitude of 0.006 SI, with inclination -45 and declination 90 -remanence_inclination = -45.0 -remanence_declination = 90.0 -remanence_susceptibility = 0.01 - -remanence_model = np.zeros(np.shape(susceptibility_model)) -effective_susceptibility_sphere = ( - remanence_susceptibility - * mat_utils.dip_azimuth2cartesian(remanence_inclination, remanence_declination) -) -remanence_model[ind_sphere, :] = effective_susceptibility_sphere - -# Define effective susceptibility model as a vector np.r_[chi_x, chi_y, chi_z] -plotting_model = susceptibility_model + remanence_model -model = mkvc(plotting_model) - -# Plot Effective Susceptibility Model -fig = plt.figure(figsize=(9, 4)) - -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) -plotting_model = np.sqrt(np.sum(plotting_model, axis=1) ** 2) -ax1 = fig.add_axes([0.1, 0.12, 0.73, 0.78]) -mesh.plot_slice( - plotting_map * plotting_model, - normal="Y", - ax=ax1, - ind=int(mesh.h[1].size / 2), - grid=True, - clim=(np.min(plotting_model), np.max(plotting_model)), -) -ax1.set_title("MVI Model at y = 0 m") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") - -ax2 = fig.add_axes([0.85, 0.12, 0.05, 0.78]) -norm = mpl.colors.Normalize(vmin=np.min(plotting_model), vmax=np.max(plotting_model)) -cbar = mpl.colorbar.ColorbarBase(ax2, norm=norm, orientation="vertical") -cbar.set_label( - "Effective Susceptibility Amplitude (SI)", rotation=270, labelpad=15, size=12 -) - - -################################################################### -# Simulation: Gradiometry Data for an MVI Model -# --------------------------------------------- -# -# Here we predict magnetic gradiometry data for an effective susceptibility model -# in the case of remanent magnetization. -# - -# Define the forward simulation. By setting the 'store_sensitivities' keyword -# argument to "forward_only", we simulate the data without storing the sensitivities -simulation = magnetics.simulation.Simulation3DIntegral( - survey=survey, - mesh=mesh, - chiMap=model_map, - ind_active=ind_active, - model_type="vector", - store_sensitivities="forward_only", -) - -# Compute predicted data for some model -dpred = simulation.dpred(model) -n_data = len(dpred) - -# Plot -fig = plt.figure(figsize=(13, 4)) -v_max = np.max(np.abs(dpred)) - -ax1 = fig.add_axes([0.1, 0.15, 0.25, 0.78]) -plot2Ddata( - receiver_list[0].locations, - dpred[0:n_data:3], - ax=ax1, - ncontour=30, - clim=(-v_max, v_max), - contourOpts={"cmap": "bwr"}, -) -ax1.set_title("$dBz/dx$") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("y (m)") - -ax2 = fig.add_axes([0.36, 0.15, 0.25, 0.78]) -cplot2 = plot2Ddata( - receiver_list[0].locations, - dpred[1:n_data:3], - ax=ax2, - ncontour=30, - clim=(-v_max, v_max), - contourOpts={"cmap": "bwr"}, -) -cplot2[0].set_clim((-v_max, v_max)) -ax2.set_title("$dBz/dy$") -ax2.set_xlabel("x (m)") -ax2.set_yticks([]) - -ax3 = fig.add_axes([0.62, 0.15, 0.25, 0.78]) -cplot3 = plot2Ddata( - receiver_list[0].locations, - dpred[2:n_data:3], - ax=ax3, - ncontour=30, - clim=(-v_max, v_max), - contourOpts={"cmap": "bwr"}, -) -cplot3[0].set_clim((-v_max, v_max)) -ax3.set_title("$dBz/dz$") -ax3.set_xlabel("x (m)") -ax3.set_yticks([]) - -ax4 = fig.add_axes([0.88, 0.15, 0.02, 0.79]) -norm = mpl.colors.Normalize(vmin=-v_max, vmax=v_max) -cbar = mpl.colorbar.ColorbarBase( - ax4, norm=norm, orientation="vertical", cmap=mpl.cm.bwr -) -cbar.set_label("$nT/m$", rotation=270, labelpad=15, size=12) - -plt.show() diff --git a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py index 24430e337a..403f9c9598 100644 --- a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py @@ -2,454 +2,14 @@ Sparse Norm Inversion for Total Magnetic Intensity Data on a Tensor Mesh ======================================================================== -Here we invert total magnetic intensity (TMI) data to recover a magnetic -susceptibility model. We formulate the inverse problem as an iteratively -re-weighted least-squares (IRLS) optimization problem. For this tutorial, we -focus on the following: +.. important:: - - Defining the survey from xyz formatted data - - Generating a mesh based on survey geometry - - Including surface topography - - Defining the inverse problem (data misfit, regularization, optimization) - - Specifying directives for the inversion - - Setting sparse and blocky norms - - Plotting the recovered model and data misfit - -Although we consider TMI data in this tutorial, the same approach -can be used to invert other types of geophysical data. + This tutorial has been moved to `User Tutorials + `_. + Checkout the `Iteratively Re-weighted Least-Squares Inversion + `_ + section of the `3D Inversion of TMI Data to Recover a Susceptibility Model Models + `_ tutorial. """ - -######################################################################### -# Import modules -# -------------- -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -import tarfile - -from discretize import TensorMesh -from discretize.utils import active_from_xyz -from simpeg.potential_fields import magnetics -from simpeg.utils import plot2Ddata, model_builder -from simpeg import ( - maps, - data, - inverse_problem, - data_misfit, - regularization, - optimization, - directives, - inversion, - utils, -) - -# sphinx_gallery_thumbnail_number = 3 - -############################################# -# Load Data and Plot -# ------------------ -# -# File paths for assets we are loading. To set up the inversion, we require -# topography and field observations. The true model defined on the whole mesh -# is loaded to compare with the inversion result. These files are stored as a -# tar-file on our google cloud bucket: -# "https://storage.googleapis.com/simpeg/doc-assets/magnetics.tar.gz" -# - -# storage bucket where we have the data -data_source = "https://storage.googleapis.com/simpeg/doc-assets/magnetics.tar.gz" - -# download the data -downloaded_data = utils.download(data_source, overwrite=True) - -# unzip the tarfile -tar = tarfile.open(downloaded_data, "r") -tar.extractall() -tar.close() - -# path to the directory containing our data -dir_path = downloaded_data.split(".")[0] + os.path.sep - -# files to work with -topo_filename = dir_path + "magnetics_topo.txt" -data_filename = dir_path + "magnetics_data.obs" - - -############################################# -# Load Data and Plot -# ------------------ -# -# Here we load and plot synthetic TMI data. Topography is generally -# defined as an (N, 3) array. TMI data is generally defined with 4 columns: -# x, y, z and data. -# - -topo_xyz = np.loadtxt(str(topo_filename)) -dobs = np.loadtxt(str(data_filename)) - -receiver_locations = dobs[:, 0:3] -dobs = dobs[:, -1] - -# Plot -fig = plt.figure(figsize=(6, 5)) -v_max = np.max(np.abs(dobs)) - -ax1 = fig.add_axes([0.1, 0.1, 0.75, 0.85]) -plot2Ddata( - receiver_locations, - dobs, - ax=ax1, - ncontour=30, - clim=(-v_max, v_max), - contourOpts={"cmap": "bwr"}, -) -ax1.set_title("TMI Anomaly") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("y (m)") - -ax2 = fig.add_axes([0.85, 0.05, 0.05, 0.9]) -norm = mpl.colors.Normalize(vmin=-np.max(np.abs(dobs)), vmax=np.max(np.abs(dobs))) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.bwr -) -cbar.set_label("$nT$", rotation=270, labelpad=15, size=12) - -plt.show() - -############################################# -# Assign Uncertainty -# ------------------ -# -# Inversion with SimPEG requires that we define standard deviation on our data. -# This represents our estimate of the noise in our data. For magnetic inversions, -# a constant floor value is generall applied to all data. For this tutorial, the -# standard deviation on each datum will be 2% of the maximum observed magnetics -# anomaly value. -# - -maximum_anomaly = np.max(np.abs(dobs)) - -std = 0.02 * maximum_anomaly * np.ones(len(dobs)) - -############################################# -# Defining the Survey -# ------------------- -# -# Here, we define survey that will be used for the simulation. Magnetic -# surveys are simple to create. The user only needs an (N, 3) array to define -# the xyz locations of the observation locations, the list of field components -# which are to be modeled and the properties of the Earth's field. -# - -# Define the component(s) of the field we are inverting as a list. Here we will -# invert total magnetic intensity data. -components = ["tmi"] - -# Use the observation locations and components to define the receivers. To -# simulate data, the receivers must be defined as a list. -receiver_list = magnetics.receivers.Point(receiver_locations, components=components) - -receiver_list = [receiver_list] - -# Define the inducing field H0 = (intensity [nT], inclination [deg], declination [deg]) -inclination = 90 -declination = 0 -strength = 50000 - -source_field = magnetics.sources.UniformBackgroundField( - receiver_list=receiver_list, - amplitude=strength, - inclination=inclination, - declination=declination, -) - -# Define the survey -survey = magnetics.survey.Survey(source_field) - -############################################# -# Defining the Data -# ----------------- -# -# Here is where we define the data that is inverted. The data is defined by -# the survey, the observation values and the standard deviations. -# - -data_object = data.Data(survey, dobs=dobs, standard_deviation=std) - - -############################################# -# Defining a Tensor Mesh -# ---------------------- -# -# Here, we create the tensor mesh that will be used to invert TMI data. -# If desired, we could define an OcTree mesh. -# - -dh = 5.0 -hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hy = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] -hz = [(dh, 5, -1.3), (dh, 15)] -mesh = TensorMesh([hx, hy, hz], "CCN") - -######################################################## -# Starting/Reference Model and Mapping on Tensor Mesh -# --------------------------------------------------- -# -# Here, we would create starting and/or reference models for the inversion as -# well as the mapping from the model space to the active cells. Starting and -# reference models can be a constant background value or contain a-priori -# structures. Here, the background is 1e-4 SI. -# - -# Define background susceptibility model in SI. Don't make this 0! -# Otherwise the gradient for the 1st iteration is zero and the inversion will -# not converge. -background_susceptibility = 1e-4 - -# Find the indecies of the active cells in forward model (ones below surface) -active_cells = active_from_xyz(mesh, topo_xyz) - -# Define mapping from model to active cells -nC = int(active_cells.sum()) -model_map = maps.IdentityMap(nP=nC) # model consists of a value for each cell - -# Define starting model -starting_model = background_susceptibility * np.ones(nC) - -############################################## -# Define the Physics -# ------------------ -# -# Here, we define the physics of the magnetics problem by using the simulation -# class. -# - -# Define the problem. Define the cells below topography and the mapping -simulation = magnetics.simulation.Simulation3DIntegral( - survey=survey, - mesh=mesh, - model_type="scalar", - chiMap=model_map, - ind_active=active_cells, -) - - -####################################################################### -# Define Inverse Problem -# ---------------------- -# -# The inverse problem is defined by 3 things: -# -# 1) Data Misfit: a measure of how well our recovered model explains the field data -# 2) Regularization: constraints placed on the recovered model and a priori information -# 3) Optimization: the numerical approach used to solve the inverse problem -# - -# Define the data misfit. Here the data misfit is the L2 norm of the weighted -# residual between the observed data and the data predicted for a given model. -# Within the data misfit, the residual between predicted and observed data are -# normalized by the data's standard deviation. -dmis = data_misfit.L2DataMisfit(data=data_object, simulation=simulation) - -# Define the regularization (model objective function) -reg = regularization.Sparse( - mesh, - active_cells=active_cells, - mapping=model_map, - reference_model=starting_model, - gradient_type="total", -) - -# Define sparse and blocky norms p, qx, qy, qz -reg.norms = [0, 0, 0, 0] - -# Define how the optimization problem is solved. Here we will use a projected -# Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.ProjectedGNCG( - maxIter=20, lower=0.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=1e-3 -) - -# Here we define the inverse problem that is to be solved -inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) - -####################################################################### -# Define Inversion Directives -# --------------------------- -# -# Here we define any directives that are carried out during the inversion. This -# includes the cooling schedule for the trade-off parameter (beta), stopping -# criteria for the inversion and saving inversion results at each iteration. -# - -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=5) - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# Defines the directives for the IRLS regularization. This includes setting -# the cooling schedule for the trade-off parameter. -update_IRLS = directives.Update_IRLS( - f_min_change=1e-4, - max_irls_iterations=30, - coolEpsFact=1.5, - beta_tol=1e-2, -) - -# Updating the preconditioner if it is model dependent. -update_jacobi = directives.UpdatePreconditioner() - -# Setting a stopping criteria for the inversion. -target_misfit = directives.TargetMisfit(chifact=1) - -# Add sensitivity weights -sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) - -# The directives are defined as a list. -directives_list = [ - sensitivity_weights, - starting_beta, - save_iteration, - update_IRLS, - update_jacobi, -] - -##################################################################### -# Running the Inversion -# --------------------- -# -# To define the inversion object, we need to define the inversion problem and -# the set of directives. We can then run the inversion. -# - -# Here we combine the inverse problem and the set of directives -inv = inversion.BaseInversion(inv_prob, directives_list) - -# Print target misfit to compare with convergence -# print("Target misfit is " + str(target_misfit.target)) - -# Run the inversion -recovered_model = inv.run(starting_model) - -############################################################## -# Recreate True Model -# ------------------- -# - - -background_susceptibility = 0.0001 -sphere_susceptibility = 0.01 - -true_model = background_susceptibility * np.ones(nC) -ind_sphere = model_builder.get_indices_sphere( - np.r_[0.0, 0.0, -45.0], 15.0, mesh.cell_centers -) -ind_sphere = ind_sphere[active_cells] -true_model[ind_sphere] = sphere_susceptibility - - -############################################################ -# Plotting True Model and Recovered Model -# --------------------------------------- -# - -# Plot True Model -fig = plt.figure(figsize=(9, 4)) -plotting_map = maps.InjectActiveCells(mesh, active_cells, np.nan) - -ax1 = fig.add_axes([0.08, 0.1, 0.75, 0.8]) -mesh.plot_slice( - plotting_map * true_model, - normal="Y", - ax=ax1, - ind=int(mesh.shape_cells[1] / 2), - grid=True, - clim=(np.min(true_model), np.max(true_model)), - pcolor_opts={"cmap": "viridis"}, -) -ax1.set_title("Model slice at y = 0 m") - -ax2 = fig.add_axes([0.85, 0.1, 0.05, 0.8]) -norm = mpl.colors.Normalize(vmin=np.min(true_model), vmax=np.max(true_model)) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.viridis, format="%.1e" -) -cbar.set_label("SI", rotation=270, labelpad=15, size=12) - -plt.show() - -# Plot Recovered Model -fig = plt.figure(figsize=(9, 4)) -plotting_map = maps.InjectActiveCells(mesh, active_cells, np.nan) - -ax1 = fig.add_axes([0.08, 0.1, 0.75, 0.8]) -mesh.plot_slice( - plotting_map * recovered_model, - normal="Y", - ax=ax1, - ind=int(mesh.shape_cells[1] / 2), - grid=True, - clim=(np.min(recovered_model), np.max(recovered_model)), - pcolor_opts={"cmap": "viridis"}, -) -ax1.set_title("Model slice at y = 0 m") - -ax2 = fig.add_axes([0.85, 0.1, 0.05, 0.8]) -norm = mpl.colors.Normalize(vmin=np.min(recovered_model), vmax=np.max(recovered_model)) -cbar = mpl.colorbar.ColorbarBase( - ax2, norm=norm, orientation="vertical", cmap=mpl.cm.viridis, format="%.1e" -) -cbar.set_label("SI", rotation=270, labelpad=15, size=12) - -plt.show() - -################################################################### -# Plotting Predicted Data and Misfit -# ---------------------------------- -# - -# Predicted data with final recovered model -dpred = inv_prob.dpred - -# Observed data | Predicted data | Normalized data misfit -data_array = np.c_[dobs, dpred, (dobs - dpred) / std] - -fig = plt.figure(figsize=(17, 4)) -plot_title = ["Observed", "Predicted", "Normalized Misfit"] -plot_units = ["nT", "nT", ""] - -ax1 = 3 * [None] -ax2 = 3 * [None] -norm = 3 * [None] -cbar = 3 * [None] -cplot = 3 * [None] -v_lim = [np.max(np.abs(dobs)), np.max(np.abs(dobs)), np.max(np.abs(data_array[:, 2]))] - -for ii in range(0, 3): - ax1[ii] = fig.add_axes([0.33 * ii + 0.03, 0.11, 0.25, 0.84]) - cplot[ii] = plot2Ddata( - receiver_list[0].locations, - data_array[:, ii], - ax=ax1[ii], - ncontour=30, - clim=(-v_lim[ii], v_lim[ii]), - contourOpts={"cmap": "bwr"}, - ) - ax1[ii].set_title(plot_title[ii]) - ax1[ii].set_xlabel("x (m)") - ax1[ii].set_ylabel("y (m)") - - ax2[ii] = fig.add_axes([0.33 * ii + 0.27, 0.11, 0.01, 0.84]) - norm[ii] = mpl.colors.Normalize(vmin=-v_lim[ii], vmax=v_lim[ii]) - cbar[ii] = mpl.colorbar.ColorbarBase( - ax2[ii], norm=norm[ii], orientation="vertical", cmap=mpl.cm.bwr - ) - cbar[ii].set_label(plot_units[ii], rotation=270, labelpad=15, size=12) - -plt.show() diff --git a/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py b/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py index 7ea20813c7..f4c1c78023 100644 --- a/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py +++ b/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py @@ -3,169 +3,13 @@ Simulate a 1D Sounding over a Layered Earth =========================================== -Here we use the module *simpeg.electromangetics.static.resistivity* to predict -sounding data over a 1D layered Earth. In this tutorial, we focus on the following: +.. important:: - - General definition of sources and receivers - - How to define the survey - - How to predict voltage or apparent resistivity data - - The units of the model and resulting data + This tutorial has been moved to `User Tutorials + `_. -For this tutorial, we will simulate sounding data over a layered Earth using -a Wenner array. The end product is a sounding curve which tells us how the -electrical resistivity changes with depth. + Checkout the `1D Forward Simulation for a Single Sounding + `_ tutorial. """ - -######################################################################### -# Import modules -# -------------- -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt - -from simpeg import maps -from simpeg.electromagnetics.static import resistivity as dc -from simpeg.utils import plot_1d_layer_model - -mpl.rcParams.update({"font.size": 16}) - -write_output = False - -# sphinx_gallery_thumbnail_number = 2 - - -##################################################################### -# Create Survey -# ------------- -# -# Here we demonstrate a general way to define sources and receivers. -# For pole and dipole sources, we must define the A or AB electrode locations, -# respectively. For the pole and dipole receivers, we must define the M or -# MN electrode locations, respectively. -# - -a_min = 20.0 -a_max = 500.0 -n_stations = 25 - -# Define the 'a' spacing for Wenner array measurements for each reading -electrode_separations = np.linspace(a_min, a_max, n_stations) - -source_list = [] # create empty array for sources to live - -for ii in range(0, len(electrode_separations)): - # Extract separation parameter for sources and receivers - a = electrode_separations[ii] - - # AB electrode locations for source. Each is a (1, 3) numpy array - A_location = np.r_[-1.5 * a, 0.0, 0.0] - B_location = np.r_[1.5 * a, 0.0, 0.0] - - # MN electrode locations for receivers. Each is an (N, 3) numpy array - M_location = np.r_[-0.5 * a, 0.0, 0.0] - N_location = np.r_[0.5 * a, 0.0, 0.0] - - # Create receivers list. Define as pole or dipole. - receiver_list = dc.receivers.Dipole( - M_location, N_location, data_type="apparent_resistivity" - ) - receiver_list = [receiver_list] - - # Define the source properties and associated receivers - source_list.append(dc.sources.Dipole(receiver_list, A_location, B_location)) - -# Define survey -survey = dc.Survey(source_list) - - -############################################### -# Defining a 1D Layered Earth Model -# --------------------------------- -# -# Here, we define the layer thicknesses and electrical resistivities for our -# 1D simulation. If we have N layers, we define N electrical resistivity -# values and N-1 layer thicknesses. The lowest layer is assumed to extend to -# infinity. In the case of a halfspace, the layer thicknesses would be -# an empty array. -# - -# Define layer thicknesses. -layer_thicknesses = np.r_[100.0, 100.0] - -# Define layer resistivities. -model = np.r_[1e3, 4e3, 2e2] - -# Define mapping from model to 1D layers. -model_map = maps.IdentityMap(nP=len(model)) - -############################################################### -# Plot Resistivity Model -# ---------------------- -# -# Here we plot the 1D resistivity model. -# - -# Plot the 1D model -ax = plot_1d_layer_model(layer_thicknesses, model_map * model) -ax.set_xlabel(r"Resistivity ($\Omega m$)") - -####################################################################### -# Define the Forward Simulation and Predict DC Resistivity Data -# ------------------------------------------------------------- -# -# Here we predict DC resistivity data. If the keyword argument *rhoMap* is -# defined, the simulation will expect a resistivity model. If the keyword -# argument *sigmaMap* is defined, the simulation will expect a conductivity model. -# - -simulation = dc.simulation_1d.Simulation1DLayers( - survey=survey, - rhoMap=model_map, - thicknesses=layer_thicknesses, -) - -# Predict data for a given model -dpred = simulation.dpred(model) - -# Plot apparent resistivities on sounding curve -fig = plt.figure(figsize=(11, 5)) -ax1 = fig.add_axes([0.1, 0.1, 0.75, 0.85]) -ax1.semilogy(1.5 * electrode_separations, dpred, "b") -ax1.set_xlabel("AB/2 (m)") -ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)") -plt.show() - - -######################################################################### -# Optional: Export Data -# --------------------- -# -# Export data and true model -# - -if write_output: - dir_path = os.path.dirname(__file__).split(os.path.sep) - dir_path.extend(["outputs"]) - dir_path = os.path.sep.join(dir_path) + os.path.sep - - if not os.path.exists(dir_path): - os.mkdir(dir_path) - - np.random.seed(145) - noise = 0.025 * dpred * np.random.randn(len(dpred)) - - data_array = np.c_[ - survey.locations_a, - survey.locations_b, - survey.locations_m, - survey.locations_n, - dpred + noise, - ] - - fname = dir_path + "app_res_1d_data.dobs" - np.savetxt(fname, data_array, fmt="%.4e") diff --git a/tutorials/05-dcr/plot_fwd_2_dcr2d.py b/tutorials/05-dcr/plot_fwd_2_dcr2d.py index 8095504f90..c1ca6be0f1 100644 --- a/tutorials/05-dcr/plot_fwd_2_dcr2d.py +++ b/tutorials/05-dcr/plot_fwd_2_dcr2d.py @@ -3,327 +3,13 @@ DC Resistivity Forward Simulation in 2.5D ========================================= -Here we use the module *simpeg.electromagnetics.static.resistivity* to predict -DC resistivity data and plot using a pseudosection. In this tutorial, we focus -on the following: +.. important:: - - How to define the survey - - How to define the forward simulation - - How to predict normalized voltage data for a synthetic conductivity model - - How to include surface topography - - The units of the model and resulting data + This tutorial has been moved to `User Tutorials + `_. + Checkout the `2.5D Forward Simulation + `_ tutorial. -""" - -######################################################################### -# Import modules -# -------------- -# - -from discretize import TreeMesh -from discretize.utils import mkvc, active_from_xyz - -from simpeg.utils import model_builder -from simpeg.utils.io_utils.io_utils_electromagnetics import write_dcip2d_ubc -from simpeg import maps, data -from simpeg.electromagnetics.static import resistivity as dc -from simpeg.electromagnetics.static.utils.static_utils import ( - generate_dcip_sources_line, - apparent_resistivity_from_voltage, - plot_pseudosection, -) - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -from matplotlib.colors import LogNorm - -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - -write_output = False -mpl.rcParams.update({"font.size": 16}) -# sphinx_gallery_thumbnail_number = 3 - - -############################################################### -# Defining Topography -# ------------------- -# -# Here we define surface topography as an (N, 3) numpy array. Topography could -# also be loaded from a file. In our case, our survey takes place within a set -# of valleys that run North-South. -# - -x_topo, y_topo = np.meshgrid( - np.linspace(-3000, 3000, 601), np.linspace(-3000, 3000, 101) -) -z_topo = 40.0 * np.sin(2 * np.pi * x_topo / 800) - 40.0 -x_topo, y_topo, z_topo = mkvc(x_topo), mkvc(y_topo), mkvc(z_topo) -topo_xyz = np.c_[x_topo, y_topo, z_topo] - -# Create 2D topography. Since our 3D topography only changes in the x direction, -# it is easy to define the 2D topography projected along the survey line. For -# arbitrary topography and for an arbitrary survey orientation, the user must -# define the 2D topography along the survey line. -topo_2d = np.unique(topo_xyz[:, [0, 2]], axis=0) - -##################################################################### -# Create Dipole-Dipole Survey -# --------------------------- -# -# Here we define a single EW survey line that uses a dipole-dipole configuration. -# For the source, we must define the AB electrode locations. For the receivers -# we must define the MN electrode locations. Instead of creating the survey -# from scratch (see 1D example), we will use the *generat_dcip_survey_line* utility. -# - -# Define survey line parameters -survey_type = "dipole-dipole" -dimension_type = "2D" -data_type = "volt" -end_locations = np.r_[-400.0, 400.0] -station_separation = 40.0 -num_rx_per_src = 10 - -# Generate source list for DC survey line -source_list = generate_dcip_sources_line( - survey_type, - data_type, - dimension_type, - end_locations, - topo_2d, - num_rx_per_src, - station_separation, -) - -# Define survey -survey = dc.survey.Survey(source_list, survey_type=survey_type) - -############################################################### -# Create Tree Mesh -# ------------------ -# -# Here, we create the Tree mesh that will be used to predict DC data. -# - -dh = 4 # base cell width -dom_width_x = 3200.0 # domain width x -dom_width_z = 2400.0 # domain width z -nbcx = 2 ** int(np.round(np.log(dom_width_x / dh) / np.log(2.0))) # num. base cells x -nbcz = 2 ** int(np.round(np.log(dom_width_z / dh) / np.log(2.0))) # num. base cells z - -# Define the base mesh -hx = [(dh, nbcx)] -hz = [(dh, nbcz)] -mesh = TreeMesh([hx, hz], x0="CN") - -# Mesh refinement based on topography -mesh.refine_surface( - topo_xyz[:, [0, 2]], - padding_cells_by_level=[0, 0, 4, 4], - finalize=False, -) - -# Mesh refinement near transmitters and receivers. First we need to obtain the -# set of unique electrode locations. -electrode_locations = np.c_[ - survey.locations_a, - survey.locations_b, - survey.locations_m, - survey.locations_n, -] - -unique_locations = np.unique( - np.reshape(electrode_locations, (4 * survey.nD, 2)), axis=0 -) - -mesh.refine_points(unique_locations, padding_cells_by_level=[4, 4], finalize=False) - -# Refine core mesh region -xp, zp = np.meshgrid([-600.0, 600.0], [-400.0, 0.0]) -xyz = np.c_[mkvc(xp), mkvc(zp)] -mesh.refine_bounding_box(xyz, padding_cells_by_level=[0, 0, 2, 8], finalize=False) - -mesh.finalize() - -############################################################### -# Create Conductivity Model and Mapping for Tree Mesh -# ----------------------------------------------------- -# -# It is important that electrodes are not modeled as being in the air. Even if the -# electrodes are properly located along surface topography, they may lie above -# the discretized topography. This step is carried out to ensure all electrodes -# lie on the discretized surface. -# - -# Define conductivity model in S/m (or resistivity model in Ohm m) -air_conductivity = 1e-8 -background_conductivity = 1e-2 -conductor_conductivity = 1e-1 -resistor_conductivity = 1e-3 - -# Find active cells in forward modeling (cell below surface) -ind_active = active_from_xyz(mesh, topo_xyz[:, [0, 2]]) -# Define mapping from model to active cells -nC = int(ind_active.sum()) -conductivity_map = maps.InjectActiveCells(mesh, ind_active, air_conductivity) - -# Define model -conductivity_model = background_conductivity * np.ones(nC) - -ind_conductor = model_builder.get_indices_sphere( - np.r_[-120.0, -160.0], 60.0, mesh.gridCC -) -ind_conductor = ind_conductor[ind_active] -conductivity_model[ind_conductor] = conductor_conductivity - -ind_resistor = model_builder.get_indices_sphere(np.r_[120.0, -100.0], 60.0, mesh.gridCC) -ind_resistor = ind_resistor[ind_active] -conductivity_model[ind_resistor] = resistor_conductivity - -# Plot Conductivity Model -fig = plt.figure(figsize=(9, 4)) - -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) -norm = LogNorm(vmin=1e-3, vmax=1e-1) - -ax1 = fig.add_axes([0.14, 0.17, 0.68, 0.7]) -mesh.plot_image( - plotting_map * conductivity_model, ax=ax1, grid=False, pcolor_opts={"norm": norm} -) -ax1.set_xlim(-600, 600) -ax1.set_ylim(-600, 0) -ax1.set_title("Conductivity Model") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") - -ax2 = fig.add_axes([0.84, 0.17, 0.03, 0.7]) -cbar = mpl.colorbar.ColorbarBase(ax2, norm=norm, orientation="vertical") -cbar.set_label(r"$\sigma$ (S/m)", rotation=270, labelpad=15, size=12) - -plt.show() - - -############################################################### -# Project Survey to Discretized Topography -# ---------------------------------------- -# -# It is important that electrodes are not model as being in the air. Even if the -# electrodes are properly located along surface topography, they may lie above -# the discretized topography. This step is carried out to ensure all electrodes -# like on the discretized surface. -# - -survey.drape_electrodes_on_topography(mesh, ind_active, option="top") - - -####################################################################### -# Predict DC Resistivity Data -# --------------------------- -# -# Here we predict DC resistivity data. If the keyword argument *sigmaMap* is -# defined, the simulation will expect a conductivity model. If the keyword -# argument *rhoMap* is defined, the simulation will expect a resistivity model. -# - -simulation = dc.simulation_2d.Simulation2DNodal( - mesh, survey=survey, sigmaMap=conductivity_map, solver=Solver -) - -# Predict the data by running the simulation. The data are the raw voltage in -# units of volts. -dpred = simulation.dpred(conductivity_model) - -####################################################################### -# Plotting in Pseudo-Section -# -------------------------- -# -# Here, we demonstrate how to plot 2D data in pseudo-section. -# First, we plot the voltages in pseudo-section as a scatter plot. This -# allows us to visualize the pseudo-sensitivity locations for our survey. -# Next, we plot the apparent conductivities in pseudo-section as a filled -# contour plot. -# - -# Plot voltages pseudo-section -fig = plt.figure(figsize=(12, 5)) -ax1 = fig.add_axes([0.1, 0.15, 0.75, 0.78]) -plot_pseudosection( - survey, - dobs=np.abs(dpred), - plot_type="scatter", - ax=ax1, - scale="log", - cbar_label="V/A", - scatter_opts={"cmap": mpl.cm.viridis}, -) -ax1.set_title("Normalized Voltages") -plt.show() - -# Get apparent conductivities from volts and survey geometry -apparent_conductivities = 1 / apparent_resistivity_from_voltage(survey, dpred) - -# Plot apparent conductivity pseudo-section -fig = plt.figure(figsize=(12, 5)) -ax1 = fig.add_axes([0.1, 0.15, 0.75, 0.78]) -plot_pseudosection( - survey, - dobs=apparent_conductivities, - plot_type="contourf", - ax=ax1, - scale="log", - cbar_label="S/m", - mask_topography=True, - contourf_opts={"levels": 20, "cmap": mpl.cm.viridis}, -) -ax1.set_title("Apparent Conductivity") -plt.show() - -####################################################################### -# Optional: Write out dpred -# ------------------------- -# -# Write DC resistivity data, topography and true model -# - -if write_output: - dir_path = os.path.dirname(__file__).split(os.path.sep) - dir_path.extend(["outputs"]) - dir_path = os.path.sep.join(dir_path) + os.path.sep - - if not os.path.exists(dir_path): - os.mkdir(dir_path) - - # Add 10% Gaussian noise to each datum - np.random.seed(225) - std = 0.05 * np.abs(dpred) - dc_noise = std * np.random.randn(len(dpred)) - dobs = dpred + dc_noise - - # Create a survey with the original electrode locations - # and not the shifted ones - # Generate source list for DC survey line - source_list = generate_dcip_sources_line( - survey_type, - data_type, - dimension_type, - end_locations, - topo_xyz, - num_rx_per_src, - station_separation, - ) - survey_original = dc.survey.Survey(source_list) - - # Write out data at their original electrode locations (not shifted) - data_obj = data.Data(survey_original, dobs=dobs, standard_deviation=std) - fname = dir_path + "dc_data.obs" - write_dcip2d_ubc(fname, data_obj, "volt", "dobs") - - fname = dir_path + "topo_xyz.txt" - np.savetxt(fname, topo_xyz, fmt="%.4e") +""" diff --git a/tutorials/05-dcr/plot_fwd_3_dcr3d.py b/tutorials/05-dcr/plot_fwd_3_dcr3d.py index ea7293d267..d731fbc8fd 100644 --- a/tutorials/05-dcr/plot_fwd_3_dcr3d.py +++ b/tutorials/05-dcr/plot_fwd_3_dcr3d.py @@ -3,384 +3,13 @@ DC Resistivity Forward Simulation in 3D ======================================= -Here we use the module *simpeg.electromagnetics.static.resistivity* to predict -DC resistivity data on an OcTree mesh. In this tutorial, we focus on the following: +.. important:: - - How to define the survey - - How to definine a tree mesh based on the survey geometry - - How to define the forward simulations - - How to predict DC data for a synthetic conductivity model - - How to include surface topography - - The units of the model and resulting data - - Plotting DC data in 3D + This tutorial has been moved to `User Tutorials + `_. - -In this case, we simulate dipole-dipole data for three East-West lines and two -North-South lines. + Checkout the `3D Forward Simulation + `_ tutorial. """ - -############################################################## -# Import modules -# -------------- -# -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt - -from discretize import TreeMesh -from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz - -from simpeg import maps, data -from simpeg.utils import model_builder -from simpeg.utils.io_utils.io_utils_electromagnetics import write_dcip_xyz -from simpeg.electromagnetics.static import resistivity as dc -from simpeg.electromagnetics.static.utils.static_utils import ( - generate_dcip_sources_line, - apparent_resistivity_from_voltage, -) - -# To plot DC data in 3D, the user must have the plotly package -try: - import plotly - from simpeg.electromagnetics.static.utils.static_utils import plot_3d_pseudosection - - has_plotly = True -except ImportError: - has_plotly = False - pass - -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - -mpl.rcParams.update({"font.size": 16}) -write_output = False - -# sphinx_gallery_thumbnail_number = 2 - -######################################################################### -# Defining Topography -# ------------------- -# -# Here we define surface topography as an (N, 3) numpy array. Topography could -# also be loaded from a file. In our case, our survey takes place within a circular -# depression. -# - -x_topo, y_topo = np.meshgrid( - np.linspace(-2100, 2100, 141), np.linspace(-2000, 2000, 141) -) -s = np.sqrt(x_topo**2 + y_topo**2) -z_topo = 10 + (1 / np.pi) * 140 * (-np.pi / 2 + np.arctan((s - 600.0) / 160.0)) -x_topo, y_topo, z_topo = mkvc(x_topo), mkvc(y_topo), mkvc(z_topo) -topo_xyz = np.c_[x_topo, y_topo, z_topo] - -######################################################################### -# Construct the DC Survey -# ----------------------- -# -# Here we define 5 DC lines that use a dipole-dipole electrode configuration; -# three lines along the East-West direction and 2 lines along the North-South direction. -# For each source, we must define the AB electrode locations. For each receiver -# we must define the MN electrode locations. Instead of creating the survey -# from scratch (see 1D example), we will use the *generat_dcip_sources_line* utility. -# This utility will give us the source list for a given DC/IP line. We can append -# the sources for multiple lines to create the survey. -# - -# Define the parameters for each survey line -survey_type = "dipole-dipole" -data_type = "volt" -dimension_type = "3D" -end_locations_list = [ - np.r_[-1000.0, 1000.0, 0.0, 0.0], - np.r_[-350.0, -350.0, -1000.0, 1000.0], - np.r_[350.0, 350.0, -1000.0, 1000.0], -] -station_separation = 100.0 -num_rx_per_src = 8 - -# The source lists for each line can be appended to create the source -# list for the whole survey. -source_list = [] -for ii in range(0, len(end_locations_list)): - source_list += generate_dcip_sources_line( - survey_type, - data_type, - dimension_type, - end_locations_list[ii], - topo_xyz, - num_rx_per_src, - station_separation, - ) - -# Define the survey -survey = dc.survey.Survey(source_list) - -################################################################# -# Create OcTree Mesh -# ------------------ -# -# Here, we create the OcTree mesh that will be used to predict DC data. -# -# - -# Defining domain side and minimum cell size -dh = 25.0 # base cell width -dom_width_x = 6000.0 # domain width x -dom_width_y = 6000.0 # domain width y -dom_width_z = 4000.0 # domain width z -nbcx = 2 ** int(np.round(np.log(dom_width_x / dh) / np.log(2.0))) # num. base cells x -nbcy = 2 ** int(np.round(np.log(dom_width_y / dh) / np.log(2.0))) # num. base cells y -nbcz = 2 ** int(np.round(np.log(dom_width_z / dh) / np.log(2.0))) # num. base cells z - -# Define the base mesh -hx = [(dh, nbcx)] -hy = [(dh, nbcy)] -hz = [(dh, nbcz)] -mesh = TreeMesh([hx, hy, hz], x0="CCN") - -# Mesh refinement based on topography -k = np.sqrt(np.sum(topo_xyz[:, 0:2] ** 2, axis=1)) < 1200 -mesh = refine_tree_xyz( - mesh, topo_xyz[k, :], octree_levels=[0, 6, 8], method="surface", finalize=False -) - -# Mesh refinement near sources and receivers. -electrode_locations = np.r_[ - survey.locations_a, survey.locations_b, survey.locations_m, survey.locations_n -] -unique_locations = np.unique(electrode_locations, axis=0) -mesh = refine_tree_xyz( - mesh, unique_locations, octree_levels=[4, 6, 4], method="radial", finalize=False -) - -# Finalize the mesh -mesh.finalize() - -################################################################ -# Create Conductivity Model and Mapping for OcTree Mesh -# ----------------------------------------------------- -# -# Here we define the conductivity model that will be used to predict DC -# resistivity data. The model consists of a conductive sphere and a -# resistive sphere within a moderately conductive background. Note that -# you can carry through this work flow with a resistivity model if desired. -# - -# Define conductivity model in S/m (or resistivity model in Ohm m) -air_value = 1e-8 -background_value = 1e-2 -conductor_value = 1e-1 -resistor_value = 1e-3 - -# Find active cells in forward modeling (cell below surface) -ind_active = active_from_xyz(mesh, topo_xyz) - -# Define mapping from model to active cells -nC = int(ind_active.sum()) -conductivity_map = maps.InjectActiveCells(mesh, ind_active, air_value) - -# Define model -conductivity_model = background_value * np.ones(nC) - -ind_conductor = model_builder.get_indices_sphere( - np.r_[-350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] -) -conductivity_model[ind_conductor] = conductor_value - -ind_resistor = model_builder.get_indices_sphere( - np.r_[350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] -) -conductivity_model[ind_resistor] = resistor_value - -# Plot Conductivity Model -fig = plt.figure(figsize=(10, 4)) - -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) -log_mod = np.log10(conductivity_model) - -ax1 = fig.add_axes([0.15, 0.15, 0.68, 0.75]) -mesh.plot_slice( - plotting_map * log_mod, - ax=ax1, - normal="Y", - ind=int(len(mesh.h[1]) / 2), - grid=True, - clim=(np.log10(resistor_value), np.log10(conductor_value)), - pcolor_opts={"cmap": mpl.cm.viridis}, -) -ax1.set_title("Conductivity Model") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") -ax1.set_xlim([-1000, 1000]) -ax1.set_ylim([-1000, 0]) - -ax2 = fig.add_axes([0.84, 0.15, 0.03, 0.75]) -norm = mpl.colors.Normalize( - vmin=np.log10(resistor_value), vmax=np.log10(conductor_value) -) -cbar = mpl.colorbar.ColorbarBase( - ax2, cmap=mpl.cm.viridis, norm=norm, orientation="vertical", format="$10^{%.1f}$" -) -cbar.set_label("Conductivity [S/m]", rotation=270, labelpad=15, size=12) - -########################################################## -# Project Survey to Discretized Topography -# ---------------------------------------- -# -# It is important that electrodes are not modeled as being in the air. Even if the -# electrodes are properly located along surface topography, they may lie above -# the *discretized* topography. This step is carried out to ensure all electrodes -# lie on the discretized surface. -# -# - -survey.drape_electrodes_on_topography(mesh, ind_active, option="top") - -############################################################ -# Predict DC Resistivity Data -# --------------------------- -# -# Here we predict DC resistivity data. If the keyword argument *sigmaMap* is -# defined, the simulation will expect a conductivity model. If the keyword -# argument *rhoMap* is defined, the simulation will expect a resistivity model. -# -# -# - -# Define the DC simulation -simulation = dc.simulation.Simulation3DNodal( - mesh, survey=survey, sigmaMap=conductivity_map, solver=Solver -) - -# Predict the data by running the simulation. The data are the measured voltage -# normalized by the source current in units of V/A. -dpred = simulation.dpred(conductivity_model) - -######################################################### -# Plot DC Data in 3D Pseudosection -# -------------------------------- -# -# Here we demonstrate how 3D DC resistivity data can be represented on a 3D -# pseudosection plot. To use this utility, you must have Python's *plotly* -# package. Here, we represent the data as apparent conductivities. -# -# The *plot_3d_pseudosection* utility allows the user to plot all pseudosection -# points, or plot the pseudosection plots that lie within some distance of -# one or more planes. -# - -# Since the data are normalized voltage, we must convert predicted -# to apparent conductivities. -apparent_conductivity = 1 / apparent_resistivity_from_voltage( - survey, - dpred, -) - -# For large datasets or for surveys with unconventional electrode geometry, -# interpretation can be challenging if we plot every datum. Here, we plot -# 3 out of the 5 survey lines to better image anomalous structures. -# To plot ALL of the data, simply remove the keyword argument *plane_points* -# when calling *plot_3d_pseudosection*. -plane_points = [] -p1, p2, p3 = np.array([-1000, 0, 0]), np.array([1000, 0, 0]), np.array([0, 0, -1000]) -plane_points.append([p1, p2, p3]) -p1, p2, p3 = ( - np.array([-350, -1000, 0]), - np.array([-350, 1000, 0]), - np.array([-350, 0, -1000]), -) -plane_points.append([p1, p2, p3]) -p1, p2, p3 = ( - np.array([350, -1000, 0]), - np.array([350, 1000, 0]), - np.array([350, 0, -1000]), -) -plane_points.append([p1, p2, p3]) - -if has_plotly: - fig = plot_3d_pseudosection( - survey, - apparent_conductivity, - scale="log", - units="S/m", - plane_points=plane_points, - plane_distance=15, - ) - - fig.update_layout( - title_text="Apparent Conductivity", - title_x=0.5, - title_font_size=24, - width=650, - height=500, - scene_camera=dict(center=dict(x=0.05, y=0, z=-0.4)), - ) - - plotly.io.show(fig) - -else: - print("INSTALL 'PLOTLY' TO VISUALIZE 3D PSEUDOSECTIONS") - -######################################################## -# Optional: Write Predicted DC Data -# --------------------------------- -# -# Write DC resistivity data, topography and true model -# - -if write_output: - dir_path = os.path.dirname(__file__).split(os.path.sep) - dir_path.extend(["outputs"]) - dir_path = os.path.sep.join(dir_path) + os.path.sep - - if not os.path.exists(dir_path): - os.mkdir(dir_path) - - # Add 5% Gaussian noise to each datum - np.random.seed(433) - std = 0.1 * np.abs(dpred) - noise = std * np.random.randn(len(dpred)) - dobs = dpred + noise - - # Create dictionary that stores line IDs - N = int(survey.nD / len(end_locations_list)) - lineID = np.r_[np.ones(N), 2 * np.ones(N), 3 * np.ones(N)] - out_dict = {"LINEID": lineID} - - # Create a survey with the original electrode locations - # and not the shifted ones - source_list = [] - for ii in range(0, len(end_locations_list)): - source_list += generate_dcip_sources_line( - survey_type, - data_type, - dimension_type, - end_locations_list[ii], - topo_xyz, - num_rx_per_src, - station_separation, - ) - survey_original = dc.survey.Survey(source_list) - - # Write out data at their original electrode locations (not shifted) - data_obj = data.Data(survey_original, dobs=dobs, standard_deviation=std) - - fname = dir_path + "dc_data.xyz" - write_dcip_xyz( - fname, - data_obj, - data_header="V/A", - uncertainties_header="UNCERT", - out_dict=out_dict, - ) - - fname = dir_path + "topo_xyz.txt" - np.savetxt(fname, topo_xyz, fmt="%.4e") diff --git a/tutorials/05-dcr/plot_inv_1_dcr_sounding.py b/tutorials/05-dcr/plot_inv_1_dcr_sounding.py index 75deb01d18..54159c7a65 100644 --- a/tutorials/05-dcr/plot_inv_1_dcr_sounding.py +++ b/tutorials/05-dcr/plot_inv_1_dcr_sounding.py @@ -3,324 +3,16 @@ Least-Squares 1D Inversion of Sounding Data =========================================== -Here we use the module *simpeg.electromangetics.static.resistivity* to invert -DC resistivity sounding data and recover a 1D electrical resistivity model. -In this tutorial, we focus on the following: +.. important:: - - How to define sources and receivers from a survey file - - How to define the survey - - 1D inversion of DC resistivity data + This tutorial has been moved to `User Tutorials + `_. -For this tutorial, we will invert sounding data collected over a layered Earth using -a Wenner array. The end product is layered Earth model which explains the data. + Checkout the `Weighted Least-Squares Inversion + `_ + section in the + `1D Inversion for a Single Sounding + `_ tutorial. """ - -######################################################################### -# Import modules -# -------------- -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -import tarfile - -from discretize import TensorMesh - -from simpeg import ( - maps, - data, - data_misfit, - regularization, - optimization, - inverse_problem, - inversion, - directives, - utils, -) -from simpeg.electromagnetics.static import resistivity as dc -from simpeg.utils import plot_1d_layer_model - -mpl.rcParams.update({"font.size": 16}) - -# sphinx_gallery_thumbnail_number = 2 - -############################################# -# Define File Names -# ----------------- -# -# Here we provide the file paths to assets we need to run the inversion. The -# Path to the true model is also provided for comparison with the inversion -# results. These files are stored as a tar-file on our google cloud bucket: -# "https://storage.googleapis.com/simpeg/doc-assets/dcr1d.tar.gz" -# - -# storage bucket where we have the data -data_source = "https://storage.googleapis.com/simpeg/doc-assets/dcr1d.tar.gz" - -# download the data -downloaded_data = utils.download(data_source, overwrite=True) - -# unzip the tarfile -tar = tarfile.open(downloaded_data, "r") -tar.extractall() -tar.close() - -# path to the directory containing our data -dir_path = downloaded_data.split(".")[0] + os.path.sep - -# files to work with -data_filename = dir_path + "app_res_1d_data.dobs" - - -############################################# -# Load Data, Define Survey and Plot -# --------------------------------- -# -# Here we load the observed data, define the DC survey geometry and plot the -# data values. -# - -# Load data -dobs = np.loadtxt(str(data_filename)) - -# Extract source and receiver electrode locations and the observed data -A_electrodes = dobs[:, 0:3] -B_electrodes = dobs[:, 3:6] -M_electrodes = dobs[:, 6:9] -N_electrodes = dobs[:, 9:12] -dobs = dobs[:, -1] - -# Define survey -unique_tx, k = np.unique(np.c_[A_electrodes, B_electrodes], axis=0, return_index=True) -n_sources = len(k) -k = np.sort(k) -k = np.r_[k, len(k) + 1] - -source_list = [] -for ii in range(0, n_sources): - # MN electrode locations for receivers. Each is an (N, 3) numpy array - M_locations = M_electrodes[k[ii] : k[ii + 1], :] - N_locations = N_electrodes[k[ii] : k[ii + 1], :] - receiver_list = [ - dc.receivers.Dipole( - M_locations, - N_locations, - data_type="apparent_resistivity", - ) - ] - - # AB electrode locations for source. Each is a (1, 3) numpy array - A_location = A_electrodes[k[ii], :] - B_location = B_electrodes[k[ii], :] - source_list.append(dc.sources.Dipole(receiver_list, A_location, B_location)) - -# Define survey -survey = dc.Survey(source_list) - -# Plot apparent resistivities on sounding curve as a function of Wenner separation -# parameter. -electrode_separations = 0.5 * np.sqrt( - np.sum((survey.locations_a - survey.locations_b) ** 2, axis=1) -) - -fig = plt.figure(figsize=(11, 5)) -mpl.rcParams.update({"font.size": 14}) -ax1 = fig.add_axes([0.15, 0.1, 0.7, 0.85]) -ax1.semilogy(electrode_separations, dobs, "b") -ax1.set_xlabel("AB/2 (m)") -ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)") -plt.show() - -############################################### -# Assign Uncertainties -# -------------------- -# -# Inversion with SimPEG requires that we define standard deviation on our data. -# This represents our estimate of the noise in our data. For DC sounding data, -# a relative error is applied to each datum. For this tutorial, the relative -# error on each datum will be 2%. - -std = 0.02 * np.abs(dobs) - - -############################################### -# Define Data -# -------------------- -# -# Here is where we define the data that are inverted. The data are defined by -# the survey, the observation values and the standard deviation. -# - -data_object = data.Data(survey, dobs=dobs, standard_deviation=std) - - -############################################### -# Defining a 1D Layered Earth (1D Tensor Mesh) -# -------------------------------------------- -# -# Here, we define the layer thicknesses for our 1D simulation. To do this, we use -# the TensorMesh class. -# - -# Define layer thicknesses -layer_thicknesses = 5 * np.logspace(0, 1, 25) - -# Define a mesh for plotting and regularization. -mesh = TensorMesh([(np.r_[layer_thicknesses, layer_thicknesses[-1]])], "0") - -print(mesh) - -############################################################### -# Define a Starting and Reference Model -# ------------------------------------- -# -# Here, we create starting and/or reference models for the inversion as -# well as the mapping from the model space to the active cells. Starting and -# reference models can be a constant background value or contain a-priori -# structures. Here, the starting model is log(1000) Ohm meters. -# -# Define log-resistivity values for each layer since our model is the -# log-resistivity. Don't make the values 0! -# Otherwise the gradient for the 1st iteration is zero and the inversion will -# not converge. - -# Define model. A resistivity (Ohm meters) or conductivity (S/m) for each layer. -starting_model = np.log(2e2 * np.ones((len(layer_thicknesses) + 1))) - -# Define mapping from model to active cells. -model_map = maps.IdentityMap(nP=len(starting_model)) * maps.ExpMap() - -####################################################################### -# Define the Physics -# ------------------ -# -# Here we define the physics of the problem using the Simulation1DLayers class. -# - -simulation = dc.simulation_1d.Simulation1DLayers( - survey=survey, - rhoMap=model_map, - thicknesses=layer_thicknesses, -) - - -####################################################################### -# Define Inverse Problem -# ---------------------- -# -# The inverse problem is defined by 3 things: -# -# 1) Data Misfit: a measure of how well our recovered model explains the field data -# 2) Regularization: constraints placed on the recovered model and a priori information -# 3) Optimization: the numerical approach used to solve the inverse problem -# -# - -# Define the data misfit. Here the data misfit is the L2 norm of the weighted -# residual between the observed data and the data predicted for a given model. -# Within the data misfit, the residual between predicted and observed data are -# normalized by the data's standard deviation. -dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_object) - -# Define the regularization (model objective function) -reg = regularization.WeightedLeastSquares( - mesh, alpha_s=1.0, alpha_x=1.0, reference_model=starting_model -) - -# Define how the optimization problem is solved. Here we will use an inexact -# Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.InexactGaussNewton(maxIter=30, maxIterCG=20) - -# Define the inverse problem -inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) - -####################################################################### -# Define Inversion Directives -# --------------------------- -# -# Here we define any directives that are carried out during the inversion. This -# includes the cooling schedule for the trade-off parameter (beta), stopping -# criteria for the inversion and saving inversion results at each iteration. -# - -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e0) - -# Set the rate of reduction in trade-off parameter (beta) each time the -# the inverse problem is solved. And set the number of Gauss-Newton iterations -# for each trade-off paramter value. -beta_schedule = directives.BetaSchedule(coolingFactor=5.0, coolingRate=3.0) - -# Apply and update sensitivity weighting as the model updates -update_sensitivity_weights = directives.UpdateSensitivityWeights() - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# Setting a stopping criteria for the inversion. -target_misfit = directives.TargetMisfit(chifact=1) - -# The directives are defined as a list. -directives_list = [ - update_sensitivity_weights, - starting_beta, - beta_schedule, - save_iteration, - target_misfit, -] - -##################################################################### -# Running the Inversion -# --------------------- -# -# To define the inversion object, we need to define the inversion problem and -# the set of directives. We can then run the inversion. -# - -# Here we combine the inverse problem and the set of directives -inv = inversion.BaseInversion(inv_prob, directives_list) - -# Run the inversion -recovered_model = inv.run(starting_model) - -############################################################ -# Examining the Results -# --------------------- -# - -# Define true model and layer thicknesses -true_model = np.r_[1e3, 4e3, 2e2] -true_layers = np.r_[100.0, 100.0] - -# Plot true model and recovered model -fig = plt.figure(figsize=(6, 4)) -x_min = np.min([np.min(model_map * recovered_model), np.min(true_model)]) -x_max = np.max([np.max(model_map * recovered_model), np.max(true_model)]) - -ax1 = fig.add_axes([0.2, 0.15, 0.7, 0.7]) -plot_1d_layer_model(true_layers, true_model, ax=ax1, plot_elevation=True, color="b") -plot_1d_layer_model( - layer_thicknesses, - model_map * recovered_model, - ax=ax1, - plot_elevation=True, - color="r", -) -ax1.set_xlabel(r"Resistivity ($\Omega m$)") -ax1.set_xlim(0.9 * x_min, 1.1 * x_max) -ax1.legend(["True Model", "Recovered Model"]) - -# Plot the true and apparent resistivities on a sounding curve -fig = plt.figure(figsize=(11, 5)) -ax1 = fig.add_axes([0.2, 0.1, 0.6, 0.8]) -ax1.semilogy(electrode_separations, dobs, "b") -ax1.semilogy(electrode_separations, inv_prob.dpred, "r") -ax1.set_xlabel("AB/2 (m)") -ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)") -ax1.legend(["True Sounding Curve", "Predicted Sounding Curve"]) -plt.show() diff --git a/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py b/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py index f461b716dd..8d203ad958 100644 --- a/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py +++ b/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py @@ -3,325 +3,16 @@ Sparse 1D Inversion of Sounding Data ==================================== -Here we use the module *simpeg.electromangetics.static.resistivity* to invert -DC resistivity sounding data and recover a 1D electrical resistivity model. -In this tutorial, we focus on the following: +.. important:: - - How to define sources and receivers from a survey file - - How to define the survey - - 1D inversion of DC resistivity data with iteratively re-weighted least-squares + This tutorial has been moved to `User Tutorials + `_. -For this tutorial, we will invert sounding data collected over a layered Earth using -a Wenner array. The end product is layered Earth model which explains the data. + Checkout the `Iteratively Re-weighted Least-Squares Inversion + `_ + section in the + `1D Inversion for a Single Sounding + `_ tutorial. """ - -######################################################################### -# Import modules -# -------------- -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -import tarfile - -from discretize import TensorMesh - -from simpeg import ( - maps, - data, - data_misfit, - regularization, - optimization, - inverse_problem, - inversion, - directives, - utils, -) -from simpeg.electromagnetics.static import resistivity as dc -from simpeg.utils import plot_1d_layer_model - -mpl.rcParams.update({"font.size": 16}) - -# sphinx_gallery_thumbnail_number = 2 - -############################################# -# Define File Names -# ----------------- -# -# Here we provide the file paths to assets we need to run the inversion. The -# Path to the true model is also provided for comparison with the inversion -# results. These files are stored as a tar-file on our google cloud bucket: -# "https://storage.googleapis.com/simpeg/doc-assets/dcr1d.tar.gz" -# - -# storage bucket where we have the data -data_source = "https://storage.googleapis.com/simpeg/doc-assets/dcr1d.tar.gz" - -# download the data -downloaded_data = utils.download(data_source, overwrite=True) - -# unzip the tarfile -tar = tarfile.open(downloaded_data, "r") -tar.extractall() -tar.close() - -# path to the directory containing our data -dir_path = downloaded_data.split(".")[0] + os.path.sep - -# files to work with -data_filename = dir_path + "app_res_1d_data.dobs" - - -############################################# -# Load Data, Define Survey and Plot -# --------------------------------- -# -# Here we load the observed data, define the DC survey geometry and plot the -# data values. -# - -# Load data -dobs = np.loadtxt(str(data_filename)) - -# Extract source and receiver electrode locations and the observed data -A_electrodes = dobs[:, 0:3] -B_electrodes = dobs[:, 3:6] -M_electrodes = dobs[:, 6:9] -N_electrodes = dobs[:, 9:12] -dobs = dobs[:, -1] - -# Define survey -unique_tx, k = np.unique(np.c_[A_electrodes, B_electrodes], axis=0, return_index=True) -n_sources = len(k) -k = np.sort(k) -k = np.r_[k, len(k) + 1] - -source_list = [] -for ii in range(0, n_sources): - # MN electrode locations for receivers. Each is an (N, 3) numpy array - M_locations = M_electrodes[k[ii] : k[ii + 1], :] - N_locations = N_electrodes[k[ii] : k[ii + 1], :] - receiver_list = [ - dc.receivers.Dipole( - M_locations, - N_locations, - data_type="apparent_resistivity", - ) - ] - - # AB electrode locations for source. Each is a (1, 3) numpy array - A_location = A_electrodes[k[ii], :] - B_location = B_electrodes[k[ii], :] - source_list.append(dc.sources.Dipole(receiver_list, A_location, B_location)) - -# Define survey -survey = dc.Survey(source_list) - -# Plot apparent resistivities on sounding curve as a function of Wenner separation -# parameter. -electrode_separations = 0.5 * np.sqrt( - np.sum((survey.locations_a - survey.locations_b) ** 2, axis=1) -) - -fig = plt.figure(figsize=(11, 5)) -mpl.rcParams.update({"font.size": 14}) -ax1 = fig.add_axes([0.15, 0.1, 0.7, 0.85]) -ax1.semilogy(electrode_separations, dobs, "b") -ax1.set_xlabel("AB/2 (m)") -ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)") -plt.show() - -############################################### -# Assign Uncertainties -# -------------------- -# -# Inversion with SimPEG requires that we define standard deviation on our data. -# This represents our estimate of the noise in our data. For DC sounding data, -# a relative error is applied to each datum. For this tutorial, the relative -# error on each datum will be 2%. - -std = 0.02 * np.abs(dobs) - - -############################################### -# Define Data -# -------------------- -# -# Here is where we define the data that are inverted. The data are defined by -# the survey, the observation values and the standard deviation. -# - -data_object = data.Data(survey, dobs=dobs, standard_deviation=std) - - -############################################### -# Defining a 1D Layered Earth (1D Tensor Mesh) -# -------------------------------------------- -# -# Here, we define the layer thicknesses for our 1D simulation. To do this, we use -# the TensorMesh class. -# - -# Define layer thicknesses -layer_thicknesses = 5 * np.logspace(0, 1, 25) - -# Define a mesh for plotting and regularization. -mesh = TensorMesh([(np.r_[layer_thicknesses, layer_thicknesses[-1]])], "0") - -print(mesh) - -############################################################### -# Define a Starting and Reference Model -# ------------------------------------- -# -# Here, we create starting and/or reference models for the inversion as -# well as the mapping from the model space to the active cells. Starting and -# reference models can be a constant background value or contain a-priori -# structures. Here, the starting model is log(1000) Ohm meters. -# -# Define log-resistivity values for each layer since our model is the -# log-resistivity. Don't make the values 0! -# Otherwise the gradient for the 1st iteration is zero and the inversion will -# not converge. - -# Define model. A resistivity (Ohm meters) or conductivity (S/m) for each layer. -starting_model = np.log(2e2 * np.ones((len(layer_thicknesses) + 1))) - -# Define mapping from model to active cells. -model_map = maps.IdentityMap(nP=len(starting_model)) * maps.ExpMap() - -####################################################################### -# Define the Physics -# ------------------ -# -# Here we define the physics of the problem using the Simulation1DLayers class. -# - -simulation = dc.simulation_1d.Simulation1DLayers( - survey=survey, - rhoMap=model_map, - thicknesses=layer_thicknesses, -) - - -####################################################################### -# Define Inverse Problem -# ---------------------- -# -# The inverse problem is defined by 3 things: -# -# 1) Data Misfit: a measure of how well our recovered model explains the field data -# 2) Regularization: constraints placed on the recovered model and a priori information -# 3) Optimization: the numerical approach used to solve the inverse problem -# -# - -# Define the data misfit. Here the data misfit is the L2 norm of the weighted -# residual between the observed data and the data predicted for a given model. -# Within the data misfit, the residual between predicted and observed data are -# normalized by the data's standard deviation. -dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_object) - -# Define the regularization (model objective function). Here, 'p' defines the -# the norm of the smallness term and 'q' defines the norm of the smoothness -# term. -reg = regularization.Sparse(mesh, mapping=model_map) -reg.reference_model = starting_model -p = 0 -q = 0 -reg.norms = [p, q] - -# Define how the optimization problem is solved. Here we will use an inexact -# Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.ProjectedGNCG(maxIter=100, maxIterLS=20, maxIterCG=20, tolCG=1e-3) - -# Define the inverse problem -inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) - -####################################################################### -# Define Inversion Directives -# --------------------------- -# -# Here we define any directives that are carried out during the inversion. This -# includes the cooling schedule for the trade-off parameter (beta), stopping -# criteria for the inversion and saving inversion results at each iteration. -# - -# Apply and update sensitivity weighting as the model updates -update_sensitivity_weights = directives.UpdateSensitivityWeights() - -# Reach target misfit for L2 solution, then use IRLS until model stops changing. -IRLS = directives.Update_IRLS(max_irls_iterations=40, minGNiter=1, f_min_change=1e-5) - -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=20) - -# Update the preconditionner -update_Jacobi = directives.UpdatePreconditioner() - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# The directives are defined as a list. -directives_list = [ - update_sensitivity_weights, - IRLS, - starting_beta, - update_Jacobi, - save_iteration, -] - -##################################################################### -# Running the Inversion -# --------------------- -# -# To define the inversion object, we need to define the inversion problem and -# the set of directives. We can then run the inversion. -# - -# Here we combine the inverse problem and the set of directives -inv = inversion.BaseInversion(inv_prob, directives_list) - -# Run the inversion -recovered_model = inv.run(starting_model) - -############################################################ -# Examining the Results -# --------------------- -# - -# Define true model and layer thicknesses -true_model = np.r_[1e3, 4e3, 2e2] -true_layers = np.r_[100.0, 100.0] - -# Extract Least-Squares model -l2_model = inv_prob.l2model - -# Plot true model and recovered model -fig = plt.figure(figsize=(6, 4)) -x_min = np.min(np.r_[model_map * recovered_model, model_map * l2_model, true_model]) -x_max = np.max(np.r_[model_map * recovered_model, model_map * l2_model, true_model]) - -ax1 = fig.add_axes([0.2, 0.15, 0.7, 0.7]) -plot_1d_layer_model(true_layers, true_model, ax=ax1, color="k") -plot_1d_layer_model(layer_thicknesses, model_map * l2_model, ax=ax1, color="b") -plot_1d_layer_model(layer_thicknesses, model_map * recovered_model, ax=ax1, color="r") -ax1.set_xlabel(r"Resistivity ($\Omega m$)") -ax1.set_xlim(0.9 * x_min, 1.1 * x_max) -ax1.legend(["True Model", "L2-Model", "Sparse Model"]) - -# Plot the true and apparent resistivities on a sounding curve -fig = plt.figure(figsize=(11, 5)) -ax1 = fig.add_axes([0.2, 0.1, 0.6, 0.8]) -ax1.semilogy(electrode_separations, dobs, "k") -ax1.semilogy(electrode_separations, simulation.dpred(l2_model), "b") -ax1.semilogy(electrode_separations, simulation.dpred(recovered_model), "r") -ax1.set_xlabel("AB/2 (m)") -ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)") -ax1.legend(["True Sounding Curve", "Predicted (L2-Model)", "Predicted (Sparse)"]) -plt.show() diff --git a/tutorials/05-dcr/plot_inv_1_dcr_sounding_parametric.py b/tutorials/05-dcr/plot_inv_1_dcr_sounding_parametric.py index 63936d4e9d..b0686b6662 100644 --- a/tutorials/05-dcr/plot_inv_1_dcr_sounding_parametric.py +++ b/tutorials/05-dcr/plot_inv_1_dcr_sounding_parametric.py @@ -3,326 +3,17 @@ Parametric 1D Inversion of Sounding Data ======================================== -Here we use the module *simpeg.electromangetics.static.resistivity* to invert -DC resistivity sounding data and recover the resistivities and layer thicknesses -for a 1D layered Earth. In this tutorial, we focus on the following: +.. important:: - - How to define sources and receivers from a survey file - - How to define the survey - - Defining a model that consists of resistivities and layer thicknesses + This tutorial has been moved to `User Tutorials + `_. -For this tutorial, we will invert sounding data collected over a layered Earth using -a Wenner array. The end product is layered Earth model which explains the data. + Checkout the `Parametric Inversion + `_ + section in the + `1D Inversion for a Single Sounding + `_ tutorial. """ - -######################################################################### -# Import modules -# -------------- -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -import tarfile - -from discretize import TensorMesh - -from simpeg import ( - maps, - data, - data_misfit, - regularization, - optimization, - inverse_problem, - inversion, - directives, - utils, -) -from simpeg.electromagnetics.static import resistivity as dc -from simpeg.utils import plot_1d_layer_model - -mpl.rcParams.update({"font.size": 16}) - -# sphinx_gallery_thumbnail_number = 2 - - -############################################# -# Define File Names -# ----------------- -# -# Here we provide the file paths to assets we need to run the inversion. The -# Path to the true model is also provided for comparison with the inversion -# results. These files are stored as a tar-file on our google cloud bucket: -# "https://storage.googleapis.com/simpeg/doc-assets/dcr1d.tar.gz" -# - -# storage bucket where we have the data -data_source = "https://storage.googleapis.com/simpeg/doc-assets/dcr1d.tar.gz" - -# download the data -downloaded_data = utils.download(data_source, overwrite=True) - -# unzip the tarfile -tar = tarfile.open(downloaded_data, "r") -tar.extractall() -tar.close() - -# path to the directory containing our data -dir_path = downloaded_data.split(".")[0] + os.path.sep - -# files to work with -data_filename = dir_path + "app_res_1d_data.dobs" - - -############################################# -# Load Data, Define Survey and Plot -# --------------------------------- -# -# Here we load the observed data, define the DC survey geometry and plot the -# data values. -# - -# Load data -dobs = np.loadtxt(str(data_filename)) - -A_electrodes = dobs[:, 0:3] -B_electrodes = dobs[:, 3:6] -M_electrodes = dobs[:, 6:9] -N_electrodes = dobs[:, 9:12] -dobs = dobs[:, -1] - -# Define survey -unique_tx, k = np.unique(np.c_[A_electrodes, B_electrodes], axis=0, return_index=True) -n_sources = len(k) -k = np.sort(k) -k = np.r_[k, len(k) + 1] - -source_list = [] -for ii in range(0, n_sources): - # MN electrode locations for receivers. Each is an (N, 3) numpy array - M_locations = M_electrodes[k[ii] : k[ii + 1], :] - N_locations = N_electrodes[k[ii] : k[ii + 1], :] - receiver_list = [ - dc.receivers.Dipole( - M_locations, - N_locations, - data_type="apparent_resistivity", - ) - ] - - # AB electrode locations for source. Each is a (1, 3) numpy array - A_location = A_electrodes[k[ii], :] - B_location = B_electrodes[k[ii], :] - source_list.append(dc.sources.Dipole(receiver_list, A_location, B_location)) - -# Define survey -survey = dc.Survey(source_list) - -# Plot apparent resistivities on sounding curve as a function of Wenner separation -# parameter. -electrode_separations = 0.5 * np.sqrt( - np.sum((survey.locations_a - survey.locations_b) ** 2, axis=1) -) - -fig = plt.figure(figsize=(11, 5)) -mpl.rcParams.update({"font.size": 14}) -ax1 = fig.add_axes([0.15, 0.1, 0.7, 0.85]) -ax1.semilogy(electrode_separations, dobs, "b") -ax1.set_xlabel("AB/2 (m)") -ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)") -plt.show() - -############################################### -# Assign Uncertainties -# -------------------- -# -# Inversion with SimPEG requires that we define standard deviation on our data. -# This represents our estimate of the noise in our data. For DC sounding data, -# a relative error is applied to each datum. For this tutorial, the relative -# error on each datum will be 2.5%. -# - -std = 0.025 * dobs - - -############################################### -# Define Data -# -------------------- -# -# Here is where we define the data that are inverted. The data are defined by -# the survey, the observation values and the standard deviation. -# - -data_object = data.Data(survey, dobs=dobs, standard_deviation=std) - -############################################################### -# Defining the Starting Model and Mapping -# --------------------------------------- -# -# In this case, the model consists of parameters which define the respective -# resistivities and thickness for a set of horizontal layer. Here, we choose to -# define a model consisting of 3 layers. -# - -# Define the resistivities and thicknesses for the starting model. The thickness -# of the bottom layer is assumed to extend downward to infinity so we don't -# need to define it. -resistivities = np.r_[1e3, 1e3, 1e3] -layer_thicknesses = np.r_[50.0, 50.0] - -# Define a mesh for plotting and regularization. -mesh = TensorMesh([(np.r_[layer_thicknesses, layer_thicknesses[-1]])], "0") -print(mesh) - -# Define model. We are inverting for the layer resistivities and layer thicknesses. -# Since the bottom layer extends to infinity, it is not a model parameter for -# which we need to invert. For a 3 layer model, there is a total of 5 parameters. -# For stability, our model is the log-resistivity and log-thickness. -starting_model = np.r_[np.log(resistivities), np.log(layer_thicknesses)] - -# Since the model contains two different properties for each layer, we use -# wire maps to distinguish the properties. -wire_map = maps.Wires(("rho", mesh.nC), ("t", mesh.nC - 1)) -resistivity_map = maps.ExpMap(nP=mesh.nC) * wire_map.rho -layer_map = maps.ExpMap(nP=mesh.nC - 1) * wire_map.t - -####################################################################### -# Define the Physics -# ------------------ -# -# Here we define the physics of the problem. The data consists of apparent -# resistivity values. This is defined here. -# - -simulation = dc.simulation_1d.Simulation1DLayers( - survey=survey, - rhoMap=resistivity_map, - thicknessesMap=layer_map, -) - -####################################################################### -# Define Inverse Problem -# ---------------------- -# -# The inverse problem is defined by 3 things: -# -# 1) Data Misfit: a measure of how well our recovered model explains the field data -# 2) Regularization: constraints placed on the recovered model and a priori information -# 3) Optimization: the numerical approach used to solve the inverse problem -# -# - -# Define the data misfit. Here the data misfit is the L2 norm of the weighted -# residual between the observed data and the data predicted for a given model. -# Within the data misfit, the residual between predicted and observed data are -# normalized by the data's standard deviation. -dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_object) - -# Define the regularization on the parameters related to resistivity -mesh_rho = TensorMesh([mesh.h[0].size]) -reg_rho = regularization.WeightedLeastSquares( - mesh_rho, alpha_s=0.01, alpha_x=1, mapping=wire_map.rho -) - -# Define the regularization on the parameters related to layer thickness -mesh_t = TensorMesh([mesh.h[0].size - 1]) -reg_t = regularization.WeightedLeastSquares( - mesh_t, alpha_s=0.01, alpha_x=1, mapping=wire_map.t -) - -# Combine to make regularization for the inversion problem -reg = reg_rho + reg_t - -# Define how the optimization problem is solved. Here we will use an inexact -# Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.InexactGaussNewton(maxIter=50, maxIterCG=30) - -# Define the inverse problem -inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) - -####################################################################### -# Define Inversion Directives -# --------------------------- -# -# Here we define any directives that are carried out during the inversion. This -# includes the cooling schedule for the trade-off parameter (beta), stopping -# criteria for the inversion and saving inversion results at each iteration. -# - -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e1) - -# Set the rate of reduction in trade-off parameter (beta) each time the -# the inverse problem is solved. And set the number of Gauss-Newton iterations -# for each trade-off paramter value. -beta_schedule = directives.BetaSchedule(coolingFactor=5.0, coolingRate=3.0) - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# Setting a stopping criteria for the inversion. -target_misfit = directives.TargetMisfit(chifact=0.1) - -# The directives are defined in a list -directives_list = [ - starting_beta, - beta_schedule, - target_misfit, -] - -##################################################################### -# Running the Inversion -# --------------------- -# -# To define the inversion object, we need to define the inversion problem and -# the set of directives. We can then run the inversion. -# - -# Here we combine the inverse problem and the set of directives -inv = inversion.BaseInversion(inv_prob, directiveList=directives_list) - -# Run the inversion -recovered_model = inv.run(starting_model) - -############################################################ -# Examining the Results -# --------------------- -# - -# Define true model and layer thicknesses -true_model = np.r_[1e3, 4e3, 2e2] -true_layers = np.r_[100.0, 100.0] - -# Plot true model and recovered model -fig = plt.figure(figsize=(5, 5)) - -x_min = np.min([np.min(resistivity_map * recovered_model), np.min(true_model)]) -x_max = np.max([np.max(resistivity_map * recovered_model), np.max(true_model)]) - -ax1 = fig.add_axes([0.2, 0.15, 0.7, 0.7]) -plot_1d_layer_model(true_layers, true_model, ax=ax1, plot_elevation=True, color="b") -plot_1d_layer_model( - layer_map * recovered_model, - resistivity_map * recovered_model, - ax=ax1, - plot_elevation=True, - color="r", -) -ax1.set_xlabel(r"Resistivity ($\Omega m$)") -ax1.set_xlim(0.9 * x_min, 1.1 * x_max) -ax1.legend(["True Model", "Recovered Model"]) - -# Plot the true and apparent resistivities on a sounding curve -fig = plt.figure(figsize=(11, 5)) -ax1 = fig.add_axes([0.2, 0.05, 0.6, 0.8]) -ax1.semilogy(electrode_separations, dobs, "b") -ax1.semilogy(electrode_separations, inv_prob.dpred, "r") -ax1.set_xlabel("AB/2 (m)") -ax1.set_ylabel(r"Apparent Resistivity ($\Omega m$)") -ax1.legend(["True Sounding Curve", "Predicted Sounding Curve"]) -plt.show() diff --git a/tutorials/05-dcr/plot_inv_2_dcr2d.py b/tutorials/05-dcr/plot_inv_2_dcr2d.py index f28bb8d26d..e0766f3be1 100644 --- a/tutorials/05-dcr/plot_inv_2_dcr2d.py +++ b/tutorials/05-dcr/plot_inv_2_dcr2d.py @@ -2,460 +2,15 @@ 2.5D DC Resistivity Least-Squares Inversion =========================================== -Here we invert a line of DC resistivity data to recover an electrical -conductivity model. We formulate the inverse problem as a least-squares -optimization problem. For this tutorial, we focus on the following: +.. important:: - - Defining the survey - - Generating a mesh based on survey geometry - - Including surface topography - - Defining the inverse problem (data misfit, regularization, directives) - - Applying sensitivity weighting - - Plotting the recovered model and data misfit + This tutorial has been moved to `User Tutorials + `_. + Checkout the `Weighted Least-Squares Inversion + `_ + section in the + `2.5D DC Resistivity Inversion + `_ tutorial. """ - -######################################################################### -# Import modules -# -------------- -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -from matplotlib.colors import LogNorm -import tarfile - -from discretize import TreeMesh -from discretize.utils import mkvc, active_from_xyz - -from simpeg.utils import model_builder -from simpeg import ( - maps, - data_misfit, - regularization, - optimization, - inverse_problem, - inversion, - directives, - utils, -) -from simpeg.electromagnetics.static import resistivity as dc -from simpeg.electromagnetics.static.utils.static_utils import ( - plot_pseudosection, -) -from simpeg.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc - -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - -mpl.rcParams.update({"font.size": 16}) -# sphinx_gallery_thumbnail_number = 4 - - -############################################# -# Download Assets -# --------------- -# -# Here we provide the file paths to assets we need to run the inversion. The -# path to the true model conductivity and chargeability models are also -# provided for comparison with the inversion results. These files are stored as a -# tar-file on our google cloud bucket: -# "https://storage.googleapis.com/simpeg/doc-assets/dcr2d.tar.gz" -# - -# storage bucket where we have the data -data_source = "https://storage.googleapis.com/simpeg/doc-assets/dcr2d.tar.gz" - -# download the data -downloaded_data = utils.download(data_source, overwrite=True) - -# unzip the tarfile -tar = tarfile.open(downloaded_data, "r") -tar.extractall() -tar.close() - -# path to the directory containing our data -dir_path = downloaded_data.split(".")[0] + os.path.sep - -# files to work with -topo_filename = dir_path + "topo_xyz.txt" -data_filename = dir_path + "dc_data.obs" - - -############################################# -# Load Data, Define Survey and Plot -# --------------------------------- -# -# Here we load the observed data, define the DC and IP survey geometry and -# plot the data values using pseudo-sections. -# **Warning**: In the following example, the observations file is assumed to be -# sorted by sources -# - -# Load data -topo_xyz = np.loadtxt(str(topo_filename)) -dc_data = read_dcip2d_ubc(data_filename, "volt", "general") - -####################################################################### -# Plot Observed Data in Pseudo-Section -# ------------------------------------ -# -# Here, we demonstrate how to plot 2D data in pseudo-section. -# First, we plot the actual data (voltages) in pseudo-section as a scatter plot. -# This allows us to visualize the pseudo-sensitivity locations for our survey. -# Next, we plot the data as apparent conductivities in pseudo-section with a filled -# contour plot. -# - -# Plot voltages pseudo-section -fig = plt.figure(figsize=(12, 5)) -ax1 = fig.add_axes([0.1, 0.15, 0.75, 0.78]) -plot_pseudosection( - dc_data, - plot_type="scatter", - ax=ax1, - scale="log", - cbar_label="V/A", - scatter_opts={"cmap": mpl.cm.viridis}, -) -ax1.set_title("Normalized Voltages") -plt.show() - -# Plot apparent conductivity pseudo-section -fig = plt.figure(figsize=(12, 5)) -ax1 = fig.add_axes([0.1, 0.15, 0.75, 0.78]) -plot_pseudosection( - dc_data, - plot_type="contourf", - ax=ax1, - scale="log", - data_type="apparent conductivity", - cbar_label="S/m", - mask_topography=True, - contourf_opts={"levels": 20, "cmap": mpl.cm.viridis}, -) -ax1.set_title("Apparent Conductivity") -plt.show() - -#################################################### -# Assign Uncertainties -# -------------------- -# -# Inversion with SimPEG requires that we define the uncertainties on our data. -# This represents our estimate of the standard deviation of the -# noise in our data. For DC data, the uncertainties are 10% of the absolute value -# -# - -dc_data.standard_deviation = 0.05 * np.abs(dc_data.dobs) - -######################################################## -# Create Tree Mesh -# ------------------ -# -# Here, we create the Tree mesh that will be used to invert DC data. -# - -dh = 4 # base cell width -dom_width_x = 3200.0 # domain width x -dom_width_z = 2400.0 # domain width z -nbcx = 2 ** int(np.round(np.log(dom_width_x / dh) / np.log(2.0))) # num. base cells x -nbcz = 2 ** int(np.round(np.log(dom_width_z / dh) / np.log(2.0))) # num. base cells z - -# Define the base mesh -hx = [(dh, nbcx)] -hz = [(dh, nbcz)] -mesh = TreeMesh([hx, hz], x0="CN") - -# Mesh refinement based on topography -mesh.refine_surface( - topo_xyz[:, [0, 2]], - padding_cells_by_level=[0, 0, 4, 4], - finalize=False, -) - -# Mesh refinement near transmitters and receivers. First we need to obtain the -# set of unique electrode locations. -electrode_locations = np.c_[ - dc_data.survey.locations_a, - dc_data.survey.locations_b, - dc_data.survey.locations_m, - dc_data.survey.locations_n, -] - -unique_locations = np.unique( - np.reshape(electrode_locations, (4 * dc_data.survey.nD, 2)), axis=0 -) - -mesh.refine_points(unique_locations, padding_cells_by_level=[4, 4], finalize=False) - -# Refine core mesh region -xp, zp = np.meshgrid([-600.0, 600.0], [-400.0, 0.0]) -xyz = np.c_[mkvc(xp), mkvc(zp)] -mesh.refine_bounding_box(xyz, padding_cells_by_level=[0, 0, 2, 8], finalize=False) - -mesh.finalize() - - -############################################################### -# Project Surveys to Discretized Topography -# ----------------------------------------- -# -# It is important that electrodes are not model as being in the air. Even if the -# electrodes are properly located along surface topography, they may lie above -# the discretized topography. This step is carried out to ensure all electrodes -# like on the discretized surface. -# - -# Create 2D topography. Since our 3D topography only changes in the x direction, -# it is easy to define the 2D topography projected along the survey line. For -# arbitrary topography and for an arbitrary survey orientation, the user must -# define the 2D topography along the survey line. -topo_2d = np.unique(topo_xyz[:, [0, 2]], axis=0) - -# Find cells that lie below surface topography -ind_active = active_from_xyz(mesh, topo_2d) - -# Extract survey from data object -survey = dc_data.survey - -# Shift electrodes to the surface of discretized topography -survey.drape_electrodes_on_topography(mesh, ind_active, option="top") - -# Reset survey in data object -dc_data.survey = survey - - -######################################################## -# Starting/Reference Model and Mapping on Tree Mesh -# --------------------------------------------------- -# -# Here, we would create starting and/or reference models for the DC inversion as -# well as the mapping from the model space to the active cells. Starting and -# reference models can be a constant background value or contain a-priori -# structures. Here, the starting model is the natural log of 0.01 S/m. -# - -# Define conductivity model in S/m (or resistivity model in Ohm m) -air_conductivity = np.log(1e-8) -background_conductivity = np.log(1e-2) - -active_map = maps.InjectActiveCells(mesh, ind_active, np.exp(air_conductivity)) -nC = int(ind_active.sum()) - -conductivity_map = active_map * maps.ExpMap() - -# Define model -starting_conductivity_model = background_conductivity * np.ones(nC) - -############################################## -# Define the Physics of the DC Simulation -# --------------------------------------- -# -# Here, we define the physics of the DC resistivity problem. -# - -# Define the problem. Define the cells below topography and the mapping -simulation = dc.simulation_2d.Simulation2DNodal( - mesh, survey=survey, sigmaMap=conductivity_map, solver=Solver, storeJ=True -) - -####################################################################### -# Define DC Inverse Problem -# ------------------------- -# -# The inverse problem is defined by 3 things: -# -# 1) Data Misfit: a measure of how well our recovered model explains the field data -# 2) Regularization: constraints placed on the recovered model and a priori information -# 3) Optimization: the numerical approach used to solve the inverse problem -# -# - -# Define the data misfit. Here the data misfit is the L2 norm of the weighted -# residual between the observed data and the data predicted for a given model. -# Within the data misfit, the residual between predicted and observed data are -# normalized by the data's standard deviation. -dmis = data_misfit.L2DataMisfit(data=dc_data, simulation=simulation) - -# Define the regularization (model objective function) -reg = regularization.WeightedLeastSquares( - mesh, - active_cells=ind_active, - reference_model=starting_conductivity_model, -) - -reg.reference_model_in_smooth = True # Reference model in smoothness term - -# Define how the optimization problem is solved. Here we will use an -# Inexact Gauss Newton approach. -opt = optimization.InexactGaussNewton(maxIter=40) - -# Here we define the inverse problem that is to be solved -inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) - -####################################################################### -# Define DC Inversion Directives -# ------------------------------ -# -# Here we define any directives that are carried out during the inversion. This -# includes the cooling schedule for the trade-off parameter (beta), stopping -# criteria for the inversion and saving inversion results at each iteration. -# - -# Apply and update sensitivity weighting as the model updates -update_sensitivity_weighting = directives.UpdateSensitivityWeights() - -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e1) - -# Set the rate of reduction in trade-off parameter (beta) each time the -# the inverse problem is solved. And set the number of Gauss-Newton iterations -# for each trade-off paramter value. -beta_schedule = directives.BetaSchedule(coolingFactor=3, coolingRate=2) - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# Setting a stopping criteria for the inversion. -target_misfit = directives.TargetMisfit(chifact=1) - -# Update preconditioner -update_jacobi = directives.UpdatePreconditioner() - -directives_list = [ - update_sensitivity_weighting, - starting_beta, - beta_schedule, - save_iteration, - target_misfit, - update_jacobi, -] - -##################################################################### -# Running the DC Inversion -# ------------------------ -# -# To define the inversion object, we need to define the inversion problem and -# the set of directives. We can then run the inversion. -# - -# Here we combine the inverse problem and the set of directives -dc_inversion = inversion.BaseInversion(inv_prob, directiveList=directives_list) - -# Run inversion -recovered_conductivity_model = dc_inversion.run(starting_conductivity_model) - -############################################################ -# Recreate True Conductivity Model -# -------------------------------- -# - -true_background_conductivity = 1e-2 -true_conductor_conductivity = 1e-1 -true_resistor_conductivity = 1e-3 - -true_conductivity_model = true_background_conductivity * np.ones(len(mesh)) - -ind_conductor = model_builder.get_indices_sphere( - np.r_[-120.0, -180.0], 60.0, mesh.gridCC -) -true_conductivity_model[ind_conductor] = true_conductor_conductivity - -ind_resistor = model_builder.get_indices_sphere(np.r_[120.0, -180.0], 60.0, mesh.gridCC) -true_conductivity_model[ind_resistor] = true_resistor_conductivity - -true_conductivity_model[~ind_active] = np.NaN - -############################################################ -# Plotting True and Recovered Conductivity Model -# ---------------------------------------------- -# - -# Plot True Model -norm = LogNorm(vmin=1e-3, vmax=1e-1) - -fig = plt.figure(figsize=(9, 4)) -ax1 = fig.add_axes([0.14, 0.17, 0.68, 0.7]) -im = mesh.plot_image( - true_conductivity_model, ax=ax1, grid=False, pcolor_opts={"norm": norm} -) -ax1.set_xlim(-600, 600) -ax1.set_ylim(-600, 0) -ax1.set_title("True Conductivity Model") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") - -ax2 = fig.add_axes([0.84, 0.17, 0.03, 0.7]) -cbar = mpl.colorbar.ColorbarBase(ax2, norm=norm, orientation="vertical") -cbar.set_label(r"$\sigma$ (S/m)", rotation=270, labelpad=15, size=12) - -plt.show() - -# # Plot Recovered Model -fig = plt.figure(figsize=(9, 4)) - -recovered_conductivity = conductivity_map * recovered_conductivity_model -recovered_conductivity[~ind_active] = np.NaN - -ax1 = fig.add_axes([0.14, 0.17, 0.68, 0.7]) -mesh.plot_image( - recovered_conductivity, normal="Y", ax=ax1, grid=False, pcolor_opts={"norm": norm} -) -ax1.set_xlim(-600, 600) -ax1.set_ylim(-600, 0) -ax1.set_title("Recovered Conductivity Model") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") - -ax2 = fig.add_axes([0.84, 0.17, 0.03, 0.7]) -cbar = mpl.colorbar.ColorbarBase(ax2, norm=norm, orientation="vertical") -cbar.set_label(r"$\sigma$ (S/m)", rotation=270, labelpad=15, size=12) - -plt.show() - -################################################################### -# Plotting Predicted DC Data and Misfit -# ------------------------------------- -# - -# Predicted data from recovered model -dpred = inv_prob.dpred -dobs = dc_data.dobs -std = dc_data.standard_deviation - -# Plot -fig = plt.figure(figsize=(9, 13)) -data_array = [np.abs(dobs), np.abs(dpred), (dobs - dpred) / std] -plot_title = ["Observed Voltage", "Predicted Voltage", "Normalized Misfit"] -plot_units = ["V/A", "V/A", ""] -scale = ["log", "log", "linear"] - -ax1 = 3 * [None] -cax1 = 3 * [None] -cbar = 3 * [None] -cplot = 3 * [None] - -for ii in range(0, 3): - ax1[ii] = fig.add_axes([0.15, 0.72 - 0.33 * ii, 0.65, 0.21]) - cax1[ii] = fig.add_axes([0.81, 0.72 - 0.33 * ii, 0.03, 0.21]) - cplot[ii] = plot_pseudosection( - survey, - data_array[ii], - "contourf", - ax=ax1[ii], - cax=cax1[ii], - scale=scale[ii], - cbar_label=plot_units[ii], - mask_topography=True, - contourf_opts={"levels": 25, "cmap": mpl.cm.viridis}, - ) - ax1[ii].set_title(plot_title[ii]) - -plt.show() diff --git a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py index f84891fb15..7eedf1d219 100644 --- a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py +++ b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py @@ -2,480 +2,16 @@ 2.5D DC Resistivity Inversion with Sparse Norms =============================================== -Here we invert a line of DC resistivity data to recover an electrical -conductivity model. We formulate the inverse problem as a least-squares -optimization problem. For this tutorial, we focus on the following: +.. important:: - - Defining the survey - - Generating a mesh based on survey geometry - - Including surface topography - - Defining the inverse problem (data misfit, regularization, directives) - - Applying sensitivity weighting - - Plotting the recovered model and data misfit + This tutorial has been moved to `User Tutorials + `_. + Checkout the `Iteratively Re-weighted Least-Squares Inversion + `_ + section in the + `2.5D DC Resistivity Inversion + `_ tutorial. -""" - -######################################################################### -# Import modules -# -------------- -# - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -from matplotlib.colors import LogNorm -import tarfile - -from discretize import TreeMesh -from discretize.utils import mkvc, active_from_xyz - -from simpeg.utils import model_builder -from simpeg import ( - maps, - data_misfit, - regularization, - optimization, - inverse_problem, - inversion, - directives, - utils, -) -from simpeg.electromagnetics.static import resistivity as dc -from simpeg.electromagnetics.static.utils.static_utils import ( - plot_pseudosection, - apparent_resistivity_from_voltage, -) -from simpeg.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc - -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - -mpl.rcParams.update({"font.size": 16}) -# sphinx_gallery_thumbnail_number = 3 - - -############################################# -# Define File Names -# ----------------- -# -# Here we provide the file paths to assets we need to run the inversion. The -# path to the true model conductivity and chargeability models are also -# provided for comparison with the inversion results. These files are stored as a -# tar-file on our google cloud bucket: -# "https://storage.googleapis.com/simpeg/doc-assets/dcr2d.tar.gz" -# - -# storage bucket where we have the data -data_source = "https://storage.googleapis.com/simpeg/doc-assets/dcr2d.tar.gz" - -# download the data -downloaded_data = utils.download(data_source, overwrite=True) - -# unzip the tarfile -tar = tarfile.open(downloaded_data, "r") -tar.extractall() -tar.close() - -# path to the directory containing our data -dir_path = downloaded_data.split(".")[0] + os.path.sep - -# files to work with -topo_filename = dir_path + "topo_xyz.txt" -data_filename = dir_path + "dc_data.obs" - - -############################################# -# Load Data, Define Survey and Plot -# --------------------------------- -# -# Here we load the observed data, define the DC and IP survey geometry and -# plot the data values using pseudo-sections. -# **Warning**: In the following example, the observations file is assumed to be -# sorted by sources -# - -# Load data -topo_xyz = np.loadtxt(str(topo_filename)) -dc_data = read_dcip2d_ubc(data_filename, "volt", "general") - -####################################################################### -# Plot Observed Data in Pseudo-Section -# ------------------------------------ -# -# Here, we demonstrate how to plot 2D data in pseudo-section. -# First, we plot the actual data (voltages) in pseudo-section as a scatter plot. -# This allows us to visualize the pseudo-sensitivity locations for our survey. -# Next, we plot the data as apparent conductivities in pseudo-section with a filled -# contour plot. -# - -# Plot voltages pseudo-section -fig = plt.figure(figsize=(12, 5)) -ax1 = fig.add_axes([0.1, 0.15, 0.75, 0.78]) -plot_pseudosection( - dc_data, - plot_type="scatter", - ax=ax1, - scale="log", - cbar_label="V/A", - scatter_opts={"cmap": mpl.cm.viridis}, -) -ax1.set_title("Normalized Voltages") -plt.show() - -# Get apparent conductivities from volts and survey geometry -apparent_conductivities = 1 / apparent_resistivity_from_voltage( - dc_data.survey, dc_data.dobs -) - -# Plot apparent conductivity pseudo-section -fig = plt.figure(figsize=(12, 5)) -ax1 = fig.add_axes([0.1, 0.15, 0.75, 0.78]) -plot_pseudosection( - dc_data.survey, - apparent_conductivities, - plot_type="contourf", - ax=ax1, - scale="log", - cbar_label="S/m", - mask_topography=True, - contourf_opts={"levels": 20, "cmap": mpl.cm.viridis}, -) -ax1.set_title("Apparent Conductivity") -plt.show() - -#################################################### -# Assign Uncertainties -# -------------------- -# -# Inversion with SimPEG requires that we define the uncertainties on our data. -# This represents our estimate of the standard deviation of the -# noise in our data. For DC data, the uncertainties are 10% of the absolute value. -# -# - -dc_data.standard_deviation = 0.05 * np.abs(dc_data.dobs) - -######################################################## -# Create Tree Mesh -# ------------------ -# -# Here, we create the Tree mesh that will be used invert the DC data -# - -dh = 4 # base cell width -dom_width_x = 3200.0 # domain width x -dom_width_z = 2400.0 # domain width z -nbcx = 2 ** int(np.round(np.log(dom_width_x / dh) / np.log(2.0))) # num. base cells x -nbcz = 2 ** int(np.round(np.log(dom_width_z / dh) / np.log(2.0))) # num. base cells z - -# Define the base mesh -hx = [(dh, nbcx)] -hz = [(dh, nbcz)] -mesh = TreeMesh([hx, hz], x0="CN") - -# Mesh refinement based on topography -mesh.refine_surface( - topo_xyz[:, [0, 2]], - padding_cells_by_level=[0, 0, 4, 4], - finalize=False, -) - -# Mesh refinement near transmitters and receivers. First we need to obtain the -# set of unique electrode locations. -electrode_locations = np.c_[ - dc_data.survey.locations_a, - dc_data.survey.locations_b, - dc_data.survey.locations_m, - dc_data.survey.locations_n, -] - -unique_locations = np.unique( - np.reshape(electrode_locations, (4 * dc_data.survey.nD, 2)), axis=0 -) - -mesh.refine_points(unique_locations, padding_cells_by_level=[4, 4], finalize=False) - -# Refine core mesh region -xp, zp = np.meshgrid([-600.0, 600.0], [-400.0, 0.0]) -xyz = np.c_[mkvc(xp), mkvc(zp)] -mesh.refine_bounding_box(xyz, padding_cells_by_level=[0, 0, 2, 8], finalize=False) - -mesh.finalize() - - -############################################################### -# Project Surveys to Discretized Topography -# ----------------------------------------- -# -# It is important that electrodes are not model as being in the air. Even if the -# electrodes are properly located along surface topography, they may lie above -# the discretized topography. This step is carried out to ensure all electrodes -# like on the discretized surface. -# - -# Create 2D topography. Since our 3D topography only changes in the x direction, -# it is easy to define the 2D topography projected along the survey line. For -# arbitrary topography and for an arbitrary survey orientation, the user must -# define the 2D topography along the survey line. -topo_2d = np.unique(topo_xyz[:, [0, 2]], axis=0) - -# Find cells that lie below surface topography -ind_active = active_from_xyz(mesh, topo_2d) - -# Extract survey from data object -survey = dc_data.survey - -# Shift electrodes to the surface of discretized topography -survey.drape_electrodes_on_topography(mesh, ind_active, option="top") - -# Reset survey in data object -dc_data.survey = survey - - -######################################################## -# Starting/Reference Model and Mapping on Tree Mesh -# --------------------------------------------------- -# -# Here, we would create starting and/or reference models for the DC inversion as -# well as the mapping from the model space to the active cells. Starting and -# reference models can be a constant background value or contain a-priori -# structures. Here, the starting model is the natural log of 0.01 S/m. -# - -# Define conductivity model in S/m (or resistivity model in Ohm m) -air_conductivity = np.log(1e-8) -background_conductivity = np.log(1e-2) - -active_map = maps.InjectActiveCells(mesh, ind_active, np.exp(air_conductivity)) -nC = int(ind_active.sum()) -conductivity_map = active_map * maps.ExpMap() - -# Define model -starting_conductivity_model = background_conductivity * np.ones(nC) - -############################################## -# Define the Physics of the DC Simulation -# --------------------------------------- -# -# Here, we define the physics of the DC resistivity problem. -# - -# Define the problem. Define the cells below topography and the mapping -simulation = dc.simulation_2d.Simulation2DNodal( - mesh, survey=survey, sigmaMap=conductivity_map, solver=Solver, storeJ=True -) - -####################################################################### -# Define DC Inverse Problem -# ------------------------- -# -# The inverse problem is defined by 3 things: -# -# 1) Data Misfit: a measure of how well our recovered model explains the field data -# 2) Regularization: constraints placed on the recovered model and a priori information -# 3) Optimization: the numerical approach used to solve the inverse problem -# -# - -# Define the data misfit. Here the data misfit is the L2 norm of the weighted -# residual between the observed data and the data predicted for a given model. -# Within the data misfit, the residual between predicted and observed data are -# normalized by the data's standard deviation. -dmis = data_misfit.L2DataMisfit(data=dc_data, simulation=simulation) - -# Define the regularization (model objective function). Here, 'p' defines the -# the norm of the smallness term, 'qx' defines the norm of the smoothness -# in x and 'qz' defines the norm of the smoothness in z. -regmap = maps.IdentityMap(nP=int(ind_active.sum())) - -reg = regularization.Sparse( - mesh, - active_cells=ind_active, - reference_model=starting_conductivity_model, - mapping=regmap, - gradient_type="total", - alpha_s=0.01, - alpha_x=1, - alpha_y=1, -) - -reg.reference_model_in_smooth = True # Include reference model in smoothness - -p = 0 -qx = 1 -qz = 1 -reg.norms = [p, qx, qz] - -# Define how the optimization problem is solved. Here we will use an inexact -# Gauss-Newton approach. -opt = optimization.InexactGaussNewton(maxIter=40) - -# Here we define the inverse problem that is to be solved -inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) - -####################################################################### -# Define DC Inversion Directives -# ------------------------------ -# -# Here we define any directives that are carried out during the inversion. This -# includes the cooling schedule for the trade-off parameter (beta), stopping -# criteria for the inversion and saving inversion results at each iteration. -# - -# Apply and update sensitivity weighting as the model updates -update_sensitivity_weighting = directives.UpdateSensitivityWeights() - -# Reach target misfit for L2 solution, then use IRLS until model stops changing. -update_IRLS = directives.Update_IRLS( - max_irls_iterations=25, minGNiter=1, chifact_start=1.0 -) - -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e1) - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# Update preconditioner -update_jacobi = directives.UpdatePreconditioner() - -directives_list = [ - update_sensitivity_weighting, - update_IRLS, - starting_beta, - save_iteration, - update_jacobi, -] - -##################################################################### -# Running the DC Inversion -# ------------------------ -# -# To define the inversion object, we need to define the inversion problem and -# the set of directives. We can then run the inversion. -# - -# Here we combine the inverse problem and the set of directives -dc_inversion = inversion.BaseInversion(inv_prob, directiveList=directives_list) - -# Run inversion -recovered_conductivity_model = dc_inversion.run(starting_conductivity_model) - -############################################################ -# Recreate True Conductivity Model -# -------------------------------- -# - -true_background_conductivity = 1e-2 -true_conductor_conductivity = 1e-1 -true_resistor_conductivity = 1e-3 - -true_conductivity_model = true_background_conductivity * np.ones(len(mesh)) - -ind_conductor = model_builder.get_indices_sphere( - np.r_[-120.0, -180.0], 60.0, mesh.gridCC -) -true_conductivity_model[ind_conductor] = true_conductor_conductivity - -ind_resistor = model_builder.get_indices_sphere(np.r_[120.0, -180.0], 60.0, mesh.gridCC) -true_conductivity_model[ind_resistor] = true_resistor_conductivity - -true_conductivity_model[~ind_active] = np.NaN - -############################################################ -# Plotting True and Recovered Conductivity Model -# ---------------------------------------------- -# - -# Get L2 and sparse recovered model in base 10 -l2_conductivity = conductivity_map * inv_prob.l2model -l2_conductivity[~ind_active] = np.NaN - -recovered_conductivity = conductivity_map * recovered_conductivity_model -recovered_conductivity[~ind_active] = np.NaN - -# Plot True Model -norm = LogNorm(vmin=1e-3, vmax=1e-1) - -fig = plt.figure(figsize=(9, 15)) -ax1 = 3 * [None] -ax2 = 3 * [None] -title_str = [ - "True Conductivity Model", - "Smooth Recovered Model", - "Sparse Recovered Model", -] -plotting_model = [ - true_conductivity_model, - l2_conductivity, - recovered_conductivity, -] - -for ii in range(0, 3): - ax1[ii] = fig.add_axes([0.14, 0.75 - 0.3 * ii, 0.68, 0.2]) - mesh.plot_image( - plotting_model[ii], - ax=ax1[ii], - grid=False, - range_x=[-700, 700], - range_y=[-600, 0], - pcolor_opts={"norm": norm}, - ) - ax1[ii].set_xlim(-600, 600) - ax1[ii].set_ylim(-600, 0) - ax1[ii].set_title(title_str[ii]) - ax1[ii].set_xlabel("x (m)") - ax1[ii].set_ylabel("z (m)") - - ax2[ii] = fig.add_axes([0.84, 0.75 - 0.3 * ii, 0.03, 0.2]) - cbar = mpl.colorbar.ColorbarBase(ax2[ii], norm=norm, orientation="vertical") - cbar.set_label(r"$\sigma$ (S/m)", rotation=270, labelpad=15, size=12) - -plt.show() - -################################################################### -# Plotting Predicted DC Data and Misfit -# ------------------------------------- -# - -# Predicted data from recovered model -dpred = inv_prob.dpred -dobs = dc_data.dobs -std = dc_data.standard_deviation - -# Plot -fig = plt.figure(figsize=(9, 13)) -data_array = [np.abs(dobs), np.abs(dpred), (dobs - dpred) / std] -plot_title = ["Observed Voltage", "Predicted Voltage", "Normalized Misfit"] -plot_units = ["V/A", "V/A", ""] -scale = ["log", "log", "linear"] - -ax1 = 3 * [None] -cax1 = 3 * [None] -cbar = 3 * [None] -cplot = 3 * [None] - -for ii in range(0, 3): - ax1[ii] = fig.add_axes([0.15, 0.72 - 0.33 * ii, 0.65, 0.21]) - cax1[ii] = fig.add_axes([0.81, 0.72 - 0.33 * ii, 0.03, 0.21]) - cplot[ii] = plot_pseudosection( - survey, - data_array[ii], - "contourf", - ax=ax1[ii], - cax=cax1[ii], - scale=scale[ii], - cbar_label=plot_units[ii], - mask_topography=True, - contourf_opts={"levels": 25, "cmap": mpl.cm.viridis}, - ) - ax1[ii].set_title(plot_title[ii]) - -plt.show() +""" diff --git a/tutorials/05-dcr/plot_inv_3_dcr3d.py b/tutorials/05-dcr/plot_inv_3_dcr3d.py index 4ca93a6106..fb156a3d32 100644 --- a/tutorials/05-dcr/plot_inv_3_dcr3d.py +++ b/tutorials/05-dcr/plot_inv_3_dcr3d.py @@ -3,508 +3,13 @@ 3D Least-Squares Inversion of DC Resistivity Data ================================================= -Here we invert 5 lines of DC data to recover an electrical -conductivity model. We formulate the corresponding -inverse problem as a least-squares optimization problem. -For this tutorial, we focus on the following: +.. important:: - - Generating a mesh based on survey geometry - - Including surface topography - - Defining the inverse problem (data misfit, regularization, directives) - - Applying sensitivity weighting - - Plotting the recovered model and data misfit + This tutorial has been moved to `User Tutorials + `_. -The DC data are measured voltages normalized by the source current in V/A. + Checkout the `3D DC Resistivity Inversion + `_ tutorial. """ - -################################################################# -# Import Modules -# -------------- -# - - -import os -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt -import tarfile - -from discretize import TreeMesh -from discretize.utils import refine_tree_xyz, active_from_xyz - -from simpeg.utils import model_builder -from simpeg.utils.io_utils.io_utils_electromagnetics import read_dcip_xyz -from simpeg import ( - maps, - data_misfit, - regularization, - optimization, - inverse_problem, - inversion, - directives, - utils, -) -from simpeg.electromagnetics.static import resistivity as dc -from simpeg.electromagnetics.static.utils.static_utils import ( - apparent_resistivity_from_voltage, -) - -# To plot DC/IP data in 3D, the user must have the plotly package -try: - import plotly - from simpeg.electromagnetics.static.utils.static_utils import plot_3d_pseudosection - - has_plotly = True -except ImportError: - has_plotly = False - pass - -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - -mpl.rcParams.update({"font.size": 16}) - -# sphinx_gallery_thumbnail_number = 3 - -########################################################## -# Download Assets -# --------------- -# -# Here we provide the file paths to assets we need to run the inversion. The -# path to the true model conductivity and chargeability models are also -# provided for comparison with the inversion results. These files are stored as a -# tar-file on our google cloud bucket: -# "https://storage.googleapis.com/simpeg/doc-assets/dcr3d.tar.gz" -# -# -# - -# storage bucket where we have the data -data_source = "https://storage.googleapis.com/simpeg/doc-assets/dcr3d.tar.gz" - -# download the data -downloaded_data = utils.download(data_source, overwrite=True) - -# unzip the tarfile -tar = tarfile.open(downloaded_data, "r") -tar.extractall() -tar.close() - -# path to the directory containing our data -dir_path = downloaded_data.split(".")[0] + os.path.sep - -# files to work with -topo_filename = dir_path + "topo_xyz.txt" -dc_data_filename = dir_path + "dc_data.xyz" - -######################################################## -# Load Data and Topography -# ------------------------ -# -# Here we load the observed data and topography. -# -# - -topo_xyz = np.loadtxt(str(topo_filename)) - -dc_data = read_dcip_xyz( - dc_data_filename, - "volt", - data_header="V/A", - uncertainties_header="UNCERT", - is_surface_data=False, -) - - -########################################################## -# Plot Observed Data in Pseudosection -# ----------------------------------- -# -# Here we plot the observed DC and IP data in 3D pseudosections. -# To use this utility, you must have Python's *plotly* package. -# Here, we represent the DC data as apparent conductivities -# and the IP data as apparent chargeabilities. -# - -# Convert predicted data to apparent conductivities -apparent_conductivity = 1 / apparent_resistivity_from_voltage( - dc_data.survey, - dc_data.dobs, -) - -if has_plotly: - fig = plot_3d_pseudosection( - dc_data.survey, - apparent_conductivity, - scale="log", - units="S/m", - plane_distance=15, - ) - - fig.update_layout( - title_text="Apparent Conductivity", - title_x=0.5, - title_font_size=24, - width=650, - height=500, - scene_camera=dict( - center=dict(x=0, y=0, z=-0.4), eye=dict(x=1.5, y=-1.5, z=1.8) - ), - ) - - plotly.io.show(fig) - -else: - print("INSTALL 'PLOTLY' TO VISUALIZE 3D PSEUDOSECTIONS") - - -#################################################### -# Assign Uncertainties -# -------------------- -# -# Inversion with SimPEG requires that we define the uncertainties on our data. -# This represents our estimate of the standard deviation of the -# noise in our data. For DC data, the uncertainties are 10% of the absolute value. -# -# - -dc_data.standard_deviation = 0.1 * np.abs(dc_data.dobs) - - -################################################################ -# Create Tree Mesh -# ---------------- -# -# Here, we create the Tree mesh that will be used to invert -# DC data. -# - - -dh = 25.0 # base cell width -dom_width_x = 6000.0 # domain width x -dom_width_y = 6000.0 # domain width y -dom_width_z = 4000.0 # domain width z -nbcx = 2 ** int(np.round(np.log(dom_width_x / dh) / np.log(2.0))) # num. base cells x -nbcy = 2 ** int(np.round(np.log(dom_width_y / dh) / np.log(2.0))) # num. base cells y -nbcz = 2 ** int(np.round(np.log(dom_width_z / dh) / np.log(2.0))) # num. base cells z - -# Define the base mesh -hx = [(dh, nbcx)] -hy = [(dh, nbcy)] -hz = [(dh, nbcz)] -mesh = TreeMesh([hx, hy, hz], x0="CCN") - -# Mesh refinement based on topography -k = np.sqrt(np.sum(topo_xyz[:, 0:2] ** 2, axis=1)) < 1200 -mesh = refine_tree_xyz( - mesh, topo_xyz[k, :], octree_levels=[0, 6, 8], method="surface", finalize=False -) - -# Mesh refinement near sources and receivers. -electrode_locations = np.r_[ - dc_data.survey.locations_a, - dc_data.survey.locations_b, - dc_data.survey.locations_m, - dc_data.survey.locations_n, -] -unique_locations = np.unique(electrode_locations, axis=0) -mesh = refine_tree_xyz( - mesh, unique_locations, octree_levels=[4, 6, 4], method="radial", finalize=False -) - -# Finalize the mesh -mesh.finalize() - -####################################################### -# Project Electrodes to Discretized Topography -# -------------------------------------------- -# -# It is important that electrodes are not modeled as being in the air. Even if the -# electrodes are properly located along surface topography, they may lie above -# the discretized topography. This step is carried out to ensure all electrodes -# lie on the discretized surface. -# - -# Find cells that lie below surface topography -ind_active = active_from_xyz(mesh, topo_xyz) - -# Extract survey from data object -dc_survey = dc_data.survey - -# Shift electrodes to the surface of discretized topography -dc_survey.drape_electrodes_on_topography(mesh, ind_active, option="top") - -# Reset survey in data object -dc_data.survey = dc_survey - -################################################################# -# Starting/Reference Model and Mapping on OcTree Mesh -# --------------------------------------------------- -# -# Here, we create starting and/or reference models for the DC inversion as -# well as the mapping from the model space to the active cells. Starting and -# reference models can be a constant background value or contain a-priori -# structures. Here, the starting model is the natural log of 0.01 S/m. -# -# - -# Define conductivity model in S/m (or resistivity model in Ohm m) -air_conductivity = np.log(1e-8) -background_conductivity = np.log(1e-2) - -# Define the mapping from active cells to the entire domain -active_map = maps.InjectActiveCells(mesh, ind_active, np.exp(air_conductivity)) -nC = int(ind_active.sum()) - -# Define the mapping from the model to the conductivity of the entire domain -conductivity_map = active_map * maps.ExpMap() - -# Define starting model -starting_conductivity_model = background_conductivity * np.ones(nC) - -############################################################### -# Define the Physics of the DC Simulation -# --------------------------------------- -# -# Here, we define the physics of the DC resistivity simulation. -# -# - -dc_simulation = dc.simulation.Simulation3DNodal( - mesh, survey=dc_survey, sigmaMap=conductivity_map, solver=Solver, storeJ=True -) - -################################################################# -# Define DC Inverse Problem -# ------------------------- -# -# The inverse problem is defined by 3 things: -# -# 1) Data Misfit: a measure of how well our recovered model explains the field data -# 2) Regularization: constraints placed on the recovered model and a priori information -# 3) Optimization: the numerical approach used to solve the inverse problem -# -# - - -# Define the data misfit. Here the data misfit is the L2 norm of the weighted -# residual between the observed data and the data predicted for a given model. -# Within the data misfit, the residual between predicted and observed data are -# normalized by the data's standard deviation. -dc_data_misfit = data_misfit.L2DataMisfit(data=dc_data, simulation=dc_simulation) - -# Define the regularization (model objective function) -dc_regularization = regularization.WeightedLeastSquares( - mesh, - active_cells=ind_active, - reference_model=starting_conductivity_model, -) - -dc_regularization.reference_model_in_smooth = ( - True # Include reference model in smoothness -) - -# Define how the optimization problem is solved. -dc_optimization = optimization.InexactGaussNewton( - maxIter=15, maxIterLS=20, maxIterCG=30, tolCG=1e-2 -) - -# Here we define the inverse problem that is to be solved -dc_inverse_problem = inverse_problem.BaseInvProblem( - dc_data_misfit, dc_regularization, dc_optimization -) - -################################################# -# Define DC Inversion Directives -# ------------------------------ -# -# Here we define any directives that are carried out during the inversion. This -# includes the cooling schedule for the trade-off parameter (beta), stopping -# criteria for the inversion and saving inversion results at each iteration. -# -# - -# Apply and update sensitivity weighting as the model updates -update_sensitivity_weighting = directives.UpdateSensitivityWeights() - -# Defining a starting value for the trade-off parameter (beta) between the data -# misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e1) - -# Set the rate of reduction in trade-off parameter (beta) each time the -# the inverse problem is solved. And set the number of Gauss-Newton iterations -# for each trade-off paramter value. -beta_schedule = directives.BetaSchedule(coolingFactor=2.5, coolingRate=2) - -# Options for outputting recovered models and predicted data for each beta. -save_iteration = directives.SaveOutputEveryIteration(save_txt=False) - -# Setting a stopping criteria for the inversion. -target_misfit = directives.TargetMisfit(chifact=1) - -# Apply and update preconditioner as the model updates -update_jacobi = directives.UpdatePreconditioner() - -directives_list = [ - update_sensitivity_weighting, - starting_beta, - beta_schedule, - save_iteration, - target_misfit, - update_jacobi, -] - -######################################################### -# Running the DC Inversion -# ------------------------ -# -# To define the inversion object, we need to define the inversion problem and -# the set of directives. We can then run the inversion. -# - -# Here we combine the inverse problem and the set of directives -dc_inversion = inversion.BaseInversion( - dc_inverse_problem, directiveList=directives_list -) - -# Run inversion -recovered_conductivity_model = dc_inversion.run(starting_conductivity_model) - - -############################################################### -# Recreate True Conductivity Model -# -------------------------------- -# - -# Define conductivity model in S/m (or resistivity model in Ohm m) -background_value = 1e-2 -conductor_value = 1e-1 -resistor_value = 1e-3 - -# Define model -true_conductivity_model = background_value * np.ones(nC) - -ind_conductor = model_builder.get_indices_sphere( - np.r_[-350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] -) -true_conductivity_model[ind_conductor] = conductor_value - -ind_resistor = model_builder.get_indices_sphere( - np.r_[350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] -) -true_conductivity_model[ind_resistor] = resistor_value -true_conductivity_model_log10 = np.log10(true_conductivity_model) - - -############################################################### -# Plotting True and Recovered Conductivity Model -# ---------------------------------------------- -# - - -# Plot True Model -fig = plt.figure(figsize=(10, 4)) - -plotting_map = maps.InjectActiveCells(mesh, ind_active, np.nan) - -ax1 = fig.add_axes([0.15, 0.15, 0.67, 0.75]) -mesh.plot_slice( - plotting_map * true_conductivity_model_log10, - ax=ax1, - normal="Y", - ind=int(len(mesh.h[1]) / 2), - grid=False, - clim=(true_conductivity_model_log10.min(), true_conductivity_model_log10.max()), - pcolor_opts={"cmap": mpl.cm.viridis}, -) -ax1.set_title("True Conductivity Model") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") -ax1.set_xlim([-1000, 1000]) -ax1.set_ylim([-1000, 0]) - -ax2 = fig.add_axes([0.84, 0.15, 0.03, 0.75]) -norm = mpl.colors.Normalize( - vmin=true_conductivity_model_log10.min(), vmax=true_conductivity_model_log10.max() -) -cbar = mpl.colorbar.ColorbarBase( - ax2, cmap=mpl.cm.viridis, norm=norm, orientation="vertical", format="$10^{%.1f}$" -) -cbar.set_label("Conductivity [S/m]", rotation=270, labelpad=15, size=12) - -# Plot recovered model -recovered_conductivity_model_log10 = np.log10(np.exp(recovered_conductivity_model)) - -fig = plt.figure(figsize=(10, 4)) - -ax1 = fig.add_axes([0.15, 0.15, 0.67, 0.75]) -mesh.plot_slice( - plotting_map * recovered_conductivity_model_log10, - ax=ax1, - normal="Y", - ind=int(len(mesh.h[1]) / 2), - grid=False, - clim=(true_conductivity_model_log10.min(), true_conductivity_model_log10.max()), - pcolor_opts={"cmap": mpl.cm.viridis}, -) -ax1.set_title("Recovered Conductivity Model") -ax1.set_xlabel("x (m)") -ax1.set_ylabel("z (m)") -ax1.set_xlim([-1000, 1000]) -ax1.set_ylim([-1000, 0]) - -ax2 = fig.add_axes([0.84, 0.15, 0.03, 0.75]) -norm = mpl.colors.Normalize( - vmin=true_conductivity_model_log10.min(), vmax=true_conductivity_model_log10.max() -) -cbar = mpl.colorbar.ColorbarBase( - ax2, cmap=mpl.cm.viridis, norm=norm, orientation="vertical", format="$10^{%.1f}$" -) -cbar.set_label("Conductivity [S/m]", rotation=270, labelpad=15, size=12) -plt.show() - -####################################################################### -# Plotting Normalized Data Misfit or Predicted DC Data -# ---------------------------------------------------- -# -# To see how well the recovered model reproduces the observed data, -# it is a good idea to compare the predicted and observed data. -# Here, we accomplish this by plotting the normalized misfit. -# - -# Predicted data from recovered model -dpred_dc = dc_inverse_problem.dpred - -# Compute the normalized data misfit -dc_normalized_misfit = (dc_data.dobs - dpred_dc) / dc_data.standard_deviation - -if has_plotly: - # Plot IP Data - fig = plot_3d_pseudosection( - dc_data.survey, - dc_normalized_misfit, - scale="linear", - units="", - vlim=[-2, 2], - plane_distance=15, - ) - - fig.update_layout( - title_text="Normalized Data Misfit", - title_x=0.5, - title_font_size=24, - width=650, - height=500, - scene_camera=dict( - center=dict(x=0, y=0, z=-0.4), eye=dict(x=1.5, y=-1.5, z=1.8) - ), - ) - - plotly.io.show(fig) - -else: - print("INSTALL 'PLOTLY' TO VISUALIZE 3D PSEUDOSECTIONS") diff --git a/tutorials/06-ip/plot_fwd_2_dcip2d.py b/tutorials/06-ip/plot_fwd_2_dcip2d.py index 4e1add360e..4c89e6bf83 100644 --- a/tutorials/06-ip/plot_fwd_2_dcip2d.py +++ b/tutorials/06-ip/plot_fwd_2_dcip2d.py @@ -49,11 +49,6 @@ import matplotlib.pyplot as plt from matplotlib.colors import LogNorm -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - mpl.rcParams.update({"font.size": 16}) write_output = False @@ -245,9 +240,7 @@ # argument *rhoMap* is defined, the simulation will expect a resistivity model. # -dc_simulation = dc.Simulation2DNodal( - mesh, survey=dc_survey, sigmaMap=conductivity_map, solver=Solver -) +dc_simulation = dc.Simulation2DNodal(mesh, survey=dc_survey, sigmaMap=conductivity_map) # Predict the data by running the simulation. The data are the raw voltage in # units of volts. @@ -320,7 +313,7 @@ ) # Define survey -ip_survey = ip.survey.Survey(source_list, survey_type=survey_type) +ip_survey = ip.survey.Survey(source_list) # Drape over discrete topography ip_survey.drape_electrodes_on_topography(mesh, ind_active, option="top") @@ -397,7 +390,6 @@ survey=ip_survey, etaMap=chargeability_map, sigma=conductivity_map * conductivity_model, - solver=Solver, ) # Run forward simulation and predicted IP data. The data are the voltage (V) diff --git a/tutorials/06-ip/plot_fwd_3_dcip3d.py b/tutorials/06-ip/plot_fwd_3_dcip3d.py index dc5a07db96..3a6f1a2ff7 100644 --- a/tutorials/06-ip/plot_fwd_3_dcip3d.py +++ b/tutorials/06-ip/plot_fwd_3_dcip3d.py @@ -57,10 +57,6 @@ has_plotly = False pass -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) write_output = False @@ -262,9 +258,7 @@ # # Define the DC simulation -dc_simulation = dc.Simulation3DNodal( - mesh, survey=dc_survey, sigmaMap=conductivity_map, solver=Solver -) +dc_simulation = dc.Simulation3DNodal(mesh, survey=dc_survey, sigmaMap=conductivity_map) # Predict the data by running the simulation. The data are the measured voltage # normalized by the source current in units of V/A. @@ -360,7 +354,7 @@ ) # Define survey -ip_survey = ip.survey.Survey(source_list, survey_type=survey_type) +ip_survey = ip.survey.Survey(source_list) # Drape to discretized topography as before ip_survey.drape_electrodes_on_topography(mesh, ind_active, option="top") @@ -439,7 +433,6 @@ survey=ip_survey, etaMap=chargeability_map, sigma=conductivity_map * conductivity_model, - solver=Solver, ) # Run forward simulation and predicted IP data. The data are the voltage (V) diff --git a/tutorials/06-ip/plot_inv_2_dcip2d.py b/tutorials/06-ip/plot_inv_2_dcip2d.py index 72cf5c2639..19529d1eaa 100644 --- a/tutorials/06-ip/plot_inv_2_dcip2d.py +++ b/tutorials/06-ip/plot_inv_2_dcip2d.py @@ -54,10 +54,6 @@ ) from simpeg.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) # sphinx_gallery_thumbnail_number = 7 @@ -280,7 +276,7 @@ # Define the problem. Define the cells below topography and the mapping dc_simulation = dc.Simulation2DNodal( - mesh, survey=dc_survey, sigmaMap=conductivity_map, solver=Solver, storeJ=True + mesh, survey=dc_survey, sigmaMap=conductivity_map, storeJ=True ) ####################################################################### @@ -395,7 +391,7 @@ ind_resistor = model_builder.get_indices_sphere(np.r_[120.0, -180.0], 60.0, mesh.gridCC) true_conductivity_model[ind_resistor] = true_resistor_conductivity -true_conductivity_model[~ind_active] = np.NaN +true_conductivity_model[~ind_active] = np.nan # Plot True Model norm = LogNorm(vmin=1e-3, vmax=1e-1) @@ -421,7 +417,7 @@ fig = plt.figure(figsize=(9, 4)) recovered_conductivity = conductivity_map * recovered_conductivity_model -recovered_conductivity[~ind_active] = np.NaN +recovered_conductivity[~ind_active] = np.nan ax1 = fig.add_axes([0.14, 0.17, 0.68, 0.7]) mesh.plot_image( @@ -519,7 +515,6 @@ survey=ip_survey, etaMap=chargeability_map, sigma=conductivity_map * recovered_conductivity_model, - solver=Solver, storeJ=True, ) @@ -546,7 +541,7 @@ # Define how the optimization problem is solved. Here it is a projected # Gauss Newton with Conjugate Gradient solver. ip_optimization = optimization.ProjectedGNCG( - maxIter=15, lower=0.0, upper=1000.0, maxIterCG=30, tolCG=1e-2 + maxIter=15, lower=0.0, upper=1000.0, cg_maxiter=30, cg_rtol=1e-3 ) # Here we define the inverse problem that is to be solved @@ -601,10 +596,10 @@ true_chargeability_model = np.zeros(len(mesh)) true_chargeability_model[ind_conductor] = sphere_chargeability -true_chargeability_model[~ind_active] = np.NaN +true_chargeability_model[~ind_active] = np.nan recovered_chargeability = chargeability_map * recovered_chargeability_model -recovered_chargeability[~ind_active] = np.NaN +recovered_chargeability[~ind_active] = np.nan # Plot True Model fig = plt.figure(figsize=(9, 4)) diff --git a/tutorials/06-ip/plot_inv_3_dcip3d.py b/tutorials/06-ip/plot_inv_3_dcip3d.py index 94b4cd9407..1c5aa8f9bc 100644 --- a/tutorials/06-ip/plot_inv_3_dcip3d.py +++ b/tutorials/06-ip/plot_inv_3_dcip3d.py @@ -63,11 +63,6 @@ has_plotly = False pass -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - mpl.rcParams.update({"font.size": 16}) # sphinx_gallery_thumbnail_number = 7 @@ -323,7 +318,7 @@ # dc_simulation = dc.Simulation3DNodal( - mesh, survey=dc_survey, sigmaMap=conductivity_map, solver=Solver, storeJ=True + mesh, survey=dc_survey, sigmaMap=conductivity_map, storeJ=True ) ################################################################# @@ -357,7 +352,9 @@ ) # Define how the optimization problem is solved. -dc_optimization = optimization.InexactGaussNewton(maxIter=15, maxIterCG=30, tolCG=1e-2) +dc_optimization = optimization.InexactGaussNewton( + maxIter=15, cg_maxiter=30, cg_rtol=1e-2 +) # Here we define the inverse problem that is to be solved dc_inverse_problem = inverse_problem.BaseInvProblem( @@ -591,7 +588,6 @@ survey=ip_survey, etaMap=chargeability_map, sigma=conductivity_map * recovered_conductivity_model, - solver=Solver, storeJ=True, ) @@ -618,7 +614,7 @@ # Define how the optimization problem is solved. ip_optimization = optimization.ProjectedGNCG( - maxIter=15, lower=0.0, upper=10, maxIterCG=30, tolCG=1e-2 + maxIter=15, lower=0.0, upper=10, cg_maxiter=30, cg_rtol=1e-3 ) # Here we define the inverse problem that is to be solved diff --git a/tutorials/07-fdem/plot_fwd_2_fem_cyl.py b/tutorials/07-fdem/plot_fwd_2_fem_cyl.py index 9bace19d92..450db8f27c 100644 --- a/tutorials/07-fdem/plot_fwd_2_fem_cyl.py +++ b/tutorials/07-fdem/plot_fwd_2_fem_cyl.py @@ -34,11 +34,6 @@ import matplotlib as mpl import matplotlib.pyplot as plt -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - write_file = False # sphinx_gallery_thumbnail_number = 2 @@ -184,7 +179,9 @@ # simulation = fdem.simulation.Simulation3DMagneticFluxDensity( - mesh, survey=survey, sigmaMap=model_map, solver=Solver + mesh, + survey=survey, + sigmaMap=model_map, ) ###################################################### diff --git a/tutorials/07-fdem/plot_fwd_3_fem_3d.py b/tutorials/07-fdem/plot_fwd_3_fem_3d.py index f49bb96056..b7c4345257 100644 --- a/tutorials/07-fdem/plot_fwd_3_fem_3d.py +++ b/tutorials/07-fdem/plot_fwd_3_fem_3d.py @@ -40,10 +40,6 @@ import matplotlib as mpl import matplotlib.pyplot as plt -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver save_file = False @@ -224,7 +220,7 @@ # simulation = fdem.simulation.Simulation3DMagneticFluxDensity( - mesh, survey=survey, sigmaMap=model_map, solver=Solver + mesh, survey=survey, sigmaMap=model_map ) ###################################################### diff --git a/tutorials/07-fdem/plot_inv_1_em1dfm.py b/tutorials/07-fdem/plot_inv_1_em1dfm.py index f5e5366765..bb6b88a90a 100644 --- a/tutorials/07-fdem/plot_inv_1_em1dfm.py +++ b/tutorials/07-fdem/plot_inv_1_em1dfm.py @@ -244,7 +244,7 @@ # Define how the optimization problem is solved. Here we will use an inexact # Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.ProjectedGNCG(maxIter=50, maxIterLS=20, maxIterCG=30, tolCG=1e-3) +opt = optimization.ProjectedGNCG(maxIter=50, maxIterLS=20, cg_maxiter=30, cg_rtol=1e-3) # Define the inverse problem inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) @@ -270,9 +270,7 @@ save_iteration = directives.SaveOutputEveryIteration(save_txt=False) # Directive for the IRLS -update_IRLS = directives.Update_IRLS( - max_irls_iterations=30, minGNiter=1, coolEpsFact=1.5, update_beta=True -) +update_IRLS = directives.UpdateIRLS(max_irls_iterations=30, irls_cooling_factor=1.5) # Updating the preconditionner if it is model dependent. update_jacobi = directives.UpdatePreconditioner() diff --git a/tutorials/08-tdem/plot_fwd_2_tem_cyl.py b/tutorials/08-tdem/plot_fwd_2_tem_cyl.py index f3b81c0361..fe6da5d38c 100644 --- a/tutorials/08-tdem/plot_fwd_2_tem_cyl.py +++ b/tutorials/08-tdem/plot_fwd_2_tem_cyl.py @@ -36,11 +36,6 @@ import matplotlib as mpl import matplotlib.pyplot as plt -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - write_file = False # sphinx_gallery_thumbnail_number = 2 @@ -204,7 +199,7 @@ # simulation = tdem.simulation.Simulation3DMagneticFluxDensity( - mesh, survey=survey, sigmaMap=model_map, solver=Solver + mesh, survey=survey, sigmaMap=model_map ) # Set the time-stepping for the simulation diff --git a/tutorials/08-tdem/plot_fwd_3_tem_3d.py b/tutorials/08-tdem/plot_fwd_3_tem_3d.py index 9b9aecbf79..ab7423170a 100644 --- a/tutorials/08-tdem/plot_fwd_3_tem_3d.py +++ b/tutorials/08-tdem/plot_fwd_3_tem_3d.py @@ -40,10 +40,6 @@ import matplotlib.pyplot as plt import os -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver save_file = False @@ -284,7 +280,7 @@ # simulation = tdem.simulation.Simulation3DMagneticFluxDensity( - mesh, survey=survey, sigmaMap=model_map, solver=Solver, t0=-0.002 + mesh, survey=survey, sigmaMap=model_map, t0=-0.002 ) # Set the time-stepping for the simulation diff --git a/tutorials/08-tdem/plot_inv_1_em1dtm.py b/tutorials/08-tdem/plot_inv_1_em1dtm.py index e0dcede58a..f5e4bfa0a0 100644 --- a/tutorials/08-tdem/plot_inv_1_em1dtm.py +++ b/tutorials/08-tdem/plot_inv_1_em1dtm.py @@ -233,7 +233,7 @@ # Define how the optimization problem is solved. Here we will use an inexact # Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.ProjectedGNCG(maxIter=100, maxIterLS=20, maxIterCG=30, tolCG=1e-3) +opt = optimization.ProjectedGNCG(maxIter=100, maxIterLS=20, cg_maxiter=30, cg_rtol=1e-3) # Define the inverse problem inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) @@ -250,7 +250,7 @@ # Defining a starting value for the trade-off parameter (beta) between the data # misfit and the regularization. -starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e1) +starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e2) # Update the preconditionner update_Jacobi = directives.UpdatePreconditioner() @@ -259,9 +259,7 @@ save_iteration = directives.SaveOutputEveryIteration(save_txt=False) # Directives for the IRLS -update_IRLS = directives.Update_IRLS( - max_irls_iterations=30, minGNiter=1, coolEpsFact=1.5, update_beta=True -) +update_IRLS = directives.UpdateIRLS(max_irls_iterations=30, irls_cooling_factor=1.5) # Updating the preconditionner if it is model dependent. update_jacobi = directives.UpdatePreconditioner() diff --git a/tutorials/10-vrm/plot_fwd_1_vrm_layer.py b/tutorials/10-vrm/plot_fwd_1_vrm_layer.py index b783113a15..4553ce56b1 100644 --- a/tutorials/10-vrm/plot_fwd_1_vrm_layer.py +++ b/tutorials/10-vrm/plot_fwd_1_vrm_layer.py @@ -173,7 +173,7 @@ simulation = vrm.Simulation3DLinear( mesh, survey=survey, - indActive=ind_active, + active_cells=ind_active, refinement_factor=2, refinement_distance=[2.0, 4.0], ) diff --git a/tutorials/10-vrm/plot_fwd_2_vrm_topsoil.py b/tutorials/10-vrm/plot_fwd_2_vrm_topsoil.py index a1a60bec98..6f2857eb77 100644 --- a/tutorials/10-vrm/plot_fwd_2_vrm_topsoil.py +++ b/tutorials/10-vrm/plot_fwd_2_vrm_topsoil.py @@ -228,7 +228,7 @@ simulation = vrm.Simulation3DLinear( mesh, survey=survey, - indActive=ind_active, + active_cells=ind_active, refinement_factor=2, refinement_distance=[2.0, 4.0], ) diff --git a/tutorials/10-vrm/plot_fwd_3_vrm_tem.py b/tutorials/10-vrm/plot_fwd_3_vrm_tem.py index fece2a72ec..0d7adc2f5f 100644 --- a/tutorials/10-vrm/plot_fwd_3_vrm_tem.py +++ b/tutorials/10-vrm/plot_fwd_3_vrm_tem.py @@ -39,11 +39,6 @@ import matplotlib.pyplot as plt import matplotlib as mpl -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - # sphinx_gallery_thumbnail_number = 3 ################################################################### @@ -156,7 +151,9 @@ time_steps = [(5e-06, 20), (0.0001, 20), (0.001, 21)] tdem_simulation = tdem.simulation.Simulation3DMagneticFluxDensity( - mesh, survey=tdem_survey, sigmaMap=model_map, solver=Solver + mesh, + survey=tdem_survey, + sigmaMap=model_map, ) tdem_simulation.time_steps = time_steps @@ -256,7 +253,7 @@ vrm_simulation = vrm.Simulation3DLogUniform( mesh, survey=vrm_survey, - indActive=ind_active, + active_cells=ind_active, refinement_factor=1, refinement_distance=[100.0], chi0=chi0_model, diff --git a/tutorials/12-seismic/plot_inv_1_tomography_2D.py b/tutorials/12-seismic/plot_inv_1_tomography_2D.py index 896def24fd..c25adc23c9 100644 --- a/tutorials/12-seismic/plot_inv_1_tomography_2D.py +++ b/tutorials/12-seismic/plot_inv_1_tomography_2D.py @@ -223,7 +223,7 @@ # Define how the optimization problem is solved. opt = optimization.ProjectedGNCG( - maxIter=100, lower=0.0, upper=1e6, maxIterLS=20, maxIterCG=10, tolCG=1e-4 + maxIter=100, lower=0.0, upper=1e6, maxIterLS=20, cg_maxiter=10, cg_rtol=1e-3 ) # Here we define the inverse problem that is to be solved @@ -240,11 +240,11 @@ # # Reach target misfit for L2 solution, then use IRLS until model stops changing. -update_IRLS = directives.Update_IRLS( +update_IRLS = directives.UpdateIRLS( f_min_change=1e-4, max_irls_iterations=30, - coolEpsFact=1.5, - beta_tol=1e-2, + irls_cooling_factor=1.5, + misfit_tolerance=1e-2, ) # Defining a starting value for the trade-off parameter (beta) between the data diff --git a/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py b/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py index 951807633a..ae20614ebe 100755 --- a/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py +++ b/tutorials/13-joint_inversion/plot_inv_3_cross_gradient_pf.py @@ -288,7 +288,7 @@ survey=survey_grav, mesh=mesh, rhoMap=wires.density, - ind_active=ind_active, + active_cells=ind_active, engine="choclo", ) @@ -297,7 +297,7 @@ mesh=mesh, model_type="scalar", chiMap=wires.susceptibility, - ind_active=ind_active, + active_cells=ind_active, ) @@ -343,8 +343,8 @@ lower=-2.0, upper=2.0, maxIterLS=20, - maxIterCG=100, - tolCG=1e-3, + cg_maxiter=100, + cg_rtol=1e-3, tolX=1e-3, ) @@ -418,10 +418,10 @@ # values on active cells. true_model_dens = np.loadtxt(dir_path + "true_model_dens.txt") -true_model_dens[~ind_active] = np.NaN +true_model_dens[~ind_active] = np.nan true_model_susc = np.loadtxt(dir_path + "true_model_susc.txt") -true_model_susc[~ind_active] = np.NaN +true_model_susc[~ind_active] = np.nan # Plot True Model fig = plt.figure(figsize=(9, 8)) diff --git a/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py b/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py index d4a1e3cc13..de1c71c773 100644 --- a/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py +++ b/tutorials/14-pgi/plot_inv_1_joint_pf_pgi_full_info_tutorial.py @@ -233,7 +233,7 @@ survey=data_grav.survey, mesh=mesh, rhoMap=wires.den, - ind_active=actv, + active_cells=actv, engine="choclo", ) dmis_grav = data_misfit.L2DataMisfit(data=data_grav, simulation=simulation_grav) @@ -242,7 +242,7 @@ survey=data_mag.survey, mesh=mesh, chiMap=wires.sus, - ind_active=actv, + active_cells=actv, ) dmis_mag = data_misfit.L2DataMisfit(data=data_mag, simulation=simulation_mag) @@ -394,8 +394,8 @@ lower=lowerbound, upper=upperbound, maxIterLS=20, - maxIterCG=100, - tolCG=1e-4, + cg_maxiter=100, + cg_rtol=1e-3, ) # create inverse problem invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) diff --git a/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py b/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py index 78582411f1..5132e90456 100644 --- a/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py +++ b/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py @@ -234,7 +234,7 @@ survey=data_grav.survey, mesh=mesh, rhoMap=wires.den, - ind_active=actv, + active_cells=actv, engine="choclo", ) dmis_grav = data_misfit.L2DataMisfit(data=data_grav, simulation=simulation_grav) @@ -243,7 +243,7 @@ survey=data_mag.survey, mesh=mesh, chiMap=wires.sus, - ind_active=actv, + active_cells=actv, ) dmis_mag = data_misfit.L2DataMisfit(data=data_mag, simulation=simulation_mag) @@ -424,8 +424,8 @@ lower=lowerbound, upper=upperbound, maxIterLS=20, - maxIterCG=100, - tolCG=1e-4, + cg_maxiter=100, + cg_rtol=1e-3, ) # create inverse problem invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) diff --git a/tutorials/_temporary/plot_4c_fdem3d_inversion.py b/tutorials/_temporary/plot_4c_fdem3d_inversion.py index c035e10caf..06006bf9a0 100644 --- a/tutorials/_temporary/plot_4c_fdem3d_inversion.py +++ b/tutorials/_temporary/plot_4c_fdem3d_inversion.py @@ -46,11 +46,6 @@ utils, ) -try: - from pymatsolver import Pardiso as Solver -except ImportError: - from simpeg import SolverLU as Solver - # sphinx_gallery_thumbnail_number = 3 ############################################# @@ -289,7 +284,9 @@ # simulation = fdem.simulation.Simulation3DMagneticFluxDensity( - mesh, survey=survey, sigmaMap=model_map, Solver=Solver + mesh, + survey=survey, + sigmaMap=model_map, ) @@ -324,9 +321,9 @@ # Define how the optimization problem is solved. Here we will use a projected # Gauss-Newton approach that employs the conjugate gradient solver. # opt = optimization.ProjectedGNCG( -# maxIterCG=5, tolCG=1e-2, lower=-10, upper=5 +# cg_maxiter=5, cg_rtol=1e-2, lower=-10, upper=5 # ) -opt = optimization.InexactGaussNewton(maxIterCG=5, tolCG=1e-2) +opt = optimization.InexactGaussNewton(cg_maxiter=5, cg_rtol=1e-2) # Here we define the inverse problem that is to be solved inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) diff --git a/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py b/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py index f4694830de..85a9b66fb8 100644 --- a/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py +++ b/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py @@ -18,7 +18,6 @@ import matplotlib as mpl from matplotlib import pyplot as plt from discretize import TensorMesh -from pymatsolver import PardisoSolver from simpeg import maps from simpeg.utils import mkvc @@ -265,7 +264,6 @@ def PolygonInd(mesh, pts): parallel=False, n_cpu=2, verbose=True, - Solver=PardisoSolver, ) # simulation.model = sounding_models diff --git a/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py b/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py index 936b238b3c..0643bca063 100644 --- a/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py +++ b/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py @@ -18,7 +18,6 @@ import matplotlib as mpl from matplotlib import pyplot as plt from discretize import TensorMesh -from pymatsolver import PardisoSolver from simpeg.utils import mkvc from simpeg import ( @@ -224,12 +223,11 @@ thicknesses=thicknesses, sigmaMap=mapping, topo=topo, - Solver=PardisoSolver, ) # simulation = em1d.simulation.StitchedEM1DTMSimulation( # survey=survey, thicknesses=thicknesses, sigmaMap=mapping, -# topo=topo, parallel=True, n_cpu=4, verbose=True, Solver=PardisoSolver +# topo=topo, parallel=True, n_cpu=4, verbose=True # ) @@ -286,7 +284,7 @@ # Define how the optimization problem is solved. Here we will use an inexact # Gauss-Newton approach that employs the conjugate gradient solver. -opt = optimization.InexactGaussNewton(maxIter=40, maxIterCG=20) +opt = optimization.InexactGaussNewton(maxIter=40, cg_maxiter=20) # Define the inverse problem inv_prob = inverse_problem.BaseInvProblem(dmis, reg, opt) @@ -308,7 +306,7 @@ # IRLS = directives.Update_IRLS(max_irls_iterations=40, minGNiter=1, f_min_change=1e-5, chifact_start=2) # IRLS = directives.Update_IRLS( # max_irls_iterations=20, minGNiter=1, fix_Jmatrix=True, coolingRate=2, -# beta_tol=1e-2, f_min_change=1e-5, +# misfit_tolerance=1e-2, f_min_change=1e-5, # chifact_start = 1. # ) @@ -326,14 +324,12 @@ save_iteration = directives.SaveOutputEveryIteration(save_txt=False) -update_IRLS = directives.Update_IRLS( +update_IRLS = directives.UpdateIRLS( max_irls_iterations=20, - minGNiter=1, - fix_Jmatrix=True, f_min_change=1e-3, - coolingRate=3, ) + # Updating the preconditionner if it is model dependent. update_jacobi = directives.UpdatePreconditioner() @@ -347,13 +343,10 @@ # The directives are defined as a list. directives_list = [ - # sensitivity_weights, starting_beta, - beta_schedule, save_iteration, - # target_misfit, update_IRLS, - # update_jacobi, + beta_schedule, ] #####################################################################