diff --git a/.azure-pipelines/matrix.yml b/.azure-pipelines/matrix.yml index d4e275d169..4269e7bebe 100644 --- a/.azure-pipelines/matrix.yml +++ b/.azure-pipelines/matrix.yml @@ -3,7 +3,7 @@ parameters: py_vers: ['3.8'] test: ['tests/em', 'tests/base tests/flow tests/seis tests/utils tests/meta', - 'tests/docs', + 'tests/docs -s -v', 'tests/examples/test_examples_1.py', 'tests/examples/test_examples_2.py', 'tests/examples/test_examples_3.py', @@ -21,7 +21,17 @@ jobs: displayName: ${{ os }}_${{ py_vers }}_${{ test }} pool: vmImage: ${{ os }} + timeoutInMinutes: 120 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) + - 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" @@ -30,10 +40,14 @@ jobs: - 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 + cp environment_test.yml environment_test_with_pyversion.yml + echo " - python="${{ py_vers }} >> 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 + echo "\nList installed packages" + conda list displayName: Create Anaconda testing environment - script: | @@ -42,13 +56,26 @@ jobs: pip install -e . displayName: Build package + - script: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + python -c "import simpeg; print(simpeg.__version__)" + displayName: Check SimPEG version + - script: | source "${HOME}/conda/etc/profile.d/conda.sh" conda activate simpeg-test export KMP_WARNINGS=0 - pytest ${{ test }} --cov-config=.coveragerc --cov=SimPEG --cov-report=xml --cov-report=html -W ignore::DeprecationWarning + 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 diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index 5bcd931905..0000000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[bumpversion] -current_version = 0.19.0 -files = setup.py SimPEG/__init__.py docs/conf.py - diff --git a/.coveragerc b/.coveragerc index 8cb6275aa3..a79c8987f3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,4 +1,4 @@ [run] -source = SimPEG +source = simpeg omit = */setup.py diff --git a/.flake8 b/.flake8 index b770d960c0..14d1e5f416 100644 --- a/.flake8 +++ b/.flake8 @@ -6,6 +6,8 @@ # ---------------- [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) @@ -20,12 +22,97 @@ 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 # ------------------------------- diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 5e2e876ec4..130f534ba2 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -7,7 +7,7 @@ body: - type: markdown attributes: value: > - Thanks for your use of SimPEG and for taking the time to report a bug! Please + Thanks for using SimPEG and taking the time to report a bug! Please first double check that there is not already a bug report on this issue by searching through the existing bugs. @@ -19,13 +19,13 @@ body: - type: textarea attributes: - label: "Reproducable code example:" + label: "Reproducible code example:" description: > Please submit a small, but complete, code sample that reproduces the bug or missing functionality. It should be able to be copy-pasted - into a Python interpreter and ran as-is. + into a Python interpreter and run as-is. placeholder: | - import SimPEG + import simpeg << your code here >> render: python validations: @@ -44,8 +44,8 @@ body: attributes: label: "Runtime information:" 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. + Please include the output from `simpeg.Report()` to describe your system for us. + Paste the output from `from simpeg import Report; print(Report())` below. validations: required: true @@ -58,4 +58,4 @@ body: placeholder: | << your explanation here >> validations: - required: false \ No newline at end of file + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index f4108d53c7..6180153534 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,5 +3,5 @@ contact_links: url: https://simpeg.discourse.group/ about: "If you have a question on how to use SimPEG, please submit them to our discourse page." - name: Development-related matters - url: http://slack.simpeg.xyz/ - about: "If you would like to discuss SimPEG, any geophysics related problems, or need help from the SimPEG team, get in touch with us on slack." + url: https://mattermost.softwareunderground.org/simpeg + about: "If you would like to discuss SimPEG, any geophysics related problems, or need help from the SimPEG team, get in touch with us on Mattermost." diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 318a563659..5d8d196a5e 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -6,16 +6,17 @@ body: - type: markdown attributes: value: > - If you'd like to request a new feature in SimPEG, or suggest changes in the - functionality of certain functions, we recommend getting in touch with the - developers on [slack](https://slack.simpeg.xyz), in addition to opening an - issue or pull request here. + If you'd like to request a new feature in SimPEG, or suggest changes in + the functionality of certain functions, we recommend getting in touch + with the developers on + [Mattermost](https://mattermost.softwareunderground.org/simpeg), in + addition to opening an Issue or Pull Request here. You can also check out our - [Contributor Guide](https://docs.simpeg.xyz/content/basic/contributing.html) + [Contributor Guide](https://docs.simpeg.xyz/content/getting_started/contributing/index.html) if you need more information. - type: textarea attributes: label: "Proposed new feature or change:" validations: - required: true \ No newline at end of file + 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 6abd27d75a..10b970aebf 100644 --- a/.github/ISSUE_TEMPLATE/post-install.yml +++ b/.github/ISSUE_TEMPLATE/post-install.yml @@ -1,5 +1,5 @@ name: Post-install/importing issue -description: Report an issue if you have trouble importing SimPEG after installation. +description: Report an issue if you have trouble importing `simpeg` after installation. title: "" labels: [Installation] diff --git a/.github/ISSUE_TEMPLATE/release-checklist.md b/.github/ISSUE_TEMPLATE/release-checklist.md new file mode 100644 index 0000000000..543bdb1fdd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/release-checklist.md @@ -0,0 +1,168 @@ +--- +name: Release checklist +about: "Maintainers only: Checklist for making a new release" +title: "Release vX.Y.Z" +labels: "maintenance" +assignees: "" +--- + + + +**Target date:** YYYY/MM/DD + +## Generate release notes + +### Autogenerate release notes with GitHub + +- [ ] Generate a draft for a new Release in GitHub. +- [ ] Create a new tag for it (the version number with a leading `v`). +- [ ] Generate release notes automatically. +- [ ] Copy those notes and paste them into a `notes.md` file. +- [ ] Discard the draft (we'll generate a new one later on). + +### Add release notes to the docs + +- [ ] Convert the Markdown file to RST with: `pandoc notes.md -o notes.rst`. +- [ ] Generate list of contributors from the release notes with: + ```bash + 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`): + ```bash + export last_release="v0.20.0" + git shortlog HEAD...$last_release -sne > contributors + git log HEAD...$last_release | grep "Co-authored-by" | sed 's/Co-authored-by://' | sed 's/^[[:space:]]*/ /' | sort | uniq -c | sort -nr | sed 's/^ //' >> contributors + sort -rn contributors + ``` +- [ ] Transform GitHub handles into links to their profiles: + ```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. +- [ ] 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. + + +
+Template for release notes: + +```rst +.. __notes: + +=========================== +SimPEG Release Notes +=========================== + +MONTH DAYth, YEAR + +.. contents:: Highlights + :depth: 3 + +Updates +======= + +New features +------------ + +.. + list new features under subheadings, include link to related PRs + +Documentation +------------- + +.. + list improvements to documentation + +Bugfixes +-------- + +.. + list bugfixes, include link to related PRs + +Breaking changes +---------------- + +.. + list breaking changes introduced in this new release, include link to + releated PRs + +Contributors +============ + +.. + paste list of contributors that was generated in `notes.rst` + +Pull Requests +============= + +.. + paste list of PRs that were copied to `notes.rst` +``` + +
+ + +### Add new version to version switcher + +Edit the `docs/_static/versions.json` file and: + +- [ ] Add an entry for the new version. +- [ ] Move the line with `"name":` to the new entry (so the new version is set + as the _latest_ one). +- [ ] Update the version number in the `"name":` line. +- [ ] 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 +- [ ] 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` +- [ ] Generate release notes automatically. +- [ ] 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 they finish: + +- [ ] 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: + +- [ ] 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: + +- [ ] 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 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index daaf7c1746..c6b74e7c19 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -3,7 +3,7 @@ Thanks for contributing a pull request to SimPEG! Remember to use a personal fork of SimPEG to propose changes. Check out the stages of a pull request at -https://docs.simpeg.xyz/content/basic/contributing.html#pull-request +https://docs.simpeg.xyz/content/getting_started/contributing/pull-requests.html Note that we are a team of volunteers and we appreciate your patience during the review process. @@ -18,12 +18,11 @@ 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/basic/practices.html#style). -* [ ] Added [tests](https://docs.simpeg.xyz/content/basic/practices.html#testing) to verify changes to the code. +* [ ] 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. * [ ] Added necessary documentation to any new functions/classes following the - expect [style](https://docs.simpeg.xyz/content/basic/practices.html#documentation). -* [ ] Added relevant method tags (i.e. `GRAV`, `EM`, etc.) -* [ ] Marked as ready for review (ff this is was a draft PR), and converted + expect [style](https://docs.simpeg.xyz/content/getting_started/practices.html#documentation). +* [ ] 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. @@ -39,4 +38,4 @@ Feel free to remove lines from this template that do not apply to you pull reque \ No newline at end of file +--> diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000000..0151372c8d --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,58 @@ +name : Reviewdog PR Annotations +on: [pull_request_target] + +jobs: + flake8: + runs-on: ubuntu-latest + name: Flake8 check + steps: + - name: Checkout target repository source + uses: actions/checkout@v4 + + - name: Setup Python env + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies to run the flake8 checks + run: pip install -r requirements_style.txt + + - name: checkout pull request source + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + path: pr_source + + - name: flake8 review + uses: reviewdog/action-flake8@v3 + with: + workdir: pr_source + github_token: ${{ secrets.GITHUB_TOKEN }} + reporter: github-pr-review + + black: + name: Black check + runs-on: ubuntu-latest + steps: + - name: Checkout target repository source + uses: actions/checkout@v4 + + - name: Setup Python env + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies to run the black checks + run: pip install -r requirements_style.txt + + - name: checkout pull request source + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + path: 'pr_source' + + - uses: reviewdog/action-black@v3 + with: + workdir: 'pr_source' + github_token: ${{ secrets.GITHUB_TOKEN }} + reporter: github-pr-review \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0b177b79ab..5609580098 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ develop-eggs .installed.cfg lib lib64 +.idea __pycache__ # Installer logs @@ -48,13 +49,16 @@ docs/content/api/generated/* docs/content/examples/* docs/content/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/* @@ -88,3 +92,6 @@ tutorials/13-joint_inversion/cross_gradient_data/* *.npy *.mod *.tar.gz + +# setuptools_scm +simpeg/version.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 25cb7d45d1..ccb608b94b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,17 @@ repos: - repo: https://github.com/psf/black - rev: 24.4.2 + rev: 24.3.0 hooks: - id: black - language_version: python3.10 + language_version: python3 + - repo: https://github.com/pycqa/flake8 + rev: 7.0.0 + hooks: + - id: flake8 + language_version: python3 + 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 diff --git a/AUTHORS.rst b/AUTHORS.rst index 55de84f924..e12ab5a8d8 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -18,4 +18,11 @@ - Thibaut Astic, (`@thast `_) - Michael Mitchell, (`@micmitch `_) - I-Kang Ding, (`@ikding `_) -- Richard Scott (`@bluetyson `_) +- Richard Scott, (`@bluetyson `_) +- Xiaolong Wei, (`@xiaolongw1223 `_) +- Santiago Soler, (`@santisoler `_) +- Nick Williams, (`@nwilliams-kobold `_) +- John Weis, (`@johnweis0480 `_) +- Kalen Martens, (`@kalen-sj `_) +- Williams A. Lima (`@ghwilliams `_) +- Ying Hu, (`@YingHuuu `_) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..10c0f3522b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +# Contributing to SimPEG + +First of all, we are glad you are here! We welcome contributions and input +from the community. + +You can find guidelines on how to contribute to SimPEG in the [Contributing to +SimPEG](https://docs.simpeg.xyz/content/getting_started/contributing/index.html) +section of our documentation. + + +## Licensing + +All code contributed to SimPEG is licensed under the [MIT license](LICENSE) +which allows open and commercial use and extension of SimPEG. If you did not +write the code yourself, it is your responsibility to ensure that the existing +license is compatible and included in the contributed files or you can obtain +permission from the original author to relicense the code. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index d1e0d55b44..0000000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,137 +0,0 @@ -.. _contributing: - -Contributing to SimPEG -======================= - -First of all, we are glad you are here! We welcome contributions and input -from the community. - -This document is a set of guidelines for contributing to the repositories -hosted in the `SimPEG `_ organization on GitHub. -These repositories are maintained on a volunteer basis. - -.. _questions: - -Questions -========= - -If you have a question regarding a specific use of SimPEG, the fastest way -to get a response is by posting on our Discourse discussion forum: -https://simpeg.discourse.group/. Alternatively, if you prefer real-time chat, -you can join our slack group at http://slack.simpeg.xyz. -Please do not create an issue to ask a question. - -.. _Issues: - -Issues -====== - -Issues are a place for you to suggest enhancements or raise problems you are -having with the package to the developers. - -.. _bugs: - -Bugs ----- - -When reporting an issue, please be as descriptive and provide sufficient -detail to reproduce the error. Whenever possible, if you can include a small -example that produces the error, this will help us resolve issues faster. - - -.. _suggest enhancements: - -Suggesting enhancements ------------------------ - -We welcome ideas for improvements on SimPEG! When writing an issue to suggest -an improvement, please - -- use a descriptive title, -- explain where the gap in current functionality is, -- include a pseudocode sketch of the intended functionality - -We will use the issue as a place to discuss and provide feedback. Please -remember that SimPEG is maintained on a volunteer basis. If you suggest an -enhancement, we certainly appreciate if you are also willing to take action -and start a pull request! - - -.. _pull_requests: - -Pull Requests -============= - -We welcome contributions to SimPEG in the form of pull requests (PR) - -.. _contributing_new_code: - -Contributing new code ---------------------- - -.. _getting started: https://docs.simpeg.xyz/content/basic/installing_for_developers.html - -.. _practices: https://docs.simpeg.xyz/content/basic/practices.html - -.. _testing: https://docs.simpeg.xyz/content/basic/practices.html#testing - -.. _documentation: https://docs.simpeg.xyz/content/basic/practices.html#documentation - -.. _code style: https://docs.simpeg.xyz/content/basic/practices.html#style - -If you have an idea for how to improve SimPEG, please first create an issue -and `suggest enhancements`_. We will use the -issue as a place to discuss and make decisions on the suggestion. Once you are -ready to take action and commit some code to SimPEG, please check out -`getting started`_ for -tips on setting up a development environment and `practices`_ -for a description of the development practices we aim to follow. In particular, - -- `testing`_ -- `documentation`_ -- `code style`_ - -are aspects we look for in all pull requests. We do code reviews on pull -requests, with the aim of promoting best practices and ensuring that new -contributions can be built upon by the SimPEG community. - -.. _pr_stages: - -Stages of a pull request ------------------------- - -When first creating a pull request (PR), try to make your suggested changes as tightly -scoped as possible (try to solve one problem at a time). The fewer changes you make, the faster -your branch will be merged! - -If your pull request is not ready for final review, but you still want feedback -on your current coding process please mark it as a draft pull request. Once you -feel the pull request is ready for final review, you can convert the draft PR to -an open PR by selecting the ``Ready for review`` button at the bottom of the page. - -Once a pull request is in ``open`` status and you are ready for review, please ping -the simpeg developers in a github comment ``@simpeg/simpeg-developers`` to request a -review. At minimum for a PR to be eligible to merge, we look for - -- 100% (or as close as possible) difference testing. Meaning any new code is completely tested. -- All tests are passing. -- All reviewer comments (if any) have been addressed. -- A developer approves the PR. - -After all these steps are satisfied, a ``@simpeg/simpeg-admin`` will merge your pull request into -the main branch (feel free to ping one of us on github). - -This being said, all simpeg developers and admins are essentially volunteers -providing their time for the benefit of the community. This does mean that -it might take some time for us to get your PR. - - -Licensing -========= - -All code contributed to SimPEG is licensed under the `MIT license -`_ which allows open -and commercial use and extension of SimPEG. If you did not write -the code yourself, it is your responsibility to ensure that the existing -license is compatible and included in the contributed files or you can obtain -permission from the original author to relicense the code. diff --git a/LICENSE b/LICENSE index 79e70af749..cd67a7669e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013-2023 SimPEG Developers +Copyright (c) 2013-2024 SimPEG Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/MANIFEST.in b/MANIFEST.in index c74cefe094..7c04fa62b2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,10 @@ -include *.rst LICENSE +prune .github +prune .azure-pipelines +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 diff --git a/Makefile b/Makefile index 958d76e4a6..20c18b456c 100644 --- a/Makefile +++ b/Makefile @@ -1,82 +1,28 @@ -STYLE_CHECK_FILES = SimPEG examples tutorials tests +STYLE_CHECK_FILES = simpeg examples tutorials tests -# Define flake8 warnings that shouldn't be catched for now. -# Every time a new warning is solved througout the entire project, it should be -# removed to this list. -# This list is only meant to be used in the flake-permissive target and it's -# a temprary solution until every flake8 warning is solved in SimPEG. -# The first set of rules (up to W504) are the default ignored ones by flake8. -# Since we are using the --ignore option we are overriding them. They are -# included in the list so we keep ignoring them while running the -# flake-permissive target. -FLAKE8_IGNORE = "E121,E123,E126,E226,E24,E704,W503,W504,\ - B017,\ - B028,\ - D100,\ - D101,\ - D102,\ - D103,\ - D104,\ - D105,\ - D107,\ - D200,\ - D201,\ - D202,\ - D205,\ - D208,\ - D209,\ - D210,\ - D211,\ - D300,\ - D400,\ - D401,\ - D402,\ - D403,\ - D412,\ - D414,\ - D419,\ - E402,\ - E711,\ - E731,\ - F403,\ - F405,\ - F522,\ - F523,\ - F524,\ - F541,\ - F811,\ - F821,\ - RST201,\ - RST203,\ - RST206,\ - RST210,\ - RST212,\ - RST213,\ - RST215,\ - RST219,\ - RST301,\ - RST303,\ - RST304,\ - RST307,\ - RST499,\ - W291,\ - W293,\ -" +.PHONY: help build coverage lint graphs tests docs check black flake -.PHONY: build coverage lint graphs tests docs check black flake +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 "" build: python setup.py build_ext --inplace coverage: - nosetests --logging-level=INFO --with-coverage --cover-package=SimPEG --cover-html + nosetests --logging-level=INFO --with-coverage --cover-package=simpeg --cover-html open cover/index.html lint: - pylint --output-format=html SimPEG > pylint.html + pylint --output-format=html simpeg> pylint.html graphs: - pyreverse -my -A -o pdf -p SimPEG SimPEG/**.py SimPEG/**/**.py + pyreverse -my -A -o pdf -p simpeg simpeg/**.py simpeg/**/**.py tests: nosetests --logging-level=INFO @@ -98,6 +44,6 @@ flake: flake8 --version flake8 ${FLAKE8_OPTS} ${STYLE_CHECK_FILES} -flake-permissive: +flake-all: flake8 --version - flake8 ${FLAKE8_OPTS} --ignore ${FLAKE8_IGNORE} ${STYLE_CHECK_FILES} + flake8 ${FLAKE8_OPTS} --ignore "" ${STYLE_CHECK_FILES} diff --git a/README.rst b/README.rst index 09eace6923..54041d24e3 100644 --- a/README.rst +++ b/README.rst @@ -1,15 +1,15 @@ .. image:: https://raw.github.com/simpeg/simpeg/main/docs/images/simpeg-logo.png - :alt: SimPEG Logo + :alt: simpeg Logo SimPEG ****** -.. image:: https://img.shields.io/pypi/v/SimPEG.svg - :target: https://pypi.python.org/pypi/SimPEG +.. image:: https://img.shields.io/pypi/v/simpeg.svg + :target: https://pypi.python.org/pypi/simpeg :alt: Latest PyPI version -.. image:: https://img.shields.io/conda/v/conda-forge/SimPEG.svg - :target: https://anaconda.org/conda-forge/SimPEG +.. image:: https://img.shields.io/conda/v/conda-forge/simpeg.svg + :target: https://anaconda.org/conda-forge/simpeg :alt: Latest conda-forge version .. image:: https://img.shields.io/github/license/simpeg/simpeg.svg @@ -28,10 +28,10 @@ SimPEG :target: https://doi.org/10.5281/zenodo.596373 .. image:: https://img.shields.io/discourse/users?server=http%3A%2F%2Fsimpeg.discourse.group%2F - :target: http://simpeg.discourse.group/ + :target: https://simpeg.discourse.group/ -.. image:: https://img.shields.io/badge/Slack-simpeg-4A154B.svg?logo=slack - :target: http://slack.simpeg.xyz +.. image:: https://img.shields.io/badge/simpeg-purple?logo=mattermost&label=Mattermost + :target: https://mattermost.softwareunderground.org/simpeg .. image:: https://img.shields.io/badge/Youtube%20channel-GeoSci.xyz-FF0000.svg?logo=youtube :target: https://www.youtube.com/channel/UCBrC4M8_S4GXhyHht7FyQqw @@ -46,7 +46,7 @@ The vision is to create a package for finite volume simulation with applications * supports 1D, 2D and 3D problems * designed for large-scale inversions -You are welcome to join our forum and engage with people who use and develop SimPEG at: http://simpeg.discourse.group/. +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. @@ -109,7 +109,8 @@ Questions If you have a question regarding a specific use of SimPEG, the fastest way to get a response is by posting on our Discourse discussion forum: https://simpeg.discourse.group/. Alternatively, if you prefer real-time chat, -you can join our slack group at http://slack.simpeg.xyz. +you can join our Mattermost Team at +https://mattermost.softwareunderground.org/simpeg. Please do not create an issue to ask a question. @@ -121,7 +122,8 @@ 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 `slack `_. +on even numbered Wednesdays. Find more info on our +`Mattermost `_. Links @@ -134,8 +136,8 @@ Forums: https://simpeg.discourse.group/ -Slack (real time chat): -http://slack.simpeg.xyz +Mattermost (real time chat): +https://mattermost.softwareunderground.org/simpeg Documentation: @@ -159,5 +161,5 @@ Contributing We always welcome contributions towards SimPEG whether they are adding new code, suggesting improvements to existing codes, identifying bugs, providing examples, or anything that will improve SimPEG. -Please checkout the `contributing guide `_ -for more information on how to contribute. \ No newline at end of file +Please checkout the `contributing guide `_ +for more information on how to contribute. diff --git a/SimPEG.py b/SimPEG.py new file mode 100644 index 0000000000..c923431ba0 --- /dev/null +++ b/SimPEG.py @@ -0,0 +1,11 @@ +import sys +import warnings + +warnings.warn( + "Importing `SimPEG` is deprecated. please import from `simpeg`.", + FutureWarning, + stacklevel=2, +) +import simpeg + +sys.modules["SimPEG"] = simpeg diff --git a/SimPEG/dask/__init__.py b/SimPEG/dask/__init__.py deleted file mode 100644 index ad2f2a8c4f..0000000000 --- a/SimPEG/dask/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -try: - import SimPEG.dask.simulation - import SimPEG.dask.electromagnetics.frequency_domain.simulation - import SimPEG.dask.electromagnetics.static.resistivity.simulation - import SimPEG.dask.electromagnetics.static.resistivity.simulation_2d - import SimPEG.dask.electromagnetics.static.induced_polarization.simulation - import SimPEG.dask.electromagnetics.static.induced_polarization.simulation_2d - import SimPEG.dask.electromagnetics.time_domain.simulation - import SimPEG.dask.potential_fields.base - import SimPEG.dask.potential_fields.gravity.simulation - import SimPEG.dask.potential_fields.magnetics.simulation - import SimPEG.dask.simulation - import SimPEG.dask.data_misfit - import SimPEG.dask.inverse_problem - import SimPEG.dask.objective_function - -except ImportError as err: - print("unable to load dask operations") - print(err) diff --git a/SimPEG/data_misfit.py b/SimPEG/data_misfit.py deleted file mode 100644 index 768a714182..0000000000 --- a/SimPEG/data_misfit.py +++ /dev/null @@ -1,260 +0,0 @@ -import numpy as np -from .utils import Counter, sdiag, timeIt, Identity, validate_type, mkvc -from .data import Data -from .simulation import BaseSimulation -from .objective_function import L2ObjectiveFunction - -__all__ = ["L2DataMisfit"] - - -class BaseDataMisfit(L2ObjectiveFunction): - """ - BaseDataMisfit - - .. note:: - You should inherit from this class to create your own data misfit - term. - """ - - def __init__(self, data, simulation, model_map=None, debug=False, counter=None, **kwargs): - super().__init__(**kwargs) - - self.data = data - self.simulation = simulation - self.debug = debug - self.count = counter - - self.model_map = model_map - - @property - def model_map(self): - return getattr(self, "_model_map", None) - - @model_map.setter - def model_map(self, value): - if value is None: - value = Identity() - self._model_map = value - self._has_fields = True - - - @property - def data(self): - """A SimPEG data class containing the observed data. - - Returns - ------- - SimPEG.data.Data - """ - return self._data - - @data.setter - def data(self, value): - self._data = validate_type("data", value, Data, cast=False) - - @property - def simulation(self): - """A SimPEG simulation. - - Returns - ------- - SimPEG.simulation.BaseSimulation - """ - return self._simulation - - @simulation.setter - def simulation(self, value): - self._simulation = validate_type( - "simulation", value, BaseSimulation, cast=False - ) - - @property - def debug(self): - """Print debugging information. - - Returns - ------- - bool - """ - return self._debug - - @debug.setter - def debug(self, value): - self._debug = validate_type("debug", value, bool) - - @property - def counter(self): - """Set this to a ``SimPEG.utils.Counter`` if you want to count things. - - Returns - ------- - SimPEG.utils.Counter or None - """ - return self._counter - - @counter.setter - def counter(self, value): - if value is not None: - value = validate_type("counter", value, Counter, cast=False) - - @property - def nP(self): - """ - number of model parameters - """ - if self._mapping is not None: - return self.mapping.nP - elif self.simulation.model is not None: - return len(self.simulation.model) - else: - return "*" - - @property - def nD(self): - """ - number of data - """ - return self.data.nD - - @property - def shape(self): - """""" - return (self.nD, self.nP) - - @property - def W(self): - """W - The data weighting matrix. - The default is based on the norm of the data plus a noise floor. - :rtype: scipy.sparse.csr_matrix - :return: W - """ - - if getattr(self, "_W", None) is None: - if self.data is None: - raise Exception( - "data with standard deviations must be set before the data " - "misfit can be constructed. Please set the data: " - "dmis.data = Data(dobs=dobs, relative_error=rel" - ", noise_floor=eps)" - ) - standard_deviation = self.data.standard_deviation - if standard_deviation is None: - raise Exception( - "data standard deviations must be set before the data misfit " - "can be constructed (data.relative_error = 0.05, " - "data.noise_floor = 1e-5), alternatively, the W matrix " - "can be set directly (dmisfit.W = 1./standard_deviation)" - ) - if any(standard_deviation <= 0): - raise Exception( - "data.standard_deviation must be strictly positive to construct " - "the W matrix. Please set data.relative_error and or " - "data.noise_floor." - ) - self._W = sdiag(1 / (standard_deviation)) - return self._W - - @W.setter - def W(self, value): - if isinstance(value, Identity): - value = np.ones(self.data.nD) - if len(value.shape) < 2: - value = sdiag(value) - assert value.shape == ( - self.data.nD, - self.data.nD, - ), "W must have shape ({nD},{nD}), not ({val0}, {val1})".format( - nD=self.data.nD, val0=value.shape[0], val1=value.shape[1] - ) - self._W = value - - def residual(self, m, f=None): - 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) - - -class L2DataMisfit(BaseDataMisfit): - r""" - The data misfit with an l_2 norm: - - .. math:: - - \mu_\text{data} = - \frac{1}{2} - \left| - \mathbf{W}_d (\mathbf{d}_\text{pred} - \mathbf{d}_\text{obs}) - \right|_2^2 - """ - - @timeIt - def __call__(self, m, f=None): - "__call__(m, f=None)" - - R = self.W * self.residual(m, f=f) - return 0.5 * np.vdot(R, R) - - @timeIt - def deriv(self, m, f=None): - r""" - Derivative of the data misfit - - .. math:: - - \mathbf{J}^{\top} \mathbf{W}^{\top} \mathbf{W} - (\mathbf{d} - \mathbf{d}^{obs}) - - :param numpy.ndarray m: model - :param SimPEG.fields.Fields f: fields object - """ - - if f is None: - f = self.simulation.fields(m) - - return self.simulation.Jtvec( - m, self.W.T * (self.W * self.residual(m, f=f)), f=f - ) - - @timeIt - def deriv2(self, m, v, f=None): - r""" - Second derivative of the data misfit - - .. math:: - - \mathbf{J}^{\top} \mathbf{W}^{\top} \mathbf{W} \mathbf{J} - - :param numpy.ndarray m: model - :param numpy.ndarray v: vector - :param SimPEG.fields.Fields f: fields object - """ - - if f is None: - f = self.simulation.fields(m) - - return self.simulation.Jtvec_approx( - m, self.W * (self.W * self.simulation.Jvec_approx(m, v, f=f)), f=f - ) - - def getJtJdiag(self, m): - """ - Evaluate the main diagonal of JtJ - """ - if getattr(self.simulation, "getJtJdiag", None) is None: - raise AttributeError( - "Simulation does not have a getJtJdiag attribute." - + "Cannot form the sensitivity explicitly" - ) - - mapping_deriv = self.model_map.deriv(m) - - if self.model_map is not None: - m = mapping_deriv @ m - - jtjdiag = self.simulation.getJtJdiag(m, W=self.W) - - if self.model_map is not None: - jtjdiag = mkvc((sdiag(np.sqrt(jtjdiag)) @ mapping_deriv).power(2).sum(axis=0)) - - return jtjdiag \ No newline at end of file diff --git a/SimPEG/directives/__init__.py b/SimPEG/directives/__init__.py deleted file mode 100644 index 8e814ffa9f..0000000000 --- a/SimPEG/directives/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -from .directives import ( - InversionDirective, - DirectiveList, - BetaEstimateMaxDerivative, - BetaEstimate_ByEig, - BetaSchedule, - TargetMisfit, - SaveEveryIteration, - SaveModelEveryIteration, - SaveOutputEveryIteration, - SaveOutputDictEveryIteration, - Update_IRLS, - UpdatePreconditioner, - Update_Wj, - AlphasSmoothEstimate_ByEig, - MultiTargetMisfits, - ScalingMultipleDataMisfits_ByEig, - JointScalingSchedule, - UpdateSensitivityWeights, - ProjectSphericalBounds, - VectorInversion, - SaveIterationsGeoH5 -) - -from .pgi_directives import ( - PGI_UpdateParameters, - PGI_BetaAlphaSchedule, - PGI_AddMrefInSmooth, -) - -from .sim_directives import ( - SimilarityMeasureInversionDirective, - SimilarityMeasureSaveOutputEveryIteration, - PairedBetaEstimate_ByEig, - PairedBetaSchedule, - MovingAndMultiTargetStopping, -) diff --git a/SimPEG/electromagnetics/analytics/__init__.py b/SimPEG/electromagnetics/analytics/__init__.py deleted file mode 100644 index ebcc3a0946..0000000000 --- a/SimPEG/electromagnetics/analytics/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .TDEM import hzAnalyticDipoleT, hzAnalyticCentLoopT -from .FDEM import hzAnalyticDipoleF -from .FDEMcasing import * -from .DC import * -from .FDEMDipolarfields import * -from .NSEM import MT_LayeredEarth diff --git a/SimPEG/electromagnetics/frequency_domain/simulation.py b/SimPEG/electromagnetics/frequency_domain/simulation.py deleted file mode 100644 index 4a13430f6a..0000000000 --- a/SimPEG/electromagnetics/frequency_domain/simulation.py +++ /dev/null @@ -1,879 +0,0 @@ -import numpy as np -import scipy.sparse as sp -from discretize.utils import Zero - -from ...data import Data -from ...utils import mkvc, validate_type -from ..base import BaseEMSimulation -from ..utils import omega -from .survey import Survey -from .fields import ( - FieldsFDEM, - Fields3DElectricField, - Fields3DMagneticFluxDensity, - Fields3DMagneticField, - Fields3DCurrentDensity, -) - - -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}`. - - """ - - fieldsPair = FieldsFDEM - - def __init__(self, mesh, survey=None, forward_only=False, **kwargs): - super().__init__(mesh=mesh, survey=survey, **kwargs) - self.forward_only = forward_only - - @property - def survey(self): - """The simulations survey. - - Returns - ------- - SimPEG.electromagnetics.frequency_domain.survey.Survey - """ - if self._survey is None: - raise AttributeError("Simulation must have a survey set") - return self._survey - - @survey.setter - def survey(self, value): - if value is not None: - value = validate_type("survey", value, Survey, cast=False) - self._survey = value - - @property - def forward_only(self): - """If True, A-inverse not stored at each frequency in forward simulation. - - Returns - ------- - bool - """ - return self._forward_only - - @forward_only.setter - def forward_only(self, value): - self._forward_only = validate_type("forward_only", value, bool) - - # @profile - def fields(self, m=None): - """ - Solve the forward problem for the fields. - - :param numpy.ndarray m: inversion model (nP,) - :rtype: numpy.ndarray - :return f: forward solution - """ - - if m is not None: - self.model = m - - try: - self.Ainv - except AttributeError: - self.Ainv = len(self.survey.frequencies) * [None] - - f = self.fieldsPair(self) - - for i_f, freq in enumerate(self.survey.frequencies): - A = self.getA(freq) - rhs = self.getRHS(freq) - Ainv = self.solver(A, **self.solver_opts) - u = Ainv * rhs - if not self.forward_only: - self.Ainv[i_f] = Ainv - - Srcs = self.survey.get_sources_by_frequency(freq) - f[Srcs, self._solutionType] = u - return f - - # @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,) - """ - - if f is None: - f = self.fields(m) - - self.model = m - - Jv = Data(self.survey) - - for nf, freq in enumerate(self.survey.frequencies): - for src in self.survey.get_sources_by_frequency(freq): - u_src = f[src, self._solutionType] - dA_dm_v = self.getADeriv(freq, u_src, v, adjoint=False) - 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) - - return Jv.dobs - - def Jtvec(self, m, v, f=None): - """ - Sensitivity transpose times a vector - - :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,) - """ - - if f is None: - f = self.fields(m) - - self.model = m - - # Ensure v is a data object. - if not isinstance(v, Data): - v = Data(self.survey, v) - - Jtv = np.zeros(m.size) - - for nf, freq in enumerate(self.survey.frequencies): - for src in self.survey.get_sources_by_frequency(freq): - u_src = f[src, self._solutionType] - df_duT_sum = 0 - df_dmT_sum = 0 - for rx in src.receiver_list: - df_duT, df_dmT = rx.evalDeriv( - src, self.mesh, f, v=v[src, rx], adjoint=True - ) - if not isinstance(df_duT, Zero): - df_duT_sum += df_duT - if not isinstance(df_dmT, Zero): - df_dmT_sum += df_dmT - - ATinvdf_duT = self.Ainv[nf] * df_duT_sum - - dA_dmT = self.getADeriv(freq, u_src, ATinvdf_duT, adjoint=True) - dRHS_dmT = self.getRHSDeriv(freq, src, ATinvdf_duT, adjoint=True) - du_dmT = -dA_dmT + dRHS_dmT - - df_dmT_sum += du_dmT - Jtv += np.real(df_dmT_sum) - - return mkvc(Jtv) - - # @profile - def getSourceTerm(self, freq, source=None): - """ - Evaluates the sources for a given frequency and puts them in matrix - form - - :param float freq: Frequency - :rtype: tuple - :return: (s_m, s_e) (nE or nF, nSrc) - """ - if source is not None: - Srcs = [source] - else: - Srcs = self.survey.get_sources_by_frequency(freq) - - n_fields = sum(src._fields_per_source for src in Srcs) - if self._formulation == "EB": - s_m = np.zeros((self.mesh.nF, n_fields), dtype=complex, order="F") - s_e = np.zeros((self.mesh.nE, n_fields), dtype=complex, order="F") - elif self._formulation == "HJ": - s_m = np.zeros((self.mesh.nE, n_fields), dtype=complex, order="F") - s_e = np.zeros((self.mesh.nF, n_fields), dtype=complex, order="F") - - i = 0 - for src in Srcs: - ii = i + src._fields_per_source - smi, sei = src.eval(self) - if not isinstance(smi, Zero) and smi.ndim == 1: - smi = smi[:, None] - if not isinstance(sei, Zero) and sei.ndim == 1: - sei = sei[:, None] - s_m[:, i:ii] = s_m[:, i:ii] + smi - s_e[:, i:ii] = s_e[:, i:ii] + sei - i = ii - return s_m, s_e - - -############################################################################### -# E-B Formulation # -############################################################################### - - -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) - - - 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" - _formulation = "EB" - fieldsPair = Fields3DElectricField - - def getA(self, freq): - r""" - System matrix - - .. math :: - - \mathbf{A} = \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f} \mathbf{C} - + i \omega \mathbf{M^e_{\sigma}} - - :param float freq: Frequency - :rtype: scipy.sparse.csr_matrix - :return: A - """ - - MfMui = self.MfMui - MeSigma = self.MeSigma - C = self.mesh.edge_curl - - return C.T.tocsr() * MfMui * C + 1j * omega(freq) * MeSigma - - 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,) - """ - - 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. - - .. math :: - - \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}} - - """ - - C = self.mesh.edge_curl - - if adjoint: - return self.MfMuiDeriv(C * u).T * (C * v) - - return C.T * (self.MfMuiDeriv(C * u) * v) - - def getADeriv(self, freq, u, v, adjoint=False): - return self.getADeriv_sigma(freq, u, v, adjoint) + self.getADeriv_mui( - freq, u, v, adjoint - ) - - def getRHS(self, freq): - r""" - Right hand side for the system - - .. math :: - - \mathbf{RHS} = \mathbf{C}^{\top} - \mathbf{M_{\mu^{-1}}^f}\mathbf{s_m} - - i\omega\mathbf{M_e}\mathbf{s_e} - - :param float freq: Frequency - :rtype: numpy.ndarray - :return: RHS (nE, nSrc) - """ - - s_m, s_e = self.getSourceTerm(freq) - C = self.mesh.edge_curl - MfMui = self.MfMui - - 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 - """ - - C = self.mesh.edge_curl - MfMui = self.MfMui - s_m, s_e = self.getSourceTerm(freq, source=src) - s_mDeriv, s_eDeriv = src.evalDeriv(self, adjoint=adjoint) - MfMuiDeriv = self.MfMuiDeriv(s_m) - - if adjoint: - return ( - s_mDeriv(MfMui * (C * v)) - + MfMuiDeriv.T * (C * v) - - 1j * omega(freq) * s_eDeriv(v) - ) - return C.T * (MfMui * s_mDeriv(v) + MfMuiDeriv * v) - 1j * omega( - freq - ) * s_eDeriv(v) - - -class Simulation3DMagneticFluxDensity(BaseFDEMSimulation): - r""" - We eliminate :math:`\mathbf{e}` using - - .. math :: - - \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" - _formulation = "EB" - fieldsPair = Fields3DMagneticFluxDensity - - def getA(self, freq): - r""" - System matrix - - .. math :: - - \mathbf{A} = \mathbf{C} \mathbf{M^e_{\sigma}}^{-1} - \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f} + i \omega - - :param float freq: Frequency - :rtype: scipy.sparse.csr_matrix - :return: A - """ - - MfMui = self.MfMui - MeSigmaI = self.MeSigmaI - C = self.mesh.edge_curl - iomega = 1j * omega(freq) * sp.eye(self.mesh.nF) - - A = C * (MeSigmaI * (C.T.tocsr() * MfMui)) + iomega - - if self._makeASymmetric: - return MfMui.T.tocsr() * A - 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,) - """ - - MfMui = self.MfMui - C = self.mesh.edge_curl - MeSigmaIDeriv = self.MeSigmaIDeriv - vec = C.T * (MfMui * u) - - if adjoint: - return MeSigmaIDeriv(vec, C.T * v, adjoint) - return C * MeSigmaIDeriv(vec, v, adjoint) - - # if adjoint: - # return MeSigmaIDeriv.T * (C.T * v) - # return C * (MeSigmaIDeriv * v) - - def getADeriv_mui(self, freq, u, v, adjoint=False): - MfMuiDeriv = self.MfMuiDeriv(u) - MeSigmaI = self.MeSigmaI - C = self.mesh.edge_curl - - if adjoint: - return MfMuiDeriv.T * (C * (MeSigmaI.T * (C.T * v))) - return C * (MeSigmaI * (C.T * (MfMuiDeriv * v))) - - def getADeriv(self, freq, u, v, adjoint=False): - if adjoint is True and self._makeASymmetric: - v = self.MfMui * v - - ADeriv = self.getADeriv_sigma(freq, u, v, adjoint) + self.getADeriv_mui( - freq, u, v, adjoint - ) - - if adjoint is False and self._makeASymmetric: - return self.MfMui.T * ADeriv - - return ADeriv - - def getRHS(self, freq): - r""" - Right hand side for the system - - .. math :: - - \mathbf{RHS} = \mathbf{s_m} + - \mathbf{M^e_{\sigma}}^{-1}\mathbf{s_e} - - :param float freq: Frequency - :rtype: numpy.ndarray - :return: RHS (nE, nSrc) - """ - - s_m, s_e = self.getSourceTerm(freq) - C = self.mesh.edge_curl - MeSigmaI = self.MeSigmaI - - RHS = s_m + C * (MeSigmaI * s_e) - - if self._makeASymmetric is True: - MfMui = self.MfMui - return MfMui.T * RHS - - 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 - """ - - C = self.mesh.edge_curl - s_m, s_e = src.eval(self) - MfMui = self.MfMui - - if self._makeASymmetric and adjoint: - v = self.MfMui * v - - # MeSigmaIDeriv = self.MeSigmaIDeriv(s_e) - s_mDeriv, s_eDeriv = src.evalDeriv(self, adjoint=adjoint) - - if not adjoint: - # RHSderiv = C * (MeSigmaIDeriv * v) - RHSderiv = C * self.MeSigmaIDeriv(s_e, v, adjoint) - SrcDeriv = s_mDeriv(v) + C * (self.MeSigmaI * s_eDeriv(v)) - elif adjoint: - # RHSderiv = MeSigmaIDeriv.T * (C.T * v) - RHSderiv = self.MeSigmaIDeriv(s_e, C.T * v, adjoint) - SrcDeriv = s_mDeriv(v) + s_eDeriv(self.MeSigmaI.T * (C.T * v)) - - if self._makeASymmetric is True and not adjoint: - return MfMui.T * (SrcDeriv + RHSderiv) - - return RHSderiv + SrcDeriv - - -############################################################################### -# H-J Formulation # -############################################################################### - - -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) - - - 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" - _formulation = "HJ" - fieldsPair = Fields3DCurrentDensity - - def getA(self, freq): - r""" - System matrix - - .. math :: - - \mathbf{A} = \mathbf{C} \mathbf{M^e_{\mu^{-1}}} - \mathbf{C}^{\top} \mathbf{M^f_{\sigma^{-1}}} + i\omega - - :param float freq: Frequency - :rtype: scipy.sparse.csr_matrix - :return: A - """ - - MeMuI = self.MeMuI - MfRho = self.MfRho - C = self.mesh.edge_curl - iomega = 1j * omega(freq) * sp.eye(self.mesh.nF) - - A = C * MeMuI * C.T.tocsr() * MfRho + iomega - - if self._makeASymmetric is True: - return MfRho.T.tocsr() * A - 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,) - """ - - MeMuI = self.MeMuI - C = self.mesh.edge_curl - - if adjoint: - vec = C * (MeMuI.T * (C.T * v)) - return self.MfRhoDeriv(u, vec, adjoint) - return C * (MeMuI * (C.T * (self.MfRhoDeriv(u, v, adjoint)))) - - def getADeriv_mu(self, freq, u, v, adjoint=False): - C = self.mesh.edge_curl - MfRho = self.MfRho - - MeMuIDeriv = self.MeMuIDeriv(C.T * (MfRho * u)) - - if adjoint is True: - # if self._makeASymmetric: - # v = MfRho * v - return MeMuIDeriv.T * (C.T * v) - - Aderiv = C * (MeMuIDeriv * v) - # if self._makeASymmetric: - # Aderiv = MfRho.T * Aderiv - return Aderiv - - def getADeriv(self, freq, u, v, adjoint=False): - if adjoint and self._makeASymmetric: - v = self.MfRho * v - - ADeriv = self.getADeriv_rho(freq, u, v, adjoint) + self.getADeriv_mu( - freq, u, v, adjoint - ) - - if not adjoint and self._makeASymmetric: - return self.MfRho.T * ADeriv - - return ADeriv - - def getRHS(self, freq): - r""" - Right hand side for the system - - .. math :: - - \mathbf{RHS} = \mathbf{C} \mathbf{M_{\mu}^e}^{-1}\mathbf{s_m} - - i\omega \mathbf{s_e} - - :param float freq: Frequency - :rtype: numpy.ndarray - :return: RHS (nE, nSrc) - """ - - s_m, s_e = self.getSourceTerm(freq) - C = self.mesh.edge_curl - MeMuI = self.MeMuI - - RHS = C * (MeMuI * s_m) - 1j * omega(freq) * s_e - if self._makeASymmetric is True: - MfRho = self.MfRho - return MfRho.T * RHS - - 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 - """ - - # RHS = C * (MeMuI * s_m) - 1j * omega(freq) * s_e - # if self._makeASymmetric is True: - # MfRho = self.MfRho - # return MfRho.T*RHS - - C = self.mesh.edge_curl - MeMuI = self.MeMuI - MeMuIDeriv = self.MeMuIDeriv - s_mDeriv, s_eDeriv = src.evalDeriv(self, adjoint=adjoint) - s_m, _ = self.getSourceTerm(freq, source=src) - - if adjoint: - if self._makeASymmetric: - MfRho = self.MfRho - v = MfRho * v - CTv = C.T * v - return ( - s_mDeriv(MeMuI.T * CTv) - + MeMuIDeriv(s_m).T * CTv - - 1j * omega(freq) * s_eDeriv(v) - ) - - else: - RHSDeriv = C * (MeMuI * s_mDeriv(v) + MeMuIDeriv(s_m) * v) - 1j * omega( - freq - ) * s_eDeriv(v) - - if self._makeASymmetric: - MfRho = self.MfRho - return MfRho.T * RHSDeriv - return RHSDeriv - - -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 - - .. 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" - _formulation = "HJ" - fieldsPair = Fields3DMagneticField - - def getA(self, freq): - r""" - System matrix - - .. math:: - - \mathbf{A} = \mathbf{C}^{\top} \mathbf{M_{\rho}^f} \mathbf{C} + - i \omega \mathbf{M_{\mu}^e} - - - :param float freq: Frequency - :rtype: scipy.sparse.csr_matrix - :return: A - - """ - - MeMu = self.MeMu - MfRho = self.MfRho - C = self.mesh.edge_curl - - return C.T.tocsr() * (MfRho * 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 - - .. math:: - - \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,) - """ - C = self.mesh.edge_curl - if adjoint: - return self.MfRhoDeriv(C * u, C * v, adjoint) - return C.T * self.MfRhoDeriv(C * u, v, adjoint) - - def getADeriv_mu(self, freq, u, v, adjoint=False): - MeMuDeriv = self.MeMuDeriv(u) - - if adjoint is True: - return 1j * omega(freq) * (MeMuDeriv.T * v) - - return 1j * omega(freq) * (MeMuDeriv * v) - - def getADeriv(self, freq, u, v, adjoint=False): - 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 - - .. math :: - - \mathbf{RHS} = \mathbf{M^e} \mathbf{s_m} + \mathbf{C}^{\top} - \mathbf{M_{\rho}^f} \mathbf{s_e} - - :param float freq: Frequency - :rtype: numpy.ndarray - :return: RHS (nE, nSrc) - - """ - - s_m, s_e = self.getSourceTerm(freq) - C = self.mesh.edge_curl - MfRho = self.MfRho - - return s_m + C.T * (MfRho * 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 - """ - - _, s_e = src.eval(self) - C = self.mesh.edge_curl - MfRho = self.MfRho - - # MfRhoDeriv = self.MfRhoDeriv(s_e) - # if not adjoint: - # RHSDeriv = C.T * (MfRhoDeriv * v) - # elif adjoint: - # RHSDeriv = MfRhoDeriv.T * (C * v) - if not adjoint: - RHSDeriv = C.T * (self.MfRhoDeriv(s_e, v, adjoint)) - elif adjoint: - RHSDeriv = self.MfRhoDeriv(s_e, C * v, adjoint) - - s_mDeriv, s_eDeriv = src.evalDeriv(self, adjoint=adjoint) - - return RHSDeriv + s_mDeriv(v) + C.T * (MfRho * s_eDeriv(v)) diff --git a/SimPEG/electromagnetics/time_domain/simulation.py b/SimPEG/electromagnetics/time_domain/simulation.py deleted file mode 100644 index 8d414c339d..0000000000 --- a/SimPEG/electromagnetics/time_domain/simulation.py +++ /dev/null @@ -1,1178 +0,0 @@ -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 -from .survey import Survey -from .fields import ( - Fields3DMagneticFluxDensity, - Fields3DElectricField, - Fields3DMagneticField, - Fields3DCurrentDensity, - FieldsDerivativesEB, - FieldsDerivativesHJ, -) - - -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. - """ - - def __init__(self, mesh, survey=None, dt_threshold=1e-8, **kwargs): - super().__init__(mesh=mesh, survey=survey, **kwargs) - self.dt_threshold = dt_threshold - if self.muMap is not None: - raise NotImplementedError( - "Time domain EM simulations do not support magnetic permeability " - "inversion, yet." - ) - - @property - def survey(self): - """The survey for the simulation - Returns - ------- - SimPEG.electromagnetics.time_domain.survey.Survey - """ - if self._survey is None: - raise AttributeError("Simulation must have a survey set") - return self._survey - - @survey.setter - def survey(self, value): - if value is not None: - value = validate_type("survey", value, Survey, cast=False) - self._survey = value - - @property - def dt_threshold(self): - """The threshold used to determine if a previous matrix factor can be reused. - - If the difference in time steps falls below this threshold, the factored matrix - is re-used. - - Returns - ------- - float - """ - return self._dt_threshold - - @dt_threshold.setter - 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. - - :param numpy.ndarray m: inversion model (nP,) - :rtype: SimPEG.electromagnetics.time_domain.fields.FieldsTDEM - :return f: fields object - """ - - self.model = m - - f = self.fieldsPair(self) - - # set initial fields - f[:, self._fieldType + "Solution", 0] = self.getInitialFields() - - if self.verbose: - print("{}\nCalculating fields(m)\n{}".format("*" * 50, "*" * 50)) - - # timestep to solve forward - Ainv = None - for tInd, dt in enumerate(self.time_steps): - # keep factors if dt is the same as previous step b/c A will be the - # same - if Ainv is not None and ( - tInd > 0 and abs(dt - self.time_steps[tInd - 1]) > self.dt_threshold - ): - Ainv.clean() - Ainv = None - - if Ainv is None: - A = self.getAdiag(tInd) - if self.verbose: - print("Factoring... (dt = {:e})".format(dt)) - Ainv = self.solver(A, **self.solver_opts) - if self.verbose: - print("Done") - - rhs = self.getRHS(tInd + 1) # this is on the nodes of the time mesh - Asubdiag = self.getAsubdiag(tInd) - - if self.verbose: - print(" Solving... (tInd = {:d})".format(tInd + 1)) - - # taking a step - sol = Ainv * (rhs - Asubdiag * f[:, (self._fieldType + "Solution"), tInd]) - - if self.verbose: - print(" Done...") - - if sol.ndim == 1: - sol.shape = (sol.size, 1) - f[:, self._fieldType + "Solution", tInd + 1] = sol - - if self.verbose: - print("{}\nDone calculating fields(m)\n{}".format("*" * 50, "*" * 50)) - - # clean factors and return - Ainv.clean() - return f - - def Jvec(self, m, v, f=None): - r""" - Jvec computes the sensitivity times a vector - - .. 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} - - where - - .. 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}} - - """ - - if f is None: - f = self.fields(m) - - ftype = self._fieldType + "Solution" # the thing we solved for - self.model = m - - # mat to store previous time-step's solution deriv times a vector for - # each source - # size: nu x nSrc - - # this is a bit silly - - # if self._fieldType == 'b' or self._fieldType == 'j': - # ifields = np.zeros((self.mesh.n_faces, len(Srcs))) - # elif self._fieldType == 'e' or self._fieldType == 'h': - # ifields = np.zeros((self.mesh.n_edges, len(Srcs))) - - # for i, src in enumerate(self.survey.source_list): - dun_dm_v = np.hstack( - [ - mkvc(self.getInitialFieldsDeriv(src, v, f=f), 2) - for src in self.survey.source_list - ] - ) - # can over-write this at each timestep - # store the field derivs we need to project to calc full deriv - df_dm_v = self.Fields_Derivs(self) - - Adiaginv = None - - for tInd, dt in zip(range(self.nT), self.time_steps): - # keep factors if dt is the same as previous step b/c A will be the - # same - if Adiaginv is not None and (tInd > 0 and dt != self.time_steps[tInd - 1]): - Adiaginv.clean() - Adiaginv = None - - if Adiaginv is None: - A = self.getAdiag(tInd) - Adiaginv = self.solver(A, **self.solver_opts) - - Asubdiag = self.getAsubdiag(tInd) - - for i, src in enumerate(self.survey.source_list): - # here, we are lagging by a timestep, so filling in as we go - for projField in set([rx.projField for rx in src.receiver_list]): - df_dmFun = getattr(f, "_%sDeriv" % projField, None) - # df_dm_v is dense, but we only need the times at - # (rx.P.T * ones > 0) - # This should be called rx.footprint - - df_dm_v[src, "{}Deriv".format(projField), tInd] = df_dmFun( - tInd, src, dun_dm_v[:, i], v - ) - - un_src = f[src, ftype, tInd + 1] - - # cell centered on time mesh - dA_dm_v = self.getAdiagDeriv(tInd, un_src, v) - # on nodes of time mesh - dRHS_dm_v = self.getRHSDeriv(tInd + 1, src, v) - - dAsubdiag_dm_v = self.getAsubdiagDeriv(tInd, f[src, ftype, tInd], v) - - JRHS = dRHS_dm_v - dAsubdiag_dm_v - dA_dm_v - - # step in time and overwrite - if tInd != len(self.time_steps + 1): - dun_dm_v[:, i] = Adiaginv * (JRHS - Asubdiag * dun_dm_v[:, i]) - - Jv = [] - for src in self.survey.source_list: - for rx in src.receiver_list: - Jv.append( - rx.evalDeriv( - src, - self.mesh, - self.time_mesh, - f, - mkvc(df_dm_v[src, "%sDeriv" % rx.projField, :]), - ) - ) - Adiaginv.clean() - # del df_dm_v, dun_dm_v, Asubdiag - # return mkvc(Jv) - return np.hstack(Jv) - - def Jtvec(self, m, v, f=None): - r""" - Jvec computes the adjoint of the sensitivity times a vector - - .. math:: - - \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} - - where - - .. math:: - - \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 - """ - - 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) - - df_duT_v = self.Fields_Derivs(self) - - # same size as fields at a single timestep - ATinv_df_duT_v = np.zeros( - ( - len(self.survey.source_list), - len(f[self.survey.source_list[0], ftype, 0]), - ), - dtype=float, - ) - JTv = np.zeros(m.shape, dtype=float) - - # Loop over sources and receivers to create a fields object: - # PT_v, df_duT_v, df_dmT_v - # initialize storage for PT_v (don't need to preserve over sources) - PT_v = self.Fields_Derivs(self) - for src in self.survey.source_list: - # Looping over initializing field class is appending memory! - # PT_v = Fields_Derivs(self.mesh) # initialize storage - # #for PT_v (don't need to preserve over sources) - # initialize size - df_duT_v[src, "{}Deriv".format(self._fieldType), :] = np.zeros_like( - f[src, self._fieldType, :] - ) - - for rx in src.receiver_list: - PT_v[src, "{}Deriv".format(rx.projField), :] = rx.evalDeriv( - src, self.mesh, self.time_mesh, f, mkvc(v[src, rx]), adjoint=True - ) # this is += - - # PT_v = np.reshape(curPT_v,(len(curPT_v)/self.time_mesh.nN, - # self.time_mesh.nN), order='F') - df_duTFun = getattr(f, "_{}Deriv".format(rx.projField), None) - - for tInd in range(self.nT + 1): - cur = df_duTFun( - tInd, - src, - None, - mkvc(PT_v[src, "{}Deriv".format(rx.projField), tInd]), - adjoint=True, - ) - - df_duT_v[src, "{}Deriv".format(self._fieldType), tInd] = df_duT_v[ - src, "{}Deriv".format(self._fieldType), tInd - ] + mkvc(cur[0], 2) - JTv = cur[1] + JTv - - del PT_v # no longer need this - - AdiagTinv = None - - # Do the back-solve through time - # if the previous timestep is the same: no need to refactor the matrix - # for tInd, dt in zip(range(self.nT), self.time_steps): - - for tInd in reversed(range(self.nT)): - # tInd = tIndP - 1 - if AdiagTinv is not None and ( - tInd <= self.nT and self.time_steps[tInd] != self.time_steps[tInd + 1] - ): - AdiagTinv.clean() - AdiagTinv = None - - # refactor if we need to - if AdiagTinv is None: # and tInd > -1: - Adiag = self.getAdiag(tInd) - AdiagTinv = self.solver(Adiag.T.tocsr(), **self.solver_opts) - - if tInd < self.nT - 1: - Asubdiag = self.getAsubdiag(tInd + 1) - - for isrc, src in enumerate(self.survey.source_list): - # solve against df_duT_v - if tInd >= self.nT - 1: - # last timestep (first to be solved) - ATinv_df_duT_v[isrc, :] = ( - AdiagTinv - * df_duT_v[src, "{}Deriv".format(self._fieldType), tInd + 1] - ) - 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, :]) - ) - - dAsubdiagT_dm_v = self.getAsubdiagDeriv( - tInd, f[src, ftype, tInd], ATinv_df_duT_v[isrc, :], adjoint=True - ) - - dRHST_dm_v = self.getRHSDeriv( - tInd + 1, src, ATinv_df_duT_v[isrc, :], adjoint=True - ) # on nodes of time mesh - - un_src = f[src, ftype, tInd + 1] - # cell centered on time mesh - dAT_dm_v = self.getAdiagDeriv( - tInd, un_src, ATinv_df_duT_v[isrc, :], adjoint=True - ) - - JTv = JTv + mkvc(-dAT_dm_v - dAsubdiagT_dm_v + dRHST_dm_v) - - # Treat the initial condition - - # del df_duT_v, ATinv_df_duT_v, A, Asubdiag - if AdiagTinv is not None: - AdiagTinv.clean() - - 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 - """ - - Srcs = self.survey.source_list - - if self._formulation == "EB": - s_m = np.zeros((self.mesh.n_faces, len(Srcs))) - s_e = np.zeros((self.mesh.n_edges, len(Srcs))) - elif self._formulation == "HJ": - s_m = np.zeros((self.mesh.n_edges, len(Srcs))) - s_e = np.zeros((self.mesh.n_faces, len(Srcs))) - - for i, src in enumerate(Srcs): - smi, sei = src.eval(self, self.times[tInd]) - s_m[:, i] = s_m[:, i] + smi - s_e[:, i] = s_e[:, i] + sei - - return s_m, s_e - - def getInitialFields(self): - """ - Ask the sources for initial fields - """ - - Srcs = self.survey.source_list - - if self._fieldType in ["b", "j"]: - ifields = np.zeros((self.mesh.n_faces, len(Srcs))) - elif self._fieldType in ["e", "h"]: - ifields = np.zeros((self.mesh.n_edges, len(Srcs))) - - if self.verbose: - print("Calculating Initial fields") - - for i, src in enumerate(Srcs): - ifields[:, i] = ifields[:, i] + getattr( - src, "{}Initial".format(self._fieldType), None - )(self) - - return ifields - - def getInitialFieldsDeriv(self, src, v, adjoint=False, f=None): - ifieldsDeriv = mkvc( - getattr(src, "{}InitialDeriv".format(self._fieldType), None)( - self, v, adjoint, f - ) - ) - - # take care of any utils.zero cases - if adjoint is False: - if self._fieldType in ["b", "j"]: - ifieldsDeriv += np.zeros(self.mesh.n_faces) - elif self._fieldType in ["e", "h"]: - ifieldsDeriv += np.zeros(self.mesh.n_edges) - - elif adjoint is True: - if self._fieldType in ["b", "j"]: - ifieldsDeriv += np.zeros(self.mesh.n_faces) - elif self._fieldType in ["e", "h"]: - ifieldsDeriv[0] += np.zeros(self.mesh.n_edges) - ifieldsDeriv[1] += np.zeros_like(self.model) # take care of a Zero() case - - return ifieldsDeriv - - # Store matrix factors if we need to solve the DC problem to get the - # initial condition - @property - def Adcinv(self): - if not hasattr(self, "getAdc"): - raise NotImplementedError( - "Support for galvanic sources has not been implemented for " - "{}-formulation".format(self._fieldType) - ) - if getattr(self, "_Adcinv", None) is None: - if self.verbose: - print("Factoring the system matrix for the DC problem") - Adc = self.getAdc() - self._Adcinv = self.solver(Adc) - 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 - - -############################################################################### -# # -# E-B Formulation # -# # -############################################################################### - -# ------------------------------- Simulation3DMagneticFluxDensity ------------------------------- # - - -class Simulation3DMagneticFluxDensity(BaseTDEMSimulation): - r""" - Starting from the quasi-static E-B formulation of Maxwell's equations - (semi-discretized) - - .. math:: - - \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 - - .. math:: - - \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} - - - to obtain a second order semi-discretized system in :math:`\mathbf{b}` - - .. math:: - - \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} - - - and moving everything except the time derivative to the rhs gives - - .. 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} - - For the time discretization, we use backward euler. To solve for the - :math:`n+1` th time step, we have - - .. math:: - - \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} - - - re-arranging to put :math:`\mathbf{b}^{n+1}` on the left hand side gives - - .. 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}) - - """ - - _fieldType = "b" - _formulation = "EB" - fieldsPair = Fields3DMagneticFluxDensity #: A SimPEG.EM.TDEM.Fields3DMagneticFluxDensity object - Fields_Derivs = FieldsDerivativesEB - - def getAdiag(self, tInd): - r""" - System matrix at a given time index - - .. math:: - - (\mathbf{I} + \mathbf{dt} \mathbf{C} \mathbf{M_{\sigma}^e}^{-1} - \mathbf{C}^{\top} \mathbf{M_{\mu^{-1}}^f}) - - """ - assert tInd >= 0 and tInd < self.nT - - dt = self.time_steps[tInd] - C = self.mesh.edge_curl - MeSigmaI = self.MeSigmaI - MfMui = self.MfMui - I = speye(self.mesh.n_faces) - - A = 1.0 / dt * I + (C * (MeSigmaI * (C.T.tocsr() * MfMui))) - - if self._makeASymmetric is True: - return MfMui.T.tocsr() * A - return A - - def getAdiagDeriv(self, tInd, u, v, adjoint=False): - """ - Derivative of ADiag - """ - C = self.mesh.edge_curl - - # def MeSigmaIDeriv(x): - # return self.MeSigmaIDeriv(x) - - MfMui = self.MfMui - - if adjoint: - if self._makeASymmetric is True: - v = MfMui * v - return self.MeSigmaIDeriv(C.T * (MfMui * u), C.T * v, adjoint) - - ADeriv = C * (self.MeSigmaIDeriv(C.T * (MfMui * u), v, adjoint)) - - if self._makeASymmetric is True: - return MfMui.T * ADeriv - return ADeriv - - def getAsubdiag(self, tInd): - """ - Matrix below the diagonal - """ - - dt = self.time_steps[tInd] - MfMui = self.MfMui - Asubdiag = -1.0 / dt * sp.eye(self.mesh.n_faces) - - if self._makeASymmetric is True: - return MfMui.T * Asubdiag - - return Asubdiag - - def getAsubdiagDeriv(self, tInd, u, v, adjoint=False): - return Zero() * v - - def getRHS(self, tInd): - """ - Assemble the RHS - """ - C = self.mesh.edge_curl - MeSigmaI = self.MeSigmaI - MfMui = self.MfMui - - s_m, s_e = self.getSourceTerm(tInd) - - rhs = C * (MeSigmaI * s_e) + s_m - if self._makeASymmetric is True: - return MfMui.T * rhs - return rhs - - def getRHSDeriv(self, tInd, src, v, adjoint=False): - """ - Derivative of the RHS - """ - - C = self.mesh.edge_curl - MeSigmaI = self.MeSigmaI - - _, s_e = src.eval(self, self.times[tInd]) - s_mDeriv, s_eDeriv = src.evalDeriv(self, self.times[tInd], adjoint=adjoint) - - if adjoint: - if self._makeASymmetric is True: - v = self.MfMui * v - if isinstance(s_e, Zero): - MeSigmaIDerivT_v = Zero() - else: - MeSigmaIDerivT_v = self.MeSigmaIDeriv(s_e, C.T * v, adjoint) - - RHSDeriv = MeSigmaIDerivT_v + s_eDeriv(MeSigmaI.T * (C.T * v)) + s_mDeriv(v) - - return RHSDeriv - - if isinstance(s_e, Zero): - MeSigmaIDeriv_v = Zero() - else: - MeSigmaIDeriv_v = self.MeSigmaIDeriv(s_e, v, adjoint) - - RHSDeriv = C * MeSigmaIDeriv_v + C * MeSigmaI * s_eDeriv(v) + s_mDeriv(v) - - if self._makeASymmetric is True: - return self.MfMui.T * RHSDeriv - return RHSDeriv - - -# ------------------------------- Simulation3DElectricField ------------------------------- # -class Simulation3DElectricField(BaseTDEMSimulation): - r""" - Solve the EB-formulation of Maxwell's equations for the electric field, e. - - Starting with - - .. math:: - - \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} - - - we eliminate :math:`\frac{\partial b}{\partial t}` using - - .. math:: - - \frac{\partial \mathbf{b}}{\partial t} = - \nabla \times \mathbf{e} + \mathbf{s_m} - - - taking the time-derivative of Ampere's law, we see - - .. math:: - - \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} - - - which gives us - - .. math:: - - \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} - - - """ - - _fieldType = "e" - _formulation = "EB" - fieldsPair = Fields3DElectricField #: A Fields3DElectricField - Fields_Derivs = FieldsDerivativesEB - - # @profile - def Jtvec(self, m, v, f=None): - """ - Jvec computes the adjoint of the sensitivity times a vector - """ - - 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) - - df_duT_v = self.Fields_Derivs(self) - - # same size as fields at a single timestep - ATinv_df_duT_v = np.zeros( - ( - len(self.survey.source_list), - len(f[self.survey.source_list[0], ftype, 0]), - ), - dtype=float, - ) - JTv = np.zeros(m.shape, dtype=float) - - # Loop over sources and receivers to create a fields object: - # PT_v, df_duT_v, df_dmT_v - # initialize storage for PT_v (don't need to preserve over sources) - PT_v = self.Fields_Derivs(self) - for src in self.survey.source_list: - # Looping over initializing field class is appending memory! - # PT_v = Fields_Derivs(self.mesh) # initialize storage - # #for PT_v (don't need to preserve over sources) - # initialize size - df_duT_v[src, "{}Deriv".format(self._fieldType), :] = np.zeros_like( - f[src, self._fieldType, :] - ) - - for rx in src.receiver_list: - PT_v[src, "{}Deriv".format(rx.projField), :] = rx.evalDeriv( - src, self.mesh, self.time_mesh, f, mkvc(v[src, rx]), adjoint=True - ) - # this is += - - # PT_v = np.reshape(curPT_v,(len(curPT_v)/self.time_mesh.nN, - # self.time_mesh.nN), order='F') - df_duTFun = getattr(f, "_{}Deriv".format(rx.projField), None) - - for tInd in range(self.nT + 1): - cur = df_duTFun( - tInd, - src, - None, - mkvc(PT_v[src, "{}Deriv".format(rx.projField), tInd]), - adjoint=True, - ) - - df_duT_v[src, "{}Deriv".format(self._fieldType), tInd] = df_duT_v[ - src, "{}Deriv".format(self._fieldType), tInd - ] + mkvc(cur[0], 2) - JTv = cur[1] + JTv - - # no longer need this - del PT_v - - AdiagTinv = None - - # Do the back-solve through time - # if the previous timestep is the same: no need to refactor the matrix - # for tInd, dt in zip(range(self.nT), self.time_steps): - - for tInd in reversed(range(self.nT)): - # tInd = tIndP - 1 - if AdiagTinv is not None and ( - tInd <= self.nT and self.time_steps[tInd] != self.time_steps[tInd + 1] - ): - AdiagTinv.clean() - AdiagTinv = None - - # refactor if we need to - if AdiagTinv is None: # and tInd > -1: - Adiag = self.getAdiag(tInd) - AdiagTinv = self.solver(Adiag.T, **self.solver_opts) - - if tInd < self.nT - 1: - Asubdiag = self.getAsubdiag(tInd + 1) - - for isrc, src in enumerate(self.survey.source_list): - # solve against df_duT_v - if tInd >= self.nT - 1: - # last timestep (first to be solved) - ATinv_df_duT_v[isrc, :] = ( - AdiagTinv - * df_duT_v[src, "{}Deriv".format(self._fieldType), tInd + 1] - ) - 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, :]) - ) - - dAsubdiagT_dm_v = self.getAsubdiagDeriv( - tInd, f[src, ftype, tInd], ATinv_df_duT_v[isrc, :], adjoint=True - ) - - dRHST_dm_v = self.getRHSDeriv( - tInd + 1, src, ATinv_df_duT_v[isrc, :], adjoint=True - ) # on nodes of time mesh - - un_src = f[src, ftype, tInd + 1] - # cell centered on time mesh - dAT_dm_v = self.getAdiagDeriv( - tInd, un_src, ATinv_df_duT_v[isrc, :], adjoint=True - ) - - JTv = JTv + mkvc(-dAT_dm_v - dAsubdiagT_dm_v + dRHST_dm_v) - - # Treating initial condition when a galvanic source is included - tInd = -1 - Grad = self.mesh.nodal_gradient - - for isrc, src in enumerate(self.survey.source_list): - if src.srcType == "galvanic": - ATinv_df_duT_v[isrc, :] = Grad * ( - self.Adcinv - * ( - Grad.T - * ( - mkvc( - df_duT_v[ - src, "{}Deriv".format(self._fieldType), tInd + 1 - ] - ) - - Asubdiag.T * mkvc(ATinv_df_duT_v[isrc, :]) - ) - ) - ) - - dRHST_dm_v = self.getRHSDeriv( - tInd + 1, src, ATinv_df_duT_v[isrc, :], adjoint=True - ) # on nodes of time mesh - - un_src = f[src, ftype, tInd + 1] - # cell centered on time mesh - dAT_dm_v = self.MeSigmaDeriv( - un_src, ATinv_df_duT_v[isrc, :], adjoint=True - ) - - JTv = JTv + mkvc(-dAT_dm_v + dRHST_dm_v) - - # del df_duT_v, ATinv_df_duT_v, A, Asubdiag - if AdiagTinv is not None: - AdiagTinv.clean() - - return mkvc(JTv).astype(float) - - def getAdiag(self, tInd): - """ - Diagonal of the system matrix at a given time index - """ - assert tInd >= 0 and tInd < self.nT - - dt = self.time_steps[tInd] - C = self.mesh.edge_curl - MfMui = self.MfMui - MeSigma = self.MeSigma - - 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 - """ - assert tInd >= 0 and tInd < self.nT - - dt = self.time_steps[tInd] - # MeSigmaDeriv = self.MeSigmaDeriv(u) - - if adjoint: - return 1.0 / dt * self.MeSigmaDeriv(u, v, adjoint) - - return 1.0 / dt * self.MeSigmaDeriv(u, v, adjoint) - - def getAsubdiag(self, tInd): - """ - Matrix below the diagonal - """ - assert tInd >= 0 and tInd < self.nT - - dt = self.time_steps[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 - """ - dt = self.time_steps[tInd] - - if adjoint: - return -1.0 / dt * self.MeSigmaDeriv(u, v, adjoint) - - return -1.0 / dt * self.MeSigmaDeriv(u, v, adjoint) - - def getRHS(self, tInd): - """ - right hand side - """ - # Omit this: Note input was tInd+1 - # if tInd == len(self.time_steps): - # tInd = tInd - 1 - - dt = self.time_steps[tInd - 1] - s_m, s_e = self.getSourceTerm(tInd) - _, s_en1 = self.getSourceTerm(tInd - 1) - - 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() - - 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 - - def getAdcDeriv(self, u, v, adjoint=False): - 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() - - -############################################################################### -# # -# H-J Formulation # -# # -############################################################################### - -# ------------------------------- Simulation3DMagneticField ------------------------------- # - - -class Simulation3DMagneticField(BaseTDEMSimulation): - r""" - Solve the H-J formulation of Maxwell's equations for the magnetic field h. - - We start with Maxwell's equations in terms of the magnetic field and - current density - - .. math:: - - \nabla \times \rho \mathbf{j} + \mu \frac{\partial h}{\partial t} = \mathbf{s_m} \ - \nabla \times \mathbf{h} - \mathbf{j} = \mathbf{s_e} - - - and eliminate :math:`\mathbf{j}` using - - .. math:: - - \mathbf{j} = \nabla \times \mathbf{h} - \mathbf{s_e} - - - giving - - .. math:: - - \nabla \times \rho \nabla \times \mathbf{h} + \mu \frac{\partial h}{\partial t} - = \nabla \times \rho \mathbf{s_e} + \mathbf{s_m} - - - """ - - _fieldType = "h" - _formulation = "HJ" - fieldsPair = Fields3DMagneticField #: Fields object pair - Fields_Derivs = FieldsDerivativesHJ - - def getAdiag(self, tInd): - """ - System matrix at a given time index - - """ - assert tInd >= 0 and tInd < self.nT - - dt = self.time_steps[tInd] - C = self.mesh.edge_curl - MfRho = self.MfRho - MeMu = self.MeMu - - return C.T * (MfRho * C) + 1.0 / dt * MeMu - - def getAdiagDeriv(self, tInd, u, v, adjoint=False): - assert tInd >= 0 and tInd < self.nT - - C = self.mesh.edge_curl - - if adjoint: - return self.MfRhoDeriv(C * u, C * v, adjoint) - - return C.T * self.MfRhoDeriv(C * u, v, adjoint) - - def getAsubdiag(self, tInd): - assert tInd >= 0 and tInd < self.nT - - dt = self.time_steps[tInd] - - return -1.0 / dt * self.MeMu - - def getAsubdiagDeriv(self, tInd, u, v, adjoint=False): - return Zero() - - def getRHS(self, tInd): - C = self.mesh.edge_curl - MfRho = self.MfRho - s_m, s_e = self.getSourceTerm(tInd) - - return C.T * (MfRho * s_e) + s_m - - def getRHSDeriv(self, tInd, src, v, adjoint=False): - C = self.mesh.edge_curl - s_m, s_e = src.eval(self, self.times[tInd]) - - if adjoint is True: - return self.MfRhoDeriv(s_e, C * v, adjoint) - # assumes no source derivs - return C.T * self.MfRhoDeriv(s_e, v, adjoint) - - def getRHSDeriv(self, tInd, src, v, adjoint=False): - return Zero() # assumes no derivs on sources - - def getAdc(self): - 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): - D = sdiag(self.mesh.cell_volumes) * self.mesh.face_divergence - G = D.T - - if adjoint: - # This is the same as - # self.MfRhoIDeriv(G * u, D.T * v, adjoint=True) - return self.MfRhoIDeriv(G * u, G * v, adjoint=True) - return D * self.MfRhoIDeriv(G * u, v) - - -# ------------------------------- Simulation3DCurrentDensity ------------------------------- # - - -class Simulation3DCurrentDensity(BaseTDEMSimulation): - r""" - Solve the H-J formulation for current density - - In this case, we eliminate :math:`\partial \mathbf{h} / \partial t` and - solve for :math:`\mathbf{j}` - - """ - - _fieldType = "j" - _formulation = "HJ" - fieldsPair = Fields3DCurrentDensity #: Fields object pair - Fields_Derivs = FieldsDerivativesHJ - - def getAdiag(self, tInd): - """ - System matrix at a given time index - - """ - assert tInd >= 0 and tInd < self.nT - - dt = self.time_steps[tInd] - C = self.mesh.edge_curl - MfRho = self.MfRho - MeMuI = self.MeMuI - eye = sp.eye(self.mesh.n_faces) - - A = C * (MeMuI * (C.T * MfRho)) + 1.0 / dt * eye - - if self._makeASymmetric: - return MfRho.T * A - - return A - - def getAdiagDeriv(self, tInd, u, v, adjoint=False): - assert tInd >= 0 and tInd < self.nT - - C = self.mesh.edge_curl - MfRho = self.MfRho - MeMuI = self.MeMuI - - if adjoint: - if self._makeASymmetric: - v = MfRho * v - return self.MfRhoDeriv(u, C * (MeMuI.T * (C.T * v)), adjoint) - - ADeriv = C * (MeMuI * (C.T * self.MfRhoDeriv(u, v, adjoint))) - if self._makeASymmetric: - return MfRho.T * ADeriv - return ADeriv - - def getAsubdiag(self, tInd): - assert tInd >= 0 and tInd < self.nT - eye = sp.eye(self.mesh.n_faces) - - dt = self.time_steps[tInd] - - if self._makeASymmetric: - return -1.0 / dt * self.MfRho.T - return -1.0 / dt * eye - - def getAsubdiagDeriv(self, tInd, u, v, adjoint=False): - return Zero() - - def getRHS(self, tInd): - if tInd == len(self.time_steps): - tInd = tInd - 1 - - C = self.mesh.edge_curl - MeMuI = self.MeMuI - dt = self.time_steps[tInd] - s_m, s_e = self.getSourceTerm(tInd) - _, s_en1 = self.getSourceTerm(tInd - 1) - - rhs = -1.0 / dt * (s_e - s_en1) + C * MeMuI * s_m - if self._makeASymmetric: - return self.MfRho.T * rhs - return rhs - - def getRHSDeriv(self, tInd, src, v, adjoint=False): - return Zero() # assumes no derivs on sources - - def getAdc(self): - 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): - D = sdiag(self.mesh.cell_volumes) * self.mesh.face_divergence - G = D.T - - if adjoint: - # This is the same as - # self.MfRhoIDeriv(G * u, D.T * v, adjoint=True) - return self.MfRhoIDeriv(G * u, G * v, adjoint=True) - return D * self.MfRhoIDeriv(G * u, v) diff --git a/SimPEG/objective_function.py b/SimPEG/objective_function.py deleted file mode 100644 index b63d1bfb71..0000000000 --- a/SimPEG/objective_function.py +++ /dev/null @@ -1,446 +0,0 @@ -import numpy as np -import scipy.sparse as sp - -from discretize.tests import check_derivative - -from .maps import IdentityMap -from .props import BaseSimPEG -from .utils import set_kwargs, timeIt, Zero, Identity - -__all__ = ["BaseObjectiveFunction", "ComboObjectiveFunction", "L2ObjectiveFunction"] - - -class BaseObjectiveFunction(BaseSimPEG): - """ - Base Objective Function - - Inherit this to build your own objective function. If building a - regularization, have a look at - :class:`SimPEG.regularization.BaseRegularization` as there are additional - methods and properties tailored to regularization of a model. Similarly, - for building a data misfit, see :class:`SimPEG.DataMisfit.BaseDataMisfit`. - """ - - counter = None - debug = False - - mapPair = IdentityMap #: Base class of expected maps - _mapping = None #: An IdentityMap instance. - _has_fields = False #: should we have the option to store fields - - _nP = None #: number of parameters - - def __init__(self, nP=None, **kwargs): - if nP is not None: - self._nP = nP - set_kwargs(self, **kwargs) - - def __call__(self, x, f=None): - raise NotImplementedError( - "__call__ has not been implemented for {} yet".format( - self.__class__.__name__ - ) - ) - - @property - def nP(self): - """ - Number of model parameters expected. - """ - if self._nP is not None: - return self._nP - if getattr(self, "mapping", None) is not None: - return self.mapping.nP - return "*" - - @property - def _nC_residual(self): - """ - Shape of the residual - """ - if getattr(self, "mapping", None) is not None: - return self.mapping.shape[0] - else: - return self.nP - - @property - def mapping(self): - """ - A `SimPEG.Maps` instance - """ - if self._mapping is None: - if self._nP is not None: - self._mapping = self.mapPair(nP=self.nP) - else: - self._mapping = self.mapPair() - return self._mapping - - @mapping.setter - def mapping(self, value): - assert isinstance(value, self.mapPair), ( - "mapping must be an instance of a {}, not a {}" - ).format(self.mapPair, value.__class__.__name__) - self._mapping = value - - @timeIt - def deriv(self, x, **kwargs): - """ - First derivative of the objective function with respect to the model - """ - raise NotImplementedError( - "The method deriv has not been implemented for {}".format( - self.__class__.__name__ - ) - ) - - @timeIt - def deriv2(self, x, v=None, **kwargs): - """ - Second derivative of the objective function with respect to the model - """ - raise NotImplementedError( - "The method _deriv2 has not been implemented for {}".format( - self.__class__.__name__ - ) - ) - - def _test_deriv(self, x=None, num=4, plotIt=False, **kwargs): - print("Testing {0!s} Deriv".format(self.__class__.__name__)) - if x is None: - if self.nP == "*": - x = np.random.randn(np.random.randint(1e2, high=1e3)) - else: - x = np.random.randn(self.nP) - - return check_derivative( - lambda m: [self(m), self.deriv(m)], x, num=num, plotIt=plotIt, **kwargs - ) - - def _test_deriv2(self, x=None, num=4, plotIt=False, **kwargs): - print("Testing {0!s} Deriv2".format(self.__class__.__name__)) - if x is None: - if self.nP == "*": - x = np.random.randn(np.random.randint(1e2, high=1e3)) - else: - x = np.random.randn(self.nP) - - v = x + 0.1 * np.random.rand(len(x)) - expectedOrder = kwargs.pop("expectedOrder", 1) - return check_derivative( - lambda m: [self.deriv(m).dot(v), self.deriv2(m, v=v)], - x, - num=num, - expectedOrder=expectedOrder, - plotIt=plotIt, - **kwargs - ) - - def test(self, x=None, num=4, plotIt=False, **kwargs): - """ - Run a convergence test on both the first and second derivatives - they - should be second order! - """ - deriv = self._test_deriv(x=x, num=num, **kwargs) - deriv2 = self._test_deriv2(x=x, num=num, plotIt=False, **kwargs) - return deriv & deriv2 - - __numpy_ufunc__ = True - - def __add__(self, objfct2): - if isinstance(objfct2, Zero): - return self - - if not isinstance(objfct2, BaseObjectiveFunction): - raise Exception( - "Cannot add type {} to an objective function. Only " - "ObjectiveFunctions can be added together".format( - objfct2.__class__.__name__ - ) - ) - - if ( - self.__class__.__name__ != "ComboObjectiveFunction" - ): # not isinstance(self, ComboObjectiveFunction): - self = 1 * self - - if ( - objfct2.__class__.__name__ != "ComboObjectiveFunction" - ): # not isinstance(objfct2, ComboObjectiveFunction): - objfct2 = 1 * objfct2 - - objfctlist = self.objfcts + objfct2.objfcts - multipliers = self.multipliers + objfct2.multipliers - - return ComboObjectiveFunction(objfcts=objfctlist, multipliers=multipliers) - - def __radd__(self, objfct2): - return self + objfct2 - - def __mul__(self, multiplier): - return ComboObjectiveFunction([self], [multiplier]) - - def __rmul__(self, multiplier): - return self * multiplier - - def __div__(self, denominator): - return self.__mul__(1.0 / denominator) - - def __truediv__(self, denominator): - return self.__mul__(1.0 / denominator) - - def __rdiv__(self, denominator): - return self.__mul__(1.0 / denominator) - - -class ComboObjectiveFunction(BaseObjectiveFunction): - """ - A composite objective function that consists of multiple objective - functions. Objective functions are stored in a list, and multipliers - are stored in a parallel list. - - .. code::python - - import SimPEG.ObjectiveFunction - phi1 = ObjectiveFunction.L2ObjectiveFunction(nP=10) - phi2 = ObjectiveFunction.L2ObjectiveFunction(nP=10) - - phi = 2*phi1 + 3*phi2 - - is equivalent to - - .. code::python - - import SimPEG.ObjectiveFunction - phi1 = ObjectiveFunction.L2ObjectiveFunction(nP=10) - phi2 = ObjectiveFunction.L2ObjectiveFunction(nP=10) - - phi = ObjectiveFunction.ComboObjectiveFunction( - [phi1, phi2], [2, 3] - ) - - """ - - _multiplier_types = (float, None, Zero, np.float64, int, np.integer) # Directive - _multipliers = None - - def __init__(self, objfcts=None, multipliers=None, **kwargs): - if objfcts is None: - objfcts = [] - if multipliers is None: - multipliers = len(objfcts) * [1] - - self._nP = "*" - - assert len(objfcts) == len(multipliers), ( - "Must have the same number of Objective Functions and Multipliers " - "not {} and {}".format(len(objfcts), len(multipliers)) - ) - - def validate_list(objfctlist, multipliers): - """ - ensure that the number of parameters expected by each objective - function is the same, ensure that if multpliers are supplied, that - list matches the length of the objective function list - """ - for fct, mult in zip(objfctlist, multipliers): - assert isinstance(fct, BaseObjectiveFunction), ( - "Unrecognized objective function type {} in objfcts. " - "All entries in objfcts must inherit from " - "ObjectiveFunction".format(fct.__class__.__name__) - ) - - assert type(mult) in self._multiplier_types, ( - "Objective Functions can only be multiplied by a " - "float, or a properties.Float, not a {}, {}".format( - type(mult), mult - ) - ) - - if fct.nP != "*": - if self._nP != "*": - assert self._nP == fct.nP, ( - "Objective Functions must all have the same " - "nP={}, not {}".format(self.nP, [f.nP for f in objfcts]) - ) - else: - self._nP = fct.nP - - validate_list(objfcts, multipliers) - - self.objfcts = objfcts - self._multipliers = multipliers - - super(ComboObjectiveFunction, self).__init__(**kwargs) - - def __len__(self): - return len(self.multipliers) - - def __getitem__(self, key): - return self.multipliers[key], self.objfcts[key] - - @property - def multipliers(self): - return self._multipliers - - @multipliers.setter - def multipliers(self, value): - for val in value: - assert ( - type(val) in self._multiplier_types - ), "Multiplier must be in type {} not {}".format( - self._multiplier_types, type(val) - ) - - assert len(value) == len(self.objfcts), ( - "the length of multipliers should be the same as the number of" - " objective functions ({}), not {}".format(len(self.objfcts), len(value)) - ) - - self._multipliers = value - - def __call__(self, m, f=None): - fct = 0.0 - for i, phi in enumerate(self): - multiplier, objfct = phi - if multiplier == 0.0: # don't evaluate the fct - continue - else: - if f is not None and objfct._has_fields: - fct += multiplier * objfct(m, f=f[i]) - else: - fct += multiplier * objfct(m) - return fct - - def deriv(self, m, f=None): - """ - First derivative of the composite objective function is the sum of the - derivatives of each objective function in the list, weighted by their - respective multplier. - - :param numpy.ndarray m: model - :param SimPEG.Fields f: Fields object (if applicable) - """ - g = Zero() - for i, phi in enumerate(self): - multiplier, objfct = phi - if multiplier == 0.0: # don't evaluate the fct - continue - else: - if f is not None and objfct._has_fields: - aux = objfct.deriv(m, f=f[i]) - if not isinstance(aux, Zero): - g += multiplier * aux - else: - aux = objfct.deriv(m) - if not isinstance(aux, Zero): - g += multiplier * aux - return g - - def deriv2(self, m, v=None, f=None): - """ - Second derivative of the composite objective function is the sum of the - second derivatives of each objective function in the list, weighted by - their respective multplier. - - :param numpy.ndarray m: model - :param numpy.ndarray v: vector we are multiplying by - :param SimPEG.Fields f: Fields object (if applicable) - """ - H = Zero() - for i, phi in enumerate(self): - multiplier, objfct = phi - if multiplier == 0.0: # don't evaluate the fct - continue - else: - if f is not None and objfct._has_fields: - objfct_H = objfct.deriv2(m, v, f=f[i]) - else: - objfct_H = objfct.deriv2(m, v) - H = H + multiplier * objfct_H - return H - - # This assumes all objective functions have a W. - # The base class currently does not. - @property - def W(self): - """ - W matrix for the full objective function. Includes multiplying by the - square root of alpha. - """ - W = [] - for mult, fct in self: - curW = np.sqrt(mult) * fct.W - if not isinstance(curW, Zero): - W.append(curW) - return sp.vstack(W) - - def get_functions_of_type(self, fun_class) -> list: - """ - Find an objective function type from a ComboObjectiveFunction class. - """ - target = [] - if isinstance(self, fun_class): - target += [self] - else: - for fct in self.objfcts: - if isinstance(fct, ComboObjectiveFunction): - target += [fct.get_functions_of_type(fun_class)] - elif isinstance(fct, fun_class): - target += [fct] - - return [fun for fun in target if fun] - - -class L2ObjectiveFunction(BaseObjectiveFunction): - r""" - An L2-Objective Function - - .. math:: - - \phi = \frac{1}{2}||\mathbf{W} \mathbf{m}||^2 - """ - - def __init__(self, W=None, **kwargs): - super(L2ObjectiveFunction, self).__init__(**kwargs) - if W is not None: - if self.nP == "*": - self._nP = W.shape[1] - self._W = W - - @property - def W(self): - """ - Weighting matrix. The default if not sepcified is an identity. - """ - if getattr(self, "_W", None) is None: - if self._nC_residual != "*": - self._W = sp.eye(self._nC_residual) - else: - self._W = Identity() - return self._W - - def __call__(self, m): - r = self.W * (self.mapping * m) - return 0.5 * r.dot(r) - - def deriv(self, m): - """ - First derivative with respect to the model - - :param numpy.ndarray m: model - """ - return self.mapping.deriv(m).T * (self.W.T * (self.W * (self.mapping * m))) - - def deriv2(self, m, v=None): - """ - Second derivative with respect to the model - - :param numpy.ndarray m: model - :param numpy.ndarray v: vector to multiply by - """ - if v is not None: - return self.mapping.deriv(m).T * ( - self.W.T * (self.W * (self.mapping.deriv(m) * v)) - ) - W = self.W * self.mapping.deriv(m) - return W.T * W diff --git a/SimPEG/potential_fields/gravity/simulation.py b/SimPEG/potential_fields/gravity/simulation.py deleted file mode 100644 index 9ba2c41133..0000000000 --- a/SimPEG/potential_fields/gravity/simulation.py +++ /dev/null @@ -1,271 +0,0 @@ -import numpy as np -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 SimPEG import props -from SimPEG.utils import mkvc, sdiag - -from ...base import BasePDESimulation -from ..base import BaseEquivalentSourceLayerSimulation, BasePFSimulation - - -class Simulation3DIntegral(BasePFSimulation): - """ - Gravity simulation in integral form. - - """ - - rho, rhoMap, rhoDeriv = props.Invertible("Density") - - def __init__(self, mesh, rho=None, rhoMap=None, **kwargs): - super().__init__(mesh, model_map=rhoMap, **kwargs) - self.rho = rho - self.rhoMap = rhoMap - - def fields(self, m): - self.model = m - - if self.store_sensitivities is None: - # Compute the linear operation without forming the full dense Jmatrix - fields = mkvc(self.linear_operator()) - else: - fields = self.Jmatrix @ (self.rho).astype(np.float32) - - return np.asarray(fields) - - def getJtJdiag(self, m, W=None, f=None): - """ - Return the diagonal of JtJ - """ - 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.Jmatrix.shape[1]) - for i in range(len(W)): - diag += W[i] * (self.Jmatrix[i] * self.Jmatrix[i]) - self._gtg_diagonal = diag - else: - diag = self._gtg_diagonal - return mkvc((sdiag(np.sqrt(diag)) @ self.rhoDeriv).power(2).sum(axis=0)) - - def getJ(self, m, f=None): - """ - Sensitivity matrix - """ - return self.Jmatrix.dot(self.rhoDeriv) - - def Jvec(self, m, v, f=None): - """ - Sensitivity times a vector - """ - dmu_dm_v = self.rhoDeriv @ v - return self.Jmatrix @ dmu_dm_v.astype(np.float32) - - def Jtvec(self, m, v, f=None): - """ - Sensitivity transposed times a vector - """ - Jtvec = self.Jmatrix.T @ v.astype(np.float32) - return np.asarray(self.rhoDeriv.T @ Jtvec) - - @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 - and for all components of the survey. - - :param numpy.ndarray receiver_location: array with shape (n_receivers, 3) - Array of receiver locations as x, y, z columns. - :param list[str] components: List of gravity components chosen from: - 'gx', 'gy', 'gz', 'gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz', 'guv' - :param float tolerance: Small constant to avoid singularity near nodes and edges. - :rtype numpy.ndarray: rows - :returns: ndarray with shape (n_components, n_cells) - Dense array mapping of the contribution of all active cells to data components:: - - rows = - g_1 = [g_1x g_1y g_1z] - g_2 = [g_2x g_2y g_2z] - ... - g_c = [g_cx g_cy g_cz] - - """ - dr = self._nodes - receiver_location - dx = dr[..., 0] - dy = dr[..., 1] - dz = dr[..., 2] - - node_evals = {} - if "gx" in components: - node_evals["gx"] = prism_fz(dy, dz, dx) - if "gy" in components: - node_evals["gy"] = prism_fz(dz, dx, dy) - if "gz" in components: - node_evals["gz"] = prism_fz(dx, dy, dz) - if "gxy" in components: - node_evals["gxy"] = prism_fzx(dy, dz, dx) - if "gxz" in components: - node_evals["gxz"] = prism_fzx(dx, dy, dz) - if "gyz" in components: - node_evals["gyz"] = prism_fzy(dx, dy, dz) - if "gxx" in components or "guv" in components: - node_evals["gxx"] = prism_fzz(dy, dz, dx) - if "gyy" in components or "guv" in components: - node_evals["gyy"] = prism_fzz(dz, dx, dy) - if "guv" in components: - node_evals["guv"] = (node_evals["gyy"] - node_evals["gxx"]) * 0.5 - # (NN - EE) / 2 - inside_adjust = False - if "gzz" in components: - if "gxx" not in node_evals or "gyy" not in node_evals: - node_evals["gzz"] = prism_fzz(dx, dy, dz) - else: - inside_adjust = True - # The below need to be adjusted for observation points within a cell. - # because `gxx + gyy + gzz = -4 * pi * G * rho` - # gzz = - gxx - gyy - 4 * np.pi * G * rho[in_cell] - node_evals["gzz"] = -node_evals["gxx"] - node_evals["gyy"] - - rows = {} - for component in set(components): - vals = node_evals[component] - if self._unique_inv is not None: - vals = vals[self._unique_inv] - cell_vals = ( - vals[0] - - vals[1] - - vals[2] - + vals[3] - - vals[4] - + vals[5] - + vals[6] - - vals[7] - ) - if inside_adjust and component == "gzz": - # should subtract 4 * pi to the cell containing the observation point - # just need a little logic to find the containing cell - # cell_vals[inside_cell] += 4 * np.pi - pass - if self.store_sensitivities is None: - rows[component] = cell_vals @ self.rho - else: - rows[component] = cell_vals - if len(component) == 3: - rows[component] *= constants.G * 1e12 # conversion for Eotvos - else: - rows[component] *= constants.G * 1e8 # conversion for mGal - - return np.stack([rows[component] for component in components]) - - -class SimulationEquivalentSourceLayer( - BaseEquivalentSourceLayerSimulation, Simulation3DIntegral -): - """ - Equivalent source layer simulations - - Parameters - ---------- - 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. - 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. - """ - - -class Simulation3DDifferential(BasePDESimulation): - r"""Finite volume simulation class for gravity. - - Notes - ----- - From Blakely (1996), the scalar potential :math:`\phi` outside the source region - is obtained by solving a Poisson's equation: - - .. math:: - \nabla^2 \phi = 4 \pi \gamma \rho - - where :math:`\gamma` is the gravitational constant and :math:`\rho` defines the - distribution of density within the source region. - - Applying the finite volumn method, we can solve the Poisson's equation on a - 3D voxel grid according to: - - .. math:: - \big [ \mathbf{D M_f D^T} \big ] \mathbf{u} = - \mathbf{M_c \, \rho} - """ - - rho, rhoMap, rhoDeriv = props.Invertible("Specific density (g/cc)") - - def __init__(self, mesh, rho=1.0, rhoMap=None, **kwargs): - super().__init__(mesh, **kwargs) - self.rho = rho - self.rhoMap = rhoMap - - self._Div = self.mesh.face_divergence - - def getRHS(self): - """Return right-hand side for the linear system""" - Mc = self.Mcc - rho = self.rho - return -Mc * rho - - def getA(self): - r""" - GetA creates and returns the A matrix for the Gravity nodal problem - - The A matrix has the form: - - .. math :: - - \mathbf{A} = \Div(\Mf Mui)^{-1}\Div^{T} - """ - # Constructs A with 0 dirichlet - if getattr(self, "_A", None) is None: - self._A = self._Div * self.Mf * self._Div.T.tocsr() - return self._A - - def fields(self, m=None): - r"""Compute fields - - **INCOMPLETE** - - Parameters - ---------- - m: (nP) np.ndarray - The model - - Returns - ------- - dict - The fields - """ - if m is not None: - self.model = m - - A = self.getA() - RHS = self.getRHS() - - Ainv = self.solver(A) - u = Ainv * RHS - - gField = 4.0 * np.pi * NewtG * 1e8 * self._Div * u - - return {"G": gField, "u": u} diff --git a/SimPEG/regularization/__init__.py b/SimPEG/regularization/__init__.py deleted file mode 100644 index c411dd3d00..0000000000 --- a/SimPEG/regularization/__init__.py +++ /dev/null @@ -1,283 +0,0 @@ -r""" -============================================= -Regularization (:mod:`SimPEG.regularization`) -============================================= -.. currentmodule:: SimPEG.regularization - -If there is one model that has a misfit that equals the desired tolerance, -then there are infinitely many other models which can fit to the same degree. -The challenge is to find that model which has the desired characteristics and -is compatible with a priori information. A single model can be selected from -an infinite ensemble by measuring the length, or norm, of each model. Then a -smallest, or sometimes largest, member can be isolated. Our goal is to design -a norm that embodies our prior knowledge and, when minimized, yields a -realistic candidate for the solution of our problem. The norm can penalize -variation from a reference model, spatial derivatives of the model, or some -combination of these. - -WeightedLeastSquares Regularization -=================================== - -Here we will define regularization of a model, m, in general however, this -should be thought of as (m-m_ref) but otherwise it is exactly the same: - -.. math:: - - R(m) = - \int_\Omega - \frac{\alpha_x}{2} - \left( \frac{\partial m}{\partial x} \right)^2 - + \frac{\alpha_y}{2} - \left( \frac{\partial m}{\partial y} \right)^2 - \partial v - -Our discrete gradient operator works on cell centers and gives the derivative -on the cell faces, which is not where we want to be evaluating this integral. -We need to average the values back to the cell-centers before we integrate. To -avoid null spaces, we square first and then average. In 2D with ij notation it -looks like this: - -.. math:: - - R(m) \approx - \sum_{ij} - \left[ - \frac{\alpha_x}{2} \left[ - \left( \frac{m_{i+1,j} - m_{i,j}}{h} \right)^2 - + \left( \frac{m_{i,j} - m_{i-1,j}}{h} \right)^2 - \right] \\ - + \frac{\alpha_y}{2} \left[ - \left( \frac{m_{i,j+1} - m_{i,j}}{h} \right)^2 - + \left( \frac{m_{i,j} - m_{i,j-1}}{h} \right)^2 - \right] - \right] h^2 - -If we let D_1 be the derivative matrix in the x direction - -.. math:: - - \mathbf{D}_1 = \mathbf{I}_2\otimes\mathbf{d}_1 - -.. math:: - - \mathbf{D}_2 = \mathbf{d}_2\otimes\mathbf{I}_1 - -Where d_1 is the one dimensional derivative: - -.. math:: - - \mathbf{d}_1 = \frac{1}{h} \left[ \begin{array}{cccc} - -1 & 1 & & \\ - & \ddots & \ddots&\\ - & & -1 & 1\end{array} \right] - -.. math:: - - R(m) \approx - \mathbf{v}^\top \left[ - \frac{\alpha_x}{2} \mathbf{A}_1 (\mathbf{D}_1 m) \odot (\mathbf{D}_1 m) - + \frac{\alpha_y}{2} \mathbf{A}_2 (\mathbf{D}_2 m) \odot (\mathbf{D}_2 m) - \right] - -Recall that this is really a just point wise multiplication, or a diagonal -matrix times a vector. When we multiply by something in a diagonal we can -interchange and it gives the same results (i.e. it is point wise) - -.. math:: - - \mathbf{a\odot b} - = \text{diag}(\mathbf{a})\mathbf{b} - = \text{diag}(\mathbf{b})\mathbf{a} - = \mathbf{b\odot a} - -and the transpose also is true (but the sizes have to make sense...): - -.. math:: - - \mathbf{a}^\top\text{diag}(\mathbf{b}) = \mathbf{b}^\top\text{diag}(\mathbf{a}) - -So R(m) can simplify to: - -.. math:: - - R(m) \approx - \mathbf{m}^\top \left[ - \frac{\alpha_x}{2} \mathbf{D}_1^\top - \text{diag} (\mathbf{A}_1^\top \mathbf{v}) \mathbf{D}_1 - + \frac{\alpha_y}{2} \mathbf{D}_2^\top - \text{diag} (\mathbf{A}_2^\top \mathbf{v}) \mathbf{D}_2 - \right] - \mathbf{m} - -We will define W_x as: - -.. math:: - - \mathbf{W}_x = - \sqrt{\alpha_x} - \text{diag} \left(\sqrt{\mathbf{A}_1^\top\mathbf{v}}\right) \mathbf{D}_1 - - -And then W as a tall matrix of all of the different regularization terms: - -.. math:: - - \mathbf{W} = \left[ \begin{array}{c} - \mathbf{W}_s\\ - \mathbf{W}_x\\ - \mathbf{W}_y\end{array} \right] - -Then we can write - -.. math:: - - R(m) \approx \frac{1}{2}\mathbf{m^\top W^\top W m} - - -The API -======= - -Least Squares Regularizations ------------------------------ -.. autosummary:: - :toctree: generated/ - - WeightedLeastSquares - Smallness - SmoothnessFirstOrder - SmoothnessSecondOrder - -Sparse Regularizations ----------------------- -We have also implemented several sparse regularizations with a variable norm. - -.. autosummary:: - :toctree: generated/ - - Sparse - SparseSmallness - SparseSmoothness - -Joint Regularizations ---------------------- -There are several joint inversion regularizers available - -.. autosummary:: - :toctree: generated/ - - CrossGradient - JointTotalVariation - PGI - PGIsmallness - LinearCorrespondence - -Base Regularization classes ---------------------------- -.. autosummary:: - :toctree: generated/ - - RegularizationMesh - BaseRegularization - BaseSimilarityMeasure - BaseSparse - -""" -from ..utils.code_utils import deprecate_class -from .base import ( - BaseRegularization, - WeightedLeastSquares, - BaseSimilarityMeasure, - Smallness, - SmoothnessFirstOrder, - SmoothnessSecondOrder, -) -from .regularization_mesh import RegularizationMesh -from .sparse import BaseSparse, SparseSmallness, SparseSmoothness, Sparse -from .pgi import PGIsmallness, PGI -from .cross_gradient import CrossGradient -from .correspondence import LinearCorrespondence -from .jtv import JointTotalVariation - - -@deprecate_class(removal_version="0.19.0", future_warn=True) -class SimpleSmall(Smallness): - """Deprecated class, replaced by Smallness.""" - - pass - - -@deprecate_class(removal_version="0.19.0", future_warn=True) -class SimpleSmoothDeriv(SmoothnessFirstOrder): - """Deprecated class, replaced by SmoothnessFirstOrder.""" - - pass - - -@deprecate_class(removal_version="0.19.0", future_warn=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", future_warn=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", future_warn=True) -class Small(Smallness): - """Deprecated class, replaced by Smallness.""" - - pass - - -@deprecate_class(removal_version="0.19.0", future_warn=True) -class SmoothDeriv(SmoothnessFirstOrder): - """Deprecated class, replaced by SmoothnessFirstOrder.""" - - pass - - -@deprecate_class(removal_version="0.19.0", future_warn=True) -class SmoothDeriv2(SmoothnessSecondOrder): - """Deprecated class, replaced by SmoothnessSecondOrder.""" - - pass - - -@deprecate_class(removal_version="0.19.0", future_warn=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", future_warn=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/base.py b/SimPEG/regularization/base.py deleted file mode 100644 index 4c74cad41f..0000000000 --- a/SimPEG/regularization/base.py +++ /dev/null @@ -1,1269 +0,0 @@ -from __future__ import annotations - -import numpy as np -from discretize.base import BaseMesh -import warnings -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 - -import scipy.sparse as sp - - -class BaseRegularization(BaseObjectiveFunction): - """ - Base class for regularization. Inherit this for building your own - regularization. The base regularization assumes a weighted l2-norm style of - regularization. However, if you wish to employ a different norm, the - methods :meth:`__call__`, :meth:`deriv` and :meth:`deriv2` can be - over-written - - :param discretize.base.BaseMesh mesh: SimPEG mesh - :param active_cells: Array of bool defining the set of active cells. - :param mapping: Model map - :param reference_model: Array of model values used to constrain the inversion - :param units: Model units identifier. Special case for 'radian' - :param weights: Weight multipliers to customize the least-squares function. - """ - - def __init__( - self, - mesh: RegularizationMesh | BaseMesh, - active_cells: np.ndarray | None = None, - mapping: maps.IdentityMap | None = None, - reference_model: np.ndarray | None = None, - units: str | None = None, - weights: dict | None = None, - **kwargs, - ): - if isinstance(mesh, BaseMesh): - mesh = RegularizationMesh(mesh) - - if not isinstance(mesh, RegularizationMesh): - raise TypeError( - f"'regularization_mesh' must be of type {RegularizationMesh} or {BaseMesh}. " - f"Value of type {type(mesh)} provided." - ) - - self._model = None - self._parent = None - self._regularization_mesh = mesh - self._weights = {} - - if active_cells is not None: - self.active_cells = active_cells - - self.mapping = mapping - - super().__init__(**kwargs) - - self.reference_model = reference_model - self.units = units - - if weights is not None: - if not isinstance(weights, dict): - weights = {"user_weights": weights} - self.set_weights(**weights) - - @property - def active_cells(self) -> np.ndarray: - """A boolean array of active cells on the regularization - - Returns - ------- - (n_cells, ) Array of bool - - Notes - ----- - If this is set with an array of integers, it interprets it as an array - listing the active cell indices. - """ - return self.regularization_mesh.active_cells - - @active_cells.setter - def active_cells(self, values: np.ndarray | None): - self.regularization_mesh.active_cells = values - - if values is not None: - volume_term = "volume" in self._weights - self._weights = {} - self._W = None - if volume_term: - self.set_weights(volume=self.regularization_mesh.vol) - - indActive = deprecate_property( - active_cells, - "indActive", - "active_cells", - "0.19.0", - future_warn=True, - error=False, - ) - - @property - def model(self) -> np.ndarray: - """Physical property model""" - return self._model - - @model.setter - def model(self, values: np.ndarray | float): - if isinstance(values, float): - values = np.ones(self._nC_residual) * values - - values = validate_ndarray_with_shape( - "model", values, shape=(self._nC_residual,), dtype=float - ) - - self._model = values - - @property - def mapping(self) -> maps.IdentityMap: - """Mapping applied to the model values""" - return self._mapping - - @mapping.setter - def mapping(self, mapping: maps.IdentityMap): - if mapping is None: - mapping = maps.IdentityMap() - if not isinstance(mapping, maps.IdentityMap): - raise TypeError( - f"'mapping' must be of type {maps.IdentityMap}. " - f"Value of type {type(mapping)} provided." - ) - self._mapping = mapping - - @property - def parent(self): - """ - The parent objective function - """ - return self._parent - - @parent.setter - def parent(self, parent): - combo_class = ComboObjectiveFunction - if not isinstance(parent, combo_class): - raise TypeError( - f"Invalid parent of type '{parent.__class__.__name__}'. " - f"Parent must be a {combo_class.__name__}." - ) - self._parent = parent - - @property - def units(self) -> str | None: - """Specify the model units. Special care given to 'radian' values""" - return self._units - - @units.setter - def units(self, units: str | None): - if units is not None and not isinstance(units, str): - raise TypeError( - f"'units' must be None or type str. " - f"Value of type {type(units)} provided." - ) - self._units = units - - @property - def _weights_shapes(self) -> tuple[int] | str: - """Acceptable lengths for the weights - - Returns - ------- - list of tuple - Each tuple represents accetable shapes for the weights - """ - if ( - getattr(self, "_regularization_mesh", None) is not None - and self.regularization_mesh.nC != "*" - ): - return (self.regularization_mesh.nC,) - - if getattr(self, "_mapping", None) is not None and self.mapping.shape != "*": - return (self.mapping.shape[0],) - - return ("*",) - - @property - def reference_model(self) -> np.ndarray: - """Reference physical property model""" - return self._reference_model - - @reference_model.setter - def reference_model(self, values: np.ndarray | float): - if values is not None: - if isinstance(values, float): - values = np.ones(self._nC_residual) * values - - values = validate_ndarray_with_shape( - "reference_model", values, shape=(self._nC_residual,), dtype=float - ) - self._reference_model = values - - mref = deprecate_property( - reference_model, - "mref", - "reference_model", - "0.19.0", - future_warn=True, - error=False, - ) - - @property - def regularization_mesh(self) -> RegularizationMesh: - """Regularization mesh""" - return self._regularization_mesh - - regmesh = deprecate_property( - regularization_mesh, - "regmesh", - "regularization_mesh", - "0.19.0", - future_warn=True, - error=False, - ) - - @property - def cell_weights(self) -> np.ndarray: - """Deprecated property for 'volume' and user defined weights.""" - warnings.warn( - "cell_weights are deprecated please access weights using the `set_weights`," - " `get_weights`, and `remove_weights` functionality. This will be removed in 0.19.0", - FutureWarning, - ) - return np.prod(list(self._weights.values()), axis=0) - - @cell_weights.setter - def cell_weights(self, value): - warnings.warn( - "cell_weights are deprecated please access weights using the `set_weights`," - " `get_weights`, and `remove_weights` functionality. This will be removed in 0.19.0", - FutureWarning, - ) - self.set_weights(cell_weights=value) - - def get_weights(self, key) -> np.ndarray: - """Weights for a given key.""" - return self._weights[key] - - def set_weights(self, **weights): - """Adds (or updates) the specified weights to the regularization - - Parameters: - ----------- - **kwargs : key, numpy.ndarray - Each keyword argument is added to the weights used by the regularization. - They can be accessed with their keyword argument. - - Examples - -------- - >>> import discretize - >>> from SimPEG.regularization import Smallness - >>> mesh = discretize.TensorMesh([2, 3, 2]) - >>> reg = Smallness(mesh) - >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) - >>> reg.get_weights('my_weight') - array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) - """ - for key, values in weights.items(): - values = validate_ndarray_with_shape( - "weights", values, shape=self._weights_shapes, dtype=float - ) - self._weights[key] = values - self._W = None - - def remove_weights(self, key): - """Removes the weights with a given key""" - try: - self._weights.pop(key) - except KeyError as error: - raise KeyError(f"{key} is not in the weights dictionary") from error - self._W = None - - @property - def W(self) -> np.ndarray: - """ - Weighting matrix - """ - if getattr(self, "_W", None) is None: - weights = np.prod(list(self._weights.values()), axis=0) - self._W = utils.sdiag(weights**0.5) - return self._W - - @property - def _nC_residual(self) -> int: - """ - Shape of the residual - """ - - nC = getattr(self.regularization_mesh, "nC", None) - mapping = getattr(self, "_mapping", None) - - if mapping is not None and mapping.shape[1] != "*": - return self.mapping.shape[1] - - if nC != "*" and nC is not None: - return self.regularization_mesh.nC - - return self._weights_shapes[0] - - def _delta_m(self, m) -> np.ndarray: - if self.reference_model is None: - return m - return ( - m - self.reference_model - ) # in case self.reference_model is Zero, returns type m - - @utils.timeIt - def __call__(self, m): - r""" - We use a weighted 2-norm objective function - - .. math:: - - r(m) = \frac{1}{2} \| \mathbf{W} \mathbf{f(m)} \|_2^2 - """ - r = self.W * self.f_m(m) - return 0.5 * r.dot(r) - - def f_m(self, m) -> np.ndarray: - raise AttributeError("Regularization class must have a 'f_m' implementation.") - - def f_m_deriv(self, m) -> sp.csr_matrix: - raise AttributeError( - "Regularization class must have a 'f_m_deriv' implementation." - ) - - @utils.timeIt - def deriv(self, m) -> np.ndarray: - r""" - The regularization is: - - .. math:: - - R(m) = \frac{1}{2}\mathbf{(m-m_\text{ref})^\top W^\top - W(m-m_\text{ref})} - - So the derivative is straight forward: - - .. math:: - - R(m) = \mathbf{W^\top W (m-m_\text{ref})} - - """ - r = self.W * self.f_m(m) - return self.f_m_deriv(m).T * (self.W.T * r) - - @utils.timeIt - def deriv2(self, m, v=None) -> sp.csr_matrix: - r""" - Second derivative - - :param numpy.ndarray m: geophysical model - :param numpy.ndarray v: vector to multiply - :rtype: scipy.sparse.sp.csr_matrix - :return: WtW, or if v is supplied WtW*v (numpy.ndarray) - - The regularization is: - - .. math:: - - R(m) = \frac{1}{2}\mathbf{(m-m_\text{ref})^\top W^\top - W(m-m_\text{ref})} - - So the second derivative is straight forward: - - .. math:: - - R(m) = \mathbf{W^\top W} - - """ - f_m_deriv = self.f_m_deriv(m) - if v is None: - return f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv) - - return f_m_deriv.T * (self.W.T * (self.W * (f_m_deriv * v))) - - -class Smallness(BaseRegularization): - r""" - Small regularization - L2 regularization on the difference between a - model and a reference model. - - .. math:: - - r(m) = \frac{1}{2}(\mathbf{m} - \mathbf{m_ref})^\top \mathbf{V}^T - \mathbf{W}^T - \mathbf{W} \mathbf{V} (\mathbf{m} - \mathbf{m_{ref}}) - - where - :math:`\mathbf{m}` is the model, - :math:`\mathbf{m_{ref}}` is a reference model, - :math:`\mathbf{V}` are square root of cell volumes and - :math:`\mathbf{W}` is a weighting matrix (default Identity). If fixed or - free weights are provided, then it is :code:`diag(np.sqrt(weights))`). - - - **Optional Inputs** - - :param discretize.base.BaseMesh mesh: SimPEG mesh - :param int shape: number of parameters - :param IdentityMap mapping: regularization mapping, takes the model from - model space to the space you want to regularize in - :param numpy.ndarray reference_model: reference model - :param numpy.ndarray active_cells: active cell indices for reducing the size - of differential operators in the definition of a regularization mesh - :param numpy.ndarray weights: cell weights - - """ - - _multiplier_pair = "alpha_s" - - def __init__(self, mesh, **kwargs): - super().__init__(mesh, **kwargs) - self.set_weights(volume=self.regularization_mesh.vol) - - def f_m(self, m) -> np.ndarray: - """ - Model residual - """ - return self.mapping * self._delta_m(m) - - def f_m_deriv(self, m) -> sp.csr_matrix: - """ - Derivative of the model residual - """ - return self.mapping.deriv(self._delta_m(m)) - - -class SmoothnessFirstOrder(BaseRegularization): - """ - Smooth Regularization. This base class regularizes on the first - spatial derivative, optionally normalized by the base cell size. - - **Optional Inputs** - - :param discretize.base.BaseMesh mesh: SimPEG mesh - :param IdentityMap mapping: regularization mapping, takes the model from - model space to the space you want to regularize in - :param numpy.ndarray reference_model: reference model - :param numpy.ndarray active_cells: active cell indices for reducing the - size of differential operators in the definition of a regularization mesh - :param numpy.ndarray weights: cell weights - :param bool reference_model_in_smooth: include the reference model in the - smoothness computation? (eg. look at Deriv of m (False) or Deriv of - (m-reference_model) (True)) - :param numpy.ndarray weights: vector of cell weights (applied in all terms) - """ - - def __init__( - self, mesh, orientation="x", reference_model_in_smooth=False, **kwargs - ): - self.reference_model_in_smooth = reference_model_in_smooth - - if orientation not in ["x", "y", "z"]: - raise ValueError("Orientation must be 'x', 'y' or 'z'") - - if orientation == "y" and mesh.dim < 2: - raise ValueError( - "Mesh must have at least 2 dimensions to regularize along the " - "y-direction." - ) - elif orientation == "z" and mesh.dim < 3: - raise ValueError( - "Mesh must have at least 3 dimensions to regularize along the " - "z-direction" - ) - self._orientation = orientation - self.__cell_distances = None - super().__init__(mesh=mesh, **kwargs) - self.set_weights(volume=self.regularization_mesh.vol) - - @property - def _weights_shapes(self): - """Acceptable lengths for the weights - - Returns - ------- - tuple - A tuple of each acceptable lengths for the weights - """ - n_active_f, n_active_c = getattr( - self.regularization_mesh, "aveCC2F{}".format(self.orientation) - ).shape - return [(n_active_f,), (n_active_c,)] - - @property - def cell_gradient(self): - """Cell gradient operator""" - if getattr(self, "_cell_gradient", None) is None: - self._cell_gradient = getattr( - self.regularization_mesh, "cell_gradient_{}".format(self.orientation) - ) - return self._cell_gradient - - @property - def reference_model_in_smooth(self) -> bool: - """ - Use the reference model in the model gradient penalties. - """ - return self._reference_model_in_smooth - - @reference_model_in_smooth.setter - def reference_model_in_smooth(self, value: bool): - if not isinstance(value, bool): - raise TypeError( - "'reference_model_in_smooth must be of type 'bool'. " - f"Value of type {type(value)} provided." - ) - self._reference_model_in_smooth = value - - def _delta_m(self, m): - if self.reference_model is None or not self.reference_model_in_smooth: - return m - return m - self.reference_model - - @property - def _multiplier_pair(self): - return f"alpha_{self.orientation}" - - def f_m(self, m): - """ - Model gradient - """ - dfm_dl = self.mapping * self._delta_m(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) -> sp.csr_matrix: - """ - Derivative of the model gradient - """ - return self.cell_gradient @ self.mapping.deriv(self._delta_m(m)) - - @property - def W(self): - """ - Weighting matrix that takes the volumes, free weights, fixed weights and - length scales of the difference operator (normalized optional). - """ - if getattr(self, "_W", None) is None: - average_cell_2_face = getattr( - self.regularization_mesh, "aveCC2F{}".format(self.orientation) - ) - weights = 1.0 - for values in self._weights.values(): - if values.shape[0] == self.regularization_mesh.nC: - values = average_cell_2_face * values - weights *= values - self._W = utils.sdiag(weights**0.5) - return self._W - - @property - def _cell_distances(self): - """ - Distances between cell centers for the cell center difference. - """ - return getattr(self.regularization_mesh, f"cell_distances_{self.orientation}") - - @property - def orientation(self): - return self._orientation - - -class SmoothnessSecondOrder(SmoothnessFirstOrder): - """ - This base class regularizes on the second - spatial derivative, optionally normalized by the base cell size. - - **Optional Inputs** - - :param discretize.base.BaseMesh mesh: SimPEG mesh - :param int nP: number of parameters - :param IdentityMap mapping: regularization mapping, takes the model from - model space to the space you want to regularize in - :param numpy.ndarray reference_model: reference model - :param numpy.ndarray active_cells: active cell indices for reducing the - size of differential operators in the definition of a regularization mesh - :param numpy.ndarray weights: cell weights - :param bool reference_model_in_smooth: include the reference model in the - smoothness computation? (eg. look at Deriv of m (False) or Deriv of - (m-reference_model) (True)) - :param numpy.ndarray weights: vector of cell weights (applied in all terms) - """ - - def f_m(self, m): - """ - Second model derivative - """ - dfm_dl = self.mapping * self._delta_m(m) - - if self.units is not None and self.units.lower() == "radian": - return self.cell_gradient.T @ ( - utils.mat_utils.coterminal(self.cell_gradient.sign() @ dfm_dl) - / self.length_scales - ) - - dfm_dl2 = self.cell_gradient @ dfm_dl - - return self.cell_gradient.T @ dfm_dl2 - - def f_m_deriv(self, m) -> sp.csr_matrix: - """ - Derivative of the second model residual - """ - return ( - self.cell_gradient.T - @ self.cell_gradient - @ self.mapping.deriv(self._delta_m(m)) - ) - - @property - def W(self): - """ - Weighting matrix - """ - if getattr(self, "_W", None) is None: - weights = np.prod(list(self._weights.values()), axis=0) - self._W = utils.sdiag(weights**0.5) - - return self._W - - @property - def _multiplier_pair(self): - return f"alpha_{self.orientation}{self.orientation}" - - -############################################################################### -# # -# Base Combo Regularization # -# # -############################################################################### - - -class WeightedLeastSquares(ComboObjectiveFunction): - r"""Weighted least squares measure on model smallness and smoothness. - - L2 regularization with both smallness and smoothness (first order - derivative) contributions. - - Parameters - ---------- - mesh : discretize.base.BaseMesh - The mesh on which the model parameters are defined. This is used - for constructing difference operators for the smoothness terms. - active_cells : array_like of bool or int, optional - List of active cell indices, or a `mesh.n_cells` boolean array - describing active cells. - alpha_s : float, optional - Smallness weight - alpha_x, alpha_y, alpha_z : float or None, optional - First order smoothness weights for the respective dimensions. - `None` implies setting these weights using the `length_scale` - parameters. - alpha_xx, alpha_yy, alpha_zz : float, optional - 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 - A mapping to apply to the model before regularization. - reference_model : array_like, optional - reference_model_in_smooth : bool, optional - Whether to include the reference model in the smoothness terms. - weights : None, array_like, or dict or array_like, optional - User defined weights. It is recommended to interact with weights using - the `get_weights`, `set_weights` functionality. - - Notes - ----- - The function defined here approximates: - - .. math:: - \phi_m(\mathbf{m}) = \alpha_s \| W_s (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - + \alpha_x \| W_x \frac{\partial}{\partial x} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - + \alpha_y \| W_y \frac{\partial}{\partial y} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - + \alpha_z \| W_z \frac{\partial}{\partial z} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - - Note if the key word argument `reference_model_in_smooth` is False, then mref is not - included in the smoothness contribution. - - If length scales are used to set the smoothness weights, alphas are respectively set internally using: - >>> alpha_x = (length_scale_x * min(mesh.edge_lengths)) ** 2 - """ - - _model = None - - def __init__( - self, - mesh, - active_cells=None, - alpha_s=1.0, - alpha_x=None, - alpha_y=None, - alpha_z=None, - alpha_xx=0.0, - alpha_yy=0.0, - alpha_zz=0.0, - length_scale_x=None, - length_scale_y=None, - length_scale_z=None, - mapping=None, - reference_model=None, - reference_model_in_smooth=False, - weights=None, - **kwargs, - ): - if isinstance(mesh, BaseMesh): - mesh = RegularizationMesh(mesh) - - if not isinstance(mesh, RegularizationMesh): - TypeError( - f"'regularization_mesh' must be of type {RegularizationMesh} or {BaseMesh}. " - f"Value of type {type(mesh)} provided." - ) - self._regularization_mesh = mesh - if active_cells is not None: - self._regularization_mesh.active_cells = active_cells - - self.alpha_s = alpha_s - if alpha_x is not None: - if length_scale_x is not None: - raise ValueError( - "Attempted to set both alpha_x and length_scale_x at the same time. Please " - "use only one of them" - ) - self.alpha_x = alpha_x - else: - self.length_scale_x = length_scale_x - - if alpha_y is not None: - if length_scale_y is not None: - raise ValueError( - "Attempted to set both alpha_y and length_scale_y at the same time. Please " - "use only one of them" - ) - self.alpha_y = alpha_y - else: - self.length_scale_y = length_scale_y - - if alpha_z is not None: - if length_scale_z is not None: - raise ValueError( - "Attempted to set both alpha_z and length_scale_z at the same time. Please " - "use only one of them" - ) - self.alpha_z = alpha_z - else: - self.length_scale_z = length_scale_z - - # do this to allow child classes to also pass a list of objfcts to this constructor - if "objfcts" not in kwargs: - objfcts = [ - Smallness(mesh=self.regularization_mesh), - SmoothnessFirstOrder(mesh=self.regularization_mesh, orientation="x"), - SmoothnessSecondOrder(mesh=self.regularization_mesh, orientation="x"), - ] - - if mesh.dim > 1: - objfcts.extend( - [ - SmoothnessFirstOrder( - mesh=self.regularization_mesh, orientation="y" - ), - SmoothnessSecondOrder( - mesh=self.regularization_mesh, orientation="y" - ), - ] - ) - - if mesh.dim > 2: - objfcts.extend( - [ - SmoothnessFirstOrder( - mesh=self.regularization_mesh, orientation="z" - ), - SmoothnessSecondOrder( - mesh=self.regularization_mesh, orientation="z" - ), - ] - ) - else: - objfcts = kwargs.pop("objfcts") - super().__init__(objfcts=objfcts, **kwargs) - - for fun in objfcts: - fun.parent = self - - self.mapping = mapping - self.reference_model = reference_model - self.reference_model_in_smooth = reference_model_in_smooth - self.alpha_xx = alpha_xx - self.alpha_yy = alpha_yy - self.alpha_zz = alpha_zz - if weights is not None: - if not isinstance(weights, dict): - weights = {"user_weights": weights} - self.set_weights(**weights) - - def set_weights(self, **weights): - """Update weights in children objective functions""" - for fct in self.objfcts: - fct.set_weights(**weights) - - def remove_weights(self, key): - """removes weights in children objective functions""" - for fct in self.objfcts: - fct.remove_weights(key) - - @property - def cell_weights(self): - # All of the objective functions should have the same weights, - # so just grab the one from smallness here, which should also - # trigger the deprecation warning - return self.objfcts[0].cell_weights - - @cell_weights.setter - def cell_weights(self, value): - warnings.warn( - "cell_weights are deprecated please access weights using the `set_weights`," - " `get_weights`, and `remove_weights` functionality. This will be removed in 0.19.0", - FutureWarning, - ) - self.set_weights(cell_weights=value) - - @property - def alpha_s(self): - """smallness weight""" - return self._alpha_s - - @alpha_s.setter - def alpha_s(self, value): - if value is None: - value = 1.0 - try: - value = float(value) - except (ValueError, TypeError): - raise TypeError(f"alpha_s must be a real number, saw type{type(value)}") - if value < 0: - raise ValueError(f"alpha_s must be non-negative, not {value}") - self._alpha_s = value - - @property - def alpha_x(self): - """weight for the first x-derivative""" - return self._alpha_x - - @alpha_x.setter - def alpha_x(self, value): - try: - value = float(value) - except (ValueError, TypeError): - raise TypeError(f"alpha_x must be a real number, saw type{type(value)}") - if value < 0: - raise ValueError(f"alpha_x must be non-negative, not {value}") - self._alpha_x = value - - @property - def alpha_y(self): - """weight for the first y-derivative""" - return self._alpha_y - - @alpha_y.setter - def alpha_y(self, value): - try: - value = float(value) - except (ValueError, TypeError): - raise TypeError(f"alpha_y must be a real number, saw type{type(value)}") - if value < 0: - raise ValueError(f"alpha_y must be non-negative, not {value}") - self._alpha_y = value - - @property - def alpha_z(self): - """weight for the first z-derivative""" - return self._alpha_z - - @alpha_z.setter - def alpha_z(self, value): - try: - value = float(value) - except (ValueError, TypeError): - raise TypeError(f"alpha_z must be a real number, saw type{type(value)}") - if value < 0: - raise ValueError(f"alpha_z must be non-negative, not {value}") - self._alpha_z = value - - @property - def alpha_xx(self): - """weight for the second x-derivative""" - return self._alpha_xx - - @alpha_xx.setter - def alpha_xx(self, value): - if value is None: - value = (self.length_scale_x * self.regularization_mesh.base_length) ** 4.0 - try: - value = float(value) - except (ValueError, TypeError): - raise TypeError(f"alpha_xx must be a real number, saw type{type(value)}") - if value < 0: - raise ValueError(f"alpha_xx must be non-negative, not {value}") - self._alpha_xx = value - - @property - def alpha_yy(self): - """weight for the second y-derivative""" - return self._alpha_yy - - @alpha_yy.setter - def alpha_yy(self, value): - if value is None: - value = (self.length_scale_y * self.regularization_mesh.base_length) ** 4.0 - try: - value = float(value) - except (ValueError, TypeError): - raise TypeError(f"alpha_yy must be a real number, saw type{type(value)}") - if value < 0: - raise ValueError(f"alpha_yy must be non-negative, not {value}") - self._alpha_yy = value - - @property - def alpha_zz(self): - """weight for the second z-derivative""" - return self._alpha_zz - - @alpha_zz.setter - def alpha_zz(self, value): - if value is None: - value = (self.length_scale_z * self.regularization_mesh.base_length) ** 4.0 - try: - value = float(value) - except (ValueError, TypeError): - raise TypeError(f"alpha_zz must be a real number, saw type{type(value)}") - if value < 0: - raise ValueError(f"alpha_zz must be non-negative, not {value}") - self._alpha_zz = value - - @property - def length_scale_x(self): - """Constant multiplier of the base length scale on model gradients along x.""" - return np.sqrt(self.alpha_x) / self.regularization_mesh.base_length - - @length_scale_x.setter - def length_scale_x(self, value: float): - if value is None: - value = 1.0 - try: - value = float(value) - except (TypeError, ValueError): - raise TypeError( - f"length_scale_x must be a real number, saw type{type(value)}" - ) - self.alpha_x = (value * self.regularization_mesh.base_length) ** 2 - - @property - def length_scale_y(self): - """Constant multiplier of the base length scale on model gradients along y.""" - return np.sqrt(self.alpha_y) / self.regularization_mesh.base_length - - @length_scale_y.setter - def length_scale_y(self, value: float): - if value is None: - value = 1.0 - try: - value = float(value) - except (TypeError, ValueError): - raise TypeError( - f"length_scale_y must be a real number, saw type{type(value)}" - ) - self.alpha_y = (value * self.regularization_mesh.base_length) ** 2 - - @property - def length_scale_z(self): - """Constant multiplier of the base length scale on model gradients along z.""" - return np.sqrt(self.alpha_z) / self.regularization_mesh.base_length - - @length_scale_z.setter - def length_scale_z(self, value: float): - if value is None: - value = 1.0 - try: - value = float(value) - except (TypeError, ValueError): - raise TypeError( - f"length_scale_z must be a real number, saw type{type(value)}" - ) - self.alpha_z = (value * self.regularization_mesh.base_length) ** 2 - - @property - def reference_model_in_smooth(self) -> bool: - """ - Use the reference model in the model gradient penalties. - """ - return self._reference_model_in_smooth - - @reference_model_in_smooth.setter - def reference_model_in_smooth(self, value: bool): - if not isinstance(value, bool): - raise TypeError( - "'reference_model_in_smooth must be of type 'bool'. " - f"Value of type {type(value)} provided." - ) - self._reference_model_in_smooth = value - for fct in self.objfcts: - if getattr(fct, "reference_model_in_smooth", None) is not None: - fct.reference_model_in_smooth = value - - # Other properties and methods - @property - def nP(self): - """ - number of model parameters - """ - if getattr(self, "mapping", None) is not None and self.mapping.nP != "*": - return self.mapping.nP - elif ( - getattr(self, "_regularization_mesh", None) is not None - and self.regularization_mesh.nC != "*" - ): - return self.regularization_mesh.nC - else: - return "*" - - @property - def _nC_residual(self): - """ - Shape of the residual - """ - - nC = getattr(self.regularization_mesh, "nC", None) - mapping = getattr(self, "_mapping", None) - - if mapping is not None and mapping.shape[1] != "*": - return self.mapping.shape[1] - elif nC != "*" and nC is not None: - return self.regularization_mesh.nC - else: - return self.nP - - def _delta_m(self, m): - if self.reference_model is None: - return m - return m - self.reference_model - - @property - def multipliers(self): - """ - Factors that multiply the objective functions that are summed together - to build to composite regularization - """ - return [getattr(self, objfct._multiplier_pair) for objfct in self.objfcts] - - @property - def active_cells(self) -> np.ndarray: - """Indices of active cells in the mesh""" - return self.regularization_mesh.active_cells - - @active_cells.setter - def active_cells(self, values: np.ndarray): - self.regularization_mesh.active_cells = values - active_cells = self.regularization_mesh.active_cells - # notify the objtecive functions that the active_cells changed - for objfct in self.objfcts: - objfct.active_cells = active_cells - - indActive = deprecate_property( - active_cells, - "indActive", - "active_cells", - "0.19.0", - error=False, - future_warn=True, - ) - - @property - def reference_model(self) -> np.ndarray: - """Reference physical property model""" - return self._reference_model - - @reference_model.setter - def reference_model(self, values: np.ndarray | float): - if isinstance(values, float): - values = np.ones(self._nC_residual) * values - - for fct in self.objfcts: - fct.reference_model = values - - self._reference_model = values - - mref = deprecate_property( - reference_model, - "mref", - "reference_model", - "0.19.0", - future_warn=True, - error=False, - ) - - @property - def model(self) -> np.ndarray: - """Physical property model""" - return self._model - - @model.setter - def model(self, values: np.ndarray | float): - if isinstance(values, float): - values = np.ones(self._nC_residual) * values - - for fct in self.objfcts: - fct.model = values - - self._model = values - - @property - def units(self) -> str: - """Specify the model units. Special care given to 'radian' values""" - return self._units - - @units.setter - def units(self, units: str | None): - if units is not None and not isinstance(units, str): - raise TypeError( - f"'units' must be None or type str. " - f"Value of type {type(units)} provided." - ) - for fct in self.objfcts: - fct.units = units - self._units = units - - @property - def regularization_mesh(self) -> RegularizationMesh: - """Regularization mesh""" - return self._regularization_mesh - - @property - def mapping(self) -> maps.IdentityMap: - """Mapping applied to the model values""" - return self._mapping - - @mapping.setter - def mapping(self, mapping: maps.IdentityMap): - if mapping is None: - mapping = maps.IdentityMap(nP=self._nC_residual) - - if not isinstance(mapping, maps.IdentityMap): - raise TypeError( - f"'mapping' must be of type {maps.IdentityMap}. " - f"Value of type {type(mapping)} provided." - ) - self._mapping = mapping - - for fct in self.objfcts: - fct.mapping = mapping - - -############################################################################### -# # -# Base Coupling Regularization # -# # -############################################################################### -class BaseSimilarityMeasure(BaseRegularization): - - """ - Base class for the similarity term in joint inversions. Inherit this for building - your own similarity term. The BaseSimilarityMeasure assumes two different - geophysical models through one similarity term. However, if you wish - to combine more than two models, e.g., 3 models, - you may want to add a total of three coupling terms: - - e.g., lambda1*(m1, m2) + lambda2*(m1, m3) + lambda3*(m2, m3) - - where, lambdas are weights for coupling terms. m1, m2 and m3 indicate - three different models. - """ - - def __init__(self, mesh, wire_map, **kwargs): - super().__init__(mesh, **kwargs) - self.wire_map = wire_map - - @property - def wire_map(self): - return self._wire_map - - @wire_map.setter - def wire_map(self, wires: list[maps.IdentityMap]): - try: - m1, m2 = wires # Assume a map has been passed for each model. - except ValueError: - ValueError("Wire map must have two model mappings") - - if m1.shape[0] != m2.shape[0]: - raise ValueError( - f"All models must be the same size! Got {m1.shape[0]} and {m2.shape[0]}" - ) - self._wire_map = wires - - @property - def wire_map_deriv(self): - if getattr(self, "_wire_map_deriv", None) is None: - self._wire_map_deriv = sp.vstack([wire.P for wire in self.wire_map]) - return self._wire_map_deriv - - @property - def nP(self): - """ - number of model parameters - """ - return self.wire_map[0].nP - - def deriv(self, model): - """ - First derivative of the coupling term with respect to individual models. - Returns an array of dimensions [k*M,1], - k: number of models we are inverting for. - M: number of cells in each model. - - """ - raise NotImplementedError( - "The method deriv has not been implemented for {}".format( - self.__class__.__name__ - ) - ) - - def deriv2(self, model, v=None): - """ - Second derivative of the coupling term with respect to individual models. - Returns either an array of dimensions [k*M,1] (v is not None), or - sparse matrix of dimensions [k*M, k*M] (v is None). - k: number of models we are inverting for. - M: number of cells in each model. - - """ - raise NotImplementedError( - "The method _deriv2 has not been implemented for {}".format( - self.__class__.__name__ - ) - ) - - @property - def _nC_residual(self): - """ - Shape of the residual - """ - return self.wire_map[0].nP - - def __call__(self, model): - """Returns the computed value of the coupling term.""" - raise NotImplementedError( - "The method __call__ has not been implemented for {}".format( - self.__class__.__name__ - ) - ) diff --git a/SimPEG/regularization/correspondence.py b/SimPEG/regularization/correspondence.py deleted file mode 100644 index e94c0efe82..0000000000 --- a/SimPEG/regularization/correspondence.py +++ /dev/null @@ -1,122 +0,0 @@ -import numpy as np -import scipy.sparse as sp -from ..utils import validate_ndarray_with_shape - -from .. import utils -from .base import BaseSimilarityMeasure - - -class LinearCorrespondence(BaseSimilarityMeasure): - r""" - The petrophysical linear constraint for joint inversions. - - ..math:: - - \phi_c({\mathbf m}_{\mathbf1},{\mathbf m}_{\mathbf2}) - = \lambda\sum_{i=1}^M (k_1*m_1 + k_2*m_2 + k_3) - - Assuming that we are working with two models only. - - """ - - def __init__(self, mesh, wire_map, coefficients=None, **kwargs): - super().__init__(mesh, wire_map, **kwargs) - if coefficients is None: - coefficients = np.r_[1.0, -1.0, 0.0] - self.coefficients = coefficients - - @property - def coefficients(self): - """coefficients for the linear relationship between parameters. - - Returns - ------- - (3) numpy.ndarray of float - """ - return self._coefficients - - @coefficients.setter - def coefficients(self, value): - self._coefficients = validate_ndarray_with_shape( - "coefficients", value, shape=(3,) - ) - - def relation(self, model): - """ - Computes the values of petrophysical linear relationship between two different - geophysical models. - - The linear relationship is defined as: - - f(m1, m2) = k1*m1 + k2*m2 + k3 - - :param numpy.ndarray model: stacked array of individual models - np.c_[model1, model2,...] - - :rtype: float - :return: linearly related petrophysical values of two different models, - dimension: M by 1, :M number of model parameters. - - """ - m1, m2 = self.wire_map * model - k1, k2, k3 = self.coefficients - - return k1 * m1 + k2 * m2 + k3 - - def __call__(self, model): - """ - Computes the sum of values of petrophysical linear relationship - between two different geophysical models. - - :param numpy.ndarray model: stacked array of individual models - np.c_[model1, model2,...] - - :rtype: float - :return: a scalar value. - """ - - result = self.relation(model) - return 0.5 * result.T @ result - - def deriv(self, model): - """Computes the Jacobian of the coupling term. - - :param list of numpy.ndarray ind_models: [model1, model2,...] - - :rtype: numpy.ndarray - :return: result: gradient of the coupling term with respect to model1, model2, - :dimension 2M by 1, :M number of model parameters. - """ - k1, k2, k3 = self.coefficients - r = self.relation(model) - dc_dm1 = k1 * r - dc_dm2 = k2 * r - - result = np.r_[dc_dm1, dc_dm2] - - return result - - def deriv2(self, model, v=None): - """Computes the Hessian of the linear coupling term. - - :param list of numpy.ndarray ind_models: [model1, model2, ...] - :param numpy.ndarray v: vector to be multiplied by Hessian - :rtype: scipy.sparse.csr_matrix if v is None - numpy.ndarray if v is not None - :return Hessian matrix: | h11, h21 | :dimension 2M*2M. - | | - | h12, h22 | - """ - - k1, k2, k3 = self.coefficients - if v is not None: - v1, v2 = self.wire_map * v - p1 = k1**2 * v1 + k2 * k1 * v2 - p2 = k2 * k1 * v1 + k2**2 * v2 - return np.r_[p1, p2] - else: - n = self.regularization_mesh.nC - A = utils.sdiag(np.ones(n) * (k1**2)) - B = utils.sdiag(np.ones(n) * (k2**2)) - C = utils.sdiag(np.ones(n) * (k1 * k2)) - return sp.bmat([[A, C], [C, B]], format="csr") diff --git a/SimPEG/regularization/cross_gradient.py b/SimPEG/regularization/cross_gradient.py deleted file mode 100644 index af435f6346..0000000000 --- a/SimPEG/regularization/cross_gradient.py +++ /dev/null @@ -1,298 +0,0 @@ -import numpy as np -import scipy.sparse as sp - -from .base import BaseSimilarityMeasure -from ..utils import validate_type -from ..utils.mat_utils import coterminal - -############################################################################### -# # -# Cross-Gradient # -# # -############################################################################### - - -class CrossGradient(BaseSimilarityMeasure): - r""" - The cross-gradient constraint for joint inversions. - - ..math:: - \phi_c(\mathbf{m_1},\mathbf{m_2}) = \lambda \sum_{i=1}^{M} \| - \nabla \mathbf{m_1}_i \times \nabla \mathbf{m_2}_i \|^2 - - All methods assume that we are working with two models only. - - """ - - def __init__(self, mesh, wire_map, approx_hessian=True, normalize=False, **kwargs): - super().__init__(mesh, wire_map=wire_map, **kwargs) - self.approx_hessian = approx_hessian - self._units = ["metric", "metric"] - self.normalize = normalize - regmesh = self.regularization_mesh - - if regmesh.mesh.dim not in (2, 3): - raise ValueError("Cross-Gradient is only defined for 2D or 3D") - self._G = regmesh.cell_gradient - self._Av = sp.diags(np.sqrt(regmesh.vol)) * regmesh.average_face_to_cell - - @property - def approx_hessian(self): - """whether to use the semi-positive definate approximation for the hessian. - Returns - ------- - bool - """ - return self._approx_hessian - - @approx_hessian.setter - def approx_hessian(self, value): - self._approx_hessian = validate_type("approx_hessian", value, bool) - - def _model_gradients(self, models): - """ - Compute gradient on faces - """ - gradients = [] - - for unit, wire in zip(self.units, self.wire_map): - model = wire * models - if unit == "radian": - gradient = [] - components = "xyz" if self.regularization_mesh.dim == 3 else "xy" - for comp in components: - distances = getattr( - self.regularization_mesh, f"cell_distances_{comp}" - ) - cell_grad = getattr( - self.regularization_mesh, f"cell_gradient_{comp}" - ) - gradient.append( - coterminal(cell_grad * model * distances) / distances - ) - - gradient = np.hstack(gradient) / np.pi - else: - gradient = self._G @ model - - gradients.append(gradient) - - return gradients - - def _calculate_gradient(self, model, normalized=False, rtol=1e-6): - """ - Calculate the spatial gradients of the model using central difference. - - Concatenates gradient components into a single array. - [[x_grad1, y_grad1, z_grad1], - [x_grad2, y_grad2, z_grad2], - [x_grad3, y_grad3, z_grad3],...] - - :param numpy.ndarray model: model - - :rtype: numpy.ndarray - :return: gradient_vector: array where each row represents a model cell, - and each column represents a component of the gradient. - - """ - regmesh = self.regularization_mesh - Avs = [regmesh.aveFx2CC, regmesh.aveFy2CC] - if regmesh.dim == 3: - Avs.append(regmesh.aveFz2CC) - Av = sp.block_diag(Avs) - - # Compute the gradients and concatenate components. - grad_models = self._model_gradients(model) - - gradients = [] - for gradient in grad_models: - gradient = (Av @ (gradient)).reshape((-1, regmesh.dim), order="F") - - if normalized: - norms = np.linalg.norm(gradient, axis=-1) - ind = norms <= norms.max() * rtol - norms[ind] = 1.0 - gradient /= norms[:, None] - gradient[ind] = 0.0 - # set gradient to 0 if amplitude of gradient is extremely small - gradients.append(gradient) - - return gradients - - def calculate_cross_gradient(self, model, normalized=False, rtol=1e-6): - """ - Calculates the cross-gradients of the models at each cell center. - - Parameters - ---------- - model : numpy.ndarray - The input model, which will be automatically separated into the two - parameters internally - normalized : bool, optional - Whether to normalize the gradient - rtol : float, optional - relative cuttoff for small gradients in the normalization - - Returns - ------- - cross_grad : numpy.ndarray - The norm of the cross gradient vector in each active cell. - """ - # Compute the gradients and concatenate components. - grad_m1, grad_m2 = self._calculate_gradient( - model, normalized=normalized, rtol=rtol - ) - - # for each model cell, compute the cross product of the gradient vectors. - cross_prod = np.cross(grad_m1, grad_m2) - if self.regularization_mesh.dim == 3: - cross_prod = np.linalg.norm(cross_prod, axis=-1) - - return cross_prod - - def __call__(self, model): - r""" - Computes the sum of all cross-gradient values at all cell centers. - - :param numpy.ndarray model: stacked array of individual models - np.c_[model1, model2,...] - :param bool normalized: returns value of normalized cross-gradient if True - - :rtype: float - :returns: the computed value of the cross-gradient term. - - - ..math:: - - \phi_c(\mathbf{m_1},\mathbf{m_2}) - = \lambda \sum_{i=1}^{M} \|\nabla \mathbf{m_1}_i \times \nabla \mathbf{m_2}_i \|^2 - = \sum_{i=1}^{M} \|\nabla \mathbf{m_1}_i\|^2 \ast \|\nabla \mathbf{m_2}_i\|^2 - - (\nabla \mathbf{m_1}_i \cdot \nabla \mathbf{m_2}_i )^2 - = \|\phi_{cx}\|^2 + \|\phi_{cy}\|^2 + \|\phi_{cz}\|^2 - - (optional strategy, not used in this script) - - """ - Av = self._Av - G = self._G - g_m1, g_m2 = self._model_gradients(model) - - return 0.5 * np.sum( - np.sum((Av @ g_m1**2) * (Av @ g_m2**2) - (Av @ (g_m1 * g_m2)) ** 2) - ) - - def deriv(self, model): - """ - Computes the Jacobian of the cross-gradient. - - :param list of numpy.ndarray ind_models: [model1, model2,...] - - :rtype: numpy.ndarray - :return: result: gradient of the cross-gradient with respect to model1, model2 - - """ - Av = self._Av - G = self._G - g_m1, g_m2 = self._model_gradients(model) - - deriv = np.r_[ - (((Av @ g_m2**2) @ Av) * g_m1) @ G - - (((Av @ (g_m1 * g_m2)) @ Av) * g_m2) @ G, - (((Av @ g_m1**2) @ Av) * g_m2) @ G - - (((Av @ (g_m1 * g_m2)) @ Av) * g_m1) @ G, - ] - - return self.wire_map_deriv.T * deriv - - def deriv2(self, model, v=None): - r"""Hessian of the regularization function evaluated for the model provided. - - Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), - this method evalutate and returns the second derivative (Hessian) with respect to the model parameters: - For a model :math:`\mathbf{m}` consisting of two physical properties such that: - - .. math:: - \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} - - The Hessian has the form: - - .. math:: - \frac{\partial^2 \phi}{\partial \mathbf{m}^2} = - \begin{bmatrix} - \dfrac{\partial^2 \phi}{\partial \mathbf{m_1}^2} & - \dfrac{\partial^2 \phi}{\partial \mathbf{m_1} \partial \mathbf{m_2}} \\ - \dfrac{\partial^2 \phi}{\partial \mathbf{m_2} \partial \mathbf{m_1}} & - \dfrac{\partial^2 \phi}{\partial \mathbf{m_2}^2} - \end{bmatrix} - - When a vector :math:`(\mathbf{v})` is supplied, the method returns the Hessian - times the vector: - - .. math:: - \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \, \mathbf{v} - - Parameters - ---------- - model : (n_param, ) numpy.ndarray - The model; a vector array containing all physical properties. - v : None, (n_param, ) numpy.ndarray (optional) - A numpy array to model the Hessian by. - - Returns - ------- - (n_param, n_param) scipy.sparse.csr_matrix | (n_param, ) numpy.ndarray - If the input argument *v* is ``None``, the Hessian - for the models provided is returned. If *v* is not ``None``, - the Hessian multiplied by the vector provided is returned. - """ - Av = self._Av - G = self._G - - g_m1, g_m2 = self._model_gradients(model) - - d11_mid = Av.T @ (Av @ g_m2**2) - d12_mid = -(Av.T @ (Av @ (g_m1 * g_m2))) - d22_mid = Av.T @ (Av @ g_m1**2) - - if v is None: - D11_mid = sp.diags(d11_mid) - D12_mid = sp.diags(d12_mid) - D22_mid = sp.diags(d22_mid) - if not self.approx_hessian: - D11_mid = D11_mid - sp.diags(g_m2) @ Av.T @ Av @ sp.diags(g_m2) - D12_mid = ( - D12_mid - + 2 * sp.diags(g_m1) @ Av.T @ Av @ sp.diags(g_m2) - - sp.diags(g_m2) @ Av.T @ Av @ sp.diags(g_m1) - ) - D22_mid = D22_mid - sp.diags(g_m1) @ Av.T @ Av @ sp.diags(g_m1) - D11 = G.T @ D11_mid @ G - D12 = G.T @ D12_mid @ G - D22 = G.T @ D22_mid @ G - - return ( - self.wire_map_deriv.T - @ sp.bmat([[D11, D12], [D12.T, D22]], format="csr") - * self.wire_map_deriv - ) # factor of 2 from derviative of | grad m1 x grad m2 | ^2 - - else: - v1, v2 = (wire * v for wire in self.wire_map) - - Gv1 = G @ v1 - Gv2 = G @ v2 - p1 = G.T @ (d11_mid * Gv1 + d12_mid * Gv2) - p2 = G.T @ (d12_mid * Gv1 + d22_mid * Gv2) - if not self.approx_hessian: - p1 += G.T @ ( - -g_m2 * (Av.T @ (Av @ (g_m2 * Gv1))) # d11*v1 full addition - + 2 * g_m1 * (Av.T @ (Av @ (g_m2 * Gv2))) # d12*v2 full addition - - g_m2 * (Av.T @ (Av @ (g_m1 * Gv2))) # d12*v2 continued - ) - - p2 += G.T @ ( - -g_m1 * (Av.T @ (Av @ (g_m1 * Gv2))) # d22*v2 full addition - + 2 * g_m2 * (Av.T @ (Av @ (g_m1 * Gv1))) # d12.T*v1 full addition - - g_m1 * (Av.T @ (Av @ (g_m2 * Gv1))) # d12.T*v1 fcontinued - ) - return self.wire_map_deriv.T * np.r_[p1, p2] diff --git a/SimPEG/regularization/jtv.py b/SimPEG/regularization/jtv.py deleted file mode 100644 index 8766be65eb..0000000000 --- a/SimPEG/regularization/jtv.py +++ /dev/null @@ -1,167 +0,0 @@ -import numpy as np -import scipy.sparse as sp - -from .base import BaseSimilarityMeasure - - -############################################################################### -# # -# Joint Total Variation # -# # -############################################################################### - - -class JointTotalVariation(BaseSimilarityMeasure): - r""" - The joint total variation constraint for joint inversions. - - ..math :: - \phi_sim(\mathbf{m_1},\mathbf{m_2}) = \lambda \sum_{i=1}^{M} V_i\sqrt{| - \nabla \mathbf{m_1}_i|^2 +|\nabla \mathbf{m_2}_i|^2} - """ - - def __init__(self, mesh, wire_map, eps=1e-8, **kwargs): - super().__init__(mesh, wire_map=wire_map, **kwargs) - self.set_weights(volume=self.regularization_mesh.vol) - self.eps = eps - - self._G = self.regularization_mesh.cell_gradient - - @property - def W(self): - """ - Weighting matrix - """ - if getattr(self, "_W", None) is None: - weights = np.prod(list(self._weights.values()), axis=0) - self._W = ( - sp.diags(weights**2) * self.regularization_mesh.average_face_to_cell - ) - return self._W - - @property - def wire_map(self): - return self._wire_map - - @wire_map.setter - def wire_map(self, wires): - n = self.regularization_mesh.nC - maps = wires.maps - for _, mapping in maps: - map_n = mapping.shape[0] - if n != map_n: - raise ValueError( - f"All mapping outputs must match the number of cells in " - f"the regularization mesh! Got {n} and {map_n}" - ) - self._wire_map = wires - - def __call__(self, model): - """ - Computes the sum of all joint total variation values. - - Parameters - ---------- - model : numpy.ndarray - stacked array of individual models np.r_[model1, model2,...] - - Returns - ------- - float - The computed value of the joint total variation term. - """ - W = self.W - G = self._G - v2 = self.regularization_mesh.vol**2 - g2 = 0 - for m in self.wire_map * model: - g_m = G @ m - g2 += g_m**2 - W_g = W @ g2 - sq = np.sqrt(W_g + self.eps * v2) - return np.sum(sq) - - def deriv(self, model): - """ - Computes the derivative of the joint total variation. - - Parameters - ---------- - model : numpy.ndarray - stacked array of individual models np.r_[model1, model2,...] - - Returns - ------- - numpy.ndarray - The gradient of joint total variation with respect to the model - """ - W = self.W - G = self._G - g2 = 0 - gs = [] - v2 = self.regularization_mesh.vol**2 - for m in self.wire_map * model: - g_mi = G @ m - g2 += g_mi**2 - gs.append(g_mi) - W_g = W @ g2 - sq = np.sqrt(W_g + self.eps * v2) - mid = W.T @ (1 / sq) - ps = [] - for g_mi in gs: - ps.append(G.T @ (mid * g_mi)) - return np.concatenate(ps) - - def deriv2(self, model, v=None): - """ - Computes the Hessian of the joint total variation. - - Parameters - ---------- - model : numpy.ndarray - Stacked array of individual models - v : numpy.ndarray, optional - An array to multiply the Hessian by. - - Returns - ------- - numpy.ndarray or scipy.sparse.csr_matrix - The Hessian of joint total variation with respect to the model times a - vector or the full Hessian if `v` is `None`. - """ - W = self.W - G = self._G - v2 = self.regularization_mesh.vol**2 - gs = [] - g2 = 0 - for m in self.wire_map * model: - g_m = G @ m - g2 += g_m**2 - gs.append(g_m) - - W_g = W @ g2 - sq = np.sqrt(W_g + self.eps * v2) - mid = W.T @ (1 / sq) - - if v is not None: - g_vs = [] - tmp_sum = 0 - for vi, g_i in zip(self.wire_map * v, gs): - g_vi = G @ vi - tmp_sum += W.T @ ((W @ (g_i * g_vi)) / sq**3) - g_vs.append(g_vi) - ps = [] - for g_vi, g_i in zip(g_vs, gs): - ps.append(G.T @ (mid * g_vi - g_i * tmp_sum)) - return np.concatenate(ps) - else: - Pieces = [] - Diags = [] - SQ = sp.diags(sq**-1.5) - diag_block = G.T @ sp.diags(mid) @ G - for g_mi in gs: - Pieces.append(SQ @ W @ sp.diags(g_mi) @ G) - Diags.append(diag_block) - Row = sp.hstack(Pieces, format="csr") - Diag = sp.block_diag(Diags, format="csr") - return Diag - Row.T @ Row diff --git a/SimPEG/regularization/pgi.py b/SimPEG/regularization/pgi.py deleted file mode 100644 index fc38cb0e3d..0000000000 --- a/SimPEG/regularization/pgi.py +++ /dev/null @@ -1,848 +0,0 @@ -from __future__ import annotations - -import copy -import warnings - -import numpy as np -import scipy.sparse as sp - -from ..maps import IdentityMap, Wires -from ..objective_function import ComboObjectiveFunction -from ..utils import ( - Identity, - deprecate_property, - mkvc, - sdiag, - timeIt, - validate_float, - validate_ndarray_with_shape, -) -from .base import RegularizationMesh, Smallness, WeightedLeastSquares - -############################################################################### -# # -# Petrophysically And Geologically Guided Regularization # -# # -############################################################################### - - -# Simple Petrophysical Regularization -##################################### - - -class PGIsmallness(Smallness): - """ - Smallness term for the petrophysically constrained regularization (PGI) - with cell_weights similar to the regularization.tikhonov.SimpleSmall class. - - PARAMETERS - ---------- - :param SimPEG.utils.WeightedGaussianMixture gmm: GMM to use - :param SimPEG.maps.Wires wiresmap: wires mapping to the various physical properties - :param list maplist: list of SimPEG.maps for each physical property. - :param discretize.BaseMesh mesh: tensor, QuadTree or Octree mesh - :param boolean approx_gradient: use the L2-approximation of the gradient, default is True - :param boolean approx_eval: use the L2-approximation evaluation of the smallness term - """ - - _multiplier_pair = "alpha_pgi" - _maplist = None - _wiresmap = None - - def __init__( - self, - gmmref, - gmm=None, - wiresmap=None, - maplist=None, - mesh=None, - approx_gradient=True, # L2 approximate of the gradients - approx_eval=True, # L2 approximate of the value - approx_hessian=True, - non_linear_relationships=False, - **kwargs, - ): - self.gmmref = copy.deepcopy(gmmref) - self.gmmref.order_clusters_GM_weight() - self.approx_gradient = approx_gradient - self.approx_eval = approx_eval - self.approx_hessian = approx_hessian - self.non_linear_relationships = non_linear_relationships - self._gmm = copy.deepcopy(gmm) - self.wiresmap = wiresmap - self.maplist = maplist - - if "mapping" in kwargs: - warnings.warn( - f"Property 'mapping' of class {type(self)} cannot be set. Defaults to IdentityMap." - ) - kwargs.pop("mapping") - - weights = kwargs.pop("weights", None) - - super().__init__(mesh=mesh, mapping=IdentityMap(nP=self.shape[0]), **kwargs) - - # Save repetitive computations (see withmapping implementation) - self._r_first_deriv = None - self._r_second_deriv = None - - if weights is not None: - if isinstance(weights, (np.ndarray, list)): - weights = {"user_weights": np.r_[weights].flatten()} - self.set_weights(**weights) - - def set_weights(self, **weights): - for key, values in weights.items(): - values = validate_ndarray_with_shape("weights", values, dtype=float) - - if values.shape[0] == self.regularization_mesh.nC: - values = np.tile(values, len(self.wiresmap.maps)) - - values = validate_ndarray_with_shape( - "weights", values, shape=(self._nC_residual,), dtype=float - ) - - self._weights[key] = values - - self._W = None - - @property - def gmm(self): - if getattr(self, "_gmm", None) is None: - self._gmm = copy.deepcopy(self.gmmref) - return self._gmm - - @gmm.setter - def gmm(self, gm): - if gm is not None: - self._gmm = copy.deepcopy(gm) - - @property - def shape(self): - """""" - return (self.wiresmap.nP,) - - def membership(self, m): - modellist = self.wiresmap * m - model = np.c_[[a * b for a, b in zip(self.maplist, modellist)]].T - return self.gmm.predict(model) - - def compute_quasi_geology_model(self): - # used once mref is built - mreflist = self.wiresmap * self.reference_model - mrefarray = np.c_[[a for a in mreflist]].T - return np.c_[ - [((mrefarray - mean) ** 2).sum(axis=1) for mean in self.gmm.means_] - ].argmin(axis=0) - - @property - def non_linear_relationships(self): - """Flag for non-linear GMM relationships""" - return self._non_linear_relationships - - @non_linear_relationships.setter - def non_linear_relationships(self, value: bool): - if not isinstance(value, bool): - raise ValueError( - "Input value for 'non_linear_relationships' must be of type 'bool'. " - f"Provided {value} of type {type(value)}." - ) - self._non_linear_relationships = value - - @property - def wiresmap(self): - if getattr(self, "_wiresmap", None) is None: - self._wiresmap = Wires(("m", self.regularization_mesh.nC)) - return self._wiresmap - - @wiresmap.setter - def wiresmap(self, wires): - if self._maplist is not None and len(wires.maps) != len(self._maplist): - raise Exception( - f"Provided 'wiresmap' should have wires the len of 'maplist' {len(self._maplist)}." - ) - - if not isinstance(wires, Wires): - raise ValueError(f"Attribure 'wiresmap' should be of type {Wires} or None.") - - self._wiresmap = wires - - @property - def maplist(self): - if getattr(self, "_maplist", None) is None: - self._maplist = [ - IdentityMap(nP=self.regularization_mesh.nC) - for maps in self.wiresmap.maps - ] - return self._maplist - - @maplist.setter - def maplist(self, maplist): - if self._wiresmap is not None and len(maplist) != len(self._wiresmap.maps): - raise Exception( - f"Provided 'maplist' should be a list of maps equal to the 'wiresmap' list of len {len(self._maplist)}." - ) - - if not isinstance(maplist, (list, type(None))): - raise ValueError( - f"Attribute 'maplist' should be a list of maps or None.{type(maplist)} was given." - ) - - if isinstance(maplist, list) and not all( - isinstance(m, IdentityMap) for m in maplist - ): - raise ValueError( - f"Attribute 'maplist' should be a list of maps or None.{type(maplist)} was given." - ) - - self._maplist = maplist - - @timeIt - def __call__(self, m, external_weights=True): - if external_weights: - W = self.W - else: - W = Identity() - - if getattr(self, "reference_model", None) is None: - self.reference_model = mkvc(self.gmm.means_[self.membership(m)]) - - if self.approx_eval: - membership = self.compute_quasi_geology_model() - dm = self.wiresmap * (m) - dmref = self.wiresmap * (self.reference_model) - dmm = np.c_[[a * b for a, b in zip(self.maplist, dm)]].T - if self.non_linear_relationships: - dmm = np.r_[ - [ - self.gmm.cluster_mapping[membership[i]] * dmm[i].reshape(-1, 2) - for i in range(dmm.shape[0]) - ] - ].reshape(-1, 2) - - dmmref = np.c_[[a for a in dmref]].T - dmr = dmm - dmmref - 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))] - ] - 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)) - ] - ] - else: - r1 = np.r_[ - [ - np.dot(self.gmm.precisions_[membership[i]], np.r_[r0[i]]) - for i in range(len(r0)) - ] - ] - - return 0.5 * mkvc(r0).dot(mkvc(r1)) - - else: - modellist = self.wiresmap * m - model = np.c_[[a * b for a, b in zip(self.maplist, modellist)]].T - - if self.non_linear_relationships: - score = self.gmm.score_samples(model) - score_vec = mkvc(np.r_[[score for maps in self.wiresmap.maps]]) - return -np.sum((W.T * W) * score_vec) / len(self.wiresmap.maps) - - else: - if external_weights and getattr(self.W, "diagonal", None) is not None: - sensW = np.c_[ - [wire[1] * self.W.diagonal() for wire in self.wiresmap.maps] - ].T - else: - sensW = np.ones_like(model) - - score = self.gmm.score_samples_with_sensW(model, sensW) - # score_vec = mkvc(np.r_[[score for maps in self.wiresmap.maps]]) - # return -np.sum((W.T * W) * score_vec) / len(self.wiresmap.maps) - return -np.sum(score) - - @timeIt - def deriv(self, m): - if getattr(self, "reference_model", None) is None: - self.reference_model = mkvc(self.gmm.means_[self.membership(m)]) - - membership = self.compute_quasi_geology_model() - modellist = self.wiresmap * m - dmmodel = np.c_[[a * b for a, b in zip(self.maplist, modellist)]].T - mreflist = self.wiresmap * self.reference_model - mD = [a.deriv(b) for a, b in zip(self.maplist, modellist)] - mD = sp.block_diag(mD) - - if self.non_linear_relationships: - dmmodel = np.r_[ - [ - self.gmm.cluster_mapping[membership[i]] * dmmodel[i].reshape(-1, 2) - for i in range(dmmodel.shape[0]) - ] - ].reshape(-1, 2) - - if self.approx_gradient: - dmmref = np.c_[[a for a in mreflist]].T - dm = dmmodel - dmmref - r0 = (self.W * (mkvc(dm))).reshape(dm.shape, order="F") - - if self.gmm.covariance_type == "tied": - 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))]] - ) - 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)) - ] - ] - ) - else: - if self.non_linear_relationships: - r = mkvc( - np.r_[ - [ - mkvc( - self.gmm.cluster_mapping[membership[i]].deriv( - dmmodel[i], - v=np.dot( - self.gmm.precisions_[membership[i]], r0[i] - ), - ) - ) - for i in range(dmmodel.shape[0]) - ] - ] - ) - - 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)) - ] - ] - ) - return mkvc(mD.T * (self.W.T * r)) - - else: - if self.non_linear_relationships: - raise Exception("Not implemented") - - modellist = self.wiresmap * m - model = np.c_[[a * b for a, b in zip(self.maplist, modellist)]].T - - if getattr(self.W, "diagonal", None) is not None: - sensW = np.c_[ - [wire[1] * self.W.diagonal() for wire in self.wiresmap.maps] - ].T - else: - sensW = np.ones_like(model) - - score = self.gmm.score_samples_with_sensW(model, sensW) - # score = self.gmm.score_samples(model) - score_vec = np.hstack([score for maps in self.wiresmap.maps]) - - logP = np.zeros((len(model), self.gmm.n_components)) - W = [] - logP = self.gmm._estimate_log_gaussian_prob_with_sensW( - model, - sensW, - self.gmm.means_, - self.gmm.precisions_cholesky_, - self.gmm.covariance_type, - ) - for k in range(self.gmm.n_components): - if self.gmm.covariance_type == "tied": - # logP[:, k] = mkvc( - # multivariate_normal( - # self.gmm.means_[k], self.gmm.covariances_ - # ).logpdf(model) - # ) - - W.append( - self.gmm.weights_[k] - * mkvc( - np.r_[ - [ - np.dot( - np.diag(sensW[i]).dot( - self.gmm.precisions_.dot(np.diag(sensW[i])) - ), - (model[i] - self.gmm.means_[k]).T, - ) - for i in range(len(model)) - ] - ] - ) - ) - elif ( - self.gmm.covariance_type == "diag" - or self.gmm.covariance_type == "spherical" - ): - # logP[:, k] = mkvc( - # multivariate_normal( - # self.gmm.means_[k], - # self.gmm.covariances_[k] * np.eye(len(self.wiresmap.maps)), - # ).logpdf(model) - # ) - W.append( - self.gmm.weights_[k] - * mkvc( - np.r_[ - [ - np.dot( - np.diag(sensW[i]).dot( - ( - self.gmm.precisions_[k] - * np.eye(len(self.wiresmap.maps)) - ).dot(np.diag(sensW[i])) - ), - (model[i] - self.gmm.means_[k]).T, - ) - for i in range(len(model)) - ] - ] - ) - ) - else: - # logP[:, k] = mkvc( - # multivariate_normal( - # self.gmm.means_[k], self.gmm.covariances_[k] - # ).logpdf(model) - # ) - W.append( - self.gmm.weights_[k] - * mkvc( - np.r_[ - [ - np.dot( - np.diag(sensW[i]).dot( - self.gmm.precisions_[k].dot( - np.diag(sensW[i]) - ) - ), - (model[i] - self.gmm.means_[k]).T, - ) - for i in range(len(model)) - ] - ] - ) - ) - W = np.c_[W].T - logP = np.vstack([logP for maps in self.wiresmap.maps]) - numer = (W * np.exp(logP)).sum(axis=1) - r = numer / (np.exp(score_vec)) - return mkvc(mD.T * r) - - @timeIt - def deriv2(self, m, v=None): - if getattr(self, "reference_model", None) is None: - self.reference_model = mkvc(self.gmm.means_[self.membership(m)]) - - if self.approx_hessian: - # we approximate it with the covariance of the cluster - # whose each point belong - membership = self.compute_quasi_geology_model() - modellist = self.wiresmap * m - dmmodel = np.c_[[a * b for a, b in zip(self.maplist, modellist)]].T - mD = [a.deriv(b) for a, b in zip(self.maplist, modellist)] - mD = sp.block_diag(mD) - if self._r_second_deriv is None: - if self.gmm.covariance_type == "tied": - if self.non_linear_relationships: - r = np.r_[ - [ - self.gmm.cluster_mapping[membership[i]].deriv( - dmmodel[i], - v=( - self.gmm.cluster_mapping[membership[i]].deriv( - dmmodel[i], v=self.gmm.precisions_ - ) - ).T, - ) - for i in range(len(dmmodel)) - ] - ] - else: - r = self.gmm.precisions_[np.newaxis, :, :][ - np.zeros_like(membership) - ] - elif ( - self.gmm.covariance_type == "spherical" - or self.gmm.covariance_type == "diag" - ): - if self.non_linear_relationships: - r = np.r_[ - [ - self.gmm.cluster_mapping[membership[i]].deriv( - dmmodel[i], - v=( - self.gmm.cluster_mapping[membership[i]].deriv( - dmmodel[i], - v=self.gmm.precisions_[membership[i]] - * np.eye(len(self.wiresmap.maps)), - ) - ).T, - ) - for i in range(len(dmmodel)) - ] - ] - else: - r = np.r_[ - [ - self.gmm.precisions_[memb] - * np.eye(len(self.wiresmap.maps)) - for memb in membership - ] - ] - else: - if self.non_linear_relationships: - r = np.r_[ - [ - self.gmm.cluster_mapping[membership[i]].deriv( - dmmodel[i], - v=( - self.gmm.cluster_mapping[membership[i]].deriv( - dmmodel[i], - v=self.gmm.precisions_[membership[i]], - ) - ).T, - ) - for i in range(len(dmmodel)) - ] - ] - else: - r = self.gmm.precisions_[membership] - - self._r_second_deriv = r - - if v is not None: - mDv = self.wiresmap * (mD * v) - mDv = np.c_[mDv] - r0 = (self.W * (mkvc(mDv))).reshape(mDv.shape, order="F") - return mkvc( - mD.T - * ( - self.W - * ( - mkvc( - np.r_[ - [ - np.dot(self._r_second_deriv[i], r0[i]) - for i in range(len(r0)) - ] - ] - ) - ) - ) - ) - else: - # Forming the Hessian by diagonal blocks - hlist = [ - [ - self._r_second_deriv[:, i, j] - for i in range(len(self.wiresmap.maps)) - ] - for j in range(len(self.wiresmap.maps)) - ] - Hr = sp.csc_matrix((0, 0), dtype=np.float64) - for i in range(len(self.wiresmap.maps)): - Hc = sp.csc_matrix((0, 0), dtype=np.float64) - for j in range(len(self.wiresmap.maps)): - Hc = sp.hstack([Hc, sdiag(hlist[i][j])]) - Hr = sp.vstack([Hr, Hc]) - - Hr = Hr.dot(self.W) - - return (mD.T * mD) * (self.W * (Hr)) - - else: - if self.non_linear_relationships: - raise Exception("Not implemented") - - # non distinct clusters positive definite approximated Hessian - modellist = self.wiresmap * m - model = np.c_[[a * b for a, b in zip(self.maplist, modellist)]].T - - if getattr(self.W, "diagonal", None) is not None: - sensW = np.c_[ - [wire[1] * self.W.diagonal() for wire in self.wiresmap.maps] - ].T - else: - sensW = np.ones_like(model) - - mD = [a.deriv(b) for a, b in zip(self.maplist, modellist)] - mD = sp.block_diag(mD) - - score = self.gmm.score_samples_with_sensW(model, sensW) - logP = np.zeros((len(model), self.gmm.n_components)) - W = [] - logP = self.gmm._estimate_weighted_log_prob_with_sensW( - model, - sensW, - ) - for k in range(self.gmm.n_components): - if self.gmm.covariance_type == "tied": - W.append( - [ - np.diag(sensW[i]).dot( - self.gmm.precisions_.dot(np.diag(sensW[i])) - ) - for i in range(len(model)) - ] - ) - elif ( - self.gmm.covariance_type == "diag" - or self.gmm.covariance_type == "spherical" - ): - W.append( - [ - np.diag(sensW[i]).dot( - ( - self.gmm.precisions_[k] - * np.eye(len(self.wiresmap.maps)) - ).dot(np.diag(sensW[i])) - ) - for i in range(len(model)) - ] - ) - else: - W.append( - [ - np.diag(sensW[i]).dot( - self.gmm.precisions_[k].dot(np.diag(sensW[i])) - ) - for i in range(len(model)) - ] - ) - W = np.c_[W] - - hlist = [ - [ - (W[:, :, i, j].T * np.exp(logP)).sum(axis=1) / np.exp(score) - for i in range(len(self.wiresmap.maps)) - ] - for j in range(len(self.wiresmap.maps)) - ] - - # Forming the Hessian by diagonal blocks - Hr = sp.csc_matrix((0, 0), dtype=np.float64) - for i in range(len(self.wiresmap.maps)): - Hc = sp.csc_matrix((0, 0), dtype=np.float64) - for j in range(len(self.wiresmap.maps)): - Hc = sp.hstack([Hc, sdiag(hlist[i][j])]) - Hr = sp.vstack([Hr, Hc]) - Hr = (mD.T * mD) * Hr - - if v is not None: - return Hr.dot(v) - - return Hr - - -class PGI(ComboObjectiveFunction): - """ - class similar to regularization.tikhonov.Simple, with a PGIsmallness. - PARAMETERS - ---------- - :param SimPEG.utils.WeightedGaussianMixture gmmref: refereence/prior GMM - :param SimPEG.utils.WeightedGaussianMixture gmm: GMM to use - :param SimPEG.maps.Wires wiresmap: wires mapping to the various physical properties - :param list maplist: list of SimPEG.maps for each physical property. - :param discretize.BaseMesh mesh: tensor, QuadTree or Octree mesh - :param boolean approx_gradient: use the L2-approximation of the gradient, default is True - :param boolean approx_eval: use the L2-approximation evaluation of the smallness term - """ - - def __init__( - self, - mesh, - gmmref, - alpha_x=None, - alpha_y=None, - alpha_z=None, - alpha_xx=0.0, - alpha_yy=0.0, - alpha_zz=0.0, - gmm=None, - wiresmap=None, - maplist=None, - alpha_pgi=1.0, - approx_hessian=True, - approx_gradient=True, - approx_eval=True, - weights_list=None, - non_linear_relationships: bool = False, - reference_model_in_smooth: bool = False, - **kwargs, - ): - self._wiresmap = wiresmap - self._maplist = maplist - self.regularization_mesh = mesh - self.gmmref = copy.deepcopy(gmmref) - self.gmmref.order_clusters_GM_weight() - - objfcts = [ - PGIsmallness( - gmmref, - mesh=self.regularization_mesh, - gmm=gmm, - wiresmap=self.wiresmap, - maplist=self.maplist, - approx_eval=approx_eval, - approx_gradient=approx_gradient, - approx_hessian=approx_hessian, - non_linear_relationships=non_linear_relationships, - weights=weights_list, - **kwargs, - ) - ] - - if not isinstance(weights_list, list): - weights_list = [weights_list] * len(self.maplist) - - for model_map, wire, weights in zip( - self.maplist, self.wiresmap.maps, weights_list - ): - objfcts += [ - WeightedLeastSquares( - alpha_s=0.0, - alpha_x=alpha_x, - alpha_y=alpha_y, - alpha_z=alpha_z, - alpha_xx=alpha_xx, - alpha_yy=alpha_yy, - alpha_zz=alpha_zz, - mesh=self.regularization_mesh, - mapping=model_map * wire[1], - weights=weights, - **kwargs, - ) - ] - - super().__init__(objfcts=objfcts) - self.reference_model_in_smooth = reference_model_in_smooth - self.alpha_pgi = alpha_pgi - - @property - def alpha_pgi(self): - """PGI smallness weight""" - if getattr(self, "_alpha_pgi", None) is None: - self._alpha_pgi = self.multipliers[0] - return self._alpha_pgi - - @alpha_pgi.setter - def alpha_pgi(self, value): - value = validate_float("alpha_pgi", value, min_val=0.0) - self._alpha_pgi = value - self._multipliers[0] = value - - @property - def gmm(self): - return self.objfcts[0].gmm - - @gmm.setter - def gmm(self, gm): - self.objfcts[0].gmm = copy.deepcopy(gm) - - def membership(self, m): - return self.objfcts[0].membership(m) - - def compute_quasi_geology_model(self): - return self.objfcts[0].compute_quasi_geology_model() - - @property - def wiresmap(self): - if getattr(self, "_wiresmap", None) is None: - self._wiresmap = Wires(("m", self.regularization_mesh.nC)) - return self._wiresmap - - @property - def maplist(self): - if getattr(self, "_maplist", None) is None: - self._maplist = [ - IdentityMap(nP=self.regularization_mesh.nC) - for maps in self.wiresmap.maps - ] - return self._maplist - - @property - def regularization_mesh(self) -> RegularizationMesh: - """Regularization mesh""" - return self._regularization_mesh - - @regularization_mesh.setter - def regularization_mesh(self, mesh: RegularizationMesh): - if not isinstance(mesh, RegularizationMesh): - mesh = RegularizationMesh(mesh) - - self._regularization_mesh = mesh - - @property - def reference_model_in_smooth(self) -> bool: - """ - Use the reference model in the model gradient penalties. - """ - return self._reference_model_in_smooth - - @reference_model_in_smooth.setter - def reference_model_in_smooth(self, value: bool): - if not isinstance(value, bool): - raise TypeError( - "'reference_model_in_smooth must be of type 'bool'. " - f"Value of type {type(value)} provided." - ) - self._reference_model_in_smooth = value - for fct in self.objfcts[1:]: - if getattr(fct, "reference_model_in_smooth", None) is not None: - fct.reference_model_in_smooth = value - - @property - def reference_model(self) -> np.ndarray: - """Reference physical property model""" - return self.objfcts[0].reference_model - - @reference_model.setter - def reference_model(self, values: np.ndarray | float): - if isinstance(values, float): - values = np.ones(self._nC_residual) * values - - for fct in self.objfcts: - fct.reference_model = values - - mref = deprecate_property( - reference_model, - "mref", - "reference_model", - "0.19.0", - future_warn=True, - error=False, - ) diff --git a/SimPEG/regularization/sparse.py b/SimPEG/regularization/sparse.py deleted file mode 100644 index 2c28e94075..0000000000 --- a/SimPEG/regularization/sparse.py +++ /dev/null @@ -1,339 +0,0 @@ -from __future__ import annotations - -import numpy as np - -from discretize.base import BaseMesh - -from .base import ( - BaseRegularization, - WeightedLeastSquares, - RegularizationMesh, - Smallness, - SmoothnessFirstOrder, -) -from .. import utils -from ..utils import ( - validate_ndarray_with_shape, - validate_float, - validate_type, - validate_string, -) - - -class BaseSparse(BaseRegularization): - """ - Base class for building up the components of the Sparse Regularization - """ - - def __init__(self, mesh, norm=2.0, irls_scaled=True, irls_threshold=1e-8, **kwargs): - super().__init__(mesh=mesh, **kwargs) - self.norm = norm - self.irls_scaled = irls_scaled - self.irls_threshold = irls_threshold - - @property - def irls_scaled(self) -> bool: - """ - Scale irls weights. - """ - return self._irls_scaled - - @irls_scaled.setter - def irls_scaled(self, value: bool): - self._irls_scaled = validate_type("irls_scaled", value, bool, cast=False) - - @property - def irls_threshold(self): - """ - Constant added to the denominator of the IRLS weights for stability. - """ - return self._irls_threshold - - @irls_threshold.setter - def irls_threshold(self, value): - self._irls_threshold = validate_float( - "irls_threshold", value, min_val=0.0, inclusive_min=False - ) - - @property - def norm(self): - """ - Value of the norm - """ - return self._norm - - @norm.setter - def norm(self, value: float | np.ndarray | None): - if value is None: - value = np.ones(self._weights_shapes[0]) * 2.0 - expected_shapes = self._weights_shapes - if isinstance(expected_shapes, list): - expected_shapes = expected_shapes[0] - value = validate_ndarray_with_shape( - "norm", value, shape=[expected_shapes, (1,)], dtype=float - ) - if value.shape == (1,): - value = np.full(expected_shapes[0], value) - - if np.any(value < 0) or np.any(value > 2): - raise ValueError( - "Value provided for 'norm' should be in the interval [0, 2]" - ) - self._norm = value - - def get_lp_weights(self, f_m): - """ - Utility function to get the IRLS weights. - By default, the weights are scaled by the gradient of the IRLS on - the max of the l2-norm. - """ - lp_scale = np.ones_like(f_m) - if self.irls_scaled: - # Scale on l2-norm gradient: f_m.max() - l2_max = np.ones_like(f_m) * np.abs(f_m).max() - # Compute theoretical maximum gradients for p < 1 - l2_max[self.norm < 1] = self.irls_threshold / np.sqrt( - 1.0 - self.norm[self.norm < 1] - ) - lp_values = l2_max / (l2_max**2.0 + self.irls_threshold**2.0) ** ( - 1.0 - self.norm / 2.0 - ) - lp_scale[lp_values != 0] = np.abs(f_m).max() / lp_values[lp_values != 0] - - return lp_scale / (f_m**2.0 + self.irls_threshold**2.0) ** ( - 1.0 - self.norm / 2.0 - ) - - -class SparseSmallness(BaseSparse, Smallness): - """ - Sparse smallness regularization - - **Inputs** - - :param int norm: norm on the smallness - """ - - _multiplier_pair = "alpha_s" - - def update_weights(self, m): - """ - Compute and store the irls weights. - """ - f_m = self.f_m(m) - self.set_weights(irls=self.get_lp_weights(f_m)) - - -class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): - """ - Base Class for sparse regularization on first spatial derivatives - """ - - def __init__(self, mesh, orientation="x", gradient_type="total", **kwargs): - if "gradientType" in kwargs: - self.gradientType = kwargs.pop("gradientType") - else: - self.gradient_type = gradient_type - super().__init__(mesh=mesh, orientation=orientation, **kwargs) - - def update_weights(self, m): - """ - Compute and store the irls weights. - """ - if self.gradient_type == "total" and self.parent is not None: - f_m = np.zeros(self.regularization_mesh.nC) - for obj in self.parent.objfcts: - if isinstance(obj, SparseSmoothness): - avg = getattr(self.regularization_mesh, f"aveF{obj.orientation}2CC") - f_m += np.abs(avg * obj.f_m(m)) - - f_m = getattr(self.regularization_mesh, f"aveCC2F{self.orientation}") * f_m - - else: - f_m = self.f_m(m) - - self.set_weights(irls=self.get_lp_weights(f_m)) - - @property - def gradient_type(self) -> str: - """ - Choice of gradient measure used in the irls weights - """ - return self._gradient_type - - @gradient_type.setter - def gradient_type(self, value: str): - self._gradient_type = validate_string( - "gradient_type", value, ["total", "components"] - ) - - gradientType = utils.code_utils.deprecate_property( - gradient_type, "gradientType", "0.19.0", error=False, future_warn=True - ) - - -class Sparse(WeightedLeastSquares): - r""" - The regularization is: - - .. math:: - - R(m) = \frac{1}{2}\mathbf{(m-m_\text{ref})^\top W^\top R^\top R - W(m-m_\text{ref})} - - where the IRLS weight - - .. math:: - - R = \eta \text{diag} \left[\mathbf{r}_s \right]^{1/2} \ - r_{s_i} = {\Big( {({m_i}^{(k-1)})}^{2} + \epsilon^2 \Big)}^{p_s/2 - 1} - - where k denotes the iteration number. So the derivative is straight forward: - - .. math:: - - R(m) = \mathbf{W^\top R^\top R W (m-m_\text{ref})} - - The IRLS weights are re-computed after each beta solves using - :class:`~SimPEG.directives.Update_IRLS` within the inversion directives. - """ - - def __init__( - self, - mesh, - active_cells=None, - norms=None, - gradient_type="total", - irls_scaled=True, - irls_threshold=1e-8, - **kwargs, - ): - if not isinstance(mesh, RegularizationMesh): - mesh = RegularizationMesh(mesh) - - if not isinstance(mesh, RegularizationMesh): - TypeError( - f"'regularization_mesh' must be of type {RegularizationMesh} or {BaseMesh}. " - f"Value of type {type(mesh)} provided." - ) - self._regularization_mesh = mesh - if active_cells is not None: - self._regularization_mesh.active_cells = active_cells - - objfcts = [ - SparseSmallness(mesh=self.regularization_mesh), - SparseSmoothness(mesh=self.regularization_mesh, orientation="x"), - ] - - if mesh.dim > 1: - objfcts.append( - SparseSmoothness(mesh=self.regularization_mesh, orientation="y") - ) - - if mesh.dim > 2: - objfcts.append( - SparseSmoothness(mesh=self.regularization_mesh, orientation="z") - ) - - gradientType = kwargs.pop("gradientType", None) - super().__init__( - self.regularization_mesh, - objfcts=objfcts, - **kwargs, - ) - if norms is None: - norms = [1] * (mesh.dim + 1) - self.norms = norms - - if gradientType is not None: - # Trigger deprecation warning - self.gradientType = gradientType - else: - self.gradient_type = gradient_type - - self.irls_scaled = irls_scaled - self.irls_threshold = irls_threshold - - @property - def gradient_type(self) -> str: - """ - Choice of gradient measure used in the irls weights - """ - return self._gradient_type - - @gradient_type.setter - def gradient_type(self, value: str): - for fct in self.objfcts: - if hasattr(fct, "gradient_type"): - fct.gradient_type = value - - self._gradient_type = value - - gradientType = utils.code_utils.deprecate_property( - gradient_type, "gradientType", "0.19.0", error=False, future_warn=True - ) - - @property - def norms(self): - """ - Value of the norm - """ - return self._norms - - @norms.setter - def norms(self, values: list | np.ndarray | None): - if values is not None: - if len(values) != len(self.objfcts): - raise ValueError( - f"The number of values provided for 'norms', {len(values)}, does not " - f"match the number of regularization functions, {len(self.objfcts)}." - ) - else: - values = [None] * len(self.objfcts) - previous_norms = getattr(self, "_norms", [None] * len(self.objfcts)) - try: - for val, fct in zip(values, self.objfcts): - fct.norm = val - self._norms = values - except Exception as err: - # reset the norms if failed - for val, fct in zip(previous_norms, self.objfcts): - fct.norm = val - raise err - - @property - def irls_scaled(self) -> bool: - """ - Scale irls weights. - """ - return self._irls_scaled - - @irls_scaled.setter - def irls_scaled(self, value: bool): - value = validate_type("irls_scaled", value, bool, cast=False) - for fct in self.objfcts: - fct.irls_scaled = value - self._irls_scaled = value - - @property - def irls_threshold(self): - """ - Constant added to the denominator of the IRLS weights for stability. - """ - return self._irls_threshold - - @irls_threshold.setter - def irls_threshold(self, value): - value = validate_float( - "irls_threshold", value, min_val=0.0, inclusive_min=False - ) - for fct in self.objfcts: - fct.irls_threshold = value - self._irls_threshold = value - - def update_weights(self, model): - """ - Trigger irls update on all children - """ - for fct in self.objfcts: - fct.update_weights(model) diff --git a/SimPEG/simulation.py b/SimPEG/simulation.py deleted file mode 100644 index 1b2e00e9e3..0000000000 --- a/SimPEG/simulation.py +++ /dev/null @@ -1,689 +0,0 @@ -""" -Define simulation classes -""" -from __future__ import annotations - -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 - -from . import props -from .data import SyntheticData, Data -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 - -__all__ = ["LinearSimulation", "ExponentialSinusoidSimulation"] - - -############################################################################## -# # -# Simulation Base Classes # -# # -############################################################################## - - -class BaseSimulation(props.HasModel): - """ - BaseSimulation is the base class for all geophysical forward simulations in - SimPEG. - """ - - ########################################################################### - # Properties - - _REGISTRY = {} - - @property - def mesh(self): - """Discretize mesh for the simulation - - Returns - ------- - discretize.base.BaseMesh - """ - 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 - - @property - def survey(self): - """The survey for the simulation. - - Returns - ------- - SimPEG.survey.BaseSurvey - """ - return self._survey - - @survey.setter - def survey(self, value): - if value is not None: - value = validate_type("survey", value, BaseSurvey, cast=False) - self._survey = value - - @property - def counter(self): - """The counter. - - Returns - ------- - None or SimPEG.utils.Counter - """ - return self._counter - - @counter.setter - def counter(self, value): - if value is not None: - value = validate_type("counter", value, Counter, cast=False) - self._counter = value - - @property - def sensitivity_path(self): - """Path to store the sensitivty. - - Returns - ------- - str - """ - return self._sensitivity_path - - @sensitivity_path.setter - def sensitivity_path(self, value): - self._sensitivity_path = validate_string("sensitivity_path", value) - - @property - def solver(self): - """Linear algebra solver (e.g. from pymatsolver). - - Returns - ------- - class - A solver class that, when instantiated allows a multiplication with the - returned object. - """ - 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): - """Options passed to the `solver` class on initialization. - - Returns - ------- - dict - Passed as keyword arguments 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): - """Verbosity flag. - - Returns - ------- - bool - """ - return self._verbose - - @verbose.setter - def verbose(self, value): - self._verbose = validate_type("verbose", value, bool) - - ########################################################################### - # Instantiation - - def __init__( - self, - mesh=None, - survey=None, - solver=None, - solver_opts=None, - sensitivity_path=None, - store_sensitivities=None, - counter=None, - verbose=False, - **kwargs, - ): - self._jtjdiag: np.ndarray | None = None - self.store_sensitivities: str | None = store_sensitivities - 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) - - ########################################################################### - # Methods - - def fields(self, m=None): - """ - u = fields(m) - The field given the model. - :param numpy.ndarray m: model - :rtype: numpy.ndarray - :return: u, the fields - """ - raise NotImplementedError("fields has not been implemented for this ") - - def dpred(self, m=None, f=None): - r""" - dpred(m, f=None) - Create the projected data from a model. - The fields, f, (if provided) will be used for the predicted data - instead of recalculating the fields (which may be expensive!). - - .. math:: - - d_\text{pred} = P(f(m)) - - Where P is a projection of the fields onto the data space. - """ - if self.survey is None: - raise AttributeError( - "The survey has not yet been set and is required to compute " - "data. Please set the survey for the simulation: " - "simulation.survey = survey" - ) - - if f is None: - if m is None: - m = self.model - - f = self.fields(m) - - data = Data(self.survey) - 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) - - @timeIt - def Jvec(self, m, v, f=None): - """ - Jv = Jvec(m, v, f=None) - Effect of J(m) on a vector v. - :param numpy.ndarray m: model - :param numpy.ndarray v: vector to multiply - :param Fields f: fields - :rtype: numpy.ndarray - :return: Jv - """ - raise NotImplementedError("Jvec is not yet implemented.") - - @timeIt - def Jtvec(self, m, v, f=None): - """ - Jtv = Jtvec(m, v, f=None) - Effect of transpose of J(m) on a vector v. - :param numpy.ndarray m: model - :param numpy.ndarray v: vector to multiply - :param Fields f: fields - :rtype: numpy.ndarray - :return: JTv - """ - raise NotImplementedError("Jt is not yet implemented.") - - @timeIt - def Jvec_approx(self, m, v, f=None): - """Jvec_approx(m, v, f=None) - Approximate effect of J(m) on a vector v - :param numpy.ndarray m: model - :param numpy.ndarray v: vector to multiply - :param Fields f: fields - :rtype: numpy.ndarray - :return: approxJv - """ - return self.Jvec(m, v, f) - - @timeIt - def Jtvec_approx(self, m, v, f=None): - """Jtvec_approx(m, v, f=None) - Approximate effect of transpose of J(m) on a vector v. - :param numpy.ndarray m: model - :param numpy.ndarray v: vector to multiply - :param Fields f: fields - :rtype: numpy.ndarray - :return: JTv - """ - return self.Jtvec(m, v, f) - - @count - def residual(self, m, dobs, f=None): - r""" - The data residual: - - .. math:: - - \mu_\\text{data} = \mathbf{d}_\\text{pred} - \mathbf{d}_\\text{obs} - - :param numpy.ndarray m: geophysical model - :param numpy.ndarray f: fields - :rtype: numpy.ndarray - :return: data residual - """ - return mkvc(self.dpred(m, f=f) - dobs) - - def make_synthetic_data( - self, m, relative_error=0.05, noise_floor=0.0, f=None, add_noise=False, **kwargs - ): - """ - Make synthetic data given a model, and a standard deviation. - :param numpy.ndarray m: geophysical model - :param numpy.ndarray | float relative_error: standard deviation - :param numpy.ndarray | float noise_floor: noise floor - :param numpy.ndarray f: fields for the given model (if pre-calculated) - """ - - std = kwargs.pop("std", None) - if std is not None: - raise TypeError( - "The std parameter has been removed. " "Please use relative_error." - ) - - if f is None: - f = self.fields(m) - - dclean = self.dpred(m, f=f) - - if add_noise is True: - std = np.sqrt((relative_error * np.abs(dclean)) ** 2 + noise_floor**2) - noise = std * np.random.randn(*dclean.shape) - dobs = dclean + noise - else: - dobs = dclean - - return SyntheticData( - survey=self.survey, - dobs=dobs, - dclean=dclean, - relative_error=relative_error, - noise_floor=noise_floor, - ) - - @property - def store_sensitivities(self): - """Options for storing sensitivities. - - There are 3 options: - - - None: sensitivities are not stored - - 'ram': sensitivity matrix stored in RAM - - 'disk': sensitivities written and stored to disk - - Returns - ------- - {'disk', 'ram', None} - A string defining the model type for the simulation or None. - """ - return self._store_sensitivities - - @store_sensitivities.setter - def store_sensitivities(self, value: str | None): - if value is None: - self._store_sensitivities = None - else: - self._store_sensitivities = validate_string( - "store_sensitivities", value, ["disk", "ram"] - ) - - -class BaseTimeSimulation(BaseSimulation): - """ - Base class for a time domain simulation - """ - - @property - def time_steps(self): - """The time steps for the time domain simulation. - - You can set as an array of dt's or as a list of tuples/floats. - If it is set as a list, tuples are unpacked with - `discretize.utils.unpack_widths``. - - For example, the following setters are the same:: - - >>> sim.time_steps = [(1e-6, 3), 1e-5, (1e-4, 2)] - >>> sim.time_steps = np.r_[1e-6,1e-6,1e-6,1e-5,1e-4,1e-4] - - Returns - ------- - numpy.ndarray - - See Also - -------- - discretize.utils.unpack_widths - """ - return self._time_steps - - @time_steps.setter - def time_steps(self, value): - if value is not None: - if isinstance(value, list): - value = unpack_widths(value) - value = validate_ndarray_with_shape("time_steps", value, shape=("*",)) - self._time_steps = value - del self.time_mesh - - @property - def t0(self): - """Start time for the discretization. - - Returns - ------- - float - """ - return self._t0 - - @t0.setter - def t0(self, value): - self._t0 = validate_float("t0", value) - del self.time_mesh - - def __init__(self, mesh=None, t0=0.0, time_steps=None, **kwargs): - self.t0 = t0 - self.time_steps = time_steps - super().__init__(mesh=mesh, **kwargs) - - @property - def time_mesh(self): - if getattr(self, "_time_mesh", None) is None: - self._time_mesh = TensorMesh( - [ - self.time_steps, - ], - x0=[self.t0], - ) - return self._time_mesh - - @time_mesh.deleter - def time_mesh(self): - if hasattr(self, "_time_mesh"): - del self._time_mesh - - @property - def nT(self): - return self.time_mesh.n_cells - - @property - def times(self): - "Modeling times" - return self.time_mesh.nodes_x - - def dpred(self, m=None, f=None): - r""" - dpred(m, f=None) - Create the projected data from a model. - The fields, f, (if provided) will be used for the predicted data - instead of recalculating the fields (which may be expensive!). - - .. math:: - - d_\text{pred} = P(f(m)) - - Where P is a projection of the fields onto the data space. - """ - if self.survey is None: - raise AttributeError( - "The survey has not yet been set and is required to compute " - "data. Please set the survey for the simulation: " - "simulation.survey = survey" - ) - - if f is None: - f = self.fields(m) - - data = Data(self.survey) - 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 - - -############################################################################## -# # -# Linear Simulation # -# # -############################################################################## - - -class LinearSimulation(BaseSimulation): - """ - Class for a linear simulation of the form - - .. math:: - - d = Gm - - where :math:`d` is a vector of the data, `G` is the simulation matrix and - :math:`m` is the model. - Inherit this class to build a linear simulation. - """ - - linear_model, model_map, model_deriv = props.Invertible( - "The model for a linear problem" - ) - - def __init__(self, mesh=None, linear_model=None, model_map=None, Jmatrix=None, **kwargs): - super().__init__(mesh=mesh, **kwargs) - self.linear_model = linear_model - self.model_map = model_map - self.solver = None - self._Jmatrix = None - self._gtg_diagonal = None - - if Jmatrix is not None: - self.Jmatrix = Jmatrix - - if self.survey is None: - # Give it an empty survey - self.survey = BaseSurvey([]) - if self.survey.nD == 0: - # try seting the number of data to Jmatrix - if getattr(self, "Jmatrix", None) is not None: - self.survey._vnD = np.r_[self.Jmatrix.shape[0]] - - @property - def Jmatrix(self): - if self._Jmatrix is None: - if hasattr(self, "linear_operator"): - self._Jmatrix = self.linear_operator() - else: - warnings.warn("Jmatrix has not been implemented for the simulation") - return self._Jmatrix - - @Jmatrix.setter - def Jmatrix(self, Jmatrix): - # Allows setting Jmatrix in a LinearSimulation - # TODO should be validated - self._Jmatrix = Jmatrix - - def fields(self, m): - self.model = m - return self.Jmatrix.dot(self.linear_model) - - def dpred(self, m=None, f=None): - if m is not None: - self.model = m - if f is not None: - return f - return self.fields(self.model) - - def getJ(self, m, f=None): - self.model = m - # self.model_deriv is likely a sparse matrix - # and Jmatrix is possibly dense, thus we need to do.. - return (self.model_deriv.T.dot(self.Jmatrix.T)).T - - def Jvec(self, m, v, f=None): - self.model = m - return self.Jmatrix.dot(self.model_deriv * v) - - def Jtvec(self, m, v, f=None): - self.model = m - return self.model_deriv.T * self.Jmatrix.T.dot(v) - - -class ExponentialSinusoidSimulation(LinearSimulation): - r""" - This is the simulation class for the linear problem consisting of - exponentially decaying sinusoids. The rows of the G matrix are - - .. math:: - - \int_x e^{p j_k x} \cos(\pi q j_k x) \quad, j_k \in [j_0, ..., j_n] - """ - - @property - def n_kernels(self): - """The number of kernels for the linear problem - - Returns - ------- - int - """ - return self._n_kernels - - @n_kernels.setter - def n_kernels(self, value): - self._n_kernels = validate_integer("n_kernels", value, min_val=1) - - @property - def p(self): - """Rate of exponential decay of the kernel. - - Returns - ------- - float - """ - return self._p - - @p.setter - def p(self, value): - self._p = validate_float("p", value) - - @property - def q(self): - """rate of oscillation of the kernel. - - Returns - ------- - float - """ - return self._q - - @q.setter - def q(self, value): - self._q = validate_float("q", value) - - @property - def j0(self): - """Maximum value for :math:`j_k = j_0`. - - Returns - ------- - float - """ - return self._j0 - - @j0.setter - def j0(self, value): - self._j0 = validate_float("j0", value) - - @property - def jn(self): - """Maximum value for :math:`j_k = j_n`. - - Returns - ------- - float - """ - return self._jn - - @jn.setter - def jn(self, value): - self._jn = validate_float("jn", value) - - def __init__(self, n_kernels=20, p=-0.25, q=0.25, j0=0.0, jn=60.0, **kwargs): - self.n_kernels = n_kernels - self.p = p - self.q = q - self.j0 = j0 - self.jn = jn - super(ExponentialSinusoidSimulation, self).__init__(**kwargs) - - @property - def jk(self): - """ - Parameters controlling the spread of kernel functions - """ - if getattr(self, "_jk", None) is None: - self._jk = np.linspace(self.j0, self.jn, self.n_kernels) - return self._jk - - def g(self, k): - """ - Kernel functions for the decaying oscillating exponential functions. - """ - return np.exp(self.p * self.jk[k] * self.mesh.cell_centers_x) * np.cos( - np.pi * self.q * self.jk[k] * self.mesh.cell_centers_x - ) - - @property - def G(self): - """ - Matrix whose rows are the kernel functions - """ - if getattr(self, "_G", None) is None: - G = np.empty((self.n_kernels, self.mesh.nC)) - - for i in range(self.n_kernels): - G[i, :] = self.g(i) * self.mesh.h[0] - - self._G = G - return self._G diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d50c72b28e..7866c36069 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -8,12 +8,22 @@ trigger: include: - '*' +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 + pr: branches: include: - '*' exclude: - '*no-ci*' + + stages: - stage: StyleChecks @@ -42,7 +52,7 @@ stages: - script: | pip install -r requirements_style.txt displayName: "Install dependencies to run the checks" - - script: make flake-permissive + - script: make flake displayName: "Run flake8" - job: @@ -55,7 +65,7 @@ stages: - script: | pip install -r requirements_style.txt displayName: "Install dependencies to run the checks" - - script: FLAKE8_OPTS="--exit-zero" make flake + - script: FLAKE8_OPTS="--exit-zero" make flake-all displayName: "Run flake8" - stage: Testing @@ -73,8 +83,17 @@ stages: vmImage: ubuntu-latest variables: python.version: '3.8' - timeoutInMinutes: 180 + timeoutInMinutes: 240 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) + - script: | git config --global user.name ${GH_NAME} git config --global user.email ${GH_EMAIL} @@ -92,10 +111,14 @@ stages: - script: | source "${HOME}/conda/etc/profile.d/conda.sh" source "${HOME}/conda/etc/profile.d/mamba.sh" - echo " - python="$(python.version) >> environment_test.yml - mamba env create -f environment_test.yml + 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 + echo "\nList installed packages" + conda list displayName: Create Anaconda testing environment - script: | @@ -104,6 +127,12 @@ stages: pip install -e . displayName: Build package + - script: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + python -c "import simpeg; print(simpeg.__version__)" + displayName: Check SimPEG version + - script: | source "${HOME}/conda/etc/profile.d/conda.sh" conda activate simpeg-test @@ -140,3 +169,228 @@ stages: displayName: Push documentation to simpeg-docs env: GH_TOKEN: $(gh.token) + +- stage: Deploy_dev_docs_experimental + dependsOn: Testing + condition: eq(variables['Build.Reason'], 'Schedule') # run only scheduled triggers + jobs: + - job: + displayName: Deploy dev docs to simpeg-doctest (experimental) + pool: + vmImage: ubuntu-latest + variables: + python.version: '3.8' + timeoutInMinutes: 240 + 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: | + 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: | + 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 + + - bash: | + 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 + echo "\nList installed packages" + conda list + displayName: Create Anaconda testing environment + + - bash: | + 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 -c "import simpeg; print(simpeg.__version__)" + displayName: Check SimPEG version + + - bash: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + export KMP_WARNINGS=0 + make -C docs html + displayName: Building documentation + + # Upload dev build of the docs to a dev branch in simpeg/simpeg-doctest + # and update submodule in the gh-pages branch + - bash: | + # Push new docs + # ------------- + # 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-doctest.git + cd simpeg-doctest + # Remove all files + shopt -s dotglob # configure bash to include dotfiles in * globs + export GLOBIGNORE=".git" # ignore .git directory in glob + git rm -rf * # remove all files + # Copy the built docs to the root of the repo + cp -r $BUILD_SOURCESDIRECTORY/docs/_build/html/* -t . + # Commit the new docs. Amend to avoid having a very large history. + git add . + 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-doctest (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 + # Commit changes + git add dev + 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-doctest (gh-pages branch)." + git push -q origin gh-pages 2>&1 >/dev/null + echo -e "\nFinished updating submodule dev." + + # Unset dotglob + shopt -u dotglob + export GLOBIGNORE="" + displayName: Push documentation to simpeg-doctest (dev branch) + env: + GH_TOKEN: $(gh.token) + +- stage: Deploy_release_docs_experimental + dependsOn: Testing + condition: startsWith(variables['build.sourceBranch'], 'refs/tags/') + jobs: + - job: + displayName: Deploy release docs to simpeg-doctest (experimental) + pool: + vmImage: ubuntu-latest + variables: + python.version: '3.8' + timeoutInMinutes: 240 + 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: | + 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: | + 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 + + - bash: | + 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 + echo "\nList installed packages" + conda list + displayName: Create Anaconda testing environment + + - bash: | + 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 -c "import simpeg; print(simpeg.__version__)" + displayName: Check SimPEG version + + - bash: | + source "${HOME}/conda/etc/profile.d/conda.sh" + conda activate simpeg-test + export KMP_WARNINGS=0 + make -C docs html + displayName: Building documentation + + # Upload release build of the docs to gh-pages branch in simpeg/simpeg-doctest + - bash: | + # Capture version + # TODO: we should be able to get the version from the + # build.sourceBranch variable + version=$(git tag --points-at HEAD) + if [ -n "$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-doctest.git + cd simpeg-doctest + # 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 + # Commit the new docs. + git add "$version" README.md .nojekyll latest + 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-doctest." + git push -fq origin gh-pages 2>&1 >/dev/null + echo -e "\nFinished uploading generated files." + displayName: Push documentation to simpeg-doctest + env: + GH_TOKEN: $(gh.token) \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile index 15a80d7bad..3751f949e1 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -14,7 +14,7 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -.PHONY: all api help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext +.PHONY: all api help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext serve help: @echo "Please use \`make ' where is one of" @@ -38,6 +38,7 @@ help: @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " serve to serve the docs locally" clean: rm -rf $(BUILDDIR)/* @@ -86,17 +87,17 @@ qthelp: @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/SimPEG.qhcp" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/simpeg.qhcp" @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/SimPEG.qhc" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/simpeg.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/SimPEG" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/SimPEG" + @echo "# mkdir -p $$HOME/.local/share/devhelp/simpeg" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/simpeg" @echo "# devhelp" epub: @@ -166,3 +167,6 @@ doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." + +serve: + cd $(BUILDDIR)/html && python -m http.server 8001 diff --git a/docs/README.md b/docs/README.md index df4d1fb872..5dd5d195fc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,5 +4,5 @@ A place where we automatically deploy the SimPEG documentation. This repository is automatically edited by our army of robots on the Azure CI service. Changes to the documentation should not be made -here but rather in the [`simpeg`](https://github.com/simpeg/simpeg) +here but rather in the [SimPEG](https://github.com/simpeg/simpeg) repository. diff --git a/docs/_static/versions.json b/docs/_static/versions.json new file mode 100644 index 0000000000..265c9718fa --- /dev/null +++ b/docs/_static/versions.json @@ -0,0 +1,31 @@ +[ + { + "version": "dev", + "url": "https://doctest.simpeg.xyz/dev/" + }, + { + "name": "v0.21.1 (latest)", + "version": "v0.21.1", + "url": "https://doctest.simpeg.xyz/v0.21.1/" + }, + { + "version": "v0.21.0", + "url": "https://doctest.simpeg.xyz/v0.21.0/" + }, + { + "version": "v0.20.0", + "url": "https://doctest.simpeg.xyz/v0.20.0/" + }, + { + "version": "v0.19.0", + "url": "https://doctest.simpeg.xyz/v0.19.0/" + }, + { + "version": "v0.18.1", + "url": "https://doctest.simpeg.xyz/v0.18.1/" + }, + { + "version": "v0.18.0", + "url": "https://doctest.simpeg.xyz/v0.18.0/" + } +] diff --git a/docs/_templates/autosummary/attribute.rst b/docs/_templates/autosummary/attribute.rst new file mode 100644 index 0000000000..820f45286e --- /dev/null +++ b/docs/_templates/autosummary/attribute.rst @@ -0,0 +1,7 @@ +:orphan: + +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} \ No newline at end of file diff --git a/docs/_templates/autosummary/base.rst b/docs/_templates/autosummary/base.rst new file mode 100644 index 0000000000..ef8e6277cb --- /dev/null +++ b/docs/_templates/autosummary/base.rst @@ -0,0 +1,9 @@ +{% if objtype == 'property' %} +:orphan: +{% endif %} + +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} \ No newline at end of file diff --git a/docs/_templates/autosummary/method.rst b/docs/_templates/autosummary/method.rst new file mode 100644 index 0000000000..820f45286e --- /dev/null +++ b/docs/_templates/autosummary/method.rst @@ -0,0 +1,7 @@ +:orphan: + +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} \ No newline at end of file diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index a06350d4c1..d4fe319939 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -8,15 +8,4 @@ - - {% endblock %} diff --git a/docs/conf.py b/docs/conf.py index 24a4527520..38bdeef6b2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,13 +13,12 @@ import sys import os -from datetime import datetime from sphinx_gallery.sorting import FileNameSortKey import glob -import SimPEG +import simpeg +from packaging.version import parse import plotly.io as pio -import subprocess -import shutil +from importlib.metadata import version pio.renderers.default = "sphinx_gallery" @@ -47,10 +46,8 @@ "sphinx.ext.extlinks", "sphinx.ext.intersphinx", "sphinx.ext.mathjax", - "sphinx_toolbox.collapse", "sphinx_gallery.gen_gallery", "sphinx.ext.todo", - "sphinx.ext.linkcode", "matplotlib.sphinxext.plot_directive", ] @@ -58,9 +55,7 @@ autosummary_generate = True numpydoc_attributes_as_param_list = False -# This has to be set to false in order to make the doc build in a -# reasonable amount of time. -numpydoc_show_inherited_class_members = False +numpydoc_show_inherited_class_members = True # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -76,16 +71,16 @@ # General information about the project. project = "SimPEG" -copyright = "2013 - 2023, SimPEG Team, http://simpeg.xyz" +copyright = "2013 - 2023, SimPEG Team, https://simpeg.xyz" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # -# The short X.Y version. -version = "0.19.0" # The full version, including alpha/beta/rc tags. -release = "0.19.0" +release = version("simpeg") +# The short X.Y version. +version = ".".join(release.split(".")[:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -140,17 +135,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 @@ -165,44 +167,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 = """ @@ -236,64 +239,78 @@ def linkcode_resolve(domain, info): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -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": [ - {"name": "SimPEG", "url": "https://simpeg.xyz"}, - {"name": "Contact", "url": "http://slack.simpeg.xyz"}, - ], - "icon_links": [ - { - "name": "GitHub", - "url": "https://github.com/simpeg/simpeg", - "icon": "fab fa-github", - }, - { - "name": "Slack", - "url": "http://slack.simpeg.xyz/", - "icon": "fab fa-slack", - }, - { - "name": "Discourse", - "url": "https://simpeg.discourse.group/", - "icon": "fab fa-discourse", - }, - { - "name": "Youtube", - "url": "https://www.youtube.com/c/geoscixyz", - "icon": "fab fa-youtube", - }, - { - "name": "Twitter", - "url": "https://twitter.com/simpegpy", - "icon": "fab fa-twitter", - }, - ], - "use_edit_page_button": False, - } - html_logo = "images/simpeg-logo.png" - - html_static_path = ["_static"] - - html_css_files = [ - "css/custom.css", - ] - - html_context = { - "github_user": "simpeg", - "github_repo": "simpeg", - "github_version": "main", - "doc_path": "docs", - } -except Exception: - html_theme = "default" +external_links = [ + dict(name="User Tutorials", url="https://simpeg.xyz/user-tutorials"), + dict(name="SimPEG", url="https://simpeg.xyz"), + dict(name="Contact", url="https://mattermost.softwareunderground.org/simpeg"), +] + +# Define SimPEG version for the version switcher +simpeg_version = parse(simpeg.__version__) +if simpeg_version.is_devrelease: + switcher_version = "dev" +else: + switcher_version = f"v{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", + }, + { + "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://doctest.simpeg.xyz/latest/_static/versions.json", + }, +} + +html_logo = "images/simpeg-logo.png" + +html_static_path = ["_static"] + +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 @@ -366,7 +383,7 @@ def linkcode_resolve(domain, info): # html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = "SimPEGdoc" +htmlhelp_basename = "simpegdoc" # -- Options for LaTeX output -------------------------------------------------- @@ -383,7 +400,7 @@ def linkcode_resolve(domain, info): # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ("index", "SimPEG.tex", "SimPEG Documentation", "SimPEG Team", "manual"), + ("index", "simpeg.tex", "SimPEG Documentation", "SimPEG Team", "manual"), ] # The name of an image file (relative to this directory) to place at the top of @@ -411,7 +428,7 @@ def linkcode_resolve(domain, info): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [("index", "simpeg", "SimPEG Documentation", ["SimPEG Team"], 1)] +man_pages = [("index", "SimPEG", "SimPEG Documentation", ["SimPEG Team"], 1)] # If true, show URL addresses after external links. # man_show_urls = False @@ -421,9 +438,10 @@ def linkcode_resolve(domain, info): "python": ("https://docs.python.org/3/", None), "numpy": ("https://numpy.org/doc/stable/", None), "scipy": ("https://docs.scipy.org/doc/scipy/reference/", None), - "matplotlib": ("http://matplotlib.org/stable/", None), + "matplotlib": ("https://matplotlib.org/stable/", None), "properties": ("https://propertiespy.readthedocs.io/en/latest/", None), - "discretize": ("http://discretize.simpeg.xyz/en/main/", None), + "discretize": ("https://discretize.simpeg.xyz/en/main/", None), + "pymatsolver": ("https://pymatsolver.readthedocs.io/en/latest/", None), } numpydoc_xref_param_type = True @@ -466,7 +484,7 @@ def linkcode_resolve(domain, info): "within_subsection_order": FileNameSortKey, "filename_pattern": "\.py", "backreferences_dir": "content/api/generated/backreferences", - "doc_module": "SimPEG", + "doc_module": "simpeg", "show_memory": True, "image_scrapers": image_scrapers, } 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 a66c578f7c..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.spontaneous_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.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 55faddc116..e401b4422d 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,14 +23,21 @@ Base SimPEG .. toctree:: :maxdepth: 3 - SimPEG + simpeg Regularizations --------------- .. toctree:: :maxdepth: 2 - SimPEG.regularization + simpeg.regularization + +Directives +---------- +.. toctree:: + :maxdepth: 2 + + simpeg.directives Utilities --------- @@ -40,7 +47,7 @@ Classes and functions for performing useful operations. .. toctree:: :maxdepth: 2 - SimPEG.utils + simpeg.utils Meta ---- @@ -49,4 +56,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.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/basic/contributing.rst b/docs/content/basic/contributing.rst deleted file mode 100644 index b1cd2f37dc..0000000000 --- a/docs/content/basic/contributing.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../../../CONTRIBUTING.rst diff --git a/docs/content/basic/installing_for_developers.rst b/docs/content/basic/installing_for_developers.rst deleted file mode 100644 index 25c0ea1bf2..0000000000 --- a/docs/content/basic/installing_for_developers.rst +++ /dev/null @@ -1,180 +0,0 @@ -.. _getting_started_developers: - -Getting Started: for Developers -=============================== - -- **Purpose:** To download and set up your environment for using and developing within SimPEG. - - -.. _getting_started_installing_python: - -Installing Python ------------------ - -.. 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/ - -SimPEG is written in Python_! To install and maintain your Python_ -environment, Anaconda_ is a package manager that you can use. -If you and Python_ are not yet acquainted, we highly -recommend checking out `Software Carpentry `_. - -.. _Python: https://www.python.org/ - -.. _Anaconda: https://www.anaconda.com/products/individual - -.. _getting_started_working_with_git_and_github: - -Working with Git and GitHub ---------------------------- - -.. image:: https://github.githubassets.com/images/modules/logos_page/Octocat.png - :align: right - :width: 100 - :target: https://github.com - - -To keep track of your code changes and contribute back to SimPEG, you will -need a github_ account, then fork the `SimPEG repository `_ -to your local account. -(`How to fork a repo `_). - - -.. _github: https://github.com - -Next, clone your fork to your computer so that you have a local copy. We recommend setting up a -directory in your home directory to put your version-controlled repositories (e.g. called :code:`git`). -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 - -.. _SourceTree: https://www.sourcetreeapp.com/ - -.. _GitKraken: https://www.gitkraken.com/ - -2. Using a desktop client such as SourceTree_ or GitKraken_. - - .. image:: ../../images/sourceTreeSimPEG.png - :align: center - :width: 400 - :target: https://www.sourcetreeapp.com/ - - If this is your first time managing a github_ repository through SourceTree_, - it is also handy to set up the remote account so it remembers your github_ - user name and password - - .. image:: ../../images/sourceTreeRemote.png - :align: center - :width: 400 - -For managing your copy of SimPEG and contributing back to the main -repository, have a look at the article: `A successful git branching model -`_ - - -.. _getting_started_setting_up_your_environment: - -Setting up your 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. :: - - conda env create -f environment_test.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``). - -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, executed from the base level directory -would be :: - - pip install --no-deps -e . - -This is called an editable mode install (`-e`). This will make a symbolic link for you to -the working simpeg directory for that python environment to use and you can then -make use of any changes you have made to the repository without re-installing it. This -command (`--no-deps`) also ensures pip won't unintentionally re-install a package that -was previously installed with conda. This practice also allows you to uninstall SimPEG -if so desired :: - - pip uninstall SimPEG - -.. note:: - We no longer recommend modifying your python path environment variable as a way - to install SimPEG for developers. - -.. _getting_started_jupyter_notebook: - -Jupyter Notebook ----------------- - -.. image:: https://raw.githubusercontent.com/jupyter/design/master/logos/Square%20Logo/squarelogo-greytext-orangebody-greymoons/squarelogo-greytext-orangebody-greymoons.svg - :align: right - :width: 100 - -The SimPEG team loves the `Jupyter notebook`_. It is an interactive -development environment. It is installed it you used Anaconda_ and can be -launched from a terminal using:: - - jupyter notebook - - -.. _getting_started_if_all_is_well: - -If all is well ... ------------------- - -You should be able to open a terminal within SimPEG/tutorials and run an example, ie.:: - - python 02-linear_inversion/plot_inv_1_inversion_lsq.py - -or you can download and run the :ref:`notebook from the docs `. - -.. image:: /content/tutorials/02-linear_inversion/images/sphx_glr_plot_inv_1_inversion_lsq_003.png - -You are now set up to SimPEG! - -If all is not well ... ----------------------- - -Submit an issue_ and `change this file`_! - -.. _issue: https://github.com/simpeg/simpeg/issues - -.. _change this file: https://github.com/simpeg/simpeg/edit/main/docs/content/api_getting_started_developers.rst - - -Advanced: Installing Solvers ----------------------------- - -Pardiso_ is a direct solvers that can be used for solving large(ish) -linear systems of equations. The provided testing environment should install -the necessary solvers for you. pymatsolver_ If you wish to modify pymatsolver_ as well -follow the instructions to download and install pymatsolver_. - -.. _Pardiso: https://www.pardiso-project.org - -.. _pymatsolver: https://github.com/rowanc1/pymatsolver - -If you open a `Jupyter notebook`_ and are able to run:: - - from pymatsolver import Pardiso - -.. _Jupyter notebook: http://jupyter.org/ - -then you have succeeded! Otherwise, make an `issue in pymatsolver`_. - -.. _issue in pymatsolver: https://github.com/rowanc1/pymatsolver/issues diff --git a/docs/content/basic/practices.rst b/docs/content/basic/practices.rst deleted file mode 100644 index eff9542b39..0000000000 --- a/docs/content/basic/practices.rst +++ /dev/null @@ -1,267 +0,0 @@ -.. _practices: - -Practices -========= - -- **Purpose**: In the development of SimPEG, we strive to follow best practices. Here, we - provide an overview of those practices and some tools we use to support them. - -Here we cover - -- testing_ -- style_ -- licensing_ - -.. _testing: - -Testing -------- - -.. image:: https://dev.azure.com/simpeg/simpeg/_apis/build/status/simpeg.simpeg?branchName=main - :target: https://dev.azure.com/simpeg/simpeg/_build/latest?definitionId=2&branchName=main - :alt: Azure pipeline - -.. image:: https://codecov.io/gh/simpeg/simpeg/branch/main/graph/badge.svg - :target: https://codecov.io/gh/simpeg/simpeg - :alt: Coverage status - -On each update, SimPEG is tested using the continuous integration service -`Azure pipelines `_. -We use `Codecov `_ to check and provide stats on how much -of the code base is covered by tests. This tells which lines of code have been -run in the test suite. It does not tell you about the quality of the tests run! -In order to assess that, have a look at the tests we are running - they tell you -the assumptions that we do not want to break within the code base. - -Within the repository, the tests are located in the top-level **tests** -directory. Tests are organized similar to the structure of the repository. -There are several types of tests we employ, this is not an exhaustive list, -but meant to provide a few places to look when you are developing and would -like to check that the code you wrote satisfies the assumptions you think it -should. - -Testing is performed with :code:`pytest` which is available through PyPI. -Checkout the docs on `pytest `_. - - -Compare with known values -^^^^^^^^^^^^^^^^^^^^^^^^^ - -In a simple case, you might know the exact value of what the output should be -and you can :code:`assert` that this is in fact the case. For example, -we setup a 3D :code:`BaseRectangularMesh` and assert that it has 3 dimensions. - -.. code:: python - - from discretize.base import BaseRectangularMesh - import numpy as np - - mesh = BaseRectangularMesh([6, 2, 3]) - - def test_mesh_dimensions(): - assert mesh.dim == 3 - -All functions with the naming convention :code:`test_XXX` -are run. Here we check that the dimensions are correct for the 3D mesh. - -If the value is not an integer, you can be subject to floating point errors, -so :code:`assert ==` might be too harsh. In this case, you will want to use -the ``numpy.testing`` module to check for approximate equals. For instance, - -.. code:: python - - import numpy as np - import discretize - from SimPEG import maps - - def test_map_multiplication(self): - mesh = discretize.TensorMesh([2,3]) - exp_map = maps.ExpMap(mesh) - vert_map = maps.SurjectVertical1D(mesh) - combo = exp_map*vert_map - m = np.arange(3.0) - t_true = np.exp(np.r_[0,0,1,1,2,2.]) - np.testing.assert_allclose(combo * m, t_true) - -These are rather simple examples, more advanced tests might include `solving an -electromagnetic problem numerically and comparing it to an analytical -solution `_ , or -`performing an adjoint test `_ to test :code:`Jvec` and :code:`Jtvec`. - - -.. _order_test: - -Order and Derivative Tests -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Order tests can be used when you are testing differential operators (we are using a second-order, -staggered grid discretization for our operators). For example, testing a 2D -curl operator in `test_operators.py `_ - -.. code:: python - - import numpy as np - import unittest - from discretize.tests import OrderTest - - class TestCurl2D(OrderTest): - name = "Cell Grad 2D - Dirichlet" - meshTypes = ['uniformTensorMesh'] - meshDimension = 2 - meshSizes = [8, 16, 32, 64] - - def getError(self): - # Test function - ex = lambda x, y: np.cos(y) - ey = lambda x, y: np.cos(x) - sol = lambda x, y: -np.sin(x)+np.sin(y) - - sol_curl2d = call2(sol, self.M.gridCC) - Ec = cartE2(self.M, ex, ey) - sol_ana = self.M.edge_curl*self.M.project_face_vector(Ec) - err = np.linalg.norm((sol_curl2d-sol_ana), np.inf) - - return err - - def test_order(self): - self.orderTest() - -Derivative tests are a particular type or :ref:`order_test`, and since they -are used so extensively, SimPEG includes a :code:`check_derivative` method. - -In the case -of testing a derivative, we consider a Taylor expansion of a function about -:math:`x`. For a small perturbation :math:`\Delta x`, - -.. math:: - - f(x + \Delta x) \simeq f(x) + J(x) \Delta x + \mathcal{O}(h^2) - -As :math:`\Delta x` decreases, we expect :math:`\|f(x) - f(x + \Delta x)\|` to -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 `_ - -.. code:: python - - def deriv_test(fdemType, comp): - - # setup simulation, survey - - def fun(x): - return survey.dpred(x), lambda x: sim.Jvec(x0, x) - return tests.check_derivative(fun, x0, num=2, plotIt=False, eps=FLR) - -.. _documentation: - -Documentation -------------- - -Documentation helps others use your code! Please document new contributions. -SimPEG tries to follow the `numpydoc` style of docstrings (check out the -`style guide `_). -SimPEG then uses `sphinx `_ to build the documentation. -When documenting a new class or function, please include a description -(with math if it solves an equation), inputs, outputs and preferably a small example. - -For example: - -.. code:: python - - - class WeightedLeastSquares(BaseComboRegularization): - r"""Weighted least squares measure on model smallness and smoothness. - - L2 regularization with both smallness and smoothness (first order - derivative) contributions. - - Parameters - ---------- - mesh : discretize.base.BaseMesh - The mesh on which the model parameters are defined. This is used - for constructing difference operators for the smoothness terms. - active_cells : array_like of bool or int, optional - List of active cell indices, or a `mesh.n_cells` boolean array - describing active cells. - alpha_s : float, optional - Smallness weight - alpha_x, alpha_y, alpha_z : float or None, optional - First order smoothness weights for the respective dimensions. - `None` implies setting these weights using the `length_scale` - parameters. - alpha_xx, alpha_yy, alpha_zz : float, optional - 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 - A mapping to apply to the model before regularization. - reference_model : array_like, optional - reference_model_in_smooth : bool, optional - Whether to include the reference model in the smoothness terms. - weights : None, array_like, or dict or array_like, optional - User defined weights. It is recommended to interact with weights using - the `get_weights`, `set_weights` functionality. - - Notes - ----- - The function defined here approximates: - - .. math:: - \phi_m(\mathbf{m}) = \alpha_s \| W_s (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - + \alpha_x \| W_x \frac{\partial}{\partial x} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - + \alpha_y \| W_y \frac{\partial}{\partial y} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - + \alpha_z \| W_z \frac{\partial}{\partial z} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 - - Note if the key word argument `reference_model_in_smooth` is False, then mref is not - included in the smoothness contribution. - - If length scales are used to set the smoothness weights, alphas are respectively set internally using: - >>> alpha_x = (length_scale_x * min(mesh.edge_lengths)) ** 2 - """ - - -.. _style: - -Style ------ - -Consistency makes code more readable and easier for collaborators to jump in. -`PEP 8 `_ provides conventions for -coding in Python. SimPEG is currently not `PEP 8 -`_ compliant, but we are working -towards it and would appreciate contributions that do too! Often, most python -text editors can be configured to issue warnings for non-compliant styles. - -SimPEG uses `black `_ version 23.1.0 to autoformat -the code base, and all additions to the code are tested to ensure that they are -compliant with `black`. We recommend installing `pre-commit `_ -hooks that are run on every commit to automatically ensure compliance. - -We also actively update the code base to ensure pep8 compliance by checking with -`flake8 `_ This performs style checks that could lead -towards bugs, performs checks on consistent documentation formatting, or just -identify poor coding practices. This is an ongoing process where we are fixing one -style warning at a time. The fixed style warnings are checked to ensure no new code -goes against an already established style. This test can also be installed locally -using pre-commit hooks, similar to `black` above. - - -.. _licensing: - -Licensing ---------- - -.. image:: https://img.shields.io/badge/license-MIT-blue.svg - :target: https://github.com/simpeg/simpeg/blob/main/LICENSE - :alt: MIT license - -We want SimPEG to be a useful resource for the geoscience community and -believe that following open development practices is the best way to do that. -SimPEG is licensed under the `MIT license -`_ which is allows open -and commercial use and extension of SimPEG. It does not force packages that -use SimPEG to be open source nor does it restrict commercial use. diff --git a/docs/content/getting_started.rst b/docs/content/getting_started.rst deleted file mode 100644 index 907eaf8766..0000000000 --- a/docs/content/getting_started.rst +++ /dev/null @@ -1,16 +0,0 @@ -.. _getting_started: - -=============== -Getting Started -=============== - -Here you'll find instructions on getting up and running with ``SimPEG``. - -.. toctree:: - :maxdepth: 2 - - basic/big_picture - basic/installing - basic/installing_for_developers - basic/practices - basic/contributing diff --git a/docs/content/basic/big_picture.rst b/docs/content/getting_started/big_picture.rst similarity index 92% rename from docs/content/basic/big_picture.rst rename to docs/content/getting_started/big_picture.rst index ee3be2f3a3..f8060f0bb6 100644 --- a/docs/content/basic/big_picture.rst +++ b/docs/content/getting_started/big_picture.rst @@ -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` diff --git a/docs/content/getting_started/contributing/advanced.rst b/docs/content/getting_started/contributing/advanced.rst new file mode 100644 index 0000000000..655a0e4dca --- /dev/null +++ b/docs/content/getting_started/contributing/advanced.rst @@ -0,0 +1,25 @@ +.. _advanced: + +Advanced: Installing Solvers +---------------------------- + +Pardiso_ is a direct solver that can be used for solving large(ish) +linear systems of equations. The provided testing environment should install +the necessary solvers for you. If you wish to modify pymatsolver_ as well +follow the instructions to download and install pymatsolver_. + +.. _Pardiso: https://www.pardiso-project.org + +.. _pymatsolver: https://github.com/simpeg/pymatsolver + +If you open a `Jupyter notebook`_ and are able to run: + +.. code:: python + + from pymatsolver import Pardiso + +.. _Jupyter notebook: https://jupyter.org/ + +then you have succeeded! Otherwise, make an `issue in pymatsolver`_. + +.. _issue in pymatsolver: https://github.com/simpeg/pymatsolver/issues diff --git a/docs/content/getting_started/contributing/code-style.rst b/docs/content/getting_started/contributing/code-style.rst new file mode 100644 index 0000000000..75f235d107 --- /dev/null +++ b/docs/content/getting_started/contributing/code-style.rst @@ -0,0 +1,48 @@ +.. _code-style: + +Code style +========== + +Consistency makes code more readable and easier for collaborators to jump in. +SimPEG uses Black_ to autoformat its codebase, and flake8_ to lint its code and +enforce style rules. Black_ can automatically format SimPEG's codebase to +ensure it complies with Black code style. flake8_ performs style checks, raises +warnings on code that could lead towards bugs, performs checks on consistent +documentation formatting, and identifies poor coding practices. + +.. hint:: + + If you :ref:`configure pre-commit `, it will + automatically run Black and flake8 on every commit. + +One can manually run Black_ and flake8_ anytime. +Run ``black`` on SimPEG directories that contain Python source files: + +.. code:: + + black simpeg examples tutorials tests + +Run ``flake8`` on the whole project with: + +.. code:: + + flake8 + +.. important:: + + Following code style rules can be challenging for new contributors. These + rules are meant to ease the development process, not to generate an obstacle + to contribute. Please, don't hesistate to **ask for help** if your + contribution raises some flake8 errors. And **feel free to push** code that + **don't follow our code style 100%** in :ref:`pull-requests`. Other + developers will be there to help you solve them. + +.. note:: + + SimPEG is currently not `PEP 8 `_ + compliant and is not following all flake8 rules, but we are working towards + it and would appreciate contributions that do too! + +.. _Black: https://black.readthedocs.io/ +.. _flake8: https://flake8.pycqa.org/ +.. _pre-commit: https://pre-commit.com/ diff --git a/docs/content/getting_started/contributing/documentation.rst b/docs/content/getting_started/contributing/documentation.rst new file mode 100644 index 0000000000..8ef695961a --- /dev/null +++ b/docs/content/getting_started/contributing/documentation.rst @@ -0,0 +1,87 @@ +.. _documentation: + +Documentation +------------- + +Documentation helps others use your code! Please document new contributions. +SimPEG tries to follow the `numpydoc` style of docstrings (check out the +`style guide `_). +SimPEG then uses `sphinx `_ to build the documentation. +When documenting a new class or function, please include a description +(with math if it solves an equation), inputs, outputs and preferably a small example. + +For example: + +.. code:: python + + + class WeightedLeastSquares(BaseComboRegularization): + r"""Weighted least squares measure on model smallness and smoothness. + + L2 regularization with both smallness and smoothness (first order + derivative) contributions. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + The mesh on which the model parameters are defined. This is used + for constructing difference operators for the smoothness terms. + active_cells : array_like of bool or int, optional + List of active cell indices, or a `mesh.n_cells` boolean array + describing active cells. + alpha_s : float, optional + Smallness weight + alpha_x, alpha_y, alpha_z : float or None, optional + First order smoothness weights for the respective dimensions. + `None` implies setting these weights using the `length_scale` + parameters. + alpha_xx, alpha_yy, alpha_zz : float, optional + 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 + A mapping to apply to the model before regularization. + reference_model : array_like, optional + reference_model_in_smooth : bool, optional + Whether to include the reference model in the smoothness terms. + weights : None, array_like, or dict or array_like, optional + User defined weights. It is recommended to interact with weights using + the `get_weights`, `set_weights` functionality. + + Notes + ----- + The function defined here approximates: + + .. math:: + \phi_m(\mathbf{m}) = \alpha_s \| W_s (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 + + \alpha_x \| W_x \frac{\partial}{\partial x} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 + + \alpha_y \| W_y \frac{\partial}{\partial y} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 + + \alpha_z \| W_z \frac{\partial}{\partial z} (\mathbf{m} - \mathbf{m_{ref}} ) \|^2 + + Note if the key word argument `reference_model_in_smooth` is False, then mref is not + included in the smoothness contribution. + + If length scales are used to set the smoothness weights, alphas are respectively set internally using: + >>> alpha_x = (length_scale_x * min(mesh.edge_lengths)) ** 2 + """ + + + +Building the documentation +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you would like to see the documentation changes. +In the repo's root directory, enter the following in your terminal. + +.. code:: + + make all + +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. + +.. code:: + + make serve diff --git a/docs/content/getting_started/contributing/index.rst b/docs/content/getting_started/contributing/index.rst new file mode 100644 index 0000000000..cb26c41c54 --- /dev/null +++ b/docs/content/getting_started/contributing/index.rst @@ -0,0 +1,94 @@ +.. _contributing: + +Contributing to SimPEG +======================= + +First of all, we are glad you are here! We welcome contributions and input +from the community. + +In these pages we set some guidelines for contributing to the repositories +hosted in the `SimPEG `_ organization on GitHub. +These repositories are maintained on a volunteer basis. + +Please, be considerate and respectful of others. Remember that everyone in +the SimPEG community must abide by our `Code of +Conduct `_. + +Ask questions +------------- + +If you have a question regarding a specific use of SimPEG, the fastest way +to get a response is by posting on our Discourse discussion forum: +https://simpeg.discourse.group/. Alternatively, if you prefer real-time chat, +you can join our Mattermost Team at +https://mattermost.softwareunderground.org/simpeg. +Please do not create an issue to ask a question. + +.. _issues: + +GitHub Issues +------------- + +Issues are a place for you to suggest enhancements or raise problems you are +having with the package to the developers. Feel free to open new +`Issues in SimPEG's GitHub repository +`_. + +.. _bugs: + +Bugs +~~~~ + +If you found a bug in SimPEG, please report it through a +`new Issue `_. +Please be as descriptive as possible and provide sufficient detail to reproduce the error. +Whenever possible, if you can include a small example that reproduces the error, +this will help us resolve issues faster. + +.. _suggest-enhancements: + +Suggesting enhancements +~~~~~~~~~~~~~~~~~~~~~~~ + +We welcome ideas for improvements on SimPEG! When writing an issue to suggest +an improvement, please + +- use a descriptive title, +- explain where the gap in current functionality is, +- include a pseudocode sketch of the intended functionality + +We will use the issue as a place to discuss and provide feedback. Please +remember that SimPEG is maintained on a volunteer basis. If you suggest an +enhancement, we certainly appreciate if you are also willing to take action +and start a Pull Request! + + +Contributing with code +---------------------- + +We are glad to receive contributions in form of code to SimPEG, weather it +fixes a bug, adds a new feature, improves the documentation or extends our +tests. In the following pages you'll find information on how to get started to +contribute with new code to SimPEG. + +.. toctree:: + :maxdepth: 1 + + working-with-github + setting-up-environment + code-style + documentation + testing + pull-requests + advanced + + +Licensing +~~~~~~~~~ + +All code contributed to SimPEG is licensed under the `MIT license +`_ which allows open +and commercial use and extension of SimPEG. If you did not write +the code yourself, it is your responsibility to ensure that the existing +license is compatible and included in the contributed files or you can obtain +permission from the original author to relicense the code. diff --git a/docs/content/getting_started/contributing/pull-requests.rst b/docs/content/getting_started/contributing/pull-requests.rst new file mode 100644 index 0000000000..75ee05bcdd --- /dev/null +++ b/docs/content/getting_started/contributing/pull-requests.rst @@ -0,0 +1,54 @@ +.. _pull-requests: + +Pull Requests +============= + +We welcome contributions to SimPEG in the form of pull requests (PR). + +Stages of a pull request +------------------------ + +When first creating a pull request (PR), try to make your suggested changes as +tightly scoped as possible (try to solve one problem at a time). The fewer +changes you make, the faster your branch will be merged! + +If your pull request is not ready for final review, but you still want feedback +on your current coding process please mark it as a draft pull request. Once you +feel the pull request is ready for final review, you can convert the draft PR to +an open PR by selecting the ``Ready for review`` button at the bottom of the page. + +Once a pull request is in ``open`` status and you are ready for review, please +ping the simpeg developers in a github comment ``@simpeg/simpeg-developers`` to +request a review. At minimum for a PR to be eligible to merge, we look for + +- 100% (or as close as possible) difference testing. Meaning any new code is + completely tested. +- All tests are passing. +- All reviewer comments (if any) have been addressed. +- A developer approves the PR. + +After all these steps are satisfied, a ``@simpeg/simpeg-admin`` will merge your +pull request into the main branch (feel free to ping one of us on Github). + +This being said, all SimPEG developers and admins are essentially volunteers +providing their time for the benefit of the community. This does mean that +it might take some time for us to get your PR. + +Merging a Pull Request +---------------------- + +The ``@simpeg/simpeg-admin`` will merge a Pull Request to the `main` branch +using the `Squash and Merge +`_ +strategy: all commits made to the PR branch will be _squashed_ to a single +commit that will be added to `main`. + +SimPEG admins will ensure that the commit message is descriptive and +comprehensive. Contributors can help by providing a descriptive and +comprehensive PR description of the changes that were applied and the reasons +behind them. This will be greatly appreciated. + +Admins will mention other authors that made significant contributions to +the PR in the commit message, following GitHub's approach for `Creating +co-authored commits +`_. diff --git a/docs/content/getting_started/contributing/setting-up-environment.rst b/docs/content/getting_started/contributing/setting-up-environment.rst new file mode 100644 index 0000000000..d775983dff --- /dev/null +++ b/docs/content/getting_started/contributing/setting-up-environment.rst @@ -0,0 +1,164 @@ +.. _setting-up-environment: + +Setting up your environment +=========================== + +Install Python +-------------- + +First you will need to install Python. You can find instructions in +:ref:`installing_python`. We highly encourage to install Anaconda_ or +Miniforge_. + +Create environment +------------------ + +To get started developing SimPEG we recommend setting up an environment using +the ``conda`` 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. + +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 +repository ` and run: + +.. code:: + + conda env create -f environment_test.yml + +.. 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. + + +Once the environment is successfully created, you can *activate* it with + +.. code:: + + conda activate simpeg-test + + +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, +executed from the base level directory would be: + +.. code:: + + pip install --no-deps -e . + +This is called an editable mode install (`-e`). This will make a symbolic link +for you to the working ``simpeg`` directory for that Python environment to use +and you can then make use of any changes you have made to the repository +without re-installing it. This command (`--no-deps`) also ensures pip won't +unintentionally re-install a package that was previously installed with conda. +This practice also allows you to uninstall SimPEG if so desired: + +.. code:: + + pip uninstall SimPEG + +.. note:: + + We no longer recommend modifying your Python path environment variable as + a way to install SimPEG for developers. + +.. _Anaconda: 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 +example, i.e. + +.. code:: + + python 02-linear_inversion/plot_inv_1_inversion_lsq.py + +or you can download and run the :ref:`notebook from the docs +`. + +.. image:: ../../tutorials/02-linear_inversion/images/sphx_glr_plot_inv_1_inversion_lsq_003.png + +You are now set up to SimPEG! + +.. note:: + + If all is not well, please submit an issue_ and `change this file`_! + +.. _issue: https://github.com/simpeg/simpeg/issues +.. _change this file: https://github.com/simpeg/simpeg/edit/main/docs/content/getting_started/contributing/setting-up-environment.rst + + +.. _configure-pre-commit: + +Configure pre-commit (optional) +------------------------------- + +We recommend using pre-commit_ to ensure that your new code follows the code +style of SimPEG. pre-commit will run Black_ and flake8_ before any commit you +make. To configure it, you need to navigate to your cloned SimPEG repo and run: + +.. code:: + + pre-commit install + +.. note:: + + Using ``pre-commit`` is recommended, but not necessary. You can still + manually run Black_ and flake8_. See our :ref:`code-style` page for more + details. + +If for some reason you want to stop using ``pre-commit`` on SimPEG, you can +permanently configure it to stop running automatically with: + +.. code:: + + pre-commit uninstall + +Alternatively, you can temporarily bypass ``pre-commit`` when committing some changes by running: + +.. code:: + + git commit --no-verify + +This is specially useful if the checks run by ``pre-commit`` are failing, but +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-test`` 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_test.yml diff --git a/docs/content/getting_started/contributing/testing.rst b/docs/content/getting_started/contributing/testing.rst new file mode 100644 index 0000000000..282bc90997 --- /dev/null +++ b/docs/content/getting_started/contributing/testing.rst @@ -0,0 +1,146 @@ +.. _testing: + +Testing +======= + +.. image:: https://dev.azure.com/simpeg/simpeg/_apis/build/status/simpeg.simpeg?branchName=main + :target: https://dev.azure.com/simpeg/simpeg/_build/latest?definitionId=2&branchName=main + :alt: Azure pipeline + +.. image:: https://codecov.io/gh/simpeg/simpeg/branch/main/graph/badge.svg + :target: https://codecov.io/gh/simpeg/simpeg + :alt: Coverage status + +On each update, SimPEG is tested using the continuous integration service +`Azure pipelines `_. +We use `Codecov `_ to check and provide stats on how much +of the code base is covered by tests. This tells which lines of code have been +run in the test suite. It does not tell you about the quality of the tests run! +In order to assess that, have a look at the tests we are running - they tell you +the assumptions that we do not want to break within the code base. + +Within the repository, the tests are located in the top-level **tests** +directory. Tests are organized similar to the structure of the repository. +There are several types of tests we employ, this is not an exhaustive list, +but meant to provide a few places to look when you are developing and would +like to check that the code you wrote satisfies the assumptions you think it +should. + +Testing is performed with :code:`pytest` which is available through PyPI. +Checkout the docs on `pytest `_. + + +Compare with known values +------------------------- + +In a simple case, you might know the exact value of what the output should be +and you can :code:`assert` that this is in fact the case. For example, +we setup a 3D :code:`BaseRectangularMesh` and assert that it has 3 dimensions. + +.. code:: python + + from discretize.base import BaseRectangularMesh + import numpy as np + + mesh = BaseRectangularMesh([6, 2, 3]) + + def test_mesh_dimensions(): + assert mesh.dim == 3 + +All functions with the naming convention :code:`test_XXX` +are run. Here we check that the dimensions are correct for the 3D mesh. + +If the value is not an integer, you can be subject to floating point errors, +so :code:`assert ==` might be too harsh. In this case, you will want to use +the ``numpy.testing`` module to check for approximate equals. For instance, + +.. code:: python + + import numpy as np + import discretize + from simpeg import maps + + def test_map_multiplication(self): + mesh = discretize.TensorMesh([2,3]) + exp_map = maps.ExpMap(mesh) + vert_map = maps.SurjectVertical1D(mesh) + combo = exp_map*vert_map + m = np.arange(3.0) + t_true = np.exp(np.r_[0,0,1,1,2,2.]) + np.testing.assert_allclose(combo * m, t_true) + +These are rather simple examples, more advanced tests might include `solving an +electromagnetic problem numerically and comparing it to an analytical solution +`_ +, or `performing an adjoint test +`_ +to test :code:`Jvec` and :code:`Jtvec`. + + +.. _order_test: + +Order and Derivative Tests +-------------------------- + +Order tests can be used when you are testing differential operators (we are +using a second-order, staggered grid discretization for our operators). For +example, testing a 2D curl operator in `test_operators.py +`_ + +.. code:: python + + import numpy as np + import unittest + from discretize.tests import OrderTest + + class TestCurl2D(OrderTest): + name = "Cell Grad 2D - Dirichlet" + meshTypes = ['uniformTensorMesh'] + meshDimension = 2 + meshSizes = [8, 16, 32, 64] + + def getError(self): + # Test function + ex = lambda x, y: np.cos(y) + ey = lambda x, y: np.cos(x) + sol = lambda x, y: -np.sin(x)+np.sin(y) + + sol_curl2d = call2(sol, self.M.gridCC) + Ec = cartE2(self.M, ex, ey) + sol_ana = self.M.edge_curl*self.M.project_face_vector(Ec) + err = np.linalg.norm((sol_curl2d-sol_ana), np.inf) + + return err + + def test_order(self): + self.orderTest() + +Derivative tests are a particular type of :ref:`order_test`, and since they +are used so extensively, discretize includes a :code:`check_derivative` method. + +In the case +of testing a derivative, we consider a Taylor expansion of a function about +:math:`x`. For a small perturbation :math:`\Delta x`, + +.. math:: + + f(x + \Delta x) \simeq f(x) + J(x) \Delta x + \mathcal{O}(h^2) + +As :math:`\Delta x` decreases, we expect :math:`\|f(x) - f(x + \Delta x)\|` to +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 `_ + +.. code:: python + + def deriv_test(fdemType, comp): + + # setup simulation, survey + + def fun(x): + return survey.dpred(x), lambda x: sim.Jvec(x0, x) + return tests.check_derivative(fun, x0, num=2, plotIt=False, eps=FLR) diff --git a/docs/content/getting_started/contributing/working-with-github.rst b/docs/content/getting_started/contributing/working-with-github.rst new file mode 100644 index 0000000000..cb944eadd0 --- /dev/null +++ b/docs/content/getting_started/contributing/working-with-github.rst @@ -0,0 +1,50 @@ +.. _working-with-github: + +Working with Git and GitHub +--------------------------- + +.. image:: https://github.githubassets.com/images/modules/logos_page/Octocat.png + :align: right + :width: 100 + :target: https://github.com + + +To keep track of your code changes and contribute back to SimPEG, you will +need a Github_ account. Then fork the `SimPEG repository +`_ to your local account. +(`How to fork a repo `_). + + +Next, clone your fork to your computer so that you have a local copy. We recommend setting up a +directory in your home directory to put your version-controlled repositories (e.g. called :code:`git`). +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 + +2. Using a desktop client such as SourceTree_ or GitKraken_. + + .. image:: ../../../images/sourceTreeSimPEG.png + :align: center + :width: 400 + :target: https://www.sourcetreeapp.com/ + + If this is your first time managing a github_ repository through SourceTree_, + it is also handy to set up the remote account so it remembers your github_ + user name and password + + .. image:: ../../../images/sourceTreeRemote.png + :align: center + :width: 400 + +For managing your copy of SimPEG and contributing back to the main +repository, have a look at the article: `A successful git branching model +`_ + +.. _Github: https://github.com +.. _SourceTree: https://www.sourcetreeapp.com/ +.. _GitKraken: https://www.gitkraken.com/ + + + diff --git a/docs/content/getting_started/index.rst b/docs/content/getting_started/index.rst new file mode 100644 index 0000000000..dfef8b8d96 --- /dev/null +++ b/docs/content/getting_started/index.rst @@ -0,0 +1,14 @@ +.. _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/basic/installing.rst b/docs/content/getting_started/installing.rst similarity index 54% rename from docs/content/basic/installing.rst rename to docs/content/getting_started/installing.rst index 0044b4d981..06724787e7 100644 --- a/docs/content/basic/installing.rst +++ b/docs/content/getting_started/installing.rst @@ -9,16 +9,28 @@ Getting Started with SimPEG Prerequisite: Installing Python =============================== -We highly recommend installing python using -`Anaconda `_ (or the alternative -`Mambaforge `_). -It installs `python `_, -`Jupyter `_ and other core -python libraries for scientific computing. +SimPEG is written in Python_! +We highly recommend installing it using Anaconda_ (or the alternative Miniforge_). +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 `_. -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 `_. +.. 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 +.. _Miniforge: https://github.com/conda-forge/miniforge .. _installing_simpeg: @@ -30,21 +42,29 @@ Conda Forge ----------- SimPEG is available through `conda-forge` and you can install is using the -`conda package manager `_ that comes with the Anaconda -distribution: +`conda package manager `_ that comes with the Anaconda_ +or Miniforge_ distributions: .. code:: conda install SimPEG --channel conda-forge -Installing through `conda`/`mamba` is our recommended method of installation. +Installing through `conda` 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``). + + 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 ---- @@ -88,11 +108,11 @@ 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`. +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 -`slack `_. +`Mattermost `_. Useful Links ============ @@ -108,8 +128,8 @@ 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 `_ +* `Python Wiki `_ Lists packages and resources for scientific computing in Python +* `Jupyter `_ Numpy and Matlab ---------------- @@ -128,9 +148,9 @@ Editing Python -------------- There are numerous ways to edit and test Python (see -`PythonWiki `_ for an overview) and +`PythonWiki `_ for an overview) and in our group at least the following options are being used: -* `Jupyter `_ +* `Jupyter `_ * `Sublime `_ * `PyCharm `_ diff --git a/docs/content/release/0.14.0-notes.rst b/docs/content/release/0.14.0-notes.rst index dfb8949f38..1c4aa36702 100644 --- a/docs/content/release/0.14.0-notes.rst +++ b/docs/content/release/0.14.0-notes.rst @@ -92,7 +92,7 @@ easier to interface with external codes for inversion purposes, as all that is needed to be defined to use a ``Simulation`` for an ``InvProblem``, is ``sim.dpred``, ``sim.Jvec`` and ``sim.Jtvec``. -Please see the documentation for the :class:`SimPEG.simulation.BaseSimulation` class +Please see the documentation for the :class:`simpeg.simulation.BaseSimulation` class as well as the individual methods' ``Simulation``-s, for a detailed description of arguments, but largely it accepts the same arguments as the ``Problem`` class, but now also requires a ``Survey`` to be set. @@ -105,9 +105,9 @@ data, we are concerned with the data. Thus we would like to enforce this importa by making data live in a dedicated ``Data`` class. This ``Data`` class can act like a smart dictionary to grab data associated with a specific source, receiver combination. More importantly, this ``Data`` class is where we store information related to observed -data and its errors. This class started in the ``SimPEG.Survey`` module, but has -now been moved into its own new module ``SimPEG.data``. See the documentation for -the :class:`SimPEG.data.Data` for all of the details. +data and its errors. This class started in the ``simpeg.Survey`` module, but has +now been moved into its own new module ``simpeg.data``. See the documentation for +the :class:`simpeg.data.Data` for all of the details. Previously, @@ -179,7 +179,7 @@ the definition of the classic data misfit measure. The ``Simulation`` class handles the forward operation, :math:`\mathcal{F}`, and the ``Data`` class handles the noise, :math:`\textbf{W}_d=diag(\frac{1}{\sigma_i})`, and the observed data, :math:`\vec{d}_{obs}`. See the documentation for the -:class:`SimPEG.data_misfit.L2DataMisfit` for all of the details. +:class:`simpeg.data_misfit.L2DataMisfit` for all of the details. Previously, @@ -210,18 +210,18 @@ This feature is experimental at the moment and can be toggled on like so, .. code-block:: python - import SimPEG.dask + import simpeg.dask which will then enable parallel operations for a few modules. It will specifically replace these functions with ``dask`` versions, -* ``SimPEG.potential_fields.BasePFSimulation.linear_operator`` -* ``SimPEG.potential_fields.magnetics.Simulation3DIntegral.getJtJdiag`` -* ``SimPEG.potential_fields.gravity.Simulation3DIntegral.getJtJdiag`` -* ``SimPEG.electromagnetics.static.resistivity.simulation.BaseDCSimulation.getJ`` -* ``SimPEG.electromagnetics.static.resistivity.simulation.BaseDCSimulation.getJtJdiag`` -* ``SimPEG.electromagnetics.static.induced_polarization.simulation.BaseDCSimulation.getJ`` -* ``SimPEG.electromagnetics.static.induced_polarization.simulation.BaseDCSimulation.getJtJdiag`` +* ``simpeg.potential_fields.BasePFSimulation.linear_operator`` +* ``simpeg.potential_fields.magnetics.Simulation3DIntegral.getJtJdiag`` +* ``simpeg.potential_fields.gravity.Simulation3DIntegral.getJtJdiag`` +* ``simpeg.electromagnetics.static.resistivity.simulation.BaseDCSimulation.getJ`` +* ``simpeg.electromagnetics.static.resistivity.simulation.BaseDCSimulation.getJtJdiag`` +* ``simpeg.electromagnetics.static.induced_polarization.simulation.BaseDCSimulation.getJ`` +* ``simpeg.electromagnetics.static.induced_polarization.simulation.BaseDCSimulation.getJtJdiag`` Changelog ========= @@ -317,19 +317,19 @@ moved around and been renamed. There are now two separate modules within ``poten ``gravity`` and ``magnetics``. All of the classes in ``PF.BaseGrav`` have been moved to ``potential_fields.gravity``, and the classes in ``PF.BaseMag`` have been moved to ``potential_fields.magnetics``. The ``Map``-s that were within them have -been deprecated and can instead be found in ``SimPEG.maps``. +been deprecated and can instead be found in ``simpeg.maps``. The option of a ``coordinate_system`` for the magnetics simulation is no longer -valid and will throw an ``AttributeError``. Instead use the :class:`SimPEG.maps.SphericalSystem`. +valid and will throw an ``AttributeError``. Instead use the :class:`simpeg.maps.SphericalSystem`. Improvements and Additions to ``resistivity`` --------------------------------------------- -We have made a few improvements to the ``SimPEG.electromagnetics.static.resistivity`` +We have made a few improvements to the ``simpeg.electromagnetics.static.resistivity`` that were motivated by our work under the Geoscientists Without Borders project. One is that we now have a 1D layered Earth simulation class, -:class:`SimPEG.electromagnetics.static.resistivity.simulation_1d.Simulation1DLayers`, +:class:`simpeg.electromagnetics.static.resistivity.simulation_1d.Simulation1DLayers`, that can be used to invert resistivity sounding data for conductivity and/or thicknesses of a set number of layers. diff --git a/docs/content/release/0.14.1-notes.rst b/docs/content/release/0.14.1-notes.rst index da4cb3dc61..a7dd5b1463 100644 --- a/docs/content/release/0.14.1-notes.rst +++ b/docs/content/release/0.14.1-notes.rst @@ -33,7 +33,7 @@ Because of the improvement to the ``resitivity.Fields2D`` object, the previous Resistivity Dipole source and receiver -------------------------------------- -For the ``Dipole`` receiver in ``SimPEG.electromagnetics.static.resistivity.receivers``, +For the ``Dipole`` receiver in ``simpeg.electromagnetics.static.resistivity.receivers``, the ``locationsM`` and ``locationsN`` parameters are deprecated. They are now called ``locations_m`` and ``locations_n``. There are now two ways to create a ``Dipole`` receiver: @@ -47,7 +47,7 @@ or rx = resistivity.receivers.Dipole(locations=(locations_m, locations_n)) -Similarly, for the, ``Dipole`` source in ``SimPEG.electromagnetics.static.resistivity.sources``, +Similarly, for the, ``Dipole`` source in ``simpeg.electromagnetics.static.resistivity.sources``, the ``locationA`` and ``locationB`` parameters are deprecated. They are now called ``location_a`` and ``location_b``. There are now also two ways to create a ``Dipole`` source: diff --git a/docs/content/release/0.14.2-notes.rst b/docs/content/release/0.14.2-notes.rst index 77200797af..ba4dcc8fa8 100644 --- a/docs/content/release/0.14.2-notes.rst +++ b/docs/content/release/0.14.2-notes.rst @@ -13,14 +13,14 @@ New Things ========== @ikding was kind enough to add in more predefined EM waveforms: the -``SimPEG.electromagnetics.time_domain.source.TriangularWaveform`` and -``SimPEG.electromagnetics.time_domain.source.HalfSineWaveform``. +``simpeg.electromagnetics.time_domain.source.TriangularWaveform`` and +``simpeg.electromagnetics.time_domain.source.HalfSineWaveform``. Changes ======= -The defaults solvers located in ``SimPEG.utils.solver_utils`` will now accept arbitrary +The defaults solvers located in ``simpeg.utils.solver_utils`` will now accept arbitrary unused kwargs when wrapping a solver. It will warn the user that a kwarg will be unused. This should allow us to set method specific solver options that wont break the default solvers when they are not used. @@ -32,7 +32,7 @@ Bugs Squashed We unfortunately allowed a small bug in the ``drape_electrodes_to_topography`` function in the last release. This should now work properly for 3D ``discretize.TensorMesh`` again. -There was also a syntax error in ``SimPEG.electromagnetics.frequency_domain.sources.RawVec_m`` +There was also a syntax error in ``simpeg.electromagnetics.frequency_domain.sources.RawVec_m`` that was identified and fixed. There were also typos fixed, type checks were converted to ``isinstance`` checks instead, diff --git a/docs/content/release/0.14.3-notes.rst b/docs/content/release/0.14.3-notes.rst index d095731eae..7a0f34efee 100644 --- a/docs/content/release/0.14.3-notes.rst +++ b/docs/content/release/0.14.3-notes.rst @@ -14,12 +14,12 @@ New Things ========== There is now a file reader for UBC formatted gravity gradient data files. -The class ``SimPEG.electromagnetics.frequency_domain.sources.LineCurrent`` now accepts +The class ``simpeg.electromagnetics.frequency_domain.sources.LineCurrent`` now accepts a keyword argument option ``current`` to set the strength of the ``LineCurrent``. -This is similar behavoir to ```SimPEG.electromagnetics.frequency_domain.sources.CircularLoop``. +This is similar behavoir to ```simpeg.electromagnetics.frequency_domain.sources.CircularLoop``. There are now a few more default options that are setting for the plotting routines -in ``SimPEG.utils.plot_utils.plot2Ddata``. We also now specifically overriding these default +in ``simpeg.utils.plot_utils.plot2Ddata``. We also now specifically overriding these default options for ``norm``, ``levels``, and ``zorder`` for the ``contourOpts`` dictionary input. There is also now a ``streamplotOpts`` dictionary input as well to handle keyword arguments to the stream plot. @@ -35,10 +35,10 @@ SimPEG's coordinate system convention. SimPEG uses a right handed coordinate sys with z being positive upwards, whereas in the UBC file, gravity was defined as positive downwards. -The user provided mapping for ``SimPEG.simulation.LinearSimulation`` was not being +The user provided mapping for ``simpeg.simulation.LinearSimulation`` was not being properly taken into account, and has now been fixed. -We have re-anabled the mu inverse testing of ``SimPEG.electromagnetics.frequency_domain`` +We have re-anabled the mu inverse testing of ``simpeg.electromagnetics.frequency_domain`` classes, ensuring those tests also pass. diff --git a/docs/content/release/0.15.0-notes.rst b/docs/content/release/0.15.0-notes.rst index 0fdc3aab50..8588c79dcb 100644 --- a/docs/content/release/0.15.0-notes.rst +++ b/docs/content/release/0.15.0-notes.rst @@ -13,7 +13,7 @@ Highlights ========== * PGI formulation by @thast * Refactoring of ``static_utils.py`` -* :func:`SimPEG.electromagnetics.frequency_domain.sources.LineCurrent` source +* :func:`simpeg.electromagnetics.frequency_domain.sources.LineCurrent` source * Updates for DC Boundary Conditions * Bug fixes! @@ -44,7 +44,7 @@ Boundary Conditions =================== With the most recent ``0.7.0`` version of ``discretize``, we have updated the DC boundary conditions to flexibly support more mesh types. As part of this, we have also changed -the default boundary condition for the simulations in ``SimPEG.electromagnetics.static.resistivity`` +the default boundary condition for the simulations in ``simpeg.electromagnetics.static.resistivity`` to be the ``Robin`` type condition (equivalent to the ``Mixed`` option previously). This enables both Pole-Pole solutions for the nodal formulation (something that was not possible previously). We also caught a error in the 2D DC resistivity simulations' @@ -52,7 +52,7 @@ previous ``Mixed`` boundary condition option that caused incorrect results. Static Utils ============ -The ``static_utils`` module within ``SimPEG.electromagnetics.static.resistivity`` has +The ``static_utils`` module within ``simpeg.electromagnetics.static.resistivity`` has been given a pass through with many internal changes to make it more flexible in its handling of dc resitivity surveys. Wenner-type arrays should be better supported by the psuedosection plotting utility. It now also includes a 3D psuedosection plotting diff --git a/docs/content/release/0.18.0-notes.rst b/docs/content/release/0.18.0-notes.rst index 751a0d149b..e0c7313a20 100644 --- a/docs/content/release/0.18.0-notes.rst +++ b/docs/content/release/0.18.0-notes.rst @@ -46,7 +46,7 @@ We have deprecated the `Tikhonov` and `Simple` regularizers as it was confusing average user to choose one or the other. They were actually doing very much the same thing, unintentionally, just with slightly different ways of setting parameters for the weighting of the different regularization components. These have now been changed into -a single ``SimPEG.regularization.WeightedLeastSquares`` class. +a single ``simpeg.regularization.WeightedLeastSquares`` class. As a part of this, we have also overhauled the internal workings of regularizations to give a more intuitive user experience to setting different weights for the model diff --git a/docs/content/release/0.19.0-notes.rst b/docs/content/release/0.19.0-notes.rst index db2a6c4fa2..765ad0c709 100644 --- a/docs/content/release/0.19.0-notes.rst +++ b/docs/content/release/0.19.0-notes.rst @@ -31,7 +31,7 @@ sweet on the continuous integration service. ``MetaSimulation`` ------------------ -`SimPEG` contains a new simulation class called ``SimPEG.meta.MetaSimulation``. This experimental +`SimPEG` contains a new simulation class called ``simpeg.meta.MetaSimulation``. This experimental simulation essentially wraps together many simulations into a single simulation. Several common problems can fit into this simulation of simulations model, including tiled (domain decomposition) simulations, time-lapse simulations, or laterally constrained simulations. This approach also is @@ -58,14 +58,14 @@ of a pull request that you should follow. Directive updates ----------------- There are a few new options for determining an initial trade-off parameter for the -regularization functions in inversions. You can now use ``SimPEG.directives.BetaEstimateMaxDerivative`` +regularization functions in inversions. You can now use ``simpeg.directives.BetaEstimateMaxDerivative`` -There are also a few more nobs to turn on ``SimPEG.directives.UpdateSensitivityWeights``, +There are also a few more nobs to turn on ``simpeg.directives.UpdateSensitivityWeights``, controlling how the weights are thresholded and normalized. JointTotalVariation extension ----------------------------- -``SimPEG.regularization.JointTotalVariation`` now supports an arbitrary number of physical +``simpeg.regularization.JointTotalVariation`` now supports an arbitrary number of physical properties models (instead of just two). Many bug fixes diff --git a/docs/content/release/0.20.0-notes.rst b/docs/content/release/0.20.0-notes.rst new file mode 100644 index 0000000000..7eab92d463 --- /dev/null +++ b/docs/content/release/0.20.0-notes.rst @@ -0,0 +1,124 @@ +.. _0.20.0_notes: + +=========================== +SimPEG 0.20.0 Release Notes +=========================== + +August 9th, 2023 + +This minor release contains many bugfixes and additions to the code base, including improvements to +documentation for regularization. + +.. contents:: Highlights + :depth: 2 + + +Updates +======= + +Spontaneous Potential +--------------------- +The spontaneous (self) potential module has finally been re-implemented into the +simulation framework of simpeg 0.14.0. Check out the new module at +:py:mod:`simpeg.electromagnetics.static.spontaneous_potential`. + +MVI inversions +-------------- +There are now two new support regularization functions relevant to vector inversions in +the cartesian domain. an amplitude based regularization, and a direction based regularization, +which both support reference models. + +FDEM +---- +The frequency domain simulations now support forward modeling with electrical permittivity as a property + +Flake8 improvements +------------------- +We continue to add improvements to the internal code structures to be more in line with +flake8 practices. + +MetaSimulation +-------------- +There is now a multiprocessing version of the `MetaSimulation` class for interested users +to experiment with. + +Documentation +------------- +We've made substantial additions to the regularization documentation. + +We have also updated the getting started guides to represent current recommend +practices for installing, developing with, and contributing to SimPEG. + +Others +------ +We've added support for taking derivatives of anisotropic models, which also fixed derivatives of +properties on tetrahedral and curvilinear meshes. + +Invertible properties may now be not required for certain simulations (e.g. permittivity in +a FDEM simulation). + +Last but not least, there are of course many bugfixes! + +Contributors +============ +This is a combination of contributors and reviewers who've made contributions towards +this release (in no particular order). + +* `@jcapriot `__ +* `@santisoler `__ +* `@domfournier `__ +* `@dccowan `__ +* `@thibaut-kobold `__ +* `@nwilliams-kobold `__ +* `@lheagy `__ +* `@yanang007* `__ +* `@andieie* `__ + +Pull requests +============= +* `#1103 `__: Amplitude regularization +* `#1195 `__: Refactor PGI_BetaAlphaSchedule directive +* `#1201 `__: Multiprocessing MetaSimulation +* `#1211 `__: Sp reimplement +* `#1212 `__: Add a linearity property to mappings +* `#1213 `__: Pydata sphinx theme updates +* `#1214 `__: Cross reference vector +* `#1215 `__: Meta/meta patches +* `#1216 `__: Tiny typo triggers error when displaying error output string +* `#1217 `__: Update index.rst +* `#1224 `__: Replace deprecated numpy type aliases with builtin types +* `#1225 `__: Regularization docstrings +* `#1229 `__: Generalize __add__ for any ComboObjectiveFunction +* `#1230 `__: Discretize 0.9.0updates +* `#1231 `__: Fix IP simulation / inversion with SimPEG.dask +* `#1234 `__: General Doc cleanup +* `#1235 `__: conditionally allow invertible property to also be optional +* `#1236 `__: FDEM permittivity +* `#1237 `__: Anisotropy derivative support +* `#1238 `__: Move flake8 ignored rules to `.flake8` and rename Makefile targets +* `#1239 `__: Add flake8 to pre-commit configuration +* `#1240 `__: Merge docs for developers into a Contributing section +* `#1241 `__: Refactor `BaseObjectiveFunction` and `ComboObjectiveFunction` +* `#1242 `__: Fix flake `E711` error: wrong comparison with None +* `#1243 `__: Fix flake `E731` error: assign lambda functions +* `#1244 `__: Fix flake `F403` and `F405` errors: don't use star imports +* `#1245 `__: Fix `F522`, `F523`, `F524` flake errors: format calls +* `#1246 `__: Fix `F541` flake error: f-string without placeholder +* `#1247 `__: Simplify CONTRIBUTING.md +* `#1248 `__: Fix F811 flake error: remove redefinitions +* `#1249 `__: Add more hints about pre-commit in documentation +* `#1250 `__: Rename "basic" directory in docs to "getting_started" +* `#1251 `__: Test patches +* `#1252 `__: Fix W291 and W293 flake errors: white spaces +* `#1253 `__: Always calculate gzz if needed +* `#1254 `__: Fix B028 flake error: non-explicit stacklevel +* `#1256 `__: Make units of gravity simulations more explicit +* `#1257 `__: unpack the data misfits for plotting tikhonov curves +* `#1258 `__: Update pull_request_template.md +* `#1260 `__: Optionally import utm +* `#1261 `__: Set storage type of pf sensitivity matrix +* `#1262 `__: final unresolved comments for PR #1225 +* `#1264 `__: Fix sparse inversion example: remove beta schedule +* `#1267 `__: Add building docs and serving them to documentation +* `#1274 `__: use setuptools_scm to track version +* `#1275 `__: 0.20.0 staging \ No newline at end of file diff --git a/docs/content/release/0.21.0-notes.rst b/docs/content/release/0.21.0-notes.rst new file mode 100644 index 0000000000..f1fd561ba0 --- /dev/null +++ b/docs/content/release/0.21.0-notes.rst @@ -0,0 +1,276 @@ +.. _0.21.0_notes: + +=========================== +SimPEG 0.21.0 Release Notes +=========================== + +April 8th, 2024 + +.. contents:: Highlights + :depth: 3 + +Updates +======= + +New features +------------ + +Gravity simulation using Choclo +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now we can use a faster and more memory efficient implementation of the gravity +simulation ``simpeg.potential_fields.gravity.Simulation3DIntegral``, making use +of Choclo and Numba. To make use of this functionality you will need to +`install Choclo `__ in +addition to ``SimPEG``. + +See https://github.com/simpeg/simpeg/pull/1285. + +Use Dask with MetaSimulation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A new ``simpeg.meta.DaskMetaSimulation`` class has been added that allows to +use Dask with ``simpeg.meta.MetaSimulations``. + +See https://github.com/simpeg/simpeg/pull/1199. + +Rotated Gradients +~~~~~~~~~~~~~~~~~ + +Added a new ``simpeg.regularization.SmoothnessFullGradient`` regularization +class that allows to regularize first order smoothness along any arbitrary +direction, enabling anisotropic weighting. This regularization also works for +a ``SimplexMesh``. + +See https://github.com/simpeg/simpeg/pull/1167. + +Logistic Sigmoid Map +~~~~~~~~~~~~~~~~~~~~ + +New ``simpeg.map.LogisticSigmoidMap`` mapping class that computes the logistic +sigmoid of the model parameters. This is an alternative method to incorporate +upper and lower bounds on model parameters. + +See https://github.com/simpeg/simpeg/pull/1352. + +Create Jacobian matrix in NSEM and FDEM simulations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The frequency domain electromagnetic simulations (including natural source) now +support creating and storing the Jacobian matrix. You can access it by using +the ``getJ`` method. + +See https://github.com/simpeg/simpeg/pull/1276. + + +Documentation +------------- + +This new release includes major improvements in documentation pages: more +detailed docstrings of classes and methods, the addition of directive classes +to the API reference, improvements to the contributing guide, among corrections +and fixes. + + +Breaking changes +---------------- + +Removal of deprecated bits +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Several deprecated bits of code has been removed in this release. From old +classes, methods and properties that were marked for deprecation a few releases +back. These removals simplify the SimPEG API and cleans up the codebase. + +Remove factor of half in data misfits and regularizations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Simplify the definition of data misfit and regularization terms by removing the +leading factor of one half from these functions. This change makes it easier to +interpret the resulting values of these objective functions, while +avoiding confusions with their definition. + +See https://github.com/simpeg/simpeg/pull/1326. + + +Bugfixes +-------- + +A few bugs have been fixed: + +- Fix issue with lengthscales in coterminal angle calculations by + `@domfournier `__ in https://github.com/simpeg/simpeg/pull/1299 +- ISSUE-1341: Set parent of objective functions by `@domfournier `__ in + https://github.com/simpeg/simpeg/pull/1342 +- Ravel instead of flatten by `@thibaut-kobold `__ in + https://github.com/simpeg/simpeg/pull/1343 +- Fix implementation of coterminal function by `@domfournier `__ in + https://github.com/simpeg/simpeg/pull/1334 +- Simpeg vector update by `@johnweis0480 `__ in + https://github.com/simpeg/simpeg/pull/1329 + + +Contributors +============ + +This is a combination of contributors and reviewers who've made contributions +towards this release (in no particular order). + +* `@ckohnke `__ +* `@dccowan `__ +* `@domfournier `__ +* `@ghwilliams `__ +* `@jcapriot `__ +* `@JKutt `__ +* `@johnweis0480 `__ +* `@lheagy `__ +* `@mplough-kobold `__ +* `@santisoler `__ +* `@thibaut-kobold `__ +* `@YingHuuu `__ + +We would like to highlight the contributions made by new contributors: + +- `@mplough-kobold `__ made their first + contribution in https://github.com/simpeg/simpeg/pull/1282 +- `@ghwilliams `__ made their first contribution + in https://github.com/simpeg/simpeg/pull/1292 +- `@johnweis0480 `__ made their first + contribution in https://github.com/simpeg/simpeg/pull/1329 +- `@ckohnke `__ made their first contribution in + https://github.com/simpeg/simpeg/pull/1352 +- `@YingHuuu `__ made their first contribution in + https://github.com/simpeg/simpeg/pull/1344 + + +Pull Requests +============= + +- Add 0.20.0 release notes to toc by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1277 +- add plausible analytics to simpeg docs by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1279 +- Refresh links in documentation by `@mplough-kobold `__ in + https://github.com/simpeg/simpeg/pull/1282 +- Run pytest on Azure with increased verbosity by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1287 - Allow to use random seed in make_synthetic_data by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1286 +- pgi doc by `@thibaut-kobold `__ in + https://github.com/simpeg/simpeg/pull/1291 +- Fix deprecation warning for gradientType in SparseSmoothness by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1284 +- Gravity simulation with Choclo as engine by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1285 +- Fix minor flake8 warning by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1307 +- ISSUE-1298: Use normal distributed noise in example. by `@domfournier `__ + in https://github.com/simpeg/simpeg/pull/1312 +- Ditch deprecated functions in utils.model_builder by `@domfournier `__ in + https://github.com/simpeg/simpeg/pull/1311 - Triaxial magnetic gradient forward modelling by `@thibaut-kobold `__ in + https://github.com/simpeg/simpeg/pull/1288 +- Documentation improvements for classes in Objective Function Pieces + by `@ghwilliams `__ in https://github.com/simpeg/simpeg/pull/1292 +- Fix description of source_field in gravity survey by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1322 +- Add ``weights_keys`` method to ``BaseRegularization`` by `@santisoler `__ + in https://github.com/simpeg/simpeg/pull/1320 +- Bump versions of flake8 and black and pin flake plugins by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1330 +- Move ``__init__`` in ``BaseSimulation`` to the top of the class by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1323 +- Simpeg vector update by `@johnweis0480 `__ in + https://github.com/simpeg/simpeg/pull/1329 +- Fix typo in error messages by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1324 +- Fix issue with lengthscales in coterminal angle calculations by + `@domfournier `__ in https://github.com/simpeg/simpeg/pull/1299 +- Simplify check for invalid multipliers by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1336 +- Ravel instead of flatten by `@thibaut-kobold `__ in + https://github.com/simpeg/simpeg/pull/1343 +- Fix implementation of coterminal function by `@domfournier `__ in + https://github.com/simpeg/simpeg/pull/1334 +- Update cross gradient hessian approximation by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1355 +- ISSUE-1341: Set parent of objective functions by `@domfournier `__ in + https://github.com/simpeg/simpeg/pull/1342 +- Fix partial derivatives in regularization docs by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1362 +- Remove factor of half in data misfits and regularizations by `@lheagy `__ + in https://github.com/simpeg/simpeg/pull/1326 +- Improvements to template for a bug report issue by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1359 +- Simplify a few gravity simulation tests by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1363 +- Exponential Sinusoids Simulation by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1337 +- Replace magnetic SourceField for UniformBackgroundField by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1364 +- Remove deprecated regularization classes by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1365 +- Removed deprecated properties of UpdateSensitivityWeights by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1368 +- Replace indActive for active_cells in regularizations by `@santisoler `__ + in https://github.com/simpeg/simpeg/pull/1366 +- Remove the debug argument from InversionDirective by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1370 +- Remove cellDiff properties of RegularizationMesh by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1371 +- Remove deprecated bits of code by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1372 +- Use choclo in gravity tutorials by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1378 +- Remove surface2ind_topo by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1374 +- Speed up sphinx documentation building by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1382 +- Add docs/sg_execution_times.rst to .gitignore by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1380 +- Describe merge process of Pull Requests in docs by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1375 +- Simplify private methods in gravity simulation by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1384 +- Update Slack links: point to Mattermost by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1385 +- added getJ for fdem and nsem simulations by `@JKutt `__ in + https://github.com/simpeg/simpeg/pull/1276 +- Add LogisticSigmoidMap by `@ckohnke `__ in + https://github.com/simpeg/simpeg/pull/1352 +- Remove the cell_weights attribute in regularizations by `@santisoler `__ + in https://github.com/simpeg/simpeg/pull/1376 +- Remove regmesh, mref and gradientType from regularizations by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1377 +- Test if gravity sensitivities are stored on disk by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1388 +- Check if mesh is 3D when using Choclo in gravity simulation by + `@santisoler `__ in https://github.com/simpeg/simpeg/pull/1386 +- Rotated Gradients by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1167 +- Add directives to the API Reference by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1397 +- Remove deprecated modelType in mag simulation by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1399 +- Remove mref property of PGI regularization by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1400 +- Add link to User Tutorials to navbar in docs by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1401 +- Improve documentation for base simulation classes by `@ghwilliams `__ in + https://github.com/simpeg/simpeg/pull/1295 +- Enforce regularization ``weights`` as dictionaries by `@YingHuuu `__ in + https://github.com/simpeg/simpeg/pull/1344 +- Minor adjustments to Sphinx configuration by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1398 +- Update AUTHORS.rst by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1259 +- Update year in LICENSE by `@lheagy `__ in + https://github.com/simpeg/simpeg/pull/1404 +- Dask MetaSim by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1199 +- Add Ying and Williams to AUTHORS.rst by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1405 +- Remove link to “twitter” by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1406 +- Bump Black version to 24.3.0 by `@santisoler `__ in + https://github.com/simpeg/simpeg/pull/1403 +- Publish documentation on azure `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1412 diff --git a/docs/content/release/0.21.1-notes.rst b/docs/content/release/0.21.1-notes.rst new file mode 100644 index 0000000000..cd35017d87 --- /dev/null +++ b/docs/content/release/0.21.1-notes.rst @@ -0,0 +1,30 @@ +.. _0.21.1_notes: + +=========================== +SimPEG 0.21.1 Release Notes +=========================== + +April 10th, 2024 + +.. contents:: Highlights + :depth: 2 + +Updates +======= + +Minor fix when importing Dask in the ``meta`` module: Dask is an optional +dependency. + +Contributors +============ + +This is a combination of contributors and reviewers who've made contributions +towards this release (in no particular order). + +* `@jcapriot `__ + +Pull Requests +============= + +* Fix hard dask dependency by `@jcapriot `__ in + https://github.com/simpeg/simpeg/pull/1415 diff --git a/docs/content/release/index.rst b/docs/content/release/index.rst index dd81c94be7..49daf1cfc9 100644 --- a/docs/content/release/index.rst +++ b/docs/content/release/index.rst @@ -5,6 +5,9 @@ Release Notes .. toctree:: :maxdepth: 2 + 0.21.1 <0.21.1-notes> + 0.21.0 <0.21.0-notes> + 0.20.0 <0.20.0-notes> 0.19.0 <0.19.0-notes> 0.18.1 <0.18.1-notes> 0.18.0 <0.18.0-notes> diff --git a/docs/index.rst b/docs/index.rst index 3cf569fb33..7f742f6860 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,7 +5,7 @@ :hidden: :titlesonly: - content/getting_started + content/getting_started/index content/user_guide content/api/index content/release/index diff --git a/docs/make.bat b/docs/make.bat index 2ac3df69ca..012450a5a7 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -99,9 +99,9 @@ if "%1" == "qthelp" ( echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\SimPEG.qhcp + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\simpeg.qhcp echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\SimPEG.ghc + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\simpeg.ghc goto end ) diff --git a/environment_test.yml b/environment_test.yml index 3622dd2175..84940f92d6 100644 --- a/environment_test.yml +++ b/environment_test.yml @@ -7,9 +7,10 @@ dependencies: - scikit-learn>=1.2 - pymatsolver>=0.2 - matplotlib - - discretize>=0.8 - - geoana>=0.4.0 + - discretize>=0.10 + - geoana>=0.5.0 - empymod>=2.0.0 + - setuptools_scm - pandas - dask - zarr @@ -18,7 +19,6 @@ dependencies: - h5py - sphinx - sphinx-gallery>=0.1.13 - - sphinx-toolbox - sphinxcontrib-apidoc - pydata-sphinx-theme - nbsphinx @@ -36,11 +36,14 @@ dependencies: - pyvista - pip - python-kaleido + # Optional dependencies + - choclo # Linters and code style - - black==23.1.0 - - flake8 - - flake8-bugbear - - flake8-builtins - - flake8-mutable - - flake8-rst-docstrings - - flake8-docstrings + - 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_block_in_layer.py b/examples/01-maps/plot_block_in_layer.py index 4b116ceeb2..66d9cf741d 100644 --- a/examples/01-maps/plot_block_in_layer.py +++ b/examples/01-maps/plot_block_in_layer.py @@ -21,8 +21,9 @@ ] """ + import discretize -from SimPEG import maps +from simpeg import maps import numpy as np import matplotlib.pyplot as plt diff --git a/examples/01-maps/plot_combo.py b/examples/01-maps/plot_combo.py index 86a98cf4aa..9ba327b66c 100644 --- a/examples/01-maps/plot_combo.py +++ b/examples/01-maps/plot_combo.py @@ -7,9 +7,9 @@ modeling. We will also assume that we are working in log conductivity still, so after the transformation we map to conductivity space. To do this we will introduce the vertical 1D map -(:class:`SimPEG.maps.SurjectVertical1D`), which does the first part of +(:class:`simpeg.maps.SurjectVertical1D`), which does the first part of what we just described. The second part will be done by the -:class:`SimPEG.maps.ExpMap` described above. +:class:`simpeg.maps.ExpMap` described above. .. code-block:: python :linenos: @@ -26,8 +26,9 @@ right). Just to be sure that the derivative is correct, you should always run the test on the mapping that you create. """ + import discretize -from SimPEG import maps +from simpeg import maps import numpy as np import matplotlib.pyplot as plt diff --git a/examples/01-maps/plot_layer.py b/examples/01-maps/plot_layer.py index 90600bde0a..6472ae4169 100644 --- a/examples/01-maps/plot_layer.py +++ b/examples/01-maps/plot_layer.py @@ -17,8 +17,9 @@ 'layer thickness' ] """ + import discretize -from SimPEG import maps +from simpeg import maps import numpy as np import matplotlib.pyplot as plt diff --git a/examples/01-maps/plot_mesh2mesh.py b/examples/01-maps/plot_mesh2mesh.py index c5cd310e66..b5621e5ae2 100644 --- a/examples/01-maps/plot_mesh2mesh.py +++ b/examples/01-maps/plot_mesh2mesh.py @@ -4,8 +4,9 @@ This mapping allows you to go from one mesh to another. """ + import discretize -from SimPEG import maps, utils +from simpeg import maps, utils import matplotlib.pyplot as plt @@ -14,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.randomModel(M.vnC, seed=79, its=50) + V = utils.model_builder.create_random_model(M.vnC, 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 a41d684ceb..7cbc4f89c5 100644 --- a/examples/01-maps/plot_sumMap.py +++ b/examples/01-maps/plot_sumMap.py @@ -12,9 +12,10 @@ """ + from discretize import TensorMesh from discretize.utils import active_from_xyz -from SimPEG import ( +from simpeg import ( utils, maps, regularization, @@ -24,13 +25,13 @@ directives, inversion, ) -from SimPEG.potential_fields import magnetics +from simpeg.potential_fields import magnetics import numpy as np import matplotlib.pyplot as plt def run(plotIt=True): - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # Create a mesh dx = 5.0 @@ -62,7 +63,12 @@ def run(plotIt=True): # Create a MAGsurvey rxLoc = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] rxLoc = magnetics.Point(rxLoc) - srcField = magnetics.SourceField([rxLoc], parameters=H0) + srcField = magnetics.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) survey = magnetics.Survey(srcField) # We can now create a susceptibility model and generate data @@ -72,7 +78,7 @@ def run(plotIt=True): model[mesh.gridCC[:, 0] < 0] = 0.01 # Add a block in half-space - model = utils.model_builder.addBlock( + model = utils.model_builder.add_block( mesh.gridCC, model, np.r_[-10, -10, 20], np.r_[10, 10, 40], 0.05 ) @@ -133,17 +139,19 @@ def run(plotIt=True): regMesh = TensorMesh([len(domains)]) reg_m1 = regularization.Sparse(regMesh, mapping=wires.homo) - reg_m1.cell_weights = wires.homo * wr + reg_m1.set_weights(weights=wires.homo * wr) + reg_m1.norms = [0, 2] - reg_m1.mref = np.zeros(sumMap.shape[1]) + reg_m1.reference_model = np.zeros(sumMap.shape[1]) # Regularization for the voxel model reg_m2 = regularization.Sparse( mesh, active_cells=actv, mapping=wires.hetero, gradient_type="components" ) - reg_m2.cell_weights = wires.hetero * wr + reg_m2.set_weights(weights=wires.hetero * wr) + reg_m2.norms = [0, 0, 0, 0] - reg_m2.mref = np.zeros(sumMap.shape[1]) + reg_m2.reference_model = np.zeros(sumMap.shape[1]) reg = reg_m1 + reg_m2 diff --git a/examples/02-gravity/plot_inv_grav_tiled.py b/examples/02-gravity/plot_inv_grav_tiled.py index d44988356d..f5676a5938 100644 --- a/examples/02-gravity/plot_inv_grav_tiled.py +++ b/examples/02-gravity/plot_inv_grav_tiled.py @@ -5,12 +5,13 @@ Invert data in tiles. """ + import os import numpy as np import matplotlib.pyplot as plt -from SimPEG.potential_fields import gravity -from SimPEG import ( +from simpeg.potential_fields import gravity +from simpeg import ( maps, data, data_misfit, @@ -22,7 +23,7 @@ ) from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz -from SimPEG import utils +from simpeg import utils ############################################################################### # Setup @@ -115,7 +116,7 @@ # Here a simple block in half-space # Get the indices of the magnetized block model = np.zeros(mesh.nC) -ind = utils.model_builder.getIndicesBlock( +ind = utils.model_builder.get_indices_block( np.r_[-10, -10, -30], np.r_[10, 10, -10], mesh.gridCC, @@ -243,7 +244,7 @@ ) saveDict = directives.SaveOutputEveryIteration(save_txt=False) update_Jacobi = directives.UpdatePreconditioner() -sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) +sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) inv = inversion.BaseInversion( invProb, directiveList=[update_IRLS, sensitivity_weights, betaest, update_Jacobi, saveDict], diff --git a/examples/03-magnetics/plot_0_analytic.py b/examples/03-magnetics/plot_0_analytic.py index 8384445f2e..114aa5c00a 100644 --- a/examples/03-magnetics/plot_0_analytic.py +++ b/examples/03-magnetics/plot_0_analytic.py @@ -5,8 +5,9 @@ Comparing the magnetics field in Vancouver to Seoul """ + import numpy as np -from SimPEG.potential_fields.magnetics import analytics +from simpeg.potential_fields.magnetics import analytics import matplotlib.pyplot as plt from mpl_toolkits.axes_grid1 import make_axes_locatable 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 cc9176afdd..c9e1ab7b4d 100644 --- a/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py +++ b/examples/03-magnetics/plot_inv_mag_MVI_Sparse_TreeMesh.py @@ -11,13 +11,12 @@ Cartesian coordinate system, and second for a compact model using the Spherical formulation. -The inverse problem uses the :class:'SimPEG.regularization.Sparse' +The inverse problem uses the :class:'simpeg.regularization.Sparse' that """ -from discretize import TreeMesh -from SimPEG import ( +from simpeg import ( data, data_misfit, directives, @@ -28,11 +27,11 @@ regularization, ) -from SimPEG import utils -from SimPEG.utils import mkvc +from simpeg import utils +from simpeg.utils import mkvc from discretize.utils import active_from_xyz, mesh_builder_xyz, refine_tree_xyz -from SimPEG.potential_fields import magnetics +from simpeg.potential_fields import magnetics import scipy as sp import numpy as np import matplotlib.pyplot as plt @@ -52,7 +51,7 @@ # np.random.seed(1) # We will assume a vertical inducing field -H0 = (50000.0, 90.0, 0.0) +h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # The magnetization is set along a different direction (induced + remanence) M = np.array([45.0, 90.0]) @@ -75,7 +74,12 @@ # Create a MAGsurvey xyzLoc = np.c_[mkvc(X.T), mkvc(Y.T), mkvc(Z.T)] rxLoc = magnetics.receivers.Point(xyzLoc) -srcField = magnetics.sources.SourceField(receiver_list=[rxLoc], parameters=H0) +srcField = magnetics.sources.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, +) survey = magnetics.survey.Survey(srcField) # Here how the topography looks with a quick interpolation, just a Gaussian... @@ -117,100 +121,6 @@ actv = active_from_xyz(mesh, topo) nC = int(actv.sum()) -########################################################################### -# A simple function to plot vectors in TreeMesh -# -# Should eventually end up on discretize -# - - -def plotVectorSectionsOctree( - mesh, - m, - normal="X", - ind=0, - vmin=None, - vmax=None, - scale=1.0, - vec="k", - axs=None, - actvMap=None, - fill=True, -): - """ - Plot section through a 3D tensor model - """ - # plot recovered model - normalInd = {"X": 0, "Y": 1, "Z": 2}[normal] - antiNormalInd = {"X": [1, 2], "Y": [0, 2], "Z": [0, 1]}[normal] - - h2d = (mesh.h[antiNormalInd[0]], mesh.h[antiNormalInd[1]]) - x2d = (mesh.x0[antiNormalInd[0]], mesh.x0[antiNormalInd[1]]) - - #: Size of the sliced dimension - szSliceDim = len(mesh.h[normalInd]) - if ind is None: - ind = int(szSliceDim // 2) - - cc_tensor = [None, None, None] - for i in range(3): - cc_tensor[i] = np.cumsum(np.r_[mesh.x0[i], mesh.h[i]]) - cc_tensor[i] = (cc_tensor[i][1:] + cc_tensor[i][:-1]) * 0.5 - slice_loc = cc_tensor[normalInd][ind] - - # Create a temporary TreeMesh with the slice through - temp_mesh = TreeMesh(h2d, x2d) - level_diff = mesh.max_level - temp_mesh.max_level - - XS = [None, None, None] - XS[antiNormalInd[0]], XS[antiNormalInd[1]] = np.meshgrid( - cc_tensor[antiNormalInd[0]], cc_tensor[antiNormalInd[1]] - ) - XS[normalInd] = np.ones_like(XS[antiNormalInd[0]]) * slice_loc - loc_grid = np.c_[XS[0].reshape(-1), XS[1].reshape(-1), XS[2].reshape(-1)] - inds = np.unique(mesh._get_containing_cell_indexes(loc_grid)) - - grid2d = mesh.gridCC[inds][:, antiNormalInd] - levels = mesh._cell_levels_by_indexes(inds) - level_diff - temp_mesh.insert_cells(grid2d, levels) - tm_gridboost = np.empty((temp_mesh.nC, 3)) - tm_gridboost[:, antiNormalInd] = temp_mesh.gridCC - tm_gridboost[:, normalInd] = slice_loc - - # Interpolate values to mesh.gridCC if not 'CC' - mx = actvMap * m[:, 0] - my = actvMap * m[:, 1] - mz = actvMap * m[:, 2] - - m = np.c_[mx, my, mz] - - # Interpolate values from mesh.gridCC to grid2d - ind_3d_to_2d = mesh._get_containing_cell_indexes(tm_gridboost) - v2d = m[ind_3d_to_2d, :] - amp = np.sum(v2d**2.0, axis=1) ** 0.5 - - if axs is None: - axs = plt.subplot(111) - - if fill: - temp_mesh.plot_image(amp, ax=axs, clim=[vmin, vmax], grid=True) - - axs.quiver( - temp_mesh.gridCC[:, 0], - temp_mesh.gridCC[:, 1], - v2d[:, antiNormalInd[0]], - v2d[:, antiNormalInd[1]], - pivot="mid", - scale_units="inches", - scale=scale, - linewidths=(1,), - edgecolors=(vec), - headaxislength=0.1, - headwidth=10, - headlength=30, - ) - - ########################################################################### # Forward modeling data # --------------------- @@ -226,7 +136,7 @@ def plotVectorSectionsOctree( M_xyz = utils.mat_utils.dip_azimuth2cartesian(M[0], M[1]) # Get the indicies of the magnetized block -ind = utils.model_builder.getIndicesBlock( +ind = utils.model_builder.get_indices_block( np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, @@ -271,16 +181,19 @@ def plotVectorSectionsOctree( # Plot the vector model ax = plt.subplot(2, 1, 2) -plotVectorSectionsOctree( - mesh, - model, - axs=ax, +mesh.plot_slice( + actv_plot * model.reshape((-1, 3), order="F"), + v_type="CCv", + view="vec", + ax=ax, normal="Y", ind=66, - actvMap=actv_plot, - scale=0.5, - vmin=0.0, - vmax=0.025, + grid=True, + quiver_opts={ + "pivot": "mid", + "scale": 5 * np.abs(model).max(), + "scale_units": "inches", + }, ) ax.set_xlim([-200, 200]) ax.set_ylim([-100, 75]) @@ -380,26 +293,35 @@ def plotVectorSectionsOctree( # Create a Combo Regularization # Regularize the amplitude of the vectors reg_a = regularization.Sparse( - mesh, gradient_type="components", active_cells=actv, mapping=wires.amp + mesh, + gradient_type="total", + active_cells=actv, + mapping=wires.amp, + norms=[0.0, 1.0, 1.0, 1.0], # Only norm on gradients used, + reference_model=np.zeros(3 * nC), ) -reg_a.norms = [0.0, 0.0, 0.0, 0.0] # Sparse on the model and its gradients -reg_a.reference_model = np.zeros(3 * nC) # Regularize the vertical angle of the vectors reg_t = regularization.Sparse( - mesh, gradient_type="components", active_cells=actv, mapping=wires.theta + mesh, + gradient_type="total", + active_cells=actv, + mapping=wires.theta, + alpha_s=0.0, # No reference angle, + norms=[0.0, 1.0, 1.0, 1.0], # Only norm on gradients used, ) -reg_t.alpha_s = 0.0 # No reference angle reg_t.units = "radian" -reg_t.norms = [0.0, 0.0, 0.0, 0.0] # Only norm on gradients used # Regularize the horizontal angle of the vectors reg_p = regularization.Sparse( - mesh, gradient_type="components", active_cells=actv, mapping=wires.phi + mesh, + gradient_type="total", + active_cells=actv, + mapping=wires.phi, + alpha_s=0.0, # No reference angle, + norms=[0.0, 1.0, 1.0, 1.0], # Only norm on gradients used, ) -reg_p.alpha_s = 0.0 # No reference angle reg_p.units = "radian" -reg_p.norms = [0.0, 0.0, 0.0, 0.0] # Only norm on gradients used reg = reg_a + reg_t + reg_p reg.reference_model = np.zeros(3 * nC) @@ -456,16 +378,19 @@ def plotVectorSectionsOctree( plt.figure(figsize=(8, 8)) ax = plt.subplot(2, 1, 1) -plotVectorSectionsOctree( - mesh, - mrec_MVIC.reshape((nC, 3), order="F"), - axs=ax, +mesh.plot_slice( + actv_plot * mrec_MVIC.reshape((nC, 3), order="F"), + v_type="CCv", + view="vec", + ax=ax, normal="Y", - ind=65, - actvMap=actv_plot, - scale=0.05, - vmin=0.0, - vmax=0.005, + ind=66, + grid=True, + quiver_opts={ + "pivot": "mid", + "scale": 5 * np.abs(mrec_MVIC).max(), + "scale_units": "inches", + }, ) ax.set_xlim([-200, 200]) ax.set_ylim([-100, 75]) @@ -476,23 +401,26 @@ def plotVectorSectionsOctree( ax = plt.subplot(2, 1, 2) vec_xyz = utils.mat_utils.spherical2cartesian( - invProb.model.reshape((nC, 3), order="F") + mrec_MVI_S.reshape((nC, 3), order="F") ).reshape((nC, 3), order="F") -plotVectorSectionsOctree( - mesh, - vec_xyz, - axs=ax, +mesh.plot_slice( + actv_plot * vec_xyz, + v_type="CCv", + view="vec", + ax=ax, normal="Y", - ind=65, - actvMap=actv_plot, - scale=0.4, - vmin=0.0, - vmax=0.025, + ind=66, + grid=True, + quiver_opts={ + "pivot": "mid", + "scale": 5 * np.abs(vec_xyz).max(), + "scale_units": "inches", + }, ) ax.set_xlim([-200, 200]) ax.set_ylim([-100, 75]) -ax.set_title("Sparse model (Spherical)") +ax.set_title("Sparse model (L0L2)") ax.set_xlabel("x") ax.set_ylabel("y") plt.gca().set_aspect("equal", adjustable="box") diff --git a/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py new file mode 100644 index 0000000000..edd69836d2 --- /dev/null +++ b/examples/03-magnetics/plot_inv_mag_MVI_VectorAmplitude.py @@ -0,0 +1,253 @@ +""" +Magnetic inversion on a TreeMesh +================================ + +In this example, we demonstrate the use of a Magnetic Vector Inverison +on 3D TreeMesh for the inversion of magnetic data. + +The inverse problem uses the :class:'simpeg.regularization.VectorAmplitude' +regularization borrowed from ... + +""" + +from simpeg import ( + data, + data_misfit, + directives, + maps, + inverse_problem, + optimization, + inversion, + regularization, +) + +from simpeg import utils +from simpeg.utils import mkvc, sdiag + +from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz +from simpeg.potential_fields import magnetics +import numpy as np +import matplotlib.pyplot as plt + + +# sphinx_gallery_thumbnail_number = 3 + +############################################################################### +# Setup +# ----- +# +# Define the survey and model parameters +# +# First we need to define the direction of the inducing field +# As a simple case, we pick a vertical inducing field of magnitude 50,000 nT. +# +# +np.random.seed(1) +# We will assume a vertical inducing field +h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) + +# Create grid of points for topography +# Lets create a simple Gaussian topo and set the active cells +[xx, yy] = np.meshgrid(np.linspace(-200, 200, 50), np.linspace(-200, 200, 50)) +b = 100 +A = 50 +zz = A * np.exp(-0.5 * ((xx / b) ** 2.0 + (yy / b) ** 2.0)) +topo = np.c_[utils.mkvc(xx), utils.mkvc(yy), utils.mkvc(zz)] + +# Create an array of observation points +xr = np.linspace(-100.0, 100.0, 20) +yr = np.linspace(-100.0, 100.0, 20) +X, Y = np.meshgrid(xr, yr) +Z = A * np.exp(-0.5 * ((X / b) ** 2.0 + (Y / b) ** 2.0)) + 5 + +# Create a MAGsurvey +xyzLoc = np.c_[mkvc(X.T), mkvc(Y.T), mkvc(Z.T)] +rxLoc = magnetics.receivers.Point(xyzLoc) +srcField = magnetics.sources.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, +) +survey = magnetics.survey.Survey(srcField) + +############################################################################### +# Inversion Mesh +# -------------- +# +# Here, we create a TreeMesh with base cell size of 5 m. +# + +# Create a mesh +h = [5, 5, 5] +padDist = np.ones((3, 2)) * 100 + +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=[2, 6], finalize=True +) + + +# Define an active cells from topo +actv = active_from_xyz(mesh, topo) +nC = int(actv.sum()) + +########################################################################### +# Forward modeling data +# --------------------- +# +# We can now create a magnetization model and generate data. +# +model_azm_dip = np.zeros((mesh.nC, 2)) +model_amp = np.ones(mesh.nC) * 1e-8 +ind = utils.model_builder.get_indices_block( + 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 + +# Remove air cells +model_azm_dip = model_azm_dip[actv, :] +model_amp = model_amp[actv] +model = sdiag(model_amp) * utils.mat_utils.dip_azimuth2cartesian( + model_azm_dip[:, 0], model_azm_dip[:, 1] +) + +# Create reduced identity map +idenMap = maps.IdentityMap(nP=nC * 3) + +# Create the simulation +simulation = magnetics.simulation.Simulation3DIntegral( + survey=survey, mesh=mesh, chiMap=idenMap, ind_active=actv, model_type="vector" +) + +# Compute some data and add some random noise +d = simulation.dpred(mkvc(model)) +std = 10 # nT +synthetic_data = d + np.random.randn(len(d)) * std +wd = np.ones(len(d)) * std + +# Assign data and uncertainties to the survey +data_object = data.Data(survey, dobs=synthetic_data, standard_deviation=wd) + +# Create a projection matrix for plotting later +actv_plot = maps.InjectActiveCells(mesh, actv, np.nan) + + +###################################################################### +# Inversion +# --------- +# +# We can now attempt the inverse calculations. +# + +# Create sensitivity weights from our linear forward operator +rxLoc = survey.source_field.receiver_list[0].locations + +# This Mapping connects the regularizations for the three-component +# vector model +wires = maps.Wires(("p", nC), ("s", nC), ("t", nC)) +m0 = np.ones(3 * nC) * 1e-4 # Starting model + +# Create the regularization on the amplitude of magnetization +reg = regularization.VectorAmplitude( + mesh, + mapping=idenMap, + active_cells=actv, + reference_model_in_smooth=True, + norms=[0.0, 2.0, 2.0, 2.0], + gradient_type="total", +) + +# Data misfit function +dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_object) +dmis.W = 1.0 / data_object.standard_deviation + +# The optimization scheme +opt = optimization.ProjectedGNCG( + maxIter=20, lower=-10, upper=10.0, maxIterLS=20, maxIterCG=20, tolCG=1e-4 +) + +# The inverse problem +invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) + +# Estimate the initial beta factor +betaest = directives.BetaEstimate_ByEig(beta0_ratio=1e1) + +# Add sensitivity weights +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) + +# Pre-conditioner +update_Jacobi = directives.UpdatePreconditioner() + + +inv = inversion.BaseInversion( + invProb, directiveList=[sensitivity_weights, IRLS, update_Jacobi, betaest] +) + +# Run the inversion +mrec = inv.run(m0) + + +############################################################# +# Final Plot +# ---------- +# +# Let's compare the smooth and compact model +# +# +# + +plt.figure(figsize=(12, 6)) +ax = plt.subplot(2, 2, 1) +im = utils.plot_utils.plot2Ddata(xyzLoc, synthetic_data, ax=ax) +plt.colorbar(im[0]) +ax.set_title("Predicted data.") +plt.gca().set_aspect("equal", adjustable="box") + +for ii, (title, mvec) in enumerate( + [("True model", model), ("Smooth model", invProb.l2model), ("Sparse model", mrec)] +): + ax = plt.subplot(2, 2, ii + 2) + mesh.plot_slice( + actv_plot * mvec.reshape((-1, 3), order="F"), + v_type="CCv", + view="vec", + ax=ax, + normal="Y", + grid=True, + quiver_opts={ + "pivot": "mid", + "scale": 8 * np.abs(mvec).max(), + "scale_units": "inches", + }, + ) + ax.set_xlim([-200, 200]) + ax.set_ylim([-100, 75]) + ax.set_title(title) + ax.set_xlabel("x") + ax.set_ylabel("z") + plt.gca().set_aspect("equal", adjustable="box") + +plt.show() + +print("END") +# Plot the final predicted data and the residual +# plt.figure() +# ax = plt.subplot(1, 2, 1) +# utils.plot_utils.plot2Ddata(xyzLoc, invProb.dpred, ax=ax) +# ax.set_title("Predicted data.") +# plt.gca().set_aspect("equal", adjustable="box") +# +# ax = plt.subplot(1, 2, 2) +# utils.plot_utils.plot2Ddata(xyzLoc, synthetic_data - invProb.dpred, ax=ax) +# ax.set_title("Data residual.") +# plt.gca().set_aspect("equal", adjustable="box") diff --git a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py index e62cc9ec9f..d114a30609 100644 --- a/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py +++ b/examples/03-magnetics/plot_inv_mag_nonLinear_Amplitude.py @@ -11,8 +11,8 @@ recover 3-component magnetic data. This data is then transformed to amplitude Secondly, we invert the non-linear inverse problem with -:class:`SimPEG.directives.UpdateSensitivityWeights`. We also -uses the :class:`SimPEG.regularization.Sparse` to apply sparsity +:class:`simpeg.directives.UpdateSensitivityWeights`. We also +uses the :class:`simpeg.regularization.Sparse` to apply sparsity assumption in order to improve the recovery of a compact prism. """ @@ -20,7 +20,7 @@ import scipy as sp import numpy as np import matplotlib.pyplot as plt -from SimPEG import ( +from simpeg import ( data, data_misfit, directives, @@ -31,9 +31,9 @@ regularization, ) -from SimPEG.potential_fields import magnetics -from SimPEG import utils -from SimPEG.utils import mkvc +from simpeg.potential_fields import magnetics +from simpeg import utils +from simpeg.utils import mkvc from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz # sphinx_gallery_thumbnail_number = 4 @@ -50,7 +50,7 @@ # # We will assume a vertical inducing field -H0 = (50000.0, 90.0, 0.0) +h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # The magnetization is set along a different direction (induced + remanence) M = np.array([45.0, 90.0]) @@ -75,7 +75,12 @@ # Create a MAGsurvey rxLoc = np.c_[mkvc(X.T), mkvc(Y.T), mkvc(Z.T)] receiver_list = magnetics.receivers.Point(rxLoc) -srcField = magnetics.sources.SourceField(receiver_list=[receiver_list], parameters=H0) +srcField = magnetics.sources.UniformBackgroundField( + receiver_list=[receiver_list], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, +) survey = magnetics.survey.Survey(srcField) # Here how the topography looks with a quick interpolation, just a Gaussian... @@ -125,14 +130,14 @@ M_xyz = utils.mat_utils.dip_azimuth2cartesian(np.ones(nC) * M[0], np.ones(nC) * M[1]) # Get the indicies of the magnetized block -ind = utils.model_builder.getIndicesBlock( +ind = utils.model_builder.get_indices_block( 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 +# be applied in by the :class:`simpeg.PF.Magnetics` problem model = np.zeros(mesh.nC) model[ind] = chi_e @@ -228,9 +233,9 @@ # Create a regularization function, in this case l2l2 reg = regularization.Sparse( - mesh, indActive=surf, mapping=maps.IdentityMap(nP=nC), alpha_z=0 + mesh, active_cells=surf, mapping=maps.IdentityMap(nP=nC), alpha_z=0 ) -reg.mref = np.zeros(nC) +reg.reference_model = np.zeros(nC) # Specify how the optimization will proceed, set susceptibility bounds to inf opt = optimization.ProjectedGNCG( @@ -267,7 +272,12 @@ # receiver_list = magnetics.receivers.Point(rxLoc, components=["bx", "by", "bz"]) -srcField = magnetics.sources.SourceField(receiver_list=[receiver_list], parameters=H0) +srcField = magnetics.sources.UniformBackgroundField( + receiver_list=[receiver_list], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, +) surveyAmp = magnetics.survey.Survey(srcField) simulation = magnetics.simulation.Simulation3DIntegral( @@ -335,9 +345,9 @@ data_obj = data.Data(survey, dobs=bAmp, noise_floor=wd) # Create a sparse regularization -reg = regularization.Sparse(mesh, indActive=actv, mapping=idenMap) +reg = regularization.Sparse(mesh, active_cells=actv, mapping=idenMap) reg.norms = [1, 0, 0, 0] -reg.mref = np.zeros(nC) +reg.reference_model = np.zeros(nC) # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_obj) diff --git a/examples/04-dcip/plot_dc_analytic.py b/examples/04-dcip/plot_dc_analytic.py index ec16ee2672..b47ba4ed2a 100644 --- a/examples/04-dcip/plot_dc_analytic.py +++ b/examples/04-dcip/plot_dc_analytic.py @@ -5,16 +5,17 @@ Comparison of the analytic and numerical solution for a direct current resistivity dipole in 3D. """ + import discretize -from SimPEG import utils +from simpeg import utils import numpy as np import matplotlib.pyplot as plt -from SimPEG.electromagnetics.static import resistivity as DC +from simpeg.electromagnetics.static import resistivity as DC try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver cs = 25.0 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 6bde3d8dda..7f1ba7dfd2 100644 --- a/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.py +++ b/examples/04-dcip/plot_inv_dcip_dipoledipole_3Dinversion_twospheres.py @@ -17,7 +17,7 @@ """ import discretize -from SimPEG import ( +from simpeg import ( maps, utils, data_misfit, @@ -27,14 +27,14 @@ directives, inversion, ) -from SimPEG.electromagnetics.static import resistivity as DC, utils as DCutils +from simpeg.electromagnetics.static import resistivity as DC, utils as DCutils import numpy as np import matplotlib.pyplot as plt try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver np.random.seed(12345) 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 1f2eac7f9d..8304fce3d6 100644 --- a/examples/04-dcip/plot_inv_dcip_dipoledipole_parametric_inversion.py +++ b/examples/04-dcip/plot_inv_dcip_dipoledipole_parametric_inversion.py @@ -16,10 +16,10 @@ User is promoted to try different initial values of the parameterized model. """ -from SimPEG.electromagnetics.static import resistivity as DC, utils as DCutils +from simpeg.electromagnetics.static import resistivity as DC, utils as DCutils from discretize import TensorMesh from discretize.utils import active_from_xyz -from SimPEG import ( +from simpeg import ( maps, utils, data_misfit, @@ -37,7 +37,7 @@ try: from pymatsolver import PardisoSolver as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver def run( diff --git a/examples/04-dcip/plot_read_DC_data_with_IO_class.py b/examples/04-dcip/plot_read_DC_data_with_IO_class.py index 28e5707834..0b1d09faa2 100644 --- a/examples/04-dcip/plot_read_DC_data_with_IO_class.py +++ b/examples/04-dcip/plot_read_DC_data_with_IO_class.py @@ -13,9 +13,9 @@ import shutil import os import matplotlib.pyplot as plt -from SimPEG.electromagnetics.static import resistivity as DC -from SimPEG import Report -from SimPEG.utils.io_utils import download +from simpeg.electromagnetics.static import resistivity as DC +from simpeg import Report +from simpeg.utils.io_utils import download ############################################################################### # Download an example DC data csv file diff --git a/examples/05-fdem/plot_0_fdem_analytic.py b/examples/05-fdem/plot_0_fdem_analytic.py index 19b2e39a67..24f3ecc75c 100644 --- a/examples/05-fdem/plot_0_fdem_analytic.py +++ b/examples/05-fdem/plot_0_fdem_analytic.py @@ -2,7 +2,7 @@ Simulation with Analytic FDEM Solutions ======================================= -Here, the module *SimPEG.electromagnetics.analytics.FDEM* is used to simulate +Here, the module *simpeg.electromagnetics.analytics.FDEM* is used to simulate harmonic electric and magnetic field for both electric and magnetic dipole sources in a wholespace. @@ -15,8 +15,8 @@ # import numpy as np -from SimPEG import utils -from SimPEG.electromagnetics.analytics.FDEM import ( +from simpeg import utils +from simpeg.electromagnetics.analytics.FDEM import ( ElectricDipoleWholeSpace, MagneticDipoleWholeSpace, ) 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 e03c17f1d8..43699d6743 100644 --- a/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py +++ b/examples/05-fdem/plot_inv_fdem_loop_loop_2Dinversion.py @@ -7,7 +7,7 @@ We will use only Horizontal co-planar orientations (vertical magnetic dipole), and look at the real and imaginary parts of the secondary magnetic field. -We use the :class:`SimPEG.maps.Surject2Dto3D` mapping to invert for a 2D model +We use the :class:`simpeg.maps.Surject2Dto3D` mapping to invert for a 2D model and perform the forward modelling in 3D. """ @@ -19,10 +19,10 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver import discretize -from SimPEG import ( +from simpeg import ( maps, optimization, data_misfit, @@ -32,7 +32,7 @@ directives, Report, ) -from SimPEG.electromagnetics import frequency_domain as FDEM +from simpeg.electromagnetics import frequency_domain as FDEM ############################################################################### # Setup @@ -271,7 +271,7 @@ def plot_data(data, ax=None, color="C0", label=""): # -------------------- # # We create the data misfit, simple regularization -# (a least-squares-style regularization, :class:`SimPEG.regularization.LeastSquareRegularization`) +# (a least-squares-style regularization, :class:`simpeg.regularization.LeastSquareRegularization`) # The smoothness and smallness contributions can be set by including # `alpha_s, alpha_x, alpha_y` as input arguments when the regularization is # created. The default reference model in the regularization is the starting @@ -281,7 +281,7 @@ def plot_data(data, ax=None, color="C0", label=""): # We estimate the trade-off parameter, beta, between the data # misfit and regularization by the largest eigenvalue of the data misfit and # the regularization. Here, we use a fixed beta, but could alternatively -# employ a beta-cooling schedule using :class:`SimPEG.directives.BetaSchedule` +# employ a beta-cooling schedule using :class:`simpeg.directives.BetaSchedule` dmisfit = data_misfit.L2DataMisfit(simulation=prob, data=data) reg = regularization.WeightedLeastSquares(inversion_mesh) diff --git a/examples/06-tdem/plot_0_tdem_analytic.py b/examples/06-tdem/plot_0_tdem_analytic.py index 1a2892b0e0..736d792fb5 100644 --- a/examples/06-tdem/plot_0_tdem_analytic.py +++ b/examples/06-tdem/plot_0_tdem_analytic.py @@ -2,7 +2,7 @@ Simulation with Analytic TDEM Solutions ======================================= -Here, the module *SimPEG.electromagnetics.analytics.TDEM* is used to simulate +Here, the module *simpeg.electromagnetics.analytics.TDEM* is used to simulate transient electric and magnetic field for both electric and magnetic dipole sources in a wholespace. @@ -15,8 +15,8 @@ # import numpy as np -from SimPEG import utils -from SimPEG.electromagnetics.analytics.TDEM import ( +from simpeg import utils +from simpeg.electromagnetics.analytics.TDEM import ( TransientElectricDipoleWholeSpace, TransientMagneticDipoleWholeSpace, ) diff --git a/examples/06-tdem/plot_fwd_tdem_3d_model.py b/examples/06-tdem/plot_fwd_tdem_3d_model.py index 36637e5085..def2b65a74 100644 --- a/examples/06-tdem/plot_fwd_tdem_3d_model.py +++ b/examples/06-tdem/plot_fwd_tdem_3d_model.py @@ -2,17 +2,18 @@ Time-domain CSEM for a resistive cube in a deep marine setting ============================================================== """ + import empymod import discretize try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver import numpy as np -from SimPEG import maps -from SimPEG.electromagnetics import time_domain as TDEM +from simpeg import maps +from simpeg.electromagnetics import time_domain as TDEM import matplotlib.pyplot as plt ############################################################################### @@ -277,10 +278,10 @@ ############################################################################### -# (E) `SimPEG` +# (E) `simpeg` # ------------ # -# Set-up SimPEG-specific parameters. +# Set-up simpeg-specific parameters. # Set up the receiver list @@ -339,8 +340,8 @@ plt.plot(times, epm_bg * 1e9, ".4", lw=2, label="empymod") -plt.plot(times, spg_bg * 1e9, "C0--", label="SimPEG Background") -plt.plot(times, spg_tg * 1e9, "C1--", label="SimPEG Target") +plt.plot(times, spg_bg * 1e9, "C0--", label="simpeg Background") +plt.plot(times, spg_tg * 1e9, "C1--", label="simpeg Target") plt.ylabel("$E_x$ (nV/m)") plt.xscale("log") @@ -359,4 +360,4 @@ ############################################################################### -# empymod.Report([SimPEG, discretize, pymatsolver]) +# empymod.Report([simpeg, discretize, pymatsolver]) 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 0c7d512844..68f0b668b0 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 @@ -20,11 +20,11 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver import time -from SimPEG.electromagnetics import time_domain as TDEM -from SimPEG import utils, maps, Report +from simpeg.electromagnetics import time_domain as TDEM +from simpeg import utils, maps, Report ############################################################################### # Model Parameters diff --git a/examples/06-tdem/plot_fwd_tdem_waveforms.py b/examples/06-tdem/plot_fwd_tdem_waveforms.py index 4ec40ad2f6..c7237e1137 100644 --- a/examples/06-tdem/plot_fwd_tdem_waveforms.py +++ b/examples/06-tdem/plot_fwd_tdem_waveforms.py @@ -8,8 +8,8 @@ import matplotlib.pyplot as plt import numpy as np -from SimPEG.electromagnetics import time_domain as TDEM -from SimPEG.utils import mkvc +from simpeg.electromagnetics import time_domain as TDEM +from simpeg.utils import mkvc nT = 1000 max_t = 5e-3 diff --git a/examples/06-tdem/plot_inv_tdem_1D.py b/examples/06-tdem/plot_inv_tdem_1D.py index 95c005e96f..992dbb13fb 100644 --- a/examples/06-tdem/plot_inv_tdem_1D.py +++ b/examples/06-tdem/plot_inv_tdem_1D.py @@ -4,9 +4,10 @@ Here we will create and run a TDEM 1D inversion. """ + import numpy as np -from SimPEG.electromagnetics import time_domain -from SimPEG import ( +from simpeg.electromagnetics import time_domain +from simpeg import ( optimization, discretize, maps, 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 4a0c5625c9..06b793bb51 100644 --- a/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py +++ b/examples/06-tdem/plot_inv_tdem_1D_raw_waveform.py @@ -6,9 +6,10 @@ with VTEM waveform of which initial condition is zero, but have some on- and off-time. """ + import numpy as np import discretize -from SimPEG import ( +from simpeg import ( maps, data_misfit, regularization, @@ -18,14 +19,14 @@ directives, utils, ) -from SimPEG.electromagnetics import time_domain as TDEM, utils as EMutils +from simpeg.electromagnetics import time_domain as TDEM, utils as EMutils 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 + from simpeg import SolverLU as Solver def run(plotIt=True): diff --git a/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py b/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py index 1143b256dc..c5627008e7 100644 --- a/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py +++ b/examples/07-nsem/plot_fwd_nsem_MTTipper3D.py @@ -4,20 +4,20 @@ Forward model 3D MT data. -Test script to use SimPEG.NSEM platform to forward model +Test script to use simpeg.NSEM platform to forward model impedance and tipper synthetic data. """ import discretize -from SimPEG.electromagnetics import natural_source as NSEM -from SimPEG import utils +from simpeg.electromagnetics import natural_source as NSEM +from simpeg import utils import numpy as np import matplotlib.pyplot as plt try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import Solver + from simpeg import Solver def run(plotIt=True): @@ -40,7 +40,7 @@ def run(plotIt=True): ) # Setup the model conds = [1, 1e-2] - sig = utils.model_builder.defineBlock( + sig = utils.model_builder.create_block_in_wholespace( M.gridCC, [-100, -100, -350], [100, 100, -150], conds ) sig[M.gridCC[:, 2] > 0] = 1e-8 diff --git a/examples/08-vrm/plot_fwd_vrm.py b/examples/08-vrm/plot_fwd_vrm.py index db7e0a27ce..fba26e608f 100644 --- a/examples/08-vrm/plot_fwd_vrm.py +++ b/examples/08-vrm/plot_fwd_vrm.py @@ -5,7 +5,7 @@ Here, we predict the vertical db/dt response over a conductive and magnetically viscous Earth for a small coincident loop system. Following the theory, the total response is approximately equal to the sum of the -inductive and VRM responses modelled separately. The SimPEG.VRM module is +inductive and VRM responses modelled separately. The simpeg.VRM module is used to model the VRM response while an analytic solution for a conductive half-space is used to model the inductive response. """ @@ -15,10 +15,10 @@ # -------------- # -from SimPEG.electromagnetics import viscous_remanent_magnetization as VRM +from simpeg.electromagnetics import viscous_remanent_magnetization as VRM import numpy as np import discretize -from SimPEG import mkvc, maps +from simpeg import mkvc, maps import matplotlib.pyplot as plt import matplotlib as mpl diff --git a/examples/08-vrm/plot_inv_vrm_eq.py b/examples/08-vrm/plot_inv_vrm_eq.py index e4004a0b29..a8558023c8 100644 --- a/examples/08-vrm/plot_inv_vrm_eq.py +++ b/examples/08-vrm/plot_inv_vrm_eq.py @@ -16,10 +16,10 @@ # -------------- # -from SimPEG.electromagnetics import viscous_remanent_magnetization as VRM +from simpeg.electromagnetics import viscous_remanent_magnetization as VRM import numpy as np import discretize -from SimPEG import ( +from simpeg import ( utils, maps, data_misfit, @@ -196,7 +196,10 @@ w = w / np.max(w) w = w -reg = regularization.Smallness(mesh=mesh, indActive=actCells, cell_weights=w) +reg = regularization.Smallness( + mesh=mesh, active_cells=actCells, weights={"cell_weights": w} +) + opt = optimization.ProjectedGNCG( maxIter=20, lower=0.0, upper=1e-2, maxIterLS=20, tolCG=1e-4 ) diff --git a/examples/09-flow/plot_fwd_flow_richards_1D.py b/examples/09-flow/plot_fwd_flow_richards_1D.py index 811fc84a46..1ce540fa12 100644 --- a/examples/09-flow/plot_fwd_flow_richards_1D.py +++ b/examples/09-flow/plot_fwd_flow_richards_1D.py @@ -38,13 +38,14 @@ .. _Celia1990: http://www.webpages.uidaho.edu/ch/papers/Celia.pdf """ + import matplotlib import matplotlib.pyplot as plt import numpy as np import discretize -from SimPEG import maps -from SimPEG.flow import richards +from simpeg import maps +from simpeg.flow import richards def run(plotIt=True): diff --git a/examples/09-flow/plot_inv_flow_richards_1D.py b/examples/09-flow/plot_inv_flow_richards_1D.py index f30a739c3c..567e41f112 100644 --- a/examples/09-flow/plot_inv_flow_richards_1D.py +++ b/examples/09-flow/plot_inv_flow_richards_1D.py @@ -25,20 +25,21 @@ .. _Celia1990: http://www.webpages.uidaho.edu/ch/papers/Celia.pdf """ + import matplotlib import matplotlib.pyplot as plt import numpy as np import discretize -from SimPEG import maps -from SimPEG import regularization -from SimPEG import data_misfit -from SimPEG import optimization -from SimPEG import inverse_problem -from SimPEG import directives -from SimPEG import inversion - -from SimPEG.flow import richards +from simpeg import maps +from simpeg import regularization +from simpeg import data_misfit +from simpeg import optimization +from simpeg import inverse_problem +from simpeg import directives +from simpeg import inversion + +from simpeg.flow import richards def run(plotIt=True): 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 ca142d9bcb..86ba4c2572 100644 --- a/examples/10-pgi/plot_inv_0_PGI_Linear_1D.py +++ b/examples/10-pgi/plot_inv_0_PGI_Linear_1D.py @@ -15,7 +15,7 @@ import discretize as Mesh import matplotlib.pyplot as plt import numpy as np -from SimPEG import ( +from simpeg import ( data_misfit, directives, inverse_problem, @@ -65,7 +65,7 @@ def g(k): mtrue[mesh.cell_centers_x > 0.45] = -0.5 mtrue[mesh.cell_centers_x > 0.6] = 0 -# SimPEG problem and survey +# simpeg problem and survey prob = simulation.LinearSimulation(mesh, G=G, model_map=maps.IdentityMap()) std = 0.01 survey = prob.make_synthetic_data(mtrue, relative_error=std, add_noise=True) 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 5da5932952..693139ab38 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 @@ -12,7 +12,7 @@ import discretize as Mesh import matplotlib.pyplot as plt import numpy as np -from SimPEG import ( +from simpeg import ( data_misfit, directives, inverse_problem, @@ -233,13 +233,13 @@ def g(k): # WeightedLeastSquares Inversion reg1 = regularization.WeightedLeastSquares( - mesh, alpha_s=1.0, alpha_x=1.0, mapping=wires.m1 + mesh, alpha_s=1.0, alpha_x=1.0, mapping=wires.m1, weights={"cell_weights": wr1} ) -reg1.cell_weights = wr1 + reg2 = regularization.WeightedLeastSquares( - mesh, alpha_s=1.0, alpha_x=1.0, mapping=wires.m2 + mesh, alpha_s=1.0, alpha_x=1.0, mapping=wires.m2, weights={"cell_weights": wr2} ) -reg2.cell_weights = wr2 + reg = reg1 + reg2 opt = optimization.ProjectedGNCG( 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 180685ef32..dd42c3938d 100644 --- a/examples/20-published/plot_booky_1D_time_freq_inv.py +++ b/examples/20-published/plot_booky_1D_time_freq_inv.py @@ -7,7 +7,7 @@ `https://storage.googleapis.com/simpeg/bookpurnong/bookpurnong.tar.gz `_ The forward simulation is performed on the cylindrically symmetric mesh using -:code:`SimPEG.electromagnetics.frequency_domain`, and :code:`SimPEG.electromagnetics.time_domain` +:code:`simpeg.electromagnetics.frequency_domain`, and :code:`simpeg.electromagnetics.time_domain` The RESOLVE data are inverted first. This recovered model is then used as a reference model for the SkyTEM inversion @@ -36,7 +36,7 @@ from pymatsolver import Pardiso as Solver import discretize -from SimPEG import ( +from simpeg import ( maps, utils, data_misfit, @@ -47,7 +47,7 @@ directives, data, ) -from SimPEG.electromagnetics import frequency_domain as FDEM, time_domain as TDEM +from simpeg.electromagnetics import frequency_domain as FDEM, time_domain as TDEM def download_and_unzip_data( @@ -261,7 +261,7 @@ def run(plotIt=True, saveFig=False, cleanup=True): inv = inversion.BaseInversion(invProb, directiveList=[target]) reg.alpha_s = 1e-3 reg.alpha_x = 1.0 - reg.mref = m0.copy() + reg.reference_model = m0.copy() opt.LSshorten = 0.5 opt.remember("xc") # run the inversion @@ -379,7 +379,7 @@ def run(plotIt=True, saveFig=False, cleanup=True): reg.alpha_x = 1.0 opt.LSshorten = 0.5 opt.remember("xc") - reg.mref = mopt_re # Use RESOLVE model as a reference model + reg.reference_model = mopt_re # Use RESOLVE model as a reference model # run the inversion mopt_sky = inv.run(m0) diff --git a/examples/20-published/plot_booky_1Dstitched_resolve_inv.py b/examples/20-published/plot_booky_1Dstitched_resolve_inv.py index fc10a3317c..ec900f6da8 100644 --- a/examples/20-published/plot_booky_1Dstitched_resolve_inv.py +++ b/examples/20-published/plot_booky_1Dstitched_resolve_inv.py @@ -7,7 +7,7 @@ `https://storage.googleapis.com/simpeg/bookpurnong/bookpurnong.tar.gz `_ The forward simulation is performed on the cylindrically symmetric mesh using -:code:`SimPEG.electromagnetics.frequency_domain`. +:code:`simpeg.electromagnetics.frequency_domain`. Lindsey J. Heagy, Rowan Cockett, Seogi Kang, Gudni K. Rosenkjaer, Douglas W. Oldenburg, A framework for simulation and inversion in electromagnetics, @@ -32,7 +32,7 @@ from scipy.spatial import cKDTree import discretize -from SimPEG import ( +from simpeg import ( maps, utils, data_misfit, @@ -43,7 +43,7 @@ directives, data, ) -from SimPEG.electromagnetics import frequency_domain as FDEM +from simpeg.electromagnetics import frequency_domain as FDEM def download_and_unzip_data( @@ -127,7 +127,7 @@ def resolve_1Dinversions( # regularization regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) reg = regularization.WeightedLeastSquares(regMesh) - reg.mref = mref + reg.reference_model = mref # optimization opt = optimization.InexactGaussNewton(maxIter=10) diff --git a/examples/20-published/plot_effective_medium_theory.py b/examples/20-published/plot_effective_medium_theory.py index 75ec5c89e8..f3f2d4b604 100644 --- a/examples/20-published/plot_effective_medium_theory.py +++ b/examples/20-published/plot_effective_medium_theory.py @@ -5,7 +5,7 @@ This example uses Self Consistent Effective Medium Theory to estimate the electrical conductivity of a mixture of two phases of materials. Given the electrical conductivity of each of the phases (:math:`\sigma_0`, -:math:`\sigma_1`), the :class:`SimPEG.maps.SelfConsistentEffectiveMedium` +:math:`\sigma_1`), the :class:`simpeg.maps.SelfConsistentEffectiveMedium` map takes the concentration of phase-1 (:math:`\phi_1`) and maps this to an electrical conductivity. @@ -20,7 +20,7 @@ import numpy as np import matplotlib.pyplot as plt -from SimPEG import maps +from simpeg import maps from matplotlib import rcParams rcParams["font.size"] = 12 diff --git a/examples/20-published/plot_heagyetal2017_casing.py b/examples/20-published/plot_heagyetal2017_casing.py index e37f06be34..e9f817c04d 100644 --- a/examples/20-published/plot_heagyetal2017_casing.py +++ b/examples/20-published/plot_heagyetal2017_casing.py @@ -30,10 +30,11 @@ This example was updated for SimPEG 0.14.0 on January 31st, 2020 by Joseph Capriotti """ + import discretize -from SimPEG import utils, maps, tests -from SimPEG.electromagnetics import frequency_domain as FDEM, mu_0 -from SimPEG.utils.io_utils import download +from simpeg import utils, maps, tests +from simpeg.electromagnetics import frequency_domain as FDEM, mu_0 +from simpeg.utils.io_utils import download # try: # from pymatsolver import MumpsSolver as Solver @@ -42,7 +43,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver import numpy as np import scipy.sparse as sp @@ -184,7 +185,7 @@ def meshp(self): # cell size, number of core cells, number of padding cells in the # x-direction - ncz = np.int(np.ceil(np.diff(self.casing_z)[0] / csz)) + 10 + ncz = int(np.ceil(np.diff(self.casing_z)[0] / csz)) + 10 npadzu, npadzd = 43, 43 # vector of cell widths in the z-direction @@ -265,8 +266,8 @@ def primaryMapping(self): expMapPrimary * injActMapPrimary # log(sigma) --> sigma * paramMapPrimary # log(sigma) below surface --> include air - * injectCasingParams # parametric --> casing + layered earth - * # parametric layered earth --> parametric + * injectCasingParams # parametric --> casing + layered earth # parametric layered earth --> parametric + * # layered earth + casing self.projectionMapPrimary # grab relevant parameters from full # model (eg. ignore block) @@ -618,7 +619,7 @@ def solveSecondary(self, sec_problem, sec_survey, m, plotIt=False): fields = sec_problem.fields(m) dpred = sec_problem.dpred(m, f=fields) t1 = time.time() - print(" ...done. secondary time "), t1 - t0 + print(f" ...done. secondary time {t1 - t0}") return fields, dpred diff --git a/examples/20-published/plot_heagyetal2017_cyl_inversions.py b/examples/20-published/plot_heagyetal2017_cyl_inversions.py index 98e04747a4..53f328aeaf 100644 --- a/examples/20-published/plot_heagyetal2017_cyl_inversions.py +++ b/examples/20-published/plot_heagyetal2017_cyl_inversions.py @@ -18,8 +18,9 @@ This example was updated for SimPEG 0.14.0 on January 31st, 2020 by Joseph Capriotti """ + import discretize -from SimPEG import ( +from simpeg import ( maps, utils, data_misfit, @@ -30,14 +31,14 @@ directives, ) import numpy as np -from SimPEG.electromagnetics import frequency_domain as FDEM, time_domain as TDEM, mu_0 +from simpeg.electromagnetics import frequency_domain as FDEM, time_domain as TDEM, mu_0 import matplotlib.pyplot as plt import matplotlib try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver def run(plotIt=True, saveFig=False): diff --git a/examples/20-published/plot_laguna_del_maule_inversion.py b/examples/20-published/plot_laguna_del_maule_inversion.py index 0edd61c3c2..d4dd2ffca5 100644 --- a/examples/20-published/plot_laguna_del_maule_inversion.py +++ b/examples/20-published/plot_laguna_del_maule_inversion.py @@ -11,11 +11,12 @@ Craig Miller """ + import os import shutil import tarfile -from SimPEG.potential_fields import gravity -from SimPEG import ( +from simpeg.potential_fields import gravity +from simpeg import ( data_misfit, maps, regularization, @@ -24,11 +25,11 @@ directives, inversion, ) -from SimPEG.utils import download, plot2Ddata +from simpeg.utils import download, plot2Ddata import matplotlib.pyplot as plt import numpy as np -from SimPEG.utils.drivers.gravity_driver import GravityDriver_Inv +from simpeg.utils.drivers.gravity_driver import GravityDriver_Inv def run(plotIt=True, cleanAfterRun=True): @@ -96,9 +97,9 @@ def run(plotIt=True, cleanAfterRun=True): # %% Create inversion objects reg = regularization.Sparse( - mesh, active_cells=active, mapping=staticCells, gradientType="total" + mesh, active_cells=active, mapping=staticCells, gradient_type="total" ) - reg.mref = driver.mref[dynamic] + reg.reference_model = driver.mref[dynamic] reg.norms = [0.0, 1.0, 1.0, 1.0] # reg.norms = driver.lpnorms diff --git a/examples/20-published/plot_load_booky.py b/examples/20-published/plot_load_booky.py index c582c5b245..7708279cf6 100644 --- a/examples/20-published/plot_load_booky.py +++ b/examples/20-published/plot_load_booky.py @@ -25,7 +25,7 @@ import tarfile import os import shutil -from SimPEG import utils +from simpeg import utils import discretize diff --git a/examples/20-published/plot_richards_celia1990.py b/examples/20-published/plot_richards_celia1990.py index 798ec47149..2c93764cd2 100644 --- a/examples/20-published/plot_richards_celia1990.py +++ b/examples/20-published/plot_richards_celia1990.py @@ -39,12 +39,13 @@ .. _Celia1990: http://www.webpages.uidaho.edu/ch/papers/Celia.pdf """ + import matplotlib.pyplot as plt import numpy as np import discretize -from SimPEG import maps -from SimPEG.flow import richards +from simpeg import maps +from simpeg.flow import richards def run(plotIt=True): diff --git a/examples/20-published/plot_schenkel_morrison_casing.py b/examples/20-published/plot_schenkel_morrison_casing.py index c1410a3f7d..6654e0ad08 100644 --- a/examples/20-published/plot_schenkel_morrison_casing.py +++ b/examples/20-published/plot_schenkel_morrison_casing.py @@ -35,7 +35,7 @@ and achieve the CPU time of ~30s is Mumps, which was installed using pymatsolver_ -.. _pymatsolver: https://github.com/rowanc1/pymatsolver +.. _pymatsolver: https://github.com/simpeg/pymatsolver This example is on figshare: https://dx.doi.org/10.6084/m9.figshare.3126961.v1 @@ -44,17 +44,18 @@ a citation would be much appreciated! """ + import matplotlib.pylab as plt import numpy as np import discretize -from SimPEG import maps, utils -from SimPEG.electromagnetics import frequency_domain as FDEM +from simpeg import maps, utils +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 + from simpeg import SolverLU as Solver def run(plotIt=True): @@ -105,7 +106,7 @@ def run(plotIt=True): nza = 10 # cell size, number of core cells, number of padding cells in the # x-direction - ncz, npadzu, npadzd = np.int(np.ceil(np.diff(casing_z)[0] / csz)) + 10, 68, 68 + ncz, npadzu, npadzd = int(np.ceil(np.diff(casing_z)[0] / csz)) + 10, 68, 68 # vector of cell widths in the z-direction hz = utils.unpack_widths([(csz, npadzd, -1.3), (csz, ncz), (csz, npadzu, 1.3)]) diff --git a/examples/20-published/plot_tomo_joint_with_volume.py b/examples/20-published/plot_tomo_joint_with_volume.py index f9d678ed4f..1de1d798f4 100644 --- a/examples/20-published/plot_tomo_joint_with_volume.py +++ b/examples/20-published/plot_tomo_joint_with_volume.py @@ -22,9 +22,9 @@ import scipy.sparse as sp import matplotlib.pyplot as plt -from SimPEG.seismic import straight_ray_tomography as tomo +from simpeg.seismic import straight_ray_tomography as tomo import discretize -from SimPEG import ( +from simpeg import ( maps, utils, regularization, @@ -42,7 +42,7 @@ class Volume(objective_function.BaseObjectiveFunction): .. math:: - \phi_v = \frac{1}{2}|| \int_V m dV - \text{knownVolume} ||^2 + \phi_v = || \int_V m dV - \text{knownVolume} ||^2 """ def __init__(self, mesh, knownVolume=0.0, **kwargs): @@ -60,25 +60,27 @@ def knownVolume(self, value): self._knownVolume = utils.validate_float("knownVolume", value, min_val=0.0) def __call__(self, m): - return 0.5 * (self.estVol(m) - self.knownVolume) ** 2 + return (self.estVol(m) - self.knownVolume) ** 2 def estVol(self, m): return np.inner(self.mesh.cell_volumes, m) def deriv(self, m): # return (self.mesh.cell_volumes * np.inner(self.mesh.cell_volumes, m)) - return self.mesh.cell_volumes * ( - self.knownVolume - np.inner(self.mesh.cell_volumes, m) - ) + return ( + 2 + * self.mesh.cell_volumes + * (self.knownVolume - np.inner(self.mesh.cell_volumes, m)) + ) # factor of 2 from deriv of ||estVol - knownVol||^2 def deriv2(self, m, v=None): if v is not None: - return utils.mkvc( + return 2 * utils.mkvc( self.mesh.cell_volumes * np.inner(self.mesh.cell_volumes, v) ) else: # TODO: this is inefficent. It is a fully dense matrix - return sp.csc_matrix( + return 2 * sp.csc_matrix( np.outer(self.mesh.cell_volumes, self.mesh.cell_volumes) ) @@ -101,7 +103,7 @@ def run(plotIt=True): # phi model phi0 = 0 phi1 = 0.65 - phitrue = utils.model_builder.defineBlock( + phitrue = utils.model_builder.create_block_in_wholespace( M.gridCC, [0.4, 0.6], [0.6, 0.4], [phi1, phi0] ) diff --git a/examples/20-published/plot_vadose_vangenuchten.py b/examples/20-published/plot_vadose_vangenuchten.py index 95b8d10af3..03f3ea997b 100644 --- a/examples/20-published/plot_vadose_vangenuchten.py +++ b/examples/20-published/plot_vadose_vangenuchten.py @@ -10,10 +10,11 @@ The RETC code for quantifying the hydraulic functions of unsaturated soils, Van Genuchten, M Th, Leij, F J, Yates, S R """ + import matplotlib.pyplot as plt import discretize -from SimPEG.flow import richards +from simpeg.flow import richards def run(plotIt=True): diff --git a/examples/_archived/plot_inv_dcip_2_5Dinversion.py b/examples/_archived/plot_inv_dcip_2_5Dinversion.py index 4ea9868683..717cf0cb5c 100644 --- a/examples/_archived/plot_inv_dcip_2_5Dinversion.py +++ b/examples/_archived/plot_inv_dcip_2_5Dinversion.py @@ -17,10 +17,10 @@ subsequent IP inversion to recover a chargeability model. """ -from SimPEG.electromagnetics.static import resistivity as DC -from SimPEG.electromagnetics.static import induced_polarization as IP -from SimPEG.electromagnetics.static.utils import generate_dcip_survey, genTopography -from SimPEG import maps, utils +from simpeg.electromagnetics.static import resistivity as DC +from simpeg.electromagnetics.static import induced_polarization as IP +from simpeg.electromagnetics.static.utils import generate_dcip_survey, genTopography +from simpeg import maps, utils from discretize.utils import active_from_xyz import matplotlib.pyplot as plt from matplotlib import colors @@ -30,7 +30,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver def run(plotIt=True, survey_type="dipole-dipole"): @@ -64,13 +64,13 @@ def run(plotIt=True, survey_type="dipole-dipole"): survey_dc.drape_electrodes_on_topography(mesh, actind, option="top") # Build conductivity and chargeability model - blk_inds_c = utils.model_builder.getIndicesSphere( + blk_inds_c = utils.model_builder.get_indices_sphere( np.r_[60.0, -25.0], 12.5, mesh.gridCC ) - blk_inds_r = utils.model_builder.getIndicesSphere( + blk_inds_r = utils.model_builder.get_indices_sphere( np.r_[140.0, -25.0], 12.5, mesh.gridCC ) - blk_inds_charg = utils.model_builder.getIndicesSphere( + blk_inds_charg = utils.model_builder.get_indices_sphere( np.r_[100.0, -25], 12.5, mesh.gridCC ) sigma = np.ones(mesh.nC) * 1.0 / 100.0 diff --git a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py index ca907925e4..dafcfee6a9 100644 --- a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py +++ b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion.py @@ -12,9 +12,9 @@ 'dipole-pole', and 'pole-pole'. """ -from SimPEG.electromagnetics.static import resistivity as DC -from SimPEG.electromagnetics.static.utils import generate_dcip_survey, genTopography -from SimPEG import ( +from simpeg.electromagnetics.static import resistivity as DC +from simpeg.electromagnetics.static.utils import generate_dcip_survey, genTopography +from simpeg import ( maps, utils, data_misfit, @@ -33,7 +33,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver def run(plotIt=True, survey_type="dipole-dipole"): @@ -66,10 +66,10 @@ def run(plotIt=True, survey_type="dipole-dipole"): survey.drape_electrodes_on_topography(mesh, actind, option="top") # Build a conductivity model - blk_inds_c = utils.model_builder.getIndicesSphere( + blk_inds_c = utils.model_builder.get_indices_sphere( np.r_[60.0, -25.0], 12.5, mesh.gridCC ) - blk_inds_r = utils.model_builder.getIndicesSphere( + blk_inds_r = utils.model_builder.get_indices_sphere( np.r_[140.0, -25.0], 12.5, mesh.gridCC ) sigma = np.ones(mesh.nC) * 1.0 / 100.0 @@ -146,7 +146,7 @@ def run(plotIt=True, survey_type="dipole-dipole"): regmap = maps.IdentityMap(nP=int(actind.sum())) # Related to inversion - reg = regularization.Sparse(mesh, indActive=actind, mapping=regmap) + reg = regularization.Sparse(mesh, active_cells=actind, mapping=regmap) opt = optimization.InexactGaussNewton(maxIter=15) invProb = inverse_problem.BaseInvProblem(dmisfit, reg, opt) beta = directives.BetaSchedule(coolingFactor=5, coolingRate=2) 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 6873c2c38f..a2c67c696a 100644 --- a/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py +++ b/examples/_archived/plot_inv_dcip_dipoledipole_2_5Dinversion_irls.py @@ -20,9 +20,9 @@ """ -from SimPEG.electromagnetics.static import resistivity as DC -from SimPEG.electromagnetics.static.utils import generate_dcip_survey, genTopography -from SimPEG import ( +from simpeg.electromagnetics.static import resistivity as DC +from simpeg.electromagnetics.static.utils import generate_dcip_survey, genTopography +from simpeg import ( maps, utils, data_misfit, @@ -41,7 +41,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver def run(plotIt=True, survey_type="dipole-dipole", p=0.0, qx=2.0, qz=2.0): @@ -74,10 +74,10 @@ def run(plotIt=True, survey_type="dipole-dipole", p=0.0, qx=2.0, qz=2.0): survey.drape_electrodes_on_topography(mesh, actind, option="top") # Build a conductivity model - blk_inds_c = utils.model_builder.getIndicesSphere( + blk_inds_c = utils.model_builder.get_indices_sphere( np.r_[60.0, -25.0], 12.5, mesh.gridCC ) - blk_inds_r = utils.model_builder.getIndicesSphere( + blk_inds_r = utils.model_builder.get_indices_sphere( np.r_[140.0, -25.0], 12.5, mesh.gridCC ) sigma = np.ones(mesh.nC) * 1.0 / 100.0 @@ -155,9 +155,8 @@ def run(plotIt=True, survey_type="dipole-dipole", p=0.0, qx=2.0, qz=2.0): # Related to inversion reg = regularization.Sparse( - mesh, indActive=actind, mapping=regmap, gradientType="components" + mesh, active_cells=actind, mapping=regmap, gradient_type="components" ) - # gradientType = 'components' reg.norms = [p, qx, qz, 0.0] IRLS = directives.Update_IRLS( max_irls_iterations=20, minGNiter=1, beta_search=False, fix_Jmatrix=True diff --git a/examples/_archived/plot_inv_grav_linear.py b/examples/_archived/plot_inv_grav_linear.py index d84bcc5bd9..7d6e07ab05 100644 --- a/examples/_archived/plot_inv_grav_linear.py +++ b/examples/_archived/plot_inv_grav_linear.py @@ -6,13 +6,14 @@ with a compact norm """ + import numpy as np import matplotlib.pyplot as plt from discretize import TensorMesh from discretize.utils import active_from_xyz -from SimPEG.potential_fields import gravity -from SimPEG import ( +from simpeg.potential_fields import gravity +from simpeg import ( maps, data, data_misfit, @@ -23,8 +24,8 @@ inversion, ) -from SimPEG import utils -from SimPEG.utils import plot2Ddata +from simpeg import utils +from simpeg.utils import plot2Ddata def run(plotIt=True): @@ -102,7 +103,7 @@ def run(plotIt=True): rxLoc = survey.source_field.receiver_list[0].locations # Create a regularization - reg = regularization.Sparse(mesh, indActive=actv, mapping=idenMap) + reg = regularization.Sparse(mesh, active_cells=actv, mapping=idenMap) reg.norms = [0, 0, 0, 0] # Data misfit function @@ -127,7 +128,7 @@ def run(plotIt=True): ) saveDict = directives.SaveOutputEveryIteration(save_txt=False) update_Jacobi = directives.UpdatePreconditioner() - sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) + sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) inv = inversion.BaseInversion( invProb, directiveList=[ diff --git a/examples/_archived/plot_inv_mag_linear.py b/examples/_archived/plot_inv_mag_linear.py index 09bfe42a64..8656f1dce1 100644 --- a/examples/_archived/plot_inv_mag_linear.py +++ b/examples/_archived/plot_inv_mag_linear.py @@ -6,13 +6,14 @@ with a compact norm """ + import matplotlib.pyplot as plt import numpy as np from discretize import TensorMesh from discretize.utils import active_from_xyz -from SimPEG.potential_fields import magnetics -from SimPEG import utils -from SimPEG import ( +from simpeg.potential_fields import magnetics +from simpeg import utils +from simpeg import ( data, data_misfit, maps, @@ -26,7 +27,7 @@ def run(plotIt=True): # Define the inducing field parameter - H0 = (50000, 90, 0) + h0_amplitude, h0_inclination, h0_declination = (50000, 90, 0) # Create a mesh dx = 5.0 @@ -64,7 +65,12 @@ def run(plotIt=True): # Create a MAGsurvey rxLoc = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] rxLoc = magnetics.receivers.Point(rxLoc, components=["tmi"]) - srcField = magnetics.sources.SourceField(receiver_list=[rxLoc], parameters=H0) + srcField = magnetics.sources.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) survey = magnetics.survey.Survey(srcField) # We can now create a susceptibility model and generate data @@ -99,7 +105,7 @@ def run(plotIt=True): data_object = data.Data(survey, dobs=synthetic_data, noise_floor=wd) # Create a regularization - reg = regularization.Sparse(mesh, indActive=actv, mapping=idenMap) + reg = regularization.Sparse(mesh, active_cells=actv, mapping=idenMap) reg.mref = np.zeros(nC) reg.norms = [0, 0, 0, 0] # reg.eps_p, reg.eps_q = 1e-0, 1e-0 @@ -126,7 +132,7 @@ def run(plotIt=True): saveDict = directives.SaveOutputEveryIteration(save_txt=False) update_Jacobi = directives.UpdatePreconditioner() # Add sensitivity weights - sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) + sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) inv = inversion.BaseInversion( invProb, diff --git a/pyproject.toml b/pyproject.toml index 4768216d17..309bfd5e54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "Mira-SimPEG" -version = "0.19.0.9-alpha.1" +version = "0.21.2.1-alpha.1" license = "MIT" description = "Mira Geoscience fork of SimPEG: Simulation and Parameter Estimation in Geophysics" @@ -31,7 +31,7 @@ classifiers = [ ] packages = [ - { include = "SimPEG" }, + { include = "simpeg" }, ] exclude = ["tests*", "examples*", "tutorials*"] diff --git a/requirements_dev.txt b/requirements_dev.txt index 7ccd8f573d..5bab4a0c4b 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -24,3 +24,4 @@ pre-commit twine memory_profiler plotly +setuptools_scm diff --git a/requirements_style.txt b/requirements_style.txt index 24c9cae2b4..a4fd699571 100644 --- a/requirements_style.txt +++ b/requirements_style.txt @@ -1,7 +1,7 @@ black==24.3.0 -flake8 -flake8-bugbear -flake8-builtins -flake8-mutable -flake8-rst-docstrings -flake8-docstrings +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/SimPEG/__init__.py b/simpeg/__init__.py similarity index 81% rename from SimPEG/__init__.py rename to simpeg/__init__.py index 4f390f6886..0dc76dab2d 100644 --- a/SimPEG/__init__.py +++ b/simpeg/__init__.py @@ -1,8 +1,8 @@ """ ======================================================== -Base SimPEG Classes (:mod:`SimPEG`) +Base SimPEG Classes (:mod:`simpeg`) ======================================================== -.. currentmodule:: SimPEG +.. currentmodule:: simpeg SimPEG is built off of several base classes that define the general structure of simulations and inversion operations. @@ -78,6 +78,7 @@ maps.InjectActiveCells maps.MuRelative maps.LogMap + maps.LogisticSigmoidMap maps.ParametricBlock maps.ParametricCircleMap maps.ParametricEllipsoid @@ -148,6 +149,7 @@ from . import regularization from . import survey from . import simulation +from . import typing from . import utils from .utils import mkvc @@ -163,7 +165,21 @@ SolverBiCG, ) -__version__ = "0.19.0" __author__ = "SimPEG Team" __license__ = "MIT" -__copyright__ = "2013 - 2020, SimPEG Team, http://simpeg.xyz" +__copyright__ = "2013 - 2024, SimPEG Team, https://simpeg.xyz" + + +# Version +try: + # - Released versions just tags: 0.8.0 + # - GitHub commits add .dev#+hash: 0.8.1.dev4+g2785721 + # - Uncommitted changes add timestamp: 0.8.1.dev4+g2785721.d20191022 + from simpeg.version import version as __version__ +except ImportError: + # If it was not installed, then we don't know the version. We could throw a + # warning here, but this case *should* be rare. SimPEG should be + # installed properly! + from datetime import datetime + + __version__ = "unknown-" + datetime.today().strftime("%Y%m%d") diff --git a/SimPEG/base/__init__.py b/simpeg/base/__init__.py similarity index 100% rename from SimPEG/base/__init__.py rename to simpeg/base/__init__.py diff --git a/SimPEG/base/pde_simulation.py b/simpeg/base/pde_simulation.py similarity index 80% rename from SimPEG/base/pde_simulation.py rename to simpeg/base/pde_simulation.py index cf1e2ac081..f8739dcef3 100644 --- a/SimPEG/base/pde_simulation.py +++ b/simpeg/base/pde_simulation.py @@ -1,6 +1,6 @@ import numpy as np import scipy.sparse as sp -from discretize.utils import Zero +from discretize.utils import Zero, TensorType from ..simulation import BaseSimulation from .. import props from scipy.constants import mu_0 @@ -8,41 +8,75 @@ def __inner_mat_mul_op(M, u, v=None, adjoint=False): u = np.squeeze(u) - if v is not None: - if u.ndim > 1: - if u.shape[1] > 1 and not isinstance(u, (sp.csr_matrix, sp.csc_matrix, sp.coo_matrix)): + + if sp.issparse(M): + if v is not None: + if v.ndim > 1: + v = np.squeeze(v) + if u.ndim > 1: # u has multiple fields if v.ndim == 1: - v = sp.diags(v) + v = v[:, None] + if adjoint and v.shape[1] != u.shape[1] and v.shape[1] > 1: + # make sure v is a good shape + v = v.reshape(u.shape[0], -1, u.shape[1]) else: - u = u[:, 0] - if u.ndim == 1: - if v.ndim > 1: - if v.shape[1] == 1: - v = v[:, 0] - else: - u = sp.diags(u) - if v.ndim > 2: - u = u[:, :, None] - if adjoint: - if u.ndim > 1 and u.shape[1] > 1 and not isinstance(u, sp.dia_matrix): - return M.T * ( - u[:, None, :] * - v.reshape((u.shape[0], -1, u.shape[1])) - ).sum(axis=2) - return M.T * (u * v) - if u.ndim > 1 and u.shape[1] > 1: - return np.squeeze(u[:, None, :] * (M * v)[:, :, None]) - return u * (M * v) - else: + if v.ndim > 1: + u = u[:, None] + if v.ndim > 2: + u = u[:, None, :] + if adjoint: + if u.ndim > 1 and u.shape[-1] > 1: + return M.T * (u * v).sum(axis=-1) + return M.T * (u * v) + if u.ndim > 1 and u.shape[1] > 1: + return np.squeeze(u[:, None, :] * (M * v)[:, :, None]) + return u * (M * v) + + else: + if u.ndim > 1: + UM = sp.vstack([sp.diags(u[:, i]) @ M for i in range(u.shape[1])]) + else: + U = sp.diags(u, format="csr") + UM = U @ M + if adjoint: + return UM.T + return UM + elif isinstance(M, tuple): + # assume it was a tuple of M_func, prop_deriv + M_deriv_func, prop_deriv = M if u.ndim > 1: - UM = sp.vstack([sp.diags(u[:, i]) @ M for i in range(u.shape[1])]) + Mu = [M_deriv_func(u[:, i]) for i in range(u.shape[1])] + if v is None: + Mu = sp.vstack([M @ prop_deriv for M in Mu]) + if adjoint: + return Mu.T + return Mu + elif v.ndim > 1: + v = np.squeeze(v) + if adjoint: + return sum( + [prop_deriv.T @ (Mu[i].T @ v[..., i]) for i in range(u.shape[1])] + ) + pv = prop_deriv @ v + return np.stack([M @ pv for M in Mu], axis=-1) else: - U = sp.diags(u, format="csr") - UM = U @ M - if adjoint: - return UM.T - return UM + Mu = M_deriv_func(u) + if v is None: + Mu = Mu @ prop_deriv + if adjoint: + return Mu.T + return Mu + elif v.ndim > 1: + v = np.squeeze(v) + if adjoint: + return prop_deriv.T @ (Mu.T @ v) + return Mu @ (prop_deriv @ v) + else: + raise TypeError( + "The stashed property derivative is an unexpected type. Expected either a `tuple` or a " + f"sparse matrix. Received a {type(M)}." + ) def with_property_mass_matrices(property_name): @@ -239,10 +273,22 @@ def MfDeriv_prop(self, u, v=None, adjoint=False): return Zero() stash_name = f"_Mf_{arg}_deriv" if getattr(self, stash_name, None) is None: - M_prop_deriv = self.mesh.get_face_inner_product_deriv( - np.ones(self.mesh.n_cells) - )(np.ones(self.mesh.n_faces)) * getattr(self, f"{arg.lower()}Deriv") - setattr(self, stash_name, M_prop_deriv) + prop = getattr(self, arg.lower()) + t_type = TensorType(self.mesh, prop) + + M_deriv_func = self.mesh.get_face_inner_product_deriv(model=prop) + prop_deriv = getattr(self, f"{arg.lower()}Deriv") + # t_type == 3 for full tensor model, t_type < 3 for scalar, isotropic, or axis-aligned anisotropy. + if t_type < 3 and self.mesh._meshType.lower() in ( + "cyl", + "tensor", + "tree", + ): + M_prop_deriv = M_deriv_func(np.ones(self.mesh.n_faces)) @ prop_deriv + 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 ) @@ -259,10 +305,21 @@ def MeDeriv_prop(self, u, v=None, adjoint=False): return Zero() stash_name = f"_Me_{arg}_deriv" if getattr(self, stash_name, None) is None: - M_prop_deriv = self.mesh.get_edge_inner_product_deriv( - np.ones(self.mesh.n_cells) - )(np.ones(self.mesh.n_edges)) * getattr(self, f"{arg.lower()}Deriv") - setattr(self, stash_name, M_prop_deriv) + prop = getattr(self, arg.lower()) + t_type = TensorType(self.mesh, prop) + + M_deriv_func = self.mesh.get_edge_inner_product_deriv(model=prop) + prop_deriv = getattr(self, f"{arg.lower()}Deriv") + # t_type == 3 for full tensor model, t_type < 3 for scalar, isotropic, or axis-aligned anisotropy. + if t_type < 3 and self.mesh._meshType.lower() in ( + "cyl", + "tensor", + "tree", + ): + M_prop_deriv = M_deriv_func(np.ones(self.mesh.n_edges)) @ prop_deriv + 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 ) diff --git a/simpeg/dask/__init__.py b/simpeg/dask/__init__.py new file mode 100644 index 0000000000..f5a00b7334 --- /dev/null +++ b/simpeg/dask/__init__.py @@ -0,0 +1,19 @@ +try: + import simpeg.dask.simulation + import simpeg.dask.electromagnetics.frequency_domain.simulation + import simpeg.dask.electromagnetics.static.resistivity.simulation + import simpeg.dask.electromagnetics.static.resistivity.simulation_2d + import simpeg.dask.electromagnetics.static.induced_polarization.simulation + import simpeg.dask.electromagnetics.static.induced_polarization.simulation_2d + import simpeg.dask.electromagnetics.time_domain.simulation + import simpeg.dask.potential_fields.base + import simpeg.dask.potential_fields.gravity.simulation + import simpeg.dask.potential_fields.magnetics.simulation + import simpeg.dask.simulation + import simpeg.dask.data_misfit + import simpeg.dask.inverse_problem + import simpeg.dask.objective_function + +except ImportError as err: + print("unable to load dask operations") + print(err) diff --git a/SimPEG/dask/data_misfit.py b/simpeg/dask/data_misfit.py similarity index 97% rename from SimPEG/dask/data_misfit.py rename to simpeg/dask/data_misfit.py index d68b571881..6ac67a6031 100644 --- a/SimPEG/dask/data_misfit.py +++ b/simpeg/dask/data_misfit.py @@ -2,7 +2,7 @@ from ..data_misfit import L2DataMisfit from ..fields import Fields -from ..utils import mkvc, sdiag +from ..utils import mkvc from .utils import compute import dask.array as da from scipy.sparse import csr_matrix as csr @@ -19,6 +19,7 @@ def dask_call(self, m, f=None): return compute(self, phi_d) return phi_d + L2DataMisfit.__call__ = dask_call @@ -92,4 +93,4 @@ def dask_residual(self, m, f=None): raise Exception(f"Attribute f must be or type {Fields}, numpy.array or None.") -L2DataMisfit.residual = dask_residual \ No newline at end of file +L2DataMisfit.residual = dask_residual diff --git a/SimPEG/dask/electromagnetics/__init__.py b/simpeg/dask/electromagnetics/__init__.py similarity index 100% rename from SimPEG/dask/electromagnetics/__init__.py rename to simpeg/dask/electromagnetics/__init__.py diff --git a/SimPEG/dask/electromagnetics/frequency_domain/__init__.py b/simpeg/dask/electromagnetics/frequency_domain/__init__.py similarity index 100% rename from SimPEG/dask/electromagnetics/frequency_domain/__init__.py rename to simpeg/dask/electromagnetics/frequency_domain/__init__.py diff --git a/SimPEG/dask/electromagnetics/frequency_domain/simulation.py b/simpeg/dask/electromagnetics/frequency_domain/simulation.py similarity index 90% rename from SimPEG/dask/electromagnetics/frequency_domain/simulation.py rename to simpeg/dask/electromagnetics/frequency_domain/simulation.py index a93884556d..eb8527ed8a 100644 --- a/SimPEG/dask/electromagnetics/frequency_domain/simulation.py +++ b/simpeg/dask/electromagnetics/frequency_domain/simulation.py @@ -3,17 +3,17 @@ import numpy as np import scipy.sparse as sp from multiprocessing import cpu_count -from dask import array, compute, delayed, config -from dask.distributed import get_client, Client, performance_report -from SimPEG.dask.simulation import dask_Jvec, dask_Jtvec, dask_getJtJdiag -from SimPEG.dask.utils import get_parallel_blocks -from SimPEG.electromagnetics.natural_source.sources import PlanewaveXYPrimary +from dask import array, compute, delayed + +# from dask.distributed import get_client, Client, performance_report +from simpeg.dask.simulation import dask_Jvec, dask_Jtvec, dask_getJtJdiag +from simpeg.dask.utils import get_parallel_blocks +from simpeg.electromagnetics.natural_source.sources import PlanewaveXYPrimary import zarr from tqdm import tqdm Sim.sensitivity_path = "./sensitivity/" Sim.gtgdiag = None -Sim.store_sensitivities = True Sim.getJtJdiag = dask_getJtJdiag Sim.Jvec = dask_Jvec @@ -48,9 +48,9 @@ def dask_getSourceTerm(self, freq, source=None): block_compute.append(source_evaluation(self, block)) - eval = compute(block_compute)[0] + blocks = compute(block_compute)[0] s_m, s_e = [], [] - for block in eval: + for block in blocks: if block[0]: s_m += block[0] s_e += block[1] @@ -81,14 +81,14 @@ def dask_getSourceTerm(self, freq, source=None): @delayed def evaluate_receivers(block, mesh, fields): data = [] - for source, ind, receiver in block: + for source, _, receiver in block: data.append(receiver.eval(source, mesh, fields).flatten()) return np.hstack(data) def dask_dpred(self, m=None, f=None, compute_J=False): - """ + r""" dpred(m, f=None) Create the projected data from a model. The fields, f, (if provided) will be used for the predicted data @@ -110,7 +110,7 @@ def dask_dpred(self, m=None, f=None, compute_J=False): if f is None: if m is None: m = self.model - f, Ainv = self.fields(m, return_Ainv=compute_J) + f = self.fields(m, return_Ainv=compute_J) all_receivers = [] @@ -118,11 +118,11 @@ def dask_dpred(self, m=None, f=None, compute_J=False): for rx in src.receiver_list: all_receivers.append((src, ind, rx)) - receiver_blocks = np.array_split(all_receivers, cpu_count()) + receiver_blocks = np.array_split(np.asarray(all_receivers), cpu_count()) rows = [] mesh = delayed(self.mesh) for block in receiver_blocks: - n_data = np.sum(rec.nD for _, _, rec in block) + n_data = np.sum([rec.nD for _, _, rec in block]) if n_data == 0: continue @@ -137,7 +137,7 @@ def dask_dpred(self, m=None, f=None, compute_J=False): data = compute(array.hstack(rows))[0] if compute_J and self._Jmatrix is None: - Jmatrix = self.compute_J(f=f, Ainv=Ainv) + Jmatrix = self.compute_J(f=f) return data, Jmatrix return data @@ -167,25 +167,25 @@ def fields(self, m=None, return_Ainv=False): Ainv_solve.clean() if return_Ainv: - return f, Ainv - else: - return f, None + self.Ainv = Ainv + + return f Sim.fields = fields -def compute_J(self, f=None, Ainv=None): +def compute_J(self, f=None): if f is None: - f, Ainv = self.fields(self.model, return_Ainv=True) + f = self.fields(self.model, return_Ainv=True) - if len(Ainv) > 1: + if len(self.Ainv) > 1: raise NotImplementedError( "Current implementation of parallelization assumes a single frequency per simulation. " "Consider creating one misfit per frequency." ) - A_i = list(Ainv.values())[0] + A_i = list(self.Ainv.values())[0] m_size = self.model.size if self.store_sensitivities == "disk": @@ -227,13 +227,13 @@ def compute_J(self, f=None, Ainv=None): for block_derivs_chunks, addresses_chunks in tqdm( zip(blocks_receiver_derivs, blocks), ncols=len(blocks_receiver_derivs), - desc=f"Sensitivities at {list(Ainv)[0]} Hz", + desc=f"Sensitivities at {list(self.Ainv)[0]} Hz", ): Jmatrix = parallel_block_compute( self, Jmatrix, block_derivs_chunks, A_i, fields_array, addresses_chunks ) - for A in Ainv.values(): + for A in self.Ainv.values(): A.clean() if self.store_sensitivities == "disk": diff --git a/SimPEG/dask/electromagnetics/static/__init__.py b/simpeg/dask/electromagnetics/static/__init__.py similarity index 100% rename from SimPEG/dask/electromagnetics/static/__init__.py rename to simpeg/dask/electromagnetics/static/__init__.py diff --git a/SimPEG/dask/electromagnetics/static/induced_polarization/__init__.py b/simpeg/dask/electromagnetics/static/induced_polarization/__init__.py similarity index 100% rename from SimPEG/dask/electromagnetics/static/induced_polarization/__init__.py rename to simpeg/dask/electromagnetics/static/induced_polarization/__init__.py diff --git a/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py b/simpeg/dask/electromagnetics/static/induced_polarization/simulation.py similarity index 80% rename from SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py rename to simpeg/dask/electromagnetics/static/induced_polarization/simulation.py index 588a226ddb..81f2db5a0b 100644 --- a/SimPEG/dask/electromagnetics/static/induced_polarization/simulation.py +++ b/simpeg/dask/electromagnetics/static/induced_polarization/simulation.py @@ -1,8 +1,7 @@ -import scipy.sparse as sp - from .....electromagnetics.static.induced_polarization.simulation import ( BaseIPSimulation as Sim, ) + from .....data import Data import dask.array as da from dask.distributed import Future @@ -11,10 +10,11 @@ numcodecs.blosc.use_threads = False -Sim.sensitivity_path = './sensitivity/' +Sim.sensitivity_path = "./sensitivity/" from ..resistivity.simulation import ( - compute_J, dask_getSourceTerm, + compute_J, + dask_getSourceTerm, ) Sim.compute_J = compute_J @@ -25,7 +25,6 @@ def dask_fields(self, m=None, return_Ainv=False): if m is not None: self.model = m - A = self.getA() Ainv = self.solver(A, **self.solver_opts) RHS = self.getRHS() @@ -33,31 +32,29 @@ def dask_fields(self, m=None, return_Ainv=False): f = self.fieldsPair(self) f[:, self._solutionType] = Ainv * RHS - Ainv.clean() - if self._scale is None: scale = Data(self.survey, np.ones(self.survey.nD)) # loop through receivers to check if they need to set the _dc_voltage for src in self.survey.source_list: for rx in src.receiver_list: if ( - rx.data_type == "apparent_chargeability" - or self._data_type == "apparent_chargeability" + rx.data_type == "apparent_chargeability" + or self._data_type == "apparent_chargeability" ): scale[src, rx] = 1.0 / rx.eval(src, self.mesh, f) self._scale = scale.dobs if return_Ainv: - return f, self.solver(sp.csr_matrix(A.T), **self.solver_opts) - else: - return f, None + self.Ainv = Ainv + + return f Sim.fields = dask_fields def dask_dpred(self, m=None, f=None, compute_J=False): - """ + r""" dpred(m, f=None) Create the projected data from a model. The fields, f, (if provided) will be used for the predicted data @@ -78,8 +75,8 @@ def dask_dpred(self, m=None, f=None, compute_J=False): if self._Jmatrix is None or self._scale is None: if m is None: m = self.model - f, Ainv = self.fields(m, return_Ainv=True) - self._Jmatrix = self.compute_J(f=f, Ainv=Ainv) + f = self.fields(m, return_Ainv=True) + self._Jmatrix = self.compute_J(f=f) data = self.Jvec(m, m) @@ -94,19 +91,19 @@ def dask_dpred(self, m=None, f=None, compute_J=False): def dask_getJtJdiag(self, m, W=None): """ - Return the diagonal of JtJ + Return the diagonal of JtJ """ self.model = m - if self._jtjdiag is None: + if getattr(self, "_jtjdiag", None) is None: if isinstance(self.Jmatrix, Future): self.Jmatrix # Wait to finish if W is None: W = self._scale * np.ones(self.nD) else: - W = (self._scale * W.diagonal())**2.0 + W = (self._scale * W.diagonal()) ** 2.0 - diag = da.einsum('i,ij,ij->j', W, self.Jmatrix, self.Jmatrix) + diag = da.einsum("i,ij,ij->j", W, self.Jmatrix, self.Jmatrix) if isinstance(diag, da.Array): diag = np.asarray(diag.compute()) @@ -121,7 +118,7 @@ def dask_getJtJdiag(self, m, W=None): def dask_Jvec(self, m, v, f=None): """ - Compute sensitivity matrix (J) and vector (v) product. + Compute sensitivity matrix (J) and vector (v) product. """ self.model = m @@ -139,7 +136,7 @@ def dask_Jvec(self, m, v, f=None): def dask_Jtvec(self, m, v, f=None): """ - Compute adjoint sensitivity matrix (J^T) and vector (v) product. + Compute adjoint sensitivity matrix (J^T) and vector (v) product. """ self.model = m @@ -151,5 +148,5 @@ def dask_Jtvec(self, m, v, f=None): return da.dot(v * self._scale, self.Jmatrix).astype(np.float32) -Sim.Jtvec = dask_Jtvec +Sim.Jtvec = dask_Jtvec diff --git a/SimPEG/dask/electromagnetics/static/induced_polarization/simulation_2d.py b/simpeg/dask/electromagnetics/static/induced_polarization/simulation_2d.py similarity index 68% rename from SimPEG/dask/electromagnetics/static/induced_polarization/simulation_2d.py rename to simpeg/dask/electromagnetics/static/induced_polarization/simulation_2d.py index 3015df0d2e..963fc12451 100644 --- a/SimPEG/dask/electromagnetics/static/induced_polarization/simulation_2d.py +++ b/simpeg/dask/electromagnetics/static/induced_polarization/simulation_2d.py @@ -1,5 +1,3 @@ -import scipy.sparse as sp - from .....electromagnetics.static.induced_polarization.simulation import ( Simulation2DNodal as Sim, ) @@ -9,14 +7,9 @@ numcodecs.blosc.use_threads = False -Sim.sensitivity_path = './sensitivity/' +Sim.sensitivity_path = "./sensitivity/" -from .simulation import ( - dask_getJtJdiag, - dask_Jvec, - dask_Jtvec, - dask_dpred -) +from .simulation import dask_getJtJdiag, dask_Jvec, dask_Jtvec, dask_dpred from ..resistivity.simulation_2d import compute_J, dask_getSourceTerm @@ -36,15 +29,13 @@ def dask_fields(self, m=None, return_Ainv=False): f = self.fieldsPair(self) f._quad_weights = self._quad_weights - Ainv_out = {} + Ainv = {} for iky, ky in enumerate(kys): A = self.getA(ky) - Ainv = self.solver(A, **self.solver_opts) - Ainv_out[iky] = self.solver(sp.csr_matrix(A.T), **self.solver_opts) - RHS = self.getRHS(ky) - f[:, self._solutionType, iky] = Ainv * RHS + Ainv[iky] = self.solver(A, **self.solver_opts) - Ainv.clean() + RHS = self.getRHS(ky) + f[:, self._solutionType, iky] = Ainv[iky] * RHS if self._scale is None: scale = Data(self.survey, np.ones(self.survey.nD)) @@ -53,18 +44,16 @@ def dask_fields(self, m=None, return_Ainv=False): for src in self.survey.source_list: for rx in src.receiver_list: if ( - rx.data_type == "apparent_chargeability" - or self._data_type == "apparent_chargeability" + rx.data_type == "apparent_chargeability" + or self._data_type == "apparent_chargeability" ): scale[src, rx] = 1.0 / rx.eval(src, self.mesh, f_fwd) self._scale = scale.dobs if return_Ainv: - return f, Ainv_out - else: - return f, None + self.Ainv = Ainv - -Sim.fields = dask_fields + return f +Sim.fields = dask_fields diff --git a/SimPEG/dask/electromagnetics/static/resistivity/__init__.py b/simpeg/dask/electromagnetics/static/resistivity/__init__.py similarity index 100% rename from SimPEG/dask/electromagnetics/static/resistivity/__init__.py rename to simpeg/dask/electromagnetics/static/resistivity/__init__.py diff --git a/SimPEG/dask/electromagnetics/static/resistivity/simulation.py b/simpeg/dask/electromagnetics/static/resistivity/simulation.py similarity index 75% rename from SimPEG/dask/electromagnetics/static/resistivity/simulation.py rename to simpeg/dask/electromagnetics/static/resistivity/simulation.py index 4f0deaa24f..d82a0f2198 100644 --- a/SimPEG/dask/electromagnetics/static/resistivity/simulation.py +++ b/simpeg/dask/electromagnetics/static/resistivity/simulation.py @@ -1,16 +1,15 @@ -from SimPEG.dask.simulation import dask_dpred, dask_Jvec, dask_Jtvec, dask_getJtJdiag +from simpeg.dask.simulation import dask_dpred, dask_Jvec, dask_Jtvec, dask_getJtJdiag from .....electromagnetics.static.resistivity.simulation import BaseDCSimulation as Sim from .....utils import Zero import dask.array as da import numpy as np -import scipy.sparse as sp import zarr import numcodecs numcodecs.blosc.use_threads = False -Sim.sensitivity_path = './sensitivity/' +Sim.sensitivity_path = "./sensitivity/" Sim.dpred = dask_dpred Sim.getJtJdiag = dask_getJtJdiag @@ -30,33 +29,34 @@ def dask_fields(self, m=None, return_Ainv=False): f = self.fieldsPair(self) f[:, self._solutionType] = Ainv * RHS - Ainv.clean() - if return_Ainv: - return f, self.solver(sp.csr_matrix(A.T), **self.solver_opts) - else: - return f, None + self.Ainv = Ainv + + return f Sim.fields = dask_fields -def compute_J(self, f=None, Ainv=None): +def compute_J(self, f=None): if f is None: - f, Ainv = self.fields(self.model, return_Ainv=True) + f = self.fields(self.model, return_Ainv=True) m_size = self.model.size - row_chunks = int(np.ceil( - float(self.survey.nD) / np.ceil(float(m_size) * self.survey.nD * 8. * 1e-6 / self.max_chunk_size) - )) + row_chunks = int( + np.ceil( + float(self.survey.nD) + / np.ceil(float(m_size) * self.survey.nD * 8.0 * 1e-6 / self.max_chunk_size) + ) + ) if self.store_sensitivities == "disk": Jmatrix = zarr.open( - self.sensitivity_path + f"J.zarr", - mode='w', + self.sensitivity_path + "J.zarr", + mode="w", shape=(self.survey.nD, m_size), - chunks=(row_chunks, m_size) + chunks=(row_chunks, m_size), ) else: Jmatrix = np.zeros((self.survey.nD, m_size), dtype=np.float32) @@ -76,10 +76,14 @@ def compute_J(self, f=None, Ainv=None): PTv = rx.getP(self.mesh, projected_grid).toarray().T for dd in range(int(np.ceil(PTv.shape[1] / row_chunks))): - start, end = dd*row_chunks, np.min([(dd+1)*row_chunks, PTv.shape[1]]) + start, end = dd * row_chunks, np.min( + [(dd + 1) * row_chunks, PTv.shape[1]] + ) df_duTFun = getattr(f, "_{0!s}Deriv".format(rx.projField), None) - df_duT, df_dmT = df_duTFun(source, None, PTv[:, start:end], adjoint=True) - ATinvdf_duT = Ainv * df_duT + df_duT, df_dmT = df_duTFun( + source, None, PTv[:, start:end], adjoint=True + ) + ATinvdf_duT = self.Ainv * df_duT dA_dmT = self.getADeriv(u_source, ATinvdf_duT, adjoint=True) dRHS_dmT = self.getRHSDeriv(source, ATinvdf_duT, adjoint=True) du_dmT = -dA_dmT @@ -101,12 +105,12 @@ def compute_J(self, f=None, Ainv=None): if self.store_sensitivities == "disk": Jmatrix.set_orthogonal_selection( (np.arange(count, count + row_chunks), slice(None)), - blocks[:row_chunks, :].astype(np.float32) + blocks[:row_chunks, :].astype(np.float32), ) else: - Jmatrix[count: count + row_chunks, :] = ( - blocks[:row_chunks, :].astype(np.float32) - ) + Jmatrix[count : count + row_chunks, :] = blocks[ + :row_chunks, : + ].astype(np.float32) blocks = blocks[row_chunks:, :].astype(np.float32) count += row_chunks @@ -118,19 +122,16 @@ def compute_J(self, f=None, Ainv=None): if self.store_sensitivities == "disk": Jmatrix.set_orthogonal_selection( (np.arange(count, self.survey.nD), slice(None)), - blocks.astype(np.float32) + blocks.astype(np.float32), ) else: - Jmatrix[count: self.survey.nD, :] = ( - blocks.astype(np.float32) - ) - + Jmatrix[count : self.survey.nD, :] = blocks.astype(np.float32) - Ainv.clean() + self.Ainv.clean() if self.store_sensitivities == "disk": del Jmatrix - return da.from_zarr(self.sensitivity_path + f"J.zarr") + return da.from_zarr(self.sensitivity_path + "J.zarr") else: return Jmatrix diff --git a/SimPEG/dask/electromagnetics/static/resistivity/simulation_2d.py b/simpeg/dask/electromagnetics/static/resistivity/simulation_2d.py similarity index 75% rename from SimPEG/dask/electromagnetics/static/resistivity/simulation_2d.py rename to simpeg/dask/electromagnetics/static/resistivity/simulation_2d.py index c28e6d2cbd..08b5ba08de 100644 --- a/SimPEG/dask/electromagnetics/static/resistivity/simulation_2d.py +++ b/simpeg/dask/electromagnetics/static/resistivity/simulation_2d.py @@ -1,6 +1,6 @@ -import scipy.sparse as sp - -from .....electromagnetics.static.resistivity.simulation_2d import BaseDCSimulation2D as Sim +from .....electromagnetics.static.resistivity.simulation_2d import ( + BaseDCSimulation2D as Sim, +) from .simulation import dask_getJtJdiag, dask_Jvec, dask_Jtvec import dask.array as da import numpy as np @@ -9,7 +9,7 @@ numcodecs.blosc.use_threads = False -Sim.sensitivity_path = './sensitivity/' +Sim.sensitivity_path = "./sensitivity/" Sim.getJtJdiag = dask_getJtJdiag Sim.Jvec = dask_Jvec @@ -25,42 +25,50 @@ def dask_fields(self, m=None, return_Ainv=False): f = self.fieldsPair(self) f._quad_weights = self._quad_weights - Ainv_out = {} + Ainv = {} for iky, ky in enumerate(kys): A = self.getA(ky) - Ainv = self.solver(A, **self.solver_opts) - Ainv_out[iky] = self.solver(sp.csr_matrix(A.T), **self.solver_opts) - RHS = self.getRHS(ky) - f[:, self._solutionType, iky] = Ainv * RHS + Ainv[iky] = self.solver(A, **self.solver_opts) - Ainv.clean() + RHS = self.getRHS(ky) + f[:, self._solutionType, iky] = Ainv[iky] * RHS if return_Ainv: - return f, Ainv_out - else: - return f, None + self.Ainv = Ainv + + return f Sim.fields = dask_fields -def compute_J(self, f=None, Ainv=None): +def compute_J(self, f=None): kys = self._quad_points weights = self._quad_weights if f is None: - f, Ainv = self.fields(self.model, return_Ainv=True) + f = self.fields(self.model, return_Ainv=True) m_size = self.model.size - row_chunks = int(np.ceil( - float(self.survey.nD) / np.ceil(float(m_size) * self.survey.nD * len(kys) * 8. * 1e-6 / self.max_chunk_size) - )) + row_chunks = int( + np.ceil( + float(self.survey.nD) + / np.ceil( + float(m_size) + * self.survey.nD + * len(kys) + * 8.0 + * 1e-6 + / self.max_chunk_size + ) + ) + ) if self.store_sensitivities == "disk": Jmatrix = zarr.open( - self.sensitivity_path + f"J.zarr", - mode='w', + self.sensitivity_path + "J.zarr", + mode="w", shape=(self.survey.nD, m_size), - chunks=(row_chunks, m_size) + chunks=(row_chunks, m_size), ) else: Jmatrix = np.zeros((self.survey.nD, m_size), dtype=np.float32) @@ -79,13 +87,15 @@ def compute_J(self, f=None, Ainv=None): PTv = rx.getP(self.mesh, projected_grid).toarray().T for dd in range(int(np.ceil(PTv.shape[1] / row_chunks))): - start, end = dd * row_chunks, np.min([(dd + 1) * row_chunks, PTv.shape[1]]) - block = np.zeros((end-start, m_size)) + start, end = dd * row_chunks, np.min( + [(dd + 1) * row_chunks, PTv.shape[1]] + ) + block = np.zeros((end - start, m_size)) for iky, ky in enumerate(kys): u_ky = f[:, self._solutionType, iky] u_source = u_ky[:, i_src] - ATinvdf_duT = Ainv[iky] * PTv[:, start:end] + ATinvdf_duT = self.Ainv[iky] * PTv[:, start:end] dA_dmT = self.getADeriv(ky, u_source, ATinvdf_duT, adjoint=True) du_dmT = -weights[iky] * dA_dmT block += du_dmT.T.reshape((-1, m_size)) @@ -99,12 +109,12 @@ def compute_J(self, f=None, Ainv=None): if self.store_sensitivities == "disk": Jmatrix.set_orthogonal_selection( (np.arange(count, count + row_chunks), slice(None)), - blocks[:row_chunks, :].astype(np.float32) + blocks[:row_chunks, :].astype(np.float32), ) else: - Jmatrix[count: count + row_chunks, :] = ( - blocks[:row_chunks, :].astype(np.float32) - ) + Jmatrix[count : count + row_chunks, :] = blocks[ + :row_chunks, : + ].astype(np.float32) blocks = blocks[row_chunks:, :].astype(np.float32) count += row_chunks @@ -115,19 +125,17 @@ def compute_J(self, f=None, Ainv=None): if self.store_sensitivities == "disk": Jmatrix.set_orthogonal_selection( (np.arange(count, self.survey.nD), slice(None)), - blocks.astype(np.float32) + blocks.astype(np.float32), ) else: - Jmatrix[count: self.survey.nD, :] = ( - blocks.astype(np.float32) - ) + Jmatrix[count : self.survey.nD, :] = blocks.astype(np.float32) - for iky, ky in enumerate(kys): - Ainv[iky].clean() + for iky, _ in enumerate(kys): + self.Ainv[iky].clean() if self.store_sensitivities == "disk": del Jmatrix - return da.from_zarr(self.sensitivity_path + f"J.zarr") + return da.from_zarr(self.sensitivity_path + "J.zarr") else: return Jmatrix @@ -136,7 +144,7 @@ def compute_J(self, f=None, Ainv=None): def dask_dpred(self, m=None, f=None, compute_J=False): - """ + r""" dpred(m, f=None) Create the projected data from a model. The fields, f, (if provided) will be used for the predicted data @@ -164,18 +172,18 @@ def dask_dpred(self, m=None, f=None, compute_J=False): if f is None: if m is None: m = self.model - f, Ainv = self.fields(m, return_Ainv=compute_J) + f = self.fields(m, return_Ainv=compute_J) temp = np.empty(survey.nD) count = 0 for src in survey.source_list: for rx in src.receiver_list: d = rx.eval(src, self.mesh, f).dot(weights) - temp[count: count + len(d)] = d + temp[count : count + len(d)] = d count += len(d) if compute_J: - Jmatrix = self.compute_J(f=f, Ainv=Ainv) + Jmatrix = self.compute_J(f=f) return self._mini_survey_data(temp), Jmatrix return self._mini_survey_data(temp) @@ -216,5 +224,3 @@ def dask_getSourceTerm(self, _): Sim.getSourceTerm = dask_getSourceTerm - - diff --git a/SimPEG/dask/electromagnetics/time_domain/__init__.py b/simpeg/dask/electromagnetics/time_domain/__init__.py similarity index 100% rename from SimPEG/dask/electromagnetics/time_domain/__init__.py rename to simpeg/dask/electromagnetics/time_domain/__init__.py diff --git a/SimPEG/dask/electromagnetics/time_domain/simulation.py b/simpeg/dask/electromagnetics/time_domain/simulation.py similarity index 92% rename from SimPEG/dask/electromagnetics/time_domain/simulation.py rename to simpeg/dask/electromagnetics/time_domain/simulation.py index bcca19adbe..569d9f6ef1 100644 --- a/SimPEG/dask/electromagnetics/time_domain/simulation.py +++ b/simpeg/dask/electromagnetics/time_domain/simulation.py @@ -3,22 +3,20 @@ import os from ....electromagnetics.time_domain.simulation import BaseTDEMSimulation as Sim from ....utils import Zero -from SimPEG.fields import TimeFields +from simpeg.fields import TimeFields from multiprocessing import cpu_count import numpy as np import scipy.sparse as sp from dask import array, delayed -from SimPEG.dask.simulation import dask_Jvec, dask_Jtvec, dask_getJtJdiag -from SimPEG.dask.utils import get_parallel_blocks -from SimPEG.utils import mkvc -import zarr +from simpeg.dask.simulation import dask_Jvec, dask_Jtvec, dask_getJtJdiag +from simpeg.dask.utils import get_parallel_blocks +from simpeg.utils import mkvc + from time import time from tqdm import tqdm Sim.sensitivity_path = "./sensitivity/" -Sim.store_sensitivities = "ram" - Sim.getJtJdiag = dask_getJtJdiag Sim.Jvec = dask_Jvec Sim.Jtvec = dask_Jtvec @@ -74,7 +72,6 @@ def _getField(self, name, ind, src_list): pointerFields = pointerFields.reshape(pointerShapeDeflated, order="F") out = func(pointerFields, src_list, timeII) else: # loop over the time steps - nT = pointerShape[2] arrays = [] for i, TIND_i in enumerate(timeII): # Need to parallelize this @@ -102,14 +99,11 @@ def fields(self, m=None, return_Ainv=False): f = self.fieldsPair(self) f[:, self._fieldType + "Solution", 0] = self.getInitialFields() Ainv = {} - ATinv = {} for tInd, dt in enumerate(self.time_steps): if dt not in Ainv: A = self.getAdiag(tInd) Ainv[dt] = self.solver(sp.csr_matrix(A), **self.solver_opts) - if return_Ainv: - ATinv[dt] = self.solver(sp.csr_matrix(A.T), **self.solver_opts) Asubdiag = self.getAsubdiag(tInd) rhs = -Asubdiag * f[:, (self._fieldType + "Solution"), tInd] @@ -123,23 +117,20 @@ def fields(self, m=None, return_Ainv=False): sol = Ainv[dt] * rhs f[:, self._fieldType + "Solution", tInd + 1] = sol - for A in Ainv.values(): - A.clean() - if return_Ainv: - return f, ATinv - else: - return f, None + self.Ainv = Ainv + + return f Sim.fields = fields @delayed -def source_evaluation(simulation, sources, time): +def source_evaluation(simulation, sources, time_channel): s_m, s_e = [], [] for source in sources: - sm, se = source.eval(simulation, time) + sm, se = source.eval(simulation, time_channel) s_m.append(sm) s_e.append(se) @@ -158,10 +149,10 @@ def dask_getSourceTerm(self, tInd): for block in source_block: block_compute.append(source_evaluation(self, block, self.times[tInd])) - eval = dask.compute(block_compute)[0] + blocks = dask.compute(block_compute)[0] s_m, s_e = [], [] - for block in eval: + for block in blocks: if block[0]: s_m.append(block[0]) s_e.append(block[1]) @@ -178,7 +169,7 @@ def dask_getSourceTerm(self, tInd): @delayed def evaluate_receivers(block, mesh, time_mesh, fields, fields_array): data = [] - for source, ind, receiver in block: + for _, ind, receiver in block: Ps = receiver.getSpatialP(mesh, fields) Pt = receiver.getTimeP(time_mesh, fields) vector = (Pt * (Ps * fields_array[:, ind, :]).T).flatten() @@ -189,7 +180,7 @@ def evaluate_receivers(block, mesh, time_mesh, fields, fields_array): def dask_dpred(self, m=None, f=None, compute_J=False): - """ + r""" dpred(m, f=None) Create the projected data from a model. The fields, f, (if provided) will be used for the predicted data @@ -211,7 +202,7 @@ def dask_dpred(self, m=None, f=None, compute_J=False): if f is None: if m is None: m = self.model - f, Ainv = self.fields(m, return_Ainv=compute_J) + f = self.fields(m, return_Ainv=compute_J) rows = [] receiver_projection = self.survey.source_list[0].receiver_list[0].projField @@ -229,7 +220,7 @@ def dask_dpred(self, m=None, f=None, compute_J=False): receiver_blocks = np.array_split(all_receivers, cpu_count()) for block in receiver_blocks: - n_data = np.sum(rec.nD for _, _, rec in block) + n_data = np.sum([rec.nD for _, _, rec in block]) if n_data == 0: continue @@ -244,7 +235,7 @@ def dask_dpred(self, m=None, f=None, compute_J=False): data = array.hstack(rows).compute() if compute_J and self._Jmatrix is None: - Jmatrix = self.compute_J(f=f, Ainv=Ainv) + Jmatrix = self.compute_J(f=f) return data, Jmatrix return data @@ -341,7 +332,7 @@ def compute_field_derivs(simulation, fields, blocks, Jmatrix, fields_shape): if len(j_updates.data) > 0: Jmatrix += j_updates if simulation.store_sensitivities == "disk": - sens_name = simulation.sensitivity_path[:-5] + f"_{time_index % 2}.zarr" + sens_name = simulation.sensitivity_path[:-5] + f"_{time() % 2}.zarr" array.to_zarr(Jmatrix, sens_name, compute=True, overwrite=True) Jmatrix = array.from_zarr(sens_name) @@ -399,7 +390,7 @@ def get_field_deriv_block( if tInd < simulation.nT - 1: Asubdiag = simulation.getAsubdiag(tInd + 1) - for ((s_id, r_id, b_id), (rx_ind, j_ind, shape)), field_deriv in zip( + for ((s_id, r_id, b_id), (rx_ind, _, shape)), field_deriv in zip( block, field_derivs ): # Cut out early data @@ -440,7 +431,6 @@ def get_field_deriv_block( else: solve = None - update_list = [] for (address, arrays), field_deriv in zip(block, field_derivs): shape = ( field_deriv.shape[0], @@ -464,7 +454,6 @@ def compute_rows( """ Compute the rows of the sensitivity matrix for a given source and receiver. """ - n_rows = np.sum(len(chunk[1][0]) for chunk in chunks) rows = [] for address, ind_array in chunks: @@ -507,12 +496,12 @@ def compute_rows( return np.vstack(rows) -def compute_J(self, f=None, Ainv=None): +def compute_J(self, f=None): """ Compute the rows for the sensitivity matrix. """ if f is None: - f, Ainv = self.fields(self.model, return_Ainv=True) + f = self.fields(self.model, return_Ainv=True) ftype = self._fieldType + "Solution" sens_name = self.sensitivity_path[:-5] @@ -547,14 +536,13 @@ def compute_J(self, f=None, Ainv=None): ATinv_df_duT_v = {} for tInd, dt in tqdm(zip(reversed(range(self.nT)), reversed(self.time_steps))): - AdiagTinv = Ainv[dt] + AdiagTinv = self.Ainv[dt] j_row_updates = [] time_mask = data_times > simulation_times[tInd] if not np.any(time_mask): continue - tc = time() for block, field_deriv in zip(blocks, times_field_derivs[tInd + 1]): ATinv_df_duT_v = get_field_deriv_block( self, block, field_deriv, tInd, AdiagTinv, ATinv_df_duT_v, time_mask @@ -575,7 +563,7 @@ def compute_J(self, f=None, Ainv=None): ), dtype=np.float32, shape=( - np.sum(len(chunk[1][0]) for chunk in block), + np.sum([len(chunk[1][0]) for chunk in block]), self.model.size, ), ) @@ -593,7 +581,7 @@ def compute_J(self, f=None, Ainv=None): else: Jmatrix += array.vstack(j_row_updates).compute() - for A in Ainv.values(): + for A in self.Ainv.values(): A.clean() if self.store_sensitivities == "ram": diff --git a/SimPEG/dask/inverse_problem.py b/simpeg/dask/inverse_problem.py similarity index 97% rename from SimPEG/dask/inverse_problem.py rename to simpeg/dask/inverse_problem.py index 5b0be973dc..e62b856971 100644 --- a/SimPEG/dask/inverse_problem.py +++ b/simpeg/dask/inverse_problem.py @@ -5,7 +5,6 @@ from dask.distributed import Future, get_client import dask.array as da from scipy.sparse.linalg import LinearOperator -from ..simulation import LinearSimulation from ..regularization import WeightedLeastSquares, Sparse from ..data_misfit import BaseDataMisfit from ..objective_function import BaseObjectiveFunction, ComboObjectiveFunction @@ -14,11 +13,11 @@ def dask_getFields(self, m, store=False, deleteWarmstart=True): f = None - try: - client = get_client() - fields = lambda f, x, workers: client.compute(f(x), workers=workers) - except: - fields = lambda f, x: f(x) + # try: + # client = get_client() + # fields = lambda f, x, workers: client.compute(f(x), workers=workers) + # except: + # fields = lambda f, x: f(x) for mtest, u_ofmtest in self.warmstart: if m is mtest: diff --git a/SimPEG/dask/objective_function.py b/simpeg/dask/objective_function.py similarity index 92% rename from SimPEG/dask/objective_function.py rename to simpeg/dask/objective_function.py index fe742f328b..72864df5bc 100644 --- a/SimPEG/dask/objective_function.py +++ b/simpeg/dask/objective_function.py @@ -1,17 +1,17 @@ from ..objective_function import ComboObjectiveFunction, BaseObjectiveFunction -import dask + import dask.array as da -import os -import shutil + import numpy as np from dask.distributed import Future, get_client, Client from ..data_misfit import L2DataMisfit + BaseObjectiveFunction._workers = None @property def client(self): - if getattr(self, '_client', None) is None: + if getattr(self, "_client", None) is None: self._client = get_client() return self._client @@ -69,9 +69,7 @@ def dask_call(self, m, f=None): ).result() return phi else: - return np.sum( - np.r_[multipliers][:, None] * np.vstack(fcts), axis=0 - ).squeeze() + return np.sum(np.r_[multipliers][:, None] * np.vstack(fcts), axis=0).squeeze() ComboObjectiveFunction.__call__ = dask_call @@ -117,9 +115,7 @@ def dask_deriv(self, m, f=None): return self.client.compute(big_future).result() else: - return np.sum( - np.r_[multipliers][:, None] * np.vstack(g), axis=0 - ).squeeze() + return np.sum(np.r_[multipliers][:, None] * np.vstack(g), axis=0).squeeze() ComboObjectiveFunction.deriv = dask_deriv @@ -138,7 +134,7 @@ def dask_deriv2(self, m, v=None, f=None): H = [] multipliers = [] - for i, phi in enumerate(self): + for phi in self: multiplier, objfct = phi if multiplier == 0.0: # don't evaluate the fct continue diff --git a/SimPEG/dask/potential_fields/__init__.py b/simpeg/dask/potential_fields/__init__.py similarity index 100% rename from SimPEG/dask/potential_fields/__init__.py rename to simpeg/dask/potential_fields/__init__.py diff --git a/SimPEG/dask/potential_fields/base.py b/simpeg/dask/potential_fields/base.py similarity index 95% rename from SimPEG/dask/potential_fields/base.py rename to simpeg/dask/potential_fields/base.py index 717eb59719..3c5d05aedd 100644 --- a/SimPEG/dask/potential_fields/base.py +++ b/simpeg/dask/potential_fields/base.py @@ -43,7 +43,7 @@ def dask_residual(self, m, dobs, f=None): def dask_linear_operator(self): - forward_only = self.store_sensitivities is None + forward_only = self.store_sensitivities == "forward_only" row = delayed(self.evaluate_integral, pure=True) n_cells = self.nC if getattr(self, "model_type", None) == "vector": @@ -52,7 +52,7 @@ def dask_linear_operator(self): rows = [ array.from_delayed( row(receiver_location, components), - dtype=np.float32, + dtype=self.sensitivity_dtype, shape=(len(components),) if forward_only else (len(components), n_cells), ) for receiver_location, components in self.survey._location_component_iterator() @@ -112,3 +112,10 @@ def dask_linear_operator(self): Sim.linear_operator = dask_linear_operator + + +def compute_J(self): + return self.linear_operator() + + +Sim.compute_J = compute_J diff --git a/SimPEG/dask/potential_fields/gravity/__init__.py b/simpeg/dask/potential_fields/gravity/__init__.py similarity index 100% rename from SimPEG/dask/potential_fields/gravity/__init__.py rename to simpeg/dask/potential_fields/gravity/__init__.py diff --git a/SimPEG/dask/potential_fields/gravity/simulation.py b/simpeg/dask/potential_fields/gravity/simulation.py similarity index 100% rename from SimPEG/dask/potential_fields/gravity/simulation.py rename to simpeg/dask/potential_fields/gravity/simulation.py diff --git a/SimPEG/dask/potential_fields/magnetics/__init__.py b/simpeg/dask/potential_fields/magnetics/__init__.py similarity index 100% rename from SimPEG/dask/potential_fields/magnetics/__init__.py rename to simpeg/dask/potential_fields/magnetics/__init__.py diff --git a/SimPEG/dask/potential_fields/magnetics/simulation.py b/simpeg/dask/potential_fields/magnetics/simulation.py similarity index 100% rename from SimPEG/dask/potential_fields/magnetics/simulation.py rename to simpeg/dask/potential_fields/magnetics/simulation.py diff --git a/SimPEG/dask/simulation.py b/simpeg/dask/simulation.py similarity index 97% rename from SimPEG/dask/simulation.py rename to simpeg/dask/simulation.py index 06b69a0ae9..3c78f9e98e 100644 --- a/SimPEG/dask/simulation.py +++ b/simpeg/dask/simulation.py @@ -79,6 +79,7 @@ def make_synthetic_data( "The std parameter will be deprecated in SimPEG 0.15.0. " "Please use relative_error.", DeprecationWarning, + stacklevel=2, ) relative_error = std @@ -186,7 +187,7 @@ def Jmatrix(self): elif isinstance(self._Jmatrix, Future): self._Jmatrix.result() if self.store_sensitivities == "disk": - self._Jmatrix = array.from_zarr(self.sensitivity_path + f"J.zarr") + self._Jmatrix = array.from_zarr(self.sensitivity_path + "J.zarr") return self._Jmatrix @@ -195,7 +196,7 @@ def Jmatrix(self): def dask_dpred(self, m=None, f=None, compute_J=False): - """ + r""" dpred(m, f=None) Create the projected data from a model. The fields, f, (if provided) will be used for the predicted data @@ -217,7 +218,7 @@ def dask_dpred(self, m=None, f=None, compute_J=False): if f is None: if m is None: m = self.model - f, Ainv = self.fields(m, return_Ainv=compute_J) + f = self.fields(m, return_Ainv=compute_J) def evaluate_receiver(source, receiver, mesh, fields): return receiver.eval(source, mesh, fields).flatten() @@ -237,7 +238,7 @@ def evaluate_receiver(source, receiver, mesh, fields): data = array.hstack(rows).compute() if compute_J and self._Jmatrix is None: - Jmatrix = self.compute_J(f=f, Ainv=Ainv) + Jmatrix = self.compute_J(f=f) return data, Jmatrix return data diff --git a/SimPEG/dask/utils.py b/simpeg/dask/utils.py similarity index 100% rename from SimPEG/dask/utils.py rename to simpeg/dask/utils.py diff --git a/SimPEG/data.py b/simpeg/data.py similarity index 96% rename from SimPEG/data.py rename to simpeg/data.py index 5a45d2ed9b..1d53972caa 100644 --- a/SimPEG/data.py +++ b/simpeg/data.py @@ -15,7 +15,7 @@ class Data: Parameters ---------- - survey : SimPEG.survey.BaseSurvey + survey : simpeg.survey.BaseSurvey A SimPEG survey object. For each geophysical method, the survey object defines the survey geometry; i.e. sources, receivers, data type. dobs : (n) numpy.ndarray @@ -76,7 +76,8 @@ def __init__( if relative_error is not None or noise_floor is not None: warnings.warn( "Setting the standard_deviation overwrites the " - "relative_error and noise_floor" + "relative_error and noise_floor", + stacklevel=2, ) self.standard_deviation = standard_deviation @@ -97,7 +98,7 @@ def survey(self): Returns ------- - SimPEG.simulation.BaseSurvey + simpeg.simulation.BaseSurvey """ return self._survey @@ -114,7 +115,7 @@ def dobs(self): numpy.ndarray Notes - -------- + ----- This array can also be modified by directly indexing the data object using the a tuple of the survey's sources and receivers. @@ -362,24 +363,26 @@ def fromvec(self, v): class SyntheticData(Data): - r""" - Class for creating synthetic data. + r"""Synthetic data class. + + The ``SyntheticData`` class is a :py:class:`simpeg.data.Data` class that allows the + user to keep track of both clean and noisy data. Parameters ---------- - survey : SimPEG.survey.BaseSurvey + survey : simpeg.survey.BaseSurvey A SimPEG survey object. For each geophysical method, the survey object defines the survey geometry; i.e. sources, receivers, data type. dobs : numpy.ndarray Observed data. dclean : (nD) numpy.ndarray Noiseless data. - relative_error : SimPEG.data.UncertaintyArray + relative_error : float or np.ndarray Assign relative uncertainties to the data using relative error; sometimes referred to as percent uncertainties. For each datum, we assume the standard deviation of Gaussian noise is the relative error times the absolute value of the datum; i.e. :math:`C_{err} \times |d|`. - noise_floor : UncertaintyArray + noise_floor : float or np.ndarray Assign floor/absolute uncertainties to the data. For each datum, we assume standard deviation of Gaussian noise is equal to *noise_floor*. """ diff --git a/simpeg/data_misfit.py b/simpeg/data_misfit.py new file mode 100644 index 0000000000..b796f78c21 --- /dev/null +++ b/simpeg/data_misfit.py @@ -0,0 +1,374 @@ +import numpy as np +from .utils import Counter, mkvc, sdiag, timeIt, Identity, validate_type +from .data import Data +from .simulation import BaseSimulation +from .objective_function import L2ObjectiveFunction + +__all__ = ["L2DataMisfit"] + + +class BaseDataMisfit(L2ObjectiveFunction): + r"""Base data misfit class. + + Inherit this class to build your own data misfit function. The ``BaseDataMisfit`` + class inherits the :py:class:`simpeg.objective_function.L2ObjectiveFunction`. + And as a result, it is limited to building data misfit functions of the form: + + .. important:: + This class is not meant to be instantiated. You should inherit from it to + create your own data misfit class. + + .. math:: + \phi_d (\mathbf{m}) = \| \mathbf{W} f(\mathbf{m}) \|_2^2 + + where :math:`\mathbf{m}` is the model vector, :math:`\mathbf{W}` is a linear weighting + matrix, and :math:`f` is a mapping function that acts on the model. + + Parameters + ---------- + data : simpeg.data.Data + A SimPEG data object. + simulation : simpeg.simulation.BaseSimulation + A SimPEG simulation object. + debug : bool + Print debugging information. + counter : None or simpeg.utils.Counter + Assign a SimPEG ``Counter`` object to store iterations and run-times. + """ + + def __init__( + self, data, simulation, model_map=None, debug=False, counter=None, **kwargs + ): + super().__init__(has_fields=True, debug=debug, counter=counter, **kwargs) + + self.data = data + self.simulation = simulation + + self.model_map = model_map + + @property + def model_map(self): + return getattr(self, "_model_map", None) + + @model_map.setter + def model_map(self, value): + if value is None: + value = Identity() + self._model_map = value + self._has_fields = True + + @property + def data(self): + """A SimPEG data object. + + Returns + ------- + simpeg.data.Data + A SimPEG data object. + """ + return self._data + + @data.setter + def data(self, value): + self._data = validate_type("data", value, Data, cast=False) + + @property + def simulation(self): + """A SimPEG simulation object. + + Returns + ------- + simpeg.simulation.BaseSimulation + A SimPEG simulation object. + """ + return self._simulation + + @simulation.setter + def simulation(self, value): + self._simulation = validate_type( + "simulation", value, BaseSimulation, cast=False + ) + + @property + def debug(self): + """Print debugging information. + + Returns + ------- + bool + Print debugging information. + """ + return self._debug + + @debug.setter + def debug(self, value): + self._debug = validate_type("debug", value, bool) + + @property + def counter(self): + """SimPEG ``Counter`` object to store iterations and run-times. + + Returns + ------- + None or simpeg.utils.Counter + SimPEG ``Counter`` object to store iterations and run-times. + """ + return self._counter + + @counter.setter + def counter(self, value): + if value is not None: + value = validate_type("counter", value, Counter, cast=False) + + @property + def nP(self): + """Number of model parameters. + + Returns + ------- + int + Number of model parameters. + """ + if self._mapping is not None: + return self.mapping.nP + elif self.simulation.model is not None: + return len(self.simulation.model) + else: + return "*" + + @property + def nD(self): + """Number of data. + + Returns + ------- + int + Number of data. + """ + return self.data.nD + + @property + def shape(self): + """Shape of the Jacobian. + + The number of data by the number of model parameters. + + Returns + ------- + tuple of int (n_data, n_param) + Shape of the Jacobian; i.e. number of data by the number of model parameters. + """ + return (self.nD, self.nP) + + @property + def W(self): + r"""The data weighting matrix. + + For a discrete least-squares data misfit function of the form: + + .. math:: + \phi_d (\mathbf{m}) = \| \mathbf{W} \mathbf{f}(\mathbf{m}) \|_2^2 + + :math:`\mathbf{W}` is a linear weighting matrix, :math:`\mathbf{m}` is the model vector, + and :math:`\mathbf{f}` is a discrete mapping function that acts on the model vector. + + Returns + ------- + scipy.sparse.csr_matrix + The data weighting matrix. + """ + + if getattr(self, "_W", None) is None: + if self.data is None: + raise Exception( + "data with standard deviations must be set before the data " + "misfit can be constructed. Please set the data: " + "dmis.data = Data(dobs=dobs, relative_error=rel" + ", noise_floor=eps)" + ) + standard_deviation = self.data.standard_deviation + if standard_deviation is None: + raise Exception( + "data standard deviations must be set before the data misfit " + "can be constructed (data.relative_error = 0.05, " + "data.noise_floor = 1e-5), alternatively, the W matrix " + "can be set directly (dmisfit.W = 1./standard_deviation)" + ) + if any(standard_deviation <= 0): + raise Exception( + "data.standard_deviation must be strictly positive to construct " + "the W matrix. Please set data.relative_error and or " + "data.noise_floor." + ) + self._W = sdiag(1 / (standard_deviation)) + return self._W + + @W.setter + def W(self, value): + if isinstance(value, Identity): + value = np.ones(self.data.nD) + if len(value.shape) < 2: + value = sdiag(value) + assert value.shape == ( + self.data.nD, + self.data.nD, + ), "W must have shape ({nD},{nD}), not ({val0}, {val1})".format( + nD=self.data.nD, val0=value.shape[0], val1=value.shape[1] + ) + self._W = value + + def residual(self, m, f=None): + r"""Computes the data residual vector for a given model. + + Where :math:`\mathbf{d}_\text{obs}` is the observed data vector and :math:`\mathbf{d}_\text{pred}` + is the predicted data vector for a model vector :math:`\mathbf{m}`, this function + computes the data residual: + + .. math:: + \mathbf{r} = \mathbf{d}_\text{pred} - \mathbf{d}_\text{obs} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the function is evaluated. + f : None or simpeg.fields.Fields, optional + A SimPEG fields object. Used when the fields for the model *m* have + already been computed. + + Returns + ------- + (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) + + +class L2DataMisfit(BaseDataMisfit): + r"""Least-squares data misfit. + + Define the data misfit as the L2-norm of the weighted residual between observed + data and predicted data for a given model. I.e.: + + .. math:: + \phi_d (\mathbf{m}) = \big \| \mathbf{W_d} + \big ( \mathbf{d}_\text{pred} - \mathbf{d}_\text{obs} \big ) \big \|_2^2 + + where :math:`\mathbf{d}_\text{obs}` is the observed data vector, :math:`\mathbf{d}_\text{pred}` + is the predicted data vector for a model vector :math:`\mathbf{m}`, and + :math:`\mathbf{W_d}` is the data weighting matrix. The diagonal elements of + :math:`\mathbf{W_d}` are the reciprocals of the data uncertainties + :math:`\boldsymbol{\varepsilon}`. Thus: + + .. math:: + \mathbf{W_d} = \text{diag} \left ( \boldsymbol{\varepsilon}^{-1} \right ) + + Parameters + ---------- + data : simpeg.data.Data + A SimPEG data object that has observed data and uncertainties. + simulation : simpeg.simulation.BaseSimulation + A SimPEG simulation object. + debug : bool + Print debugging information. + counter : None or simpeg.utils.Counter + Assign a SimPEG ``Counter`` object to store iterations and run-times. + """ + + @timeIt + 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) + + @timeIt + def deriv(self, m, f=None): + r"""Gradient of the data misfit function evaluated for the model provided. + + Where :math:`\phi_d (\mathbf{m})` is the data misfit function, + this method evaluates and returns the derivative with respect to the model parameters; i.e. + the gradient: + + .. math:: + \frac{\partial \phi_d}{\partial \mathbf{m}} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the gradient is evaluated. + + Returns + ------- + (n_param, ) numpy.ndarray + The gradient of the data misfit function evaluated for the model provided. + """ + + if f is None: + f = self.simulation.fields(m) + + return 2 * self.simulation.Jtvec( + m, self.W.T * (self.W * self.residual(m, f=f)), f=f + ) + + @timeIt + def deriv2(self, m, v, f=None): + r"""Hessian of the data misfit function evaluated for the model provided. + + Where :math:`\phi_d (\mathbf{m})` is the data misfit function, + this method returns the second-derivative (Hessian) with respect to the model parameters: + + .. math:: + \frac{\partial^2 \phi_d}{\partial \mathbf{m}^2} + + or the second-derivative (Hessian) multiplied by a vector :math:`(\mathbf{v})`: + + .. math:: + \frac{\partial^2 \phi_d}{\partial \mathbf{m}^2} \, \mathbf{v} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the Hessian is evaluated. + v : None or (n_param, ) numpy.ndarray, optional + A vector. + + Returns + ------- + (n_param, n_param) scipy.sparse.csr_matrix or (n_param, ) numpy.ndarray + If the input argument *v* is ``None``, the Hessian of the data misfit + function for the model provided is returned. If *v* is not ``None``, + the Hessian multiplied by the vector provided is returned. + """ + + if f is None: + f = self.simulation.fields(m) + + return 2 * self.simulation.Jtvec_approx( + m, self.W * (self.W * self.simulation.Jvec_approx(m, v, f=f)), f=f + ) + + def getJtJdiag(self, m): + """ + Evaluate the main diagonal of JtJ + """ + if getattr(self.simulation, "getJtJdiag", None) is None: + raise AttributeError( + "Simulation does not have a getJtJdiag attribute." + + "Cannot form the sensitivity explicitly" + ) + + mapping_deriv = self.model_map.deriv(m) + + if self.model_map is not None: + m = mapping_deriv @ m + + jtjdiag = self.simulation.getJtJdiag(m, W=self.W) + + if self.model_map is not None: + jtjdiag = mkvc( + (sdiag(np.sqrt(jtjdiag)) @ mapping_deriv).power(2).sum(axis=0) + ) + + return jtjdiag diff --git a/simpeg/directives/__init__.py b/simpeg/directives/__init__.py new file mode 100644 index 0000000000..b0d750d869 --- /dev/null +++ b/simpeg/directives/__init__.py @@ -0,0 +1,137 @@ +""" +============================================= +Directives (:mod:`simpeg.directives`) +============================================= + +.. currentmodule:: simpeg.directives + +Directives are classes that allow us to control the inversion, perform tasks +between iterations, save information about our inversion process and more. +Directives are passed to the ``simpeg.inversion.BaseInversion`` class through +the ``directiveList`` argument. The tasks specified through the directives are +executed after each inversion iteration, following the same order as in which +they are passed in the ``directiveList``. + +Although you can write your own directive classes and plug them into your +inversion, we provide a set of useful directive classes that cover a wide range +of applications: + + +General purpose directives +========================== + +.. autosummary:: + :toctree: generated/ + + AlphasSmoothEstimate_ByEig + BetaEstimateMaxDerivative + BetaEstimate_ByEig + BetaSchedule + JointScalingSchedule + MultiTargetMisfits + ProjectSphericalBounds + ScalingMultipleDataMisfits_ByEig + TargetMisfit + UpdatePreconditioner + UpdateSensitivityWeights + Update_Wj + + +Directives to save inversion results +==================================== + +.. autosummary:: + :toctree: generated/ + + SaveEveryIteration + SaveModelEveryIteration + SaveOutputDictEveryIteration + SaveOutputEveryIteration + + +Directives related to sparse inversions +======================================= + +.. autosummary:: + :toctree: generated/ + + Update_IRLS + + +Directives related to PGI +========================= + +.. autosummary:: + :toctree: generated/ + + PGI_AddMrefInSmooth + PGI_BetaAlphaSchedule + PGI_UpdateParameters + + +Directives related to joint inversions +====================================== + +.. autosummary:: + :toctree: generated/ + + SimilarityMeasureInversionDirective + SimilarityMeasureSaveOutputEveryIteration + PairedBetaEstimate_ByEig + PairedBetaSchedule + MovingAndMultiTargetStopping + + +Base directive classes +====================== +The ``InversionDirective`` class defines the basic class for all directives. +Inherit from this class when writing your own directive. The ``DirectiveList`` +is used under the hood to handle the execution of all directives passed to the +``simpeg.inversion.BaseInversion``. + +.. autosummary:: + :toctree: generated/ + + InversionDirective + DirectiveList + +""" + +from .directives import ( + InversionDirective, + DirectiveList, + BetaEstimateDerivative, + BetaEstimateMaxDerivative, + BetaEstimate_ByEig, + BetaSchedule, + TargetMisfit, + SaveEveryIteration, + SaveModelEveryIteration, + SaveOutputEveryIteration, + SaveOutputDictEveryIteration, + Update_IRLS, + UpdatePreconditioner, + Update_Wj, + AlphasSmoothEstimate_ByEig, + MultiTargetMisfits, + ScalingMultipleDataMisfits_ByEig, + JointScalingSchedule, + UpdateSensitivityWeights, + VectorInversion, + SaveIterationsGeoH5, + ProjectSphericalBounds, +) + +from .pgi_directives import ( + PGI_UpdateParameters, + PGI_BetaAlphaSchedule, + PGI_AddMrefInSmooth, +) + +from .sim_directives import ( + SimilarityMeasureInversionDirective, + SimilarityMeasureSaveOutputEveryIteration, + PairedBetaEstimate_ByEig, + PairedBetaSchedule, + MovingAndMultiTargetStopping, +) diff --git a/SimPEG/directives/directives.py b/simpeg/directives/directives.py similarity index 87% rename from SimPEG/directives/directives.py rename to simpeg/directives/directives.py index 71f1ec6cdf..20997425a9 100644 --- a/SimPEG/directives/directives.py +++ b/simpeg/directives/directives.py @@ -1,10 +1,16 @@ +from __future__ import annotations # needed to use type operands in Python 3.8 + from pathlib import Path +from datetime import datetime + import numpy as np import matplotlib.pyplot as plt import warnings import os import scipy.sparse as sp +from ..typing import RandomSeed + from ..data_misfit import BaseDataMisfit from ..objective_function import ComboObjectiveFunction from ..maps import IdentityMap, SphericalSystem, Wires @@ -16,7 +22,6 @@ Sparse, SparseSmallness, PGIsmallness, - PGIwithNonlinearRelationshipsSmallness, SmoothnessFirstOrder, SparseSmoothness, BaseSimilarityMeasure, @@ -26,7 +31,7 @@ mkvc, set_kwargs, sdiag, - diagEst, + estimate_diagonal, spherical2cartesian, cartesian2spherical, Zero, @@ -34,7 +39,7 @@ validate_string, ) -from SimPEG.utils.mat_utils import cartesian2amplitude_dip_azimuth +from simpeg.utils.mat_utils import cartesian2amplitude_dip_azimuth from ..utils.code_utils import ( deprecate_property, @@ -44,9 +49,8 @@ validate_ndarray_with_shape, ) -from geoh5py.workspace import Workspace from geoh5py.objects import ObjectBase -from datetime import datetime +from geoh5py.ui_json.utils import fetch_active_workspace class InversionDirective: @@ -59,12 +63,12 @@ class InversionDirective: 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`. + 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. """ @@ -75,14 +79,13 @@ class InversionDirective: _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 - debug = kwargs.pop("debug", None) - if debug is not None: - self.debug = debug - else: - self.verbose = verbose + self.verbose = verbose set_kwargs(self, **kwargs) @property @@ -99,7 +102,9 @@ def verbose(self): def verbose(self, value): self._verbose = validate_type("verbose", value, bool) - debug = deprecate_property(verbose, "debug", "verbose", removal_version="0.19.0") + debug = deprecate_property( + verbose, "debug", "verbose", removal_version="0.19.0", error=True + ) @property def inversion(self): @@ -107,7 +112,7 @@ def inversion(self): Returns ------- - SimPEG.inversion.BaseInversion + simpeg.inversion.BaseInversion The inversion associated with the directive. """ if not hasattr(self, "_inversion"): @@ -120,7 +125,8 @@ def inversion(self, i): warnings.warn( "InversionDirective {0!s} has switched to a new inversion.".format( self.__class__.__name__ - ) + ), + stacklevel=2, ) self._inversion = i @@ -130,7 +136,7 @@ def invProb(self): Returns ------- - SimPEG.inverse_problem.BaseInvProblem + simpeg.inverse_problem.BaseInvProblem The inverse problem associated with the directive. """ return self.inversion.invProb @@ -141,7 +147,7 @@ def opt(self): Returns ------- - SimPEG.optimization.Minimize + simpeg.optimization.Minimize Optimization algorithm associated with the directive. """ return self.invProb.opt @@ -152,7 +158,7 @@ def reg(self): Returns ------- - SimPEG.regularization.BaseRegularization + simpeg.regularization.BaseRegularization The regularization associated with the directive. """ if getattr(self, "_reg", None) is None: @@ -176,7 +182,7 @@ def dmisfit(self): Returns ------- - SimPEG.data_misfit.BaseDataMisfit + simpeg.data_misfit.BaseDataMisfit The data misfit associated with the directive. """ if getattr(self, "_dmisfit", None) is None: @@ -204,7 +210,7 @@ def survey(self): Returns ------- - list of SimPEG.survey.Survey + list of simpeg.survey.Survey Survey for all data misfits. """ return [objfcts.simulation.survey for objfcts in self.dmisfit.objfcts] @@ -219,7 +225,7 @@ def simulation(self): Returns ------- - list of SimPEG.simulation.BaseSimulation + list of simpeg.simulation.BaseSimulation Simulation for all data misfits. """ return [objfcts.simulation for objfcts in self.dmisfit.objfcts] @@ -245,7 +251,7 @@ def validate(self, directiveList=None): Parameters ---------- - directive_list : SimPEG.directives.DirectiveList + directive_list : simpeg.directives.DirectiveList List of directives used in the inversion. Returns @@ -265,9 +271,9 @@ class DirectiveList(object): Parameters ---------- - directives : list of SimPEG.directives.InversionDirective + directives : list of simpeg.directives.InversionDirective List of directives. - inversion : SimPEG.inversion.BaseInversion + inversion : simpeg.inversion.BaseInversion The inversion associated with the directives list. debug : bool Whether or not to print debugging information. @@ -307,7 +313,7 @@ def inversion(self): Returns ------- - SimPEG.inversion.BaseInversion + simpeg.inversion.BaseInversion The inversion associated with the directives list. """ return getattr(self, "_inversion", None) @@ -318,7 +324,10 @@ def inversion(self, i): return if getattr(self, "_inversion", None) is not None: warnings.warn( - "{0!s} has switched to a new inversion.".format(self.__class__.__name__) + "{0!s} has switched to a new inversion.".format( + self.__class__.__name__ + ), + stacklevel=2, ) for d in self.dList: d.inversion = i @@ -353,21 +362,20 @@ class BaseBetaEstimator(InversionDirective): ---------- beta0_ratio : float Desired ratio between data misfit and model objective function at initial beta iteration. - seed : int, None - Seed used for random sampling. + 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, - n_pw_iter=4, - seed=None, - method="power_iteration", + seed: RandomSeed | None = None, **kwargs, ): super().__init__(**kwargs) - self.method = method self.beta0_ratio = beta0_ratio self.seed = seed @@ -393,14 +401,21 @@ def seed(self): Returns ------- - int + int, numpy.random.Generator or None """ return self._seed @seed.setter def seed(self, value): - if value is not None: - value = validate_integer("seed", value, min_val=0) + 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._seed = value def validate(self, directive_list): @@ -419,7 +434,7 @@ class BetaEstimateMaxDerivative(BaseBetaEstimator): 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` + 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`. @@ -427,8 +442,10 @@ class BetaEstimateMaxDerivative(BaseBetaEstimator): ---------- beta0_ratio: float Desired ratio between data misfit and model objective function at initial beta iteration. - seed : int, None - Seed used for random sampling. + 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 ----- @@ -458,19 +475,18 @@ class BetaEstimateMaxDerivative(BaseBetaEstimator): """ - def __init__(self, beta0_ratio=1.0, seed=None, **kwargs): - super().__init__(beta0_ratio, seed, **kwargs) + def __init__(self, beta0_ratio=1.0, seed: RandomSeed | None = None, **kwargs): + super().__init__(beta0_ratio=beta0_ratio, seed=seed, **kwargs) def initialize(self): - if self.seed is not None: - np.random.seed(self.seed) + rng = np.random.default_rng(seed=self.seed) if self.verbose: print("Calculating the beta0 parameter.") m = self.invProb.model - x0 = np.random.rand(*m.shape) + 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() @@ -480,15 +496,83 @@ def initialize(self): self.invProb.beta = self.beta0 +class BetaEstimateDerivative(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 : 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, seed: RandomSeed | None = None, **kwargs): + super().__init__(beta0_ratio=beta0_ratio, seed=seed, **kwargs) + + def initialize(self): + rng = np.random.default_rng(seed=self.seed) + + if self.verbose: + print("Calculating the beta0 parameter.") + + m = self.invProb.model + + x0 = rng.random(size=m.shape) + phi_d_deriv = self.dmisfit.deriv2(m, x0) + t = np.dot(x0, phi_d_deriv) + reg = self.reg.deriv2(m, v=x0) + b = np.dot(x0, reg) + self.ratio = np.asarray(t / b) + 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`. + 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. + 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`. @@ -498,8 +582,10 @@ class BetaEstimate_ByEig(BaseBetaEstimator): 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. + 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 ----- @@ -524,14 +610,19 @@ class BetaEstimate_ByEig(BaseBetaEstimator): 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`. + 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=beta0_ratio, n_pw_iter=n_pw_iter, seed=seed, **kwargs - ) + def __init__( + self, + beta0_ratio=1.0, + n_pw_iter=4, + seed: RandomSeed | None = None, + **kwargs, + ): + super().__init__(beta0_ratio=beta0_ratio, seed=seed, **kwargs) + self.n_pw_iter = n_pw_iter @property @@ -550,34 +641,27 @@ 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) + rng = np.random.default_rng(seed=self.seed) if self.verbose: print("Calculating the beta0 parameter.") m = self.invProb.model - if self.method == "power_iteration": - 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) - else: - x0 = np.random.rand(*m.shape) - phi_d_deriv = self.dmisfit.deriv2(m, x0) - t = np.dot(x0, phi_d_deriv) - reg = self.reg.deriv2(m, v=x0) - b = np.dot(x0, reg) - self.ratio = np.asarray(t / b) + dm_eigenvalue = eigenvalue_by_power_iteration( + self.dmisfit, + m, + n_pw_iter=self.n_pw_iter, + seed=rng, + ) + reg_eigenvalue = eigenvalue_by_power_iteration( + self.reg, + m, + n_pw_iter=self.n_pw_iter, + seed=rng, + ) + self.ratio = np.asarray(dm_eigenvalue / reg_eigenvalue) self.beta0 = self.beta0_ratio * self.ratio self.invProb.beta = self.beta0 @@ -585,7 +669,7 @@ def initialize(self): 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` + 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 @@ -658,7 +742,13 @@ class AlphasSmoothEstimate_ByEig(InversionDirective): 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): + def __init__( + self, + alpha0_ratio=1.0, + n_pw_iter=4, + seed: RandomSeed | None = None, + **kwargs, + ): super().__init__(**kwargs) self.alpha0_ratio = alpha0_ratio self.n_pw_iter = n_pw_iter @@ -700,20 +790,25 @@ def seed(self): Returns ------- - int + int, numpy.random.Generator or None """ return self._seed @seed.setter def seed(self, value): - if value is not None: - value = validate_integer("seed", value, min_val=1) + 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._seed = value def initialize(self): """""" - if self.seed is not None: - np.random.seed(self.seed) + rng = np.random.default_rng(seed=self.seed) smoothness = [] smallness = [] @@ -731,7 +826,6 @@ def initialize(self): Smallness, SparseSmallness, PGIsmallness, - PGIwithNonlinearRelationshipsSmallness, ), ): smallness += [obj] @@ -749,6 +843,7 @@ def initialize(self): smallness[0], self.invProb.model, n_pw_iter=self.n_pw_iter, + seed=rng, ) self.alpha0_ratio = self.alpha0_ratio * np.ones(len(smoothness)) @@ -764,6 +859,7 @@ def initialize(self): obj, self.invProb.model, n_pw_iter=self.n_pw_iter, + seed=rng, ) ratio = smallness_eigenvalue / smooth_i_eigenvalue @@ -785,7 +881,13 @@ class ScalingMultipleDataMisfits_ByEig(InversionDirective): The highest eigenvalue are estimated through power iterations and Rayleigh quotient. """ - def __init__(self, chi0_ratio=None, n_pw_iter=4, seed=None, **kwargs): + def __init__( + self, + chi0_ratio=None, + n_pw_iter=4, + seed: RandomSeed | None = None, + **kwargs, + ): super().__init__(**kwargs) self.chi0_ratio = chi0_ratio self.n_pw_iter = n_pw_iter @@ -827,20 +929,25 @@ def seed(self): Returns ------- - int + int, numpy.random.Generator or None """ return self._seed @seed.setter def seed(self, value): - if value is not None: - value = validate_integer("seed", value, min_val=1) + 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._seed = value def initialize(self): """""" - if self.seed is not None: - np.random.seed(self.seed) + rng = np.random.default_rng(seed=self.seed) if self.verbose: print("Calculating the scaling parameter.") @@ -863,7 +970,7 @@ def initialize(self): dm_eigenvalue_list = [] for dm in self.dmisfit.objfcts: - dm_eigenvalue_list += [eigenvalue_by_power_iteration(dm, m)] + dm_eigenvalue_list += [eigenvalue_by_power_iteration(dm, m, seed=rng)] self.chi0 = self.chi0_ratio / np.r_[dm_eigenvalue_list] self.chi0 = self.chi0 / np.sum(self.chi0) @@ -1075,17 +1182,17 @@ def phi_d_star(self): ------- float """ - # the factor of 0.5 is because we do phid = 0.5*||dpred - dobs||^2 + # 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 = 0.5 * nD + self._phi_d_star = nD return self._phi_d_star @phi_d_star.setter def phi_d_star(self, value): - # the factor of 0.5 is because we do phid = 0.5*||dpred - dobs||^2 + # phid = ||dpred - dobs||^2 if value is not None: value = validate_float( "phi_d_star", value, min_val=0.0, inclusive_min=False @@ -1181,13 +1288,13 @@ def phi_d_star(self): ------- float """ - # the factor of 0.5 is because we do phid = 0.5*|| dpred - dobs||^2 + # 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_[[0.5 * survey.nD for survey in self.survey]] + value = np.r_[[survey.nD for survey in self.survey]] else: - value = np.r_[[0.5 * self.survey.nD]] + value = np.r_[[self.survey.nD]] self._phi_d_star = value self._DMtarget = None @@ -1195,7 +1302,7 @@ def phi_d_star(self): @phi_d_star.setter def phi_d_star(self, value): - # the factor of 0.5 is because we do phid = 0.5*|| dpred - dobs||^2 + # phid =|| dpred - dobs||^2 if value is not None: value = validate_ndarray_with_shape("phi_d_star", value, shape=("*",)) self._phi_d_star = value @@ -1303,13 +1410,7 @@ def initialize(self): np.r_[ i, j, - ( - isinstance( - regpart, - PGIwithNonlinearRelationshipsSmallness, - ) - or isinstance(regpart, PGIsmallness) - ), + isinstance(regpart, PGIsmallness), ] ) for i, regobjcts in enumerate(self.invProb.reg.objfcts) @@ -1318,7 +1419,8 @@ def initialize(self): ] if smallness[smallness[:, 2] == 1][:, :2].size == 0: warnings.warn( - "There is no PGI regularization. Smallness target is turned off (TriggerSmall flag)" + "There is no PGI regularization. Smallness target is turned off (TriggerSmall flag)", + stacklevel=2, ) self.smallness = -1 self.pgi_smallness = None @@ -1346,13 +1448,7 @@ def initialize(self): ( np.r_[ j, - ( - isinstance( - regpart, - PGIwithNonlinearRelationshipsSmallness, - ) - or isinstance(regpart, PGIsmallness) - ), + isinstance(regpart, PGIsmallness), ] ) for j, regpart in enumerate(self.invProb.reg.objfcts) @@ -1361,7 +1457,8 @@ def initialize(self): 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)." + "There is no PGI regularization. Smallness target is turned off (TriggerSmall flag).", + stacklevel=2, ) self.TriggerSmall = False self.smallness = -1 @@ -1439,11 +1536,11 @@ def CLtarget(self): self._CLtarget = self.chiSmall * self.phi_ms_star elif getattr(self, "_CLtarget", None) is None: - # the factor of 0.5 is because we do phid = 0.5*|| dpred - dobs||^2 + # 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 = 0.5 * len(self.invProb.model) + self.phi_ms_star = len(self.invProb.model) self._CLtarget = self.chiSmall * self.phi_ms_star @@ -1642,7 +1739,7 @@ class SaveModelEveryIteration(SaveEveryIteration): def initialize(self): print( - "SimPEG.SaveModelEveryIteration will save your models as: " + "simpeg.SaveModelEveryIteration will save your models as: " "'{0!s}###-{1!s}.npy'".format(self.directory + os.path.sep, self.fileName) ) @@ -1658,8 +1755,6 @@ def endIter(self): class SaveOutputEveryIteration(SaveEveryIteration): """SaveOutputEveryIteration""" - save_txt = True - def __init__(self, save_txt=True, **kwargs): super().__init__(**kwargs) @@ -1682,7 +1777,7 @@ def save_txt(self, value): def initialize(self): if self.save_txt is True: print( - "SimPEG.SaveOutputEveryIteration will save your inversion " + "simpeg.SaveOutputEveryIteration will save your inversion " "progress as: '###-{0!s}.txt'".format(self.fileName) ) f = open(self.fileName + ".txt", "w") @@ -1762,7 +1857,7 @@ def load_results(self): self.f = results[:, 7] - self.target_misfit = self.invProb.dmisfit.simulation.survey.nD / 2.0 + self.target_misfit = self.invProb.dmisfit.simulation.survey.nD self.i_target = None if self.invProb.phi_d < self.target_misfit: @@ -1780,7 +1875,7 @@ def plot_misfit_curves( plot_small=False, plot_smooth=False, ): - self.target_misfit = self.invProb.dmisfit.simulation.survey.nD / 2.0 + 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: @@ -1834,7 +1929,7 @@ def plot_misfit_curves( fig.savefig(fname, dpi=dpi) def plot_tikhonov_curves(self, fname=None, dpi=200): - self.target_misfit = self.invProb.dmisfit.simulation.survey.nD / 2.0 + self.target_misfit = self.invProb.dmisfit.simulation.survey.nD self.i_target = None if self.invProb.phi_d < self.target_misfit: @@ -1905,7 +2000,7 @@ def initialize(self): self.outDict = {} if self.saveOnDisk: print( - "SimPEG.SaveOutputDictEveryIteration will save your inversion progress as dictionary: '###-{0!s}.npz'".format( + "simpeg.SaveOutputDictEveryIteration will save your inversion progress as dictionary: '###-{0!s}.npz'".format( self.fileName ) ) @@ -2093,7 +2188,7 @@ def target(self): for survey in self.survey: nD += survey.nD - self._target = nD * 0.5 * self.chifact_target + self._target = nD * self.chifact_target return self._target @@ -2107,10 +2202,10 @@ def start(self): if isinstance(self.survey, list): self._start = 0 for survey in self.survey: - self._start += survey.nD * 0.5 * self.chifact_start + self._start += survey.nD * self.chifact_start else: - self._start = self.survey.nD * 0.5 * self.chifact_start + self._start = self.survey.nD * self.chifact_start return self._start @start.setter @@ -2121,8 +2216,8 @@ def initialize(self): if self.mode == 1: self.norms = [] for reg in self.reg.objfcts: - if not hasattr(reg, "norms"): - self.norms.append([None]) + + if not isinstance(reg, Sparse): continue self.norms.append(reg.norms) @@ -2131,6 +2226,9 @@ def initialize(self): # 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: @@ -2183,7 +2281,7 @@ def endIter(self): # Print to screen for reg in self.reg.objfcts: - if not isinstance(reg, (Sparse, BaseSparse)): + if not isinstance(reg, Sparse): continue for obj in reg.objfcts: @@ -2195,8 +2293,10 @@ def endIter(self): # 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 isinstance(reg, (Sparse, BaseSparse)): - reg.update_weights(reg.model) + 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) @@ -2227,7 +2327,6 @@ def start_irls(self): 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 @@ -2235,7 +2334,8 @@ def start_irls(self): # Re-assign the norms supplied by user l2 -> lp for reg, norms in zip(self.reg.objfcts, self.norms): - if not hasattr(reg, "norms"): + if not isinstance(reg, Sparse): + continue reg.norms = norms @@ -2245,6 +2345,13 @@ def start_irls(self): # 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 @@ -2275,7 +2382,8 @@ def validate(self, directiveList): warnings.warn( "Without a Linear preconditioner, convergence may be slow. " "Consider adding `Directives.UpdatePreconditioner` to your " - "directives list" + "directives list", + stacklevel=2, ) return True @@ -2462,7 +2570,7 @@ def JtJv(v): return self.simulation.Jtvec(m, Jv) - JtJdiag = diagEst(JtJv, len(m), k=self.k) + JtJdiag = estimate_diagonal(JtJv, len(m), k=self.k) JtJdiag = JtJdiag / max(JtJdiag) self.reg.wght = JtJdiag @@ -2478,7 +2586,7 @@ class UpdateSensitivityWeights(InversionDirective): 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 + 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. @@ -2529,8 +2637,7 @@ class UpdateSensitivityWeights(InversionDirective): 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 - ^^^^^^^^^^^^ + **Thresholding:** If **global** thresholding is applied, we add a constant :math:`\tau` to the RMS sensitivities: @@ -2564,30 +2671,20 @@ def __init__( normalization_method="maximum", **kwargs, ): - if "everyIter" in kwargs.keys(): - warnings.warn( - "'everyIter' property is deprecated and will be removed in SimPEG 0.20.0." - "Please use 'every_iteration'." + # Raise errors on deprecated arguments + if (key := "everyIter") in kwargs.keys(): + raise TypeError( + f"'{key}' property has been removed. Please use 'every_iteration'.", ) - every_iteration = kwargs.pop("everyIter") - - if "threshold" in kwargs.keys(): - warnings.warn( - "'threshold' property is deprecated and will be removed in SimPEG 0.20.0." - "Please use 'threshold_value'." + if (key := "threshold") in kwargs.keys(): + raise TypeError( + f"'{key}' property has been removed. Please use 'threshold_value'.", ) - threshold_value = kwargs.pop("threshold") - - if "normalization" in kwargs.keys(): - warnings.warn( - "'normalization' property is deprecated and will be removed in SimPEG 0.20.0." - "Please define normalization using 'normalization_method'." + if (key := "normalization") in kwargs.keys(): + raise TypeError( + f"'{key}' property has been removed. " + "Please define normalization using 'normalization_method'.", ) - normalization_method = kwargs.pop("normalization") - if normalization_method is True: - normalization_method = "maximum" - else: - normalization_method = None super().__init__(**kwargs) @@ -2614,7 +2711,11 @@ 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" + every_iteration, + "everyIter", + "every_iteration", + removal_version="0.20.0", + error=True, ) @property @@ -2643,7 +2744,11 @@ 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" + threshold_value, + "threshold", + "threshold_value", + removal_version="0.20.0", + error=True, ) @property @@ -2693,17 +2798,6 @@ def normalization_method(self): def normalization_method(self, value): if value is None: self._normalization_method = value - - elif isinstance(value, bool): - warnings.warn( - "Boolean type for 'normalization_method' is deprecated and will be removed in 0.20.0." - "Please use None, 'maximum' or 'minimum'." - ) - if value: - self._normalization_method = "maximum" - else: - self._normalization_method = None - else: self._normalization_method = validate_string( "normalization_method", value, string_list=["minimum", "maximum"] @@ -2714,6 +2808,7 @@ def normalization_method(self, value): "normalization", "normalization_method", removal_version="0.20.0", + error=True, ) def initialize(self): @@ -2755,10 +2850,19 @@ def update(self): # 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 not isinstance(reg, BaseSimilarityMeasure): - wr += reg.mapping.deriv(self.invProb.model).T * ( - (reg.mapping * jtj_diag) / reg.regularization_mesh.vol**2.0 - ) + 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 @@ -2824,7 +2928,7 @@ def validate(self, directiveList): class ProjectSphericalBounds(InversionDirective): - """ + r""" Trick for spherical coordinate system. Project \theta and \phi angles back to [-\pi,\pi] using back and forth conversion. @@ -2847,8 +2951,6 @@ def initialize(self): misfit.simulation.model = m def endIter(self): - x = self.invProb.model - for misfit in self.dmisfit.objfcts: if ( hasattr(misfit.simulation, "model_type") @@ -2927,10 +3029,49 @@ def stack_channels(self, dpred: list): return self.reshape(np.hstack(dpred)) - def save_components(self, iteration: int, values: list[np.ndarray] = None): + def apply_transformations(self, prop: np.ndarray) -> np.ndarray: """ - Sort, transform and store data per components and channels. + Re-order the values and apply transformations. + """ + prop = prop.flatten() + for fun in self.transforms: + if isinstance(fun, (IdentityMap, np.ndarray, float)): + prop = fun * prop + else: + prop = fun(prop) + + if prop.ndim == 2: + prop = prop.T.flatten() + + prop = prop.reshape((len(self.channels), len(self.components), -1)) + + return prop + + def get_names( + self, component: str, channel: str, iteration: int + ) -> tuple[str, str]: + """ + Format the data and property_group name. + """ + base_name = f"Iteration_{iteration}" + if len(component) > 0: + base_name += f"_{component}" + + channel_name = base_name + if channel: + channel_name += f"_{channel}" + + if self.label is not None: + channel_name += f"_{self.label}" + base_name += f"_{self.label}" + + return channel_name, base_name + + def get_values(self, values: list[np.ndarray] | None): + """ + Get values for the inversion depending on the output type. """ + prop = self.invProb.model if values is not None: prop = self.stack_channels(values) elif self.attribute_type == "predicted": @@ -2945,28 +3086,27 @@ def save_components(self, iteration: int, values: list[np.ndarray] = None): prop = self.stack_channels(dpred) elif self.attribute_type == "sensitivities": for directive in self.inversion.directiveList.dList: - if isinstance(directive, UpdateSensitivityWeights): + if isinstance(directive, directives.UpdateSensitivityWeights): prop = self.reshape(np.sum(directive.JtJdiag, axis=0) ** 0.5) - else: - prop = self.invProb.model - # Apply transformations - prop = prop.flatten() - for fun in self.transforms: - if isinstance(fun, (IdentityMap, np.ndarray, float)): - prop = fun * prop - else: - prop = fun(prop) + return prop - if prop.ndim == 2: - prop = prop.T.flatten() + def save_components( # flake8: noqa + self, iteration: int, values: list[np.ndarray] = None + ): + """ + Sort, transform and store data per components and channels. + """ + prop = self.get_values(values) - prop = prop.reshape((len(self.channels), len(self.components), -1)) + # Apply transformations + prop = self.apply_transformations(prop) - with Workspace(self._h5_file) as w_s: + # Save results + with fetch_active_workspace(self._geoh5, mode="r+") as w_s: h5_object = w_s.get_entity(self.h5_object)[0] for cc, component in enumerate(self.components): - if component not in self.data_type.keys(): + if component not in self.data_type: self.data_type[component] = {} for ii, channel in enumerate(self.channels): @@ -2975,17 +3115,9 @@ def save_components(self, iteration: int, values: list[np.ndarray] = None): if self.sorting is not None: values = values[self.sorting] - base_name = f"Iteration_{iteration}" - if len(component) > 0: - base_name += f"_{component}" - - channel_name = base_name - if channel: - channel_name += f"_{channel}" - - if self.label is not None: - channel_name += f"_{self.label}" - base_name += f"_{self.label}" + channel_name, base_name = self.get_names( + component, channel, iteration + ) data = h5_object.add_data( { @@ -3014,14 +3146,14 @@ def write_update(self, iteration: int): """ Write update to file. """ - dirpath = Path(self._h5_file).parent + dirpath = Path(self._geoh5.h5file).parent filepath = dirpath / "SimPEG.out" if iteration == 0: - with open(filepath, "w") as f: + with open(filepath, "w", encoding="utf-8") as f: f.write("iteration beta phi_d phi_m time\n") - with open(filepath, "a") as f: + with open(filepath, "a", encoding="utf-8") as f: date_time = datetime.now().strftime("%b-%d-%Y:%H:%M:%S") f.write( f"{iteration} {self.invProb.beta:.3e} {self.invProb.phi_d:.3e} " @@ -3032,9 +3164,9 @@ def save_log(self): """ Save iteration metrics to comments. """ - dirpath = Path(self._h5_file).parent + dirpath = Path(self._geoh5.h5file).parent - with Workspace(self._h5_file) as w_s: + with fetch_active_workspace(self._geoh5, mode="r+") as w_s: h5_object = w_s.get_entity(self.h5_object)[0] for file in ["SimPEG.out", "SimPEG.log"]: @@ -3125,7 +3257,7 @@ def h5_object(self, entity: ObjectBase): ) self._h5_object = entity.uid - self._h5_file = entity.workspace.h5file + self._geoh5 = entity.workspace if getattr(entity, "n_cells", None) is not None: self.association = "CELL" @@ -3148,7 +3280,7 @@ def association(self, value): class VectorInversion(InversionDirective): """ - Control a vector inversion from Cartesian to spherical coordinates + Control a vector inversion from Cartesian to spherical coordinates. """ chifact_target = 1.0 @@ -3177,7 +3309,7 @@ def target(self): for survey in self.survey: nD += survey.nD - self._target = nD * 0.5 * self.chifact_target + self._target = nD * self.chifact_target return self._target @@ -3193,7 +3325,7 @@ def initialize(self): for dmisfit in self.dmisfit.objfcts: if getattr(dmisfit.simulation, "coordinate_system", None) is not None: - simulation.coordinate_system = self.mode + dmisfit.simulation.coordinate_system = self.mode def endIter(self): if ( @@ -3235,13 +3367,19 @@ def endIter(self): else: reg_fun.units = "amplitude" - # Turn of cross-gradient on angles + # Change units of cross-gradient on angles multipliers = [] for mult, reg in self.reg: if isinstance(reg, CrossGradient): - for wire in reg.wire_map: + units = [] + for _, wire in reg.wire_map.maps: if wire in angle_map: - mult = 0 + units.append("radian") + mult = 0 # TODO Make this optional + else: + units.append("metric") + + reg.units = units multipliers.append(mult) diff --git a/SimPEG/directives/pgi_directives.py b/simpeg/directives/pgi_directives.py similarity index 97% rename from SimPEG/directives/pgi_directives.py rename to simpeg/directives/pgi_directives.py index db332ff9bb..60f4488b90 100644 --- a/SimPEG/directives/pgi_directives.py +++ b/simpeg/directives/pgi_directives.py @@ -12,7 +12,6 @@ from ..regularization import ( PGI, PGIsmallness, - PGIwithRelationships, SmoothnessFirstOrder, SparseSmoothness, ) @@ -271,12 +270,12 @@ def update_previous_dmlist(self): @property def directives(self): - """List of all the directives in the :class:`SimPEG.inverison.BaseInversion``.""" + """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``.""" + """``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 = [ @@ -295,7 +294,7 @@ def multi_target_misfits_directive(self): @property def pgi_update_params_directive(self): - """``PGI_UpdateParam``s directive in the :class:`SimPEG.inverison.BaseInversion``.""" + """``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 = [ @@ -311,7 +310,7 @@ def pgi_update_params_directive(self): @property def pgi_regularization(self): - """PGI regularization in the :class:`SimPEG.inverse_problem.BaseInvProblem``.""" + """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: @@ -363,12 +362,7 @@ def initialize(self): 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, PGIwithRelationships)) - for regpart in self.reg.objfcts - ] - ] + np.r_[[isinstance(regpart, PGI) for regpart in self.reg.objfcts]] )[0][0] self.petrosmallness = petrosmallness @@ -413,7 +407,7 @@ def initialize(self): @property def DMtarget(self): if getattr(self, "_DMtarget", None) is None: - self.phi_d_target = 0.5 * self.invProb.dmisfit.survey.nD + self.phi_d_target = self.invProb.dmisfit.survey.nD self._DMtarget = self.chifact * self.phi_d_target return self._DMtarget diff --git a/SimPEG/directives/sim_directives.py b/simpeg/directives/sim_directives.py similarity index 98% rename from SimPEG/directives/sim_directives.py rename to simpeg/directives/sim_directives.py index 5b781fe97a..480cda76ee 100644 --- a/SimPEG/directives/sim_directives.py +++ b/simpeg/directives/sim_directives.py @@ -245,10 +245,9 @@ def initialize(self): :rtype: float :return: beta0 """ - if self.seed is not None: - np.random.seed(self.seed) + rng = np.random.default_rng(seed=self.seed) - if self.debug: + if self.verbose: print("Calculating the beta0 parameter.") m = self.invProb.model @@ -271,6 +270,7 @@ def initialize(self): dmis, m, n_pw_iter=self.n_pw_iter, + seed=rng, ) ) @@ -279,6 +279,7 @@ def initialize(self): reg, m, n_pw_iter=self.n_pw_iter, + seed=rng, ) ) @@ -305,7 +306,7 @@ def target(self): if getattr(self, "_target", None) is None: nD = np.array([survey.nD for survey in self.survey]) - self._target = nD * 0.5 * self.chifact_target + self._target = nD * self.chifact_target return self._target @@ -362,7 +363,7 @@ def target(self): nD += [survey.nD] nD = np.array(nD) - self._target = nD * 0.5 * self.chifact_target + self._target = nD * self.chifact_target return self._target diff --git a/SimPEG/electromagnetics/__init__.py b/simpeg/electromagnetics/__init__.py similarity index 83% rename from SimPEG/electromagnetics/__init__.py rename to simpeg/electromagnetics/__init__.py index 1ddc269178..23a0b24d9e 100644 --- a/SimPEG/electromagnetics/__init__.py +++ b/simpeg/electromagnetics/__init__.py @@ -1,8 +1,8 @@ """ ================================================================= -Base EM (:mod:`SimPEG.electromagnetics`) +Base EM (:mod:`simpeg.electromagnetics`) ================================================================= -.. currentmodule:: SimPEG.electromagnetics +.. currentmodule:: simpeg.electromagnetics About ``electromagnetics`` @@ -22,9 +22,9 @@ .. autosummary:: :toctree: generated/ - analytics.h[2]AnalyticDipoleT - analytics.h[2]AnalyticCentLoopT - analytics.h[2]AnalyticDipoleF + analytics.hzAnalyticDipoleT + analytics.hzAnalyticCentLoopT + analytics.hzAnalyticDipoleF analytics.getCasingEphiMagDipole analytics.getCasingHrMagDipole analytics.getCasingHzMagDipole @@ -32,6 +32,7 @@ analytics.getCasingBzMagDipole """ + from scipy.constants import mu_0, epsilon_0 from . import time_domain diff --git a/SimPEG/electromagnetics/analytics/DC.py b/simpeg/electromagnetics/analytics/DC.py similarity index 100% rename from SimPEG/electromagnetics/analytics/DC.py rename to simpeg/electromagnetics/analytics/DC.py diff --git a/SimPEG/electromagnetics/analytics/FDEM.py b/simpeg/electromagnetics/analytics/FDEM.py similarity index 90% rename from SimPEG/electromagnetics/analytics/FDEM.py rename to simpeg/electromagnetics/analytics/FDEM.py index d8ce609c32..c36027f458 100644 --- a/SimPEG/electromagnetics/analytics/FDEM.py +++ b/simpeg/electromagnetics/analytics/FDEM.py @@ -1,6 +1,6 @@ import numpy as np from scipy.constants import mu_0, pi, epsilon_0 -from SimPEG import utils +from simpeg import utils def hzAnalyticDipoleF(r, freq, sigma, secondary=True, mu=mu_0): @@ -9,25 +9,24 @@ def hzAnalyticDipoleF(r, freq, sigma, secondary=True, mu=mu_0): 1988, and the example reproduces their Figure 4.2. - .. plot:: - - import numpy as np - import matplotlib.pyplot as plt - from SimPEG import electromagnetics as EM - freq = np.logspace(-1, 5, 301) - test = EM.analytics.h[2]AnalyticDipoleF( - 100, freq, 0.01, secondary=False) - plt.loglog(freq, test.real, 'C0-', label='Real') - plt.loglog(freq, -test.real, 'C0--') - plt.loglog(freq, test.imag, 'C1-', label='Imaginary') - plt.loglog(freq, -test.imag, 'C1--') - plt.title('Response at $r=100$ m') - plt.xlim([1e-1, 1e5]) - plt.ylim([1e-12, 1e-6]) - plt.xlabel('Frequency (Hz)') - plt.ylabel('$H_z$ (A/m)') - plt.legend(loc=6) - plt.show() + Examples + -------- + >>> import matplotlib.pyplot as plt + >>> from simpeg import electromagnetics as em + >>> freq = np.logspace(-1, 5, 301) + >>> test = em.analytics.hzAnalyticDipoleF( + >>> 100, freq, 0.01, secondary=False) + >>> plt.loglog(freq, test.real, 'C0-', label='Real') + >>> plt.loglog(freq, -test.real, 'C0--') + >>> plt.loglog(freq, test.imag, 'C1-', label='Imaginary') + >>> plt.loglog(freq, -test.imag, 'C1--') + >>> plt.title('Response at $r=100$ m') + >>> plt.xlim([1e-1, 1e5]) + >>> plt.ylim([1e-12, 1e-6]) + >>> plt.xlabel('Frequency (Hz)') + >>> plt.ylabel('$H_z$ (A/m)') + >>> plt.legend(loc=6) + >>> plt.show() **Reference** @@ -76,7 +75,7 @@ def MagneticDipoleWholeSpace( .. plot:: import numpy as np - from SimPEG import electromagnetics as EM + from simpeg import electromagnetics as EM import matplotlib.pyplot as plt from scipy.constants import mu_0 freqs = np.logspace(-2, 5, 301) diff --git a/SimPEG/electromagnetics/analytics/FDEMDipolarfields.py b/simpeg/electromagnetics/analytics/FDEMDipolarfields.py similarity index 99% rename from SimPEG/electromagnetics/analytics/FDEMDipolarfields.py rename to simpeg/electromagnetics/analytics/FDEMDipolarfields.py index 2b6247df55..bfdfee7748 100644 --- a/SimPEG/electromagnetics/analytics/FDEMDipolarfields.py +++ b/simpeg/electromagnetics/analytics/FDEMDipolarfields.py @@ -1,8 +1,12 @@ import numpy as np from scipy.constants import epsilon_0, mu_0 -from SimPEG import utils +from simpeg import utils + + +def omega(f): + return 2.0 * np.pi * f + -omega = lambda f: 2.0 * np.pi * f # TODO: # r = lambda dx, dy, dz: np.sqrt( dx**2. + dy**2. + dz**2.) # k = lambda f, mu, epsilon, sig: np.sqrt( omega(f)**2. *mu*epsilon -1j*omega(f)*mu*sig ) diff --git a/SimPEG/electromagnetics/analytics/FDEMcasing.py b/simpeg/electromagnetics/analytics/FDEMcasing.py similarity index 99% rename from SimPEG/electromagnetics/analytics/FDEMcasing.py rename to simpeg/electromagnetics/analytics/FDEMcasing.py index 6088aaf9ef..0196d1277b 100644 --- a/SimPEG/electromagnetics/analytics/FDEMcasing.py +++ b/simpeg/electromagnetics/analytics/FDEMcasing.py @@ -1,7 +1,7 @@ import numpy as np from scipy.constants import mu_0, epsilon_0 -from SimPEG.electromagnetics.utils import k +from simpeg.electromagnetics.utils import k def getKc(freq, sigma, a, b, mu=mu_0, eps=epsilon_0): diff --git a/SimPEG/electromagnetics/analytics/NSEM.py b/simpeg/electromagnetics/analytics/NSEM.py similarity index 88% rename from SimPEG/electromagnetics/analytics/NSEM.py rename to simpeg/electromagnetics/analytics/NSEM.py index 15a6aec0a4..33a6772d4a 100644 --- a/SimPEG/electromagnetics/analytics/NSEM.py +++ b/simpeg/electromagnetics/analytics/NSEM.py @@ -1,38 +1,37 @@ import numpy as np from scipy.constants import epsilon_0 from scipy.constants import mu_0 -from SimPEG.electromagnetics.utils import k, omega +from simpeg.electromagnetics.utils import k, omega __all__ = ["MT_LayeredEarth"] # Evaluate Impedance Z of a layer -_ImpZ = lambda f, mu, k: omega(f) * mu / k +def _ImpZ(f, mu, k): + return omega(f) * mu / k + # Complex Cole-Cole Conductivity - EM utils -_PCC = lambda siginf, m, t, c, f: siginf * ( - 1.0 - (m / (1.0 + (1j * omega(f) * t) ** c)) -) +def _PCC(siginf, m, t, c, f): + return siginf * (1.0 - (m / (1.0 + (1j * omega(f) * t) ** c))) + # matrix P relating Up and Down components with E and H fields -_P = lambda z: np.array( - [ - [ - 1.0, - 1, - ], - [-1.0 / z, 1.0 / z], - ] -) -_Pinv = lambda z: np.array([[1.0, -z], [1.0, z]]) / 2.0 +def _P(z): + return np.array([[1.0, 1.0], [-1.0 / z, 1.0 / z]]) + + +def _Pinv(z): + return np.array([[1.0, -z], [1.0, z]]) / 2.0 + # matrix T for transition of Up and Down components accross a layer -_T = lambda h, k: np.array( - [[np.exp(1j * k * h), 0.0], [0.0, np.exp(-1j * k * h)]], -) -_Tinv = lambda h, k: np.array( - [[np.exp(-1j * k * h), 0.0], [0.0, np.exp(1j * k * h)]], -) +def _T(h, k): + return np.array([[np.exp(1j * k * h), 0.0], [0.0, np.exp(-1j * k * h)]]) + + +def _Tinv(h, k): + return np.array([[np.exp(-1j * k * h), 0.0], [0.0, np.exp(1j * k * h)]]) # Propagate Up and Down component for a certain frequency & evaluate E and H field @@ -104,7 +103,7 @@ def MT_LayeredEarth( This code compute the analytic response of a n-layered Earth to a plane wave (Magnetotellurics). All physical properties arrays convention describes the layers parameters from the top layer to the bottom layer. The solution is first developed in Ward and Hohmann 1988. - See also http://em.geosci.xyz/content/maxwell3_fdem/natural_sources/MT_N_layered_Earth.html + See also https://em.geosci.xyz/content/maxwell3_fdem/natural_sources/MT_N_layered_Earth.html :param freq: the frequency at which we take the measurements :type freq: float or numpy.ndarray diff --git a/SimPEG/electromagnetics/analytics/TDEM.py b/simpeg/electromagnetics/analytics/TDEM.py similarity index 99% rename from SimPEG/electromagnetics/analytics/TDEM.py rename to simpeg/electromagnetics/analytics/TDEM.py index b5b1ee9a04..c73ca0a815 100644 --- a/SimPEG/electromagnetics/analytics/TDEM.py +++ b/simpeg/electromagnetics/analytics/TDEM.py @@ -1,7 +1,7 @@ import numpy as np from scipy.constants import mu_0, pi from scipy.special import erf -from SimPEG import utils +from simpeg import utils def hzAnalyticDipoleT(r, t, sigma): diff --git a/simpeg/electromagnetics/analytics/__init__.py b/simpeg/electromagnetics/analytics/__init__.py new file mode 100644 index 0000000000..1df7acd782 --- /dev/null +++ b/simpeg/electromagnetics/analytics/__init__.py @@ -0,0 +1,30 @@ +from .TDEM import hzAnalyticDipoleT, hzAnalyticCentLoopT +from .FDEM import hzAnalyticDipoleF +from .FDEMcasing import ( + getKc, + getCasingEphiMagDipole, + getCasingHrMagDipole, + getCasingHzMagDipole, + getCasingBrMagDipole, + getCasingBzMagDipole, +) +from .DC import ( + DCAnalytic_Pole_Dipole, + DCAnalytic_Dipole_Pole, + DCAnalytic_Pole_Pole, + DCAnalytic_Dipole_Dipole, + DCAnalyticSphere, + AnBnfun, +) +from .FDEMDipolarfields import ( + E_from_ElectricDipoleWholeSpace, + E_galvanic_from_ElectricDipoleWholeSpace, + E_inductive_from_ElectricDipoleWholeSpace, + J_from_ElectricDipoleWholeSpace, + J_galvanic_from_ElectricDipoleWholeSpace, + J_inductive_from_ElectricDipoleWholeSpace, + H_from_ElectricDipoleWholeSpace, + B_from_ElectricDipoleWholeSpace, + A_from_ElectricDipoleWholeSpace, +) +from .NSEM import MT_LayeredEarth diff --git a/SimPEG/electromagnetics/base.py b/simpeg/electromagnetics/base.py similarity index 97% rename from SimPEG/electromagnetics/base.py rename to simpeg/electromagnetics/base.py index 2a2d9c5ebd..b57836d5b4 100644 --- a/SimPEG/electromagnetics/base.py +++ b/simpeg/electromagnetics/base.py @@ -57,7 +57,7 @@ class BaseEMSrc(BaseSrc): ---------- location : (n_dim) numpy.ndarray Location of the source - receiver_list : list of SimPEG.survey.BaseRx objects + receiver_list : list of simpeg.survey.BaseRx objects Sets the receivers associated with the source uid : uuid.UUID A universally unique identifier @@ -93,7 +93,7 @@ def eval(self, simulation): # noqa: A003 Parameters ---------- - simulation : SimPEG.electromagnetics.base.BaseEMSimulation + simulation : simpeg.electromagnetics.base.BaseEMSimulation An instance of an electromagnetic simulation Returns @@ -111,7 +111,7 @@ def evalDeriv(self, simulation, v=None, adjoint=False): Parameters ---------- - simulation : SimPEG.electromagnetics.base.BaseEMSimulation + simulation : simpeg.electromagnetics.base.BaseEMSimulation An instance of an electromagnetic simulation v : np.ndarray A vector diff --git a/SimPEG/electromagnetics/base_1d.py b/simpeg/electromagnetics/base_1d.py similarity index 100% rename from SimPEG/electromagnetics/base_1d.py rename to simpeg/electromagnetics/base_1d.py diff --git a/SimPEG/electromagnetics/frequency_domain/__init__.py b/simpeg/electromagnetics/frequency_domain/__init__.py similarity index 68% rename from SimPEG/electromagnetics/frequency_domain/__init__.py rename to simpeg/electromagnetics/frequency_domain/__init__.py index 3dad3cde28..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`) +Frequency-Domain EM (:mod:`simpeg.electromagnetics.frequency_domain`) ============================================================================== -.. currentmodule:: SimPEG.electromagnetics.frequency_domain +.. currentmodule:: simpeg.electromagnetics.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: -About ``frequency_domain`` +* :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 =========== @@ -73,6 +91,7 @@ fields.FieldsFDEM """ + from .survey import Survey from . import sources from . import receivers diff --git a/SimPEG/electromagnetics/frequency_domain/fields.py b/simpeg/electromagnetics/frequency_domain/fields.py similarity index 78% rename from SimPEG/electromagnetics/frequency_domain/fields.py rename to simpeg/electromagnetics/frequency_domain/fields.py index 740dd10dac..bf2c298cd3 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): @@ -153,7 +179,7 @@ def _eDeriv(self, src, du_dm_v, v, adjoint=False): (:math:`d\mathbf{e}/d\mathbf{u}`, :math:`d\mathb{u}/d\mathbf{m}`) for the adjoint - :param SimPEG.electromagnetics.frequency_domain.Src.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.Src.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: derivative of the solution vector with respect to the model times a vector (is None for adjoint) :param numpy.ndarray v: vector to take sensitivity product with @@ -185,7 +211,7 @@ def _bDeriv(self, src, du_dm_v, v, adjoint=False): (:math:`d\mathbf{b}/d\mathbf{u}`, :math:`d\mathb{u}/d\mathbf{m}`) for the adjoint - :param SimPEG.electromagnetics.frequency_domain.Src.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.Src.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: derivative of the solution vector with respect to the model times a vector (is None for adjoint) :param numpy.ndarray v: vector to take sensitivity product with @@ -217,7 +243,7 @@ def _bSecondaryDeriv(self, src, du_dm_v, v, adjoint=False): (:math:`d\mathbf{b}/d\mathbf{u}`, :math:`d\mathb{u}/d\mathbf{m}`) for the adjoint - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: sorce + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: sorce :param numpy.ndarray du_dm_v: derivative of the solution vector with respect to the model times a vector (is None for adjoint) :param numpy.ndarray v: vector to take sensitivity product with @@ -236,7 +262,7 @@ def _hDeriv(self, src, du_dm_v, v, adjoint=False): (:math:`d\mathbf{h}/d\mathbf{u}`, :math:`d\mathb{u}/d\mathbf{m}`) for the adjoint - :param SimPEG.electromagnetics.frequency_domain.Src.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.Src.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: derivative of the solution vector with respect to the model times a vector (is None for adjoint) :param numpy.ndarray v: vector to take sensitivity product with @@ -275,7 +301,7 @@ def _jDeriv(self, src, du_dm_v, v, adjoint=False): (:math:`d\mathbf{j}/d\mathbf{u}`, :math:`d\mathb{u}/d\mathbf{m}`) for the adjoint - :param SimPEG.electromagnetics.frequency_domain.Src.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.Src.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: derivative of the solution vector with respect to the model times a vector (is None for adjoint) :param numpy.ndarray v: vector to take sensitivity product with @@ -302,28 +328,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. + + 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 - :param discretize.base.BaseMesh mesh: mesh - :param SimPEG.electromagnetics.frequency_domain.SurveyFDEM.Survey survey: survey + 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 @@ -382,7 +446,7 @@ def _eDeriv_u(self, src, v, adjoint=False): Partial derivative of the total electric field with respect to the thing we solved for. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -399,10 +463,10 @@ def _eDeriv_m(self, src, v, adjoint=False): the model. Note that this also includes derivative contributions from the sources. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? - :rtype: SimPEG.utils.Zero + :rtype: simpeg.utils.Zero :return: product of the electric field derivative with respect to the inversion model with a vector """ @@ -462,7 +526,7 @@ def _bDeriv_u(self, src, du_dm_v, adjoint=False): Derivative of the magnetic flux density with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -480,7 +544,7 @@ def _bDeriv_m(self, src, v, adjoint=False): Derivative of the magnetic flux density with respect to the inversion model. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -512,7 +576,7 @@ def _jDeriv_u(self, src, du_dm_v, adjoint=False): Derivative of the current density with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -531,7 +595,7 @@ def _jDeriv_m(self, src, v, adjoint=False): """ Derivative of the current density with respect to the inversion model. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -570,7 +634,7 @@ def _hDeriv_u(self, src, du_dm_v, adjoint=False): Derivative of the magnetic field with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -595,7 +659,7 @@ def _hDeriv_m(self, src, v, adjoint=False): """ Derivative of the magnetic field with respect to the inversion model. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -630,28 +694,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 @@ -709,7 +812,7 @@ def _bDeriv_u(self, src, du_dm_v, adjoint=False): Partial derivative of the total magnetic flux density with respect to the thing we solved for. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -726,10 +829,10 @@ def _bDeriv_m(self, src, v, adjoint=False): on the model. Note that this also includes derivative contributions from the sources. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? - :rtype: SimPEG.utils.Zero + :rtype: simpeg.utils.Zero :return: product of the magnetic flux density derivative with respect to the inversion model with a vector """ @@ -770,14 +873,23 @@ def _eSecondary(self, bSolution, source_list): s_e = src.s_e(self.simulation) e[:, i] = e[:, i] + -s_e - return self._MeSigmaI * e + if self.simulation.permittivity is not None: + MeyhatI = self.simulation._get_edge_admittivity_property_matrix( + src.frequency, invert_matrix=True + ) + e[:, i] = MeyhatI * e[:, i] + + if self.simulation.permittivity is None: + return self._MeSigmaI * e + else: + return e def _eDeriv_u(self, src, du_dm_v, adjoint=False): """ Derivative of the electric field with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -793,7 +905,7 @@ def _eDeriv_m(self, src, v, adjoint=False): """ Derivative of the electric field with respect to the inversion model - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -834,20 +946,23 @@ def _j(self, bSolution, source_list): :return: primary current density """ - j = self._edgeCurl.T * (self._MfMui * bSolution) + if self.simulation.permittivity is None: + j = self._edgeCurl.T * (self._MfMui * bSolution) - for i, src in enumerate(source_list): - s_e = src.s_e(self.simulation) - j[:, i] = j[:, i] - s_e + for i, src in enumerate(source_list): + s_e = src.s_e(self.simulation) + j[:, i] = j[:, i] - s_e - return self._MeI * j + return self._MeI * j + else: + return self._MeI * (self._MeSigma * self._e(bSolution, source_list)) def _jDeriv_u(self, src, du_dm_v, adjoint=False): """ Partial derivative of the current density with respect to the thing we solved for. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -871,7 +986,7 @@ def _jDeriv_m(self, src, v, adjoint=False): """ Derivative of the current density with respect to the inversion model - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -897,7 +1012,7 @@ def _hDeriv_u(self, src, du_dm_v, adjoint=False): Partial derivative of the magnetic field with respect to the thing we solved for. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -918,7 +1033,7 @@ def _hDeriv_m(self, src, v, adjoint=False): """ Derivative of the magnetic field with respect to the inversion model - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -948,28 +1063,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 @@ -1039,7 +1191,7 @@ def _jDeriv_u(self, src, du_dm_v, adjoint=False): Partial derivative of the total current density with respect to the thing we solved for. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1056,10 +1208,10 @@ def _jDeriv_m(self, src, v, adjoint=False): the model. Note that this also includes derivative contributions from the sources. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? - :rtype: SimPEG.utils.Zero + :rtype: simpeg.utils.Zero :return: product of the current density derivative with respect to the inversion model with a vector """ @@ -1094,8 +1246,20 @@ def _hSecondary(self, jSolution, source_list): :return: secondary magnetic field """ - h = self._edgeCurl.T * (self._MfRho * jSolution) + if self.simulation.permittivity is not None: + h = np.zeros((self.mesh.n_edges, len(source_list)), dtype=complex) + else: + h = self._edgeCurl.T * (self._MfRho * jSolution) + for i, src in enumerate(source_list): + if self.simulation.permittivity is not None: + h[:, i] = self._edgeCurl.T * ( + self.simulation._get_face_admittivity_property_matrix( + src.frequency, invert_model=True + ) + * jSolution[:, i] + ) + h[:, i] *= -1.0 / (1j * omega(src.frequency)) s_m = src.s_m(self.simulation) h[:, i] = h[:, i] + 1.0 / (1j * omega(src.frequency)) * (s_m) @@ -1106,7 +1270,7 @@ def _hDeriv_u(self, src, du_dm_v, adjoint=False): Derivative of the magnetic field with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1132,7 +1296,7 @@ def _hDeriv_m(self, src, v, adjoint=False): """ Derivative of the magnetic field with respect to the inversion model - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1195,14 +1359,23 @@ def _e(self, jSolution, source_list): :rtype: numpy.ndarray :return: electric field """ + # if self.simulation.permittivity is None: return self._MfI * (self._MfRho * self._j(jSolution, source_list)) + # e = np.zeros((self.mesh.n_faces, len(source_list)), dtype=complex) + # for i, source in enumerate(source_list): + # Mfyhati = self.simulation._get_face_admittivity_property_matrix( + # source.frequency, invert_model=True + # ) + # e[:, i] = Mfyhati * mkvc(self._j(jSolution, [source])) + # return self._MfI * e + def _eDeriv_u(self, src, du_dm_v, adjoint=False): """ Derivative of the electric field with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1217,7 +1390,7 @@ def _eDeriv_m(self, src, v, adjoint=False): """ Derivative of the electric field with respect to the inversion model - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1250,7 +1423,7 @@ def _bDeriv_u(self, src, du_dm_v, adjoint=False): Derivative of the magnetic flux density with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1276,7 +1449,7 @@ def _bDeriv_m(self, src, v, adjoint=False): Derivative of the magnetic flux density with respect to the inversion model - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1318,28 +1491,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 @@ -1395,7 +1605,7 @@ def _hDeriv_u(self, src, du_dm_v, adjoint=False): Partial derivative of the total magnetic field with respect to the thing we solved for. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1412,10 +1622,10 @@ def _hDeriv_m(self, src, v, adjoint=False): on the model. Note that this also includes derivative contributions from the sources. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? - :rtype: SimPEG.utils.Zero + :rtype: simpeg.utils.Zero :return: product of the magnetic field derivative with respect to the inversion model with a vector """ @@ -1461,7 +1671,7 @@ def _jDeriv_u(self, src, du_dm_v, adjoint=False): Derivative of the current density with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1478,7 +1688,7 @@ def _jDeriv_m(self, src, v, adjoint=False): """ Derivative of the current density with respect to the inversion model. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1507,7 +1717,7 @@ def _eDeriv_u(self, src, du_dm_v, adjoint=False): Derivative of the electric field with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1522,7 +1732,7 @@ def _eDeriv_m(self, src, v, adjoint=False): """ Derivative of the electric field with respect to the inversion model. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1560,7 +1770,7 @@ def _bDeriv_u(self, src, du_dm_v, adjoint=False): Derivative of the magnetic flux density with respect to the thing we solved for - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -1583,7 +1793,7 @@ def _bDeriv_m(self, src, v, adjoint=False): Derivative of the magnetic flux density with respect to the inversion model. - :param SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source + :param simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray diff --git a/SimPEG/electromagnetics/frequency_domain/receivers.py b/simpeg/electromagnetics/frequency_domain/receivers.py similarity index 94% rename from SimPEG/electromagnetics/frequency_domain/receivers.py rename to simpeg/electromagnetics/frequency_domain/receivers.py index a8b6d4e256..b424135a20 100644 --- a/SimPEG/electromagnetics/frequency_domain/receivers.py +++ b/simpeg/electromagnetics/frequency_domain/receivers.py @@ -1,6 +1,5 @@ from ... import survey from ...utils import validate_string, validate_type, validate_direction -import warnings from discretize.utils import Zero @@ -33,14 +32,8 @@ def __init__( use_source_receiver_offset=False, **kwargs, ): - proj = kwargs.pop("projComp", None) - if proj is not None: - warnings.warn( - "'projComp' overrides the 'orientation' property which automatically" - " handles the projection from the mesh the receivers!!! " - "'projComp' is deprecated and will be removed in SimPEG 0.19.0." - ) - self.projComp = proj + if (key := "projComp") in kwargs.keys(): + raise TypeError(f"'{key}' property has been removed.") self.orientation = orientation self.component = component @@ -179,11 +172,11 @@ def eval(self, src, mesh, f): # noqa: A003 Parameters ---------- - src : SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc + src : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc A frequency-domain EM source mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved - f : SimPEG.electromagnetic.frequency_domain.fields.FieldsFDEM + f : simpeg.electromagnetic.frequency_domain.fields.FieldsFDEM The solution for the fields defined on the mesh Returns @@ -203,11 +196,11 @@ def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): Parameters ---------- - src : SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc + src : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc A frequency-domain EM source mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved - f : SimPEG.electromagnetic.frequency_domain.fields.FieldsFDEM + f : simpeg.electromagnetic.frequency_domain.fields.FieldsFDEM The solution for the fields defined on the mesh du_dm_v : numpy.ndarray The derivative of the fields on the mesh with respect to the model, diff --git a/simpeg/electromagnetics/frequency_domain/simulation.py b/simpeg/electromagnetics/frequency_domain/simulation.py new file mode 100644 index 0000000000..fc99133600 --- /dev/null +++ b/simpeg/electromagnetics/frequency_domain/simulation.py @@ -0,0 +1,2182 @@ +import numpy as np +import scipy.sparse as sp +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 +from .survey import Survey +from .fields import ( + FieldsFDEM, + Fields3DElectricField, + Fields3DMagneticFluxDensity, + Fields3DMagneticField, + Fields3DCurrentDensity, +) + +import warnings + + +class BaseFDEMSimulation(BaseEMSimulation): + 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)") + + def __init__( + self, + mesh, + survey=None, + forward_only=False, + permittivity=None, + storeJ=False, + **kwargs + ): + super().__init__(mesh=mesh, survey=survey, **kwargs) + self.forward_only = forward_only + if permittivity is not None: + warnings.warn( + "Simulations using permittivity have not yet been thoroughly tested and derivatives are not implemented. Contributions welcome!", + stacklevel=2, + ) + self.permittivity = permittivity + self.storeJ = storeJ + + @property + def survey(self): + """The FDEM survey object. + + Returns + ------- + .frequency_domain.survey.Survey + The FDEM survey object. + """ + if self._survey is None: + raise AttributeError("Simulation must have a survey set") + return self._survey + + @survey.setter + def survey(self, value): + if value is not None: + value = validate_type("survey", value, Survey, cast=False) + self._survey = value + self._survey = value + + @property + def storeJ(self): + """Whether to compute and store the sensitivity matrix. + + Returns + ------- + bool + Whether to compute and store the sensitivity matrix. + """ + return self._storeJ + + @storeJ.setter + def storeJ(self, value): + self._storeJ = validate_type("storeJ", value, bool) + + @property + def forward_only(self): + """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 + + @forward_only.setter + def forward_only(self, value): + self._forward_only = validate_type("forward_only", value, bool) + + def _get_admittivity(self, freq): + if self.permittivity is not None: + return self.sigma + 1j * self.permittivity * omega(freq) + else: + return self.sigma + + def _get_face_admittivity_property_matrix( + self, freq, invert_model=False, invert_matrix=False + ): + """ + Face inner product matrix with permittivity and resistivity + """ + yhat = self._get_admittivity(freq) + return self.mesh.get_face_inner_product( + yhat, invert_model=invert_model, invert_matrix=invert_matrix + ) + + def _get_edge_admittivity_property_matrix( + self, freq, invert_model=False, invert_matrix=False + ): + """ + Face inner product matrix with permittivity and resistivity + """ + yhat = self._get_admittivity(freq) + return self.mesh.get_edge_inner_product( + yhat, invert_model=invert_model, invert_matrix=invert_matrix + ) + + # @profile + def fields(self, m=None): + """Compute and return the fields for the model provided. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model. + + Returns + ------- + .frequency_domain.fields.FieldsFDEM + The FDEM fields object. + """ + + if m is not None: + self.model = m + + try: + self.Ainv + except AttributeError: + self.Ainv = len(self.survey.frequencies) * [None] + + f = self.fieldsPair(self) + + for i_f, freq in enumerate(self.survey.frequencies): + A = self.getA(freq) + rhs = self.getRHS(freq) + Ainv = self.solver(A, **self.solver_opts) + u = Ainv * rhs + if not self.forward_only: + self.Ainv[i_f] = Ainv + + Srcs = self.survey.get_sources_by_frequency(freq) + f[Srcs, self._solutionType] = u + return f + + # @profile + def Jvec(self, m, v, f=None): + 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: + f = self.fields(m) + + self.model = m + + Jv = Data(self.survey) + + for nf, freq in enumerate(self.survey.frequencies): + for src in self.survey.get_sources_by_frequency(freq): + u_src = f[src, self._solutionType] + dA_dm_v = self.getADeriv(freq, u_src, v, adjoint=False) + 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) + + return Jv.dobs + + def Jtvec(self, m, v, f=None): + 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}} + + 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: + f = self.fields(m) + + self.model = m + + # Ensure v is a data object. + if not isinstance(v, Data): + v = Data(self.survey, v) + + Jtv = np.zeros(m.size) + + for nf, freq in enumerate(self.survey.frequencies): + for src in self.survey.get_sources_by_frequency(freq): + u_src = f[src, self._solutionType] + df_duT_sum = 0 + df_dmT_sum = 0 + for rx in src.receiver_list: + df_duT, df_dmT = rx.evalDeriv( + src, self.mesh, f, v=v[src, rx], adjoint=True + ) + if not isinstance(df_duT, Zero): + df_duT_sum += df_duT + if not isinstance(df_dmT, Zero): + df_dmT_sum += df_dmT + + ATinvdf_duT = self.Ainv[nf] * df_duT_sum + + dA_dmT = self.getADeriv(freq, u_src, ATinvdf_duT, adjoint=True) + dRHS_dmT = self.getRHSDeriv(freq, src, ATinvdf_duT, adjoint=True) + du_dmT = -dA_dmT + dRHS_dmT + + df_dmT_sum += du_dmT + Jtv += np.real(df_dmT_sum) + + return mkvc(Jtv) + + def getJ(self, m, f=None): + 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. + + 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 + + if getattr(self, "_Jmatrix", None) is None: + if f is None: + f = self.fields(m) + + Ainv = self.Ainv + m_size = self.model.size + + Jmatrix = np.zeros((self.survey.nD, m_size)) + + data = Data(self.survey) + + for A_i, freq in zip(Ainv, self.survey.frequencies): + for src in self.survey.get_sources_by_frequency(freq): + u_src = f[src, self._solutionType] + + for rx in src.receiver_list: + v = np.eye(rx.nD, dtype=float) + + df_duT, df_dmT = rx.evalDeriv( + src, self.mesh, f, v=v, adjoint=True + ) + + df_duT = np.hstack([df_duT]) + ATinvdf_duT = A_i * df_duT + dA_dmT = self.getADeriv(freq, u_src, ATinvdf_duT, adjoint=True) + dRHS_dmT = self.getRHSDeriv( + freq, src, ATinvdf_duT, adjoint=True + ) + du_dmT = -dA_dmT + + if not isinstance(dRHS_dmT, Zero): + du_dmT += dRHS_dmT + if not isinstance(df_dmT[0], Zero): + 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 + + self._Jmatrix = Jmatrix + + return self._Jmatrix + + def getJtJdiag(self, m, W=None, f=None): + 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: + + .. 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 + + if getattr(self, "_gtgdiag", None) is None: + J = self.getJ(m, f=f) + + if W is None: + W = np.ones(J.shape[0]) + else: + W = W.diagonal() ** 2 + + diag = np.einsum("i,ij,ij->j", W, J, J) + + self._gtgdiag = diag + + return self._gtgdiag + + # @profile + def getSourceTerm(self, freq): + 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. + + 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) + if self._formulation == "EB": + s_m = np.zeros((self.mesh.nF, n_fields), dtype=complex, order="F") + s_e = np.zeros((self.mesh.nE, n_fields), dtype=complex, order="F") + elif self._formulation == "HJ": + s_m = np.zeros((self.mesh.nE, n_fields), dtype=complex, order="F") + s_e = np.zeros((self.mesh.nF, n_fields), dtype=complex, order="F") + + i = 0 + for src in Srcs: + ii = i + src._fields_per_source + smi, sei = src.eval(self) + if not isinstance(smi, Zero) and smi.ndim == 1: + smi = smi[:, None] + if not isinstance(sei, Zero) and sei.ndim == 1: + sei = sei[:, None] + s_m[:, i:ii] = s_m[:, i:ii] + smi + s_e[:, i:ii] = s_e[:, i:ii] + sei + i = ii + return s_m, s_e + + @property + def deleteTheseOnModelUpdate(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().deleteTheseOnModelUpdate + return toDelete + ["_Jmatrix", "_gtgdiag"] + + +############################################################################### +# E-B Formulation # +############################################################################### + + +class Simulation3DElectricField(BaseFDEMSimulation): + 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 }` + + """ + + _solutionType = "eSolution" + _formulation = "EB" + fieldsPair = Fields3DElectricField + + def getA(self, freq): + r"""System matrix for the frequency provided. + + This method returns the system matrix for the frequency provided: + + .. 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. + + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The system matrix. + """ + + MfMui = self.MfMui + C = self.mesh.edge_curl + + if self.permittivity is None: + MeSigma = self.MeSigma + A = C.T.tocsr() * MfMui * C + 1j * omega(freq) * MeSigma + else: + Meyhat = self._get_edge_admittivity_property_matrix(freq) + A = C.T.tocsr() * MfMui * C + 1j * omega(freq) * Meyhat + + return A + + def getADeriv_sigma(self, freq, u, v, adjoint=False): + 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"""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}} + + 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{\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 + + if adjoint: + return self.MfMuiDeriv(C * u).T * (C * v) + + 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) + # + self.getADeriv_permittivity(freq, u, v, adjoint) + ) + + def getRHS(self, freq): + 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} = - 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. + + 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) + C = self.mesh.edge_curl + MfMui = self.MfMui + + return C.T * (MfMui * s_m) - 1j * omega(freq) * s_e + + def getRHSDeriv(self, freq, src, v, adjoint=False): + 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 + MfMui = self.MfMui + s_m, s_e = self.getSourceTerm(freq, source=src) + s_mDeriv, s_eDeriv = src.evalDeriv(self, adjoint=adjoint) + MfMuiDeriv = self.MfMuiDeriv(s_m) + + if adjoint: + return ( + s_mDeriv(MfMui * (C * v)) + + MfMuiDeriv.T * (C * v) + - 1j * omega(freq) * s_eDeriv(v) + ) + return C.T * (MfMui * s_mDeriv(v) + MfMuiDeriv * v) - 1j * omega( + freq + ) * s_eDeriv(v) + + +class Simulation3DMagneticFluxDensity(BaseFDEMSimulation): + 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}` + + """ + + _solutionType = "bSolution" + _formulation = "EB" + fieldsPair = Fields3DMagneticFluxDensity + + def getA(self, freq): + r"""System matrix for the frequency provided. + + This method returns the system matrix for the frequency provided: + + .. 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. + + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The system matrix. + """ + + MfMui = self.MfMui + C = self.mesh.edge_curl + iomega = 1j * omega(freq) * sp.eye(self.mesh.nF) + + if self.permittivity is None: + MeSigmaI = self.MeSigmaI + A = C * (MeSigmaI * (C.T.tocsr() * MfMui)) + iomega + else: + MeyhatI = self._get_edge_admittivity_property_matrix( + freq, invert_matrix=True + ) + A = C * (MeyhatI * (C.T.tocsr() * MfMui)) + iomega + + if self._makeASymmetric: + return MfMui.T.tocsr() * A + return A + + def getADeriv_sigma(self, freq, u, v, adjoint=False): + 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 + C = self.mesh.edge_curl + MeSigmaIDeriv = self.MeSigmaIDeriv + vec = C.T * (MfMui * u) + + if adjoint: + return MeSigmaIDeriv(vec, C.T * v, adjoint) + return C * MeSigmaIDeriv(vec, v, adjoint) + + # if adjoint: + # return MeSigmaIDeriv.T * (C.T * v) + # 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 + + if adjoint: + return MfMuiDeriv.T * (C * (MeSigmaI.T * (C.T * v))) + 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 + + ADeriv = self.getADeriv_sigma(freq, u, v, adjoint) + self.getADeriv_mui( + freq, u, v, adjoint + ) + + if adjoint is False and self._makeASymmetric: + return self.MfMui.T * ADeriv + + return ADeriv + + def getRHS(self, freq): + 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 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. + + 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) + C = self.mesh.edge_curl + + if self.permittivity is None: + MeSigmaI = self.MeSigmaI + RHS = s_m + C * (MeSigmaI * s_e) + else: + MeyhatI = self._get_edge_admittivity_property_matrix( + freq, invert_matrix=True + ) + RHS = s_m + C * (MeyhatI * s_e) + + if self._makeASymmetric is True: + MfMui = self.MfMui + return MfMui.T * RHS + + return RHS + + def getRHSDeriv(self, freq, src, v, adjoint=False): + 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 + s_m, s_e = src.eval(self) + MfMui = self.MfMui + + if self._makeASymmetric and adjoint: + v = self.MfMui * v + + # MeSigmaIDeriv = self.MeSigmaIDeriv(s_e) + s_mDeriv, s_eDeriv = src.evalDeriv(self, adjoint=adjoint) + + if not adjoint: + # RHSderiv = C * (MeSigmaIDeriv * v) + RHSderiv = C * self.MeSigmaIDeriv(s_e, v, adjoint) + SrcDeriv = s_mDeriv(v) + C * (self.MeSigmaI * s_eDeriv(v)) + elif adjoint: + # RHSderiv = MeSigmaIDeriv.T * (C.T * v) + RHSderiv = self.MeSigmaIDeriv(s_e, C.T * v, adjoint) + SrcDeriv = s_mDeriv(v) + s_eDeriv(self.MeSigmaI.T * (C.T * v)) + + if self._makeASymmetric is True and not adjoint: + return MfMui.T * (SrcDeriv + RHSderiv) + + return RHSderiv + SrcDeriv + + +############################################################################### +# H-J Formulation # +############################################################################### + + +class Simulation3DCurrentDensity(BaseFDEMSimulation): + 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}` + + """ + + _solutionType = "jSolution" + _formulation = "HJ" + fieldsPair = Fields3DCurrentDensity + + permittivity = props.PhysicalProperty("Dielectric permittivity (F/m)") + + def __init__( + self, mesh, survey=None, forward_only=False, permittivity=None, **kwargs + ): + super().__init__(mesh=mesh, survey=survey, forward_only=forward_only, **kwargs) + self.permittivity = permittivity + + def getA(self, freq): + 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 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. + + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_faces, n_faces) sp.sparse.csr_matrix + The system matrix. + """ + + MeMuI = self.MeMuI + MfRho = self.MfRho + C = self.mesh.edge_curl + iomega = 1j * omega(freq) * sp.eye(self.mesh.nF) + + if self.permittivity is not None: + Mfyhati = self._get_face_admittivity_property_matrix( + freq, invert_model=True + ) + A = C * MeMuI * C.T.tocsr() * Mfyhati + iomega + else: + A = C * MeMuI * C.T.tocsr() * MfRho + iomega + + if self._makeASymmetric is True: + return MfRho.T.tocsr() * A + return A + + def getADeriv_rho(self, freq, u, v, adjoint=False): + 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 + C = self.mesh.edge_curl + + if adjoint: + vec = C * (MeMuI.T * (C.T * v)) + return self.MfRhoDeriv(u, vec, adjoint) + 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 + + MeMuIDeriv = self.MeMuIDeriv(C.T * (MfRho * u)) + + if adjoint is True: + # if self._makeASymmetric: + # v = MfRho * v + return MeMuIDeriv.T * (C.T * v) + + Aderiv = C * (MeMuIDeriv * v) + # if self._makeASymmetric: + # Aderiv = MfRho.T * Aderiv + 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 + + ADeriv = self.getADeriv_rho(freq, u, v, adjoint) + self.getADeriv_mu( + freq, u, v, adjoint + ) + + if not adjoint and self._makeASymmetric: + return self.MfRho.T * ADeriv + + return ADeriv + + def getRHS(self, freq): + 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} = - 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. + + 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) + C = self.mesh.edge_curl + MeMuI = self.MeMuI + + RHS = C * (MeMuI * s_m) - 1j * omega(freq) * s_e + if self._makeASymmetric is True: + MfRho = self.MfRho + return MfRho.T * RHS + + return RHS + + def getRHSDeriv(self, freq, src, v, adjoint=False): + 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 + # if self._makeASymmetric is True: + # MfRho = self.MfRho + # return MfRho.T*RHS + + C = self.mesh.edge_curl + MeMuI = self.MeMuI + MeMuIDeriv = self.MeMuIDeriv + s_mDeriv, s_eDeriv = src.evalDeriv(self, adjoint=adjoint) + s_m, _ = self.getSourceTerm(freq) + + if adjoint: + if self._makeASymmetric: + MfRho = self.MfRho + v = MfRho * v + CTv = C.T * v + return ( + s_mDeriv(MeMuI.T * CTv) + + MeMuIDeriv(s_m).T * CTv + - 1j * omega(freq) * s_eDeriv(v) + ) + + else: + RHSDeriv = C * (MeMuI * s_mDeriv(v) + MeMuIDeriv(s_m) * v) - 1j * omega( + freq + ) * s_eDeriv(v) + + if self._makeASymmetric: + MfRho = self.MfRho + return MfRho.T * RHSDeriv + return RHSDeriv + + +class Simulation3DMagneticField(BaseFDEMSimulation): + 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}` + + """ + + _solutionType = "hSolution" + _formulation = "HJ" + fieldsPair = Fields3DMagneticField + + def getA(self, freq): + 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}} + + 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. + + Parameters + ---------- + freq : float + The frequency in Hz. + + Returns + ------- + (n_edges, n_edges) sp.sparse.csr_matrix + The system matrix. + """ + + MeMu = self.MeMu + C = self.mesh.edge_curl + + if self.permittivity is None: + MfRho = self.MfRho + return C.T.tocsr() * (MfRho * C) + 1j * omega(freq) * MeMu + else: + Mfyhati = self._get_face_admittivity_property_matrix( + freq, invert_model=True + ) + return C.T.tocsr() * (Mfyhati * C) + 1j * omega(freq) * MeMu + + def getADeriv_rho(self, freq, u, v, adjoint=False): + 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. + + 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: + return self.MfRhoDeriv(C * u, C * v, adjoint) + 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: + return 1j * omega(freq) * (MeMuDeriv.T * v) + + 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 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:`\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. + + 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) + C = self.mesh.edge_curl + + if self.permittivity is None: + MfRho = self.MfRho + return s_m + C.T * (MfRho * s_e) + else: + Mfyhati = self._get_face_admittivity_property_matrix( + freq, invert_model=True + ) + return s_m + C.T * (Mfyhati * s_e) + + def getRHSDeriv(self, freq, src, v, adjoint=False): + 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) + C = self.mesh.edge_curl + MfRho = self.MfRho + + # MfRhoDeriv = self.MfRhoDeriv(s_e) + # if not adjoint: + # RHSDeriv = C.T * (MfRhoDeriv * v) + # elif adjoint: + # RHSDeriv = MfRhoDeriv.T * (C * v) + if not adjoint: + RHSDeriv = C.T * (self.MfRhoDeriv(s_e, v, adjoint)) + elif adjoint: + RHSDeriv = self.MfRhoDeriv(s_e, C * v, adjoint) + + s_mDeriv, s_eDeriv = src.evalDeriv(self, adjoint=adjoint) + + return RHSDeriv + s_mDeriv(v) + C.T * (MfRho * s_eDeriv(v)) diff --git a/SimPEG/electromagnetics/frequency_domain/simulation_1d.py b/simpeg/electromagnetics/frequency_domain/simulation_1d.py similarity index 99% rename from SimPEG/electromagnetics/frequency_domain/simulation_1d.py rename to simpeg/electromagnetics/frequency_domain/simulation_1d.py index fc9200b146..7d880dd09c 100644 --- a/SimPEG/electromagnetics/frequency_domain/simulation_1d.py +++ b/simpeg/electromagnetics/frequency_domain/simulation_1d.py @@ -28,7 +28,7 @@ def survey(self): Returns ------- - SimPEG.electromagnetics.frequency_domain.survey.Survey + simpeg.electromagnetics.frequency_domain.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey set") diff --git a/SimPEG/electromagnetics/frequency_domain/sources.py b/simpeg/electromagnetics/frequency_domain/sources.py similarity index 97% rename from SimPEG/electromagnetics/frequency_domain/sources.py rename to simpeg/electromagnetics/frequency_domain/sources.py index 74e121b215..c92ab26b3c 100644 --- a/SimPEG/electromagnetics/frequency_domain/sources.py +++ b/simpeg/electromagnetics/frequency_domain/sources.py @@ -26,7 +26,7 @@ class BaseFDEMSrc(BaseEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of FDEM receivers frequency : float Source frequency @@ -209,7 +209,7 @@ class RawVec_e(BaseFDEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of FDEM receivers frequency : float Source frequency @@ -247,7 +247,7 @@ class RawVec_m(BaseFDEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of FDEM receivers frequency : float Source frequency @@ -284,7 +284,7 @@ class RawVec(RawVec_e, RawVec_m): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of FDEM receivers frequency : float Source frequency @@ -362,7 +362,7 @@ class MagDipole(BaseFDEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of FDEM receivers frequency : float Source frequency @@ -643,7 +643,6 @@ def s_eDeriv(self, simulation, v, adjoint=False): class MagDipole_Bfield(MagDipole): - """ Point magnetic dipole source calculated with the analytic solution for the fields from a magnetic dipole. No discrete curl is taken, so the magnetic @@ -654,7 +653,7 @@ class MagDipole_Bfield(MagDipole): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of FDEM receivers frequency : float Source frequency @@ -735,7 +734,7 @@ class CircularLoop(MagDipole): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of FDEM receivers frequency : float Source frequency @@ -770,11 +769,12 @@ def __init__( **kwargs, ): kwargs.pop("moment", None) - N = kwargs.pop("N", None) - if N is not None: - self.N = N - else: - self.n_turns = n_turns + + # 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__( receiver_list=receiver_list, frequency=frequency, @@ -841,7 +841,8 @@ def moment(self, value): if value is not None: warnings.warn( "Moment is not set as a property. I is the product" - "of the loop radius and transmitter current" + "of the loop radius and transmitter current", + stacklevel=2, ) pass @@ -870,7 +871,9 @@ def _srcFct(self, obsLoc, coordinates="cartesian"): ) return self.n_turns * self._loop.vector_potential(obsLoc, coordinates) - N = deprecate_property(n_turns, "N", "n_turns", removal_version="0.19.0") + N = deprecate_property( + n_turns, "N", "n_turns", removal_version="0.19.0", error=True + ) class PrimSecSigma(BaseFDEMSrc): @@ -906,7 +909,7 @@ class PrimSecMappedSigma(BaseFDEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receiver.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receiver.BaseRx List of FDEM receivers frequency : float Frequency @@ -1139,7 +1142,7 @@ def s_e(self, simulation, f=None): ---------- simulation : BaseFDEMSimulation SimPEG FDEM simulation - f : SimPEG.electromagnetics.frequency_domain.field.FieldsFDEM + f : simpeg.electromagnetics.frequency_domain.field.FieldsFDEM A SimPEG FDEM fields object Returns @@ -1204,7 +1207,7 @@ class LineCurrent(BaseFDEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx List of FDEM receivers frequency : float Source frequency @@ -1292,7 +1295,7 @@ def Mejs(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation + simulation : simpeg.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation Base FDEM simulation Returns @@ -1311,7 +1314,7 @@ def Mfjs(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation + simulation : simpeg.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation Base FDEM simulation Returns @@ -1330,7 +1333,7 @@ def getRHSdc(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation + simulation : simpeg.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation Base FDEM simulation Returns diff --git a/SimPEG/electromagnetics/frequency_domain/survey.py b/simpeg/electromagnetics/frequency_domain/survey.py similarity index 97% rename from SimPEG/electromagnetics/frequency_domain/survey.py rename to simpeg/electromagnetics/frequency_domain/survey.py index 4df3a90f05..c7c0a1d453 100644 --- a/SimPEG/electromagnetics/frequency_domain/survey.py +++ b/simpeg/electromagnetics/frequency_domain/survey.py @@ -8,7 +8,7 @@ class Survey(BaseSurvey): Parameters ---------- - source_list : list of SimPEG.electromagnetic.frequency_domain.sources.BaseFDEMSrc + source_list : list of simpeg.electromagnetic.frequency_domain.sources.BaseFDEMSrc List of SimPEG FDEM sources """ diff --git a/SimPEG/electromagnetics/natural_source/__init__.py b/simpeg/electromagnetics/natural_source/__init__.py similarity index 92% rename from SimPEG/electromagnetics/natural_source/__init__.py rename to simpeg/electromagnetics/natural_source/__init__.py index ceaee93eaa..dca9d80cc5 100644 --- a/SimPEG/electromagnetics/natural_source/__init__.py +++ b/simpeg/electromagnetics/natural_source/__init__.py @@ -1,8 +1,8 @@ """ ============================================================================== -Natural Source EM (:mod:`SimPEG.electromagnetics.natural_source`) +Natural Source EM (:mod:`simpeg.electromagnetics.natural_source`) ============================================================================== -.. currentmodule:: SimPEG.electromagnetics.natural_source +.. currentmodule:: simpeg.electromagnetics.natural_source About ``natural_source`` diff --git a/SimPEG/electromagnetics/natural_source/fields.py b/simpeg/electromagnetics/natural_source/fields.py similarity index 97% rename from SimPEG/electromagnetics/natural_source/fields.py rename to simpeg/electromagnetics/natural_source/fields.py index 298671aa6d..2493c7f0bd 100644 --- a/SimPEG/electromagnetics/natural_source/fields.py +++ b/simpeg/electromagnetics/natural_source/fields.py @@ -233,7 +233,7 @@ def _eDeriv_u(self, src, du_dm_v, adjoint=False): """ Partial derivative of the total electric field with respect to the solution. - :param SimPEG.EM.NSEM.Src src: source + :param simpeg.EM.NSEM.Src src: source :param numpy.ndarray du_dm_v: vector to take product with Size (nE,) when adjoint=True, (nU,) when adjoint=False :param bool adjoint: adjoint? @@ -247,10 +247,10 @@ def _eDeriv_m(self, src, v, adjoint=False): """ Partial derivative of the total electric field with respect to the inversion model. Here, we assume that the primary does not depend on the model. Note that this also includes derivative contributions from the sources. - :param SimPEG.electromagnetics.frequency_domain.Src src: source + :param simpeg.electromagnetics.frequency_domain.Src src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? - :rtype: SimPEG.utils.Zero + :rtype: simpeg.utils.Zero :return: product of the electric field derivative with respect to the inversion model with a vector """ @@ -285,7 +285,7 @@ def _bDeriv_u(self, src, du_dm_v, adjoint=False): """ Derivative of the magnetic flux density with respect to the solution - :param SimPEG.electromagnetics.frequency_domain.Src src: source + :param simpeg.electromagnetics.frequency_domain.Src src: source :param numpy.ndarray du_dm_v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -303,7 +303,7 @@ def _bDeriv_m(self, src, v, adjoint=False): """ Derivative of the magnetic flux density with respect to the inversion model. - :param SimPEG.electromagnetics.frequency_domain.Src src: source + :param simpeg.electromagnetics.frequency_domain.Src src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -325,7 +325,7 @@ def _hDeriv_m(self, src, v, adjoint=False): """ Derivative of the magnetic flux density with respect to the inversion model. - :param SimPEG.electromagnetics.frequency_domain.Src src: source + :param simpeg.electromagnetics.frequency_domain.Src src: source :param numpy.ndarray v: vector to take product with :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -604,7 +604,7 @@ def _e_pxDeriv_u(self, src, du_dm_v, adjoint=False): """ Derivative of e_px wrt u - :param SimPEG.NSEM.src src: The source of the problem + :param simpeg.NSEM.src src: The source of the problem :param numpy.ndarray du_dm_v: vector to take product with Size (nE,) when adjoint=True, (nU,) when adjoint=False :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -621,7 +621,7 @@ def _e_pyDeriv_u(self, src, du_dm_v, adjoint=False): """ Derivative of e_py wrt u - :param SimPEG.NSEM.src src: The source of the problem + :param simpeg.NSEM.src src: The source of the problem :param numpy.ndarray du_dm_v: vector to take product with Size (nE,) when adjoint=True, (nU,) when adjoint=False :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -639,7 +639,7 @@ def _e_pxDeriv_m(self, src, v, adjoint=False): """ Derivative of e_px wrt m - :param SimPEG.NSEM.src src: The source of the problem + :param simpeg.NSEM.src src: The source of the problem :param numpy.ndarray v: vector to take product with Size (nE,) when adjoint=True, (nU,) when adjoint=False :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -654,7 +654,7 @@ def _e_pyDeriv_m(self, src, v, adjoint=False): """ Derivative of e_py wrt m - :param SimPEG.NSEM.src src: The source of the problem + :param simpeg.NSEM.src src: The source of the problem :param numpy.ndarray v: vector to take product with Size (nE,) when adjoint=True, (nU,) when adjoint=False :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -689,7 +689,7 @@ def _b_pxDeriv_u(self, src, du_dm_v, adjoint=False): """ Derivative of b_px with wrt u - :param SimPEG.NSEM.src src: The source of the problem + :param simpeg.NSEM.src src: The source of the problem :param numpy.ndarray du_dm_v: vector to take product with. Size (nF,) when adjoint=True, (nU,) when adjoint=False :param bool adjoint: adjoint? :rtype: numpy.ndarray @@ -706,7 +706,7 @@ def _b_pxDeriv_u(self, src, du_dm_v, adjoint=False): def _b_pyDeriv_u(self, src, du_dm_v, adjoint=False): """Derivative of b_py with wrt u - :param SimPEG.NSEM.src src: The source of the problem + :param simpeg.NSEM.src src: The source of the problem :param numpy.ndarray du_dm_v: vector to take product with. Size (nF,) when adjoint=True, (nU,) when adjoint=False :param bool adjoint: adjoint? :rtype: numpy.ndarray diff --git a/SimPEG/electromagnetics/natural_source/receivers.py b/simpeg/electromagnetics/natural_source/receivers.py similarity index 97% rename from SimPEG/electromagnetics/natural_source/receivers.py rename to simpeg/electromagnetics/natural_source/receivers.py index b9e822e4fc..7c6eed5207 100644 --- a/SimPEG/electromagnetics/natural_source/receivers.py +++ b/simpeg/electromagnetics/natural_source/receivers.py @@ -383,11 +383,11 @@ def eval(self, src, mesh, f, return_complex=False): # noqa: A003 Parameters ---------- - src : SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc + src : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc NSEM source mesh : discretize.TensorMesh mesh Mesh on which the discretize solution is obtained - f : SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM + f : simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM NSEM fields object of the source return_complex : bool (optional) Flag for return the complex evaluation @@ -413,11 +413,11 @@ def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): Parameters ---------- - str : SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc + str : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc NSEM source mesh : discretize.TensorMesh Mesh on which the discretize solution is obtained - f : SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM + f : simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM NSEM fields object of the source du_dm_v : None, Supply pre-computed derivative? @@ -592,11 +592,11 @@ def eval(self, src, mesh, f, return_complex=False): # noqa: A003 Parameters ---------- - src : SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc + src : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc NSEM source mesh : discretize.TensorMesh mesh Mesh on which the discretize solution is obtained - f : SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM + f : simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM NSEM fields object of the source return_complex : bool (optional) Flag for return the complex evaluation @@ -616,11 +616,11 @@ def evalDeriv(self, src, mesh, f, du_dm_v=None, v=None, adjoint=False): Parameters ---------- - str : SimPEG.electromagnetics.frequency_domain.sources.BaseFDEMSrc + str : simpeg.electromagnetics.frequency_domain.sources.BaseFDEMSrc NSEM source mesh : discretize.TensorMesh Mesh on which the discretize solution is obtained - f : SimPEG.electromagnetics.frequency_domain.fields.FieldsFDEM + f : simpeg.electromagnetics.frequency_domain.fields.FieldsFDEM NSEM fields object of the source du_dm_v : None, Supply pre-computed derivative? diff --git a/SimPEG/electromagnetics/natural_source/simulation.py b/simpeg/electromagnetics/natural_source/simulation.py similarity index 100% rename from SimPEG/electromagnetics/natural_source/simulation.py rename to simpeg/electromagnetics/natural_source/simulation.py diff --git a/SimPEG/electromagnetics/natural_source/simulation_1d.py b/simpeg/electromagnetics/natural_source/simulation_1d.py similarity index 99% rename from SimPEG/electromagnetics/natural_source/simulation_1d.py rename to simpeg/electromagnetics/natural_source/simulation_1d.py index c02ed2c66f..46de6f436e 100644 --- a/SimPEG/electromagnetics/natural_source/simulation_1d.py +++ b/simpeg/electromagnetics/natural_source/simulation_1d.py @@ -71,7 +71,7 @@ def survey(self): Returns ------- - SimPEG.electromagnetics.frequency_domain.survey.Survey + simpeg.electromagnetics.frequency_domain.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey set") diff --git a/SimPEG/electromagnetics/natural_source/sources.py b/simpeg/electromagnetics/natural_source/sources.py similarity index 93% rename from SimPEG/electromagnetics/natural_source/sources.py rename to simpeg/electromagnetics/natural_source/sources.py index 11068a1d5c..83aeed5742 100644 --- a/SimPEG/electromagnetics/natural_source/sources.py +++ b/simpeg/electromagnetics/natural_source/sources.py @@ -19,7 +19,7 @@ class Planewave(BaseFDEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of NSEM receivers frequency : float Source frequency @@ -38,7 +38,7 @@ class PlanewaveXYPrimary(Planewave): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of NSEM receivers frequency : float Source frequency @@ -96,7 +96,7 @@ def ePrimary(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.natural_source_simulation.BaseNSEMSimulation + simulation : simpeg.electromagnetics.natural_source_simulation.BaseNSEMSimulation A NSEM simulation Returns @@ -116,7 +116,7 @@ def bPrimary(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation + simulation : simpeg.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation A NSEM simulation Returns @@ -138,7 +138,7 @@ def s_e(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation + simulation : simpeg.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation A NSEM simulation Returns @@ -168,7 +168,7 @@ def s_eDeriv(self, simulation, v, adjoint=False): Parameters ---------- - simulation : SimPEG.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation + simulation : simpeg.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation A NSEM simulation v : numpy.ndarray A vector @@ -188,7 +188,7 @@ def s_eDeriv_m(self, simulation, v, adjoint=False): Parameters ---------- - simulation : SimPEG.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation + simulation : simpeg.electromagnetics.frequency_domain.simulation.BaseFDEMSimulation A NSEM simulation v : numpy.ndarray A vector diff --git a/SimPEG/electromagnetics/natural_source/survey.py b/simpeg/electromagnetics/natural_source/survey.py similarity index 97% rename from SimPEG/electromagnetics/natural_source/survey.py rename to simpeg/electromagnetics/natural_source/survey.py index 3415b212ec..935316a955 100644 --- a/SimPEG/electromagnetics/natural_source/survey.py +++ b/simpeg/electromagnetics/natural_source/survey.py @@ -20,7 +20,7 @@ class Data(BaseData, DataNSEMPlotMethods): Parameters ---------- - survey : SimPEG.survey.Survey + survey : simpeg.survey.Survey Natural source EM survey dobs : numpy.ndarray Observed data @@ -143,11 +143,11 @@ def fromRecArray(cls, recArray, srcType="primary"): 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" + The type of simpeg.EM.NSEM.SrcNSEM to be used. Either "primary" or "total" Returns ------- - SimPEG.electromagnetics.natural_source.sources.SrcNSEM + simpeg.electromagnetics.natural_source.sources.SrcNSEM Natural source """ if srcType == "primary": @@ -220,7 +220,7 @@ def fromRecArray(cls, recArray, srcType="primary"): def _rec_to_ndarr(rec_arr, data_type=float): """ Function to transform a numpy record array to a nd array. - dupe of SimPEG.electromagnetics.natural_source.utils.rec_to_ndarr to avoid circular import + dupe of simpeg.electromagnetics.natural_source.utils.rec_to_ndarr to avoid circular import """ # fix for numpy >= 1.16.0 # https://numpy.org/devdocs/release/1.16.0-notes.html#multi-field-views-return-a-view-instead-of-a-copy diff --git a/SimPEG/electromagnetics/natural_source/utils/__init__.py b/simpeg/electromagnetics/natural_source/utils/__init__.py similarity index 95% rename from SimPEG/electromagnetics/natural_source/utils/__init__.py rename to simpeg/electromagnetics/natural_source/utils/__init__.py index baae8201b5..285ec2731b 100644 --- a/SimPEG/electromagnetics/natural_source/utils/__init__.py +++ b/simpeg/electromagnetics/natural_source/utils/__init__.py @@ -1,10 +1,11 @@ -""" module SimPEG.EM.NSEM.Utils +""" module simpeg.EM.NSEM.Utils Collection of utilities that are usefull for the NSEM problem NOTE: These utilities are not well test, use with care """ + from .solutions_1d import get1DEfields # Add the names of the functions from .analytic_1d import getEHfields, getImpedance from .data_utils import ( diff --git a/SimPEG/electromagnetics/natural_source/utils/analytic_1d.py b/simpeg/electromagnetics/natural_source/utils/analytic_1d.py similarity index 98% rename from SimPEG/electromagnetics/natural_source/utils/analytic_1d.py rename to simpeg/electromagnetics/natural_source/utils/analytic_1d.py index 78de082377..ad50de476f 100644 --- a/SimPEG/electromagnetics/natural_source/utils/analytic_1d.py +++ b/simpeg/electromagnetics/natural_source/utils/analytic_1d.py @@ -30,9 +30,9 @@ def getEHfields(m1d, sigma, freq, zd, scaleUD=True, scaleValue=1): # Initiate the propagation matrix, in the order down up. UDp = np.zeros((2, m1d.nC + 1), dtype=complex) - UDp[ - 1, 0 - ] = scaleValue # Set the wave amplitude as 1 into the half-space at the bottom of the mesh + UDp[1, 0] = ( + scaleValue # Set the wave amplitude as 1 into the half-space at the bottom of the mesh + ) # Loop over all the layers, starting at the bottom layer for lnr, h in enumerate(m1d.h[0]): # lnr-number of layer, h-thickness of the layer # Calculate diff --git a/SimPEG/electromagnetics/natural_source/utils/data_utils.py b/simpeg/electromagnetics/natural_source/utils/data_utils.py similarity index 96% rename from SimPEG/electromagnetics/natural_source/utils/data_utils.py rename to simpeg/electromagnetics/natural_source/utils/data_utils.py index 2e9ebf0ea3..eb1577be2b 100644 --- a/SimPEG/electromagnetics/natural_source/utils/data_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/data_utils.py @@ -3,15 +3,14 @@ import numpy.lib.recfunctions as recFunc from scipy.constants import mu_0 -import SimPEG as simpeg -from SimPEG.electromagnetics.natural_source.survey import Survey, Data -from SimPEG.electromagnetics.natural_source.receivers import ( - PointNaturalSource, +import simpeg as simpeg +from simpeg.electromagnetics.natural_source.survey import Survey, Data +from simpeg.electromagnetics.natural_source.receivers import ( PointNaturalSource, Point3DTipper, ) -from SimPEG.electromagnetics.natural_source.sources import PlanewaveXYPrimary -from SimPEG.electromagnetics.natural_source.utils import ( +from simpeg.electromagnetics.natural_source.sources import PlanewaveXYPrimary +from simpeg.electromagnetics.natural_source.utils import ( analytic_1d, plot_data_types as pDt, ) @@ -22,7 +21,7 @@ def rotate_data(NSEMdata, rot_angle): Function that rotates clockwise by rotation angle (- negative for a counter-clockwise rotation) - :param SimPEG.electromagnetics.natural_source.Data NSEMdata: NSEM data object to process + :param simpeg.electromagnetics.natural_source.Data NSEMdata: NSEM data object to process :param float rot_angle: Rotation angel in degrees, positive for clockwise rotation """ recData = NSEMdata.toRecArray("Complex") @@ -57,7 +56,7 @@ def extract_data_info(NSEMdata): Useful when assigning uncertainties to data based on frequencies and receiver types. - :param SimPEG.electromagnetics.natural_source.Data NSEMdata: NSEM data object to process + :param simpeg.electromagnetics.natural_source.Data NSEMdata: NSEM data object to process """ dL, freqL, rxTL = [], [], [] @@ -79,7 +78,7 @@ def resample_data(NSEMdata, locs="All", freqs="All", rxs="All", verbose=False): (uses the numerator location as a reference). Also gives the option of selecting frequencies and receiver. - :param SimPEG.electromagnetics.natural_source.Data NSEMdata: NSEM data object to process + :param simpeg.electromagnetics.natural_source.Data NSEMdata: NSEM data object to process :param locs: receiver locations to use (default is 'All' locations) :type locs: numpy.ndarray, optional @@ -218,7 +217,7 @@ def convert3Dto1Dobject(NSEMdata, rxType3D="yx"): Function that converts a 3D NSEMdata of a list of 1D NSEMdata objects for running 1D inversions for. - :param SimPEG.electromagnetics.natural_source.Data NSEMdata: NSEM data object to process + :param simpeg.electromagnetics.natural_source.Data NSEMdata: NSEM data object to process :param rxType3D: component of the NSEMdata to use. Can be 'xy', 'yx' or 'det' diff --git a/SimPEG/electromagnetics/natural_source/utils/data_viewer.py b/simpeg/electromagnetics/natural_source/utils/data_viewer.py similarity index 98% rename from SimPEG/electromagnetics/natural_source/utils/data_viewer.py rename to simpeg/electromagnetics/natural_source/utils/data_viewer.py index 0d04481a16..1e8270fcaa 100644 --- a/SimPEG/electromagnetics/natural_source/utils/data_viewer.py +++ b/simpeg/electromagnetics/natural_source/utils/data_viewer.py @@ -14,7 +14,7 @@ class NSEM_data_viewer: Generates a clickble location map of the data, plotting data curves in a separate window. - :param SimPEG.electromagnetics.natural_source.Data data: Data object, needs to have assigned + :param simpeg.electromagnetics.natural_source.Data data: Data object, needs to have assigned relative_error and noise_floor :param data_dict: A dictionary of other NSEM Data objects diff --git a/SimPEG/electromagnetics/natural_source/utils/edi_files_utils.py b/simpeg/electromagnetics/natural_source/utils/edi_files_utils.py similarity index 82% rename from SimPEG/electromagnetics/natural_source/utils/edi_files_utils.py rename to simpeg/electromagnetics/natural_source/utils/edi_files_utils.py index b4383b442e..6ae02aea9e 100644 --- a/SimPEG/electromagnetics/natural_source/utils/edi_files_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/edi_files_utils.py @@ -1,15 +1,21 @@ # Functions to import and export MT EDI files. -from SimPEG import mkvc +from simpeg import mkvc from numpy.lib import recfunctions as recFunc from .data_utils import rec_to_ndarr +from discretize.utils import requires # Import modules import numpy as np import os import re -import utm +try: + import utm +except ImportError: + utm = False + +@requires({"utm": utm}) class EDIimporter: """ A class to import EDIfiles. @@ -86,7 +92,7 @@ def importFiles(self): # Find the location latD, longD, elevM = _findLatLong(EDIlines) # Transfrom coordinates - transCoord = self._transfromPoints(longD, latD) + transCoord = utm.from_latlon(latD, longD) # Extract the name of the file (station) EDIname = EDIfile.split(os.sep)[-1].split(".")[0] # Arrange the data @@ -128,37 +134,6 @@ def importFiles(self): # Assign the data self._data = outTemp - # % Assign the data to the obj - # nOutData=length(obj.data); - # obj.data(nOutData+1:nOutData+length(TEMP.data),:) = TEMP.data; - def _transfromPoints(self, longD, latD): - # Import the coordinate projections - # try: - # import osr - # except ImportError as e: - # print( - # ( - # "Could not import osr, missing the gdal" - # + "package\nCan not project coordinates" - # ) - # ) - # raise e - # # Coordinates convertor - # if self._2out is None: - # src = osr.SpatialReference() - # src.ImportFromEPSG(4326) - # out = osr.SpatialReference() - # if self._outEPSG is None: - # # Find the UTM EPSG number - # Nnr = 700 if latD < 0.0 else 600 - # utmZ = int(1 + (longD + 180.0) / 6.0) - # self._outEPSG = 32000 + Nnr + utmZ - # out.ImportFromEPSG(self._outEPSG) - # self._2out = osr.CoordinateTransformation(src, out) - # # Return the transfrom - # return self._2out.TransformPoint(longD, latD) - return utm.from_latlon(latD, longD) - # Hidden functions def _findLatLong(fileLines): diff --git a/SimPEG/electromagnetics/natural_source/utils/plot_data_types.py b/simpeg/electromagnetics/natural_source/utils/plot_data_types.py similarity index 99% rename from SimPEG/electromagnetics/natural_source/utils/plot_data_types.py rename to simpeg/electromagnetics/natural_source/utils/plot_data_types.py index aad9088c34..7eaee1e8be 100644 --- a/SimPEG/electromagnetics/natural_source/utils/plot_data_types.py +++ b/simpeg/electromagnetics/natural_source/utils/plot_data_types.py @@ -1,4 +1,5 @@ -from matplotlib import pyplot as plt, colors, numpy as np +from matplotlib import pyplot as plt, colors +import numpy as np def plotIsoFreqNSimpedance( diff --git a/SimPEG/electromagnetics/natural_source/utils/plot_utils.py b/simpeg/electromagnetics/natural_source/utils/plot_utils.py similarity index 100% rename from SimPEG/electromagnetics/natural_source/utils/plot_utils.py rename to simpeg/electromagnetics/natural_source/utils/plot_utils.py diff --git a/SimPEG/electromagnetics/natural_source/utils/solutions_1d.py b/simpeg/electromagnetics/natural_source/utils/solutions_1d.py similarity index 100% rename from SimPEG/electromagnetics/natural_source/utils/solutions_1d.py rename to simpeg/electromagnetics/natural_source/utils/solutions_1d.py diff --git a/SimPEG/electromagnetics/natural_source/utils/source_utils.py b/simpeg/electromagnetics/natural_source/utils/source_utils.py similarity index 99% rename from SimPEG/electromagnetics/natural_source/utils/source_utils.py rename to simpeg/electromagnetics/natural_source/utils/source_utils.py index 64097ee4b0..7687ff5894 100644 --- a/SimPEG/electromagnetics/natural_source/utils/source_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/source_utils.py @@ -80,7 +80,7 @@ def analytic1DModelSource(mesh, freq, sigma_1d): :return: eBG_bp, E fields for the background model at both polarizations with shape (mesh.nE, 2). """ - from SimPEG.NSEM.Utils import getEHfields + from simpeg.NSEM.Utils import getEHfields # Get a 1d solution for a halfspace background if mesh.dim == 1: diff --git a/SimPEG/electromagnetics/natural_source/utils/test_utils.py b/simpeg/electromagnetics/natural_source/utils/test_utils.py similarity index 98% rename from SimPEG/electromagnetics/natural_source/utils/test_utils.py rename to simpeg/electromagnetics/natural_source/utils/test_utils.py index e446ff7ad4..878ddaea82 100644 --- a/SimPEG/electromagnetics/natural_source/utils/test_utils.py +++ b/simpeg/electromagnetics/natural_source/utils/test_utils.py @@ -1,7 +1,7 @@ import numpy as np import discretize -from SimPEG import maps, mkvc, utils, Data +from simpeg import maps, mkvc, utils, Data from ....utils import unpack_widths from ..receivers import ( PointNaturalSource, @@ -12,7 +12,6 @@ from ..simulation import Simulation3DPrimarySecondary from .data_utils import appResPhs -np.random.seed(1100) # Define the tolerances TOLr = 5e-2 TOLp = 5e-2 @@ -510,14 +509,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) @@ -547,7 +547,7 @@ def blockInhalfSpace(conds): ccM = M.gridCC # conds = [1e-2] groundInd = ccM[:, 2] < elev - sig = utils.model_builder.defineBlock( + sig = utils.model_builder.create_block_in_wholespace( M.gridCC, np.array([-1000, -1000, -1500]), np.array([1000, 1000, -1000]), conds ) sig[~groundInd] = 1e-8 diff --git a/SimPEG/electromagnetics/static/__init__.py b/simpeg/electromagnetics/static/__init__.py similarity index 81% rename from SimPEG/electromagnetics/static/__init__.py rename to simpeg/electromagnetics/static/__init__.py index efdaece043..4db8ce3cae 100644 --- a/SimPEG/electromagnetics/static/__init__.py +++ b/simpeg/electromagnetics/static/__init__.py @@ -1,4 +1,5 @@ from . import resistivity from . import induced_polarization +from . import self_potential from . import spectral_induced_polarization from . import utils diff --git a/SimPEG/electromagnetics/static/induced_polarization/__init__.py b/simpeg/electromagnetics/static/induced_polarization/__init__.py similarity index 83% rename from SimPEG/electromagnetics/static/induced_polarization/__init__.py rename to simpeg/electromagnetics/static/induced_polarization/__init__.py index 6383f4e05c..d185ffd542 100644 --- a/SimPEG/electromagnetics/static/induced_polarization/__init__.py +++ b/simpeg/electromagnetics/static/induced_polarization/__init__.py @@ -1,8 +1,8 @@ """ ============================================================================================ -Induced Polarization (:mod:`SimPEG.electromagnetics.static.induced_polarization`) +Induced Polarization (:mod:`simpeg.electromagnetics.static.induced_polarization`) ============================================================================================ -.. currentmodule:: SimPEG.electromagnetics.static.induced_polarization +.. currentmodule:: simpeg.electromagnetics.static.induced_polarization Simulations @@ -18,8 +18,9 @@ Receivers, Sources, and Surveys =============================== The ``induced_polarization`` module makes use of receivers, sources, and surveys -defined in the ``SimPEG.electromagnetics.static.resistivity`` module. +defined in the ``simpeg.electromagnetics.static.resistivity`` module. """ + from .simulation import ( Simulation3DCellCentered, Simulation3DNodal, diff --git a/SimPEG/electromagnetics/static/induced_polarization/run.py b/simpeg/electromagnetics/static/induced_polarization/run.py similarity index 91% rename from SimPEG/electromagnetics/static/induced_polarization/run.py rename to simpeg/electromagnetics/static/induced_polarization/run.py index 550579a319..d638047cf8 100644 --- a/SimPEG/electromagnetics/static/induced_polarization/run.py +++ b/simpeg/electromagnetics/static/induced_polarization/run.py @@ -1,6 +1,6 @@ import numpy as np -from SimPEG import ( +from simpeg import ( maps, optimization, inversion, @@ -37,7 +37,7 @@ def run_inversion( regmap = maps.IdentityMap(nP=int(actind.sum())) # Related to inversion if use_sensitivity_weight: - reg = regularization.Sparse(mesh, indActive=actind, mapping=regmap) + reg = regularization.Sparse(mesh, active_cells=actind, mapping=regmap) reg.alpha_s = alpha_s reg.alpha_x = alpha_x reg.alpha_y = alpha_y @@ -45,9 +45,8 @@ def run_inversion( else: reg = regularization.Sparse( mesh, - indActive=actind, + active_cells=actind, mapping=regmap, - cell_weights=mesh.cell_volumes[actind], ) reg.alpha_s = alpha_s reg.alpha_x = alpha_x diff --git a/SimPEG/electromagnetics/static/induced_polarization/simulation.py b/simpeg/electromagnetics/static/induced_polarization/simulation.py similarity index 82% rename from SimPEG/electromagnetics/static/induced_polarization/simulation.py rename to simpeg/electromagnetics/static/induced_polarization/simulation.py index 3b74844c77..d4dfa53e3f 100644 --- a/SimPEG/electromagnetics/static/induced_polarization/simulation.py +++ b/simpeg/electromagnetics/static/induced_polarization/simulation.py @@ -1,14 +1,15 @@ +from functools import cached_property + import numpy as np import scipy.sparse as sp -from .... import props, maps -from ....data import Data +from .... import maps, props from ....base import BasePDESimulation - -from ..resistivity import Simulation3DCellCentered as DC_3D_CC -from ..resistivity import Simulation3DNodal as DC_3D_N +from ....data import Data from ..resistivity import Simulation2DCellCentered as DC_2D_CC from ..resistivity import Simulation2DNodal as DC_2D_N +from ..resistivity import Simulation3DCellCentered as DC_3D_CC +from ..resistivity import Simulation3DNodal as DC_3D_N class BaseIPSimulation(BasePDESimulation): @@ -40,6 +41,23 @@ def sigmaDeriv(self): def rhoDeriv(self): return sp.diags(self.rho) @ self.etaDeriv + @cached_property + def _scale(self): + scale = Data(self.survey, np.ones(self.survey.nD)) + if self._f is None: + # re-uses the DC simulation's fields method + self._f = super().fields(None) + try: + f = self.fields_to_space(self._f) + except AttributeError: + f = self._f + # loop through receivers to check if they need to set the _dc_voltage + 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 + eta, etaMap, etaDeriv = props.Invertible("Electrical Chargeability (V/V)") def __init__( @@ -65,7 +83,6 @@ def __init__( _Jmatrix = None _pred = None - _scale = None def fields(self, m): if self.verbose: @@ -74,19 +91,6 @@ def fields(self, m): # re-uses the DC simulation's fields method self._f = super().fields(None) - if self._scale is None: - scale = Data(self.survey, np.ones(self.survey.nD)) - try: - f = self.fields_to_space(self._f) - except AttributeError: - f = self._f - # loop through receievers to check if they need to set the _dc_voltage - 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) - self._scale = scale.dobs - self._pred = self.forward(m, f=self._f) return self._f diff --git a/SimPEG/electromagnetics/static/induced_polarization/survey.py b/simpeg/electromagnetics/static/induced_polarization/survey.py similarity index 79% rename from SimPEG/electromagnetics/static/induced_polarization/survey.py rename to simpeg/electromagnetics/static/induced_polarization/survey.py index 742df99710..54afd3e080 100644 --- a/SimPEG/electromagnetics/static/induced_polarization/survey.py +++ b/simpeg/electromagnetics/static/induced_polarization/survey.py @@ -7,14 +7,14 @@ def from_dc_to_ip_survey(dc_survey, dim="2.5D"): Parameters ---------- - dc_survey : SimPEG.electromagnetics.static.resistivity.survey.Survey + dc_survey : simpeg.electromagnetics.static.resistivity.survey.Survey DC survey object dim : {'2.5D', '1D', '3D'} Dimension of the surface. Returns ------- - SimPEG.electromagnetics.static.induced_polarization.survey.Survey + simpeg.electromagnetics.static.induced_polarization.survey.Survey An IP survey object """ source_list = dc_survey.source_list diff --git a/SimPEG/electromagnetics/static/resistivity/IODC.py b/simpeg/electromagnetics/static/resistivity/IODC.py similarity index 98% rename from SimPEG/electromagnetics/static/resistivity/IODC.py rename to simpeg/electromagnetics/static/resistivity/IODC.py index 5b06ddc5f1..cc47403d7e 100644 --- a/SimPEG/electromagnetics/static/resistivity/IODC.py +++ b/simpeg/electromagnetics/static/resistivity/IODC.py @@ -10,7 +10,7 @@ from ....utils import ( sdiag, - uniqueRows, + unique_rows, plot2Ddata, validate_type, validate_integer, @@ -95,7 +95,10 @@ def __init__( self.pad_rate_z = pad_rate_z self.ncell_per_dipole = ncell_per_dipole self.corezlength = corezlength - warnings.warn("code under construction - API might change in the future") + warnings.warn( + "code under construction - API might change in the future", + stacklevel=2, + ) @property def survey_layout(self): @@ -725,12 +728,6 @@ def geometric_factor(self, survey): G = geometric_factor(survey, space_type=self.space_type) return G - def from_ambn_locations_to_survey(self, *args, **kwargs): - raise NotImplementedError( - "from_ambn_locations_to_survey has been renamed to " - "from_abmn_locations_to_survey. It will be removed in a future version 0.17.0 of simpeg", - ) - def from_abmn_locations_to_survey( self, a_locations, @@ -764,8 +761,8 @@ def from_abmn_locations_to_survey( if times_ip is not None: self.times_ip = times_ip - uniqSrc = uniqueRows(np.c_[self.a_locations, self.b_locations]) - uniqElec = uniqueRows( + uniqSrc = unique_rows(np.c_[self.a_locations, self.b_locations]) + uniqElec = unique_rows( np.vstack( (self.a_locations, self.b_locations, self.m_locations, self.n_locations) ) @@ -959,6 +956,7 @@ def set_mesh( "Because the x coordinates of some topo and electrodes are the same," " we excluded electrodes with the same coordinates.", RuntimeWarning, + stacklevel=2, ) locs_tmp = np.vstack((topo, self.electrode_locations[~mask, :])) row_idx = np.lexsort((locs_tmp[:, 0],)) @@ -973,6 +971,7 @@ def set_mesh( "Because the x and y coordinates of some topo and electrodes are the same," " we excluded electrodes with the same coordinates.", RuntimeWarning, + stacklevel=2, ) locs_tmp = np.vstack((topo, self.electrode_locations[~mask, :])) row_idx = np.lexsort((locs_tmp[:, 1], locs_tmp[:, 0])) @@ -1253,9 +1252,8 @@ def read_ubc_dc2d_obs_file(self, filename, input_type="simple", toponame=None): topo = tmp_topo[1:, :] if topo.shape[0] != n_topo: print( - ">> # of points for the topography is not {0}, but {0}".format( - n_topo, topo.shape[0] - ) + ">> # of points for the topography is " + f"not {n_topo}, but {topo.shape[0]}" ) tmp = np.loadtxt(filename, comments="!").astype(float) e = np.zeros(tmp.shape[0], dtype=float) diff --git a/SimPEG/electromagnetics/static/resistivity/__init__.py b/simpeg/electromagnetics/static/resistivity/__init__.py similarity index 93% rename from SimPEG/electromagnetics/static/resistivity/__init__.py rename to simpeg/electromagnetics/static/resistivity/__init__.py index 4e0409892b..990c6367a4 100644 --- a/SimPEG/electromagnetics/static/resistivity/__init__.py +++ b/simpeg/electromagnetics/static/resistivity/__init__.py @@ -1,8 +1,8 @@ """ ============================================================================================ -DC Resistivity (:mod:`SimPEG.electromagnetics.static.resistivity`) +DC Resistivity (:mod:`simpeg.electromagnetics.static.resistivity`) ============================================================================================ -.. currentmodule:: SimPEG.electromagnetics.static.resistivity +.. currentmodule:: simpeg.electromagnetics.static.resistivity Simulations @@ -71,6 +71,7 @@ sources.BaseSrc receivers.BaseRx """ + from .simulation import Simulation3DCellCentered, Simulation3DNodal from .simulation_2d import Simulation2DCellCentered, Simulation2DNodal from .simulation_1d import Simulation1DLayers diff --git a/SimPEG/electromagnetics/static/resistivity/fields.py b/simpeg/electromagnetics/static/resistivity/fields.py similarity index 100% rename from SimPEG/electromagnetics/static/resistivity/fields.py rename to simpeg/electromagnetics/static/resistivity/fields.py diff --git a/SimPEG/electromagnetics/static/resistivity/fields_2d.py b/simpeg/electromagnetics/static/resistivity/fields_2d.py similarity index 100% rename from SimPEG/electromagnetics/static/resistivity/fields_2d.py rename to simpeg/electromagnetics/static/resistivity/fields_2d.py diff --git a/SimPEG/electromagnetics/static/resistivity/receivers.py b/simpeg/electromagnetics/static/resistivity/receivers.py similarity index 98% rename from SimPEG/electromagnetics/static/resistivity/receivers.py rename to simpeg/electromagnetics/static/resistivity/receivers.py index e41a080602..53c2614bb7 100644 --- a/SimPEG/electromagnetics/static/resistivity/receivers.py +++ b/simpeg/electromagnetics/static/resistivity/receivers.py @@ -158,11 +158,11 @@ def eval(self, src, mesh, f): # noqa: A003 Parameters ---------- - src : SimPEG.electromagnetics.static.resistivity.sources.BaseSrc + src : simpeg.electromagnetics.static.resistivity.sources.BaseSrc A DC/IP source mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved - f : SimPEG.electromagnetic.static.fields.FieldsDC + f : simpeg.electromagnetic.static.fields.FieldsDC The solution for the fields defined on the mesh Returns @@ -198,11 +198,11 @@ def evalDeriv(self, src, mesh, f, v=None, adjoint=False): Parameters ---------- - src : SimPEG.electromagnetics.static.resistivity.sources.BaseSrc + src : simpeg.electromagnetics.static.resistivity.sources.BaseSrc A frequency-domain EM source mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved - f : SimPEG.electromagnetic.static.resistivity.fields.FieldsDC + f : simpeg.electromagnetic.static.resistivity.fields.FieldsDC The solution for the fields defined on the mesh du_dm_v : numpy.ndarray, default = ``None`` The derivative of the fields on the mesh with respect to the model, @@ -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/run.py b/simpeg/electromagnetics/static/resistivity/run.py similarity index 92% rename from SimPEG/electromagnetics/static/resistivity/run.py rename to simpeg/electromagnetics/static/resistivity/run.py index a9a04dc3e0..1c18666f29 100644 --- a/SimPEG/electromagnetics/static/resistivity/run.py +++ b/simpeg/electromagnetics/static/resistivity/run.py @@ -1,6 +1,6 @@ import numpy as np -from SimPEG import ( +from simpeg import ( maps, optimization, inversion, @@ -37,14 +37,14 @@ def run_inversion( regmap = maps.IdentityMap(nP=int(actind.sum())) # Related to inversion if use_sensitivity_weight: - reg = regularization.Sparse(mesh, indActive=actind, mapping=regmap) + reg = regularization.Sparse(mesh, active_cells=actind, mapping=regmap) reg.alpha_s = alpha_s reg.alpha_x = alpha_x reg.alpha_y = alpha_y reg.alpha_z = alpha_z else: reg = regularization.WeightedLeastSquares( - mesh, indActive=actind, mapping=regmap + mesh, active_cells=actind, mapping=regmap ) reg.alpha_s = alpha_s reg.alpha_x = alpha_x diff --git a/SimPEG/electromagnetics/static/resistivity/simulation.py b/simpeg/electromagnetics/static/resistivity/simulation.py similarity index 99% rename from SimPEG/electromagnetics/static/resistivity/simulation.py rename to simpeg/electromagnetics/static/resistivity/simulation.py index 209874dc07..4e65a8d07a 100644 --- a/SimPEG/electromagnetics/static/resistivity/simulation.py +++ b/simpeg/electromagnetics/static/resistivity/simulation.py @@ -49,7 +49,7 @@ def survey(self): Returns ------- - SimPEG.electromagnetics.static.resistivity.survey.Survey + simpeg.electromagnetics.static.resistivity.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey") diff --git a/SimPEG/electromagnetics/static/resistivity/simulation_1d.py b/simpeg/electromagnetics/static/resistivity/simulation_1d.py similarity index 99% rename from SimPEG/electromagnetics/static/resistivity/simulation_1d.py rename to simpeg/electromagnetics/static/resistivity/simulation_1d.py index 5482025666..f2528d9a6f 100644 --- a/SimPEG/electromagnetics/static/resistivity/simulation_1d.py +++ b/simpeg/electromagnetics/static/resistivity/simulation_1d.py @@ -165,7 +165,7 @@ def survey(self): Returns ------- - SimPEG.electromagnetics.static.resistivity.survey.Survey + simpeg.electromagnetics.static.resistivity.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey.") diff --git a/SimPEG/electromagnetics/static/resistivity/simulation_2d.py b/simpeg/electromagnetics/static/resistivity/simulation_2d.py similarity index 99% rename from SimPEG/electromagnetics/static/resistivity/simulation_2d.py rename to simpeg/electromagnetics/static/resistivity/simulation_2d.py index 768c4ee118..9a593913b0 100644 --- a/SimPEG/electromagnetics/static/resistivity/simulation_2d.py +++ b/simpeg/electromagnetics/static/resistivity/simulation_2d.py @@ -28,7 +28,7 @@ class BaseDCSimulation2D(BaseElectricalPDESimulation): Base 2.5D DC problem """ - fieldsPair = Fields2D # SimPEG.EM.Static.Fields_2D + fieldsPair = Fields2D # simpeg.EM.Static.Fields_2D fieldsPair_fwd = FieldsDC # there's actually nT+1 fields, so we don't need to store the last one _mini_survey = None @@ -99,7 +99,8 @@ def g(k): if not out["success"]: warnings.warn( "Falling back to trapezoidal for integration. " - "You may need to change nky." + "You may need to change nky.", + stacklevel=2, ) do_trap = True if do_trap: @@ -133,7 +134,7 @@ def survey(self): Returns ------- - SimPEG.electromagnetics.static.resistivity.survey.Survey + simpeg.electromagnetics.static.resistivity.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey") diff --git a/SimPEG/electromagnetics/static/resistivity/sources.py b/simpeg/electromagnetics/static/resistivity/sources.py similarity index 68% rename from SimPEG/electromagnetics/static/resistivity/sources.py rename to simpeg/electromagnetics/static/resistivity/sources.py index cb2ece9736..ee46681d3b 100644 --- a/SimPEG/electromagnetics/static/resistivity/sources.py +++ b/simpeg/electromagnetics/static/resistivity/sources.py @@ -8,7 +8,7 @@ class BaseSrc(survey.BaseSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.static.resistivity.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.static.resistivity.receivers.BaseRx A list of DC/IP receivers location : (n_source, dim) numpy.ndarray Source locations @@ -66,7 +66,7 @@ def eval(self, sim): # noqa: A003 Parameters ---------- - sim : SimPEG.base.BaseElectricalPDESimulation + sim : simpeg.base.BaseElectricalPDESimulation The static electromagnetic simulation Returns @@ -94,7 +94,7 @@ def evalDeriv(self, sim): Parameters ---------- - sim : SimPEG.base.BaseElectricalPDESimulation + sim : simpeg.base.BaseElectricalPDESimulation The static electromagnetic simulation Returns @@ -137,15 +137,20 @@ class Dipole(BaseSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.static.resistivity.receivers.BaseRx + 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): @@ -224,7 +234,7 @@ class Pole(BaseSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.static.resistivity.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.static.resistivity.receivers.BaseRx A list of DC/IP receivers location : (n_source, dim) numpy.ndarray Electrode locations diff --git a/SimPEG/electromagnetics/static/resistivity/survey.py b/simpeg/electromagnetics/static/resistivity/survey.py similarity index 96% rename from SimPEG/electromagnetics/static/resistivity/survey.py rename to simpeg/electromagnetics/static/resistivity/survey.py index ecbbe58c37..8f2e438664 100644 --- a/SimPEG/electromagnetics/static/resistivity/survey.py +++ b/simpeg/electromagnetics/static/resistivity/survey.py @@ -1,13 +1,13 @@ import numpy as np -from ....utils.code_utils import deprecate_property, validate_string +from ....utils.code_utils import validate_string from ....survey import BaseSurvey from ..utils import drapeTopotoLoc from . import receivers as Rx from . import sources as Src from ..utils import static_utils -from SimPEG import data +from simpeg import data class Survey(BaseSurvey): @@ -15,7 +15,7 @@ class Survey(BaseSurvey): Parameters ---------- - source_list : list of SimPEG.electromagnetic.static.resistivity.sources.BaseSrc + source_list : list of simpeg.electromagnetic.static.resistivity.sources.BaseSrc List of SimPEG DC/IP sources survey_geometry : {"surface", "borehole", "general"} Survey geometry. @@ -152,14 +152,6 @@ def unique_electrode_locations(self): loc_n = self.locations_n return np.unique(np.vstack((loc_a, loc_b, loc_m, loc_n)), axis=0) - electrode_locations = deprecate_property( - unique_electrode_locations, - "electrode_locations", - new_name="unique_electrode_locations", - removal_version="0.17.0", - error=True, - ) - @property def source_locations(self): """ diff --git a/SimPEG/electromagnetics/static/resistivity/utils.py b/simpeg/electromagnetics/static/resistivity/utils.py similarity index 93% rename from SimPEG/electromagnetics/static/resistivity/utils.py rename to simpeg/electromagnetics/static/resistivity/utils.py index e1974f5943..3bdec534dd 100644 --- a/SimPEG/electromagnetics/static/resistivity/utils.py +++ b/simpeg/electromagnetics/static/resistivity/utils.py @@ -1,10 +1,13 @@ import numpy as np +import matplotlib.pyplot as plt from . import receivers from . import sources from .survey import Survey -from ..utils import * +# Import geometric_factor to make it available through +# simpeg.resistivity.utils.geometric_factor (to ensure backward compatibility) +from ..utils import geometric_factor # noqa: F401 def WennerSrcList(n_electrodes, a_spacing, in2D=False, plotIt=False): @@ -24,7 +27,7 @@ def WennerSrcList(n_electrodes, a_spacing, in2D=False, plotIt=False): Returns ------- - list of SimPEG.electromagnetics.static.resistivity.sources.Dipole + list of simpeg.electromagnetics.static.resistivity.sources.Dipole List of sources and their associate receivers for the Wenner survey """ diff --git a/SimPEG/electromagnetics/static/spontaneous_potential/__init__.py b/simpeg/electromagnetics/static/self_potential/__init__.py similarity index 68% rename from SimPEG/electromagnetics/static/spontaneous_potential/__init__.py rename to simpeg/electromagnetics/static/self_potential/__init__.py index 686c1c25f6..86b22da6fb 100644 --- a/SimPEG/electromagnetics/static/spontaneous_potential/__init__.py +++ b/simpeg/electromagnetics/static/self_potential/__init__.py @@ -1,8 +1,8 @@ """ ============================================================================================ -Spontaneous Potential (:mod:`SimPEG.electromagnetics.static.spontaneous_potential`) +Self Potential (:mod:`simpeg.electromagnetics.static.self_potential`) ============================================================================================ -.. currentmodule:: SimPEG.electromagnetics.static.spontaneous_potential +.. currentmodule:: simpeg.electromagnetics.static.self_potential Simulations @@ -14,14 +14,14 @@ Receivers ========= -This module makes use of the receivers in :mod:`SimPEG.electromagnetics.static.resistivity` +This module makes use of the receivers in :mod:`simpeg.electromagnetics.static.resistivity` Sources ======= .. autosummary:: :toctree: generated/ - sources.StreamingPotential + sources.StreamingCurrents Surveys ======= @@ -32,7 +32,7 @@ Maps ==== -The spontaneous potential simulation provides two specialized maps to extend to inversions +The self potential simulation provides two specialized maps to extend to inversions with different types of model sources. .. autosummary:: diff --git a/SimPEG/electromagnetics/static/spontaneous_potential/simulation.py b/simpeg/electromagnetics/static/self_potential/simulation.py similarity index 97% rename from SimPEG/electromagnetics/static/spontaneous_potential/simulation.py rename to simpeg/electromagnetics/static/self_potential/simulation.py index 72f66e5d29..402fcbb791 100644 --- a/SimPEG/electromagnetics/static/spontaneous_potential/simulation.py +++ b/simpeg/electromagnetics/static/self_potential/simulation.py @@ -8,18 +8,18 @@ class Simulation3DCellCentered(dc.Simulation3DCellCentered): - r"""A Spontaneous potential simulation. + r"""A self potential simulation. Parameters ---------- mesh : discretize.base.BaseMesh - survey : spontaneous_potential.Survey + survey : simpeg.electromagnetics.static.self_potential.Survey sigma, rho : float or array_like The conductivity/resistivity model of the subsurface. q : float, array_like, optional The charge density accumulation rate model (C/(s m^3)), also physically represents the volumetric current density (A/m^3). - qMap : SimPEG.maps.IdentityMap, optional + qMap : simpeg.maps.IdentityMap, optional The mapping used to go from the simulation model to `q`. Set this to invert for `q`. **kwargs @@ -27,7 +27,7 @@ class Simulation3DCellCentered(dc.Simulation3DCellCentered): Notes ----- - The charge density accumulation rate, :math:`q`, is related to the spontaneous + The charge density accumulation rate, :math:`q`, is related to the self electric potential, :math:`\phi`, with the same PDE, that relates current sources to potential in the resistivity case. diff --git a/SimPEG/electromagnetics/static/spontaneous_potential/sources.py b/simpeg/electromagnetics/static/self_potential/sources.py similarity index 85% rename from SimPEG/electromagnetics/static/spontaneous_potential/sources.py rename to simpeg/electromagnetics/static/self_potential/sources.py index 33a7d8c347..22a71d8560 100644 --- a/SimPEG/electromagnetics/static/spontaneous_potential/sources.py +++ b/simpeg/electromagnetics/static/self_potential/sources.py @@ -1,7 +1,7 @@ import numpy as np -from SimPEG.electromagnetics.static.resistivity import receivers -from SimPEG import survey -from SimPEG.utils import validate_list_of_types +from simpeg.electromagnetics.static.resistivity import receivers +from simpeg import survey +from simpeg.utils import validate_list_of_types class StreamingCurrents(survey.BaseSrc): diff --git a/SimPEG/electromagnetics/static/spectral_induced_polarization/__init__.py b/simpeg/electromagnetics/static/spectral_induced_polarization/__init__.py similarity index 91% rename from SimPEG/electromagnetics/static/spectral_induced_polarization/__init__.py rename to simpeg/electromagnetics/static/spectral_induced_polarization/__init__.py index bf5c81630b..9eefe9606d 100644 --- a/SimPEG/electromagnetics/static/spectral_induced_polarization/__init__.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/__init__.py @@ -1,8 +1,8 @@ """ ==================================================================================================== -Spectral Induced Polarization (:mod:`SimPEG.electromagnetics.static.induced_polarization`) +Spectral Induced Polarization (:mod:`simpeg.electromagnetics.static.induced_polarization`) ==================================================================================================== -.. currentmodule:: SimPEG.electromagnetics.static.spectral_induced_polarization +.. currentmodule:: simpeg.electromagnetics.static.spectral_induced_polarization Simulations @@ -60,6 +60,7 @@ simulation_2d.BaseSIPSimulation2D """ + from ....data import Data from .simulation import Simulation3DCellCentered, Simulation3DNodal from .simulation_2d import Simulation2DCellCentered, Simulation2DNodal diff --git a/SimPEG/electromagnetics/static/spectral_induced_polarization/data.py b/simpeg/electromagnetics/static/spectral_induced_polarization/data.py similarity index 100% rename from SimPEG/electromagnetics/static/spectral_induced_polarization/data.py rename to simpeg/electromagnetics/static/spectral_induced_polarization/data.py diff --git a/SimPEG/electromagnetics/static/spectral_induced_polarization/receivers.py b/simpeg/electromagnetics/static/spectral_induced_polarization/receivers.py similarity index 98% rename from SimPEG/electromagnetics/static/spectral_induced_polarization/receivers.py rename to simpeg/electromagnetics/static/spectral_induced_polarization/receivers.py index 30725aeec9..3fc73b0f86 100644 --- a/SimPEG/electromagnetics/static/spectral_induced_polarization/receivers.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/receivers.py @@ -144,11 +144,11 @@ def eval(self, src, mesh, f): # noqa: A003 Parameters ---------- - src : SimPEG.electromagnetics.static.spectral_induced_polarization.sources.BaseRx + src : simpeg.electromagnetics.static.spectral_induced_polarization.sources.BaseRx A spectral IP receiver mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved - f : SimPEG.electromagnetic.static.spectral_induced_polarization.Fields + f : simpeg.electromagnetic.static.spectral_induced_polarization.Fields The solution for the fields defined on the mesh Returns @@ -172,11 +172,11 @@ def evalDeriv(self, src, mesh, f, v, adjoint=False): Parameters ---------- - src : SimPEG.electromagnetics.static.spectral_induced_polarization.sources.BaseRx + src : simpeg.electromagnetics.static.spectral_induced_polarization.sources.BaseRx A spectral IP receiver mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved - f : SimPEG.electromagnetic.static.spectral_induced_polarization.Fields + f : simpeg.electromagnetic.static.spectral_induced_polarization.Fields The solution for the fields defined on the mesh v : numpy.ndarray A vector diff --git a/SimPEG/electromagnetics/static/spectral_induced_polarization/run.py b/simpeg/electromagnetics/static/spectral_induced_polarization/run.py similarity index 92% rename from SimPEG/electromagnetics/static/spectral_induced_polarization/run.py rename to simpeg/electromagnetics/static/spectral_induced_polarization/run.py index 18b766b3e1..93ee3cf5ed 100644 --- a/SimPEG/electromagnetics/static/spectral_induced_polarization/run.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/run.py @@ -1,5 +1,5 @@ import numpy as np -from SimPEG import ( +from simpeg import ( maps, optimization, inversion, @@ -142,9 +142,15 @@ def run_inversion( m_lower = np.r_[eta_lower, tau_lower, c_lower] # Set up regularization - reg_eta = regularization.Simple(mesh, mapping=wires.eta, indActive=actind) - reg_tau = regularization.Simple(mesh, mapping=wires.tau, indActive=actind) - reg_c = regularization.Simple(mesh, mapping=wires.c, indActive=actind) + reg_eta = regularization.WeightedLeastSquares( + mesh, mapping=wires.eta, active_cells=actind + ) + reg_tau = regularization.WeightedLeastSquares( + mesh, mapping=wires.tau, active_cells=actind + ) + reg_c = regularization.WeightedLeastSquares( + mesh, mapping=wires.c, active_cells=actind + ) # Todo: diff --git a/SimPEG/electromagnetics/static/spectral_induced_polarization/simulation.py b/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py similarity index 99% rename from SimPEG/electromagnetics/static/spectral_induced_polarization/simulation.py rename to simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py index 79627480a1..87e5638875 100644 --- a/SimPEG/electromagnetics/static/spectral_induced_polarization/simulation.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/simulation.py @@ -64,7 +64,7 @@ def survey(self): Returns ------- - SimPEG.electromagnetics.static.spectral_induced_polarization.survey.Survey + simpeg.electromagnetics.static.spectral_induced_polarization.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey") diff --git a/SimPEG/electromagnetics/static/spectral_induced_polarization/simulation_2d.py b/simpeg/electromagnetics/static/spectral_induced_polarization/simulation_2d.py similarity index 100% rename from SimPEG/electromagnetics/static/spectral_induced_polarization/simulation_2d.py rename to simpeg/electromagnetics/static/spectral_induced_polarization/simulation_2d.py diff --git a/SimPEG/electromagnetics/static/spectral_induced_polarization/sources.py b/simpeg/electromagnetics/static/spectral_induced_polarization/sources.py similarity index 95% rename from SimPEG/electromagnetics/static/spectral_induced_polarization/sources.py rename to simpeg/electromagnetics/static/spectral_induced_polarization/sources.py index 1bc1b71e34..2bb93a56bf 100644 --- a/SimPEG/electromagnetics/static/spectral_induced_polarization/sources.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/sources.py @@ -10,7 +10,7 @@ class BaseSrc(survey.BaseSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.static.resistivity.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.static.resistivity.receivers.BaseRx A list of DC/IP receivers location : (dim) numpy.ndarray Source location @@ -30,7 +30,7 @@ def receiver_list(self): Returns ------- - list of SimPEG.electromagnetics.static.spectral_induced_polarization.receivers.BaseRx + list of simpeg.electromagnetics.static.spectral_induced_polarization.receivers.BaseRx List of receivers associated with the source """ return self._receiver_list @@ -95,7 +95,7 @@ class Dipole(BaseSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.static.spectral_induced_polarization.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.static.spectral_induced_polarization.receivers.BaseRx A list of spectral IP receivers location_a : (dim) numpy.ndarray A electrode location; remember to set 'location_b' keyword argument to define N electrode locations. @@ -210,7 +210,7 @@ def eval(self, simulation): # noqa: A003 Parameters ---------- - sim : SimPEG.electromagnetics.static.spectral_induced_polarization.simulation.BaseDCSimulation + sim : simpeg.electromagnetics.static.spectral_induced_polarization.simulation.BaseDCSimulation A spectral IP simulation Returns @@ -238,7 +238,7 @@ class Pole(BaseSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.static.spectral_induced_polarization.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.static.spectral_induced_polarization.receivers.BaseRx list of spectral IP receivers location : (dim) array_like Electrode location @@ -254,7 +254,7 @@ def eval(self, simulation): # noqa: A003 Parameters ---------- - sim : SimPEG.electromagnetics.static.resistivity.simulation.BaseDCSimulation + sim : simpeg.electromagnetics.static.resistivity.simulation.BaseDCSimulation A DC/IP simulation Returns diff --git a/SimPEG/electromagnetics/static/spectral_induced_polarization/survey.py b/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py similarity index 95% rename from SimPEG/electromagnetics/static/spectral_induced_polarization/survey.py rename to simpeg/electromagnetics/static/spectral_induced_polarization/survey.py index 8d9a7932f3..05f5a0d460 100644 --- a/SimPEG/electromagnetics/static/spectral_induced_polarization/survey.py +++ b/simpeg/electromagnetics/static/spectral_induced_polarization/survey.py @@ -10,7 +10,7 @@ class Survey(BaseTimeSurvey): Parameters ---------- - source_list : list of SimPEG.electromagnetic.static.spectral_induced_polarization.sources.BaseSrc + source_list : list of simpeg.electromagnetic.static.spectral_induced_polarization.sources.BaseSrc List of SimPEG spectral IP sources survey_geometry : {"surface", "borehole", "general"} Survey geometry. @@ -109,14 +109,14 @@ def from_dc_to_sip_survey(survey_dc, times): Parameters ---------- - dc_survey : SimPEG.electromagnetics.static.resistivity.survey.Survey + dc_survey : simpeg.electromagnetics.static.resistivity.survey.Survey DC survey object times : numpy.ndarray Time channels Returns ------- - SimPEG.electromagnetics.static.spectral_induced_polarization.survey.Survey + simpeg.electromagnetics.static.spectral_induced_polarization.survey.Survey An SIP survey object """ source_list = survey_dc.source_list diff --git a/simpeg/electromagnetics/static/spontaneous_potential/__init__.py b/simpeg/electromagnetics/static/spontaneous_potential/__init__.py new file mode 100644 index 0000000000..86323379dd --- /dev/null +++ b/simpeg/electromagnetics/static/spontaneous_potential/__init__.py @@ -0,0 +1,69 @@ +""" +============================================================================================ +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, +) + +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/__init__.py b/simpeg/electromagnetics/static/utils/__init__.py similarity index 79% rename from SimPEG/electromagnetics/static/utils/__init__.py rename to simpeg/electromagnetics/static/utils/__init__.py index 1e5ac6adbb..e6ce7096bd 100644 --- a/SimPEG/electromagnetics/static/utils/__init__.py +++ b/simpeg/electromagnetics/static/utils/__init__.py @@ -1,8 +1,8 @@ """ ==================================================================================================== -Static Utilities (:mod:`SimPEG.electromagnetics.utils`) +Static Utilities (:mod:`simpeg.electromagnetics.utils`) ==================================================================================================== -.. currentmodule:: SimPEG.electromagnetics.static.utils +.. currentmodule:: simpeg.electromagnetics.static.utils Electrode Utilities @@ -46,36 +46,25 @@ closestPointsGrid """ + from .static_utils import ( electrode_separations, - source_receiver_midpoints, pseudo_locations, geometric_factor, apparent_resistivity_from_voltage, - apparent_resistivity, plot_pseudosection, generate_dcip_survey, - generate_dcip_survey_line, - gen_DCIPsurvey, generate_dcip_sources_line, generate_survey_from_abmn_locations, - writeUBC_DCobs, - writeUBC_DClocs, convert_survey_3d_to_2d_lines, - convertObs_DC3D_to_2D, - readUBC_DC2Dpre, - readUBC_DC3Dobs, xy_2_lineID, r_unit, - getSrc_locs, gettopoCC, drapeTopotoLoc, genTopography, closestPointsGrid, gen_3d_survey_from_2d_lines, plot_1d_layer_model, - plot_layer, - plot_pseudoSection, ) # Import if user has plotly diff --git a/SimPEG/electromagnetics/static/utils/static_utils.py b/simpeg/electromagnetics/static/utils/static_utils.py similarity index 90% rename from SimPEG/electromagnetics/static/utils/static_utils.py rename to simpeg/electromagnetics/static/utils/static_utils.py index a4d746e041..a5fcaa7e52 100644 --- a/SimPEG/electromagnetics/static/utils/static_utils.py +++ b/simpeg/electromagnetics/static/utils/static_utils.py @@ -10,7 +10,6 @@ from .. import resistivity as dc from ....utils import ( mkvc, - surface2ind_topo, model_builder, define_plane_from_points, ) @@ -23,7 +22,6 @@ from ....utils.plot_utils import plot_1d_layer_model # noqa: F401 -from ....utils.code_utils import deprecate_method try: import plotly.graph_objects as grapho @@ -77,7 +75,7 @@ def electrode_separations(survey_object, electrode_pair="all", **kwargs): Parameters ---------- - survey_object : SimPEG.electromagnetics.static.survey.Survey + survey_object : simpeg.electromagnetics.static.survey.Survey A DC or IP survey object electrode_pair : {'all', 'AB', 'MN', 'AM', 'AN', 'BM', 'BN} Which electrode separation pairs to compute. @@ -180,7 +178,7 @@ def pseudo_locations(survey, wenner_tolerance=0.1, **kwargs): Parameters ---------- - survey : SimPEG.electromagnetics.static.resistivity.Survey + survey : simpeg.electromagnetics.static.resistivity.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, @@ -204,6 +202,7 @@ def pseudo_locations(survey, wenner_tolerance=0.1, **kwargs): "The keyword arguments of this function have been deprecated." " All of the necessary information is now in the DC survey class", DeprecationWarning, + stacklevel=2, ) # Pre-allocate @@ -270,7 +269,7 @@ def geometric_factor(survey_object, space_type="half space", **kwargs): Parameters ---------- - survey_object : SimPEG.electromagnetics.static.resistivity.Survey + survey_object : simpeg.electromagnetics.static.resistivity.Survey A DC (or IP) survey object space_type : {'half space', 'whole space'} Compute geometric factor for a halfspace or wholespace. @@ -313,7 +312,7 @@ def apparent_resistivity_from_voltage( Parameters ---------- - survey : SimPEG.electromagnetics.static.resistivity.Survey + survey : simpeg.electromagnetics.static.resistivity.Survey A DC survey volts : (nD) numpy.ndarray Normalized voltage measurements [V/A] @@ -353,24 +352,40 @@ def convert_survey_3d_to_2d_lines( Parameters ---------- - survey : SimPEG.electromagnetics.static.resistivity.Survey + survey : simpeg.electromagnetics.static.resistivity.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'} 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. Returns ------- - survey_list : list of SimPEG.electromagnetics.static.resistivity.Survey + 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) @@ -406,19 +421,19 @@ def convert_survey_3d_to_2d_lines( # Along line positions and elevation for electrodes on current line # in terms of position elevation a_locs_s = np.c_[ - np.dot(ab_locs_all[lineID_index, 0:2] - r0[0], uvec), + np.dot(ab_locs_all[lineID_index, 0:2] - r0, uvec), ab_locs_all[lineID_index, 2], ] b_locs_s = np.c_[ - np.dot(ab_locs_all[lineID_index, 3:5] - r0[0], uvec), + np.dot(ab_locs_all[lineID_index, 3:5] - r0, uvec), ab_locs_all[lineID_index, -1], ] m_locs_s = np.c_[ - np.dot(mn_locs_all[lineID_index, 0:2] - r0[0], uvec), + np.dot(mn_locs_all[lineID_index, 0:2] - r0, uvec), mn_locs_all[lineID_index, 2], ] n_locs_s = np.c_[ - np.dot(mn_locs_all[lineID_index, 3:5] - r0[0], uvec), + np.dot(mn_locs_all[lineID_index, 3:5] - r0, uvec), mn_locs_all[lineID_index, -1], ] @@ -505,7 +520,7 @@ def plot_pseudosection( Parameters ---------- - data : SimPEG.electromagnetics.static.survey.Survey or SimPEG.data.Data + data : simpeg.electromagnetics.static.survey.Survey or simpeg.data.Data A DC or IP survey object defining a 2D survey line, or a Data object containing that same type of survey object. dobs : numpy.ndarray (ndata,) or None @@ -543,7 +558,7 @@ def plot_pseudosection( 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`. + when it is a simpeg.data.Data object from voltage to the requested `data_type`. This occurs when `dobs` is `None`. You may also use "apparent_conductivity" or "apparent_resistivity" to define the data type. space_type : {'half space', "whole space"} @@ -562,7 +577,9 @@ def plot_pseudosection( if kwarg in kwargs: raise TypeError(r"The {kwarg} keyword has been removed.") if len(kwargs) > 0: - warnings.warn("plot_pseudosection unused kwargs: {list(kwargs.keys())}") + warnings.warn( + f"plot_pseudosection unused kwargs: {list(kwargs.keys())}", stacklevel=2 + ) if plot_type.lower() not in ["pcolor", "contourf", "scatter"]: raise ValueError( @@ -765,7 +782,7 @@ def plot_3d_pseudosection( Parameters ---------- - survey : SimPEG.electromagnetics.static.survey.Survey + survey : simpeg.electromagnetics.static.survey.Survey A DC or IP survey object dvec : numpy.ndarray A data vector containing volts, integrated chargeabilities, apparent @@ -852,7 +869,7 @@ def plot_3d_pseudosection( marker = {key: marker_opts.get(key, marker[key]) for key in marker} # 3D scatter plot - if plane_points == None: + if plane_points is None: marker["color"] = plot_vec scatter_data = [ grapho.Scatter3d( @@ -961,7 +978,7 @@ def generate_survey_from_abmn_locations( output_sorting : bool This option is used if the ABMN locations are sorted during the creation of the survey and you would like to sort any data vectors associated with the electrode locations. - If False, the function will output a SimPEG.electromagnetic.static.survey.Survey object. + If False, the function will output a simpeg.electromagnetic.static.survey.Survey object. 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. @@ -969,7 +986,7 @@ def generate_survey_from_abmn_locations( 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 @@ -1061,7 +1078,8 @@ def generate_survey_from_abmn_locations( warnings.warn( "Ordering of ABMN locations changed when generating survey. " "Associated data vectors will need sorting. Set output_sorting to " - "True for sorting indices." + "True for sorting indices.", + stacklevel=2, ) if output_sorting: @@ -1094,7 +1112,7 @@ def generate_dcip_survey(endl, survey_type, a, b, n, dim=3, **kwargs): Returns ------- - SimPEG.electromagnetics.static.resistivity.Survey + simpeg.electromagnetics.static.resistivity.Survey A DC survey object """ @@ -1283,7 +1301,7 @@ def generate_dcip_sources_line( Returns ------- - SimPEG.electromagnetics.static.resistivity.Survey + simpeg.electromagnetics.static.resistivity.Survey A DC survey object """ @@ -1623,7 +1641,7 @@ def drapeTopotoLoc(mesh, pts, ind_active=None, option="top", topo=None, **kwargs raise ValueError("Unsupported mesh dimension") if ind_active is None: - ind_active = surface2ind_topo(mesh, topo) + ind_active = discretize.utils.active_from_xyz(mesh, topo) if mesh._meshType == "TENSOR": meshtemp, topoCC = gettopoCC(mesh, ind_active, option=option) @@ -1672,13 +1690,13 @@ def genTopography(mesh, zmin, zmax, seed=None, its=100, anisotropy=None): mesh2D = discretize.TensorMesh( [mesh.h[0], mesh.h[1]], x0=[mesh.x0[0], mesh.x0[1]] ) - out = model_builder.randomModel( + out = model_builder.create_random_model( mesh.vnC[:2], bounds=[zmin, zmax], its=its, seed=seed, anisotropy=anisotropy ) return out, mesh2D elif mesh.dim == 2: mesh1D = discretize.TensorMesh([mesh.h[0]], x0=[mesh.x0[0]]) - out = model_builder.randomModel( + out = model_builder.create_random_model( mesh.vnC[:1], bounds=[zmin, zmax], its=its, seed=seed, anisotropy=anisotropy ) return out, mesh1D @@ -1759,7 +1777,7 @@ def gen_3d_survey_from_2d_lines( Returns ------- - SimPEG.dc.SurveyDC.Survey + simpeg.dc.SurveyDC.Survey A 3D DC survey object """ ylocs = np.arange(n_lines) * line_spacing + y0 @@ -1812,170 +1830,3 @@ def gen_3d_survey_from_2d_lines( line_inds=line_inds, ) return IO_3d, survey_3d - - -############ -# Deprecated -############ - - -def plot_pseudoSection( - data, - ax=None, - survey_type="dipole-dipole", - data_type="appConductivity", - space_type="half-space", - clim=None, - scale="linear", - sameratio=True, - pcolor_opts=None, - data_location=False, - dobs=None, - dim=2, -): - raise TypeError( - "The plot_pseudoSection method has been removed. Please use " - "plot_pseudosection instead." - ) - - -def apparent_resistivity( - data_object, - survey_type=None, - space_type="half space", - dobs=None, - eps=1e-10, - **kwargs, -): - raise TypeError( - "The apparent_resistivity method has been removed. Please use " - "apparent_resistivity_from_voltage instead." - ) - - -source_receiver_midpoints = deprecate_method( - pseudo_locations, "source_receiver_midpoints", "0.17.0", error=True -) - - -def plot_layer(rho, mesh, **kwargs): - raise NotImplementedError( - "The plot_layer method has been deprecated. Please use " - "plot_1d_layer_model instead. This will be removed in version" - " 0.17.0 of SimPEG", - ) - - -def convertObs_DC3D_to_2D(survey, lineID, flag="local"): - raise TypeError( - "The convertObs_DC3D_to_2D method has been removed. Please use " - "convert_3d_survey_to_2d." - ) - - -def getSrc_locs(survey): - raise NotImplementedError( - "The getSrc_locs method has been deprecated. Source " - "locations are now computed as a method of the survey " - "class. Please use Survey.source_locations(). This method " - " will be removed in version 0.17.0 of SimPEG", - ) - - -def writeUBC_DCobs( - fileName, - data, - dim, - format_type, - survey_type="dipole-dipole", - ip_type=0, - comment_lines="", -): - # """ - # Write UBC GIF DCIP 2D or 3D observation file - - # Input: - # :param str fileName: including path where the file is written out - # :param SimPEG.Data data: DC data object - # :param int dim: either 2 | 3 - # :param str format_type: either 'surface' | 'general' | 'simple' - # :param str survey_type: 'dipole-dipole' | 'pole-dipole' | - # 'dipole-pole' | 'pole-pole' | 'gradient' - - # Output: - # :return: UBC2D-Data file - # :rtype: file - # """ - - raise NotImplementedError( - "The writeUBC_DCobs method has been deprecated. Please use " - "write_dcip2d_ubc or write_dcip3d_ubc instead. These are imported " - "from SimPEG.utils.io_utils. This function will be removed in version" - " 0.17.0 of SimPEG", - ) - - -def writeUBC_DClocs( - fileName, - dc_survey, - dim, - format_type, - survey_type="dipole-dipole", - ip_type=0, - comment_lines="", -): - # """ - # Write UBC GIF DCIP 2D or 3D locations file - - # Input: - # :param str fileName: including path where the file is written out - # :param SimPEG.electromagnetics.static.resistivity.Survey dc_survey: DC survey object - # :param int dim: either 2 | 3 - # :param str survey_type: either 'SURFACE' | 'GENERAL' - - # Output: - # :rtype: file - # :return: UBC 2/3D-locations file - # """ - - raise NotImplementedError( - "The writeUBC_DClocs method has been deprecated. Please use " - "write_dcip2d_ubc or write_dcip3d_ubc instead. These are imported " - "from SimPEG.utils.io_utils. This function will be removed in version" - " 0.17.0 of SimPEG", - FutureWarning, - ) - - -def readUBC_DC2Dpre(fileName): - raise NotImplementedError( - "The readUBC_DC2Dpre method has been deprecated. Please use " - "read_dcip2d_ubc instead. This is imported " - "from SimPEG.utils.io_utils. This function will be removed in version" - " 0.17.0 of SimPEG", - ) - - -def readUBC_DC3Dobs(fileName, data_type="volt"): - raise NotImplementedError( - "The readUBC_DC3Dobs method has been deprecated. Please use " - "read_dcip3d_ubc instead. This is imported " - "from SimPEG.utils.io_utils. This function will be removed in version" - " 0.17.0 of SimPEG", - ) - - -gen_DCIPsurvey = deprecate_method( - generate_dcip_survey, "gen_DCIPsurvey", removal_version="0.17.0", error=True -) - - -def generate_dcip_survey_line( - survey_type, data_type, endl, topo, ds, dh, n, dim_flag="2.5D", sources_only=False -): - raise NotImplementedError( - "The gen_dcip_survey_line method has been deprecated. Please use " - "generate_dcip_sources_line instead. This will be removed in version" - " 0.17.0 of SimPEG", - FutureWarning, - ) diff --git a/SimPEG/electromagnetics/time_domain/__init__.py b/simpeg/electromagnetics/time_domain/__init__.py similarity index 72% rename from SimPEG/electromagnetics/time_domain/__init__.py rename to simpeg/electromagnetics/time_domain/__init__.py index dcf8dde9a8..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`) +Time-Domain EM (:mod:`simpeg.electromagnetics.time_domain`) ============================================================================== -.. currentmodule:: SimPEG.electromagnetics.time_domain +.. currentmodule:: simpeg.electromagnetics.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: -About ``time_domain`` +* :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 =========== @@ -89,6 +107,7 @@ fields.FieldsDerivativesHJ """ + from .simulation import ( Simulation3DMagneticFluxDensity, Simulation3DElectricField, diff --git a/SimPEG/electromagnetics/time_domain/fields.py b/simpeg/electromagnetics/time_domain/fields.py similarity index 66% rename from SimPEG/electromagnetics/time_domain/fields.py rename to simpeg/electromagnetics/time_domain/fields.py index 7a0b45e3b0..8ad996ad79 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""" @@ -56,7 +73,7 @@ def _dbdtDeriv(self, tInd, src, dun_dm_v, v, adjoint=False): if adjoint is True: return ( self._dbdtDeriv_u(tInd, src, v, adjoint), - Zero() + Zero(), # self._dbdtDeriv_m(tInd, src, v, adjoint), ) return self._dbdtDeriv_u(tInd, src, dun_dm_v) + self._dbdtDeriv_m(tInd, src, v) @@ -87,49 +104,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 @@ -274,19 +347,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 @@ -393,20 +508,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 @@ -560,19 +718,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/receivers.py b/simpeg/electromagnetics/time_domain/receivers.py similarity index 91% rename from SimPEG/electromagnetics/time_domain/receivers.py rename to simpeg/electromagnetics/time_domain/receivers.py index d2a3a1f8cb..98e4a5053e 100644 --- a/SimPEG/electromagnetics/time_domain/receivers.py +++ b/simpeg/electromagnetics/time_domain/receivers.py @@ -3,7 +3,6 @@ from ...utils import mkvc, validate_type, validate_direction from discretize.utils import Zero from ...survey import BaseTimeRx -import warnings class BaseRx(BaseTimeRx): @@ -25,16 +24,10 @@ def __init__( times, orientation="z", use_source_receiver_offset=False, - **kwargs + **kwargs, ): - proj = kwargs.pop("projComp", None) - if proj is not None: - warnings.warn( - "'projComp' overrides the 'orientation' property which automatically" - " handles the projection from the mesh the receivers!!! " - "'projComp' is deprecated and will be removed in SimPEG 0.19.0." - ) - self.projComp = proj + if (key := "projComp") in kwargs.keys(): + raise TypeError(f"'{key}' property has been removed.") if locations is None: raise AttributeError("'locations' are required. Cannot be 'None'") @@ -92,7 +85,7 @@ def getSpatialP(self, mesh, f): ---------- mesh : discretize.BaseMesh A discretize mesh - f : SimPEG.electromagnetics.time_domain.fields.FieldsTDEM + f : simpeg.electromagnetics.time_domain.fields.FieldsTDEM Returns ------- @@ -120,7 +113,7 @@ def getTimeP(self, time_mesh, f): ---------- time_mesh : discretize.TensorMesh A 1D ``TensorMesh`` defining the time discretization - f : SimPEG.electromagnetics.time_domain.fields.FieldsTDEM + f : simpeg.electromagnetics.time_domain.fields.FieldsTDEM Returns ------- @@ -144,7 +137,7 @@ def getP(self, mesh, time_mesh, f): A discretize mesh defining spatial discretization time_mesh : discretize.TensorMesh A 1D ``TensorMesh`` defining the time discretization - f : SimPEG.electromagnetics.time_domain.fields.FieldsTDEM + f : simpeg.electromagnetics.time_domain.fields.FieldsTDEM Returns ------- @@ -172,13 +165,13 @@ def eval(self, src, mesh, time_mesh, f): # noqa: A003 Parameters ---------- - src : SimPEG.electromagnetics.frequency_domain.sources.BaseTDEMSrc + src : simpeg.electromagnetics.frequency_domain.sources.BaseTDEMSrc A time-domain EM source mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved time_mesh : discretize.TensorMesh A 1D ``TensorMesh`` defining the time discretization - f : SimPEG.electromagnetic.time_domain.fields.FieldsTDEM + f : simpeg.electromagnetic.time_domain.fields.FieldsTDEM The solution for the fields defined on the mesh Returns @@ -195,13 +188,13 @@ def evalDeriv(self, src, mesh, time_mesh, f, v, adjoint=False): Parameters ---------- - src : SimPEG.electromagnetics.frequency_domain.sources.BaseTDEMSrc + src : simpeg.electromagnetics.frequency_domain.sources.BaseTDEMSrc A time-domain EM source mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved time_mesh : discretize.TensorMesh A 1D ``TensorMesh`` defining the time discretization - f : SimPEG.electromagnetic.time_domain.fields.FieldsTDEM + f : simpeg.electromagnetic.time_domain.fields.FieldsTDEM The solution for the fields defined on the mesh v : numpy.ndarray A vector @@ -287,13 +280,13 @@ def eval(self, src, mesh, time_mesh, f): # noqa: A003 Parameters ---------- - src : SimPEG.electromagnetics.frequency_domain.sources.BaseTDEMSrc + src : simpeg.electromagnetics.frequency_domain.sources.BaseTDEMSrc A time-domain EM source mesh : discretize.base.BaseMesh The mesh on which the discrete set of equations is solved time_mesh : discretize.TensorMesh A 1D ``TensorMesh`` defining the time discretization - f : SimPEG.electromagnetic.time_domain.fields.FieldsTDEM + f : simpeg.electromagnetic.time_domain.fields.FieldsTDEM The solution for the fields defined on the mesh Returns @@ -320,7 +313,7 @@ def getTimeP(self, time_mesh, f): ---------- time_mesh : discretize.TensorMesh A 1D ``TensorMesh`` defining the time discretization - f : SimPEG.electromagnetics.time_domain.fields.FieldsTDEM + f : simpeg.electromagnetics.time_domain.fields.FieldsTDEM Returns ------- diff --git a/simpeg/electromagnetics/time_domain/simulation.py b/simpeg/electromagnetics/time_domain/simulation.py new file mode 100644 index 0000000000..31720a94c5 --- /dev/null +++ b/simpeg/electromagnetics/time_domain/simulation.py @@ -0,0 +1,2679 @@ +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 +from .survey import Survey +from .fields import ( + Fields3DMagneticFluxDensity, + Fields3DElectricField, + Fields3DMagneticField, + Fields3DCurrentDensity, + FieldsDerivativesEB, + FieldsDerivativesHJ, +) + + +class BaseTDEMSimulation(BaseTimeSimulation, BaseEMSimulation): + 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): + super().__init__(mesh=mesh, survey=survey, **kwargs) + self.dt_threshold = dt_threshold + if self.muMap is not None: + raise NotImplementedError( + "Time domain EM simulations do not support magnetic permeability " + "inversion, yet." + ) + + @property + def survey(self): + """The TDEM survey object. + + Returns + ------- + .time_domain.survey.Survey + The TDEM survey object. + """ + if self._survey is None: + raise AttributeError("Simulation must have a survey set") + return self._survey + + @survey.setter + def survey(self, value): + if value is not None: + value = validate_type("survey", value, Survey, cast=False) + self._survey = value + + @property + def dt_threshold(self): + """Threshold used when determining the unique time-step lengths. + + 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 + + @dt_threshold.setter + def dt_threshold(self, value): + self._dt_threshold = validate_float("dt_threshold", value, min_val=0.0) + + def fields(self, m): + """Compute and return the fields for the model provided. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model. + + Returns + ------- + .time_domain.fields.FieldsTDEM + The TDEM fields object. + """ + + self.model = m + + f = self.fieldsPair(self) + + # set initial fields + f[:, self._fieldType + "Solution", 0] = self.getInitialFields() + + if self.verbose: + print("{}\nCalculating fields(m)\n{}".format("*" * 50, "*" * 50)) + + # timestep to solve forward + Ainv = None + for tInd, dt in enumerate(self.time_steps): + # keep factors if dt is the same as previous step b/c A will be the + # same + if Ainv is not None and ( + tInd > 0 and abs(dt - self.time_steps[tInd - 1]) > self.dt_threshold + ): + Ainv.clean() + Ainv = None + + if Ainv is None: + A = self.getAdiag(tInd) + if self.verbose: + print("Factoring... (dt = {:e})".format(dt)) + Ainv = self.solver(A, **self.solver_opts) + if self.verbose: + print("Done") + + rhs = self.getRHS(tInd + 1) # this is on the nodes of the time mesh + Asubdiag = self.getAsubdiag(tInd) + + if self.verbose: + print(" Solving... (tInd = {:d})".format(tInd + 1)) + + # taking a step + sol = Ainv * (rhs - Asubdiag * f[:, (self._fieldType + "Solution"), tInd]) + + if self.verbose: + print(" Done...") + + if sol.ndim == 1: + sol.shape = (sol.size, 1) + f[:, self._fieldType + "Solution", tInd + 1] = sol + + if self.verbose: + print("{}\nDone calculating fields(m)\n{}".format("*" * 50, "*" * 50)) + + # clean factors and return + Ainv.clean() + return f + + def Jvec(self, m, v, f=None): + 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 : .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: + f = self.fields(m) + + ftype = self._fieldType + "Solution" # the thing we solved for + self.model = m + + # mat to store previous time-step's solution deriv times a vector for + # each source + # size: nu x n_sources + + # this is a bit silly + + # if self._fieldType == 'b' or self._fieldType == 'j': + # ifields = np.zeros((self.mesh.n_faces, len(Srcs))) + # elif self._fieldType == 'e' or self._fieldType == 'h': + # ifields = np.zeros((self.mesh.n_edges, len(Srcs))) + + # for i, src in enumerate(self.survey.source_list): + dun_dm_v = np.hstack( + [ + mkvc(self.getInitialFieldsDeriv(src, v, f=f), 2) + for src in self.survey.source_list + ] + ) + # can over-write this at each timestep + # store the field derivs we need to project to calc full deriv + df_dm_v = self.Fields_Derivs(self) + + Adiaginv = None + + for tInd, dt in zip(range(self.nT), self.time_steps): + # keep factors if dt is the same as previous step b/c A will be the + # same + if Adiaginv is not None and (tInd > 0 and dt != self.time_steps[tInd - 1]): + Adiaginv.clean() + Adiaginv = None + + if Adiaginv is None: + A = self.getAdiag(tInd) + Adiaginv = self.solver(A, **self.solver_opts) + + Asubdiag = self.getAsubdiag(tInd) + + for i, src in enumerate(self.survey.source_list): + # here, we are lagging by a timestep, so filling in as we go + for projField in set([rx.projField for rx in src.receiver_list]): + df_dmFun = getattr(f, "_%sDeriv" % projField, None) + # df_dm_v is dense, but we only need the times at + # (rx.P.T * ones > 0) + # This should be called rx.footprint + + df_dm_v[src, "{}Deriv".format(projField), tInd] = df_dmFun( + tInd, src, dun_dm_v[:, i], v + ) + + un_src = f[src, ftype, tInd + 1] + + # cell centered on time mesh + dA_dm_v = self.getAdiagDeriv(tInd, un_src, v) + # on nodes of time mesh + dRHS_dm_v = self.getRHSDeriv(tInd + 1, src, v) + + dAsubdiag_dm_v = self.getAsubdiagDeriv(tInd, f[src, ftype, tInd], v) + + JRHS = dRHS_dm_v - dAsubdiag_dm_v - dA_dm_v + + # step in time and overwrite + if tInd != len(self.time_steps + 1): + dun_dm_v[:, i] = Adiaginv * (JRHS - Asubdiag * dun_dm_v[:, i]) + + Jv = [] + for src in self.survey.source_list: + for rx in src.receiver_list: + Jv.append( + rx.evalDeriv( + src, + self.mesh, + self.time_mesh, + f, + mkvc(df_dm_v[src, "%sDeriv" % rx.projField, :]), + ) + ) + Adiaginv.clean() + # del df_dm_v, dun_dm_v, Asubdiag + # return mkvc(Jv) + return np.hstack(Jv) + + def Jtvec(self, m, v, f=None): + 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}} + + 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 : .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: + 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) + + df_duT_v = self.Fields_Derivs(self) + + # same size as fields at a single timestep + ATinv_df_duT_v = np.zeros( + ( + len(self.survey.source_list), + len(f[self.survey.source_list[0], ftype, 0]), + ), + dtype=float, + ) + JTv = np.zeros(m.shape, dtype=float) + + # Loop over sources and receivers to create a fields object: + # PT_v, df_duT_v, df_dmT_v + # initialize storage for PT_v (don't need to preserve over sources) + PT_v = self.Fields_Derivs(self) + for src in self.survey.source_list: + # Looping over initializing field class is appending memory! + # PT_v = Fields_Derivs(self.mesh) # initialize storage + # #for PT_v (don't need to preserve over sources) + # initialize size + df_duT_v[src, "{}Deriv".format(self._fieldType), :] = np.zeros_like( + f[src, self._fieldType, :] + ) + + for rx in src.receiver_list: + PT_v[src, "{}Deriv".format(rx.projField), :] = rx.evalDeriv( + src, self.mesh, self.time_mesh, f, mkvc(v[src, rx]), adjoint=True + ) # this is += + + # PT_v = np.reshape(curPT_v,(len(curPT_v)/self.time_mesh.nN, + # self.time_mesh.nN), order='F') + df_duTFun = getattr(f, "_{}Deriv".format(rx.projField), None) + + for tInd in range(self.nT + 1): + cur = df_duTFun( + tInd, + src, + None, + mkvc(PT_v[src, "{}Deriv".format(rx.projField), tInd]), + adjoint=True, + ) + + df_duT_v[src, "{}Deriv".format(self._fieldType), tInd] = df_duT_v[ + src, "{}Deriv".format(self._fieldType), tInd + ] + mkvc(cur[0], 2) + JTv = cur[1] + JTv + + del PT_v # no longer need this + + AdiagTinv = None + + # Do the back-solve through time + # if the previous timestep is the same: no need to refactor the matrix + # for tInd, dt in zip(range(self.nT), self.time_steps): + + for tInd in reversed(range(self.nT)): + # tInd = tIndP - 1 + if AdiagTinv is not None and ( + tInd <= self.nT and self.time_steps[tInd] != self.time_steps[tInd + 1] + ): + AdiagTinv.clean() + AdiagTinv = None + + # refactor if we need to + if AdiagTinv is None: # and tInd > -1: + Adiag = self.getAdiag(tInd) + AdiagTinv = self.solver(Adiag.T.tocsr(), **self.solver_opts) + + if tInd < self.nT - 1: + Asubdiag = self.getAsubdiag(tInd + 1) + + for isrc, src in enumerate(self.survey.source_list): + # solve against df_duT_v + if tInd >= self.nT - 1: + # last timestep (first to be solved) + ATinv_df_duT_v[isrc, :] = ( + AdiagTinv + * df_duT_v[src, "{}Deriv".format(self._fieldType), tInd + 1] + ) + 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, :]) + ) + + dAsubdiagT_dm_v = self.getAsubdiagDeriv( + tInd, f[src, ftype, tInd], ATinv_df_duT_v[isrc, :], adjoint=True + ) + + dRHST_dm_v = self.getRHSDeriv( + tInd + 1, src, ATinv_df_duT_v[isrc, :], adjoint=True + ) # on nodes of time mesh + + un_src = f[src, ftype, tInd + 1] + # cell centered on time mesh + dAT_dm_v = self.getAdiagDeriv( + tInd, un_src, ATinv_df_duT_v[isrc, :], adjoint=True + ) + + JTv = JTv + mkvc(-dAT_dm_v - dAsubdiagT_dm_v + dRHST_dm_v) + + # Treat the initial condition + + # del df_duT_v, ATinv_df_duT_v, A, Asubdiag + if AdiagTinv is not None: + AdiagTinv.clean() + + return mkvc(JTv).astype(float) + + def getSourceTerm(self, tInd): + 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 + + if self._formulation == "EB": + s_m = np.zeros((self.mesh.n_faces, len(Srcs))) + s_e = np.zeros((self.mesh.n_edges, len(Srcs))) + elif self._formulation == "HJ": + s_m = np.zeros((self.mesh.n_edges, len(Srcs))) + s_e = np.zeros((self.mesh.n_faces, len(Srcs))) + + for i, src in enumerate(Srcs): + smi, sei = src.eval(self, self.times[tInd]) + s_m[:, i] = s_m[:, i] + smi + s_e[:, i] = s_e[:, i] + sei + + return s_m, s_e + + def getInitialFields(self): + """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 + + if self._fieldType in ["b", "j"]: + ifields = np.zeros((self.mesh.n_faces, len(Srcs))) + elif self._fieldType in ["e", "h"]: + ifields = np.zeros((self.mesh.n_edges, len(Srcs))) + + if self.verbose: + print("Calculating Initial fields") + + for i, src in enumerate(Srcs): + ifields[:, i] = ifields[:, i] + getattr( + src, "{}Initial".format(self._fieldType), None + )(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 + ) + ) + + # take care of any utils.zero cases + if adjoint is False: + if self._fieldType in ["b", "j"]: + ifieldsDeriv += np.zeros(self.mesh.n_faces) + elif self._fieldType in ["e", "h"]: + ifieldsDeriv += np.zeros(self.mesh.n_edges) + + elif adjoint is True: + if self._fieldType in ["b", "j"]: + ifieldsDeriv += np.zeros(self.mesh.n_faces) + elif self._fieldType in ["e", "h"]: + ifieldsDeriv[0] += np.zeros(self.mesh.n_edges) + ifieldsDeriv[1] += np.zeros_like(self.model) # take care of a Zero() case + + return ifieldsDeriv + + # Store matrix factors if we need to solve the DC problem to get the + # 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 " + "{}-formulation".format(self._fieldType) + ) + if getattr(self, "_Adcinv", None) is None: + if self.verbose: + print("Factoring the system matrix for the DC problem") + Adc = self.getAdc() + self._Adcinv = self.solver(Adc) + return self._Adcinv + + @property + def clean_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().clean_on_model_update + return items + ["_Adcinv"] #: clear DC matrix factors on any model updates + + +############################################################################### +# # +# E-B Formulation # +# # +############################################################################### + +# ------------------------------- Simulation3DMagneticFluxDensity ------------------------------- # + + +class Simulation3DMagneticFluxDensity(BaseTDEMSimulation): + 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 + + 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 + + \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} + + 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 in terms of the magnetic flux density, we obtain: + + .. math:: + \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} + + 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} + + 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 ] + + 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{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 + Fields_Derivs = FieldsDerivativesEB + + def getAdiag(self, tInd): + 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 + + 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 + + dt = self.time_steps[tInd] + C = self.mesh.edge_curl + MeSigmaI = self.MeSigmaI + MfMui = self.MfMui + I = speye(self.mesh.n_faces) + + A = 1.0 / dt * I + (C * (MeSigmaI * (C.T.tocsr() * MfMui))) + + if self._makeASymmetric is True: + return MfMui.T.tocsr() * A + 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\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 + + # def MeSigmaIDeriv(x): + # return self.MeSigmaIDeriv(x) + + MfMui = self.MfMui + + if adjoint: + if self._makeASymmetric is True: + v = MfMui * v + return self.MeSigmaIDeriv(C.T * (MfMui * u), C.T * v, adjoint) + + ADeriv = C * (self.MeSigmaIDeriv(C.T * (MfMui * u), v, adjoint)) + + if self._makeASymmetric is True: + return MfMui.T * ADeriv + 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:`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] + MfMui = self.MfMui + Asubdiag = -1.0 / dt * sp.eye(self.mesh.n_faces) + + if self._makeASymmetric is True: + return MfMui.T * Asubdiag + + 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): + 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 + MfMui = self.MfMui + + s_m, s_e = self.getSourceTerm(tInd) + + rhs = C * (MeSigmaI * s_e) + s_m + if self._makeASymmetric is True: + return MfMui.T * rhs + 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 = \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 + MeSigmaI = self.MeSigmaI + + _, s_e = src.eval(self, self.times[tInd]) + s_mDeriv, s_eDeriv = src.evalDeriv(self, self.times[tInd], adjoint=adjoint) + + if adjoint: + if self._makeASymmetric is True: + v = self.MfMui * v + if isinstance(s_e, Zero): + MeSigmaIDerivT_v = Zero() + else: + MeSigmaIDerivT_v = self.MeSigmaIDeriv(s_e, C.T * v, adjoint) + + RHSDeriv = MeSigmaIDerivT_v + s_eDeriv(MeSigmaI.T * (C.T * v)) + s_mDeriv(v) + + return RHSDeriv + + if isinstance(s_e, Zero): + MeSigmaIDeriv_v = Zero() + else: + MeSigmaIDeriv_v = self.MeSigmaIDeriv(s_e, v, adjoint) + + RHSDeriv = C * MeSigmaIDeriv_v + C * MeSigmaI * s_eDeriv(v) + s_mDeriv(v) + + if self._makeASymmetric is True: + return self.MfMui.T * RHSDeriv + return RHSDeriv + + +# ------------------------------- Simulation3DElectricField ------------------------------- # +class Simulation3DElectricField(BaseTDEMSimulation): + 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: + + .. 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 + + \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} + + 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 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} + + 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} + + 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 ] + + 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. + + """ + + _fieldType = "e" + _formulation = "EB" + fieldsPair = Fields3DElectricField #: A Fields3DElectricField + Fields_Derivs = FieldsDerivativesEB + + # @profile + def Jtvec(self, m, v, f=None): + # 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) + + df_duT_v = self.Fields_Derivs(self) + + # same size as fields at a single timestep + ATinv_df_duT_v = np.zeros( + ( + len(self.survey.source_list), + len(f[self.survey.source_list[0], ftype, 0]), + ), + dtype=float, + ) + JTv = np.zeros(m.shape, dtype=float) + + # Loop over sources and receivers to create a fields object: + # PT_v, df_duT_v, df_dmT_v + # initialize storage for PT_v (don't need to preserve over sources) + PT_v = self.Fields_Derivs(self) + for src in self.survey.source_list: + # Looping over initializing field class is appending memory! + # PT_v = Fields_Derivs(self.mesh) # initialize storage + # #for PT_v (don't need to preserve over sources) + # initialize size + df_duT_v[src, "{}Deriv".format(self._fieldType), :] = np.zeros_like( + f[src, self._fieldType, :] + ) + + for rx in src.receiver_list: + PT_v[src, "{}Deriv".format(rx.projField), :] = rx.evalDeriv( + src, self.mesh, self.time_mesh, f, mkvc(v[src, rx]), adjoint=True + ) + # this is += + + # PT_v = np.reshape(curPT_v,(len(curPT_v)/self.time_mesh.nN, + # self.time_mesh.nN), order='F') + df_duTFun = getattr(f, "_{}Deriv".format(rx.projField), None) + + for tInd in range(self.nT + 1): + cur = df_duTFun( + tInd, + src, + None, + mkvc(PT_v[src, "{}Deriv".format(rx.projField), tInd]), + adjoint=True, + ) + + df_duT_v[src, "{}Deriv".format(self._fieldType), tInd] = df_duT_v[ + src, "{}Deriv".format(self._fieldType), tInd + ] + mkvc(cur[0], 2) + JTv = cur[1] + JTv + + # no longer need this + del PT_v + + AdiagTinv = None + + # Do the back-solve through time + # if the previous timestep is the same: no need to refactor the matrix + # for tInd, dt in zip(range(self.nT), self.time_steps): + + for tInd in reversed(range(self.nT)): + # tInd = tIndP - 1 + if AdiagTinv is not None and ( + tInd <= self.nT and self.time_steps[tInd] != self.time_steps[tInd + 1] + ): + AdiagTinv.clean() + AdiagTinv = None + + # refactor if we need to + if AdiagTinv is None: # and tInd > -1: + Adiag = self.getAdiag(tInd) + AdiagTinv = self.solver(Adiag.T, **self.solver_opts) + + if tInd < self.nT - 1: + Asubdiag = self.getAsubdiag(tInd + 1) + + for isrc, src in enumerate(self.survey.source_list): + # solve against df_duT_v + if tInd >= self.nT - 1: + # last timestep (first to be solved) + ATinv_df_duT_v[isrc, :] = ( + AdiagTinv + * df_duT_v[src, "{}Deriv".format(self._fieldType), tInd + 1] + ) + 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, :]) + ) + + dAsubdiagT_dm_v = self.getAsubdiagDeriv( + tInd, f[src, ftype, tInd], ATinv_df_duT_v[isrc, :], adjoint=True + ) + + dRHST_dm_v = self.getRHSDeriv( + tInd + 1, src, ATinv_df_duT_v[isrc, :], adjoint=True + ) # on nodes of time mesh + + un_src = f[src, ftype, tInd + 1] + # cell centered on time mesh + dAT_dm_v = self.getAdiagDeriv( + tInd, un_src, ATinv_df_duT_v[isrc, :], adjoint=True + ) + + JTv = JTv + mkvc(-dAT_dm_v - dAsubdiagT_dm_v + dRHST_dm_v) + + # Treating initial condition when a galvanic source is included + tInd = -1 + Grad = self.mesh.nodal_gradient + + for isrc, src in enumerate(self.survey.source_list): + if src.srcType == "galvanic": + ATinv_df_duT_v[isrc, :] = Grad * ( + self.Adcinv + * ( + Grad.T + * ( + mkvc( + df_duT_v[ + src, "{}Deriv".format(self._fieldType), tInd + 1 + ] + ) + - Asubdiag.T * mkvc(ATinv_df_duT_v[isrc, :]) + ) + ) + ) + + dRHST_dm_v = self.getRHSDeriv( + tInd + 1, src, ATinv_df_duT_v[isrc, :], adjoint=True + ) # on nodes of time mesh + + un_src = f[src, ftype, tInd + 1] + # cell centered on time mesh + dAT_dm_v = self.MeSigmaDeriv( + un_src, ATinv_df_duT_v[isrc, :], adjoint=True + ) + + JTv = JTv + mkvc(-dAT_dm_v + dRHST_dm_v) + + # del df_duT_v, ATinv_df_duT_v, A, Asubdiag + if AdiagTinv is not None: + AdiagTinv.clean() + + return mkvc(JTv).astype(float) + + def getAdiag(self, tInd): + 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 + + dt = self.time_steps[tInd] + C = self.mesh.edge_curl + MfMui = self.MfMui + MeSigma = self.MeSigma + + return C.T.tocsr() * (MfMui * C) + 1.0 / dt * MeSigma + + 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\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 + + dt = self.time_steps[tInd] + # MeSigmaDeriv = self.MeSigmaDeriv(u) + + if adjoint: + return 1.0 / dt * self.MeSigmaDeriv(u, v, adjoint) + + return 1.0 / dt * self.MeSigmaDeriv(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\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 + + dt = self.time_steps[tInd] + + return -1.0 / dt * self.MeSigma + + 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\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] + + if adjoint: + return -1.0 / dt * self.MeSigmaDeriv(u, v, adjoint) + + return -1.0 / dt * self.MeSigmaDeriv(u, v, adjoint) + + 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^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): + # tInd = tInd - 1 + + dt = self.time_steps[tInd - 1] + s_m, s_e = self.getSourceTerm(tInd) + _, s_en1 = self.getSourceTerm(tInd - 1) + + 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): + 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^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) + + +############################################################################### +# # +# H-J Formulation # +# # +############################################################################### + +# ------------------------------- Simulation3DMagneticField ------------------------------- # + + +class Simulation3DMagneticField(BaseTDEMSimulation): + 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: + + .. 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 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} + + 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} + + 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 ] + + 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. + + """ + + _fieldType = "h" + _formulation = "HJ" + fieldsPair = Fields3DMagneticField #: Fields object pair + Fields_Derivs = FieldsDerivativesHJ + + def getAdiag(self, tInd): + 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 + + dt = self.time_steps[tInd] + C = self.mesh.edge_curl + MfRho = self.MfRho + MeMu = self.MeMu + + 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 + + if adjoint: + return self.MfRhoDeriv(C * u, C * v, adjoint) + + 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] + + 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) + + 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]) + + if adjoint is True: + return self.MfRhoDeriv(s_e, C * v, adjoint) + # 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 + + if adjoint: + # This is the same as + # self.MfRhoIDeriv(G * u, D.T * v, adjoint=True) + return self.MfRhoIDeriv(G * u, G * v, adjoint=True) + return D * self.MfRhoIDeriv(G * u, v) + + +# ------------------------------- Simulation3DCurrentDensity ------------------------------- # + + +class Simulation3DCurrentDensity(BaseTDEMSimulation): + 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: + + .. 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" + _formulation = "HJ" + fieldsPair = Fields3DCurrentDensity #: Fields object pair + Fields_Derivs = FieldsDerivativesHJ + + def getAdiag(self, tInd): + 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 + + dt = self.time_steps[tInd] + C = self.mesh.edge_curl + MfRho = self.MfRho + MeMuI = self.MeMuI + eye = sp.eye(self.mesh.n_faces) + + A = C * (MeMuI * (C.T * MfRho)) + 1.0 / dt * eye + + if self._makeASymmetric: + return MfRho.T * A + + 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 + MfRho = self.MfRho + MeMuI = self.MeMuI + + if adjoint: + if self._makeASymmetric: + v = MfRho * v + return self.MfRhoDeriv(u, C * (MeMuI.T * (C.T * v)), adjoint) + + ADeriv = C * (MeMuI * (C.T * self.MfRhoDeriv(u, v, adjoint))) + if self._makeASymmetric: + return MfRho.T * ADeriv + 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) + + dt = self.time_steps[tInd] + + if self._makeASymmetric: + return -1.0 / dt * self.MfRho.T + 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 + + C = self.mesh.edge_curl + MeMuI = self.MeMuI + dt = self.time_steps[tInd] + s_m, s_e = self.getSourceTerm(tInd) + _, s_en1 = self.getSourceTerm(tInd - 1) + + rhs = -1.0 / dt * (s_e - s_en1) + C * MeMuI * s_m + if self._makeASymmetric: + return self.MfRho.T * rhs + 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 + + if adjoint: + # This is the same as + # self.MfRhoIDeriv(G * u, D.T * v, adjoint=True) + return self.MfRhoIDeriv(G * u, G * v, adjoint=True) + return D * self.MfRhoIDeriv(G * u, v) diff --git a/SimPEG/electromagnetics/time_domain/simulation_1d.py b/simpeg/electromagnetics/time_domain/simulation_1d.py similarity index 98% rename from SimPEG/electromagnetics/time_domain/simulation_1d.py rename to simpeg/electromagnetics/time_domain/simulation_1d.py index 83568857e3..3343ec3fba 100644 --- a/SimPEG/electromagnetics/time_domain/simulation_1d.py +++ b/simpeg/electromagnetics/time_domain/simulation_1d.py @@ -36,7 +36,7 @@ def survey(self): """The survey for the simulation Returns ------- - SimPEG.electromagnetics.time_domain.survey.Survey + simpeg.electromagnetics.time_domain.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey set") @@ -153,9 +153,9 @@ def _compute_coefficients(self): def func(t, i): out = np.zeros_like(t) t = t.copy() - t[ - (t > 0.0) & (t <= t_spline_points.min()) - ] = t_spline_points.min() # constant at very low ts + t[(t > 0.0) & (t <= t_spline_points.min())] = ( + t_spline_points.min() + ) # constant at very low ts out[t > 0.0] = splines[i](np.log(t[t > 0.0])) / t[t > 0.0] return out diff --git a/SimPEG/electromagnetics/time_domain/sources.py b/simpeg/electromagnetics/time_domain/sources.py similarity index 95% rename from SimPEG/electromagnetics/time_domain/sources.py rename to simpeg/electromagnetics/time_domain/sources.py index 527f8b4bfe..2d4f83ede4 100644 --- a/SimPEG/electromagnetics/time_domain/sources.py +++ b/simpeg/electromagnetics/time_domain/sources.py @@ -139,33 +139,6 @@ def eval_deriv(self, time): """ raise NotImplementedError # needed for E-formulation - ########################## - # Deprecated - ########################## - hasInitialFields = deprecate_property( - has_initial_fields, - "hasInitialFields", - new_name="has_initial_fields", - removal_version="0.17.0", - error=True, - ) - - offTime = deprecate_property( - off_time, - "offTime", - new_name="off_time", - removal_version="0.17.0", - error=True, - ) - - eps = deprecate_property( - epsilon, - "eps", - new_name="epsilon", - removal_version="0.17.0", - error=True, - ) - class StepOffWaveform(BaseWaveform): """ @@ -183,7 +156,7 @@ class StepOffWaveform(BaseWaveform): >>> import matplotlib.pyplot as plt >>> import numpy as np - >>> from SimPEG.electromagnetics import time_domain as tdem + >>> from simpeg.electromagnetics import time_domain as tdem >>> times = np.linspace(0, 1e-4, 1000) >>> waveform = tdem.sources.StepOffWaveform(off_time=1e-5) @@ -216,7 +189,7 @@ class RampOffWaveform(BaseWaveform): >>> import matplotlib.pyplot as plt >>> import numpy as np - >>> from SimPEG.electromagnetics import time_domain as tdem + >>> from simpeg.electromagnetics import time_domain as tdem >>> times = np.linspace(0, 1e-4, 1000) >>> waveform = tdem.sources.RampOffWaveform(off_time=1e-5) @@ -272,7 +245,7 @@ class RawWaveform(BaseWaveform): >>> import matplotlib.pyplot as plt >>> import numpy as np - >>> from SimPEG.electromagnetics import time_domain as tdem + >>> from simpeg.electromagnetics import time_domain as tdem >>> def my_waveform(t): >>> period = 1e-2 @@ -318,14 +291,6 @@ def waveform_function(self, value): def eval(self, time): # noqa: A003 return self.waveform_function(time) - waveFct = deprecate_property( - waveform_function, - "waveFct", - new_name="waveform_function", - removal_version="0.17.0", - error=True, - ) - class VTEMWaveform(BaseWaveform): """ @@ -345,7 +310,7 @@ class VTEMWaveform(BaseWaveform): >>> import matplotlib.pyplot as plt >>> import numpy as np - >>> from SimPEG.electromagnetics import time_domain as tdem + >>> from simpeg.electromagnetics import time_domain as tdem >>> times = np.linspace(0, 1e-2, 1000) >>> waveform = tdem.sources.VTEMWaveform() @@ -430,26 +395,6 @@ def eval_deriv(self, time): def time_nodes(self): return np.r_[0, self.peak_time, self.off_time] - ########################## - # Deprecated - ########################## - - peakTime = deprecate_property( - peak_time, - "peakTime", - new_name="peak_time", - removal_version="0.17.0", - error=True, - ) - - a = deprecate_property( - ramp_on_rate, - "a", - new_name="ramp_on_rate", - removal_version="0.17.0", - error=True, - ) - class TrapezoidWaveform(BaseWaveform): """ @@ -469,7 +414,7 @@ class TrapezoidWaveform(BaseWaveform): >>> import matplotlib.pyplot as plt >>> import numpy as np - >>> from SimPEG.electromagnetics import time_domain as tdem + >>> from simpeg.electromagnetics import time_domain as tdem >>> times = np.linspace(0, 1e-2, 1000) >>> waveform = tdem.sources.TrapezoidWaveform(ramp_on=[0.0, 2e-3], ramp_off=[4e-3, 6e-3]) @@ -575,7 +520,7 @@ class TriangularWaveform(TrapezoidWaveform): >>> import matplotlib.pyplot as plt >>> import numpy as np - >>> from SimPEG.electromagnetics import time_domain as tdem + >>> from simpeg.electromagnetics import time_domain as tdem >>> times = np.linspace(0, 1e-2, 1000) >>> waveform = tdem.sources.TriangularWaveform(start_time=1E-3, off_time=6e-3, peak_time=3e-3) @@ -627,18 +572,6 @@ def peak_time(self, value): self._ramp_on = np.r_[self._ramp_on[0], value] self._ramp_off = np.r_[value, self._ramp_off[1]] - ########################## - # Deprecated - ########################## - - peakTime = deprecate_property( - peak_time, - "peakTime", - new_name="peak_time", - removal_version="0.17.0", - error=True, - ) - class QuarterSineRampOnWaveform(TrapezoidWaveform): """ @@ -657,7 +590,7 @@ class QuarterSineRampOnWaveform(TrapezoidWaveform): >>> import matplotlib.pyplot as plt >>> import numpy as np - >>> from SimPEG.electromagnetics import time_domain as tdem + >>> from simpeg.electromagnetics import time_domain as tdem >>> times = np.linspace(0, 1e-2, 1000) >>> waveform = tdem.sources.QuarterSineRampOnWaveform(ramp_on=(0, 2e-3), ramp_off=(3e-3, 3.5e-3)) @@ -896,7 +829,7 @@ class ExponentialWaveform(BaseWaveform): >>> import matplotlib.pyplot as plt >>> import numpy as np - >>> from SimPEG.electromagnetics import time_domain as tdem + >>> from simpeg.electromagnetics import time_domain as tdem >>> times = np.linspace(-1e-2, 1e-2, 1000) >>> waveform = tdem.sources.ExponentialWaveform() @@ -1023,7 +956,7 @@ class BaseTDEMSrc(BaseEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.frequency_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.frequency_domain.receivers.BaseRx A list of FDEM receivers location : (dim) numpy.ndarray Source locations @@ -1129,7 +1062,7 @@ def eval(self, simulation, time): # noqa: A003 Parameters ---------- - simulation : SimPEG.electromagnetics.base.BaseTDEMSimulation + simulation : simpeg.electromagnetics.base.BaseTDEMSimulation An instance of a time-domain electromagnetic simulation time : float The time at which you want to compute the source terms @@ -1149,7 +1082,7 @@ def evalDeriv(self, simulation, time, v=None, adjoint=False): Parameters ---------- - simulation : SimPEG.electromagnetics.base.BaseTDEMSimulation + simulation : simpeg.electromagnetics.base.BaseTDEMSimulation An instance of a time-domain electromagnetic simulation time : The time at which you want to compute the derivative @@ -1202,7 +1135,7 @@ class MagDipole(BaseTDEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.time_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.time_domain.receivers.BaseRx A list of TDEM receivers location : (dim) numpy.ndarray, default = np.r_[0., 0., 0.] Source location. @@ -1530,7 +1463,7 @@ class CircularLoop(MagDipole): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.time_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.time_domain.receivers.BaseRx A list of TDEM receivers location : (dim) np.ndarray, default = np.r_[0., 0., 0.] Source location. @@ -1570,11 +1503,10 @@ def __init__( if "moment" in kwargs: kwargs.pop("moment") - N = kwargs.pop("N", None) - if N is not None: - self.N = N - else: - self.n_turns = n_turns + # 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 @@ -1638,7 +1570,8 @@ def moment(self, value): if value is not None: warnings.warn( "Moment is not set as a property. I is the product" - "of the loop radius and transmitter current" + "of the loop radius and transmitter current", + stacklevel=2, ) pass @@ -1671,7 +1604,9 @@ def _srcFct(self, obsLoc, coordinates="cartesian"): ) return self.n_turns * self._loop.vector_potential(obsLoc, coordinates) - N = deprecate_property(n_turns, "N", "n_turns", removal_version="0.19.0") + N = deprecate_property( + n_turns, "N", "n_turns", removal_version="0.19.0", error=True + ) class LineCurrent(BaseTDEMSrc): @@ -1683,7 +1618,7 @@ class LineCurrent(BaseTDEMSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.time_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.time_domain.receivers.BaseRx List of TDEM receivers location : (n, 3) numpy.ndarray Array defining the node locations for the wire path. For inductive sources, @@ -1770,7 +1705,7 @@ def Mejs(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.time_domain.simulation.BaseTDEMSimulation + simulation : simpeg.electromagnetics.time_domain.simulation.BaseTDEMSimulation Base TDEM simulation Returns @@ -1789,7 +1724,7 @@ def Mfjs(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.time_domain.simulation.BaseTDEMSimulation + simulation : simpeg.electromagnetics.time_domain.simulation.BaseTDEMSimulation Base TDEM simulation Returns @@ -1808,7 +1743,7 @@ def getRHSdc(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.time_domain.simulation.BaseTDEMSimulation + simulation : simpeg.electromagnetics.time_domain.simulation.BaseTDEMSimulation Base TDEM simulation Returns @@ -1832,7 +1767,7 @@ def phiInitial(self, simulation): Parameters ---------- - simulation : SimPEG.electromagnetics.base.BaseEMSimulation + simulation : simpeg.electromagnetics.base.BaseEMSimulation An electromagnetic simulation Returns diff --git a/SimPEG/electromagnetics/time_domain/survey.py b/simpeg/electromagnetics/time_domain/survey.py similarity index 93% rename from SimPEG/electromagnetics/time_domain/survey.py rename to simpeg/electromagnetics/time_domain/survey.py index e8798d8048..946d22e823 100644 --- a/SimPEG/electromagnetics/time_domain/survey.py +++ b/simpeg/electromagnetics/time_domain/survey.py @@ -13,7 +13,7 @@ class Survey(BaseSurvey): Parameters ---------- - source_list : list of SimPEG.electromagnetic.time_domain.sources.BaseTDEMSrc + source_list : list of simpeg.electromagnetic.time_domain.sources.BaseTDEMSrc List of SimPEG TDEM sources """ diff --git a/SimPEG/electromagnetics/utils/__init__.py b/simpeg/electromagnetics/utils/__init__.py similarity index 87% rename from SimPEG/electromagnetics/utils/__init__.py rename to simpeg/electromagnetics/utils/__init__.py index 25b3f24ac3..bf7fd197b9 100644 --- a/SimPEG/electromagnetics/utils/__init__.py +++ b/simpeg/electromagnetics/utils/__init__.py @@ -1,9 +1,9 @@ """ =================================================================== -Electromagnetics Utilities (:mod:`SimPEG.electromagnetics.utils`) +Electromagnetics Utilities (:mod:`simpeg.electromagnetics.utils`) =================================================================== -.. currentmodule:: SimPEG.electromagnetics.utils +.. currentmodule:: simpeg.electromagnetics.utils Current Utilities @@ -30,6 +30,7 @@ convolve_with_waveform """ + from .waveform_utils import ( omega, k, diff --git a/SimPEG/electromagnetics/utils/current_utils.py b/simpeg/electromagnetics/utils/current_utils.py similarity index 99% rename from SimPEG/electromagnetics/utils/current_utils.py rename to simpeg/electromagnetics/utils/current_utils.py index 84eb36c7ba..959f6e1260 100644 --- a/SimPEG/electromagnetics/utils/current_utils.py +++ b/simpeg/electromagnetics/utils/current_utils.py @@ -318,7 +318,9 @@ def _poly_line_source_tree(mesh, locs): srcCellIds = mesh.get_cells_along_line(A, B) levels = mesh.cell_levels_by_index(srcCellIds) if isinstance(levels, np.ndarray) and np.any(levels != levels[0]): - warnings.warn("Warning! Line path crosses a cell level change.") + warnings.warn( + "Warning! Line path crosses a cell level change.", stacklevel=2 + ) # Starts at point A! p0 = A diff --git a/SimPEG/electromagnetics/utils/em1d_utils.py b/simpeg/electromagnetics/utils/em1d_utils.py similarity index 99% rename from SimPEG/electromagnetics/utils/em1d_utils.py rename to simpeg/electromagnetics/utils/em1d_utils.py index 21a08dbd6a..91b0ee3f61 100644 --- a/SimPEG/electromagnetics/utils/em1d_utils.py +++ b/simpeg/electromagnetics/utils/em1d_utils.py @@ -2,7 +2,7 @@ from geoana.em.fdem.base import skin_depth from geoana.em.tdem import diffusion_distance -from SimPEG import utils +from simpeg import utils def get_vertical_discretization(n_layer, minimum_dz, geomtric_factor): diff --git a/SimPEG/electromagnetics/utils/testing_utils.py b/simpeg/electromagnetics/utils/testing_utils.py similarity index 97% rename from SimPEG/electromagnetics/utils/testing_utils.py rename to simpeg/electromagnetics/utils/testing_utils.py index bf8e774d5a..ec7ad01a33 100644 --- a/SimPEG/electromagnetics/utils/testing_utils.py +++ b/simpeg/electromagnetics/utils/testing_utils.py @@ -4,8 +4,8 @@ from discretize import TensorMesh from ... import maps, utils -from SimPEG import SolverLU -from SimPEG.electromagnetics import frequency_domain as fdem +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 CONDUCTIVITY = 1e1 @@ -148,7 +148,8 @@ def crossCheckTest( TOL=1e-5, verbose=False, ): - l2norm = lambda r: np.sqrt(r.dot(r)) + def l2norm(r): + return np.sqrt(r.dot(r)) prb1 = getFDEMProblem(fdemType1, comp, SrcList, freq, useMu, verbose) mesh = prb1.mesh diff --git a/SimPEG/electromagnetics/utils/waveform_utils.py b/simpeg/electromagnetics/utils/waveform_utils.py similarity index 98% rename from SimPEG/electromagnetics/utils/waveform_utils.py rename to simpeg/electromagnetics/utils/waveform_utils.py index 7166eb62f9..55e2c2f8ab 100644 --- a/SimPEG/electromagnetics/utils/waveform_utils.py +++ b/simpeg/electromagnetics/utils/waveform_utils.py @@ -83,7 +83,7 @@ def convolve_with_waveform(func, waveform, times, fargs=None, fkwargs=None): ---------- func : callable function of `t` that should be convolved - waveform : SimPEG.electromagnetics.time_domain.waveforms.BaseWaveform + waveform : simpeg.electromagnetics.time_domain.waveforms.BaseWaveform times : array_like fargs : list, optional extra arguments given to `func` diff --git a/SimPEG/electromagnetics/viscous_remanent_magnetization/__init__.py b/simpeg/electromagnetics/viscous_remanent_magnetization/__init__.py similarity index 91% rename from SimPEG/electromagnetics/viscous_remanent_magnetization/__init__.py rename to simpeg/electromagnetics/viscous_remanent_magnetization/__init__.py index 56349b8aba..76379773ec 100644 --- a/SimPEG/electromagnetics/viscous_remanent_magnetization/__init__.py +++ b/simpeg/electromagnetics/viscous_remanent_magnetization/__init__.py @@ -1,8 +1,8 @@ """ =========================================================================================================== -Viscous Remanent Magnetization (:mod:`SimPEG.electromagnetics.viscous_remanent_magnetization`) +Viscous Remanent Magnetization (:mod:`simpeg.electromagnetics.viscous_remanent_magnetization`) =========================================================================================================== -.. currentmodule:: SimPEG.electromagnetics.viscous_remanent_magnetization +.. currentmodule:: simpeg.electromagnetics.viscous_remanent_magnetization About ``viscous_remanent_magnetization`` @@ -66,6 +66,7 @@ waveforms.BaseVRMWaveform """ + from . import receivers from . import sources from . import receivers as Rx diff --git a/SimPEG/electromagnetics/viscous_remanent_magnetization/receivers.py b/simpeg/electromagnetics/viscous_remanent_magnetization/receivers.py similarity index 100% rename from SimPEG/electromagnetics/viscous_remanent_magnetization/receivers.py rename to simpeg/electromagnetics/viscous_remanent_magnetization/receivers.py diff --git a/SimPEG/electromagnetics/viscous_remanent_magnetization/simulation.py b/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py similarity index 99% rename from SimPEG/electromagnetics/viscous_remanent_magnetization/simulation.py rename to simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py index efb063fd5a..27cfb040f1 100644 --- a/SimPEG/electromagnetics/viscous_remanent_magnetization/simulation.py +++ b/simpeg/electromagnetics/viscous_remanent_magnetization/simulation.py @@ -69,7 +69,7 @@ def survey(self): Returns ------- - SimPEG.electromagnetics.viscous_temanent_magnetization.survey.Survey + simpeg.electromagnetics.viscous_temanent_magnetization.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey.") @@ -767,9 +767,7 @@ def _getSubsetAcolumns(self, xyzc, xyzh, pp, qq, refFlag): xyzc[refFlag == qq, :] - xyzh[refFlag == qq, :] / 2 ) # Get bottom southwest corners of cells to be refined m = np.shape(xyzc_sub)[0] - xyzc_sub = np.kron( - xyzc_sub, np.ones((n**3, 1)) - ) # Kron for n**3 refined cells + xyzc_sub = np.kron(xyzc_sub, np.ones((n**3, 1))) # Kron for n**3 refined cells xyzh_sub = np.kron( xyzh_sub / n, np.ones((n**3, 1)) ) # Kron for n**3 refined cells with widths h/n diff --git a/SimPEG/electromagnetics/viscous_remanent_magnetization/sources.py b/simpeg/electromagnetics/viscous_remanent_magnetization/sources.py similarity index 93% rename from SimPEG/electromagnetics/viscous_remanent_magnetization/sources.py rename to simpeg/electromagnetics/viscous_remanent_magnetization/sources.py index d438e26a6e..b93f206a53 100644 --- a/SimPEG/electromagnetics/viscous_remanent_magnetization/sources.py +++ b/simpeg/electromagnetics/viscous_remanent_magnetization/sources.py @@ -16,11 +16,11 @@ class BaseSrcVRM(BaseSrc): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.viscous_remanent_magnetization.receivers.Point + receiver_list : list of simpeg.electromagnetics.viscous_remanent_magnetization.receivers.Point A list of VRM receivers location : (3) array_like Source location - waveform : SimPEG.electromagnetics.viscous_remanent_magnetization.waveforms.BaseVRMWaveform + waveform : simpeg.electromagnetics.viscous_remanent_magnetization.waveforms.BaseVRMWaveform A VRM waveform """ @@ -68,13 +68,13 @@ class MagDipole(BaseSrcVRM): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.viscous_remanent_magnetization.receivers.Point + receiver_list : list of simpeg.electromagnetics.viscous_remanent_magnetization.receivers.Point VRM receivers location : (3) array_like source location moment : (3) array_like dipole moment (mx, my, mz) - waveform : SimPEG.electromagnetics.viscous_remanent_magnetization.waveforms.BaseVRMWaveform + waveform : simpeg.electromagnetics.viscous_remanent_magnetization.waveforms.BaseVRMWaveform VRM waveform """ @@ -138,15 +138,9 @@ def getH0(self, xyz): + m[2] * (xyz[:, 2] - r0[2]) ) - hx0 = (1 / (4 * np.pi)) * ( - 3 * (xyz[:, 0] - r0[0]) * mdotr / r**5 - m[0] / r**3 - ) - hy0 = (1 / (4 * np.pi)) * ( - 3 * (xyz[:, 1] - r0[1]) * mdotr / r**5 - m[1] / r**3 - ) - hz0 = (1 / (4 * np.pi)) * ( - 3 * (xyz[:, 2] - r0[2]) * mdotr / r**5 - m[2] / r**3 - ) + hx0 = (1 / (4 * np.pi)) * (3 * (xyz[:, 0] - r0[0]) * mdotr / r**5 - m[0] / r**3) + hy0 = (1 / (4 * np.pi)) * (3 * (xyz[:, 1] - r0[1]) * mdotr / r**5 - m[1] / r**3) + hz0 = (1 / (4 * np.pi)) * (3 * (xyz[:, 2] - r0[2]) * mdotr / r**5 - m[2] / r**3) return np.c_[hx0, hy0, hz0] @@ -168,7 +162,7 @@ def _getRefineFlags(self, xyzc, refinement_factor, refinement_distance): """ - refFlag = np.zeros(np.shape(xyzc)[0], dtype=np.int) + refFlag = np.zeros(np.shape(xyzc)[0], dtype=int) r = np.sqrt( (xyzc[:, 0] - self.location[0]) ** 2 @@ -195,7 +189,7 @@ class CircLoop(BaseSrcVRM): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.viscous_remanent_magnetization.receivers.Point + receiver_list : list of simpeg.electromagnetics.viscous_remanent_magnetization.receivers.Point VRM receivers location : (3) array_like source location @@ -205,7 +199,7 @@ class CircLoop(BaseSrcVRM): Circular loop normal azimuth and declination Imax : float Maximum current amplitude - waveform : SimPEG.electromagnetics.viscous_remanent_magnetization.waveforms.BaseVRMWaveform + waveform : simpeg.electromagnetics.viscous_remanent_magnetization.waveforms.BaseVRMWaveform VRM waveform """ @@ -285,8 +279,7 @@ def getH0(self, xyz): (x1p / s) * (x3p * I / (2 * np.pi * s * np.sqrt(x3p**2 + (a + s) ** 2))) * ( - ((a**2 + x3p**2 + s**2) / (x3p**2 + (s - a) ** 2)) - * spec.ellipe(k) + ((a**2 + x3p**2 + s**2) / (x3p**2 + (s - a) ** 2)) * spec.ellipe(k) - spec.ellipk(k) ) ) @@ -294,8 +287,7 @@ def getH0(self, xyz): (x2p / s) * (x3p * I / (2 * np.pi * s * np.sqrt(x3p**2 + (a + s) ** 2))) * ( - ((a**2 + x3p**2 + s**2) / (x3p**2 + (s - a) ** 2)) - * spec.ellipe(k) + ((a**2 + x3p**2 + s**2) / (x3p**2 + (s - a) ** 2)) * spec.ellipe(k) - spec.ellipk(k) ) ) @@ -330,7 +322,7 @@ def _getRefineFlags(self, xyzc, refinement_factor, refinement_distance): """ - refFlag = np.zeros(np.shape(xyzc)[0], dtype=np.int) + refFlag = np.zeros(np.shape(xyzc)[0], dtype=int) r0 = self.location a = self.radius @@ -387,14 +379,14 @@ class LineCurrent(BaseSrcVRM): Parameters ---------- - receiver_list : list of SimPEG.electromagnetics.time_domain.receivers.BaseRx + receiver_list : list of simpeg.electromagnetics.time_domain.receivers.BaseRx List of TDEM receivers location : (n, 3) numpy.ndarray Array defining the node locations for the wire path. For inductive sources, you must close the loop. Imax : float Maximum current amplitude - waveform : SimPEG.electromagnetics.viscous_remanent_magnetization.waveforms.BaseVRMWaveform + waveform : simpeg.electromagnetics.viscous_remanent_magnetization.waveforms.BaseVRMWaveform VRM waveform """ @@ -523,12 +515,12 @@ def _getRefineFlags(self, xyzc, refinement_factor, refinement_distance): """ - ref_flag = np.zeros(np.shape(xyzc)[0], dtype=np.int) + ref_flag = np.zeros(np.shape(xyzc)[0], dtype=int) nSeg = np.shape(self.location)[0] - 1 for tt in range(0, nSeg): - ref_flag_tt = np.zeros(np.shape(xyzc)[0], dtype=np.int) + ref_flag_tt = np.zeros(np.shape(xyzc)[0], dtype=int) tx0 = self.location[tt, :] tx1 = self.location[tt + 1, :] a = (tx1[0] - tx0[0]) ** 2 + (tx1[1] - tx0[1]) ** 2 + (tx1[2] - tx0[2]) ** 2 @@ -546,7 +538,7 @@ def _getRefineFlags(self, xyzc, refinement_factor, refinement_distance): + (tx0[2] - xyzc[:, 2]) ** 2 - d**2 ) - e = np.array(b**2 - 4 * a * c, dtype=np.complex) + e = np.array(b**2 - 4 * a * c, dtype=complex) q_pos = (-b + np.sqrt(e)) / (2 * a) q_neg = (-b - np.sqrt(e)) / (2 * a) diff --git a/SimPEG/electromagnetics/viscous_remanent_magnetization/survey.py b/simpeg/electromagnetics/viscous_remanent_magnetization/survey.py similarity index 97% rename from SimPEG/electromagnetics/viscous_remanent_magnetization/survey.py rename to simpeg/electromagnetics/viscous_remanent_magnetization/survey.py index e2bfba0197..0f65d97d27 100644 --- a/SimPEG/electromagnetics/viscous_remanent_magnetization/survey.py +++ b/simpeg/electromagnetics/viscous_remanent_magnetization/survey.py @@ -16,7 +16,7 @@ class SurveyVRM(BaseSurvey): Parameters ---------- - source_list : list of SimPEG.electromagnetic.viscous_remanent_magnetization.sources.BaseSrcVRM + source_list : list of simpeg.electromagnetic.viscous_remanent_magnetization.sources.BaseSrcVRM List of SimPEG VRM sources t_active : numpy.ndarray of bool Active time channels used in inversion diff --git a/SimPEG/electromagnetics/viscous_remanent_magnetization/waveforms.py b/simpeg/electromagnetics/viscous_remanent_magnetization/waveforms.py similarity index 100% rename from SimPEG/electromagnetics/viscous_remanent_magnetization/waveforms.py rename to simpeg/electromagnetics/viscous_remanent_magnetization/waveforms.py diff --git a/SimPEG/fields.py b/simpeg/fields.py similarity index 56% rename from SimPEG/fields.py rename to simpeg/fields.py index 818bc67a12..fd20be1716 100644 --- a/SimPEG/fields.py +++ b/simpeg/fields.py @@ -5,23 +5,71 @@ class Fields: - """Fancy Field Storage - .. code::python - fields = Fields( - simulation=simulation, knownFields={"phi": "CC"} - ) - fields[:,'phi'] = phi - print(fields[src0,'phi']) + 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 + -------- + 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: @@ -44,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 @@ -60,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 ------- @@ -105,33 +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 knownFields(self): - """Fields known to this object.""" - return self._knownFields - @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] @@ -277,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/__init__.py b/simpeg/flow/__init__.py similarity index 100% rename from SimPEG/flow/__init__.py rename to simpeg/flow/__init__.py diff --git a/SimPEG/flow/richards/__init__.py b/simpeg/flow/richards/__init__.py similarity index 92% rename from SimPEG/flow/richards/__init__.py rename to simpeg/flow/richards/__init__.py index b22a2ea880..d195f6df1e 100644 --- a/SimPEG/flow/richards/__init__.py +++ b/simpeg/flow/richards/__init__.py @@ -1,8 +1,8 @@ """ ======================================================================= -Richards Flow (:mod:`SimPEG.flow.richards`) +Richards Flow (:mod:`simpeg.flow.richards`) ======================================================================= -.. currentmodule:: SimPEG.flow.richards +.. currentmodule:: simpeg.flow.richards About ``Richards flow`` @@ -40,6 +40,7 @@ empirical.VanGenuchtenParams """ + from . import empirical from .survey import Survey from .simulation import SimulationNDCellCentered diff --git a/SimPEG/flow/richards/empirical.py b/simpeg/flow/richards/empirical.py similarity index 99% rename from SimPEG/flow/richards/empirical.py rename to simpeg/flow/richards/empirical.py index edbf7361dd..d726aaad91 100644 --- a/SimPEG/flow/richards/empirical.py +++ b/simpeg/flow/richards/empirical.py @@ -33,8 +33,8 @@ def _partition_args(mesh, Hcond, Theta, hcond_args, theta_args, **kwargs): 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 SimPEG Mesh + counter = None #: A simpeg.utils.Counter object + mesh = None #: A discretize Mesh def __init__(self, mesh, **kwargs): self.mesh = mesh @@ -570,9 +570,7 @@ def _derivKs(self, u): dKs_dm_p = P_p * self.KsDeriv dKs_dm_n = ( P_n - * utils.sdiag( - theta_e**I * ((1.0 - (1.0 - theta_e ** (1.0 / m)) ** m) ** 2) - ) + * utils.sdiag(theta_e**I * ((1.0 - (1.0 - theta_e ** (1.0 / m)) ** m) ** 2)) * self.KsDeriv ) return dKs_dm_p + dKs_dm_n diff --git a/SimPEG/flow/richards/receivers.py b/simpeg/flow/richards/receivers.py similarity index 96% rename from SimPEG/flow/richards/receivers.py rename to simpeg/flow/richards/receivers.py index 5a2d16b109..fc815a6434 100644 --- a/SimPEG/flow/richards/receivers.py +++ b/simpeg/flow/richards/receivers.py @@ -23,7 +23,7 @@ def deriv(self, U, simulation, du_dm_v=None, v=None, adjoint=False): ---------- U : (n_time) list of (n_cells) numpy.ndarray Fields computed on the mesh. This is unused for this receiver. - simulation : SimPEG.flow.richards.simulation.SimulationNDCellCentered + simulation : simpeg.flow.richards.simulation.SimulationNDCellCentered A Richards flor simulation du_dm_v : numpy.ndarray Derivative with respect to the model times a vector @@ -65,7 +65,7 @@ def deriv(self, U, simulation, du_dm_v=None, v=None, adjoint=False): ---------- U : (n_time) list of (n_cells) numpy.ndarray Fields computed on the mesh. This is unused for this receiver. - simulation : SimPEG.flow.richards.simulation.SimulationNDCellCentered + simulation : simpeg.flow.richards.simulation.SimulationNDCellCentered A Richards flor simulation du_dm_v : numpy.ndarray Derivative with respect to the model times a vector diff --git a/SimPEG/flow/richards/simulation.py b/simpeg/flow/richards/simulation.py similarity index 98% rename from SimPEG/flow/richards/simulation.py rename to simpeg/flow/richards/simulation.py index 81452dd22b..b897230230 100644 --- a/SimPEG/flow/richards/simulation.py +++ b/simpeg/flow/richards/simulation.py @@ -87,7 +87,11 @@ def initial_conditions(self, value): ) debug = deprecate_property( - BaseTimeSimulation.verbose, "debug", "verbose", removal_version="0.19.0" + BaseTimeSimulation.verbose, + "debug", + "verbose", + removal_version="0.19.0", + future_warn=True, ) @property diff --git a/SimPEG/flow/richards/survey.py b/simpeg/flow/richards/survey.py similarity index 81% rename from SimPEG/flow/richards/survey.py rename to simpeg/flow/richards/survey.py index da79f9bdab..5bf8a47ba3 100644 --- a/SimPEG/flow/richards/survey.py +++ b/simpeg/flow/richards/survey.py @@ -18,7 +18,7 @@ def receiver_list(self): Returns ------- - list of SimPEG.survey.BaseRx + list of simpeg.survey.BaseRx List of receivers associated with the survey """ return self._receiver_list @@ -43,7 +43,7 @@ def deriv(self, simulation, f, du_dm_v=None, v=None): Parameters ---------- - simulation : SimPEG.flow.richards.simulation.SimulationNDCellCentered + simulation : simpeg.flow.richards.simulation.SimulationNDCellCentered A Richards flow simulation class f : (n_times) list of numpy.ndarray Fields @@ -68,7 +68,7 @@ def derivAdjoint(self, simulation, f, v=None): Parameters ---------- - simulation : SimPEG.flow.richards.simulation.SimulationNDCellCentered + simulation : simpeg.flow.richards.simulation.SimulationNDCellCentered A Richards flow simulation class f : (n_times) list of numpy.ndarray Fields. @@ -80,12 +80,12 @@ def derivAdjoint(self, simulation, f, v=None): numpy.ndarray Adjoint derivative with respect to model times a vector """ - dd_du = list(range(len(self.receiver_list))) - dd_dm = list(range(len(self.receiver_list))) + dd_du = 0 + dd_dm = 0 cnt = 0 - for ii, rx in enumerate(self.receiver_list): - dd_du[ii], dd_dm[ii] = rx.deriv( - f, simulation, v=v[cnt : cnt + rx.nD], adjoint=True - ) + for rx in self.receiver_list: + du, dm = rx.deriv(f, simulation, v=v[cnt : cnt + rx.nD], adjoint=True) + dd_du = dd_du + du + dd_dm = dd_dm + dm cnt += rx.nD - return np.sum(dd_du, axis=0), np.sum(dd_dm, axis=0) + return dd_du, dd_dm diff --git a/SimPEG/inverse_problem.py b/simpeg/inverse_problem.py similarity index 92% rename from SimPEG/inverse_problem.py rename to simpeg/inverse_problem.py index bafe05e4ee..e554c95cee 100644 --- a/SimPEG/inverse_problem.py +++ b/simpeg/inverse_problem.py @@ -14,13 +14,22 @@ validate_ndarray_with_shape, ) from .simulation import DefaultSolver +from .version import __version__ as simpeg_version 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, + **kwargs, ): super().__init__(**kwargs) assert isinstance(reg, BaseRegularization) or isinstance( @@ -35,6 +44,7 @@ def __init__( self.debug = debug self.counter = counter self.model = None + self.print_version = print_version # TODO: Remove: (and make iteration printers better!) self.opt.parent = self self.reg.parent = self @@ -71,11 +81,11 @@ def debug(self, value): @property def counter(self): - """Set this to a `SimPEG.utils.Counter` if you want to count things. + """Set this to a `simpeg.utils.Counter` if you want to count things. Returns ------- - None or SimPEG.utils.Counter + None or simpeg.utils.Counter """ return self._counter @@ -91,7 +101,7 @@ def dmisfit(self): Returns ------- - SimPEG.objective_function.ComboObjectiveFunction + simpeg.objective_function.ComboObjectiveFunction """ return self._dmisfit @@ -108,7 +118,7 @@ def reg(self): Returns ------- - SimPEG.objective_function.ComboObjectiveFunction + simpeg.objective_function.ComboObjectiveFunction """ return self._reg @@ -125,7 +135,7 @@ def opt(self): Returns ------- - SimPEG.optimization.Minimize + simpeg.optimization.Minimize """ return self._opt @@ -174,13 +184,16 @@ def startup(self, m0): if self.debug: print("Calling InvProblem.startup") + if self.print_version: + print(f"\nRunning inversion with SimPEG v{simpeg_version}") + for fct in self.reg.objfcts: if ( hasattr(fct, "reference_model") and getattr(fct, "reference_model", None) is None ): print( - "SimPEG.InvProblem will set Regularization.reference_model to m0." + "simpeg.InvProblem will set Regularization.reference_model to m0." ) fct.reference_model = m0 @@ -200,7 +213,7 @@ def startup(self, m0): solver_opts = objfct.simulation.solver_opts print( """ - SimPEG.InvProblem is setting bfgsH0 to the inverse of the eval2Deriv. + 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__ @@ -211,7 +224,7 @@ def startup(self, m0): if set_default: print( """ - SimPEG.InvProblem is setting bfgsH0 to the inverse of the eval2Deriv. + simpeg.InvProblem is setting bfgsH0 to the inverse of the eval2Deriv. ***Done using the default solver {} and no solver_opts.*** """.format( DefaultSolver.__name__ diff --git a/SimPEG/inversion.py b/simpeg/inversion.py similarity index 96% rename from SimPEG/inversion.py rename to simpeg/inversion.py index 09245631c0..9826f8cca9 100644 --- a/SimPEG/inversion.py +++ b/simpeg/inversion.py @@ -67,14 +67,14 @@ def debug(self): def debug(self, value): self._debug = validate_type("debug", value, bool) - #: Set this to a SimPEG.utils.Counter() if you want to count things + #: Set this to a simpeg.utils.Counter() if you want to count things @property def counter(self): """The counter. Returns ------- - None or SimPEG.utils.Counter + None or simpeg.utils.Counter """ return self._counter diff --git a/SimPEG/maps.py b/simpeg/maps.py similarity index 95% rename from SimPEG/maps.py rename to simpeg/maps.py index 62836bd6f4..3361fe725a 100644 --- a/SimPEG/maps.py +++ b/simpeg/maps.py @@ -8,6 +8,7 @@ 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 @@ -257,7 +258,7 @@ def __mul__(self, val): ) def dot(self, map1): - r"""Multiply two mappings to create a :class:`SimPEG.maps.ComboMap`. + 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, @@ -288,7 +289,7 @@ def dot(self, map1): a vector space of length 5, then takes the natural exponent. >>> import numpy as np - >>> from SimPEG.maps import ExpMap, Projection + >>> from simpeg.maps import ExpMap, Projection >>> nP1 = 1 >>> nP2 = 5 @@ -377,7 +378,7 @@ class ComboMap(IdentityMap): Parameters ---------- - maps : list of SimPEG.maps.IdentityMap + 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! @@ -387,7 +388,7 @@ class ComboMap(IdentityMap): a vector space of length 5, then takes the natural exponent. >>> import numpy as np - >>> from SimPEG.maps import ExpMap, Projection, ComboMap + >>> from simpeg.maps import ExpMap, Projection, ComboMap >>> nP1 = 1 >>> nP2 = 5 @@ -654,7 +655,7 @@ class Projection(IdentityMap): Here we define a mapping that rearranges and projects 2 model parameters to a vector space spanning 4 parameters. - >>> from SimPEG.maps import Projection + >>> from simpeg.maps import Projection >>> import numpy as np >>> nP = 2 @@ -894,7 +895,7 @@ class SurjectUnits(IdentityMap): 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 simpeg.maps import SurjectUnits >>> from discretize import TensorMesh >>> import numpy as np @@ -1276,7 +1277,7 @@ class Wires(object): 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 + >>> from simpeg.maps import Wires, ReciprocalMap >>> import numpy as np >>> p1 = np.r_[4.5, 2.7, 6.9, 7.1, 1.2] @@ -1316,23 +1317,29 @@ def __init__(self, *args): and isinstance(arg[0], str) and # TODO: this should be extended to a slice. - isinstance(arg[1], (int, np.integer)) + isinstance(arg[1], (int, np.integer, Projection)) ), ( - "Each wire needs to be a tuple: (name, length). " + "Each wire needs to be a tuple: (name, length) or (name, Projection). " "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])) + + if isinstance(arg[1], (int, np.integer)): + wire = Projection(self.nP, slice(start, start + arg[1])) + start += arg[1] + else: + wire = 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]) + self.maps = maps + self._nP = maps[0][1].nP + self._tuple = namedtuple("Model", [name for name, _ in args]) + self._projection = sp.vstack([wire.P for _, wire in self.maps]) def __mul__(self, val): assert isinstance(val, np.ndarray) @@ -1352,6 +1359,22 @@ def nP(self): """ return self._nP + def deriv(self, m): + """ + Derivative of the mapping with respect to the input parameters + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the gradient is evaluated. + + Returns + ------- + (n_param, ) numpy.ndarray + The Gradient of the mapping function evaluated for the model provided. + """ + return self._projection + class SelfConsistentEffectiveMedium(IdentityMap): r""" @@ -1696,9 +1719,7 @@ def getQ(self, alpha): 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)) + 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)) @@ -1775,7 +1796,7 @@ def _sc2phaseEMTSpheroidstransform(self, phi1): """ if not (np.all(0 <= phi1) and np.all(phi1 <= 1)): - warnings.warn("there are phis outside bounds of 0 and 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 @@ -1812,7 +1833,7 @@ def _sc2phaseEMTSpheroidstransform(self, phi1): 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") + warnings.warn("Maximum number of iterations reached", stacklevel=2) return sige2 @@ -2156,6 +2177,166 @@ 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. @@ -2520,7 +2701,7 @@ class ComplexMap(IdentityMap): (4 real and 4 imaginary values). The output of the mapping is a complex array with 4 values. - >>> from SimPEG.maps import ComplexMap + >>> from simpeg.maps import ComplexMap >>> from discretize import TensorMesh >>> import numpy as np @@ -2617,7 +2798,7 @@ def deriv(self, m, v=None): 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 simpeg.maps import ComplexMap >>> from discretize import TensorMesh >>> import numpy as np @@ -2770,8 +2951,8 @@ class SurjectVertical1D(IdentityMap): 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 simpeg.maps import SurjectVertical1D + >>> from simpeg.utils import plot_1d_layer_model >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib as mpl @@ -2902,7 +3083,7 @@ class Surject2Dto3D(IdentityMap): 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 simpeg.maps import Surject2Dto3D >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib as mpl @@ -3112,9 +3293,11 @@ def indActive(self, value): 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, + ( + self.mesh.cell_centers[self.indActive, :] + if self.indActive is not None + else self.mesh.cell_centers + ), "CC", zeros_outside=True, ) @@ -3240,7 +3423,6 @@ def nP(self): return int(self.indActive.sum()) def _transform(self, m): - if m.ndim > 1: return self.P * m + self.valInactive[:, None] @@ -3367,7 +3549,7 @@ class ParametricCircleMap(IdentityMap): 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 simpeg.maps import ParametricCircleMap >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt @@ -3645,7 +3827,7 @@ class ParametricPolyMap(IdentityMap): 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 simpeg.maps import ParametricPolyMap, InjectActiveCells >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt @@ -4003,7 +4185,7 @@ class ParametricSplineMap(IdentityMap): for the interface at the horizontal positions supplied when creating the mapping. - >>> from SimPEG.maps import ParametricSplineMap + >>> from simpeg.maps import ParametricSplineMap >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt @@ -4422,15 +4604,19 @@ def x(self): 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] + ( + 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] + ( + self.mesh.cell_centers[:, 0] + if self.indActive is None + else self.mesh.cell_centers[self.indActive, 0] + ) ][0] return self._x @@ -4446,9 +4632,11 @@ def y(self): 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] + ( + self.mesh.cell_centers[:, 1] + if self.indActive is None + else self.mesh.cell_centers[self.indActive, 1] + ) ][0] else: self._y = None @@ -4466,9 +4654,11 @@ def z(self): 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] + ( + self.mesh.cell_centers[:, 2] + if self.indActive is None + else self.mesh.cell_centers[self.indActive, 2] + ) ][0] else: self._z = None @@ -4538,7 +4728,7 @@ class ParametricLayer(BaseParametric): (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 simpeg.maps import ParametricLayer, InjectActiveCells >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt @@ -4800,7 +4990,7 @@ class ParametricBlock(BaseParametric): 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 simpeg.maps import ParametricBlock, InjectActiveCells >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt @@ -4935,12 +5125,7 @@ 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 - ) + return (self.p / 2) * (val**2 + self.epsilon**2) ** ((self.p / 2) - 1) * 2 * val # def _rotation(self, mDict): # if self.mesh.dim == 2: @@ -5148,7 +5333,7 @@ class ParametricEllipsoid(ParametricBlock): 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 simpeg.maps import ParametricEllipsoid, InjectActiveCells >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt @@ -5984,7 +6169,7 @@ def __init__( self._components = validate_integer("components", components, min_val=1) self.enforce_active = enforce_active # trigger creation of P - self._projection = None + _ = self.projection @property def global_mesh(self): @@ -6025,9 +6210,6 @@ def local_active(self): ------- (local_mesh.n_cells) numpy.ndarray of bool """ - if self._local_active is None: - getattr(self, "projection") - return self._local_active @property @@ -6076,7 +6258,7 @@ def projection(self): if self.enforce_active: self.local_active[ self.local_mesh._get_containing_cell_indexes( - self.global_mesh.cell_centers[self.global_active == False, :] + self.global_mesh.cell_centers[~self.global_active, :] ) ] = False projection = projection[self.local_active, :] @@ -6085,7 +6267,8 @@ def projection(self): [ sdiag(1.0 / np.sum(projection, axis=1)) * projection for ii in range(self.components) - ], format="csr" + ], + format="csr", ) return self._projection diff --git a/SimPEG/meta/__init__.py b/simpeg/meta/__init__.py similarity index 57% rename from SimPEG/meta/__init__.py rename to simpeg/meta/__init__.py index 1f3695e674..3dca694298 100644 --- a/SimPEG/meta/__init__.py +++ b/simpeg/meta/__init__.py @@ -1,8 +1,8 @@ """ ======================================================== -Meta SimPEG Classes (:mod:`SimPEG.meta`) +Meta SimPEG Classes (:mod:`simpeg.meta`) ======================================================== -.. currentmodule:: SimPEG.meta +.. currentmodule:: simpeg.meta SimPEG's meta module defines tools for working with simulations representing many smaller simulations working together to solve a geophysical problem. @@ -40,11 +40,22 @@ Multiprocessing --------------- -Coming soon! + +.. autosummary:: + :toctree: generated/ + + MultiprocessingMetaSimulation + MultiprocessingSumMetaSimulation + MultiprocessingRepeatedSimulation Dask ---- -Coming soon! +.. autosummary:: + :toctree: generated/ + + DaskMetaSimulation + DaskSumMetaSimulation + DaskRepeatedSimulation MPI --- @@ -57,3 +68,27 @@ """ from .simulation import MetaSimulation, SumMetaSimulation, RepeatedSimulation + +from .multiprocessing import ( + MultiprocessingMetaSimulation, + MultiprocessingSumMetaSimulation, + MultiprocessingRepeatedSimulation, +) + +try: + from .dask_sim import ( + DaskMetaSimulation, + DaskSumMetaSimulation, + DaskRepeatedSimulation, + ) +except ImportError: + + class DaskMetaSimulation(MetaSimulation): + def __init__(self, *args, **kwargs): + raise ImportError( + "This simulation requires dask.distributed. Please see installation " + "instructions at https://distributed.dask.org/" + ) + + DaskSumMetaSimulation = DaskMetaSimulation + DaskRepeatedMetaSimulation = DaskMetaSimulation diff --git a/simpeg/meta/dask_sim.py b/simpeg/meta/dask_sim.py new file mode 100644 index 0000000000..bddf091920 --- /dev/null +++ b/simpeg/meta/dask_sim.py @@ -0,0 +1,644 @@ +import numpy as np + +from simpeg.simulation import BaseSimulation +from simpeg.survey import BaseSurvey +from simpeg.maps import IdentityMap +from simpeg.utils import validate_list_of_types, validate_type +from simpeg.props import HasModel +import itertools +from dask.distributed import Client +from dask.distributed import Future +from .simulation import MetaSimulation, SumMetaSimulation +import scipy.sparse as sp +from operator import add +import warnings + + +def _store_model(mapping, sim, model): + sim.model = mapping * model + + +def _calc_fields(mapping, sim, model, apply_map=False): + if apply_map and model is not None: + return sim.fields(m=mapping @ model) + else: + return sim.fields(m=sim.model) + + +def _calc_dpred(mapping, sim, model, field, apply_map=False): + if apply_map and model is not None: + return sim.dpred(m=mapping @ model) + else: + return sim.dpred(m=sim.model, f=field) + + +def _j_vec_op(mapping, sim, model, field, v, apply_map=False): + sim_v = mapping.deriv(model) @ v + if apply_map: + return sim.Jvec(mapping @ model, sim_v, f=field) + else: + return sim.Jvec(sim.model, sim_v, f=field) + + +def _jt_vec_op(mapping, sim, model, field, v, apply_map=False): + if apply_map: + jtv = sim.Jtvec(mapping @ model, v, f=field) + else: + jtv = sim.Jtvec(sim.model, v, f=field) + return mapping.deriv(model).T @ jtv + + +def _get_jtj_diag(mapping, sim, model, field, w, apply_map=False): + w = sp.diags(w) + if apply_map: + jtj = sim.getJtJdiag(mapping @ model, w, f=field) + else: + jtj = sim.getJtJdiag(sim.model, w, f=field) + sim_jtj = sp.diags(np.sqrt(jtj)) + m_deriv = mapping.deriv(model) + return np.asarray((sim_jtj @ m_deriv).power(2).sum(axis=0)).flatten() + + +def _reduce(client, operation, items): + while len(items) > 1: + new_reduce = client.map(operation, items[::2], items[1::2]) + if len(items) % 2 == 1: + new_reduce[-1] = client.submit(operation, new_reduce[-1], items[-1]) + items = new_reduce + return client.gather(items[0]) + + +def _validate_type_or_future_of_type( + property_name, + objects, + obj_type, + client, + workers=None, + return_workers=False, +): + try: + # validate as a list of things that need to be sent. + objects = validate_list_of_types( + property_name, objects, obj_type, ensure_unique=True + ) + if workers is None: + objects = client.scatter(objects) + else: + tmp = [] + for obj, worker in zip(objects, workers): + tmp.append(client.scatter([obj], workers=worker)[0]) + objects = tmp + except TypeError: + pass + # ensure list of futures + objects = validate_list_of_types( + property_name, + objects, + Future, + ) + # Figure out where everything lives + who = client.who_has(objects) + if workers is None: + workers = [] + for obj in objects: + workers.append(who[obj.key]) + 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: + warnings.warn( + f"{property_name} {i} is not on the expected worker.", stacklevel=2 + ) + + # Ensure this runs on the expected worker + futures = [] + for obj, worker in zip(objects, workers): + futures.append( + client.submit(lambda v: not isinstance(v, obj_type), obj, workers=worker) + ) + is_not_obj = np.array(client.gather(futures)) + if np.any(is_not_obj): + raise TypeError(f"{property_name} futures must be an instance of {obj_type}") + + if return_workers: + return objects, workers + else: + return objects + + +class DaskMetaSimulation(MetaSimulation): + """Dask Distributed version of simulation of simulations. + + This class makes use of `dask.distributed` module to provide + concurrency, executing the internal simulations in parallel. This class + is meant to be a (mostly) drop in replacement for :class:`.MetaSimulation`. + If you want to test your implementation, we recommend starting with a + small problem using `MetaSimulation`, then switching it to this class. + the serial version of this class is good for testing correctness. + + Parameters + ---------- + simulations : (n_sim) list of simpeg.simulation.BaseSimulation or list of dask.distributed.Future + The list of unique simulations (or futures that would return a simulation) + that each handle a piece of the problem. + mappings : (n_sim) list of simpeg.maps.IdentityMap or list of dask.distributed.Future + The map for every simulation (or futures that would return a map). Every + map should accept the same length model, and output a model appropriate + for its paired simulation. + client : dask.distributed.Client, optional + The dask client to use for communication. + """ + + def __init__(self, simulations, mappings, client): + self._client = validate_type("client", client, Client, cast=False) + super().__init__(simulations, mappings) + + def _make_survey(self): + survey = BaseSurvey([]) + 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 = client.gather(vnD) + survey._vnD = vnD + return survey + + @property + def simulations(self): + """The future list of simulations. + + Returns + ------- + (n_sim) list of distributed.Future simpeg.simulation.BaseSimulation + """ + return self._simulations + + @simulations.setter + def simulations(self, value): + client = self.client + simulations, workers = _validate_type_or_future_of_type( + "simulations", value, BaseSimulation, client, return_workers=True + ) + self._simulations = simulations + self._workers = workers + + @property + def mappings(self): + """The future mappings paired to each simulation. + + Every mapping should accept the same length model, and output + a model that is consistent with the simulation. + + Returns + ------- + (n_sim) list of distributed.Future simpeg.maps.IdentityMap + """ + return self._mappings + + @mappings.setter + def mappings(self, value): + client = self.client + if self._repeat_sim: + mappings, workers = _validate_type_or_future_of_type( + "mappings", value, IdentityMap, client, return_workers=True + ) + else: + workers = self._workers + if len(value) != len(self.simulations): + raise ValueError( + "Must provide the same number of mappings and simulations." + ) + mappings = _validate_type_or_future_of_type( + "mappings", value, IdentityMap, client, workers=workers + ) + + # validate mapping shapes and simulation shapes + model_len = client.submit(lambda v: v.shape[1], mappings[0]).result() + + def check_mapping(mapping, sim, model_len): + if mapping.shape[1] != model_len: + # Bad mapping model length + return 1 + map_out_shape = mapping.shape[0] + for name in sim._act_map_names: + sim_mapping = getattr(sim, name) + sim_in_shape = sim_mapping.shape[1] + if ( + map_out_shape != "*" + and sim_in_shape != "*" + and sim_in_shape != map_out_shape + ): + # Inconsistent simulation input and mapping output + return 2 + # All good + return 0 + + 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. + error_checks.append( + client.submit(check_mapping, mapping, sim, model_len, workers=worker) + ) + error_checks = np.asarray(client.gather(error_checks)) + + if np.any(error_checks == 1): + 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" are inconsistent." + ) + + self._mappings = mappings + if self._repeat_sim: + self._workers = workers + + @property + def _model_map(self): + # create a bland mapping that has the correct input shape + # to test against model inputs, avoids pulling the first + # mapping back to the main task. + if not hasattr(self, "__model_map"): + client = self.client + n_m = client.submit( + lambda v: v.shape[1], + self.mappings[0], + workers=self._workers[0], + ) + n_m = client.gather(n_m) + self.__model_map = IdentityMap(nP=n_m) + return self.__model_map + + @property + def client(self): + """The distributed client that handles the internal tasks. + + Returns + ------- + distributed.Client + """ + return self._client + + @property + def model(self): + return self._model + + @model.setter + def model(self, value): + updated = HasModel.model.fset(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) + if not self._repeat_sim: + futures = [] + for mapping, sim, worker in zip( + self.mappings, self.simulations, self._workers + ): + futures.append( + client.submit( + _store_model, + mapping, + sim, + self._m_as_future, + workers=worker, + ) + ) + self.client.gather( + futures + ) # blocking call to ensure all models were stored + + def fields(self, m): + self.model = m + client = self.client + m_future = self._m_as_future + # The above should pass the model to all the internal simulations. + f = [] + for mapping, sim, worker in zip(self.mappings, self.simulations, self._workers): + f.append( + client.submit( + _calc_fields, + mapping, + sim, + m_future, + self._repeat_sim, + workers=worker, + ) + ) + return f + + def dpred(self, m=None, f=None): + if f is None: + if m is None: + m = self.model + f = self.fields(m) + client = self.client + m_future = self._m_as_future + dpred = [] + for mapping, sim, worker, field in zip( + self.mappings, self.simulations, self._workers, f + ): + dpred.append( + client.submit( + _calc_dpred, + mapping, + sim, + m_future, + field, + self._repeat_sim, + workers=worker, + ) + ) + return np.concatenate(client.gather(dpred)) + + def Jvec(self, m, v, f=None): + self.model = m + m_future = self._m_as_future + if f is None: + f = self.fields(m) + client = self.client + [v_future] = client.scatter([v], broadcast=True) + j_vec = [] + for mapping, sim, worker, field in zip( + self.mappings, self.simulations, self._workers, f + ): + j_vec.append( + client.submit( + _j_vec_op, + mapping, + sim, + m_future, + field, + v_future, + self._repeat_sim, + workers=worker, + ) + ) + return np.concatenate(self.client.gather(j_vec)) + + def Jtvec(self, m, v, f=None): + self.model = m + m_future = self._m_as_future + if f is None: + f = self.fields(m) + jt_vec = [] + client = self.client + for i, (mapping, sim, worker, field) in enumerate( + zip(self.mappings, self.simulations, self._workers, f) + ): + jt_vec.append( + client.submit( + _jt_vec_op, + mapping, + sim, + m_future, + field, + v[self._data_offsets[i] : self._data_offsets[i + 1]], + self._repeat_sim, + workers=worker, + ) + ) + # 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) + + def getJtJdiag(self, m, W=None, f=None): + self.model = m + m_future = self._m_as_future + if getattr(self, "_jtjdiag", None) is None: + if W is None: + W = np.ones(self.survey.nD) + else: + W = W.diagonal() + jtj_diag = [] + client = self.client + if f is None: + f = self.fields(m) + for i, (mapping, sim, worker, field) in enumerate( + zip(self.mappings, self.simulations, self._workers, f) + ): + sim_w = W[self._data_offsets[i] : self._data_offsets[i + 1]] + jtj_diag.append( + client.submit( + _get_jtj_diag, + mapping, + sim, + m_future, + field, + sim_w, + self._repeat_sim, + workers=worker, + ) + ) + self._jtjdiag = _reduce(client, add, jtj_diag) + + return self._jtjdiag + + +class DaskSumMetaSimulation(DaskMetaSimulation, SumMetaSimulation): + """A dask distributed version of :class:`.SumMetaSimulation`. + + A meta simulation that sums the results of the many individual + simulations. + + Parameters + ---------- + simulations : (n_sim) list of simpeg.simulation.BaseSimulation or list of dask.distributed.Future + The list of unique simulations that each handle a piece + of the problem. + mappings : (n_sim) list of simpeg.maps.IdentityMap or list of dask.distributed.Future The map for every simulation. Every map should accept the + same length model, and output a model appropriate for its + paired simulation. + client : dask.distributed.Client, optional + The dask client to use for communication. + """ + + def __init__(self, simulations, mappings, client): + super().__init__(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() + survey._vnD = [ + n_d, + ] + return survey + + @DaskMetaSimulation.simulations.setter + def simulations(self, value): + client = self.client + 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]) + 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) + ) + if np.any(client.gather(sim_check)): + raise ValueError("All simulations must have the same number of data.") + self._simulations = simulations + self._workers = workers + + def dpred(self, m=None, f=None): + if f is None: + if m is None: + m = self.model + f = self.fields(m) + client = self.client + dpred = [] + for sim, worker, field in zip(self.simulations, self._workers, f): + dpred.append( + client.submit(_calc_dpred, None, sim, None, field, workers=worker) + ) + return _reduce(client, add, dpred) + + 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) + j_vec = [] + for mapping, sim, worker, field in zip( + self.mappings, self._simulations, self._workers, f + ): + j_vec.append( + client.submit( + _j_vec_op, + mapping, + sim, + self._m_as_future, + field, + v_future, + workers=worker, + ) + ) + return _reduce(client, add, j_vec) + + def Jtvec(self, m, v, f=None): + self.model = m + if f is None: + f = self.fields(m) + jt_vec = [] + client = self.client + for mapping, sim, worker, field in zip( + self.mappings, self._simulations, self._workers, f + ): + jt_vec.append( + client.submit( + _jt_vec_op, + mapping, + sim, + self._m_as_future, + field, + v, + workers=worker, + ) + ) + # 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) + + def getJtJdiag(self, m, W=None, f=None): + self.model = m + if getattr(self, "_jtjdiag", None) is None: + jtj_diag = [] + if W is None: + W = np.ones(self.survey.nD) + else: + W = W.diagonal() + client = self.client + if f is None: + f = self.fields(m) + for mapping, sim, worker, field in zip( + self.mappings, self._simulations, self._workers, f + ): + jtj_diag.append( + client.submit( + _get_jtj_diag, + mapping, + sim, + self._m_as_future, + field, + W, + workers=worker, + ) + ) + self._jtjdiag = _reduce(client, add, jtj_diag) + + return self._jtjdiag + + +class DaskRepeatedSimulation(DaskMetaSimulation): + """A multiprocessing version of the :class:`.RepeatedSimulation`. + + This class makes use of a single simulation that is copied to each internal + process, but only once per process. + + This simulation shares internals with the :class:`.MultiprocessingMetaSimulation`. + class, as such please see that documentation for details regarding how to properly + use multiprocessing on your operating system. + + Parameters + ---------- + simulation : simpeg.simulation.BaseSimulation or dask.distributed.Future + The simulation to use repeatedly with different mappings. + mappings : (n_sim) list of simpeg.maps.IdentityMap or list of dask.distributed.Future + The list of different mappings to use (or futures that each return a mapping). + client : dask.distributed.Client, optional + The dask client to use for communication. + """ + + _repeat_sim = True + + def __init__(self, simulation, mappings, client): + self._client = validate_type("client", client, Client, cast=False) + + self.simulation = simulation + self.mappings = mappings + + self.survey = self._make_survey() + self._data_offsets = np.cumsum(np.r_[0, self.survey.vnD]) + + def _make_survey(self): + survey = BaseSurvey([]) + nD = self.client.submit(lambda s: s.survey.nD, self.simulation).result() + survey._vnD = len(self.mappings) * [nD] + return survey + + @property + def simulations(self): + return itertools.repeat(self.simulation) + + @property + def simulation(self): + """The internal simulation. + + Returns + ------- + distributed.Future of simpeg.simulation.BaseSimulation + """ + return self._simulation + + @simulation.setter + def simulation(self, value): + client = self.client + if isinstance(value, BaseSimulation): + # Scatter sim to every client + [ + value, + ] = client.scatter([value], broadcast=True) + if not ( + isinstance(value, Future) + and client.submit(lambda s: isinstance(s, BaseSimulation), value).result() + ): + raise TypeError( + "simulation must be an instance of BaseSimulation or a Future that returns" + " a BaseSimulation" + ) + self._simulation = value diff --git a/simpeg/meta/multiprocessing.py b/simpeg/meta/multiprocessing.py new file mode 100644 index 0000000000..f5aceceda6 --- /dev/null +++ b/simpeg/meta/multiprocessing.py @@ -0,0 +1,492 @@ +from multiprocessing import Process, Queue, cpu_count +from simpeg.meta import MetaSimulation, SumMetaSimulation, RepeatedSimulation +from simpeg.props import HasModel +import uuid +import numpy as np + + +class SimpleFuture: + """Represents an object stored on a seperate simulation process.""" + + def __init__(self, item_id, t_queue, r_queue): + self.item_id = item_id + self.t_queue = t_queue + self.r_queue = r_queue + + # This doesn't quite work well yet, + # Due to the fact that some fields objects from the PDE + # classes stash the simulation, so this requires serializing + # the simulation (something we explicitly want to avoid in all cases). + # def result(self): + # self.t_queue.put(("get_item", (self.item_id,))) + # item = self.r_queue.get() + # if isinstance(item, Exception): + # raise item + # return item + + def __del__(self): + # Tell the child process that this object is no longer needed in its cache. + try: + self.t_queue.put(("del_item", (self.item_id,))) + except ValueError: + # if the queue was already closed it will throw a value error + # so catch it here gracefully and continue on. + pass + + +class _SimulationProcess(Process): + """A very simple Simulation Actor process. + + It essentially encloses a single simulation in a process that will + then respond to requests to perform operations with its simulation. + it will also cache field objects created on this process instead of + returning them to the main processes, unless explicitly asked for... + """ + + def __init__(self): + super().__init__() + self.task_queue = Queue() + self.result_queue = Queue() + + def run(self): + # everything here is local to the process + # a place to cache items locally + _cached_items = {} + + # The queues are shared between the head process and the worker processes + # We use them to communicate between the two. + t_queue = self.task_queue + r_queue = self.result_queue + while True: + # Get a task from the queue + task = t_queue.get() + if task is None: + # None is a poison pill message to kill this loop. + break + op, args = task + try: + if op == "set_sim": + (sim,) = args + sim_key = uuid.uuid4().hex + _cached_items[sim_key] = sim + r_queue.put(sim_key) + elif op == "get_item": + (key,) = args + r_queue.put(_cached_items[key]) + elif op == "del_item": + (key,) = args + _cached_items.pop(key, None) + elif op == 0: + # store_model + sim_key, m = args + sim = _cached_items[sim_key] + sim.model = m + elif op == 1: + # create fields + (sim_key,) = args + sim = _cached_items[sim_key] + f_key = uuid.uuid4().hex + r_queue.put(f_key) + fields = sim.fields(sim.model) + _cached_items[f_key] = fields + elif op == 2: + # do dpred + sim_key, f_key = args + sim = _cached_items[sim_key] + fields = _cached_items[f_key] + d_pred = sim.dpred(sim.model, fields) + r_queue.put(d_pred) + elif op == 3: + # do jvec + sim_key, v, f_key = args + sim = _cached_items[sim_key] + fields = _cached_items[f_key] + jvec = sim.Jvec(sim.model, v, fields) + r_queue.put(jvec) + elif op == 4: + # do jtvec + sim_key, v, f_key = args + sim = _cached_items[sim_key] + fields = _cached_items[f_key] + jtvec = sim.Jtvec(sim.model, v, fields) + r_queue.put(jtvec) + elif op == 5: + # do jtj_diag + sim_key, w, f_key = args + sim = _cached_items[sim_key] + fields = _cached_items[f_key] + jtj = sim.getJtJdiag(sim.model, w, fields) + r_queue.put(jtj) + except Exception as err: + r_queue.put(err) + + 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) + self._my_sim = future + return future + + def store_model(self, m): + self._check_closed() + sim = self._my_sim + self.task_queue.put((0, (sim.item_id, m))) + + def get_fields(self): + self._check_closed() + 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) + return future + + def start_dpred(self, f_future): + self._check_closed() + sim = self._my_sim + self.task_queue.put((2, (sim.item_id, f_future.item_id))) + + def start_j_vec(self, v, f_future): + self._check_closed() + sim = self._my_sim + self.task_queue.put((3, (sim.item_id, v, f_future.item_id))) + + def start_jt_vec(self, v, f_future): + self._check_closed() + sim = self._my_sim + self.task_queue.put((4, (sim.item_id, v, f_future.item_id))) + + def start_jtj_diag(self, w, f_future): + self._check_closed() + sim = self._my_sim + self.task_queue.put( + ( + 5, + ( + sim.item_id, + w, + f_future.item_id, + ), + ) + ) + + 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. + + This class makes use of the `multiprocessing` module to provide + concurrency, executing the internal simulations in parallel. This class + is meant to be a (mostly) drop in replacement for :class:`.MetaSimulation`. + If you want to test your implementation, we recommend starting with a + small problem using `MetaSimulation`, then switching it to this class. + the serial version of this class is good for testing correctness. + + If using this class, please be conscious of your operating system's + default method of spawning new processes. On Windows systems this + means that the user must be sure that this code is only executed on + the main process. Usually this is solved in your main script by + protecting your function calls by checking if you are in `__main__` + with: + + >>> from simpeg.meta import MultiprocessingMetaSimulation + >>> if __name__ == '__main__': + ... # Do processing here + ... sim = MultiprocessingMetaSimulation(...) + ... sim.dpred(model) + + 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 process. + + >>> sim.join() + + Parameters + ---------- + simulations : (n_sim) list of simpeg.simulation.BaseSimulation + The list of unique simulations that each handle a piece + of the problem. + mappings : (n_sim) list of simpeg.maps.IdentityMap + The map for every simulation. Every map should accept the + same length model, and output a model appropriate for its + paired simulation. + n_processes : optional + The number of processes to spawn internally. This will default + 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") + """ + + def __init__(self, simulations, mappings, n_processes=None): + super().__init__(simulations, mappings) + + if n_processes is None: + n_processes = cpu_count() + + # split simulation,mappings up into chunks + # (Which are currently defined using MetaSimulations) + n_sim = len(simulations) + chunk_sizes = min(n_processes, n_sim) * [n_sim // n_processes] + for i in range(n_sim % n_processes): + chunk_sizes[i] += 1 + + i_start = 0 + chunk_nd = [] + processes = [] + for chunk in chunk_sizes: + if chunk == 0: + continue + i_end = i_start + chunk + sim_chunk = MetaSimulation( + self.simulations[i_start:i_end], self.mappings[i_start:i_end] + ) + chunk_nd.append(sim_chunk.survey.nD) + p = _SimulationProcess() + processes.append(p) + p.start() + p.set_sim(sim_chunk) + i_start = i_end + + self._sim_processes = processes + self._data_offsets = np.cumsum(np.r_[0, chunk_nd]) + + @MetaSimulation.model.setter + def model(self, value): + updated = HasModel.model.fset(self, value) + # Only send the model to the internal simulations if it was updated. + if updated: + for p in self._sim_processes: + p.store_model(self._model) + + def fields(self, m): + """Create fields for every simulation. + + The returned list contains the field object from each simulation. + + Parameters + ---------- + m : array_like + The full model vector. + + Returns + ------- + (n_sim) list of SimpleFuture + The list of references to the fields stored on the separate processes. + """ + self.model = m + # The above should pass the model to all the internal simulations. + f = [] + for p in self._sim_processes: + f.append(p.get_fields()) + return f + + def dpred(self, m=None, f=None): + if f is None: + if m is None: + m = self.model + f = self.fields(m) + for p, field in zip(self._sim_processes, f): + p.start_dpred(field) + + d_pred = [] + for p in self._sim_processes: + d_pred.append(p.result()) + return np.concatenate(d_pred) + + def Jvec(self, m, v, f=None): + self.model = m + if f is None: + f = self.fields(m) + for p, field in zip(self._sim_processes, f): + p.start_j_vec(v, field) + j_vec = [] + for p in self._sim_processes: + j_vec.append(p.result()) + return np.concatenate(j_vec) + + def Jtvec(self, m, v, f=None): + self.model = m + if f is None: + f = self.fields(m) + for i, (p, field) in enumerate(zip(self._sim_processes, f)): + chunk_v = v[self._data_offsets[i] : self._data_offsets[i + 1]] + p.start_jt_vec(chunk_v, field) + + jt_vec = [] + for p in self._sim_processes: + jt_vec.append(p.result()) + return np.sum(jt_vec, axis=0) + + def getJtJdiag(self, m, W=None, f=None): + self.model = m + if getattr(self, "_jtjdiag", None) is None: + if W is None: + W = np.ones(self.survey.nD) + else: + W = W.diagonal() + if f is None: + f = self.fields(m) + for i, (p, field) in enumerate(zip(self._sim_processes, f)): + chunk_w = W[self._data_offsets[i] : self._data_offsets[i + 1]] + p.start_jtj_diag(chunk_w, field) + jtj_diag = [] + for p in self._sim_processes: + jtj_diag.append(p.result()) + self._jtjdiag = np.sum(jtj_diag, axis=0) + return self._jtjdiag + + def join(self, timeout=None): + for p in self._sim_processes: + if p.is_alive(): + p.join(timeout=timeout) + + +class MultiprocessingSumMetaSimulation( + MultiprocessingMetaSimulation, SumMetaSimulation +): + """A multiprocessing version of :class:`.SumMetaSimulation`. + + See the documentation of :class:`.MultiprocessingMetaSimulation` for + details on how to use multiprocessing for you operating system. + + Parameters + ---------- + simulations : (n_sim) list of simpeg.simulation.BaseSimulation + The list of unique simulations that each handle a piece + of the problem. + mappings : (n_sim) list of simpeg.maps.IdentityMap + The map for every simulation. Every map should accept the + same length model, and output a model appropriate for its + paired simulation. + n_processes : optional + The number of processes to spawn internally. This will default + to `multiprocessing.cpu_count()`. The number of processes spawned + will be the minimum of this number and the number of simulations. + """ + + def dpred(self, m=None, f=None): + if f is None: + if m is None: + m = self.model + f = self.fields(m) + for p, field in zip(self._sim_processes, f): + p.start_dpred(field) + + d_pred = 0 + for p in self._sim_processes: + d_pred += p.result() + return d_pred + + def Jvec(self, m, v, f=None): + self.model = m + if f is None: + f = self.fields(m) + for p, field in zip(self._sim_processes, f): + p.start_j_vec(v, field) + j_vec = [] + for p in self._sim_processes: + j_vec.append(p.result()) + return np.sum(j_vec, axis=0) + + def Jtvec(self, m, v, f=None): + self.model = m + if f is None: + f = self.fields(m) + for p, field in zip(self._sim_processes, f): + p.start_jt_vec(v, field) + + jt_vec = [] + for p in self._sim_processes: + jt_vec.append(p.result()) + return np.sum(jt_vec, axis=0) + + def getJtJdiag(self, m, W=None, f=None): + self.model = m + if getattr(self, "_jtjdiag", None) is None: + if f is None: + f = self.fields(m) + for p, field in zip(self._sim_processes, f): + p.start_jtj_diag(W, field) + jtj_diag = [] + for p in self._sim_processes: + jtj_diag.append(p.result()) + self._jtjdiag = np.sum(jtj_diag, axis=0) + return self._jtjdiag + + +class MultiprocessingRepeatedSimulation( + MultiprocessingMetaSimulation, RepeatedSimulation +): + """A multiprocessing version of the :class:`.RepeatedSimulation`. + + This class makes use of a single simulation that is copied to each internal + process, but only once per process. + + This simulation shares internals with the :class:`.MultiprocessingMetaSimulation`. + class, as such please see that documentation for details regarding how to properly + use multiprocessing on your operating system. + + Parameters + ---------- + simulation : simpeg.simulation.BaseSimulation + The simulation to use repeatedly with different mappings. + mappings : (n_sim) list of simpeg.maps.IdentityMap + The list of different mappings to use. + n_processes : optional + The number of processes to spawn internally. This will default + to `multiprocessing.cpu_count()`. The number of processes spawned + will be the minimum of this number and the number of simulations. + """ + + def __init__(self, simulation, mappings, n_processes=None): + # do this to call the initializer of the Repeated Sim + super(MultiprocessingMetaSimulation, self).__init__(simulation, mappings) + + if n_processes is None: + n_processes = cpu_count() + + # split mappings up into chunks + n_sim = len(mappings) + chunk_sizes = min(n_processes, n_sim) * [n_sim // n_processes] + for i in range(n_sim % n_processes): + chunk_sizes[i] += 1 + + processes = [] + i_start = 0 + chunk_nd = [] + for chunk in chunk_sizes: + if chunk == 0: + continue + i_end = i_start + chunk + sim_chunk = RepeatedSimulation( + self.simulation, self.mappings[i_start:i_end] + ) + chunk_nd.append(sim_chunk.survey.nD) + p = _SimulationProcess() + processes.append(p) + p.start() + p.set_sim(sim_chunk) + i_start = i_end + + self._data_offsets = np.cumsum(np.r_[0, chunk_nd]) + self._sim_processes = processes diff --git a/SimPEG/meta/simulation.py b/simpeg/meta/simulation.py similarity index 91% rename from SimPEG/meta/simulation.py rename to simpeg/meta/simulation.py index 91e53c25a3..ae9846475a 100644 --- a/SimPEG/meta/simulation.py +++ b/simpeg/meta/simulation.py @@ -26,10 +26,10 @@ class MetaSimulation(BaseSimulation): Parameters ---------- - simulations : (n_sim) list of SimPEG.simulation.BaseSimulation + simulations : (n_sim) list of simpeg.simulation.BaseSimulation The list of unique simulations that each handle a piece of the problem. - mappings : (n_sim) list of SimPEG.maps.IdentityMap + mappings : (n_sim) list of simpeg.maps.IdentityMap The map for every simulation. Every map should accept the same length model, and output a model appropriate for its paired simulation. @@ -39,9 +39,9 @@ class MetaSimulation(BaseSimulation): Create a list of 1D simulations that perform a piece of a stitched problem. - >>> from SimPEG.simulation import ExponentialSinusoidSimulation - >>> from SimPEG import maps - >>> from SimPEG.meta import MetaSimulation + >>> from simpeg.simulation import ExponentialSinusoidSimulation + >>> from simpeg import maps + >>> from simpeg.meta import MetaSimulation >>> from discretize import TensorMesh >>> import matplotlib.pyplot as plt @@ -98,11 +98,14 @@ def __init__(self, simulations, mappings): self.model = None # give myself a BaseSurvey that has the number of data equal # to the sum of the sims' data. + self.survey = self._make_survey() + self._data_offsets = np.cumsum(np.r_[0, self.survey.vnD]) + + def _make_survey(self): survey = BaseSurvey([]) vnD = [sim.survey.nD for sim in self.simulations] survey._vnD = vnD - self.survey = survey - self._data_offsets = np.cumsum(np.r_[0, vnD]) + return survey @property def simulations(self): @@ -110,7 +113,7 @@ def simulations(self): Returns ------- - (n_sim) list of SimPEG.simulation.BaseSimulation + (n_sim) list of simpeg.simulation.BaseSimulation """ return self._simulations @@ -129,7 +132,7 @@ def mappings(self): Returns ------- - (n_sim) list of SimPEG.maps.IdentityMap + (n_sim) list of simpeg.maps.IdentityMap """ return self._mappings @@ -193,6 +196,11 @@ def fields(self, m): The returned list contains the field object from each simulation. + Parameters + ---------- + m : array_like + The full model vector. + Returns ------- (n_sim) list @@ -288,7 +296,10 @@ def getJtJdiag(self, m, W=None, f=None): if W is None: W = np.ones(self.survey.nD) else: - W = W.diagonal() + try: + W = W.diagonal() + except (AttributeError, TypeError, ValueError): + pass jtj_diag = 0.0 # approximate the JtJ diag on the full model space as: # sum((diag(sqrt(jtj_diag)) @ M_deriv))**2) @@ -329,8 +340,8 @@ class SumMetaSimulation(MetaSimulation): Parameters ---------- - simulations : (n_sim) list of SimPEG.simulation.BaseSimulation - mappings : (n_sim) list of SimPEG.maps.IdentityMap + simulations : (n_sim) list of simpeg.simulation.BaseSimulation + mappings : (n_sim) list of simpeg.maps.IdentityMap """ _repeat_sim = False @@ -344,11 +355,14 @@ def __init__(self, simulations, mappings): self.mappings = mappings self.model = None # give myself a BaseSurvey + self.survey = self._make_survey() + + def _make_survey(self): survey = BaseSurvey([]) survey._vnD = [ self.simulations[0].survey.nD, ] - self.survey = survey + return survey @MetaSimulation.simulations.setter def simulations(self, value): @@ -418,8 +432,10 @@ class RepeatedSimulation(MetaSimulation): Parameters ---------- - simulation : SimPEG.simulation.BaseSimulation - mappings : (n_sim) list of SimPEG.maps.IdentityMap + simulation : simpeg.simulation.BaseSimulation + The simulation to use repeatedly with different mappings. + mappings : (n_sim) list of simpeg.maps.IdentityMap + The list of different mappings to use. """ _repeat_sim = True @@ -432,11 +448,14 @@ def __init__(self, simulation, mappings): self.simulation = simulation self.mappings = mappings self.model = None + self.survey = self._make_survey() + self._data_offsets = np.cumsum(np.r_[0, self.survey.vnD]) + + def _make_survey(self): survey = BaseSurvey([]) vnD = len(self.mappings) * [self.simulation.survey.nD] survey._vnD = vnD - self.survey = survey - self._data_offsets = np.cumsum(np.r_[0, vnD]) + return survey @property def simulations(self): @@ -448,7 +467,7 @@ def simulation(self): Returns ------- - SimPEG.simulation.BaseSimulation + simpeg.simulation.BaseSimulation """ return self._simulation diff --git a/SimPEG/models.py b/simpeg/models.py similarity index 98% rename from SimPEG/models.py rename to simpeg/models.py index 741423a035..acf92068b6 100644 --- a/SimPEG/models.py +++ b/simpeg/models.py @@ -4,7 +4,7 @@ class Model(np.ndarray): def __new__(cls, input_array, mapping=None): - assert isinstance(mapping, IdentityMap), "mapping must be a SimPEG.Mapping" + assert isinstance(mapping, IdentityMap), "mapping must be a simpeg.Mapping" assert isinstance(input_array, np.ndarray), "input_array must be a numpy array" assert len(input_array.shape) == 1, "input_array must be a 1D vector" obj = np.asarray(input_array).view(cls) diff --git a/simpeg/objective_function.py b/simpeg/objective_function.py new file mode 100644 index 0000000000..e28bec0c90 --- /dev/null +++ b/simpeg/objective_function.py @@ -0,0 +1,693 @@ +from __future__ import annotations + +import numbers +import numpy as np +import scipy.sparse as sp + +from discretize.tests import check_derivative + +from .maps import IdentityMap +from .props import BaseSimPEG +from .utils import timeIt, Zero, Identity +from .typing import RandomSeed + +__all__ = ["BaseObjectiveFunction", "ComboObjectiveFunction", "L2ObjectiveFunction"] + +VALID_MULTIPLIERS = (numbers.Number, Zero) + + +class BaseObjectiveFunction(BaseSimPEG): + """Base class for creating objective functions. + + The ``BaseObjectiveFunction`` class defines properties and methods inherited by + other classes in SimPEG that represent objective functions; e.g. regularization, data misfit. + These include convenient methods for testing the order of convergence and ajoint operations. + + .. important:: + This class is not meant to be instantiated. You should inherit from it to + create your own objective function class. + + .. important:: + If building a regularization function within SimPEG, please inherit + :py:class:`simpeg.regularization.BaseRegularization`, as this class + has additional functionality related to regularization. And if building a data misfit + function, please inherit :py:class:`simpeg.data_misfit.BaseDataMisfit`. + + Parameters + ---------- + nP : int + Number of model parameters. + mapping : simpeg.mapping.BaseMap + A SimPEG mapping object that maps from the model space to the + quantity evaluated in the objective function. + has_fields : bool + If ``True``, predicted fields for a simulation and a given model can be + used to evaluate the objective function quickly. + counter : None or simpeg.utils.Counter + Assign a SimPEG ``Counter`` object to store iterations and run-times. + debug : bool + Print debugging information. + """ + + map_class = IdentityMap #: Base class of expected maps. + + def __init__( + self, + nP=None, + mapping=None, + has_fields=False, + counter=None, + debug=False, + ): + self._nP = nP + if mapping is None: + self._mapping = mapping + else: + self.mapping = mapping + self.counter = counter + self.debug = debug + self.has_fields = has_fields + + def __call__(self, x, f=None): + """Evaluate the objective function for a given model. + + Parameters + ---------- + x : (nP) numpy.ndarray + A vector representing a set of model parameters. + f : simpeg.fields.Fields, optional + Field object (if applicable). + + """ + raise NotImplementedError( + "__call__ has not been implemented for {} yet".format( + self.__class__.__name__ + ) + ) + + @property + def nP(self): + """Number of model parameters. + + Returns + ------- + int + Number of model parameters. + """ + if self._nP is not None: + return self._nP + if getattr(self, "mapping", None) is not None: + return self.mapping.nP + return "*" + + @property + def _nC_residual(self): + """Shape of the residual.""" + if getattr(self, "mapping", None) is not None: + return self.mapping.shape[0] + else: + return self.nP + + @property + def mapping(self): + """Mapping from the model to the quantity evaluated in the object function. + + Returns + ------- + simpeg.mapping.BaseMap + The mapping from the model to the quantity evaluated in the object function. + """ + if self._mapping is None: + if self._nP is not None: + self._mapping = self.map_class(nP=self.nP) + else: + self._mapping = self.map_class() + return self._mapping + + @mapping.setter + def mapping(self, value): + if not isinstance(value, self.map_class): + raise TypeError( + f"Invalid mapping of class '{value.__class__.__name__}'. " + f"It must be an instance of {self.map_class.__name__}" + ) + self._mapping = value + + @timeIt + def deriv(self, m, **kwargs): + r"""Gradient of the objective function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the objective function, + this method evaluates and returns the derivative with respect to the model parameters; i.e. + the gradient: + + .. math:: + \frac{\partial \phi}{\partial \mathbf{m}} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the gradient is evaluated. + + Returns + ------- + (n_param, ) numpy.ndarray + The gradient of the objective function evaluated for the model provided. + """ + raise NotImplementedError( + "The method deriv has not been implemented for {}".format( + self.__class__.__name__ + ) + ) + + @timeIt + def deriv2(self, m, v=None, **kwargs): + r"""Hessian of the objective function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the objective function, + this method returns the second-derivative (Hessian) with respect to the model parameters: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} + + or the second-derivative (Hessian) multiplied by a vector :math:`(\mathbf{v})`: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \, \mathbf{v} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the Hessian is evaluated. + v : None or (n_param, ) numpy.ndarray, optional + A vector. + + Returns + ------- + (n_param, n_param) scipy.sparse.csr_matrix or (n_param, ) numpy.ndarray + If the input argument *v* is ``None``, the Hessian of the objective + function for the model provided is returned. If *v* is not ``None``, + the Hessian multiplied by the vector provided is returned. + """ + raise NotImplementedError( + "The method _deriv2 has not been implemented for {}".format( + self.__class__.__name__ + ) + ) + + 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__)) + if x is None: + rng = np.random.default_rng(seed=random_seed) + 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 + ) + + 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: + 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 * 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)], + x, + num=num, + expectedOrder=expectedOrder, + plotIt=plotIt, + **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! + + Parameters + ---------- + x : None or (n_param, ) numpy.ndarray, optional + 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 + ------- + bool + ``True`` if both tests pass. ``False`` if either test fails. + + """ + 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 + + def __add__(self, other): + if isinstance(other, Zero): + return self + if not isinstance(other, BaseObjectiveFunction): + raise TypeError( + f"Cannot add type '{other.__class__.__name__}' to an objective " + "function. Only ObjectiveFunctions can be added together." + ) + objective_functions, multipliers = [], [] + for instance in (self, other): + if isinstance(instance, ComboObjectiveFunction) and instance._unpack_on_add: + objective_functions += instance.objfcts + multipliers += instance.multipliers + else: + objective_functions.append(instance) + multipliers.append(1) + combo = ComboObjectiveFunction( + objfcts=objective_functions, multipliers=multipliers + ) + return combo + + def __radd__(self, other): + return self + other + + def __mul__(self, multiplier): + return ComboObjectiveFunction(objfcts=[self], multipliers=[multiplier]) + + def __rmul__(self, multiplier): + return self * multiplier + + def __div__(self, denominator): + return self * (1.0 / denominator) + + def __truediv__(self, denominator): + return self * (1.0 / denominator) + + def __rdiv__(self, denominator): + return self * (1.0 / denominator) + + +class ComboObjectiveFunction(BaseObjectiveFunction): + r"""Composite for multiple objective functions. + + This class allows the creation of an objective function :math:`\phi` which is the sum + of a list of other objective functions :math:`\phi_i`. Each objective function has associated with it + a multiplier :math:`c_i` such that + + .. math:: + \phi = \sum_{i = 1}^N c_i \phi_i + + Parameters + ---------- + objfcts : None or list of simpeg.objective_function.BaseObjectiveFunction, optional + List containing the objective functions that will live inside the + composite class. If ``None``, an empty list will be created. + multipliers : None or list of int, optional + List containing the multipliers for each objective function + in ``objfcts``. If ``None``, a list full of ones with the same length + as ``objfcts`` will be created. + unpack_on_add : bool + Whether to unpack the multiple objective functions when adding them to + another objective function, or to add them as a whole. + + Examples + -------- + Build a simple combo objective function: + + >>> objective_fun_a = L2ObjectiveFunction(nP=3) + >>> objective_fun_b = L2ObjectiveFunction(nP=3) + >>> combo = ComboObjectiveFunction([objective_fun_a, objective_fun_b], [1, 0.5]) + >>> print(len(combo)) + 2 + >>> print(combo.multipliers) + [1, 0.5] + + Combo objective functions are also created after adding two objective functions: + + >>> combo = 2 * objective_fun_a + 3.5 * objective_fun_b + >>> print(len(combo)) + 2 + >>> print(combo.multipliers) + [2, 3.5] + + We could add two combo objective functions as well: + + >>> objective_fun_c = L2ObjectiveFunction(nP=3) + >>> objective_fun_d = L2ObjectiveFunction(nP=3) + >>> combo_1 = 4.3 * objective_fun_a + 3 * objective_fun_b + >>> combo_2 = 1.5 * objective_fun_c + 0.5 * objective_fun_d + >>> combo = combo_1 + combo_2 + >>> print(len(combo)) + 4 + >>> print(combo.multipliers) + [4.3, 3, 1.5, 0.5] + + We can choose to not unpack the objective functions when creating the + combo. For example: + + >>> objective_fun_a = L2ObjectiveFunction(nP=3) + >>> objective_fun_b = L2ObjectiveFunction(nP=3) + >>> objective_fun_c = L2ObjectiveFunction(nP=3) + >>> + >>> # Create a ComboObjectiveFunction that won't unpack + >>> combo_1 = ComboObjectiveFunction( + ... objfcts=[objective_fun_a, objective_fun_b], + ... multipliers=[0.1, 1.2], + ... unpack_on_add=False, + ... ) + >>> combo_2 = combo_1 + objective_fun_c + >>> print(len(combo_2)) + 2 + + """ + + def __init__( + self, + objfcts: list[BaseObjectiveFunction] | None = None, + multipliers=None, + unpack_on_add=True, + ): + # Define default lists if None + if objfcts is None: + objfcts = [] + if multipliers is None: + multipliers = len(objfcts) * [1] + + # Validate inputs + _check_length_objective_funcs_multipliers(objfcts, multipliers) + _validate_objective_functions(objfcts) + for multiplier in multipliers: + _validate_multiplier(multiplier) + + # Get number of parameters (nP) from objective functions + number_of_parameters = [f.nP for f in objfcts if f.nP != "*"] + if number_of_parameters: + nP = number_of_parameters[0] + else: + nP = None + + super().__init__(nP=nP) + + self.objfcts = objfcts + self._multipliers = multipliers + self._unpack_on_add = unpack_on_add + + def __len__(self): + return len(self.multipliers) + + def __getitem__(self, key): + return self.multipliers[key], self.objfcts[key] + + @property + def multipliers(self): + r"""Multipliers for the objective functions. + + For a composite objective function :math:`\phi`, that is, a weighted sum of + objective functions :math:`\phi_i` with multipliers :math:`c_i` such that + + .. math:: + \phi = \sum_{i = 1}^N c_i \phi_i, + + this method returns the multipliers :math:`c_i` in + the same order of the ``objfcts``. + + Returns + ------- + list of int + Multipliers for the objective functions. + """ + return self._multipliers + + @multipliers.setter + def multipliers(self, value): + """Set multipliers attribute after checking if they are valid.""" + for multiplier in value: + _validate_multiplier(multiplier) + _check_length_objective_funcs_multipliers(self.objfcts, value) + self._multipliers = value + + def __call__(self, m, f=None): + """Evaluate the objective functions for a given model.""" + fct = 0.0 + for i, phi in enumerate(self): + multiplier, objfct = phi + if multiplier == 0.0: # don't evaluate the fct + continue + if f is not None and objfct.has_fields: + objective_func_value = objfct(m, f=f[i]) + else: + objective_func_value = objfct(m) + fct += multiplier * objective_func_value + return fct + + def deriv(self, m, f=None): + # Docstring inherited from BaseObjectiveFunction + g = Zero() + for i, phi in enumerate(self): + multiplier, objfct = phi + if multiplier == 0.0: # don't evaluate the fct + continue + if f is not None and objfct.has_fields: + aux = objfct.deriv(m, f=f[i]) + else: + aux = objfct.deriv(m) + if not isinstance(aux, Zero): + g += multiplier * aux + return g + + def deriv2(self, m, v=None, f=None): + # Docstring inherited from BaseObjectiveFunction + H = Zero() + for i, phi in enumerate(self): + multiplier, objfct = phi + if multiplier == 0.0: # don't evaluate the fct + continue + if f is not None and objfct.has_fields: + objfct_H = objfct.deriv2(m, v, f=f[i]) + else: + objfct_H = objfct.deriv2(m, v) + H = H + multiplier * objfct_H + return H + + # This assumes all objective functions have a W. + # The base class currently does not. + @property + def W(self): + r"""Full weighting matrix for the combo objective function. + + Consider a composite objective function :math`\phi` that is a weighted sum of + objective functions :math:`\phi_i` with multipliers :math:`c_i` such that + + .. math:: + \phi = \sum_{i = 1}^N c_i \phi_i = \sum_{i = 1}^N \frac{c_i}{2} + \big \| \mathbf{W}_i \, f_i (\mathbf{m}) \big \|^2_2 + + Where each objective function :math:`\phi_i` has a weighting matrix :math:`W_i`, + this method returns the full weighting matrix for the composite objective function: + + .. math:: + \mathbf{W} = \begin{bmatrix} + \sqrt{c_1} W_i \\ \vdots \\ \sqrt{c_N} W_N + \end{bmatrix} + + Returns + ------- + scipy.sparse.csr_matrix + Full weighting matrix for the combo objective function. + """ + W = [] + for mult, fct in self: + curW = np.sqrt(mult) * fct.W + if not isinstance(curW, Zero): + W.append(curW) + return sp.vstack(W) + + def get_functions_of_type(self, fun_class) -> list: + """Return objective functions of a given type(s). + + Parameters + ---------- + fun_class : list or simpeg.objective_function.BaseObjectiveFunction + Objective function class or list of objective function classes to return. + + Returns + ------- + list of simpeg.objective_function.BaseObjectiveFunction + Objective functions of a given type(s). + """ + target = [] + if isinstance(self, fun_class): + target += [self] + else: + for fct in self.objfcts: + if isinstance(fct, ComboObjectiveFunction): + target += [fct.get_functions_of_type(fun_class)] + elif isinstance(fct, fun_class): + target += [fct] + + return [fun for fun in target if fun] + + +class L2ObjectiveFunction(BaseObjectiveFunction): + r"""Weighted least-squares objective function class. + + Weighting least-squares objective functions in SimPEG are defined as follows: + + .. math:: + \phi = \big \| \mathbf{W} f(\mathbf{m}) \big \|_2^2 + + where :math:`\mathbf{m}` are the model parameters, :math:`f` is a mapping operator, + and :math:`\mathbf{W}` is the weighting matrix. + + Parameters + ---------- + nP : int + Number of model parameters. + mapping : simpeg.mapping.BaseMap + A SimPEG mapping object that maps from the model space to the + quantity evaluated in the objective function. + W : None or scipy.sparse.csr_matrix + The weighting matrix applied in the objective function. By default, this + is set to the identity matrix. + has_fields : bool + If ``True``, predicted fields for a simulation and a given model can be + used to evaluate the objective function quickly. + counter : None or simpeg.utils.Counter + Assign a SimPEG ``Counter`` object to store iterations and run-times. + debug : bool + Print debugging information. + """ + + def __init__( + self, + nP=None, + mapping=None, + W=None, + has_fields=False, + counter=None, + debug=False, + ): + # Check if nP and shape of W are consistent + if W is not None and nP is not None and nP != W.shape[1]: + raise ValueError( + f"Number of parameters nP ('{nP}') doesn't match the number of " + f"rows ('{W.shape[1]}') of the weights matrix W." + ) + super().__init__( + nP=nP, + mapping=mapping, + has_fields=has_fields, + debug=debug, + counter=counter, + ) + if W is not None and self.nP == "*": + self._nP = W.shape[1] + self._W = W + + @property + def W(self): + """Weighting matrix applied in the objective function. + + Returns + ------- + scipy.sparse.csr_matrix + The weighting matrix applied in the objective function. + """ + if getattr(self, "_W", None) is None: + if self._nC_residual != "*": + self._W = sp.eye(self._nC_residual) + else: + self._W = Identity() + return self._W + + def __call__(self, m): + """Evaluate the objective function for a given model.""" + r = self.W * (self.mapping * m) + return r.dot(r) + + def deriv(self, m): + # Docstring inherited from BaseObjectiveFunction + return 2 * self.mapping.deriv(m).T * (self.W.T * (self.W * (self.mapping * m))) + + def deriv2(self, m, v=None): + # Docstring inherited from BaseObjectiveFunction + if v is not None: + return ( + 2 + * self.mapping.deriv(m).T + * (self.W.T * (self.W * (self.mapping.deriv(m) * v))) + ) + W = self.W * self.mapping.deriv(m) + return 2 * W.T * W + + +def _validate_objective_functions(objective_functions): + """ + Validate objective functions. + + Check if the objective functions have the right types, and if + they all have the same number of parameters. + """ + for function in objective_functions: + if not isinstance(function, BaseObjectiveFunction): + raise TypeError( + "Unrecognized objective function type " + f"{function.__class__.__name__} in 'objfcts'. " + "All objective functions must inherit from BaseObjectiveFunction." + ) + number_of_parameters = [f.nP for f in objective_functions if f.nP != "*"] + if number_of_parameters: + all_equal = all(np.equal(number_of_parameters, number_of_parameters[0])) + if not all_equal: + np_list = [f.nP for f in objective_functions] + raise ValueError( + f"Invalid number of parameters '{np_list}' found in " + "objective functions. Except for the ones with '*', they all " + "must have the same number of parameters." + ) + + +def _validate_multiplier(multiplier): + """ + Validate multiplier. + + Check if the multiplier is of a valid type. + """ + if not isinstance(multiplier, VALID_MULTIPLIERS) or isinstance(multiplier, bool): + raise TypeError( + f"Invalid multiplier '{multiplier}' of type '{type(multiplier)}'. " + "Objective functions can only be multiplied by scalar numbers." + ) + + +def _check_length_objective_funcs_multipliers(objective_functions, multipliers): + """ + Check if objective functions and multipliers have the same length. + """ + if len(objective_functions) != len(multipliers): + raise ValueError( + "Inconsistent number of elements between objective functions " + f"('{len(objective_functions)}') and multipliers " + f"('{len(multipliers)}'). They must have the same number of parameters." + ) diff --git a/SimPEG/optimization.py b/simpeg/optimization.py similarity index 99% rename from SimPEG/optimization.py rename to simpeg/optimization.py index e8f374daae..3009387844 100644 --- a/SimPEG/optimization.py +++ b/simpeg/optimization.py @@ -262,7 +262,7 @@ class Minimize(object): comment = ( "" #: Used by some functions to indicate what is going on in the algorithm ) - counter = None #: Set this to a SimPEG.utils.Counter() if you want to count things + counter = None #: Set this to a simpeg.utils.Counter() if you want to count things parent = None #: This is the parent of the optimization routine. print_type = None @@ -946,7 +946,7 @@ def bfgsH0(self): """ Approximate Hessian used in preconditioning the problem. - Must be a SimPEG.Solver + Must be a simpeg.Solver """ if getattr(self, "_bfgsH0", None) is None: print( @@ -976,7 +976,7 @@ def bfgs(self, d): def bfgsrec(self, k, n, nn, S, Y, d): """BFGS recursion""" if k < 0: - d = self.bfgsH0 * d # Assume that bfgsH0 is a SimPEG.Solver + d = self.bfgsH0 * d # Assume that bfgsH0 is a simpeg.Solver else: khat = 0 if nn == 0 else np.mod(n - nn + k, nn) gamma = np.vdot(S[:, khat], d) / np.vdot(Y[:, khat], S[:, khat]) @@ -1035,7 +1035,7 @@ class InexactGaussNewton(BFGS, Minimize, Remember): Use *nbfgs* to set the memory limitation of BFGS. To set the initial H0 to be used in BFGS, set *bfgsH0* to be a - SimPEG.Solver + simpeg.Solver """ diff --git a/SimPEG/potential_fields/__init__.py b/simpeg/potential_fields/__init__.py similarity index 81% rename from SimPEG/potential_fields/__init__.py rename to simpeg/potential_fields/__init__.py index e46138b225..21c0a35ce1 100644 --- a/SimPEG/potential_fields/__init__.py +++ b/simpeg/potential_fields/__init__.py @@ -1,8 +1,8 @@ """ ================================================================= -Base Classes and Functions (:mod:`SimPEG.potential_fields`) +Base Classes and Functions (:mod:`simpeg.potential_fields`) ================================================================= -.. currentmodule:: SimPEG.potential_fields +.. currentmodule:: simpeg.potential_fields About ``potential_fields`` diff --git a/simpeg/potential_fields/_numba_utils.py b/simpeg/potential_fields/_numba_utils.py new file mode 100644 index 0000000000..2bdea6da2a --- /dev/null +++ b/simpeg/potential_fields/_numba_utils.py @@ -0,0 +1,43 @@ +""" +Utility functions for Numba implementations + +These functions are meant to be used both in the Numba-based gravity and +magnetic simulations. +""" + +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 diff --git a/SimPEG/potential_fields/base.py b/simpeg/potential_fields/base.py similarity index 60% rename from SimPEG/potential_fields/base.py rename to simpeg/potential_fields/base.py index a7b8532d42..0eddf36e3a 100644 --- a/SimPEG/potential_fields/base.py +++ b/simpeg/potential_fields/base.py @@ -5,11 +5,16 @@ import numpy as np from scipy.sparse import csr_matrix as csr -from SimPEG.utils import mkvc +from simpeg.utils import mkvc from ..simulation import LinearSimulation from ..utils import validate_active_indices, validate_integer, validate_string +try: + import choclo +except ImportError: + choclo = None + ############################################################################### # # # Base Potential Fields Simulation # @@ -40,7 +45,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 ----- @@ -50,7 +62,7 @@ class BasePFSimulation(LinearSimulation): Therefor you must protect the calls to this class by testing if you are in the main process with: - >>> from SimPEG.potential_fields import gravity + >>> from simpeg.potential_fields import gravity >>> if __name__ == '__main__': ... # Do your processing here ... sim = gravity.Simulation3DIntegral(n_processes=4, ...) @@ -65,6 +77,9 @@ def __init__( ind_active=None, store_sensitivities="ram", n_processes=1, + sensitivity_dtype=np.float32, + engine="geoana", + numba_parallel=True, **kwargs, ): # If deprecated property set with kwargs @@ -78,15 +93,21 @@ def __init__( "forwardOnly was removed in SimPEG 0.17.0, please set store_sensitivities=None" ) - if "forward_only" in kwargs: - raise AttributeError( - "forward_only was removed in SimPEG 0.x.0, please set store_sensitivities=None" - ) + self.store_sensitivities = store_sensitivities + self.sensitivity_dtype = sensitivity_dtype + self.engine = engine + self.numba_parallel = numba_parallel + super().__init__(mesh, **kwargs) - super().__init__(mesh, store_sensitivities=store_sensitivities, **kwargs) self.solver = None self.n_processes = n_processes + # Check sensitivity_path when engine is "choclo" + self._check_engine_and_sensitivity_path() + + # Check dimensions of the mesh when engine is "choclo" + self._check_engine_and_mesh_dimensions() + # Find non-zero cells indices if ind_active is None: ind_active = np.ones(mesh.n_cells, dtype=bool) @@ -127,6 +148,52 @@ def __init__( self._nodes = nodes[unique] # unique active nodes self._unique_inv = unique_inv.reshape(cell_nodes.T.shape) + @property + def store_sensitivities(self): + """Options for storing sensitivities. + + There are 3 options: + + - 'ram': sensitivity matrix stored in RAM + - 'disk': sensitivities written and stored to disk + - 'forward_only': sensitivities are not store (only use for forward simulation) + + Returns + ------- + {'disk', 'ram', 'forward_only'} + A string defining the model type for the simulation. + """ + if self._store_sensitivities is None: + self._store_sensitivities = "ram" + return self._store_sensitivities + + @store_sensitivities.setter + def store_sensitivities(self, value): + self._store_sensitivities = validate_string( + "store_sensitivities", value, ["disk", "ram", "forward_only"] + ) + + @property + def sensitivity_dtype(self): + """dtype of the sensitivity matrix. + + Returns + ------- + numpy.float32 or numpy.float64 + The dtype used to store the sensitivity matrix + """ + if self.store_sensitivities == "forward_only": + return np.float64 + return self._sensitivity_dtype + + @sensitivity_dtype.setter + def sensitivity_dtype(self, value): + if value is not np.float32 and value is not np.float64: + raise TypeError( + "sensitivity_dtype must be either np.float32 or np.float64." + ) + self._sensitivity_dtype = value + @property def n_processes(self): return self._n_processes @@ -137,6 +204,54 @@ def n_processes(self, value): value = validate_integer("n_processes", value, min_val=1) self._n_processes = value + @property + 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 ind_active(self): """Active topography cells. @@ -148,14 +263,6 @@ def ind_active(self): """ return self._ind_active - @property - def actInd(self): - """'actInd' is deprecated. Use 'ind_active' instead.""" - raise AttributeError( - "The 'actInd' property has been deprecated. " - "Please use 'ind_active'. This will be removed in version 0.17.0 of SimPEG.", - ) - def linear_operator(self): """Return linear operator. @@ -176,35 +283,107 @@ def linear_operator(self): print(f"Found sensitivity file at {sens_name} with expected shape") kernel = np.asarray(kernel) return kernel + if self.store_sensitivities == "forward_only": + kernel_shape = (self.survey.nD,) + else: + kernel_shape = (self.survey.nD, n_cells) + dtype = self.sensitivity_dtype + kernel = np.empty(kernel_shape, dtype=dtype) if self.n_processes == 1: - kernel = [] + id0 = 0 for args in self.survey._location_component_iterator(): - kernel.append(self.evaluate_integral(*args)) + rows = self.evaluate_integral(*args) + n_c = rows.shape[0] + id1 = id0 + n_c + kernel[id0:id1] = rows.astype(dtype, copy=False) + id0 = id1 else: # multiprocessed with Pool(processes=self.n_processes) as pool: - kernel = pool.starmap( + id0 = 0 + for rows in pool.starmap( self.evaluate_integral, self.survey._location_component_iterator() - ) - if self.store_sensitivities is not None: - kernel = np.vstack(kernel) - else: - kernel = np.concatenate(kernel) + ): + n_c = rows.shape[0] + id1 = id0 + n_c + kernel[id0:id1] = rows.astype(dtype, copy=False) + id0 = id1 + + # if self.store_sensitivities != "forward_only": + # kernel = np.vstack(kernel) + # else: + # kernel = np.concatenate(kernel) + if self.store_sensitivities == "disk": print(f"writing sensitivity to {sens_name}") os.makedirs(self.sensitivity_path, exist_ok=True) np.save(sens_name, kernel) return kernel - @property - def Jmatrix(self): + 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 _check_engine_and_mesh_dimensions(self): """ - Gravity forward operator + Check dimensions of the mesh when using choclo as engine """ - if getattr(self, "_Jmatrix", None) is None: - self._Jmatrix = self.linear_operator() + if self.engine == "choclo" and self.mesh.dim != 3: + raise ValueError( + f"Invalid mesh with {self.mesh.dim} dimensions. " + "Only 3D meshes are supported when using 'choclo' as engine." + ) + + def _get_active_nodes(self): + """ + Return locations of nodes only for active cells - return self._Jmatrix + 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 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 class BaseEquivalentSourceLayerSimulation(BasePFSimulation): diff --git a/SimPEG/potential_fields/gravity/__init__.py b/simpeg/potential_fields/gravity/__init__.py similarity index 86% rename from SimPEG/potential_fields/gravity/__init__.py rename to simpeg/potential_fields/gravity/__init__.py index ff77ec99b3..193cad2f19 100644 --- a/SimPEG/potential_fields/gravity/__init__.py +++ b/simpeg/potential_fields/gravity/__init__.py @@ -1,8 +1,8 @@ """ ======================================================================= -Gravity Simulation (:mod:`SimPEG.potential_fields.gravity`) +Gravity Simulation (:mod:`simpeg.potential_fields.gravity`) ======================================================================= -.. currentmodule:: SimPEG.potential_fields.gravity +.. currentmodule:: simpeg.potential_fields.gravity About ``gravity`` @@ -16,7 +16,7 @@ Simulation3DDifferential Survey, Sources and Receivers -============================ +============================= .. autosummary:: :toctree: generated/ @@ -35,6 +35,7 @@ analytics.GravityGradientSphereFreeSpace """ + from . import survey from . import sources from . import receivers diff --git a/simpeg/potential_fields/gravity/_numba_functions.py b/simpeg/potential_fields/gravity/_numba_functions.py new file mode 100644 index 0000000000..3eb6ae9bf1 --- /dev/null +++ b/simpeg/potential_fields/gravity/_numba_functions.py @@ -0,0 +1,199 @@ +""" +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 + +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) +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 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/analytics.py b/simpeg/potential_fields/gravity/analytics.py similarity index 98% rename from SimPEG/potential_fields/gravity/analytics.py rename to simpeg/potential_fields/gravity/analytics.py index ca86d744ba..e8da42db79 100644 --- a/SimPEG/potential_fields/gravity/analytics.py +++ b/simpeg/potential_fields/gravity/analytics.py @@ -1,5 +1,5 @@ from scipy.constants import G -from SimPEG.utils import mkvc +from simpeg.utils import mkvc import numpy as np diff --git a/SimPEG/potential_fields/gravity/receivers.py b/simpeg/potential_fields/gravity/receivers.py similarity index 83% rename from SimPEG/potential_fields/gravity/receivers.py rename to simpeg/potential_fields/gravity/receivers.py index c144800fe6..de54848cf5 100644 --- a/SimPEG/potential_fields/gravity/receivers.py +++ b/simpeg/potential_fields/gravity/receivers.py @@ -9,6 +9,20 @@ class Point(survey.BaseRx): field that are simulated at each location. The length of the resulting data vector is *n_loc X n_comp*, and is organized by location then component. + .. important:: + + Density model is assumed to be in g/cc. + + .. important:: + + Acceleration components ("gx", "gy", "gz") are returned in mgal + (:math:`10^{-5} m/s^2`). + + .. important:: + + Gradient components ("gxx", "gyy", "gzz", "gxy", "gxz", "gyz") are + returned in Eotvos (:math:`10^{-9} s^{-2}`). + Parameters ---------- locations: (n_loc, 3) numpy.ndarray @@ -28,6 +42,10 @@ class Point(survey.BaseRx): - "gyz" --> z-derivative of the y-component (and visa versa) - "gzz" --> z-derivative of the z-component - "guv" --> UV component + + See also + -------- + simpeg.potential_fields.gravity.Simulation3DIntegral """ def __init__(self, locations, components="gz", **kwargs): diff --git a/simpeg/potential_fields/gravity/simulation.py b/simpeg/potential_fields/gravity/simulation.py new file mode 100644 index 0000000000..1596b15ec2 --- /dev/null +++ b/simpeg/potential_fields/gravity/simulation.py @@ -0,0 +1,514 @@ +import warnings +import numpy as np +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 simpeg import props +from simpeg.utils import mkvc, sdiag + +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, +) + +if choclo is not None: + from numba import jit + + @jit(nopython=True) + def kernel_uv(easting, northing, upward, radius): + """Kernel for Guv gradiometry component.""" + result = 0.5 * ( + choclo.prism.kernel_nn(easting, northing, upward, radius) + - choclo.prism.kernel_ee(easting, northing, upward, radius) + ) + return result + + CHOCLO_KERNELS = { + "gx": choclo.prism.kernel_e, + "gy": choclo.prism.kernel_n, + "gz": choclo.prism.kernel_u, + "gxx": choclo.prism.kernel_ee, + "gyy": choclo.prism.kernel_nn, + "gzz": choclo.prism.kernel_uu, + "gxy": choclo.prism.kernel_en, + "gxz": choclo.prism.kernel_eu, + "gyz": choclo.prism.kernel_nu, + "guv": kernel_uv, + } + + +def _get_conversion_factor(component): + """ + Return conversion factor for the given component + """ + if component in ("gx", "gy", "gz"): + conversion_factor = 1e8 + elif component in ("gxx", "gyy", "gzz", "gxy", "gxz", "gyz", "guv"): + conversion_factor = 1e12 + else: + raise ValueError(f"Invalid component '{component}'.") + return conversion_factor + + +class Simulation3DIntegral(BasePFSimulation): + """ + Gravity simulation in integral form. + + .. important:: + + Density model is assumed to be in g/cc. + + .. important:: + + Acceleration components ("gx", "gy", "gz") are returned in mgal + (:math:`10^{-5} m/s^2`). + + .. important:: + + Gradient components ("gxx", "gyy", "gzz", "gxy", "gxz", "gyz") are + returned in Eotvos (:math:`10^{-9} s^{-2}`). + + Parameters + ---------- + mesh : discretize.TreeMesh or discretize.TensorMesh + 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 + Array that indicates which cells in ``mesh`` are active cells. + rho : numpy.ndarray, optional + Density array for the active cells in the mesh. + rhoMap : Mapping, optional + Model mapping. + 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. + """ + + rho, rhoMap, rhoDeriv = props.Invertible("Density") + + def __init__( + self, + mesh, + rho=None, + rhoMap=None, + engine="geoana", + numba_parallel=True, + **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 + + # 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 gravity simulation.", + UserWarning, + stacklevel=1, + ) + self.n_processes = None + + # Define jit functions + if self.engine == "choclo": + if self.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 fields(self, m): + """ + Forward model the gravity field of the mesh on the receivers in the survey + + Parameters + ---------- + m : (n_active_cells,) numpy.ndarray + Array with values for the model. + + Returns + ------- + (nD,) numpy.ndarray + Gravity fields generated by the given model on every receiver + location. + """ + self.model = m + if self.store_sensitivities == "forward_only": + # Compute the linear operation without forming the full dense G + if self.engine == "choclo": + fields = self._forward(self.rho) + else: + fields = mkvc(self.linear_operator()) + else: + fields = self.G @ (self.rho).astype(self.sensitivity_dtype, copy=False) + return np.asarray(fields) + + def getJtJdiag(self, m, W=None, f=None): + """ + Return the diagonal of JtJ + """ + 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)) + + def getJ(self, m, f=None): + """ + Sensitivity matrix + """ + return self.G.dot(self.rhoDeriv) + + def Jvec(self, m, v, f=None): + """ + Sensitivity times a vector + """ + 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 + """ + Jtvec = self.G.T @ v.astype(self.sensitivity_dtype, copy=False) + return np.asarray(self.rhoDeriv.T @ Jtvec) + + @property + def G(self): + """ + 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() + 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 + and for all components of the survey. + + :param numpy.ndarray receiver_location: array with shape (n_receivers, 3) + Array of receiver locations as x, y, z columns. + :param list[str] components: List of gravity components chosen from: + 'gx', 'gy', 'gz', 'gxx', 'gxy', 'gxz', 'gyy', 'gyz', 'gzz', 'guv' + :param float tolerance: Small constant to avoid singularity near nodes and edges. + :rtype numpy.ndarray: rows + :returns: ndarray with shape (n_components, n_cells) + Dense array mapping of the contribution of all active cells to data components:: + + rows = + g_1 = [g_1x g_1y g_1z] + g_2 = [g_2x g_2y g_2z] + ... + g_c = [g_cx g_cy g_cz] + + """ + dr = self._nodes - receiver_location + dx = dr[..., 0] + dy = dr[..., 1] + dz = dr[..., 2] + + node_evals = {} + if "gx" in components: + node_evals["gx"] = prism_fz(dy, dz, dx) + if "gy" in components: + node_evals["gy"] = prism_fz(dz, dx, dy) + if "gz" in components: + node_evals["gz"] = prism_fz(dx, dy, dz) + if "gxy" in components: + node_evals["gxy"] = prism_fzx(dy, dz, dx) + if "gxz" in components: + node_evals["gxz"] = prism_fzx(dx, dy, dz) + if "gyz" in components: + node_evals["gyz"] = prism_fzy(dx, dy, dz) + if "gxx" in components or "guv" in components: + node_evals["gxx"] = prism_fzz(dy, dz, dx) + if "gyy" in components or "guv" in components: + node_evals["gyy"] = prism_fzz(dz, dx, dy) + if "guv" in components: + node_evals["guv"] = (node_evals["gyy"] - node_evals["gxx"]) * 0.5 + # (NN - EE) / 2 + inside_adjust = False + if "gzz" in components: + node_evals["gzz"] = prism_fzz(dx, dy, dz) + # The below should be uncommented when we are able to give the index of a + # containing cell. + # if "gxx" not in node_evals or "gyy" not in node_evals: + # node_evals["gzz"] = prism_fzz(dx, dy, dz) + # else: + # inside_adjust = True + # # The below need to be adjusted for observation points within a cell. + # # because `gxx + gyy + gzz = -4 * pi * G * rho` + # # gzz = - gxx - gyy - 4 * np.pi * G * rho[in_cell] + # node_evals["gzz"] = -node_evals["gxx"] - node_evals["gyy"] + + rows = {} + for component in set(components): + vals = node_evals[component] + if self._unique_inv is not None: + vals = vals[self._unique_inv] + cell_vals = ( + vals[0] + - vals[1] + - vals[2] + + vals[3] + - vals[4] + + vals[5] + + vals[6] + - vals[7] + ) + if inside_adjust and component == "gzz": + # should subtract 4 * pi to the cell containing the observation point + # just need a little logic to find the containing cell + # cell_vals[inside_cell] += 4 * np.pi + pass + if self.store_sensitivities == "forward_only": + rows[component] = cell_vals @ self.rho + else: + rows[component] = cell_vals + if len(component) == 3: + rows[component] *= constants.G * 1e12 # conversion for Eotvos + else: + rows[component] *= constants.G * 1e8 # conversion for mGal + + return np.stack( + [ + rows[component].astype(self.sensitivity_dtype, copy=False) + for component in components + ] + ) + + 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. + """ + # 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 + 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): + kernel_func = CHOCLO_KERNELS[component] + conversion_factor = _get_conversion_factor(component) + vector_slice = slice( + index_offset + i, index_offset + n_elements, n_components + ) + self._forward_gravity( + receivers, + active_nodes, + densities, + fields[vector_slice], + active_cell_nodes, + kernel_func, + constants.G * conversion_factor, + ) + index_offset += n_elements + return fields + + def _sensitivity_matrix(self): + """ + Compute the sensitivity matrix G + + Returns + ------- + (nD, 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() + # 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): + kernel_func = CHOCLO_KERNELS[component] + conversion_factor = _get_conversion_factor(component) + matrix_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + self._sensitivity_gravity( + receivers, + active_nodes, + sensitivity_matrix[matrix_slice, :], + active_cell_nodes, + kernel_func, + constants.G * conversion_factor, + ) + index_offset += n_rows + return sensitivity_matrix + + +class SimulationEquivalentSourceLayer( + BaseEquivalentSourceLayerSimulation, Simulation3DIntegral +): + """ + Equivalent source layer simulations + + Parameters + ---------- + 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. + 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. + """ + + +class Simulation3DDifferential(BasePDESimulation): + r"""Finite volume simulation class for gravity. + + Notes + ----- + From Blakely (1996), the scalar potential :math:`\phi` outside the source region + is obtained by solving a Poisson's equation: + + .. math:: + \nabla^2 \phi = 4 \pi \gamma \rho + + where :math:`\gamma` is the gravitational constant and :math:`\rho` defines the + distribution of density within the source region. + + Applying the finite volumn method, we can solve the Poisson's equation on a + 3D voxel grid according to: + + .. math:: + \big [ \mathbf{D M_f D^T} \big ] \mathbf{u} = - \mathbf{M_c \, \rho} + """ + + rho, rhoMap, rhoDeriv = props.Invertible("Specific density (g/cc)") + + def __init__(self, mesh, rho=1.0, rhoMap=None, **kwargs): + super().__init__(mesh, **kwargs) + self.rho = rho + self.rhoMap = rhoMap + + self._Div = self.mesh.face_divergence + + def getRHS(self): + """Return right-hand side for the linear system""" + Mc = self.Mcc + rho = self.rho + return -Mc * rho + + def getA(self): + r""" + GetA creates and returns the A matrix for the Gravity nodal problem + + The A matrix has the form: + + .. math :: + + \mathbf{A} = \Div(\Mf Mui)^{-1}\Div^{T} + """ + # Constructs A with 0 dirichlet + if getattr(self, "_A", None) is None: + self._A = self._Div * self.Mf * self._Div.T.tocsr() + return self._A + + def fields(self, m=None): + r"""Compute fields + + **INCOMPLETE** + + Parameters + ---------- + m: (nP) np.ndarray + The model + + Returns + ------- + dict + The fields + """ + if m is not None: + self.model = m + + A = self.getA() + RHS = self.getRHS() + + Ainv = self.solver(A) + u = Ainv * RHS + + gField = 4.0 * np.pi * NewtG * 1e8 * self._Div * u + + return {"G": gField, "u": u} diff --git a/SimPEG/potential_fields/gravity/sources.py b/simpeg/potential_fields/gravity/sources.py similarity index 83% rename from SimPEG/potential_fields/gravity/sources.py rename to simpeg/potential_fields/gravity/sources.py index 93c1015cd1..3e890dc68a 100644 --- a/SimPEG/potential_fields/gravity/sources.py +++ b/simpeg/potential_fields/gravity/sources.py @@ -6,7 +6,7 @@ class SourceField(BaseSrc): Parameters ---------- - receivers_list : list of SimPEG.potential_fields.receivers.Point + receivers_list : list of simpeg.potential_fields.receivers.Point List of magnetics receivers """ diff --git a/SimPEG/potential_fields/gravity/survey.py b/simpeg/potential_fields/gravity/survey.py similarity index 96% rename from SimPEG/potential_fields/gravity/survey.py rename to simpeg/potential_fields/gravity/survey.py index 25bb07759d..2808e2489a 100644 --- a/SimPEG/potential_fields/gravity/survey.py +++ b/simpeg/potential_fields/gravity/survey.py @@ -8,8 +8,8 @@ class Survey(BaseSurvey): Parameters ---------- - source_field : SimPEG.potential_fields.magnetics.sources.SourceField - A source object that defines the Earth's inducing field + source_field : simpeg.potential_fields.gravity.sources.SourceField + A source object that defines receivers locations for gravity. """ def __init__(self, source_field, **kwargs): diff --git a/SimPEG/potential_fields/magnetics/__init__.py b/simpeg/potential_fields/magnetics/__init__.py similarity index 89% rename from SimPEG/potential_fields/magnetics/__init__.py rename to simpeg/potential_fields/magnetics/__init__.py index 0db16e066f..52612898b8 100644 --- a/SimPEG/potential_fields/magnetics/__init__.py +++ b/simpeg/potential_fields/magnetics/__init__.py @@ -1,8 +1,8 @@ """ ========================================================================= -Magnetics Simulation (:mod:`SimPEG.potential_fields.magnetics`) +Magnetics Simulation (:mod:`simpeg.potential_fields.magnetics`) ========================================================================= -.. currentmodule:: SimPEG.potential_fields.magnetics +.. currentmodule:: simpeg.potential_fields.magnetics About ``magnetics`` @@ -35,6 +35,7 @@ analytics.MagSphereAnaFunA analytics.MagSphereFreeSpace """ + from . import survey from . import sources from . import receivers diff --git a/simpeg/potential_fields/magnetics/_numba_functions.py b/simpeg/potential_fields/magnetics/_numba_functions.py new file mode 100644 index 0000000000..92a5d2eacd --- /dev/null +++ b/simpeg/potential_fields/magnetics/_numba_functions.py @@ -0,0 +1,659 @@ +""" +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 : (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) 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_nodes)`` + if ``scalar_model`` is True. + The array should have a shape of ``(n_receivers, 3 * n_active_nodes)`` + 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 _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 computing assuming that the ``model`` has + susceptibilities (scalar model) for each active cell. + If False, the forward will be computing 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] + ) + ) + + +_sensitivity_tmi_serial = jit(nopython=True, parallel=False)(_sensitivity_tmi) +_sensitivity_tmi_parallel = jit(nopython=True, parallel=True)(_sensitivity_tmi) +_forward_tmi_serial = jit(nopython=True, parallel=False)(_forward_tmi) +_forward_tmi_parallel = jit(nopython=True, parallel=True)(_forward_tmi) +_forward_mag_serial = jit(nopython=True, parallel=False)(_forward_mag) +_forward_mag_parallel = jit(nopython=True, parallel=True)(_forward_mag) +_sensitivity_mag_serial = jit(nopython=True, parallel=False)(_sensitivity_mag) +_sensitivity_mag_parallel = jit(nopython=True, parallel=True)(_sensitivity_mag) diff --git a/SimPEG/potential_fields/magnetics/analytics.py b/simpeg/potential_fields/magnetics/analytics.py similarity index 98% rename from SimPEG/potential_fields/magnetics/analytics.py rename to simpeg/potential_fields/magnetics/analytics.py index 65f8a08333..fc6f7afba2 100644 --- a/SimPEG/potential_fields/magnetics/analytics.py +++ b/simpeg/potential_fields/magnetics/analytics.py @@ -1,7 +1,7 @@ from scipy.constants import mu_0 -from SimPEG import utils +from simpeg import utils -# from SimPEG import Mesh +# from simpeg import Mesh import numpy as np @@ -120,9 +120,9 @@ def CongruousMagBC(mesh, Bo, chi): indxd, indxu, indyd, indyu, indzd, indzu = mesh.face_boundary_indices const = mu_0 / (4 * np.pi) * mom - rfun = lambda x: np.sqrt( - (x[:, 0] - xc) ** 2 + (x[:, 1] - yc) ** 2 + (x[:, 2] - zc) ** 2 - ) + + def rfun(x): + return np.sqrt((x[:, 0] - xc) ** 2 + (x[:, 1] - yc) ** 2 + (x[:, 2] - zc) ** 2) mdotrx = ( mx diff --git a/SimPEG/potential_fields/magnetics/receivers.py b/simpeg/potential_fields/magnetics/receivers.py similarity index 87% rename from SimPEG/potential_fields/magnetics/receivers.py rename to simpeg/potential_fields/magnetics/receivers.py index 8a290959c1..6f885ad635 100644 --- a/SimPEG/potential_fields/magnetics/receivers.py +++ b/simpeg/potential_fields/magnetics/receivers.py @@ -23,6 +23,9 @@ class Point(survey.BaseRx): - "byy" --> y-derivative of the y-component - "byz" --> z-derivative of the y-component (and visa versa) - "bzz" --> z-derivative of the z-component + - "tmi_x"--> x-derivative of the total magnetic intensity data + - "tmi_y"--> y-derivative of the total magnetic intensity data + - "tmi_z"--> z-derivative of the total magnetic intensity data Notes ----- @@ -51,6 +54,9 @@ def __init__(self, locations, components="tmi", **kwargs): "by", "bz", "tmi", + "tmi_x", + "tmi_y", + "tmi_z", ], ) self.components = components diff --git a/SimPEG/potential_fields/magnetics/simulation.py b/simpeg/potential_fields/magnetics/simulation.py similarity index 67% rename from SimPEG/potential_fields/magnetics/simulation.py rename to simpeg/potential_fields/magnetics/simulation.py index ec829850c2..f960e63810 100644 --- a/SimPEG/potential_fields/magnetics/simulation.py +++ b/simpeg/potential_fields/magnetics/simulation.py @@ -1,33 +1,88 @@ +import warnings import numpy as np import scipy.sparse as sp +from geoana.kernels import ( + prism_fxxy, + prism_fxxz, + prism_fxyz, + prism_fzx, + prism_fzy, + prism_fzz, + prism_fzzz, +) from scipy.constants import mu_0 -from SimPEG import utils -from ..base import BasePFSimulation, BaseEquivalentSourceLayerSimulation +from simpeg import Solver, props, utils +from simpeg.utils import mat_utils, mkvc, sdiag +from simpeg.utils.code_utils import deprecate_property, validate_string, validate_type + from ...base import BaseMagneticPDESimulation -from .survey import Survey +from ..base import BaseEquivalentSourceLayerSimulation, BasePFSimulation from .analytics import CongruousMagBC +from .survey import Survey -from SimPEG import Solver -from SimPEG import props - -from SimPEG.utils import mkvc, mat_utils, sdiag -from SimPEG.utils.code_utils import validate_string, deprecate_property, validate_type -from geoana.kernels import ( - prism_fzz, - prism_fzx, - prism_fzy, - prism_fzzz, - prism_fxxy, - prism_fxxz, - prism_fxyz, +from ._numba_functions import ( + choclo, + _sensitivity_tmi_parallel, + _sensitivity_tmi_serial, + _sensitivity_mag_parallel, + _sensitivity_mag_serial, + _forward_tmi_parallel, + _forward_tmi_serial, + _forward_mag_parallel, + _forward_mag_serial, ) +if choclo is not None: + CHOCLO_SUPPORTED_COMPONENTS = {"tmi", "bx", "by", "bz"} + 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), + } + 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. + ind_active : (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)") @@ -39,15 +94,40 @@ 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, model_map=chiMap, **kwargs) + super().__init__(mesh, engine=engine, numba_parallel=numba_parallel, **kwargs) + self.chi = chi self.chiMap = chiMap self._M = None self.is_amplitude_data = is_amplitude_data + # 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 + + if self.engine == "choclo": + if self.numba_parallel: + self._sensitivity_tmi = _sensitivity_tmi_parallel + self._sensitivity_mag = _sensitivity_mag_parallel + self._forward_tmi = _forward_tmi_parallel + self._forward_mag = _forward_mag_parallel + else: + self._sensitivity_tmi = _sensitivity_tmi_serial + self._sensitivity_mag = _sensitivity_mag_serial + self._forward_tmi = _forward_tmi_serial + self._forward_mag = _forward_mag_serial + @property def model_type(self): """Type of magnetization model @@ -101,18 +181,33 @@ def M(self, M): def fields(self, model): self.model = model # model = self.chiMap * model - if self.store_sensitivities is None: - fields = mkvc(self.linear_operator()) + if self.store_sensitivities == "forward_only": + if self.engine == "choclo": + fields = self._forward(self.chi) + else: + fields = mkvc(self.linear_operator()) else: - fields = np.asarray(self.Jmatrix @ self.chi.astype(np.float32)) + fields = np.asarray( + self.G @ self.chi.astype(self.sensitivity_dtype, copy=False) + ) if self.is_amplitude_data: fields = self.compute_amplitude(fields) return fields + @property + def G(self): + if getattr(self, "_G", None) is None: + if self.engine == "choclo": + self._G = self._sensitivity_matrix() + else: + self._G = self.linear_operator() + + return self._G + modelType = deprecate_property( - model_type, "modelType", "model_type", removal_version="0.18.0" + model_type, "modelType", "model_type", removal_version="0.18.0", error=True ) @property @@ -171,7 +266,7 @@ def Jvec(self, m, v, f=None): self.model = m dmu_dm_v = self.chiDeriv @ v - Jvec = self.Jmatrix @ dmu_dm_v.astype(np.float32) + Jvec = self.G @ dmu_dm_v.astype(self.sensitivity_dtype, copy=False) if self.is_amplitude_data: # dask doesn't support an "order" argument to reshape... @@ -188,13 +283,18 @@ def Jtvec(self, m, v, f=None): v = self.ampDeriv * v # dask doesn't support and "order" argument to reshape... v = v.T.reshape(-1) # .reshape(-1, order="F") - Jtvec = self.Jmatrix.T @ v.astype(np.float32) + + Jtvec = self.G.T @ v.astype(self.sensitivity_dtype, copy=False) + return np.asarray(self.chiDeriv.T @ Jtvec) @property def ampDeriv(self): if getattr(self, "_ampDeriv", None) is None: - fields = np.asarray(self.Jmatrix.dot(self.chi).astype(np.float32)) + fields = np.asarray( + self.G.dot(self.chi).astype(self.sensitivity_dtype, copy=False) + ) + self._ampDeriv = self.normalized_fields(fields) return self._ampDeriv @@ -231,7 +331,7 @@ def evaluate_integral(self, receiver_location, components): components: list[str] List of magnetic components chosen from: - 'bx', 'by', 'bz', 'bxx', 'bxy', 'bxz', 'byy', 'byz', 'bzz' + 'tmi', 'bx', 'by', 'bz', 'bxx', 'bxy', 'bxz', 'byy', 'byz', 'bzz', 'tmi_x', 'tmi_y', 'tmi_z' OUTPUT: Tx = [Txx Txy Txz] @@ -258,38 +358,43 @@ def evaluate_integral(self, receiver_location, components): node_evals["gxz"] = prism_fzy(dy, dz, dx) if "gyz" not in node_evals: node_evals["gyz"] = prism_fzy(dx, dy, dz) - if "gxx" not in node_evals or "gyy" not in node_evals: - node_evals["gzz"] = prism_fzz(dx, dy, dz) - else: - node_evals["gzz"] = -node_evals["gxx"] - node_evals["gyy"] - - if "bxx" in components: + node_evals["gzz"] = prism_fzz(dx, dy, dz) + # the below will be uncommented when we give the containing cell index + # for interior observations. + # if "gxx" not in node_evals or "gyy" not in node_evals: + # node_evals["gzz"] = prism_fzz(dx, dy, dz) + # else: + # # This is the one that would need to be adjusted if the observation is + # # inside an active cell. + # node_evals["gzz"] = -node_evals["gxx"] - node_evals["gyy"] + + if "bxx" in components or "tmi_x" in components: node_evals["gxxx"] = prism_fzzz(dy, dz, dx) node_evals["gxxy"] = prism_fxxy(dx, dy, dz) node_evals["gxxz"] = prism_fxxz(dx, dy, dz) - if "bxy" in components: + if "bxy" in components or "tmi_x" in components or "tmi_y" in components: if "gxxy" not in node_evals: node_evals["gxxy"] = prism_fxxy(dx, dy, dz) node_evals["gyyx"] = prism_fxxz(dy, dz, dx) node_evals["gxyz"] = prism_fxyz(dx, dy, dz) - if "bxz" in components: + if "bxz" in components or "tmi_x" in components or "tmi_z" in components: if "gxxz" not in node_evals: node_evals["gxxz"] = prism_fxxz(dx, dy, dz) if "gxyz" not in node_evals: node_evals["gxyz"] = prism_fxyz(dx, dy, dz) node_evals["gzzx"] = prism_fxxy(dz, dx, dy) - if "byy" in components: + if "byy" in components or "tmi_y" in components: if "gyyx" not in node_evals: node_evals["gyyx"] = prism_fxxz(dy, dz, dx) node_evals["gyyy"] = prism_fzzz(dz, dx, dy) node_evals["gyyz"] = prism_fxxy(dy, dz, dx) - if "byz" in components: + if "byz" in components or "tmi_y" in components or "tmi_z" in components: if "gxyz" not in node_evals: node_evals["gxyz"] = prism_fxyz(dx, dy, dz) if "gyyz" not in node_evals: node_evals["gyyz"] = prism_fxxy(dy, dz, dx) node_evals["gzzy"] = prism_fxxz(dz, dx, dy) - if "bzz" in components: + if "bzz" in components or "tmi_z" in components: if "gzzx" not in node_evals: node_evals["gzzx"] = prism_fxxy(dz, dx, dy) if "gzzy" not in node_evals: @@ -335,6 +440,57 @@ def evaluate_integral(self, receiver_location, components): + tmi[1] * node_evals["gyz"] + tmi[2] * node_evals["gzz"] ) + elif component == "tmi_x": + tmi = self.tmi_projection + vals_x = ( + tmi[0] * node_evals["gxxx"] + + tmi[1] * node_evals["gxxy"] + + tmi[2] * node_evals["gxxz"] + ) + vals_y = ( + tmi[0] * node_evals["gxxy"] + + tmi[1] * node_evals["gyyx"] + + tmi[2] * node_evals["gxyz"] + ) + vals_z = ( + tmi[0] * node_evals["gxxz"] + + tmi[1] * node_evals["gxyz"] + + tmi[2] * node_evals["gzzx"] + ) + elif component == "tmi_y": + tmi = self.tmi_projection + vals_x = ( + tmi[0] * node_evals["gxxy"] + + tmi[1] * node_evals["gyyx"] + + tmi[2] * node_evals["gxyz"] + ) + vals_y = ( + tmi[0] * node_evals["gyyx"] + + tmi[1] * node_evals["gyyy"] + + tmi[2] * node_evals["gyyz"] + ) + vals_z = ( + tmi[0] * node_evals["gxyz"] + + tmi[1] * node_evals["gyyz"] + + tmi[2] * node_evals["gzzy"] + ) + elif component == "tmi_z": + tmi = self.tmi_projection + vals_x = ( + tmi[0] * node_evals["gxxz"] + + tmi[1] * node_evals["gxyz"] + + tmi[2] * node_evals["gzzx"] + ) + vals_y = ( + tmi[0] * node_evals["gxyz"] + + tmi[1] * node_evals["gyyz"] + + tmi[2] * node_evals["gzzy"] + ) + vals_z = ( + tmi[0] * node_evals["gzzx"] + + tmi[1] * node_evals["gzzy"] + + tmi[2] * node_evals["gzzz"] + ) elif component == "bxx": vals_x = node_evals["gxxx"] vals_y = node_evals["gxxy"] @@ -405,14 +561,19 @@ def evaluate_integral(self, receiver_location, components): + cell_eval_z * M[:, 2] ) - if self.store_sensitivities is None: + if self.store_sensitivities == "forward_only": rows[component] = cell_vals @ self.chi else: rows[component] = cell_vals rows[component] /= 4 * np.pi - return np.stack([rows[component] for component in components]) + return np.stack( + [ + rows[component].astype(self.sensitivity_dtype, copy=False) + for component in components + ] + ) @property def deleteTheseOnModelUpdate(self): @@ -421,6 +582,151 @@ def deleteTheseOnModelUpdate(self): 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": + self._forward_tmi( + receivers, + active_nodes, + model, + fields[vector_slice], + active_cell_nodes, + regional_field, + constant_factor, + scalar_model, + ) + else: + kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] + self._forward_mag( + 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 + if self.model_type == "scalar": + n_columns = self.nC + else: + n_columns = 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 + 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): + matrix_slice = slice( + index_offset + i, index_offset + n_rows, n_components + ) + if component == "tmi": + self._sensitivity_tmi( + receivers, + active_nodes, + sensitivity_matrix[matrix_slice, :], + active_cell_nodes, + regional_field, + constant_factor, + scalar_model, + ) + else: + kernel_x, kernel_y, kernel_z = CHOCLO_KERNELS[component] + self._sensitivity_mag( + 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 + class SimulationEquivalentSourceLayer( BaseEquivalentSourceLayerSimulation, Simulation3DIntegral @@ -462,7 +768,7 @@ def survey(self): Returns ------- - SimPEG.potential_fields.magnetics.survey.Survey + simpeg.potential_fields.magnetics.survey.Survey """ if self._survey is None: raise AttributeError("Simulation must have a survey") @@ -923,12 +1229,12 @@ def MagneticsDiffSecondaryInv(mesh, model, data, **kwargs): Inversion module for MagneticsDiffSecondary """ - from SimPEG import ( - optimization, - regularization, + from simpeg import ( directives, - objective_function, inversion, + objective_function, + optimization, + regularization, ) prob = Simulation3DDifferential(mesh, survey=data, mu=model) diff --git a/SimPEG/potential_fields/magnetics/sources.py b/simpeg/potential_fields/magnetics/sources.py similarity index 63% rename from SimPEG/potential_fields/magnetics/sources.py rename to simpeg/potential_fields/magnetics/sources.py index 6c9d13c50a..df9027f38a 100644 --- a/SimPEG/potential_fields/magnetics/sources.py +++ b/simpeg/potential_fields/magnetics/sources.py @@ -1,6 +1,9 @@ +from __future__ import annotations 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 deprecate_class, validate_float, validate_list_of_types + +from .receivers import Point class UniformBackgroundField(BaseSrc): @@ -11,35 +14,50 @@ class UniformBackgroundField(BaseSrc): Parameters ---------- - receiver_list : list of SimPEG.potential_fields.magnetics.Point - parameters : tuple of (amplitude, inclutation, declination), optional - Deprecated input for the function, provided in this position for backwards - compatibility - 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, - inclination=90, - declination=0, - **kwargs + receiver_list: Point | list[Point] | None, + amplitude: float, + inclination: float, + declination: float, ): self.amplitude = amplitude self.inclination = inclination self.declination = declination + super().__init__(receiver_list=receiver_list) - super().__init__(receiver_list=receiver_list, **kwargs) + @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 + + @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): - """Amplitude of the inducing backgound field. + """Amplitude of the inducing background field. Returns ------- @@ -92,13 +110,13 @@ def b0(self): ) -@deprecate_class(removal_version="0.19.0", future_warn=True) +@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 + 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 diff --git a/SimPEG/potential_fields/magnetics/survey.py b/simpeg/potential_fields/magnetics/survey.py similarity index 94% rename from SimPEG/potential_fields/magnetics/survey.py rename to simpeg/potential_fields/magnetics/survey.py index beed236268..56a2c3296c 100644 --- a/SimPEG/potential_fields/magnetics/survey.py +++ b/simpeg/potential_fields/magnetics/survey.py @@ -9,7 +9,7 @@ class Survey(BaseSurvey): Parameters ---------- - source_field : SimPEG.potential_fields.magnetics.sources.SourceField + source_field : simpeg.potential_fields.magnetics.sources.UniformBackgroundField A source object that defines the Earth's inducing field """ @@ -100,4 +100,4 @@ def vnD(self): # make this look like it lives in the below module -Survey.__module__ = "SimPEG.potential_fields.magnetics" +Survey.__module__ = "simpeg.potential_fields.magnetics" diff --git a/SimPEG/props.py b/simpeg/props.py similarity index 95% rename from SimPEG/props.py rename to simpeg/props.py index 578cb2cc33..5dde99be1e 100644 --- a/SimPEG/props.py +++ b/simpeg/props.py @@ -38,7 +38,7 @@ def get_property(scope): Returns ------- - SimPEG.maps.IdentityMap + simpeg.maps.IdentityMap """ def fget(self): @@ -68,16 +68,23 @@ class PhysicalProperty: reciprocal = None def __init__( - self, short_details, mapping=None, shape=None, default=None, dtype=None + self, + short_details, + mapping=None, + shape=None, + default=None, + dtype=None, + optional=False, ): self.short_details = short_details if mapping is not None: mapping.prop = self self._mapping = mapping + self.optional = optional self.shape = shape - self.dtype = None + self.dtype = dtype @property def name(self): @@ -109,6 +116,8 @@ def get_property(scope): shape_str = "" else: shape_str = f"{scope.shape} " + if scope.optional: + shape_str = f"None or {shape_str}" dtype_str = f" of {scope.dtype}" if scope.dtype is None: dtype_str = "" @@ -130,7 +139,7 @@ def fget(self): return 1.0 / value # If I don't have a mapping if scope.mapping is None: - # I done have a reciprocal, or it doesn't have a mapping + # I dont have a reciprocal, or it doesn't have a mapping if scope.reciprocal is None: return None if scope.reciprocal.mapping is None: @@ -142,14 +151,16 @@ def fget(self): ) ) # Set by mapped reciprocal - print("returning this thing?") return 1.0 / getattr(self, scope.reciprocal.name) mapping = getattr(self, scope.mapping.name, None) if mapping is None: - raise AttributeError( - f"Neither a value for `{scope.name}` or mapping for `{scope.mapping.name}` has not been set." - ) + if scope.optional: + return None + else: + raise AttributeError( + f"Neither a value for `{scope.name}` or mapping for `{scope.mapping.name}` has not been set." + ) if self.model is None: raise AttributeError( f"A `model` is required for physical property {scope.name}" @@ -238,12 +249,13 @@ def fdel(self): return property(fget=fget, fset=fset, fdel=fdel, doc=doc) -def Invertible(property_name): +def Invertible(property_name, optional=False): mapping = Mapping(f"Mapping of the inversion model to {property_name}.") physical_property = PhysicalProperty( f"{property_name.capitalize()} physical property model.", mapping=mapping, + optional=optional, ) property_derivative = Derivative( @@ -261,6 +273,7 @@ def Reciprocal(prop1, prop2): class BaseSimPEG: """""" + def __init__(self, **kwargs): for key, value in kwargs.items(): if hasattr(self, key): diff --git a/simpeg/regularization/__init__.py b/simpeg/regularization/__init__.py new file mode 100644 index 0000000000..dbc30a5984 --- /dev/null +++ b/simpeg/regularization/__init__.py @@ -0,0 +1,258 @@ +r""" +============================================= +Regularization (:mod:`simpeg.regularization`) +============================================= + +.. currentmodule:: simpeg.regularization + +``Regularization`` classes are used to impose constraints on models recovered through geophysical +inversion. Constraints may be straight forward, such as: requiring the recovered model be +spatially smooth, or using a reference model to add a-priori information. Constraints may also +be more sophisticated; e.g. cross-validation and petrophysically-guided regularization. +In SimPEG, constraints on the recovered model can be defined using a single ``Regularization`` +object, or defined as a weighted sum of ``Regularization`` objects. + +Basic Theory +------------ + +Most geophysical inverse problems suffer from non-uniqueness; i.e. there is an infinite number +of models (:math:`m`) capable of reproducing the observed data to within a specified +degree of uncertainty. The challenge is recovering a model which 1) reproduces the observed data, +and 2) reasonably approximates the subsurface structures responsible for the observed geophysical +response. To accomplish this, regularization is used to ensure the solution to the inverse +problem is unique and is geologically plausible. The regularization applied to solve the inverse +problem depends on user assumptions and a priori information. + +SimPEG uses a deterministic inversion approach to recover an appropriate model. +The algorithm does this by finding the model (:math:`m`) which minimizes a global objective +function (or penalty function) of the form: + +.. math:: + \phi (m) = \phi_d (m) + \beta \, \phi_m (m) + +The global objective function contains two terms: a data misfit term :math:`\phi_d` which +ensures data predicted by the recovered model adequately reproduces the observed data, +and the model objective function :math:`\phi_m` which is comprised of one or more +regularization functions (objective functions) :math:`\phi_i (m)`. I.e.: + +.. math:: + \phi_m (m) = \sum_i \alpha_i \, \phi_i (m) + +The model objective function imposes all the desired constraints on the recovered model. +Constants :math:`\alpha_i` weight the relative contributions of the regularization +functions comprising the model objective function. The trade-off parameter :math:`\beta` +balances the relative contribution of the data misfit and regularization functions on the +global objective function. + +Regularization classes within SimPEG correspond to different regularization (objective) +functions that can be used individually or combined to define the model objective function +:math:`\phi_m (\mathbf{m})`. For example, a combination of regularization functions that ensures +the values in the recovered model are not too large and are spatially smooth in the x and +y-directions can be expressed as: + +.. math:: + \phi_m (m) = + \alpha_s \! \int_\Omega \Bigg [ w_s(r) \, m(r)^2 \Bigg ] \, dv + + \alpha_x \! \int_\Omega \Bigg [ w_x(r) + \bigg ( \frac{\partial m}{\partial x} \bigg )^2 \Bigg ] \, dv + + \alpha_y \! \int_\Omega \Bigg [ w_y(r) + \bigg ( \frac{\partial m}{\partial y} \bigg )^2 \Bigg ] \, dv + +where :math:`w_s(r), w_x(r), w_y(r)` are user-defined weighting functions. +For practical implementation within SimPEG, the regularization function and all its dependent +variables are discretized to a numerical grid (or mesh). The model is therefore defined as a +discrete set of model parameters :math:`\mathbf{m}`. +And the regularization is implemented using a weighted sum of objective functions: + +.. math:: + \phi_m (\mathbf{m}) \approx \alpha_s \big \| \mathbf{W_s m} \big \|^2 + + \alpha_x \big \| \mathbf{W_x G_x m} \big \|^2 + + \alpha_y \big \| \mathbf{W_y G_y m} \big \|^2 + +where :math:`\mathbf{G_x}` and :math:`\mathbf{G_y}` are partial gradient operators along the x and +y-directions, respectively. :math:`\mathbf{W_s}`, :math:`\mathbf{W_x}` and :math:`\mathbf{W_y}` +are weighting matrices that apply user-defined weights and account for cell dimensions +in the inversion mesh. + + +The API +======= + +Weighted Least Squares Regularization +------------------------------------- +Weighted least squares regularization functions are defined as weighted L2-norms on the model, +its first-order directional derivative(s), or its second-order directional derivative(s). + +.. autosummary:: + :toctree: generated/ + + WeightedLeastSquares + Smallness + SmoothnessFirstOrder + SmoothnessSecondOrder + SmoothnessFullGradient + +Sparse Norm Regularization +-------------------------- +Sparse norm regularization allows for the recovery of compact and/or blocky structures. +An iteratively re-weighted least-squares approach allows smallness and smoothness +regularization functions to be defined using norms between 0 and 2. + +.. autosummary:: + :toctree: generated/ + + Sparse + SparseSmallness + SparseSmoothness + +Vector Regularizations +---------------------- +Vector regularization allows for the recovery of vector models; that is, a model +where the parameters for each cell define directional components of a vector quantity. + +.. autosummary:: + :toctree: generated/ + + CrossReferenceRegularization + VectorAmplitude + AmplitudeSmallness + AmplitudeSmoothnessFirstOrder + +Joint Regularizations +--------------------- +Regularization functions for joint inversion involving one or more physical properties. + +.. autosummary:: + :toctree: generated/ + + CrossGradient + JointTotalVariation + PGI + PGIsmallness + LinearCorrespondence + +Base Regularization Classes +--------------------------- +Base regularization classes. Inherited by other classes and not used directly +to constrain inversions. + +.. autosummary:: + :toctree: generated/ + + RegularizationMesh + BaseRegularization + BaseSimilarityMeasure + BaseSparse + BaseVectorRegularization + BaseAmplitude + +""" + +from ..utils.code_utils import deprecate_class +from .base import ( + BaseRegularization, + WeightedLeastSquares, + BaseSimilarityMeasure, + Smallness, + SmoothnessFirstOrder, + SmoothnessSecondOrder, +) +from .regularization_mesh import RegularizationMesh +from .sparse import BaseSparse, SparseSmallness, SparseSmoothness, Sparse +from .pgi import PGIsmallness, PGI +from .cross_gradient import CrossGradient +from .correspondence import LinearCorrespondence +from .jtv import JointTotalVariation +from .vector import ( + BaseVectorRegularization, + CrossReferenceRegularization, + BaseAmplitude, + VectorAmplitude, + AmplitudeSmallness, + 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 new file mode 100644 index 0000000000..570e4727aa --- /dev/null +++ b/simpeg/regularization/_gradient.py @@ -0,0 +1,271 @@ +from .base import BaseRegularization +import numpy as np +import scipy.sparse as sp +from ..utils.code_utils import validate_ndarray_with_shape + + +class SmoothnessFullGradient(BaseRegularization): + r"""Measures the gradient of a model using optionally anisotropic weighting. + + This regularizer measures the first order smoothness in a mesh ambivalent way + by observing that the N-d smoothness operator can be represented as an + inner product with an arbitrarily anisotropic weight. + + By default it assumes uniform weighting in each dimension, which works + for most ``discretize`` mesh types. + + Parameters + ---------- + mesh : discretize.BaseMesh + The mesh object to use for regularization. The mesh should either have + 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. + 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. + ortho_check : bool, optional + Whether to check `reg_dirs` for orthogonality. + **kwargs + Keyword arguments passed to the parent class ``BaseRegularization``. + + Examples + -------- + Construct of 2D measure with uniform smoothing in each direction. + + >>> from discretize import TensorMesh + >>> from simpeg.regularization import SmoothnessFullGradient + >>> mesh = TensorMesh([32, 32]) + >>> reg = SmoothnessFullGradient(mesh) + + 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. + >>> 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). + >>> sqrt2 = np.sqrt(2) + >>> reg_dirs = np.array([ + ... [sqrt2, -sqrt2], + ... [sqrt2, sqrt2], + ... ]) + >>> reg = SmoothnessFullGradient(mesh, alphas, reg_dirs=reg_dirs) + + Notes + ----- + The regularization object is the discretized form of the continuous regularization + + ..math: + f(m) = \int_V \nabla m \cdot \mathbf{a} \nabla m \hspace{5pt} \partial 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: + + ..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. + """ + + def __init__(self, mesh, alphas=None, reg_dirs=None, ortho_check=True, **kwargs): + if mesh.dim < 2: + raise TypeError("Mesh must have dimension higher than 1") + super().__init__(mesh=mesh, **kwargs) + + if alphas is None: + edge_length = np.min(mesh.edge_lengths) + alphas = edge_length**2 * np.ones(mesh.dim) + alphas = validate_ndarray_with_shape( + "alphas", + alphas, + shape=[(mesh.dim,), ("*", mesh.dim)], + dtype=float, + ) + n_active_cells = self.regularization_mesh.n_cells + if len(alphas.shape) == 1: + alphas = np.tile(alphas, (mesh.n_cells, 1)) + if alphas.shape[0] != mesh.n_cells: + # check if I need to expand from active cells to all cells (needed for discretize) + if self.active_cells is not None and alphas.shape[0] == n_active_cells: + alpha_temp = np.zeros((mesh.n_cells, mesh.dim)) + alpha_temp[self.active_cells] = alphas + alphas = alpha_temp + else: + raise IndexError( + f"`alphas` first dimension, {alphas.shape[0]}, must be either number " + f"of active cells {n_active_cells}, or the number of mesh cells {mesh.n_cells}. " + ) + if np.any(alphas < 0): + raise ValueError("`alpha` must be non-negative") + anis_alpha = alphas + + if reg_dirs is not None: + reg_dirs = validate_ndarray_with_shape( + "reg_dirs", + reg_dirs, + shape=[(mesh.dim, mesh.dim), ("*", mesh.dim, mesh.dim)], + dtype=float, + ) + if reg_dirs.shape == (mesh.dim, mesh.dim): + reg_dirs = np.tile(reg_dirs, (mesh.n_cells, 1, 1)) + if reg_dirs.shape[0] != mesh.n_cells: + # check if I need to expand from active cells to all cells (needed for discretize) + if ( + self.active_cells is not None + and reg_dirs.shape[0] == n_active_cells + ): + reg_dirs_temp = np.zeros((mesh.n_cells, mesh.dim, mesh.dim)) + reg_dirs_temp[self.active_cells] = reg_dirs + reg_dirs = reg_dirs_temp + else: + raise IndexError( + f"`reg_dirs` first dimension, {reg_dirs.shape[0]}, must be either number " + f"of active cells {n_active_cells}, or the number of mesh cells {mesh.n_cells}. " + ) + # check orthogonality? + if ortho_check: + eye = np.eye(mesh.dim) + for i, M in enumerate(reg_dirs): + if not np.allclose(eye, M @ M.T): + raise ValueError(f"Matrix {i} is not orthonormal") + # create a stack of matrices of dir @ alphas @ dir.T + anis_alpha = np.einsum("ink,ik,imk->inm", reg_dirs, anis_alpha, reg_dirs) + # Then select the upper diagonal components for input to discretize + if mesh.dim == 2: + anis_alpha = np.stack( + ( + anis_alpha[..., 0, 0], + anis_alpha[..., 1, 1], + anis_alpha[..., 0, 1], + ), + axis=-1, + ) + elif mesh.dim == 3: + anis_alpha = np.stack( + ( + anis_alpha[..., 0, 0], + anis_alpha[..., 1, 1], + anis_alpha[..., 2, 2], + anis_alpha[..., 0, 1], + anis_alpha[..., 0, 2], + anis_alpha[..., 1, 2], + ), + axis=-1, + ) + self._anis_alpha = anis_alpha + + # overwrite the call, deriv, and deriv2... + def __call__(self, m): + G = self.cell_gradient + M_f = self.W + r = G @ (self.mapping * (self._delta_m(m))) + return r @ M_f @ r + + def deriv(self, m): + m_d = self.mapping.deriv(self._delta_m(m)) + G = self.cell_gradient + M_f = self.W + r = G @ (self.mapping * (self._delta_m(m))) + 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)) + G = self.cell_gradient + M_f = self.W + if v is None: + return 2 * (m_d.T @ (G.T @ M_f @ G) @ m_d) + + return 2 * (m_d.T @ (G.T @ (M_f @ (G @ (m_d @ v))))) + + @property + def cell_gradient(self): + """The (approximate) cell gradient operator + + Returns + ------- + scipy.sparse.csr_matrix + """ + if getattr(self, "_cell_gradient", None) is None: + mesh = self.regularization_mesh.mesh + try: + cell_gradient = mesh.cell_gradient + except AttributeError: + a = mesh.face_areas + v = mesh.average_cell_to_face @ mesh.cell_volumes + cell_gradient = sp.diags(a / v) @ mesh.stencil_cell_gradient + + v = np.ones(mesh.n_cells) + # Turn off cell_gradient at boundary faces + if self.active_cells is not None: + v[~self.active_cells] = 0 + + dv = cell_gradient @ v + P = sp.diags((np.abs(dv) <= 1e-16).astype(int)) + cell_gradient = P @ cell_gradient + if self.active_cells is not None: + cell_gradient = cell_gradient[:, self.active_cells] + self._cell_gradient = cell_gradient + return self._cell_gradient + + @property + def _weights_shapes(self): + reg_mesh = self.regularization_mesh + mesh = reg_mesh.mesh + return [(mesh.n_faces,), (reg_mesh.n_cells,)] + + @property + def W(self): + """The inner product operator using rotated coordinates + + Returns + ------- + scipy.sparse.csr_matrix + + Notes + ----- + This matrix is equivalent to `W.T @ W` in most other regularizations. It uses + `discretize` inner product operators to form the matrix `W.T @ W` all at once. + """ + if getattr(self, "_W", None) is None: + mesh = self.regularization_mesh.mesh + n_faces = mesh.n_faces + n_cells = self.regularization_mesh.n_cells + cell_weights = np.ones(n_cells) + face_weights = np.ones(n_faces) + for values in self._weights.values(): + if len(values) == n_cells: + cell_weights *= values + elif len(values) == n_faces: + face_weights *= values + else: + raise ValueError( + "Weights must be either number of active cells, or number of total faces" + ) + # optionally expand the cell weights if there are inactive cells + if n_cells != len(mesh) and self.active_cells is not None: + weights = np.zeros(mesh.n_cells) + weights[self.active_cells] = cell_weights + cell_weights = weights + reg_model = self._anis_alpha * cell_weights[:, None] + # turn off measure in inactive cells + if self.active_cells is not None: + reg_model[~self.active_cells] = 0.0 + + Wf = sp.diags(np.sqrt(face_weights)) + + W = mesh.get_face_inner_product(reg_model) + + self._W = Wf @ (W @ Wf) + return self._W diff --git a/simpeg/regularization/base.py b/simpeg/regularization/base.py new file mode 100644 index 0000000000..3165244228 --- /dev/null +++ b/simpeg/regularization/base.py @@ -0,0 +1,2300 @@ +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 + +if TYPE_CHECKING: + from scipy.sparse import csr_matrix + + +class BaseRegularization(BaseObjectiveFunction): + """Base regularization class. + + The ``BaseRegularization`` class defines properties and methods inherited by + SimPEG regularization classes, and is not directly used to construct inversions. + + Parameters + ---------- + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. This is not necessarily + the same as the mesh on which the simulation is defined. + active_cells : None, (n_cells, ) numpy.ndarray of bool + 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 set to :obj:`simpeg.maps.IdentityMap`. + reference_model : None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. + units : None, str + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. + Each value is a numpy.ndarray of shape(:py:property:`~.regularization.RegularizationMesh.n_cells`, ). + + """ + + _model = None + _parent = None + _W = None + + def __init__( + self, + mesh: RegularizationMesh | BaseMesh, + active_cells: np.ndarray | None = None, + mapping: maps.IdentityMap | None = None, + reference_model: np.ndarray | None = None, + units: str | None = None, + weights: dict | None = None, + **kwargs, + ): + if isinstance(mesh, BaseMesh): + mesh = RegularizationMesh(mesh) + + if not isinstance(mesh, RegularizationMesh): + raise TypeError( + f"'regularization_mesh' must be of type {RegularizationMesh} or {BaseMesh}. " + f"Value of type {type(mesh)} provided." + ) + if weights is not None and not isinstance(weights, dict): + raise TypeError( + f"Invalid 'weights' of type '{type(weights)}'. " + "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 = {} + if active_cells is not None: + self.active_cells = active_cells + self.mapping = mapping # Set mapping using the setter + self.reference_model = reference_model + self.units = units + if weights is not None: + self.set_weights(**weights) + + @property + def active_cells(self) -> np.ndarray: + """Active cells defined on the regularization mesh. + + A boolean array defining the cells in the :py:class:`~.regularization.RegularizationMesh` + that are active (i.e. updated) throughout the inversion. The values of inactive cells + remain equal to their starting model values. + + Returns + ------- + (n_cells, ) array of bool + + Notes + ----- + If the property is set using a ``numpy.ndarray`` of ``int``, the setter interprets the + array as representing the indices of the active cells. When called however, the quantity + will have been internally converted to a boolean array. + """ + return self.regularization_mesh.active_cells + + @active_cells.setter + def active_cells(self, values: np.ndarray | None): + self.regularization_mesh.active_cells = values + + if values is not None: + volume_term = "volume" in self._weights + self._weights = {} + self._W = 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. + + Returns + ------- + (n_param, ) numpy.ndarray + The model parameters. + """ + return self._model + + @model.setter + def model(self, values: np.ndarray | float): + if isinstance(values, float): + values = np.ones(self._nC_residual) * values + + values = validate_ndarray_with_shape( + "model", values, shape=(self._nC_residual,), dtype=float + ) + + self._model = values + + @property + def mapping(self) -> maps.IdentityMap: + """Mapping from the inversion model parameters to the regularization mesh. + + Returns + ------- + simpeg.maps.BaseMap + The mapping from the inversion model parameters to the quantity defined on the + :py:class:`~.regularization.RegularizationMesh`. + """ + return self._mapping + + @mapping.setter + def mapping(self, mapping: maps.IdentityMap): + if mapping is None: + mapping = maps.IdentityMap() + if not isinstance(mapping, maps.IdentityMap): + raise TypeError( + f"'mapping' must be of type {maps.IdentityMap}. " + f"Value of type {type(mapping)} provided." + ) + self._mapping = mapping + + @property + def parent(self): + """ + The parent objective function + """ + return self._parent + + @parent.setter + def parent(self, parent): + combo_class = ComboObjectiveFunction + if not isinstance(parent, combo_class): + raise TypeError( + f"Invalid parent of type '{parent.__class__.__name__}'. " + f"Parent must be a {combo_class.__name__}." + ) + self._parent = parent + + @property + def units(self) -> str | None: + """Units for the model parameters. + + Some regularization classes behave differently depending on the units; e.g. 'radian'. + + Returns + ------- + str + Units for the model parameters. + """ + return self._units + + @units.setter + def units(self, units: str | None): + if units is not None and not isinstance(units, str): + raise TypeError( + f"'units' must be None or type str. " + f"Value of type {type(units)} provided." + ) + self._units = units + + @property + def _weights_shapes(self) -> tuple[int] | str: + """Acceptable lengths for the weights + + Returns + ------- + list of tuple + Each tuple represents accetable shapes for the weights + """ + if ( + getattr(self, "_regularization_mesh", None) is not None + and self.regularization_mesh.nC != "*" + ): + return (self.regularization_mesh.nC,) + + if getattr(self, "_mapping", None) is not None and self.mapping.shape != "*": + return (self.mapping.shape[0],) + + return ("*",) + + @property + def reference_model(self) -> np.ndarray: + """Reference model. + + Returns + ------- + None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. + """ + return self._reference_model + + @reference_model.setter + def reference_model(self, values: np.ndarray | float): + if values is not None: + if isinstance(values, float): + values = np.ones(self._nC_residual) * values + + values = validate_ndarray_with_shape( + "reference_model", values, shape=(self._nC_residual,), dtype=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. + + Mesh on which the regularization is discretized. This is not the same as + the mesh on which the simulation is defined. See :class:`.regularization.RegularizationMesh` + + Returns + ------- + .regularization.RegularizationMesh + Mesh on which the regularization is discretized. + """ + 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. + + Returns + ------- + (n_cells, ) numpy.ndarray + Cell weights for a given key. + + Examples + -------- + >>> import discretize + >>> from simpeg.regularization import Smallness + >>> mesh = discretize.TensorMesh([2, 3, 2]) + >>> reg = Smallness(mesh) + >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) + >>> reg.get_weights('my_weight') + array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) + + """ + return self._weights[key] + + def set_weights(self, **weights): + """Adds (or updates) the specified weights to the regularization. + + Parameters + ---------- + **kwargs : key, numpy.ndarray + Each keyword argument is added to the weights used by the regularization. + They can be accessed with their keyword argument. + + Examples + -------- + >>> import discretize + >>> from simpeg.regularization import Smallness + >>> mesh = discretize.TensorMesh([2, 3, 2]) + >>> reg = Smallness(mesh) + >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) + >>> reg.get_weights('my_weight') + array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) + + """ + for key, values in weights.items(): + values = validate_ndarray_with_shape( + "weights", values, shape=self._weights_shapes, dtype=float + ) + self._weights[key] = values + self._W = None + + @property + def weights_keys(self) -> list[str]: + """ + Return the keys for the existing cell weights + """ + return list(self._weights.keys()) + + def remove_weights(self, key): + """Removes the weights for the key provided. + + Parameters + ---------- + key : str + The key for the weights being removed from the cell weights dictionary. + + Examples + -------- + >>> import discretize + >>> from simpeg.regularization import Smallness + >>> mesh = discretize.TensorMesh([2, 3, 2]) + >>> reg = Smallness(mesh) + >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) + >>> reg.get_weights('my_weight') + array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) + >>> reg.remove_weights('my_weight') + """ + try: + self._weights.pop(key) + except KeyError as error: + raise KeyError(f"{key} is not in the weights dictionary") from error + self._W = None + + @property + def W(self) -> csr_matrix: + r"""Weighting matrix. + + Returns the weighting matrix for the discrete regularization function. To see how the + weighting matrix is constructed, see the *Notes* section for the :class:`Smallness` + regularization class. + + Returns + ------- + scipy.sparse.csr_matrix + The weighting matrix applied in the regularization. + """ + if getattr(self, "_W", None) is None: + weights = np.prod(list(self._weights.values()), axis=0) + self._W = utils.sdiag(weights**0.5) + return self._W + + @property + def _nC_residual(self) -> int: + """ + Shape of the residual + """ + + nC = getattr(self.regularization_mesh, "nC", None) + mapping = getattr(self, "_mapping", None) + + if mapping is not None and mapping.shape[1] != "*": + return self.mapping.shape[1] + + if nC != "*" and nC is not None: + return self.regularization_mesh.nC + + return self._weights_shapes[0] + + def _delta_m(self, m) -> np.ndarray: + if self.reference_model is None: + return m + return ( + m - self.reference_model + ) # in case self.reference_model is Zero, returns type m + + @utils.timeIt + def __call__(self, m): + """Evaluate the regularization function for the model provided. + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the function is evaluated. + + Returns + ------- + float + The regularization function evaluated for the model provided. + """ + r = self.W * self.f_m(m) + return r.dot(r) + + def f_m(self, m) -> np.ndarray: + """Not implemented for ``BaseRegularization`` class.""" + raise AttributeError("Regularization class must have a 'f_m' implementation.") + + def f_m_deriv(self, m) -> csr_matrix: + """Not implemented for ``BaseRegularization`` class.""" + raise AttributeError( + "Regularization class must have a 'f_m_deriv' implementation." + ) + + @utils.timeIt + def deriv(self, m) -> np.ndarray: + r"""Gradient of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method evaluates and returns the derivative with respect to the model parameters; i.e. + the gradient: + + .. math:: + \frac{\partial \phi}{\partial \mathbf{m}} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the gradient is evaluated. + + Returns + ------- + (n_param, ) numpy.ndarray + The Gradient of the regularization function evaluated for the model provided. + """ + r = self.W * self.f_m(m) + return 2 * self.f_m_deriv(m).T * (self.W.T * r) + + @utils.timeIt + def deriv2(self, m, v=None) -> csr_matrix: + r"""Hessian of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method returns the second-derivative (Hessian) with respect to the model parameters: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} + + or the second-derivative (Hessian) multiplied by a vector :math:`(\mathbf{v})`: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \, \mathbf{v} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the Hessian is evaluated. + v : None, (n_param, ) numpy.ndarray (optional) + A vector. + + Returns + ------- + (n_param, n_param) scipy.sparse.csr_matrix | (n_param, ) numpy.ndarray + If the input argument *v* is ``None``, the Hessian of the regularization + function for the model provided is returned. If *v* is not ``None``, + the Hessian multiplied by the vector provided is returned. + """ + f_m_deriv = self.f_m_deriv(m) + if v is None: + return 2 * f_m_deriv.T * ((self.W.T * self.W) * f_m_deriv) + + return 2 * f_m_deriv.T * (self.W.T * (self.W * (f_m_deriv * v))) + + +class Smallness(BaseRegularization): + r"""Smallness regularization for least-squares inversion. + + ``Smallness`` regularization is used to ensure that differences between the + model values in the recovered model and the reference model are small; + i.e. it preserves structures in the reference model. If a reference model is not + supplied, the starting model will be set as the reference model in the + corresponding objective function by default. Optionally, custom cell weights can be + included to control the degree of smallness being enforced throughout different + regions the model. + + See the *Notes* section below for a comprehensive description. + + Parameters + ---------- + mesh : .regularization.RegularizationMesh + Mesh on which the regularization is discretized. Not the mesh used to + define the simulation. + active_cells : None, (n_cells, ) numpy.ndarray of bool + 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. + reference_model : None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. + units : None, str + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to + a (n_cells, ) numpy.ndarray that is defined on the + :py:class:`regularization.RegularizationMesh` . + + Notes + ----- + 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. + + 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: + + .. 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. + 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 + + where + + - :math:`\mathbf{m}^{(ref)}` is a reference model (set using `reference_model`), and + - :math:`\mathbf{W}` is the weighting matrix. + + **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: + + .. math:: + \mathbf{\tilde{w}} = \mathbf{v} \odot \prod_j \mathbf{w_j} + + where :math:`\mathbf{v}` are the cell volumes. + The weighting matrix used to apply the weights for smallness regularization is given by: + + .. math:: + \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) + + Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + + >>> reg = Smallness(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2) + + The default weights that account for cell dimensions in the regularization are accessed via: + + >>> reg.get_weights('volume') + + """ + + _multiplier_pair = "alpha_s" + + def __init__(self, mesh, **kwargs): + super().__init__(mesh, **kwargs) + self.set_weights(volume=self.regularization_mesh.vol) + + def f_m(self, m) -> np.ndarray: + r"""Evaluate the regularization kernel function. + + 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. + + Parameters + ---------- + m : numpy.ndarray + The model. + + Returns + ------- + numpy.ndarray + The regularization kernel function evaluated for the model provided. + + Notes + ----- + 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 + + 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. + + We define the regularization kernel function :math:`\mathbf{f_m}` as: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} + + such that + + .. math:: + \phi_m (\mathbf{m}) = \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 + + """ + return self.mapping * self._delta_m(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: + + .. math:: + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{I} + + where :math:`\mathbf{I}` is the identity matrix. + + Parameters + ---------- + m : numpy.ndarray + The model. + + Returns + ------- + scipy.sparse.csr_matrix + The derivative of the regularization kernel function. + + Notes + ----- + 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 + + 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. + + We define the regularization kernel function :math:`\mathbf{f_m}` as: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} + + such that + + .. math:: + \phi_m (\mathbf{m}) = \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^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. + """ + return self.mapping.deriv(self._delta_m(m)) + + +class SmoothnessFirstOrder(BaseRegularization): + r"""First-order smoothness least-squares regularization. + + ``SmoothnessFirstOrder`` regularization is used to ensure that values in the recovered + model are smooth along a specified direction. When a reference model is included, + the regularization preserves gradients/interfaces within the reference model along + the direction specified (x, y or z). Optionally, custom cell weights can be used + to control the degree of smoothness being enforced throughout different regions + the model. + + See the *Notes* section below for a comprehensive description. + + Parameters + ---------- + mesh : discretize.base.BaseMesh mesh + The mesh on which the regularization is discretized. + orientation : {'x', 'y', 'z'} + The direction along which smoothness is enforced. + active_cells : None, (n_cells, ) numpy.ndarray of bool + 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. + reference_model : None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. To include the reference model in the regularization, the + `reference_model_in_smooth` property must be set to ``True``. + reference_model_in_smooth : bool, optional + Whether to include the reference model in the smoothness regularization. + units : None, str + Units for the model parameters. Some regularization classes behave differently + depending on the units; e.g. 'radian'. + weights : None, dict + Custom weights for the least-squares function. Each ``key`` points to + a ``numpy.ndarray`` that is defined on the :py:class:`regularization.RegularizationMesh`. + A (n_cells, ) ``numpy.ndarray`` is used to define weights at cell centers, which are + averaged to the appropriate faces internally when weighting is applied. + A (n_faces, ) ``numpy.ndarray`` is used to define weights directly on the faces specified + by the `orientation` input argument. + + Notes + ----- + We define the regularization function (objective function) for first-order smoothness + 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. + + 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: + + .. 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. + This is equivalent to an objective function of the form: + + .. math:: + \phi (\mathbf{m}) = \Big \| \mathbf{W \, G_x m } \, \Big \|^2 + + where + + - :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction, and + - :math:`\mathbf{W}` is the weighting matrix. + + 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. + 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``. + + **Custom weights and the weighting matrix:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of + custom weights defined on the faces specified by the `orientation` property; i.e. x-faces for + smoothness along the x-direction. Each set of weights were either defined directly on the + faces or have been averaged from cell centers. + + The weighting applied within the objective function is given by: + + .. math:: + \mathbf{\tilde{w}} = \mathbf{v_x} \odot \prod_j \mathbf{w_j} + + where :math:`\mathbf{v_x}` are cell volumes projected to x-faces. + The weighting matrix is given by: + + .. math:: + \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) + + Each set of custom weights is stored within a ``dict`` as an ``numpy.ndarray``. + A (n_cells, ) ``numpy.ndarray`` is used to define weights at cell centers, which are + averaged to the appropriate faces internally when weighting is applied. + A (n_faces, ) ``numpy.ndarray`` is used to define weights directly on the faces specified + by the `orientation` input argument. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + + >>> array_1 = np.ones(mesh.n_cells) # weights at cell centers + >>> array_2 = np.ones(mesh.n_faces_x) # weights directly on x-faces + >>> reg = SmoothnessFirstOrder( + >>> mesh, orientation='x', weights={'weights_1': array_1, 'weights_2': array_2} + >>> ) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2) + + The default weights that account for cell dimensions in the regularization are accessed via: + + >>> reg.get_weights('volume') + + """ + + def __init__( + self, mesh, orientation="x", reference_model_in_smooth=False, **kwargs + ): + self.reference_model_in_smooth = reference_model_in_smooth + + if orientation not in ["x", "y", "z"]: + raise ValueError("Orientation must be 'x', 'y' or 'z'") + + if orientation == "y" and mesh.dim < 2: + raise ValueError( + "Mesh must have at least 2 dimensions to regularize along the " + "y-direction." + ) + elif orientation == "z" and mesh.dim < 3: + raise ValueError( + "Mesh must have at least 3 dimensions to regularize along the " + "z-direction" + ) + self._orientation = orientation + + super().__init__(mesh=mesh, **kwargs) + self.set_weights(volume=self.regularization_mesh.vol) + + @property + def _weights_shapes(self): + """Acceptable lengths for the weights. + + Returns + ------- + tuple + A tuple of each acceptable lengths for the weights + """ + n_active_f, n_active_c = getattr( + self.regularization_mesh, "aveCC2F{}".format(self.orientation) + ).shape + return [(n_active_f,), (n_active_c,)] + + @property + def cell_gradient(self): + """Partial cell gradient operator. + + Returns the partial gradient operator which takes the derivative along the + orientation where smoothness is being enforced. For smoothness along the + x-direction, the resulting operator would map from cell centers to x-faces. + + Returns + ------- + scipy.sparse.csr_matrix + Partial cell gradient operator defined on the + :py:class:`.regularization.RegularizationMesh`. + """ + if getattr(self, "_cell_gradient", None) is None: + self._cell_gradient = getattr( + self.regularization_mesh, "cell_gradient_{}".format(self.orientation) + ) + return self._cell_gradient + + @property + def reference_model_in_smooth(self) -> bool: + # Inherited from BaseRegularization class + return self._reference_model_in_smooth + + @reference_model_in_smooth.setter + def reference_model_in_smooth(self, value: bool): + if not isinstance(value, bool): + raise TypeError( + "'reference_model_in_smooth must be of type 'bool'. " + f"Value of type {type(value)} provided." + ) + self._reference_model_in_smooth = value + + def _delta_m(self, m): + if self.reference_model is None or not self.reference_model_in_smooth: + return m + return m - self.reference_model + + @property + def _multiplier_pair(self): + return f"alpha_{self.orientation}" + + def f_m(self, m): + r"""Evaluate the regularization kernel function. + + For first-order smoothness regularization in the x-direction, + 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). + Similarly for smoothness along y and z. + + Parameters + ---------- + m : numpy.ndarray + The model. + + Returns + ------- + numpy.ndarray + The regularization kernel function. + + Notes + ----- + The objective function for first-order smoothness regularization along the x-direction + is given by: + + .. math:: + \phi_m (\mathbf{m}) = + \Big \| \mathbf{W G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^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. + 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 + + .. math:: + \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^2 + """ + dfm_dl = self.mapping * self._delta_m(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: + + .. math:: + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{G_x} + + where :math:`\mathbf{G_x}` is the partial cell gradient operator along x + (i.e. the x-derivative). + + Parameters + ---------- + m : numpy.ndarray + The model. + + Returns + ------- + scipy.sparse.csr_matrix + The derivative of the regularization kernel function. + + Notes + ----- + The objective function for first-order smoothness regularization along the x-direction + is given by: + + .. math:: + \phi_m (\mathbf{m}) = + \Big \| \mathbf{W G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^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. + 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 + + .. math:: + \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^2 + + The derivative with respect to the model is therefore: + + .. math:: + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{G_x} + """ + return self.cell_gradient @ self.mapping.deriv(self._delta_m(m)) + + @property + def W(self) -> csr_matrix: + r"""Weighting matrix. + + Returns the weighting matrix for the objective function. To see how the + weighting matrix is constructed, see the *Notes* section for the + :class:`SmoothnessFirstOrder` regularization class. + + Returns + ------- + scipy.sparse.csr_matrix + The weighting matrix applied in the objective function. + """ + if getattr(self, "_W", None) is None: + average_cell_2_face = getattr( + self.regularization_mesh, "aveCC2F{}".format(self.orientation) + ) + weights = 1.0 + for values in self._weights.values(): + if values.shape[0] == self.regularization_mesh.nC: + values = average_cell_2_face * values + weights *= values + self._W = utils.sdiag(weights**0.5) + return self._W + + @property + def _cell_distances(self): + """ + Distances between cell centers for the cell center difference. + """ + return getattr(self.regularization_mesh, f"cell_distances_{self.orientation}") + + @property + def orientation(self): + """Direction along which smoothness is enforced. + + Returns + ------- + {'x','y','z'} + The direction along which smoothness is enforced. + + """ + return self._orientation + + +class SmoothnessSecondOrder(SmoothnessFirstOrder): + r"""Second-order smoothness (flatness) least-squares regularization. + + ``SmoothnessSecondOrder`` regularization is used to ensure that values in the recovered + model have small second-order spatial derivatives. When a reference model is included, + the regularization preserves second-order smoothness within the reference model along + the direction specified (x, y or z). Optionally, custom cell weights can be used + to control the degree of smoothness being enforced throughout different regions + the model. + + See the *Notes* section below for a comprehensive description. + + Parameters + ---------- + mesh : discretize.base.BaseMesh mesh + The mesh on which the regularization is discretized. + orientation : {'x', 'y', 'z'} + The direction along which smoothness is enforced. + active_cells : None, (n_cells, ) numpy.ndarray of bool + 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. + reference_model : None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. To include the reference model in the regularization, the + `reference_model_in_smooth` property must be set to ``True``. + reference_model_in_smooth : bool, optional + Whether to include the reference model in the smoothness regularization. + units : None, str + Units for the model parameters. Some regularization classes behave differently + depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to + a (n_cells, ) numpy.ndarray that is defined on the + :py:class:`regularization.RegularizationMesh`. + + Notes + ----- + We define the regularization function (objective function) for second-order + 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. + + 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: + + .. 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. + This is equivalent to an objective function of the form: + + .. math:: + \phi (\mathbf{m}) = \big \| \mathbf{W \, L_x \, m } \, \big \|^2 + + where + + - :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. + 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``. + + **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: + + .. math:: + \mathbf{\tilde{w}} = \mathbf{v} \odot \prod_j \mathbf{w_j} + + where :math:`\mathbf{v}` are the cell volumes. + The weighting matrix used to apply the weights is given by: + + .. math:: + \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) + + Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + + >>> reg = SmoothnessSecondOrder(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) + + """ + + def f_m(self, m): + r"""Evaluate the regularization kernel function. + + For second-order smoothness regularization in the x-direction, + 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. + + Parameters + ---------- + m : numpy.ndarray + The model. + + Returns + ------- + numpy.ndarray + The regularization kernel function. + + Notes + ----- + The objective function for second-order smoothness regularization along the x-direction + is given by: + + .. math:: + \phi_m (\mathbf{m}) = + \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^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. + 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 ] + + such that + + .. math:: + \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^2 + """ + dfm_dl = self.mapping * self._delta_m(m) + + if self.units is not None and self.units.lower() == "radian": + return self.cell_gradient.T @ ( + utils.mat_utils.coterminal(self.cell_gradient.sign() @ dfm_dl) + / self.length_scales + ) + + dfm_dl2 = self.cell_gradient @ dfm_dl + + return self.cell_gradient.T @ dfm_dl2 + + def f_m_deriv(self, m) -> csr_matrix: + r"""Derivative of the regularization kernel function. + + For second-order smoothness 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{L_x} + + where :math:`\mathbf{L_x}` is the second-order derivative operator with respect to x. + + Parameters + ---------- + m : numpy.ndarray + The model. + + Returns + ------- + scipy.sparse.csr_matrix + The derivative of the regularization kernel function. + + Notes + ----- + The objective function for second-order smoothness regularization along the x-direction + is given by: + + .. math:: + \phi_m (\mathbf{m}) = + \Big \| \mathbf{W L_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^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. + 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 ] + + such that + + .. math:: + \phi_m (\mathbf{m}) = \Big \| \mathbf{W \, f_m} \Big \|^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} + """ + return ( + self.cell_gradient.T + @ self.cell_gradient + @ self.mapping.deriv(self._delta_m(m)) + ) + + @property + def W(self) -> csr_matrix: + r"""Weighting matrix. + + Returns the weighting matrix for the objective function. To see how the + weighting matrix is constructed, see the *Notes* section for the + :class:`SmoothnessSecondOrder` regularization class. + + Returns + ------- + scipy.sparse.csr_matrix + The weighting matrix applied in the objective function. + """ + if getattr(self, "_W", None) is None: + weights = np.prod(list(self._weights.values()), axis=0) + self._W = utils.sdiag(weights**0.5) + + return self._W + + @property + def _multiplier_pair(self): + return f"alpha_{self.orientation}{self.orientation}" + + +############################################################################### +# # +# Base Combo Regularization # +# # +############################################################################### + + +class WeightedLeastSquares(ComboObjectiveFunction): + r"""Weighted least-squares regularization using smallness and smoothness. + + Apply regularization using a weighted sum of :class:`Smallness`, :class:`SmoothnessFirstOrder`, + and/or :class:`SmoothnessSecondOrder` (optional) least-squares regularization functions. + ``Smallness`` regularization is used to ensure that values in the recovered model, + or differences between the recovered model and a reference model, are not overly + large in magnitude. ``Smoothness`` regularization is used to ensure that values in the + recovered model are smooth along specified directions. When a reference model + is included in the smoothness regularization, the inversion preserves + gradients/interfaces within the reference model. Custom weights can also be supplied + to control the degree of smallness and smoothness being + enforced throughout different regions the model. + + See the *Notes* section below for a comprehensive description. + + Parameters + ---------- + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. This is not necessarily + the same as the mesh on which the simulation is defined. + active_cells : None, (n_cells, ) numpy.ndarray of bool + 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. + reference_model : None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. + reference_model_in_smooth : bool, optional + Whether to include the reference model in the smoothness terms. + units : None, str + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) + numpy.ndarray that is defined on the :py:class:`~.regularization.RegularizationMesh`. + alpha_s : float, optional + Scaling constant for the smallness regularization term. + alpha_x, alpha_y, alpha_z : float or None, optional + Scaling constants for the first order smoothness along x, y and z, respectively. + If set to ``None``, the scaling constant is set automatically according to the + value of the `length_scale` parameter. + alpha_xx, alpha_yy, alpha_zz : 0, float + Scaling constants for the second order smoothness along x, y and z, respectively. + If set to ``None``, the scaling constant is set automatically according to the + value of the `length_scale` parameter. + length_scale_x, length_scale_y, length_scale_z : float, optional + First order smoothness length scales for the respective dimensions. + + Notes + ----- + Weighted least-squares regularization can be defined by a weighted sum of + :class:`Smallness`, :class:`SmoothnessFirstOrder` and :class:`SmoothnessSecondOrder` + regularization functions. This corresponds to a model objective function + :math:`\phi_m (m)` of the form: + + .. math:: + \phi_m (m) =& \alpha_s \int_\Omega \, w(r) + \Big [ m(r) - m^{(ref)}(r) \Big ]^2 \, dv \\ + &+ \sum_{j=x,y,z} \alpha_j \int_\Omega \, w(r) + \bigg [ \frac{\partial m}{\partial \xi_j} \bigg ]^2 \, dv \\ + &+ \sum_{j=x,y,z} \alpha_{jj} \int_\Omega \, w(r) + \bigg [ \frac{\partial^2 m}{\partial \xi_j^2} \bigg ]^2 \, dv + \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) + + 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. :math:`\xi_j` is the unit direction along :math:`j`. + Parameters :math:`\alpha_s`, :math:`\alpha_j` and :math:`\alpha_{jj}` for :math:`j=x,y,z` + are multiplier constants which weight the respective contributions of the smallness and + smoothness terms towards the regularization. + + For implementation within SimPEG, the regularization functions and their variables + must be discretized onto a `mesh`. For a continuous variable :math:`x(r)` whose + discrete representation on the mesh is given by :math:`\mathbf{x}`, we approximate + as follows: + + .. math:: + \int_\Omega w(r) \big [ x(r) \big ]^2 \, dv \approx \sum_i \tilde{w}_i \, | x_i |^2 + + where :math:`\tilde{w}_i` are amalgamated weighting constants that account for cell dimensions + in the discretization and apply user-defined weighting. Using the above approximation, + the ``WeightedLeastSquares`` regularization can be expressed as a weighted sum of + objective functions of the form: + + .. math:: + \phi_m (\mathbf{m}) =& \alpha_s + \Big \| \mathbf{W_s} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \\ + &+ \sum_{j=x,y,z} \alpha_j \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ + &+ \sum_{j=x,y,z} \alpha_{jj} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 + \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) + + where + + - :math:`\mathbf{m}` are the set of discrete model parameters (i.e. the model), + - :math:`\mathbf{m}^{(ref)}` is the reference model, + - :math:`\mathbf{G_x, \, G_y, \; G_z}` are partial cell gradients operators along x, y and z, + - :math:`\mathbf{L_x, \, L_y, \; L_z}` are second-order derivative operators with respect to x, y and z, + - :math:`\mathbf{W_s, \, W_x, \, W_y, \; W_z}` are weighting matrices. + + **Reference model in smoothness:** + + Gradients/interfaces within a discrete reference model :math:`\mathbf{m}^{(ref)}` can be + preserved by including the reference model the smoothness regularization. + In this case, the objective function becomes: + + .. math:: + \phi_m (\mathbf{m}) =& \alpha_s + \Big \| \mathbf{W_s} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \\ + &+ \sum_{j=x,y,z} \alpha_j \Big \| \mathbf{W_j G_j} + \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 \\ + &+ \sum_{j=x,y,z} \alpha_{jj} \Big \| \mathbf{W_{jj} L_j} + \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) + + This functionality is used by setting the `reference_model_in_smooth` parameter + to ``True``. + + **Alphas and length scales:** + + The :math:`\alpha` parameters scale the relative contributions of the smallness and smoothness + terms in the model objective function. Each :math:`\alpha` parameter can be set directly as a + appropriate property of the ``WeightedLeastSquares`` class; e.g. :math:`\alpha_x` is set + using the `alpha_x` property. Note that unless the parameters are set manually, second-order + smoothness is not included in the model objective function. That is, the `alpha_xx`, `alpha_yy` + and `alpha_zz` parameters are set to 0 by default. + + The relative contributions of smallness and smoothness terms on the recovered model can also + be set by leaving `alpha_s` as its default value of 1, and setting the smoothness scaling + constants based on length scales. The model objective function has been formulated such that + smallness and smoothness terms contribute equally when the length scales are equal; i.e. when + properties `length_scale_x = length_scale_y = length_scale_z`. When the `length_scale_x` + property is set, the `alpha_x` and `alpha_xx` properties are set internally as: + + >>> reg.alpha_x = (reg.length_scale_x * reg.regularization_mesh.base_length) ** 2.0 + + and + + >>> reg.alpha_xx = (ref.length_scale_x * reg.regularization_mesh.base_length) ** 4.0 + + Likewise for y and z. + + **Custom weights and weighting matrices:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of custom + cell weights that are applied to all terms in the model objective function. + The general form for the weights applied to smallness and second-order smoothness terms + is given by: + + .. math:: + \mathbf{\tilde{w}} = \mathbf{v} \odot \prod_j \mathbf{w_j} + + and weights applied to first-order smoothness terms are given by: + + .. math:: + \mathbf{\tilde{w}} = \big ( \mathbf{P \, v} \big ) \odot \prod_j \mathbf{P \, w_j} + + :math:`\mathbf{v}` are the cell volumes, and :math:`\mathbf{P}` represents the + projection matrix from cell centers to the appropriate faces; + i.e. where discrete first-order derivatives live. + + Weights for each term are used to construct their respective weighting matrices + as follows: + + .. math:: + \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) + + Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + + >>> reg = WeightedLeastSquares(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) + """ + + _model = None + + def __init__( + self, + mesh, + active_cells=None, + alpha_s=1.0, + alpha_x=None, + alpha_y=None, + alpha_z=None, + alpha_xx=0.0, + alpha_yy=0.0, + alpha_zz=0.0, + length_scale_x=None, + length_scale_y=None, + length_scale_z=None, + mapping=None, + reference_model=None, + reference_model_in_smooth=False, + weights=None, + **kwargs, + ): + if isinstance(mesh, BaseMesh): + mesh = RegularizationMesh(mesh) + + if not isinstance(mesh, RegularizationMesh): + TypeError( + f"'regularization_mesh' must be of type {RegularizationMesh} or {BaseMesh}. " + f"Value of type {type(mesh)} provided." + ) + 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: + raise ValueError( + "Attempted to set both alpha_x and length_scale_x at the same time. Please " + "use only one of them" + ) + self.alpha_x = alpha_x + else: + self.length_scale_x = length_scale_x + + if alpha_y is not None: + if length_scale_y is not None: + raise ValueError( + "Attempted to set both alpha_y and length_scale_y at the same time. Please " + "use only one of them" + ) + self.alpha_y = alpha_y + else: + self.length_scale_y = length_scale_y + + if alpha_z is not None: + if length_scale_z is not None: + raise ValueError( + "Attempted to set both alpha_z and length_scale_z at the same time. Please " + "use only one of them" + ) + self.alpha_z = alpha_z + else: + self.length_scale_z = length_scale_z + + # Check if weights is a dictionary, raise error if it's not + if weights is not None and not isinstance(weights, dict): + raise TypeError( + f"Invalid 'weights' of type '{type(weights)}'. " + "It must be a dictionary with strings as keys and arrays as values." + ) + + # do this to allow child classes to also pass a list of objfcts to this constructor + if "objfcts" not in kwargs: + objfcts = [ + Smallness(mesh=self.regularization_mesh), + SmoothnessFirstOrder(mesh=self.regularization_mesh, orientation="x"), + SmoothnessSecondOrder(mesh=self.regularization_mesh, orientation="x"), + ] + + if mesh.dim > 1: + objfcts.extend( + [ + SmoothnessFirstOrder( + mesh=self.regularization_mesh, orientation="y" + ), + SmoothnessSecondOrder( + mesh=self.regularization_mesh, orientation="y" + ), + ] + ) + + if mesh.dim > 2: + objfcts.extend( + [ + SmoothnessFirstOrder( + mesh=self.regularization_mesh, orientation="z" + ), + SmoothnessSecondOrder( + mesh=self.regularization_mesh, orientation="z" + ), + ] + ) + else: + objfcts = kwargs.pop("objfcts") + + super().__init__(objfcts=objfcts, unpack_on_add=False, **kwargs) + + for fun in objfcts: + fun.parent = self + + if active_cells is not None: + self.active_cells = active_cells + + self.mapping = mapping + self.reference_model = reference_model + self.reference_model_in_smooth = reference_model_in_smooth + self.alpha_xx = alpha_xx + self.alpha_yy = alpha_yy + self.alpha_zz = alpha_zz + if weights is not None: + self.set_weights(**weights) + + def set_weights(self, **weights): + """Adds (or updates) the specified weights for all child regularization objects. + + Parameters + ---------- + **weights : key, numpy.ndarray + Each keyword argument is added to the weights used by all child regularization objects. + They can be accessed with their keyword argument. + + Examples + -------- + >>> import discretize + >>> from simpeg.regularization import WeightedLeastSquares + >>> mesh = discretize.TensorMesh([2, 3, 2]) + >>> reg = WeightedLeastSquares(mesh) + >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) + >>> reg.get_weights('my_weight') + array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) + + """ + for fct in self.objfcts: + fct.set_weights(**weights) + + def remove_weights(self, key): + """Removes specified weights from all child regularization objects. + + Parameters + ---------- + key : str + The name of the weights being removed from all child regularization objects. + + Examples + -------- + >>> import discretize + >>> from simpeg.regularization import WeightedLeastSquares + >>> mesh = discretize.TensorMesh([2, 3, 2]) + >>> reg = WeightedLeastSquares(mesh) + >>> reg.set_weights(my_weight=np.ones(mesh.n_cells)) + >>> reg.get_weights('my_weight') + array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]) + >>> reg.remove_weights('my_weight') + """ + for fct in self.objfcts: + fct.remove_weights(key) + + @property + def cell_weights(self): + 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." + ) + + @property + def alpha_s(self): + """Multiplier constant for the smallness term. + + Returns + ------- + float + Multiplier constant for the smallness term. + """ + return self._alpha_s + + @alpha_s.setter + def alpha_s(self, value): + if value is None: + value = 1.0 + try: + value = float(value) + except (ValueError, TypeError): + raise TypeError(f"alpha_s must be a real number, saw type{type(value)}") + if value < 0: + raise ValueError(f"alpha_s must be non-negative, not {value}") + self._alpha_s = value + + @property + def alpha_x(self): + """Multiplier constant for first-order smoothness along x. + + Returns + ------- + float + Multiplier constant for first-order smoothness along x. + """ + return self._alpha_x + + @alpha_x.setter + def alpha_x(self, value): + try: + value = float(value) + except (ValueError, TypeError): + raise TypeError(f"alpha_x must be a real number, saw type{type(value)}") + if value < 0: + raise ValueError(f"alpha_x must be non-negative, not {value}") + self._alpha_x = value + + @property + def alpha_y(self): + """Multiplier constant for first-order smoothness along y. + + Returns + ------- + float + Multiplier constant for first-order smoothness along y. + """ + return self._alpha_y + + @alpha_y.setter + def alpha_y(self, value): + try: + value = float(value) + except (ValueError, TypeError): + raise TypeError(f"alpha_y must be a real number, saw type{type(value)}") + if value < 0: + raise ValueError(f"alpha_y must be non-negative, not {value}") + self._alpha_y = value + + @property + def alpha_z(self): + """Multiplier constant for first-order smoothness along z. + + Returns + ------- + float + Multiplier constant for first-order smoothness along z. + """ + return self._alpha_z + + @alpha_z.setter + def alpha_z(self, value): + try: + value = float(value) + except (ValueError, TypeError): + raise TypeError(f"alpha_z must be a real number, saw type{type(value)}") + if value < 0: + raise ValueError(f"alpha_z must be non-negative, not {value}") + self._alpha_z = value + + @property + def alpha_xx(self): + """Multiplier constant for second-order smoothness along x. + + Returns + ------- + float + Multiplier constant for second-order smoothness along x. + """ + return self._alpha_xx + + @alpha_xx.setter + def alpha_xx(self, value): + if value is None: + value = (self.length_scale_x * self.regularization_mesh.base_length) ** 4.0 + try: + value = float(value) + except (ValueError, TypeError): + raise TypeError(f"alpha_xx must be a real number, saw type{type(value)}") + if value < 0: + raise ValueError(f"alpha_xx must be non-negative, not {value}") + self._alpha_xx = value + + @property + def alpha_yy(self): + """Multiplier constant for second-order smoothness along y. + + Returns + ------- + float + Multiplier constant for second-order smoothness along y. + """ + return self._alpha_yy + + @alpha_yy.setter + def alpha_yy(self, value): + if value is None: + value = (self.length_scale_y * self.regularization_mesh.base_length) ** 4.0 + try: + value = float(value) + except (ValueError, TypeError): + raise TypeError(f"alpha_yy must be a real number, saw type{type(value)}") + if value < 0: + raise ValueError(f"alpha_yy must be non-negative, not {value}") + self._alpha_yy = value + + @property + def alpha_zz(self): + """Multiplier constant for second-order smoothness along z. + + Returns + ------- + float + Multiplier constant for second-order smoothness along z. + """ + return self._alpha_zz + + @alpha_zz.setter + def alpha_zz(self, value): + if value is None: + value = (self.length_scale_z * self.regularization_mesh.base_length) ** 4.0 + try: + value = float(value) + except (ValueError, TypeError): + raise TypeError(f"alpha_zz must be a real number, saw type{type(value)}") + if value < 0: + raise ValueError(f"alpha_zz must be non-negative, not {value}") + self._alpha_zz = value + + @property + def length_scale_x(self): + r"""Multiplier constant for smoothness along x relative to base scale length. + + Where the :math:`\Delta h` defines the base length scale (i.e. minimum cell dimension), + and :math:`\alpha_x` defines the multiplier constant for first-order smoothness along x, + the length-scale is given by: + + .. math:: + L_x = \bigg ( \frac{\alpha_x}{\Delta h} \bigg )^{1/2} + + Returns + ------- + float + Multiplier constant for smoothness along x relative to base scale length. + """ + return np.sqrt(self.alpha_x) / self.regularization_mesh.base_length + + @length_scale_x.setter + def length_scale_x(self, value: float): + if value is None: + value = 1.0 + try: + value = float(value) + except (TypeError, ValueError): + raise TypeError( + f"length_scale_x must be a real number, saw type{type(value)}" + ) + self.alpha_x = (value * self.regularization_mesh.base_length) ** 2 + + @property + def length_scale_y(self): + r"""Multiplier constant for smoothness along z relative to base scale length. + + Where the :math:`\Delta h` defines the base length scale (i.e. minimum cell dimension), + and :math:`\alpha_y` defines the multiplier constant for first-order smoothness along y, + the length-scale is given by: + + .. math:: + L_y = \bigg ( \frac{\alpha_y}{\Delta h} \bigg )^{1/2} + + Returns + ------- + float + Multiplier constant for smoothness along z relative to base scale length. + """ + return np.sqrt(self.alpha_y) / self.regularization_mesh.base_length + + @length_scale_y.setter + def length_scale_y(self, value: float): + if value is None: + value = 1.0 + try: + value = float(value) + except (TypeError, ValueError): + raise TypeError( + f"length_scale_y must be a real number, saw type{type(value)}" + ) + self.alpha_y = (value * self.regularization_mesh.base_length) ** 2 + + @property + def length_scale_z(self): + r"""Multiplier constant for smoothness along z relative to base scale length. + + Where the :math:`\Delta h` defines the base length scale (i.e. minimum cell dimension), + and :math:`\alpha_z` defines the multiplier constant for first-order smoothness along z, + the length-scale is given by: + + .. math:: + L_z = \bigg ( \frac{\alpha_z}{\Delta h} \bigg )^{1/2} + + Returns + ------- + float + Multiplier constant for smoothness along z relative to base scale length. + """ + return np.sqrt(self.alpha_z) / self.regularization_mesh.base_length + + @length_scale_z.setter + def length_scale_z(self, value: float): + if value is None: + value = 1.0 + try: + value = float(value) + except (TypeError, ValueError): + raise TypeError( + f"length_scale_z must be a real number, saw type{type(value)}" + ) + self.alpha_z = (value * self.regularization_mesh.base_length) ** 2 + + @property + def reference_model_in_smooth(self) -> bool: + """Whether to include the reference model in the smoothness objective functions. + + Returns + ------- + bool + Whether to include the reference model in the smoothness objective functions. + """ + return self._reference_model_in_smooth + + @reference_model_in_smooth.setter + def reference_model_in_smooth(self, value: bool): + if not isinstance(value, bool): + raise TypeError( + "'reference_model_in_smooth must be of type 'bool'. " + f"Value of type {type(value)} provided." + ) + self._reference_model_in_smooth = value + for fct in self.objfcts: + if getattr(fct, "reference_model_in_smooth", None) is not None: + fct.reference_model_in_smooth = value + + # Other properties and methods + @property + def nP(self): + """Number of model parameters. + + Returns + ------- + int + Number of model parameters. + """ + if getattr(self, "mapping", None) is not None and self.mapping.nP != "*": + return self.mapping.nP + elif ( + getattr(self, "_regularization_mesh", None) is not None + and self.regularization_mesh.nC != "*" + ): + return self.regularization_mesh.nC + else: + return "*" + + @property + def _nC_residual(self): + """ + Shape of the residual + """ + + nC = getattr(self.regularization_mesh, "nC", None) + mapping = getattr(self, "_mapping", None) + + if mapping is not None and mapping.shape[1] != "*": + return self.mapping.shape[1] + elif nC != "*" and nC is not None: + return self.regularization_mesh.nC + else: + return self.nP + + def _delta_m(self, m): + if self.reference_model is None: + return m + return m - self.reference_model + + @property + def multipliers(self): + r"""Multiplier constants for weighted sum of objective functions. + + For a model objective function :math:`\phi_m (\mathbf{m})` constructed using + a weighted sum of objective functions :math:`\phi_i (\mathbf{m})`, i.e.: + + .. math:: + \phi_m (\mathbf{m}) = \sum_i \alpha_i \, \phi_i (\mathbf{m}) + + the `multipliers` property returns the list of multiplier constants :math:`alpha_i` + in order. + + Returns + ------- + list of float + Multiplier constants for weighted sum of objective functions. + """ + return [getattr(self, objfct._multiplier_pair) for objfct in self.objfcts] + + @property + def active_cells(self) -> np.ndarray: + """Active cells defined on the regularization mesh. + + A boolean array defining the cells in the :py:class:`~.regularization.RegularizationMesh` + that are active (i.e. updated) throughout the inversion. The values of inactive cells + remain equal to their starting model values. + + Returns + ------- + (n_cells, ) array of bool + + Notes + ----- + If the property is set using a ``numpy.ndarray`` of ``int``, the setter interprets the + array as representing the indices of the active cells. When called however, the quantity + will have been internally converted to a boolean array. + """ + return self.regularization_mesh.active_cells + + @active_cells.setter + def active_cells(self, values: np.ndarray): + self.regularization_mesh.active_cells = values + active_cells = self.regularization_mesh.active_cells + # notify the objective functions that the active_cells changed + 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. + + Returns + ------- + None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. + """ + return self._reference_model + + @reference_model.setter + def reference_model(self, values: np.ndarray | float): + if isinstance(values, float): + values = np.ones(self._nC_residual) * values + + for fct in self.objfcts: + fct.reference_model = values + + 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. + + Returns + ------- + (n_param, ) numpy.ndarray + The model parameters. + """ + return self._model + + @model.setter + def model(self, values: np.ndarray | float): + if isinstance(values, float): + values = np.ones(self._nC_residual) * values + + for fct in self.objfcts: + fct.model = values + + self._model = values + + @property + def units(self) -> str: + """Units for the model parameters. + + Some regularization classes behave differently depending on the units; e.g. 'radian'. + + Returns + ------- + str + Units for the model parameters. + """ + return self._units + + @units.setter + def units(self, units: str | None): + if units is not None and not isinstance(units, str): + raise TypeError( + f"'units' must be None or type str. " + f"Value of type {type(units)} provided." + ) + for fct in self.objfcts: + fct.units = units + self._units = units + + @property + def regularization_mesh(self) -> RegularizationMesh: + """Regularization mesh. + + Mesh on which the regularization is discretized. This is not the same as + the mesh on which the simulation is defined. + + Returns + ------- + discretize.base.RegularizationMesh + Mesh on which the regularization is discretized. + """ + return self._regularization_mesh + + @property + def mapping(self) -> maps.IdentityMap: + """Mapping from the model to the regularization mesh. + + Returns + ------- + simpeg.maps.BaseMap + The mapping from the model parameters to the quantity defined on the + :py:class:`~simpeg.regularization.RegularizationMesh`. + """ + return self._mapping + + @mapping.setter + def mapping(self, mapping: maps.IdentityMap): + if mapping is None: + mapping = maps.IdentityMap(nP=self._nC_residual) + + if not isinstance(mapping, maps.IdentityMap): + raise TypeError( + f"'mapping' must be of type {maps.IdentityMap}. " + f"Value of type {type(mapping)} provided." + ) + self._mapping = mapping + + for fct in self.objfcts: + fct.mapping = mapping + + +############################################################################### +# # +# Base Coupling Regularization # +# # +############################################################################### +class BaseSimilarityMeasure(BaseRegularization): + """Base regularization class for joint inversion. + + The ``BaseSimilarityMeasure`` class defines properties and methods used + by regularization classes for joint inversion. It is not directly used to + constrain inversions. + + Parameters + ---------- + mesh : simpeg.regularization.RegularizationMesh + Mesh on which the regularization is discretized. This is not necessarily the same as + the mesh on which the simulation is defined. + wire_map : simpeg.maps.WireMap + Wire map connecting physical properties defined on active cells of the + :class:`RegularizationMesh` to the entire model. + """ + + def __init__(self, mesh, wire_map, **kwargs): + super().__init__(mesh, **kwargs) + self.wire_map = wire_map + + @property + def wire_map(self): + """Mapping from model to physical properties defined on the regularization mesh. + + Returns + ------- + simpeg.maps.WireMap + Mapping from model to physical properties defined on the regularization mesh. + """ + return self._wire_map + + @wire_map.setter + def wire_map(self, wires): + try: + m1, m2 = wires.maps # Assume a map has been passed for each model. + except ValueError: + ValueError("Wire map must have two model mappings") + + if m1[1].shape[0] != m2[1].shape[0]: + raise ValueError( + f"All models must be the same size! Got {m1[1].shape[0]} and {m2[1].shape[0]}" + ) + self._wire_map = wires + + @property + def nP(self): + """Number of model parameters. + + Returns + ------- + int + Number of model parameters. + """ + return self.wire_map.nP + + def deriv(self, model): + """Not implemented for ``BaseSimilarityMeasure`` class.""" + raise NotImplementedError( + "The method deriv has not been implemented for {}".format( + self.__class__.__name__ + ) + ) + + def deriv2(self, model, v=None): + """Not implemented for ``BaseSimilarityMeasure`` class.""" + raise NotImplementedError( + "The method _deriv2 has not been implemented for {}".format( + self.__class__.__name__ + ) + ) + + @property + def _nC_residual(self): + """ + Shape of the residual + """ + return self.wire_map.nP + + def __call__(self, model): + """Not implemented for ``BaseSimilarityMeasure`` class.""" + raise NotImplementedError( + "The method __call__ has not been implemented for {}".format( + self.__class__.__name__ + ) + ) diff --git a/simpeg/regularization/correspondence.py b/simpeg/regularization/correspondence.py new file mode 100644 index 0000000000..98e51443ae --- /dev/null +++ b/simpeg/regularization/correspondence.py @@ -0,0 +1,226 @@ +import numpy as np +import scipy.sparse as sp +from ..utils import validate_ndarray_with_shape + +from .. import utils +from .base import BaseSimilarityMeasure + + +class LinearCorrespondence(BaseSimilarityMeasure): + r"""Linear correspondence regularization for joint inversion with two physical properties. + + ``LinearCorrespondence`` is used to recover a model where the differences between the model + parameter values for two physical property types are minimal. ``LinearCorrespondence`` + can also be used to minimize the squared L2-norm of a linear combination of model parameters + for two physical property types. See the *Notes* section for a comprehensive description. + + Parameters + ---------- + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. This is not necessarily + the same as the mesh on which the simulation is defined. + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` + cells that are active in the inversion. If ``None``, all cells are active. + wire_map : simpeg.maps.Wires + Wire map connecting physical properties defined on active cells of the + :class:`RegularizationMesh`` to the entire model. + coefficients : None, (3) numpy.ndarray of float + Coefficients :math:`\{ \lambda_1, \lambda_2, \lambda_3 \}` for the linear relationship + between model parameters. If ``None``, the coefficients are set to + :math:`\{ 1, -1, 0 \}`. + + Notes + ----- + Let :math:`\mathbf{m}` be a discrete model consisting of two physical property types such that: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} + + Where :math:`\{ \lambda_1 , \lambda_2 , \lambda_3 \}` define scalar coefficients for a + linear combination of vectors :math:`\mathbf{m_1}` and :math:`\mathbf{m_2}`, the regularization + function (objective function) is given by: + + .. math:: + \phi (\mathbf{m}) + = \big \| \lambda_1 \mathbf{m_1} + \lambda_2 \mathbf{m_2} + \lambda_3 \big \|^2 + + Scalar coefficients :math:`\{ \lambda_1 , \lambda_2 , \lambda_3 \}` are set using the + `coefficients` property. For a true linear correspondence constraint, we set + :math:`\{ \lambda_1 , \lambda_2 , \lambda_3 \}` to :math:`\{ 1, -1, 0 \}`. + + """ + + def __init__(self, mesh, wire_map, coefficients=None, **kwargs): + super().__init__(mesh, wire_map, **kwargs) + if coefficients is None: + coefficients = np.r_[1.0, -1.0, 0.0] + self.coefficients = coefficients + + @property + def coefficients(self): + r"""Coefficients for the linear relationship between model parameters. + + For a relation vector: + + .. math:: + \mathbf{f}(\mathbf{m}) = \lambda_1 \mathbf{m_1} + \lambda_2 \mathbf{m_2} + \lambda_3 + + where + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} + + This property defines the coefficients :math:`\{ \lambda_1 , \lambda_2 , \lambda_3 \}`. + + Returns + ------- + (3, ) numpy.ndarray of float + Coefficients for the linear relationship between model parameters. + """ + return self._coefficients + + @coefficients.setter + def coefficients(self, value): + self._coefficients = validate_ndarray_with_shape( + "coefficients", value, shape=(3,) + ) + + def relation(self, model): + r"""Computes the relation vector for the model provided. + + For a model consisting of two physical properties such that: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} + + this method computer the relation vector for coefficients + :math:`\{ \lambda_1 , \lambda_2 , \lambda_3 \}` as follows: + + .. math:: + \mathbf{f}(\mathbf{m}) = \lambda_1 \mathbf{m_1} + \lambda_2 \mathbf{m_2} + \lambda_3 + + Parameters + ---------- + model : (n_param, ) numpy.ndarray + The model for which the relation vector is evaluated. + + Returns + ------- + float + The relation vector for the model provided. + """ + m1, m2 = self.wire_map * model + k1, k2, k3 = self.coefficients + + return k1 * m1 + k2 * m2 + k3 + + def __call__(self, model): + """Evaluate the regularization function for the model provided. + + Parameters + ---------- + model : (n_param, ) numpy.ndarray + The model for which the function is evaluated. + + Returns + ------- + float + The regularization function evaluated for the model provided. + """ + + result = self.relation(model) + return result.T @ result + + def deriv(self, model): + r"""Gradient of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method evaluates and returns the derivative with respect to the model parameters; + i.e. the gradient. For a model :math:`\mathbf{m}` consisting of two physical properties + such that: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} + + The gradient has the form: + + .. math:: + \frac{\partial \phi}{\partial \mathbf{m}} = + \begin{bmatrix} \dfrac{\partial \phi}{\partial \mathbf{m_1}} \\ + \dfrac{\partial \phi}{\partial \mathbf{m_2}} \end{bmatrix} + + Parameters + ---------- + model : (n_param, ) numpy.ndarray + The model; a vector array containing all physical properties. + + Returns + ------- + (n_param, ) numpy.ndarray + Gradient of the regularization function evaluated for the model provided. + """ + k1, k2, k3 = self.coefficients + r = self.relation(model) + dc_dm1 = k1 * r + dc_dm2 = k2 * r + + result = np.r_[dc_dm1, dc_dm2] + + return 2 * result + + def deriv2(self, model, v=None): + r"""Hessian of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method evalutate and returns the second derivative (Hessian) with respect to the + model parameters. For a model :math:`\mathbf{m}` consisting of two physical properties + such that: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} + + The Hessian has the form: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} = + \begin{bmatrix} + \dfrac{\partial^2 \phi}{\partial \mathbf{m_1}^2} & + \dfrac{\partial^2 \phi}{\partial \mathbf{m_1} \partial \mathbf{m_2}} \\ + \dfrac{\partial^2 \phi}{\partial \mathbf{m_2} \partial \mathbf{m_1}} & + \dfrac{\partial^2 \phi}{\partial \mathbf{m_2}^2} + \end{bmatrix} + + When a vector :math:`(\mathbf{v})` is supplied, the method returns the Hessian + times the vector: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \, \mathbf{v} + + Parameters + ---------- + model : (n_param, ) numpy.ndarray + The model; a vector array containing all physical properties. + v : None, (n_param, ) numpy.ndarray (optional) + A numpy array to model the Hessian by. + + Returns + ------- + (n_param, n_param) scipy.sparse.csr_matrix | (n_param, ) numpy.ndarray + If the input argument *v* is ``None``, the Hessian + for the models provided is returned. If *v* is not ``None``, + the Hessian multiplied by the vector provided is returned. + """ + + k1, k2, k3 = self.coefficients + if v is not None: + v1, v2 = self.wire_map * v + p1 = k1**2 * v1 + k2 * k1 * v2 + p2 = k2 * k1 * v1 + k2**2 * v2 + return 2 * np.r_[p1, p2] + else: + n = self.regularization_mesh.nC + A = utils.sdiag(np.ones(n) * (k1**2)) + B = utils.sdiag(np.ones(n) * (k2**2)) + C = utils.sdiag(np.ones(n) * (k1 * k2)) + return 2 * sp.bmat([[A, C], [C, B]], format="csr") diff --git a/simpeg/regularization/cross_gradient.py b/simpeg/regularization/cross_gradient.py new file mode 100644 index 0000000000..7632f26a5f --- /dev/null +++ b/simpeg/regularization/cross_gradient.py @@ -0,0 +1,448 @@ +import numpy as np +import scipy.sparse as sp + +from .base import BaseSimilarityMeasure +from ..utils import validate_type, coterminal + + +############################################################################### +# # +# Cross-Gradient # +# # +############################################################################### + + +class CrossGradient(BaseSimilarityMeasure): + r"""Cross-gradient regularization for joint inversion. + + ``CrossGradient`` regularization is used to ensure the location and orientation of non-zero + gradients in the recovered model are consistent across two physical property distributions. + For joint inversion involving three or more physical properties, a separate instance of + ``CrossGradient`` must be created for each physical property pair and added to the total + regularization as a weighted sum. + + Parameters + ---------- + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. This is not necessarily + the same as the mesh on which the simulation is defined. + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` + cells that are active in the inversion. If ``None``, all cells are active. + wire_map : simpeg.maps.Wires + Wire map connecting physical properties defined on active cells of the + :class:`RegularizationMesh`` to the entire model. + reference_model : None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. + units : None, str + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) + numpy.ndarray that is defined on the :py:class:`~.regularization.RegularizationMesh`. + approx_hessian : bool + Whether to use the semi-positive definate approximation for the Hessian. + + Notes + ----- + Consider the case where the model is comprised of two physical properties + :math:`m_1` and :math:`m_2`. Here, we define the regularization + function (objective function) for cross-gradient as + (`Haber and Gazit, 2013 `__): + + .. math:: + \phi (m_1, m_2) = \int_\Omega \, w(r) \, + \Big | \nabla m_1 \, \times \, \nabla m_2 \, \Big |^2 \, dv + + where :math:`w(r)` is a user-defined weighting function. + Using the identity :math:`| \vec{a} \times \vec{b} |^2 = | \vec{a} |^2 | \vec{b} |^2 - (\vec{a} \cdot \vec{b})^2`, + the regularization function can be re-expressed as: + + .. math:: + \phi (m_1, m_2) = \int_\Omega \, w(r) \, \Big [ \, + \big | \nabla m_1 \big |^2 \big | \nabla m_2 \big |^2 + - \big ( \nabla m_1 \, \cdot \, \nabla m_2 \, \big )^2 \Big ] \, dv + + 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 given by: + + .. math:: + \phi (m_1, m_2) \approx \sum_i \tilde{w}_i \, \bigg [ + \Big | (\nabla m_1)_i \Big |^2 \Big | (\nabla m_2)_i \Big |^2 + - \Big [ (\nabla m_1)_i \, \cdot \, (\nabla m_2)_i \, \Big ]^2 \, \bigg ] + + where :math:`(\nabla m_1)_i` are the gradients of property :math:`m_1` 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. + + In practice, we define the model :math:`\mathbf{m}` as a discrete + vector of the form: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} + + where :math:`\mathbf{m_1}` and :math:`\mathbf{m_2}` are the discrete representations + of the respective physical properties on the mesh. The discrete regularization function + is therefore equivalent to an objective function of the form: + + .. math:: + \phi (\mathbf{m}) = + \Big [ \mathbf{W A} \big ( \mathbf{G \, m_1} \big )^2 \Big ]^T + \Big [ \mathbf{W A} \big ( \mathbf{G \, m_2} \big )^2 \Big ] + - \bigg \| \mathbf{W A} \Big [ \big ( \mathbf{G \, m_1} \big ) + \odot \big ( \mathbf{G \, m_2} \big ) \Big ] \bigg \|^2 + + where exponents are computed elementwise, + + - :math:`\mathbf{G}` is the cell gradient operator (cell centers to faces), + - :math:`\mathbf{A}` averages vectors from faces to cell centers, and + - :math:`\mathbf{W}` is the weighting matrix. + + **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: + + .. math:: + \mathbf{\tilde{w}} = \mathbf{v} \odot \prod_j \mathbf{w_j} + + where :math:`\mathbf{v}` are the cell volumes. + The weighting matrix used to apply weights within the regularization is given by: + + .. math:: + \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) + + Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + + >>> reg = CrossGradient(mesh, wire_map, weights={'weights_1': array_1, 'weights_2': array_2}) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) + + The default weights that account for cell dimensions in the regularization are accessed via: + + >>> reg.get_weights('volume') + + """ + + def __init__( + self, mesh, wire_map, approx_hessian=True, units=["metric", "metric"], **kwargs + ): + + super().__init__(mesh, wire_map=wire_map, units=units, **kwargs) + self.approx_hessian = approx_hessian + + regmesh = self.regularization_mesh + + if regmesh.mesh.dim not in (2, 3): + raise ValueError("Cross-Gradient is only defined for 2D or 3D") + self._G = regmesh.cell_gradient + self._Av = sp.diags(np.sqrt(regmesh.vol)) * regmesh.average_face_to_cell + + @property + def approx_hessian(self): + """Whether to use the semi-positive definate approximation for the Hessian. + + Returns + ------- + bool + Whether to use the semi-positive definate approximation for the Hessian. + """ + return self._approx_hessian + + @approx_hessian.setter + def approx_hessian(self, value): + self._approx_hessian = validate_type("approx_hessian", value, bool) + + def _calculate_gradient(self, model, normalized=False, rtol=1e-6): + """ + Calculate the spatial gradients of the model using central difference. + + Concatenates gradient components into a single array. + [[x_grad1, y_grad1, z_grad1], + [x_grad2, y_grad2, z_grad2], + [x_grad3, y_grad3, z_grad3],...] + + :param numpy.ndarray model: model + + :rtype: numpy.ndarray + :return: gradient_vector: array where each row represents a model cell, + and each column represents a component of the gradient. + + """ + regmesh = self.regularization_mesh + Avs = [regmesh.aveFx2CC, regmesh.aveFy2CC] + if regmesh.dim == 3: + Avs.append(regmesh.aveFz2CC) + Av = sp.block_diag(Avs) + gradient = (Av @ (self._G @ model)).reshape((-1, regmesh.dim), order="F") + + if normalized: + norms = np.linalg.norm(gradient, axis=-1) + ind = norms <= norms.max() * rtol + norms[ind] = 1.0 + gradient /= norms[:, None] + gradient[ind] = 0.0 + # set gradient to 0 if amplitude of gradient is extremely small + + return gradient + + def calculate_cross_gradient(self, model, normalized=False, rtol=1e-6): + r"""Calculates the magnitudes of the cross-gradient vectors at cell centers. + + Computes and returns a discrete approximation to: + + .. math:: + \big | \, \nabla m_1 \, \times \, \nabla m_2 \, \big | + + at all cell centers where :math:`m_1` and :math:`m_2` define the continuous + spacial distribution of physical properties 1 and 2. + + Parameters + ---------- + model : numpy.ndarray + The input model, which will be automatically separated into the two + parameters internally. + normalized : bool, optional + Whether to normalize the cross-gradients. + rtol : float, optional + relative cuttoff for small gradients in the normalization. + + Returns + ------- + numpy.ndarray + Magnitudes of the cross-gradient vectors at cell centers. + """ + m1, m2 = self.wire_map * model + # Compute the gradients and concatenate components. + grad_m1 = self._calculate_gradient(m1, normalized=normalized, rtol=rtol) + grad_m2 = self._calculate_gradient(m2, normalized=normalized, rtol=rtol) + + # for each model cell, compute the cross product of the gradient vectors. + cross_prod = np.cross(grad_m1, grad_m2) + if self.regularization_mesh.dim == 3: + cross_prod = np.linalg.norm(cross_prod, axis=-1) + + return cross_prod + + def _model_gradients(self, models): + """ + Compute gradient on faces + """ + gradients = [] + + for unit, (name, wire) in zip(self.units, self.wire_map.maps): + model = wire * models + if unit == "radian": + gradient = [] + components = "xyz" if self.regularization_mesh.dim == 3 else "xy" + for comp in components: + distances = getattr( + self.regularization_mesh, f"cell_distances_{comp}" + ) + cell_grad = getattr( + self.regularization_mesh, f"cell_gradient_{comp}" + ) + gradient.append( + coterminal(cell_grad * model * distances) / distances + ) + + gradient = np.hstack(gradient) / np.pi + else: + gradient = self._G @ model + + gradients.append(gradient) + + return gradients + + def __call__(self, model): + """Evaluate the cross-gradient regularization function for the model provided. + + See the *Notes* section of the documentation for the :class:`CrossGradient` class + for a full description of the regularization function. + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model; a vector array containing all physical properties. + + Returns + ------- + float + The regularization function evaluated for the model provided. + """ + + Av = self._Av + + g_m1, g_m2 = self._model_gradients(model) + + return np.sum((Av @ g_m1**2) * (Av @ g_m2**2) - (Av @ (g_m1 * g_m2)) ** 2) + + def deriv(self, model): + r"""Gradient of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method evaluates and returns the derivative with respect to the model parameters; + i.e. the gradient. For a model :math:`\mathbf{m}` consisting of two physical properties + such that: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} + + The gradient has the form: + + .. math:: + 2 \frac{\partial \phi}{\partial \mathbf{m}} = + \begin{bmatrix} \dfrac{\partial \phi}{\partial \mathbf{m_1}} \\ + \dfrac{\partial \phi}{\partial \mathbf{m_2}} \end{bmatrix} + + Parameters + ---------- + model : (n_param, ) numpy.ndarray + The model; a vector array containing all physical properties. + + Returns + ------- + (n_param, ) numpy.ndarray + Gradient of the regularization function evaluated for the model provided. + """ + Av = self._Av + G = self._G + g_m1, g_m2 = self._model_gradients(model) + + return self.wire_map.deriv(model).T * ( + 2 + * np.r_[ + (((Av @ g_m2**2) @ Av) * g_m1) @ G + - (((Av @ (g_m1 * g_m2)) @ Av) * g_m2) @ G, + (((Av @ g_m1**2) @ Av) * g_m2) @ G + - (((Av @ (g_m1 * g_m2)) @ Av) * g_m1) @ G, + ] + ) # factor of 2 from derviative of | grad m1 x grad m2 | ^2 + + def deriv2(self, model, v=None): + r"""Hessian of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method evalutate and returns the second derivative (Hessian) with respect to the model parameters: + For a model :math:`\mathbf{m}` consisting of two physical properties such that: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} + + The Hessian has the form: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} = + \begin{bmatrix} + \dfrac{\partial^2 \phi}{\partial \mathbf{m_1}^2} & + \dfrac{\partial^2 \phi}{\partial \mathbf{m_1} \partial \mathbf{m_2}} \\ + \dfrac{\partial^2 \phi}{\partial \mathbf{m_2} \partial \mathbf{m_1}} & + \dfrac{\partial^2 \phi}{\partial \mathbf{m_2}^2} + \end{bmatrix} + + When a vector :math:`(\mathbf{v})` is supplied, the method returns the Hessian + times the vector: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \, \mathbf{v} + + Parameters + ---------- + model : (n_param, ) numpy.ndarray + The model; a vector array containing all physical properties. + v : None, (n_param, ) numpy.ndarray (optional) + A numpy array to model the Hessian by. + + Returns + ------- + (n_param, n_param) scipy.sparse.csr_matrix | (n_param, ) numpy.ndarray + If the input argument *v* is ``None``, the Hessian + for the models provided is returned. If *v* is not ``None``, + the Hessian multiplied by the vector provided is returned. + """ + Av = self._Av + G = self._G + + g_m1, g_m2 = self._model_gradients(model) + + d11_mid = Av.T @ (Av @ g_m2**2) + d12_mid = -(Av.T @ (Av @ (g_m1 * g_m2))) + d22_mid = Av.T @ (Av @ g_m1**2) + + if v is None: + D11_mid = sp.diags(d11_mid) + D12_mid = sp.diags(d12_mid) + D22_mid = sp.diags(d22_mid) + if not self.approx_hessian: + D11_mid = D11_mid - sp.diags(g_m2) @ Av.T @ Av @ sp.diags(g_m2) + D12_mid = ( + D12_mid + + 2 * sp.diags(g_m1) @ Av.T @ Av @ sp.diags(g_m2) + - sp.diags(g_m2) @ Av.T @ Av @ sp.diags(g_m1) + ) + D22_mid = D22_mid - sp.diags(g_m1) @ Av.T @ Av @ sp.diags(g_m1) + D11 = G.T @ D11_mid @ G + D12 = G.T @ D12_mid @ G + D22 = G.T @ D22_mid @ G + + return ( + 2 + * self.wire_map.deriv(model).T + * sp.bmat([[D11, D12], [D12.T, D22]], format="csr") + * self.wire_map.deriv(model) + ) # factor of 2 from derviative of | grad m1 x grad m2 | ^2 + + else: + v1, v2 = self.wire_map * v + + Gv1 = G @ v1 + Gv2 = G @ v2 + p1 = G.T @ (d11_mid * Gv1 + d12_mid * Gv2) + p2 = G.T @ (d12_mid * Gv1 + d22_mid * Gv2) + if not self.approx_hessian: + p1 += G.T @ ( + -g_m2 * (Av.T @ (Av @ (g_m2 * Gv1))) # d11*v1 full addition + + 2 * g_m1 * (Av.T @ (Av @ (g_m2 * Gv2))) # d12*v2 full addition + - g_m2 * (Av.T @ (Av @ (g_m1 * Gv2))) # d12*v2 continued + ) + + p2 += G.T @ ( + -g_m1 * (Av.T @ (Av @ (g_m1 * Gv2))) # d22*v2 full addition + + 2 * g_m2 * (Av.T @ (Av @ (g_m1 * Gv1))) # d12.T*v1 full addition + - g_m1 * (Av.T @ (Av @ (g_m2 * Gv1))) # d12.T*v1 fcontinued + ) + return ( + 2 * self.wire_map.deriv(model).T * np.r_[p1, p2] + ) # factor of 2 from derviative of | grad m1 x grad m2 | ^2 + + @property + def units(self) -> list[str] | None: + """Units for the model parameters. + + Some regularization classes behave differently depending on the units; e.g. 'radian'. + + Returns + ------- + str + Units for the model parameters. + """ + return self._units + + @units.setter + def units(self, units: list[str] | None): + if ( + units is not None + and not isinstance(units, list) + and not all(isinstance(u, str) for u in units) + ): + raise TypeError( + f"'units' must be None or a list of str. " + f"Value of type {type(units)} provided." + ) + self._units = units diff --git a/simpeg/regularization/jtv.py b/simpeg/regularization/jtv.py new file mode 100644 index 0000000000..0014071a30 --- /dev/null +++ b/simpeg/regularization/jtv.py @@ -0,0 +1,322 @@ +import numpy as np +import scipy.sparse as sp + +from .base import BaseSimilarityMeasure + + +############################################################################### +# # +# Joint Total Variation # +# # +############################################################################### + + +class JointTotalVariation(BaseSimilarityMeasure): + r"""Joint total variation regularization for joint inversion. + + ``JointTotalVariation`` regularization aims to ensure non-zero gradients in the recovered + model to occur at the same locations for all physical property distributions. + It assumes structures within each physical property distribution are sparse and + correlated with one another. + + Parameters + ---------- + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. This is not necessarily + the same as the mesh on which the simulation is defined. + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of :py:class:`~.regularization.RegularizationMesh` + cells that are active in the inversion. If ``None``, all cells are active. + wire_map : simpeg.maps.Wires + Wire map connecting physical properties defined on active cells of the + :class:`RegularizationMesh`` to the entire model. + reference_model : None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. + units : None, str + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) + numpy.ndarray that is defined on the :py:class:`~.regularization.RegularizationMesh`. + eps : float + Needs documentation!!! + + Notes + ----- + Consider the case where the model is comprised of two physical properties + :math:`m_1` and :math:`m_2`. Here, we define the regularization + function (objective function) for joint total variation as + (`Haber and Gazit, 2013 `__): + + .. math:: + \phi (m_1, m_2) = \int_\Omega \, w(r) \, + \Big [ \, \big | \nabla m_1 \big |^2 \, + \, \big | \nabla m_2 \big |^2 \, \Big ]^{1/2} \, dv + + where :math:`w(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 given by: + + .. math:: + \phi (m_1, m_2) \approx \sum_i \tilde{w}_i \, \bigg [ \, + \Big | (\nabla m_1)_i \Big |^2 \, + \, \Big | (\nabla m_2)_i \Big |^2 \, \bigg ]^{1/2} + + where :math:`(\nabla m_1)_i` are the gradients of property :math:`m_1` 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. + + In practice, we define the model :math:`\mathbf{m}` as a discrete + vector of the form: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \end{bmatrix} + + where :math:`\mathbf{m_1}` and :math:`\mathbf{m_2}` are the discrete representations + of the respective physical properties on the mesh. The discrete regularization function + is therefore equivalent to an objective function of the form: + + .. math:: + \phi (\mathbf{m}) = \mathbf{e}^T \Bigg ( \, + \mathbf{W \, A} \bigg [ \sum_k (\mathbf{G \, m_k})^2 \bigg ] \; + \; \epsilon \mathbf{v}^2 + \, \Bigg )^{1/2} + + where exponents are computed elementwise, + + - :math:`\mathbf{e}` is a vector of 1s, + - :math:`\mathbf{W}` is the weighting matrix for joint total variation regularization, + - :math:`\mathbf{A}` averages vectors from faces to cell centers, + - :math:`\mathbf{G}` is the cell gradient operator (cell centers to faces), + - :math:`\mathbf{v}` are the cell volumes, and + - :math:`\epsilon` is a constant added for continuous differentiability (set with the `eps` property), + + **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: + + .. math:: + \mathbf{\tilde{w}} = \mathbf{v} \odot \prod_j \mathbf{w_j} + + where :math:`\mathbf{v}` are the cell volumes. + The weighting matrix used to apply weights within the regularization is given by: + + .. math:: + \boldsymbol{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^2 \Big ) + + Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + + >>> reg = JointTotalVariation( + >>> mesh, wire_map, weights={'weights_1': array_1, 'weights_2': array_2} + >>> ) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) + + The default weights that account for cell dimensions in the regularization are accessed via: + + >>> reg.get_weights('volume') + + """ + + def __init__(self, mesh, wire_map, eps=1e-8, **kwargs): + super().__init__(mesh, wire_map=wire_map, **kwargs) + self.set_weights(volume=self.regularization_mesh.vol) + self.eps = eps + + self._G = self.regularization_mesh.cell_gradient + + @property + def W(self): + r"""Weighting matrix for joint total variation regularization. + + Returns the weighting matrix for the discrete regularization function. To see how the + weighting matrix is constructed, see the *Notes* section for the :class:`JointTotalVariation` + regularization class. + + Returns + ------- + scipy.sparse.csr_matrix + The weighting matrix applied in the regularization. + """ + if getattr(self, "_W", None) is None: + weights = np.prod(list(self._weights.values()), axis=0) + self._W = ( + sp.diags(weights**2) * self.regularization_mesh.average_face_to_cell + ) + return self._W + + @property + def wire_map(self): + # Docs inherited from BaseSimilarityMeasure + return self._wire_map + + @wire_map.setter + def wire_map(self, wires): + n = self.regularization_mesh.nC + maps = wires.maps + for _, mapping in maps: + map_n = mapping.shape[0] + if n != map_n: + raise ValueError( + f"All mapping outputs must match the number of cells in " + f"the regularization mesh! Got {n} and {map_n}" + ) + self._wire_map = wires + + def __call__(self, model): + """Evaluate the joint total variation regularization function for the model provided. + + See the *Notes* section of the documentation for the :class:`JointTotalVariation` class + for a full description of the regularization function. + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model; a vector array containing all physical properties. + + Returns + ------- + float + The regularization function evaluated for the model provided. + """ + W = self.W + G = self._G + v2 = self.regularization_mesh.vol**2 + g2 = 0 + for m in self.wire_map * model: + g_m = G @ m + g2 += g_m**2 + W_g = W @ g2 + sq = np.sqrt(W_g + self.eps * v2) + return np.sum(sq) + + def deriv(self, model): + r"""Gradient of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method evaluates and returns the derivative with respect to the model parameters; + i.e. the gradient. For a model :math:`\mathbf{m}` consisting of multiple physical properties + :math:`\mathbf{m_1}, \; \mathbf{m_2}, \; ...` such that: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \\ \vdots \end{bmatrix} + + The gradient has the form: + + .. math:: + \frac{\partial \phi}{\partial \mathbf{m}} = + \begin{bmatrix} \dfrac{\partial \phi}{\partial \mathbf{m_1}} \\ + \dfrac{\partial \phi}{\partial \mathbf{m_2}} \\ \vdots \end{bmatrix} + + Parameters + ---------- + model : (n_param, ) numpy.ndarray + The model; a vector array containing all physical properties. + + Returns + ------- + (n_param, ) numpy.ndarray + Gradient of the regularization function evaluated for the model provided. + """ + W = self.W + G = self._G + g2 = 0 + gs = [] + v2 = self.regularization_mesh.vol**2 + for m in self.wire_map * model: + g_mi = G @ m + g2 += g_mi**2 + gs.append(g_mi) + W_g = W @ g2 + sq = np.sqrt(W_g + self.eps * v2) + mid = W.T @ (1 / sq) + ps = [] + for g_mi in gs: + ps.append(G.T @ (mid * g_mi)) + return np.concatenate(ps) + + def deriv2(self, model, v=None): + r"""Hessian of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method evalutate and returns the second derivative (Hessian) with respect to the model parameters. + For a model :math:`\mathbf{m}` consisting of multiple physical properties + :math:`\mathbf{m_1}, \; \mathbf{m_2}, \; ...` such that: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_1} \\ \mathbf{m_2} \\ \vdots \end{bmatrix} + + The Hessian has the form: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} = + \begin{bmatrix} + \dfrac{\partial^2 \phi}{\partial \mathbf{m_1}^2} & + \dfrac{\partial^2 \phi}{\partial \mathbf{m_1} \partial \mathbf{m_2}} & + \cdots \\ + \dfrac{\partial^2 \phi}{\partial \mathbf{m_2} \partial \mathbf{m_1}} & + \dfrac{\partial^2 \phi}{\partial \mathbf{m_2}^2} & \; \\ + \vdots & \; & \ddots + \end{bmatrix} + + When a vector :math:`(\mathbf{v})` is supplied, the method returns the Hessian + times the vector: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \, \mathbf{v} + + Parameters + ---------- + model : (n_param, ) numpy.ndarray + The model; a vector array containing all physical properties. + v : numpy.ndarray, optional + An array to multiply the Hessian by. + + Returns + ------- + numpy.ndarray or scipy.sparse.csr_matrix + Hessian of the regularization function evaluated for the model provided. + The Hessian of joint total variation with respect to the model times a + vector or the full Hessian if `v` is `None`. + """ + W = self.W + G = self._G + v2 = self.regularization_mesh.vol**2 + gs = [] + g2 = 0 + for m in self.wire_map * model: + g_m = G @ m + g2 += g_m**2 + gs.append(g_m) + + W_g = W @ g2 + sq = np.sqrt(W_g + self.eps * v2) + mid = W.T @ (1 / sq) + + if v is not None: + g_vs = [] + tmp_sum = 0 + for vi, g_i in zip(self.wire_map * v, gs): + g_vi = G @ vi + tmp_sum += W.T @ ((W @ (g_i * g_vi)) / sq**3) + g_vs.append(g_vi) + ps = [] + for g_vi, g_i in zip(g_vs, gs): + ps.append(G.T @ (mid * g_vi - g_i * tmp_sum)) + return np.concatenate(ps) + else: + Pieces = [] + Diags = [] + SQ = sp.diags(sq**-1.5) + diag_block = G.T @ sp.diags(mid) @ G + for g_mi in gs: + Pieces.append(SQ @ W @ sp.diags(g_mi) @ G) + Diags.append(diag_block) + Row = sp.hstack(Pieces, format="csr") + Diag = sp.block_diag(Diags, format="csr") + return Diag - Row.T @ Row diff --git a/simpeg/regularization/pgi.py b/simpeg/regularization/pgi.py new file mode 100644 index 0000000000..08a8f4ddef --- /dev/null +++ b/simpeg/regularization/pgi.py @@ -0,0 +1,1398 @@ +from __future__ import annotations + +import copy +import warnings + +import numpy as np +import scipy.sparse as sp + +from ..maps import IdentityMap, Wires +from ..objective_function import ComboObjectiveFunction +from ..utils import ( + Identity, + deprecate_property, + mkvc, + sdiag, + timeIt, + validate_float, + validate_ndarray_with_shape, +) +from .base import RegularizationMesh, Smallness, WeightedLeastSquares + +############################################################################### +# # +# Petrophysically And Geologically Guided Regularization # +# # +############################################################################### + + +# Simple Petrophysical Regularization +##################################### + + +class PGIsmallness(Smallness): + r"""Smallness regularization function for petrophysically guided inversion (PGI). + + ``PGIsmallness`` is used to recover models in which the physical property values are + consistent with petrophysical information. ``PGIsmallness`` regularization assumes that + the statistical distribution of physical property values defining the model is characterized + by a Gaussian mixture model (GMM). That is, the physical property values for each specified + geological unit are characterized by a separate multivariate Gaussian distribution, + which are summed to define the GMM. ``PGIsmallness`` is generally combined with other + regularization classes to form a complete regularization for the inverse problem; see + :class:`PGI`. + + ``PGIsmallness`` can be implemented to invert for a single physical property or multiple + physical properties, each of which are defined on a linear scale (e.g. density) or a log-scale + (e.g. electrical conductivity). If the statistical distribution(s) of physical property values + for each property type are known, the GMM can be constructed and left static throughout the + inversion. Otherwise, the recovered model at each iteration is used to update the GMM. + And the updated GMM is used to constrain the recovered model for the following iteration. + + Parameters + ---------- + gmmref : simpeg.utils.WeightedGaussianMixture + Reference Gaussian mixture model. + gmm : None, simpeg.utils.WeightedGaussianMixture + Set the Gaussian mixture model used to constrain the recovered physical property model. + Can be left static throughout the inversion or updated using the + :class:`.directives.PGI_UpdateParameters` directive. If ``None``, the + :class:`.directives.PGI_UpdateParameters` directive must be used to ensure there + is a Gaussian mixture model for the inversion. + wiresmap : None, simpeg.maps.Wires + Mapping from the model to the model parameters of each type. + If ``None``, we assume only a single physical property type in the inversion. + maplist : None, list of simpeg.maps + Ordered list of mappings from model values to physical property values; + one for each physical property. If ``None``, we assume a single physical property type + in the regularization and an :class:`.maps.IdentityMap` from model values to physical + property values. + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. Implemented for + ``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. + approx_eval : bool + If ``True``, use the L2-approximation evaluation of the smallness term by assuming + physical property values of different types are uncorrelated. + approx_hessian : bool + Approximate the Hessian of the regularization function. + non_linear_relationship : bool + Whether relationships in the Gaussian mixture model are non-linear. + + Notes + ----- + For one or more physical property types (e.g. conductivity, density, susceptibility), + the ``PGIsmallness`` regularization function (objective function) is derived by setting a + Gaussian mixture model (GMM) as the prior within a Baysian inversion scheme. + For a comprehensive description, see + (`Astic, et al 2019 `__; + `Astic et al 2020 `__). + + We let :math:`\Theta` store all of the means (:math:`\boldsymbol{\mu}`), covariances + (:math:`\boldsymbol{\Sigma}`) and proportion constants (:math:`\boldsymbol{\gamma}`) + defining the GMM. And let :math:`\mathbf{z}^\ast` define an membership array that + extracts the GMM parameters for the most representative rock unit within each active cell + in the :class:`RegularizationMesh`. + + When the ``approx_eval`` property is ``True``, we assume the physical property distributions of each geologic units + are distinct (no significant overlap of their respective physical properties distribution). The GMM probability + density value at any each point of the physical property space can then be approximated by the locally dominant + Gaussian distribution. In this case, the PGI regularization function (objective function) can be expressed as a + least-square: + + .. math:: + \phi (\mathbf{m}) &= \alpha_\text{pgi} + \big | \mathbf{W} ( \Theta , \mathbf{z}^\ast ) \, (\mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \, \Big \|^2 + &+ \sum_{j=x,y,z} \alpha_j \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ + &+ \sum_{j=x,y,z} \alpha_{jj} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 + \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) + + where + + - :math:`\mathbf{m}` is the model, + - :math:`\mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast )` is the reference model, and + - :math:`\mathbf{W}(\Theta , \mathbf{z}^\ast )` is a weighting matrix. + + For the full, non-approximated PGI regularization, please refer to + (`Astic, et al 2019 `__; + `Astic et al 2020 `__). + + When the ``approx_eval`` property is ``True``, you may also set the ``approx_gradient`` and ``approx_hessian`` + properties to ``True`` so that the least-squares approximation is used to compute the gradient, as it is making the + same assumptions about the GMM. + + ``PGIsmallness`` regularization can be used for models consisting of one or more physical + property types. The ordering of the physical property types within the model is defined + using the `wiresmap`. And the mapping from model parameter values to physical property + values is specified with `maplist`. For :math:`K` physical property types, the model is + an array vector of the form: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m}_1 \\ \mathbf{m}_2 \\ \vdots \\ \mathbf{m}_K \end{bmatrix} + + **Constructing the Reference Model and Weighting Matrix:** + + The reference model used in the regularization function is constructed by extracting the means + :math:`\boldsymbol{\mu}` from the GMM using the membership array :math:`\mathbf{z}^\ast`. + We represent this vector as: + + .. math:: + \mathbf{m_{ref}} (\Theta ,{\mathbf{z}^\ast}) = \boldsymbol{\mu}_{\mathbf{z}^\ast} + + To construct the weighting matrix, :math:`\mathbf{z}^\ast` is used to extract the covariances + :math:`\boldsymbol{\Sigma}` for each cell. And the weighting matrix is given by: + + .. math:: + \mathbf{W}(\Theta ,{\mathbf{z}^\ast } ) = \boldsymbol{\Sigma}_{\mathbf{z^\ast}}^{\frac{-1}{2}} \, + diag \big ( \mathbf{w} \big ) + + **Updating the Gaussian Mixture Model:** + + When the GMM is set using the ``gmm`` property, the GMM remains static throughout the inversion. + When the ``gmm`` property set as ``None``, the GMM is learned and updated after every model update. + That is, we assume the GMM defined using the ``gmmref`` property is not completely representative + of the physical property distributions for each rock unit, and we update the all of the means + (:math:`\boldsymbol{\mu}`), covariances (:math:`\boldsymbol{\Sigma}`) and proportion constants + (:math:`\boldsymbol{\gamma}`) defining the GMM :math:`\Theta`. This is done by solving: + + .. math:: + \max_\Theta \; \mathcal{P}(\Theta | \mathbf{m}) + + using a MAP variation of the expectation-maximization clustering algorithm introduced in + Dempster (et al. 1977). + + **Updating the Membership Array:** + + As the model (and GMM) are updated throughout the inversion, the rock unit considered most + indicative of the geology within each cell is updated; which is represented by the membership + array :math:`\mathbf{z}^\ast`. W. For the current GMM with means (:math:`\boldsymbol{\mu}`), + covariances (:math:`\boldsymbol{\Sigma}`) and proportion constants (:math:`\boldsymbol{\gamma}`), + we solve the following for each cell: + + .. math:: + z_i^\ast = \max_n \; \gamma_{i,n} \, \mathcal{N} (\mathbf{m}_i | \boldsymbol{\mu}_n , \boldsymbol{\Sigma}_n) + + where + + - :math:`\mathbf{m_i}` are the model values for cell :math:`i`, + - :math:`\gamma_{i,n}` is the proportion for cell :math:`i` and rock unit :math:`n` + - :math:`\boldsymbol{\mu}_n` are the mean property values for unit :math:`n`, + - :math:`\boldsymbol{\Sigma}_n` are the covariances for unit :math:`n`, and + - :math:`\mathcal{N}` represent the multivariate Gaussian distribution. + + """ + + _multiplier_pair = "alpha_pgi" + _maplist = None + _wiresmap = None + + def __init__( + self, + gmmref, + gmm=None, + wiresmap=None, + maplist=None, + mesh=None, + approx_gradient=True, # L2 approximate of the gradients + approx_eval=True, # L2 approximate of the value + approx_hessian=True, + non_linear_relationships=False, + **kwargs, + ): + self.gmmref = copy.deepcopy(gmmref) + self.gmmref.order_clusters_GM_weight() + self.approx_gradient = approx_gradient + self.approx_eval = approx_eval + self.approx_hessian = approx_hessian + self.non_linear_relationships = non_linear_relationships + self._gmm = copy.deepcopy(gmm) + self.wiresmap = wiresmap + self.maplist = maplist + + if "mapping" in kwargs: + warnings.warn( + f"Property 'mapping' of class {type(self)} cannot be set. " + "Defaults to IdentityMap.", + stacklevel=2, + ) + kwargs.pop("mapping") + + weights = kwargs.pop("weights", None) + + super().__init__(mesh=mesh, mapping=IdentityMap(nP=self.shape[0]), **kwargs) + + # Save repetitive computations (see withmapping implementation) + self._r_first_deriv = None + self._r_second_deriv = None + + if weights is not None: + if isinstance(weights, (np.ndarray, list)): + weights = {"user_weights": np.r_[weights].flatten()} + self.set_weights(**weights) + + def set_weights(self, **weights): + """Adds (or updates) the specified weights. + + Parameters + ---------- + **weights : key, numpy.ndarray + Each keyword argument is added to the weights used the regularization object. + They can be accessed with their keyword argument. + """ + for key, values in weights.items(): + values = validate_ndarray_with_shape("weights", values, dtype=float) + + if values.shape[0] == self.regularization_mesh.nC: + values = np.tile(values, len(self.wiresmap.maps)) + + values = validate_ndarray_with_shape( + "weights", values, shape=(self._nC_residual,), dtype=float + ) + + self._weights[key] = values + + self._W = None + + @property + def gmm(self): + """Gaussian mixture model. + + If set prior to inversion, the Gaussian mixture model can be left static throughout + the inversion, or updated using the :class:`.directives.PGI_UpdateParameters` directive. + If this property is not set prior to inversion, the + :class:`.directives.PGI_UpdateParameters` directive must be used to ensure there + is a Gaussian mixture model for the inversion. + + Returns + ------- + simpeg.utils.WeightedGaussianMixture + Gaussian mixture model used to constrain the recovered physical property model. + """ + if getattr(self, "_gmm", None) is None: + self._gmm = copy.deepcopy(self.gmmref) + return self._gmm + + @gmm.setter + def gmm(self, gm): + if gm is not None: + self._gmm = copy.deepcopy(gm) + + @property + def shape(self): + """Number of model parameters. + + Returns + ------- + tuple of int + Number of model parameters. + """ + return (self.wiresmap.nP,) + + def membership(self, m): + """Compute and return membership array for the model provided. + + The membership array stores the index of the rock unit most representative of each cell. + For a Gaussian mixture model containing the means and covariances for model parameter + types (physical property types) for all rock units, this method computes the membership + array for the model `m` provided. For a description of the membership array, see the + *Notes* section within the :class:`PGIsmallness` documentation. + + Parameters + ---------- + m : (n_param, ) numpy.ndarray of float + The model. + + Returns + ------- + (n_active, ) numpy.ndarray of int + The membership array. + """ + modellist = self.wiresmap * m + model = np.c_[[a * b for a, b in zip(self.maplist, modellist)]].T + return self.gmm.predict(model) + + def compute_quasi_geology_model(self): + r"""Compute and return quasi geology model. + + For each active cell in the mesh, this method returns the mean values in the Gaussian + mixture model for the most representative rock unit, given the current model. See the + *Notes* section for a comprehensive description. + + Returns + ------- + (n_param, ) numpy.ndarray + The quasi geology physical property model. + + Notes + ----- + Consider a Gaussian mixture model (GMM) for :math:`K` physical property types and + :math:`N` rock units. The mean model parameter values for rock unit + :math:`n \in \{ 1, \ldots , N \}` in the GMM is represented by a vector + :math:`\boldsymbol{\mu}_n` of length :math:`K`. For each active cell in the mesh, the + `compute_quasi_geology_model` method computes: + + .. math:: + g_i^ = \min_{\boldsymbol{\mu}_n} \big \| \mathbf{m}_i - \boldsymbol{\mu}_n \big \|^2 + + where :math:`\mathbf{m}_i` are the model parameter values for cell :math:`i` for the + current model. The ordering of the output vector :math:`\mathbf{g}` is the same as the + model :math:`\mathbf{m}`. + """ + # used once mref is built + mreflist = self.wiresmap * self.reference_model + mrefarray = np.c_[[a for a in mreflist]].T + return np.c_[ + [((mrefarray - mean) ** 2).sum(axis=1) for mean in self.gmm.means_] + ].argmin(axis=0) + + @property + def non_linear_relationships(self): + """Whether relationships in the Gaussian mixture model are non-linear. + + Returns + ------- + bool + Whether relationships in the Gaussian mixture model are non-linear. + """ + return self._non_linear_relationships + + @non_linear_relationships.setter + def non_linear_relationships(self, value: bool): + if not isinstance(value, bool): + raise ValueError( + "Input value for 'non_linear_relationships' must be of type 'bool'. " + f"Provided {value} of type {type(value)}." + ) + self._non_linear_relationships = value + + @property + def wiresmap(self): + """Mapping from the model to the model parameters of each type. + + Returns + ------- + simpeg.maps.Wires + Mapping from the model to the model parameters of each type. + """ + if getattr(self, "_wiresmap", None) is None: + self._wiresmap = Wires(("m", self.regularization_mesh.nC)) + return self._wiresmap + + @wiresmap.setter + def wiresmap(self, wires): + if self._maplist is not None and len(wires.maps) != len(self._maplist): + raise Exception( + f"Provided 'wiresmap' should have wires the len of 'maplist' {len(self._maplist)}." + ) + + if not isinstance(wires, Wires): + raise ValueError(f"Attribure 'wiresmap' should be of type {Wires} or None.") + + self._wiresmap = wires + + @property + def maplist(self): + """Ordered list of mappings from model values to physical property values. + + Returns + ------- + list of simpeg.maps + Ordered list of mappings from model values to physical property values; + one for each physical property. + """ + if getattr(self, "_maplist", None) is None: + self._maplist = [ + IdentityMap(nP=self.regularization_mesh.nC) + for maps in self.wiresmap.maps + ] + return self._maplist + + @maplist.setter + def maplist(self, maplist): + if self._wiresmap is not None and len(maplist) != len(self._wiresmap.maps): + raise Exception( + f"Provided 'maplist' should be a list of maps equal to the 'wiresmap' list of len {len(self._maplist)}." + ) + + if not isinstance(maplist, (list, type(None))): + raise ValueError( + f"Attribute 'maplist' should be a list of maps or None.{type(maplist)} was given." + ) + + if isinstance(maplist, list) and not all( + isinstance(m, IdentityMap) for m in maplist + ): + raise ValueError( + f"Attribute 'maplist' should be a list of maps or None.{type(maplist)} was given." + ) + + self._maplist = maplist + + @timeIt + def __call__(self, m, external_weights=True): + """Evaluate the regularization function for the model provided. + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the function is evaluated. + external_weights : bool + Include custom cell weighting when evaluating the regularization function. + + Returns + ------- + float + The regularization function evaluated for the model provided. + """ + if external_weights: + W = self.W + else: + W = Identity() + + if getattr(self, "reference_model", None) is None: + self.reference_model = mkvc(self.gmm.means_[self.membership(m)]) + + if self.approx_eval: + membership = self.compute_quasi_geology_model() + dm = self.wiresmap * (m) + dmref = self.wiresmap * (self.reference_model) + dmm = np.c_[[a * b for a, b in zip(self.maplist, dm)]].T + if self.non_linear_relationships: + dmm = np.r_[ + [ + self.gmm.cluster_mapping[membership[i]] * dmm[i].reshape(-1, 2) + for i in range(dmm.shape[0]) + ] + ].reshape(-1, 2) + + dmmref = np.c_[[a for a in dmref]].T + dmr = dmm - dmmref + 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))] + ] + 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)) + ] + ] + else: + r1 = np.r_[ + [ + np.dot(self.gmm.precisions_[membership[i]], np.r_[r0[i]]) + for i in range(len(r0)) + ] + ] + + return mkvc(r0).dot(mkvc(r1)) + + else: + modellist = self.wiresmap * m + model = np.c_[[a * b for a, b in zip(self.maplist, modellist)]].T + + if self.non_linear_relationships: + score = self.gmm.score_samples(model) + score_vec = mkvc(np.r_[[score for maps in self.wiresmap.maps]]) + return -2 * np.sum((W.T * W) * score_vec) / len(self.wiresmap.maps) + + else: + if external_weights and getattr(self.W, "diagonal", None) is not None: + sensW = np.c_[ + [wire[1] * self.W.diagonal() for wire in self.wiresmap.maps] + ].T + else: + sensW = np.ones_like(model) + + score = self.gmm.score_samples_with_sensW(model, sensW) + # score_vec = mkvc(np.r_[[score for maps in self.wiresmap.maps]]) + # return -np.sum((W.T * W) * score_vec) / len(self.wiresmap.maps) + return -2 * np.sum(score) + + @timeIt + def deriv(self, m): + r"""Gradient of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method evaluates and returns the derivative with respect to the model parameters; + i.e. the gradient: + + .. math:: + \frac{\partial \phi}{\partial \mathbf{m}} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the gradient is evaluated. + + Returns + ------- + (n_param, ) numpy.ndarray + Gradient of the regularization function evaluated for the model provided. + """ + if getattr(self, "reference_model", None) is None: + self.reference_model = mkvc(self.gmm.means_[self.membership(m)]) + + membership = self.compute_quasi_geology_model() + modellist = self.wiresmap * m + dmmodel = np.c_[[a * b for a, b in zip(self.maplist, modellist)]].T + mreflist = self.wiresmap * self.reference_model + mD = [a.deriv(b) for a, b in zip(self.maplist, modellist)] + mD = sp.block_diag(mD) + + if self.non_linear_relationships: + dmmodel = np.r_[ + [ + self.gmm.cluster_mapping[membership[i]] * dmmodel[i].reshape(-1, 2) + for i in range(dmmodel.shape[0]) + ] + ].reshape(-1, 2) + + if self.approx_gradient: + dmmref = np.c_[[a for a in mreflist]].T + dm = dmmodel - dmmref + r0 = (self.W * (mkvc(dm))).reshape(dm.shape, order="F") + + if self.gmm.covariance_type == "tied": + 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))]] + ) + 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)) + ] + ] + ) + else: + if self.non_linear_relationships: + r = mkvc( + np.r_[ + [ + mkvc( + self.gmm.cluster_mapping[membership[i]].deriv( + dmmodel[i], + v=np.dot( + self.gmm.precisions_[membership[i]], r0[i] + ), + ) + ) + for i in range(dmmodel.shape[0]) + ] + ] + ) + + 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)) + ] + ] + ) + return 2 * mkvc(mD.T * (self.W.T * r)) + + else: + if self.non_linear_relationships: + raise Exception("Not implemented") + + modellist = self.wiresmap * m + model = np.c_[[a * b for a, b in zip(self.maplist, modellist)]].T + + if getattr(self.W, "diagonal", None) is not None: + sensW = np.c_[ + [wire[1] * self.W.diagonal() for wire in self.wiresmap.maps] + ].T + else: + sensW = np.ones_like(model) + + score = self.gmm.score_samples_with_sensW(model, sensW) + # score = self.gmm.score_samples(model) + score_vec = np.hstack([score for maps in self.wiresmap.maps]) + + logP = np.zeros((len(model), self.gmm.n_components)) + W = [] + logP = self.gmm._estimate_log_gaussian_prob_with_sensW( + model, + sensW, + self.gmm.means_, + self.gmm.precisions_cholesky_, + self.gmm.covariance_type, + ) + for k in range(self.gmm.n_components): + if self.gmm.covariance_type == "tied": + # logP[:, k] = mkvc( + # multivariate_normal( + # self.gmm.means_[k], self.gmm.covariances_ + # ).logpdf(model) + # ) + + W.append( + self.gmm.weights_[k] + * mkvc( + np.r_[ + [ + np.dot( + np.diag(sensW[i]).dot( + self.gmm.precisions_.dot(np.diag(sensW[i])) + ), + (model[i] - self.gmm.means_[k]).T, + ) + for i in range(len(model)) + ] + ] + ) + ) + elif ( + self.gmm.covariance_type == "diag" + or self.gmm.covariance_type == "spherical" + ): + # logP[:, k] = mkvc( + # multivariate_normal( + # self.gmm.means_[k], + # self.gmm.covariances_[k] * np.eye(len(self.wiresmap.maps)), + # ).logpdf(model) + # ) + W.append( + self.gmm.weights_[k] + * mkvc( + np.r_[ + [ + np.dot( + np.diag(sensW[i]).dot( + ( + self.gmm.precisions_[k] + * np.eye(len(self.wiresmap.maps)) + ).dot(np.diag(sensW[i])) + ), + (model[i] - self.gmm.means_[k]).T, + ) + for i in range(len(model)) + ] + ] + ) + ) + else: + # logP[:, k] = mkvc( + # multivariate_normal( + # self.gmm.means_[k], self.gmm.covariances_[k] + # ).logpdf(model) + # ) + W.append( + self.gmm.weights_[k] + * mkvc( + np.r_[ + [ + np.dot( + np.diag(sensW[i]).dot( + self.gmm.precisions_[k].dot( + np.diag(sensW[i]) + ) + ), + (model[i] - self.gmm.means_[k]).T, + ) + for i in range(len(model)) + ] + ] + ) + ) + W = np.c_[W].T + logP = np.vstack([logP for maps in self.wiresmap.maps]) + numer = (W * np.exp(logP)).sum(axis=1) + r = numer / (np.exp(score_vec)) + return 2 * mkvc(mD.T * r) + + @timeIt + def deriv2(self, m, v=None): + r"""Hessian of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method returns the second-derivative (Hessian) with respect to the model parameters: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} + + or the second-derivative (Hessian) multiplied by a vector :math:`(\mathbf{v})`: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \, \mathbf{v} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the Hessian is evaluated. + v : None, (n_param, ) numpy.ndarray (optional) + A vector. + + Returns + ------- + (n_param, n_param) scipy.sparse.csr_matrix | (n_param, ) numpy.ndarray + If the input argument *v* is ``None``, the Hessian of the regularization + function for the model provided is returned. If *v* is not ``None``, + the Hessian multiplied by the vector provided is returned. + """ + if getattr(self, "reference_model", None) is None: + self.reference_model = mkvc(self.gmm.means_[self.membership(m)]) + + if self.approx_hessian: + # we approximate it with the covariance of the cluster + # whose each point belong + membership = self.compute_quasi_geology_model() + modellist = self.wiresmap * m + dmmodel = np.c_[[a * b for a, b in zip(self.maplist, modellist)]].T + mD = [a.deriv(b) for a, b in zip(self.maplist, modellist)] + mD = sp.block_diag(mD) + if self._r_second_deriv is None: + if self.gmm.covariance_type == "tied": + if self.non_linear_relationships: + r = np.r_[ + [ + self.gmm.cluster_mapping[membership[i]].deriv( + dmmodel[i], + v=( + self.gmm.cluster_mapping[membership[i]].deriv( + dmmodel[i], v=self.gmm.precisions_ + ) + ).T, + ) + for i in range(len(dmmodel)) + ] + ] + else: + r = self.gmm.precisions_[np.newaxis, :, :][ + np.zeros_like(membership) + ] + elif ( + self.gmm.covariance_type == "spherical" + or self.gmm.covariance_type == "diag" + ): + if self.non_linear_relationships: + r = np.r_[ + [ + self.gmm.cluster_mapping[membership[i]].deriv( + dmmodel[i], + v=( + self.gmm.cluster_mapping[membership[i]].deriv( + dmmodel[i], + v=self.gmm.precisions_[membership[i]] + * np.eye(len(self.wiresmap.maps)), + ) + ).T, + ) + for i in range(len(dmmodel)) + ] + ] + else: + r = np.r_[ + [ + self.gmm.precisions_[memb] + * np.eye(len(self.wiresmap.maps)) + for memb in membership + ] + ] + else: + if self.non_linear_relationships: + r = np.r_[ + [ + self.gmm.cluster_mapping[membership[i]].deriv( + dmmodel[i], + v=( + self.gmm.cluster_mapping[membership[i]].deriv( + dmmodel[i], + v=self.gmm.precisions_[membership[i]], + ) + ).T, + ) + for i in range(len(dmmodel)) + ] + ] + else: + r = self.gmm.precisions_[membership] + + self._r_second_deriv = r + + if v is not None: + mDv = self.wiresmap * (mD * v) + 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))] + ] + ) + return 2 * mkvc(mD.T * (self.W * second_deriv_times_r0)) + else: + # Forming the Hessian by diagonal blocks + hlist = [ + [ + self._r_second_deriv[:, i, j] + for i in range(len(self.wiresmap.maps)) + ] + for j in range(len(self.wiresmap.maps)) + ] + Hr = sp.csc_matrix((0, 0), dtype=np.float64) + for i in range(len(self.wiresmap.maps)): + Hc = sp.csc_matrix((0, 0), dtype=np.float64) + for j in range(len(self.wiresmap.maps)): + Hc = sp.hstack([Hc, sdiag(hlist[i][j])]) + Hr = sp.vstack([Hr, Hc]) + + Hr = Hr.dot(self.W) + + return 2 * (mD.T * mD) * (self.W * (Hr)) + + else: + if self.non_linear_relationships: + raise Exception("Not implemented") + + # non distinct clusters positive definite approximated Hessian + modellist = self.wiresmap * m + model = np.c_[[a * b for a, b in zip(self.maplist, modellist)]].T + + if getattr(self.W, "diagonal", None) is not None: + sensW = np.c_[ + [wire[1] * self.W.diagonal() for wire in self.wiresmap.maps] + ].T + else: + sensW = np.ones_like(model) + + mD = [a.deriv(b) for a, b in zip(self.maplist, modellist)] + mD = sp.block_diag(mD) + + score = self.gmm.score_samples_with_sensW(model, sensW) + logP = np.zeros((len(model), self.gmm.n_components)) + W = [] + logP = self.gmm._estimate_weighted_log_prob_with_sensW( + model, + sensW, + ) + for k in range(self.gmm.n_components): + if self.gmm.covariance_type == "tied": + W.append( + [ + np.diag(sensW[i]).dot( + self.gmm.precisions_.dot(np.diag(sensW[i])) + ) + for i in range(len(model)) + ] + ) + elif ( + self.gmm.covariance_type == "diag" + or self.gmm.covariance_type == "spherical" + ): + W.append( + [ + np.diag(sensW[i]).dot( + ( + self.gmm.precisions_[k] + * np.eye(len(self.wiresmap.maps)) + ).dot(np.diag(sensW[i])) + ) + for i in range(len(model)) + ] + ) + else: + W.append( + [ + np.diag(sensW[i]).dot( + self.gmm.precisions_[k].dot(np.diag(sensW[i])) + ) + for i in range(len(model)) + ] + ) + W = np.c_[W] + + hlist = [ + [ + (W[:, :, i, j].T * np.exp(logP)).sum(axis=1) / np.exp(score) + for i in range(len(self.wiresmap.maps)) + ] + for j in range(len(self.wiresmap.maps)) + ] + + # Forming the Hessian by diagonal blocks + Hr = sp.csc_matrix((0, 0), dtype=np.float64) + for i in range(len(self.wiresmap.maps)): + Hc = sp.csc_matrix((0, 0), dtype=np.float64) + for j in range(len(self.wiresmap.maps)): + Hc = sp.hstack([Hc, sdiag(hlist[i][j])]) + Hr = sp.vstack([Hr, Hc]) + Hr = 2 * (mD.T * mD) * Hr + + if v is not None: + return Hr.dot(v) + + return Hr + + +class PGI(ComboObjectiveFunction): + r"""Regularization function for petrophysically guided inversion (PGI). + + ``PGI`` is used to recover models in which 1) the physical property values are consistent + with petrophysical information and 2) structures in the recovered model are geologically + plausible. ``PGI`` regularization is a weighted sum of :class:`PGIsmallness`, + :class:`SmoothnessFirstOrder` and :class:`SmoothnessSecondOrder` (optional) + regularization functions. The PGI smallness term assumes the statistical distribution of + physical property values defining the model is characterized + by a Gaussian mixture model (GMM). And the smoothness terms penalize large + spatial derivatives in the recovered model. + + ``PGI`` can be implemented to invert for a single physical property or multiple + physical properties, each of which are defined on a linear scale (e.g. density) or a log-scale + (e.g. electrical conductivity). If the statistical distribution(s) of physical property values + for each property type are known, the GMM can be constructed and left static throughout the + inversion. Otherwise, the recovered model at each iteration is used to update the GMM. + And the updated GMM is used to constrain the recovered model for the following iteration. + + Parameters + ---------- + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. Implemented for + `tensor`, `QuadTree` or `Octree` meshes. + gmmref : simpeg.utils.WeightedGaussianMixture + Reference Gaussian mixture model. + gmm : None, simpeg.utils.WeightedGaussianMixture + Set the Gaussian mixture model used to constrain the recovered physical property model. + Can be left static throughout the inversion or updated using the + :class:`.directives.PGI_UpdateParameters` directive. If ``None``, the + :class:`.directives.PGI_UpdateParameters` directive must be used to ensure there + is a Gaussian mixture model for the inversion. + alpha_pgi : float + Scaling constant for the PGI smallness term. + alpha_x, alpha_y, alpha_z : float or None, optional + Scaling constants for the first order smoothness along x, y and z, respectively. + If set to ``None``, the scaling constant is set automatically according to the + value of the `length_scale` parameter. + alpha_xx, alpha_yy, alpha_zz : 0, float + Scaling constants for the second order smoothness along x, y and z, respectively. + If set to ``None``, the scaling constant is set automatically according to the + length scales; see :class:`regularization.WeightedLeastSquares`. + wiresmap : None, simpeg.maps.Wires + Mapping from the model to the model parameters of each type. + If ``None``, we assume only a single physical property type in the inversion. + maplist : None, list of simpeg.maps + Ordered list of mappings from model values to physical property values; + one for each physical property. If ``None``, we assume a single physical property type + in the regularization and an :class:`.maps.IdentityMap` from model values to physical + property values. + approx_gradient : bool + If ``True``, use the L2-approximation of the gradient by assuming + physical property values of different types are uncorrelated. + approx_eval : bool + If ``True``, use the L2-approximation evaluation of the smallness term by assuming + physical property values of different types are uncorrelated. + approx_hessian : bool + Approximate the Hessian of the regularization function. + non_linear_relationship : bool + Whether relationships in the Gaussian mixture model are non-linear. + reference_model_in_smooth : bool, optional + Whether to include the reference model in the smoothness terms. + + Notes + ----- + For one or more physical property types (e.g. conductivity, density, susceptibility), + the ``PGI`` regularization function (objective function) is derived by using a + Gaussian mixture model (GMM) to construct the prior within a Baysian + inversion scheme. For a comprehensive description, see + (`Astic, et al 2019 `__; + `Astic et al 2020 `__). + + We let :math:`\Theta` store all of the means (:math:`\boldsymbol{\mu}`), covariances + (:math:`\boldsymbol{\Sigma}`) and proportion constants (:math:`\boldsymbol{\gamma}`) + defining the GMM. And let :math:`\mathbf{z}^\ast` define an membership array that + extracts the GMM parameters for the most representative rock unit within each active cell + in the :class:`RegularizationMesh`. The regularization function (objective function) for + ``PGI`` is given by: + + .. math:: + \phi (\mathbf{m}) &= \alpha_\text{pgi} + \big [ \mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \big ]^T + \mathbf{W} ( \Theta , \mathbf{z}^\ast ) \, + \big [ \mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \big ] \\ + &+ \sum_{j=x,y,z} \alpha_j \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ + &+ \sum_{j=x,y,z} \alpha_{jj} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 + \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) + + where + + - :math:`\mathbf{m}` is the model, + - :math:`\mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast )` is the reference model, + - :math:`\mathbf{G_x, \, G_y, \; G_z}` are partial cell gradients operators along x, y and z, + - :math:`\mathbf{L_x, \, L_y, \; L_z}` are second-order derivative operators with respect to x, y and z, + - :math:`\mathbf{W}(\Theta , \mathbf{z}^\ast )` is the weighting matrix for PGI smallness, and + - :math:`\mathbf{W_x, \, W_y, \; W_z}` are weighting matrices for smoothness terms. + + ``PGIsmallness`` regularization can be used for models consisting of one or more physical + property types. The ordering of the physical property types within the model is defined + using the `wiresmap`. And the mapping from model parameter values to physical property + values is specified with `maplist`. For :math:`K` physical property types, the model is + an array vector of the form: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m}_1 \\ \mathbf{m}_2 \\ \vdots \\ \mathbf{m}_K \end{bmatrix} + + When the ``approx_eval`` property is ``True``, we assume the physical property types have + values that are uncorrelated. In this case, the weighting matrix is diagonal and the + regularization function (objective function) can be expressed as: + + .. math:: + \phi (\mathbf{m}) &= \alpha_\text{pgi} \Big \| \mathbf{W}_{\! 1/2}(\Theta, \mathbf{z}^\ast ) \, + \big [ \mathbf{m} - \mathbf{m_{ref}}(\Theta, \mathbf{z}^\ast ) \big ] \, \Big \|^2 \\ + &+ \sum_{j=x,y,z} \alpha_j \Big \| \mathbf{W_j G_j \, m} \, \Big \|^2 \\ + &+ \sum_{j=x,y,z} \alpha_{jj} \Big \| \mathbf{W_{jj} L_j \, m} \, \Big \|^2 + \;\;\;\;\;\;\;\; \big ( \textrm{optional} \big ) + + When the ``approx_eval`` property is ``True``, you may also set the ``approx_gradient`` property + to ``True`` so that the least-squares approximation is used to compute the gradient. + + **Constructing the Reference Model and Weighting Matrix:** + + The reference model used in the regularization function is constructed by extracting the means + :math:`\boldsymbol{\mu}` from the GMM using the membership array :math:`\mathbf{z}^\ast`. + We represent this vector as: + + .. math:: + \mathbf{m_{ref}} (\Theta ,{\mathbf{z}^\ast}) = \boldsymbol{\mu}_{\mathbf{z}^\ast} + + To construct the weighting matrix, :math:`\mathbf{z}^\ast` is used to extract the covariances + :math:`\boldsymbol{\Sigma}` for each cell. And the weighting matrix is given by: + + .. math:: + \mathbf{W}(\Theta ,{\mathbf{z}^\ast } ) = \boldsymbol{\Sigma}_{\mathbf{z^\ast}}^{-1} \, + diag \big ( \mathbf{v \odot w} \big ) + + where :math:`\mathbf{v}` are the volumes of the active cells, and :math:`\mathbf{w}` + are custom cell weights. When the ``approx_eval`` property is ``True``, the off-diagonal + covariances are zero and we can use a weighting matrix of the form: + + .. math:: + \mathbf{W}_{\! 1/2}(\Theta ,{\mathbf{z}^\ast } ) = diag \Big ( \big [ \mathbf{v \odot w} + \odot \boldsymbol{\sigma}_{\mathbf{z}^\ast}^{-2} \big ]^{1/2} \Big ) + + where :math:`\boldsymbol{\sigma}_{\mathbf{z}^\ast}^2` are the variances extracted using the + membership array :math:`\mathbf{z}^\ast`. + + **Updating the Gaussian Mixture Model:** + + When the GMM is set using the ``gmm`` property, the GMM remains static throughout the inversion. + When the ``gmm`` property set as ``None``, the GMM is learned and updated after every model update. + That is, we assume the GMM defined using the ``gmmref`` property is not completely representative + of the physical property distributions for each rock unit, and we update the all of the means + (:math:`\boldsymbol{\mu}`), covariances (:math:`\boldsymbol{\Sigma}`) and proportion constants + (:math:`\boldsymbol{\gamma}`) defining the GMM :math:`\Theta`. This is done by solving: + + .. math:: + \max_\Theta \; \mathcal{P}(\Theta | \mathbf{m}) + + using a MAP variation of the expectation-maximization clustering algorithm introduced in + Dempster (et al. 1977). + + **Updating the Membership Array:** + + As the model (and GMM) are updated throughout the inversion, the rock unit considered most + indicative of the geology within each cell is updated; which is represented by the membership + array :math:`\mathbf{z}^\ast`. W. For the current GMM with means (:math:`\boldsymbol{\mu}`), + covariances (:math:`\boldsymbol{\Sigma}`) and proportion constants (:math:`\boldsymbol{\gamma}`), + we solve the following for each cell: + + .. math:: + z_i^\ast = \max_n \; \gamma_{i,n} \, \mathcal{N} (\mathbf{m}_i | \boldsymbol{\mu}_n , \boldsymbol{\Sigma}_n) + + where + + - :math:`\mathbf{m_i}` are the model values for cell :math:`i`, + - :math:`\gamma_{i,n}` is the proportion for cell :math:`i` and rock unit :math:`n` + - :math:`\boldsymbol{\mu}_n` are the mean property values for unit :math:`n`, + - :math:`\boldsymbol{\Sigma}_n` are the covariances for unit :math:`n`, and + - :math:`\mathcal{N}` represent the multivariate Gaussian distribution. + + """ + + def __init__( + self, + mesh, + gmmref, + alpha_x=None, + alpha_y=None, + alpha_z=None, + alpha_xx=0.0, + alpha_yy=0.0, + alpha_zz=0.0, + gmm=None, + wiresmap=None, + maplist=None, + alpha_pgi=1.0, + approx_hessian=True, + approx_gradient=True, + approx_eval=True, + weights_list=None, + non_linear_relationships: bool = False, + reference_model_in_smooth: bool = False, + **kwargs, + ): + self._wiresmap = wiresmap + self._maplist = maplist + self.regularization_mesh = mesh + self.gmmref = copy.deepcopy(gmmref) + self.gmmref.order_clusters_GM_weight() + + objfcts = [ + PGIsmallness( + gmmref, + mesh=self.regularization_mesh, + gmm=gmm, + wiresmap=self.wiresmap, + maplist=self.maplist, + approx_eval=approx_eval, + approx_gradient=approx_gradient, + approx_hessian=approx_hessian, + non_linear_relationships=non_linear_relationships, + weights=weights_list, + **kwargs, + ) + ] + + if not isinstance(weights_list, list): + weights_list = [weights_list] * len(self.maplist) + + for model_map, wire, weights in zip( + self.maplist, self.wiresmap.maps, weights_list + ): + weights_i = {"pgi-weights": weights} if weights is not None else None + objfcts += [ + WeightedLeastSquares( + alpha_s=0.0, + alpha_x=alpha_x, + alpha_y=alpha_y, + alpha_z=alpha_z, + alpha_xx=alpha_xx, + alpha_yy=alpha_yy, + alpha_zz=alpha_zz, + mesh=self.regularization_mesh, + mapping=model_map * wire[1], + weights=weights_i, + **kwargs, + ) + ] + + super().__init__(objfcts=objfcts, unpack_on_add=False) + self.reference_model_in_smooth = reference_model_in_smooth + self.alpha_pgi = alpha_pgi + + @property + def alpha_pgi(self): + """Scaling constant for the PGI smallness term. + + Returns + ------- + float + Scaling constant for the PGI smallness term. + """ + if getattr(self, "_alpha_pgi", None) is None: + self._alpha_pgi = self.multipliers[0] + return self._alpha_pgi + + @alpha_pgi.setter + def alpha_pgi(self, value): + value = validate_float("alpha_pgi", value, min_val=0.0) + self._alpha_pgi = value + self._multipliers[0] = value + + @property + def gmm(self): + """Gaussian mixture model. + + If set prior to inversion, the Gaussian mixture model can be left static throughout + the inversion, or updated using the :class:`.directives.PGI_UpdateParameters` directive. + If this property is not set prior to inversion, the + :class:`.directives.PGI_UpdateParameters` directive must be used to ensure there + is a Gaussian mixture model for the inversion. + + Returns + ------- + None, simpeg.utils.WeightedGaussianMixture + Gaussian mixture model. + """ + return self.objfcts[0].gmm + + @gmm.setter + def gmm(self, gm): + self.objfcts[0].gmm = copy.deepcopy(gm) + + def membership(self, m): + """Compute and return membership array for the model provided. + + The membership array stores the index of the rock unit most representative of each cell. + For a Gaussian mixture model containing the means and covariances for model parameter + types (physical property types) for all rock units, this method computes the membership + array for the model `m` provided. For a description of the membership array, see the + *Notes* section within the :class:`PGI` documentation. + + Parameters + ---------- + m : (n_param ) numpy.ndarray of float + The model. + + Returns + ------- + (n_active, ) numpy.ndarray of int + The membership array. + """ + return self.objfcts[0].membership(m) + + def compute_quasi_geology_model(self): + r"""Compute and return quasi geology model. + + For each active cell in the mesh, this method returns the mean values in the Gaussian + mixture model for the most representative rock unit, given the current model. See the + *Notes* section for a comprehensive description. + + Returns + ------- + (n_param ) numpy.ndarray + The quasi geology physical property model. + + Notes + ----- + Consider a Gaussian mixture model (GMM) for :math:`K` physical property types and + :math:`N` rock units. The mean model parameter values for rock unit + :math:`n \in \{ 1, \ldots , N \}` in the GMM is represented by a vector + :math:`\boldsymbol{\mu}_n` of length :math:`K`. For each active cell in the mesh, the + `compute_quasi_geology_model` method computes: + + .. math:: + g_i = \min_{\boldsymbol{\mu}_n} \big \| \mathbf{m}_i - \boldsymbol{\mu}_n \big \|^2 + + where :math:`\mathbf{m}_i` are the model parameter values for cell :math:`i` for the + current model. The ordering of the output vector :math:`\mathbf{g}` is the same as the + model :math:`\mathbf{m}`. + """ + return self.objfcts[0].compute_quasi_geology_model() + + @property + def wiresmap(self): + """Mapping from the model to the model parameters of each type. + + Returns + ------- + simpeg.maps.Wires + Mapping from the model to the model parameters of each type. + """ + if getattr(self, "_wiresmap", None) is None: + self._wiresmap = Wires(("m", self.regularization_mesh.nC)) + return self._wiresmap + + @property + def maplist(self): + """Ordered list of mappings from model values to physical property values. + + Returns + ------- + list of simpeg.maps + Ordered list of mappings from model values to physical property values; + one for each physical property. + """ + if getattr(self, "_maplist", None) is None: + self._maplist = [ + IdentityMap(nP=self.regularization_mesh.nC) + for maps in self.wiresmap.maps + ] + return self._maplist + + @property + def regularization_mesh(self) -> RegularizationMesh: + """Regularization mesh. + + Mesh on which the regularization is discretized. This is not the same as + the mesh on which the simulation is defined. + + Returns + ------- + discretize.base.RegularizationMesh + Mesh on which the regularization is discretized. + """ + return self._regularization_mesh + + @regularization_mesh.setter + def regularization_mesh(self, mesh: RegularizationMesh): + if not isinstance(mesh, RegularizationMesh): + mesh = RegularizationMesh(mesh) + + self._regularization_mesh = mesh + + @property + def reference_model_in_smooth(self) -> bool: + """Whether to include the reference model in the smoothness objective functions. + + Returns + ------- + bool + Whether to include the reference model in the smoothness objective functions. + """ + return self._reference_model_in_smooth + + @reference_model_in_smooth.setter + def reference_model_in_smooth(self, value: bool): + if not isinstance(value, bool): + raise TypeError( + "'reference_model_in_smooth must be of type 'bool'. " + f"Value of type {type(value)} provided." + ) + self._reference_model_in_smooth = value + for fct in self.objfcts[1:]: + if getattr(fct, "reference_model_in_smooth", None) is not None: + fct.reference_model_in_smooth = value + + @property + def reference_model(self) -> np.ndarray: + """Reference model. + + Returns + ------- + None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. + """ + return self.objfcts[0].reference_model + + @reference_model.setter + def reference_model(self, values: np.ndarray | float): + if isinstance(values, float): + values = np.ones(self._nC_residual) * values + + 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 old mode 100755 new mode 100644 similarity index 59% rename from SimPEG/regularization/regularization_mesh.py rename to simpeg/regularization/regularization_mesh.py index 8308bd56ef..dea11bb2f1 --- a/SimPEG/regularization/regularization_mesh.py +++ b/simpeg/regularization/regularization_mesh.py @@ -1,9 +1,9 @@ import numpy as np import scipy.sparse as sp -from SimPEG.utils.code_utils import deprecate_property, validate_active_indices -from .. import props -from .. import utils +from simpeg.utils.code_utils import deprecate_property, validate_active_indices + +from .. import props, utils ############################################################################### # # @@ -13,16 +13,24 @@ class RegularizationMesh(props.BaseSimPEG): - """ - **Regularization Mesh** - - This contains the operators used in the regularization. Note that these - are not necessarily true differential operators, but are constructed from - a `discretize` Mesh. - - :param discretize.base.BaseMesh mesh: problem mesh - :param numpy.ndarray active_cells: bool array, size nC, that is True where we have active cells. Used to reduce the operators so we regularize only on active cells - + """Regularization Mesh + + The ``RegularizationMesh`` class is used to construct differencing and averaging operators + for the objective function(s) defining the regularization. In practice, these operators are + not constructed by creating instances of ``RegularizationMesh``. The operators are instead + constructed (and sometimes stored) when called as a property of the mesh. + The ``RegularizationMesh`` class is built using much of the functionality from the + :py:class:`discretize.operators.differential_operators.DiffOperators` class. + However, operators constructed using the ``RegularizationMesh`` class have been modified to + act only on interior faces and active cells in the inversion, thus reducing computational cost. + + Parameters + ---------- + mesh : discretize.base.BaseMesh + Mesh on which the discrete set of model parameters are defined. + active_cells : None, (n_cells, ) numpy.ndarray of bool + Boolean array defining the set of mesh cells that are active in the inversion. + If ``None``, all cells are active. """ regularization_type = None # or 'Base' @@ -35,12 +43,21 @@ def __init__(self, mesh, active_cells=None, **kwargs): @property def active_cells(self) -> np.ndarray: - """A boolean array indicating whether a cell is active + """Active cells on the regularization mesh. + + A boolean array defining the cells in the regularization mesh that are active + (i.e. updated) throughout the inversion. The values of inactive cells + remain equal to their starting model values. + + Returns + ------- + (n_cells, ) array of bool Notes ----- - If this is set with an array of integers, it interprets it as an array - listing the active cell indices. + If the property is set using a ``numpy.ndarray`` of ``int``, the setter interprets the + array as representing the indices of the active cells. When called however, the quantity + will have been internally converted to a boolean array. """ return self._active_cells @@ -80,8 +97,12 @@ def active_cells(self, values: np.ndarray): @property def vol(self) -> np.ndarray: - """ - Reduced volume vector. + """Volumes of active mesh cells. + + Returns + ------- + (n_active, ) numpy.ndarray of float + Volumes of active mesh cells. """ if self.active_cells is None: return self.mesh.cell_volumes @@ -90,26 +111,39 @@ def vol(self) -> np.ndarray: return self._vol @property - def nC(self) -> int: - """ - Number of cells being regularized. + def n_cells(self) -> int: + """Number of active cells. + + Returns + ------- + int + Number of active cells. """ if self.active_cells is not None: return int(self.active_cells.sum()) - return self.mesh.nC + return self.mesh.n_cells + + nC = n_cells @property def dim(self) -> int: - """ - Dimension of regularization mesh (1D, 2D, 3D) + """Dimension of regularization mesh. + + Returns + ------- + {1, 2, 3} + Dimension of the regularization mesh. """ return self.mesh.dim @property def Pac(self) -> sp.csr_matrix: - """ - Projection matrix that takes from the reduced space of active cells to - full modelling space (ie. nC x nactive_cells). + """Projection matrix from active cells to all mesh cells. + + Returns + ------- + (n_cells, n_active) scipy.sparse.csr_matrix + Projection matrix from active cells to all mesh cells. """ if getattr(self, "_Pac", None) is None: if self.active_cells is None: @@ -120,9 +154,12 @@ def Pac(self) -> sp.csr_matrix: @property def Pafx(self) -> sp.csr_matrix: - """ - Projection matrix that takes from the reduced space of active x-faces - to full modelling space (ie. nFx x nactive_cells_Fx ) + """Projection matrix from active x-faces to all x-faces in the mesh. + + Returns + ------- + (n_faces_x, n_active_faces_x) scipy.sparse.csr_matrix + Projection matrix from active x-faces to all x-faces in the mesh. """ if getattr(self, "_Pafx", None) is None: if self.mesh._meshType == "TREE": @@ -143,9 +180,12 @@ def Pafx(self) -> sp.csr_matrix: @property def Pafy(self) -> sp.csr_matrix: - """ - Projection matrix that takes from the reduced space of active y-faces - to full modelling space (ie. nFy x nactive_cells_Fy ). + """Projection matrix from active y-faces to all y-faces in the mesh. + + Returns + ------- + (n_faces_y, n_active_faces_y) scipy.sparse.csr_matrix + Projection matrix from active y-faces to all y-faces in the mesh. """ if getattr(self, "_Pafy", None) is None: if self.mesh._meshType == "TREE": @@ -168,9 +208,12 @@ def Pafy(self) -> sp.csr_matrix: @property def Pafz(self) -> sp.csr_matrix: - """ - Projection matrix that takes from the reduced space of active z-faces - to full modelling space (ie. nFz x nactive_cells_Fz ). + """Projection matrix from active z-faces to all z-faces in the mesh. + + Returns + ------- + (n_faces_z, n_active_faces_z) scipy.sparse.csr_matrix + Projection matrix from active z-faces to all z-faces in the mesh. """ if getattr(self, "_Pafz", None) is None: if self.mesh._meshType == "TREE": @@ -191,9 +234,14 @@ def Pafz(self) -> sp.csr_matrix: @property def average_face_to_cell(self) -> sp.csr_matrix: - """ - Vertically stacked matrix of cell averaging operators from active - cell centers to active faces along each dimension of the mesh. + """Averaging operator from faces to cell centers. + + Built from :py:property:`~discretize.operators.differential_operators.DiffOperators.average_face_to_cell`. + + Returns + ------- + (n_cells, n_faces) scipy.sparse.csr_matrix + Averaging operator from faces to cell centers. """ if self.dim == 1: return self.aveFx2CC @@ -204,8 +252,16 @@ def average_face_to_cell(self) -> sp.csr_matrix: @property def aveFx2CC(self) -> sp.csr_matrix: - """ - Averaging from active cell centers to active x-faces. + """Averaging operator from active cell centers to active x-faces. + + Modified from the transpose of + :py:property:`~discretize.operators.differential_operators.DiffOperators.aveFx2CC`; + an operator that projects from all x-faces to all cell centers. + + Returns + ------- + (n_active_cells, n_active_faces_x) scipy.sparse.csr_matrix + Averaging operator from active cell centers to active x-faces. """ if getattr(self, "_aveFx2CC", None) is None: if self.mesh._meshType == "TREE": @@ -219,8 +275,16 @@ def aveFx2CC(self) -> sp.csr_matrix: @property def aveCC2Fx(self) -> sp.csr_matrix: - """ - Averaging from active x-faces to active cell centers. + """Averaging operator from active x-faces to active cell centers. + + Modified from + :py:property:`~discretize.operators.differential_operators.DiffOperators.aveCC2Fx`; + an operator that projects from all x-faces to all cell centers. + + Returns + ------- + (n_active_faces_x, n_active_cells) scipy.sparse.csr_matrix + Averaging operator from active x-faces to active cell centers. """ if getattr(self, "_aveCC2Fx", None) is None: if self.mesh._meshType == "TREE": @@ -235,8 +299,16 @@ def aveCC2Fx(self) -> sp.csr_matrix: @property def aveFy2CC(self) -> sp.csr_matrix: - """ - Averaging from active cell centers to active y-faces. + """Averaging operator from active cell centers to active y-faces. + + Modified from the transpose of + :py:property:`~discretize.operators.differential_operators.DiffOperators.aveFy2CC`; + an operator that projects from y-faces to cell centers. + + Returns + ------- + (n_active_cells, n_active_faces_y) scipy.sparse.csr_matrix + Averaging operator from active cell centers to active y-faces. """ if getattr(self, "_aveFy2CC", None) is None: if self.mesh._meshType == "TREE": @@ -252,8 +324,16 @@ def aveFy2CC(self) -> sp.csr_matrix: @property def aveCC2Fy(self) -> sp.csr_matrix: - """ - Averaging matrix from active y-faces to active cell centers. + """Averaging operator from active y-faces to active cell centers. + + Modified from + :py:property:`~discretize.operators.differential_operators.DiffOperators.aveCC2Fy`; + an operator that projects from all y-faces to all cell centers. + + Returns + ------- + (n_active_faces_y, n_active_cells) scipy.sparse.csr_matrix + Averaging operator from active y-faces to active cell centers. """ if getattr(self, "_aveCC2Fy", None) is None: if self.mesh._meshType == "TREE": @@ -270,8 +350,16 @@ def aveCC2Fy(self) -> sp.csr_matrix: @property def aveFz2CC(self) -> sp.csr_matrix: - """ - Averaging from active cell centers to active z-faces. + """Averaging operator from active cell centers to active z-faces. + + Modified from the transpose of + :py:property:`~discretize.operators.differential_operators.DiffOperators.aveFz2CC`; + an operator that projects from z-faces to cell centers. + + Returns + ------- + (n_active_cells, n_active_faces_z) scipy.sparse.csr_matrix + Averaging operator from active cell centers to active z-faces. """ if getattr(self, "_aveFz2CC", None) is None: if self.mesh._meshType == "TREE": @@ -285,8 +373,16 @@ def aveFz2CC(self) -> sp.csr_matrix: @property def aveCC2Fz(self) -> sp.csr_matrix: - """ - Averaging matrix from active z-faces to active cell centers. + """Averaging operator from active z-faces to active cell centers. + + Modified from + :py:property:`~discretize.operators.differential_operators.DiffOperators.aveCC2Fz`; + an operator that projects from all z-faces to all cell centers. + + Returns + ------- + (n_active_faces_z, n_active_cells) scipy.sparse.csr_matrix + Averaging operator from active z-faces to active cell centers. """ if getattr(self, "_aveCC2Fz", None) is None: if self.mesh._meshType == "TREE": @@ -301,16 +397,27 @@ def aveCC2Fz(self) -> sp.csr_matrix: @property def base_length(self) -> float: - """The smallest core cell size.""" + """Smallest dimension (i.e. edge length) for smallest cell in the mesh. + + Returns + ------- + float + Smallest dimension (i.e. edge length) for smallest cell in the mesh. + """ if getattr(self, "_base_length", None) is None: self._base_length = self.mesh.edge_lengths.min() return self._base_length @property def cell_gradient(self) -> sp.csr_matrix: - """ - Vertically stacked matrix of cell gradients along each dimension of - the mesh. + """Cell gradient operator (cell centers to faces). + + Built from :py:property:`~discretize.operators.differential_operators.DiffOperators.cell_gradient`. + + Returns + ------- + (n_faces, n_cells) scipy.sparse.csr_matrix + Cell gradient operator (cell centers to faces). """ if self.dim == 1: return self.cell_gradient_x @@ -323,8 +430,16 @@ def cell_gradient(self) -> sp.csr_matrix: @property def cell_gradient_x(self) -> sp.csr_matrix: - """ - Cell centered gradient matrix for active cells in the x-direction. + """Cell-centered x-derivative operator on active cells. + + Cell centered x-derivative operator that maps from active cells + to active x-faces. Modified from + :py:property:`~discretize.operators.differential_operators.DiffOperators.cell_gradient_x`. + + Returns + ------- + (n_active_faces_x, n_active_cells) scipy.sparse.csr_matrix + Cell-centered x-derivative operator on active cells. """ if getattr(self, "_cell_gradient_x", None) is None: if self.mesh._meshType == "TREE": @@ -345,8 +460,16 @@ def cell_gradient_x(self) -> sp.csr_matrix: @property def cell_gradient_y(self) -> sp.csr_matrix: - """ - Cell centered gradient matrix for active cells in the y-direction. + """Cell-centered y-derivative operator on active cells. + + Cell centered y-derivative operator that maps from active cells + to active y-faces. Modified from + :py:property:`~discretize.operators.differential_operators.DiffOperators.cell_gradient_y`. + + Returns + ------- + (n_active_faces_y, n_active_cells) scipy.sparse.csr_matrix + Cell-centered y-derivative operator on active cells. """ if getattr(self, "_cell_gradient_y", None) is None: if self.mesh._meshType == "TREE": @@ -367,8 +490,16 @@ def cell_gradient_y(self) -> sp.csr_matrix: @property def cell_gradient_z(self) -> sp.csr_matrix: - """ - Cell centered gradient matrix for active cells in the z-direction. + """Cell-centered z-derivative operator on active cells. + + Cell centered z-derivative operator that maps from active cells + to active z-faces. Modified from + :py:property:`~discretize.operators.differential_operators.DiffOperators.cell_gradient_z`. + + Returns + ------- + (n_active_faces_z, n_active_cells) scipy.sparse.csr_matrix + Cell-centered z-derivative operator on active cells. """ if getattr(self, "_cell_gradient_z", None) is None: if self.mesh._meshType == "TREE": @@ -392,59 +523,71 @@ def cell_gradient_z(self) -> sp.csr_matrix: "cellDiffx", "cell_gradient_x", "0.19.0", - error=False, - future_warn=True, + error=True, ) cellDiffy = deprecate_property( cell_gradient_y, "cellDiffy", "cell_gradient_y", "0.19.0", - error=False, - future_warn=True, + error=True, ) cellDiffz = deprecate_property( cell_gradient_z, "cellDiffz", "cell_gradient_z", "0.19.0", - error=False, - future_warn=True, + error=True, ) @property def cell_distances_x(self) -> np.ndarray: - """ - Cell center distance array along the x-direction. + """Cell center distance array along the x-direction. + + Returns + ------- + (n_active_faces_x, ) numpy.ndarray + Cell center distance array along the x-direction. """ if getattr(self, "_cell_distances_x", None) is None: self._cell_distances_x = self.cell_gradient_x.max( axis=1 - ).toarray().flatten() ** (-1.0) + ).toarray().ravel() ** (-1.0) + return self._cell_distances_x @property def cell_distances_y(self) -> np.ndarray: - """ - Cell center distance array along the y-direction. + """Cell center distance array along the y-direction. + + Returns + ------- + (n_active_faces_y, ) numpy.ndarray + Cell center distance array along the y-direction. """ if getattr(self, "_cell_distances_y", None) is None: self._cell_distances_y = self.cell_gradient_y.max( axis=1 - ).toarray().flatten() ** (-1.0) + ).toarray().ravel() ** (-1.0) + return self._cell_distances_y @property def cell_distances_z(self) -> np.ndarray: - """ - Cell center distance array along the z-direction. + """Cell center distance array along the z-direction. + + Returns + ------- + (n_active_faces_z, ) numpy.ndarray + Cell center distance array along the z-direction. """ if getattr(self, "_cell_distances_z", None) is None: self._cell_distances_z = self.cell_gradient_z.max( axis=1 - ).toarray().flatten() ** (-1.0) + ).toarray().ravel() ** (-1.0) + return self._cell_distances_z # Make it look like it's in the regularization module -RegularizationMesh.__module__ = "SimPEG.regularization" +RegularizationMesh.__module__ = "simpeg.regularization" diff --git a/simpeg/regularization/sparse.py b/simpeg/regularization/sparse.py new file mode 100644 index 0000000000..a917e7ecbd --- /dev/null +++ b/simpeg/regularization/sparse.py @@ -0,0 +1,1095 @@ +from __future__ import annotations + +import numpy as np + +from discretize.base import BaseMesh + +from .base import ( + BaseRegularization, + WeightedLeastSquares, + RegularizationMesh, + Smallness, + SmoothnessFirstOrder, +) +from .. import utils +from ..utils import ( + validate_ndarray_with_shape, + validate_float, + validate_type, + validate_string, +) + + +class BaseSparse(BaseRegularization): + """Base class for sparse-norm regularization. + + The ``BaseSparse`` class defines properties and methods inherited by sparse-norm + regularization classes. Sparse-norm regularization in SimPEG is implemented using + an iteratively re-weighted least squares (IRLS) approach. The ``BaseSparse`` class + however, is not directly used to define the regularization for the inverse problem. + + Parameters + ---------- + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. This is not necessarily + the same as the mesh on which the simulation is defined. + active_cells : None, (n_cells, ) numpy.ndarray of bool + 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. + reference_model : None, (n_param, ) numpy.ndarray + Reference model values used to constrain the inversion. If ``None``, the starting model + is set as the reference model. + units : None, str + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) + numpy.ndarray that is defined on the :py:class:`~.regularization.RegularizationMesh`. + norm : float + The norm used in the regularization function. Must be between within the interval [0, 2]. + irls_scaled : bool + If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. + If ``False``, do not scale. + irls_threshold : float + Constant added to IRLS weights to ensures stability in the algorithm. + + """ + + def __init__(self, mesh, norm=2.0, irls_scaled=True, irls_threshold=1e-8, **kwargs): + super().__init__(mesh=mesh, **kwargs) + self.norm = norm + self.irls_scaled = irls_scaled + self.irls_threshold = irls_threshold + + @property + def irls_scaled(self) -> bool: + """Scale IRLS weights. + + When ``True``, scaling is applied when computing IRLS weights. + The scaling acts to preserve the balance between the data misfit and the components of + the regularization based on the derivative of the l2-norm measure. And it assists the + convergence by ensuring the model does not deviate + aggressively from the global 2-norm solution during the first few IRLS iterations. + For a comprehensive description, see the documentation for :py:meth:`get_lp_weights` . + + Returns + ------- + bool + Whether to scale IRLS weights. + """ + return self._irls_scaled + + @irls_scaled.setter + def irls_scaled(self, value: bool): + self._irls_scaled = validate_type("irls_scaled", value, bool, cast=False) + + @property + def irls_threshold(self): + r"""Stability constant for computing IRLS weights. + + Returns + ------- + float + Stability constant for computing IRLS weights. + """ + return self._irls_threshold + + @irls_threshold.setter + def irls_threshold(self, value): + self._irls_threshold = validate_float( + "irls_threshold", value, min_val=0.0, inclusive_min=False + ) + + @property + def norm(self): + r"""Norm for the sparse regularization. + + Returns + ------- + None, float, (n_cells, ) numpy.ndarray + Norm for the sparse regularization. If ``None``, a 2-norm is used. + A float within the interval [0,2] represents a constant norm applied for all cells. + A ``numpy.ndarray`` object, where each entry is used to apply a different norm to each cell in the mesh. + """ + return self._norm + + @norm.setter + def norm(self, value: float | np.ndarray | None): + if value is None: + value = np.ones(self._weights_shapes[0]) * 2.0 + expected_shapes = self._weights_shapes + if isinstance(expected_shapes, list): + expected_shapes = expected_shapes[0] + value = validate_ndarray_with_shape( + "norm", value, shape=[expected_shapes, (1,)], dtype=float + ) + if value.shape == (1,): + value = np.full(expected_shapes[0], value) + + if np.any(value < 0) or np.any(value > 2): + raise ValueError( + "Value provided for 'norm' should be in the interval [0, 2]" + ) + self._norm = value + + def get_lp_weights(self, f_m): + r"""Compute and return iteratively re-weighted least-squares (IRLS) weights. + + For a regularization kernel function :math:`\mathbf{f_m}(\mathbf{m})` + evaluated at model :math:`\mathbf{m}`, compute and return the IRLS weights. + See :py:meth:`Smallness.f_m` and :py:meth:`SmoothnessFirstOrder.f_m` for examples of + least-squares regularization kernels. + + For :class:`SparseSmallness`, *f_m* is a (n_cells, ) ``numpy.ndarray``. + For :class:`SparseSmoothness`, *f_m* is a ``numpy.ndarray`` whose length corresponds + to the number of faces along a particular orientation; e.g. for smoothness along x, + the length is (n_faces_x, ). + + Parameters + ---------- + f_m : numpy.ndarray + The regularization kernel function evaluated at the current model. + + Notes + ----- + For a regularization kernel function :math:`\mathbf{f_m}` evaluated at model + :math:`\mathbf{m}`, the IRLS weights are computed via: + + .. math:: + \mathbf{w_r} = \boldsymbol{\lambda} \oslash + \Big [ \mathbf{f_m}^{\!\! 2} + \epsilon^2 \Big ]^{1 - \mathbf{p}/2} + + where :math:`\oslash` represents elementwise division, :math:`\epsilon` is a small + constant added for stability of the algorithm (set using the `irls_threshold` property), + and :math:`\mathbf{p}` defines the `norm` at each cell. + + :math:`\boldsymbol{\lambda}` applies optional scaling to the IRLS weights + (when the `irls_scaled` property is ``True``). + The scaling acts to preserve the balance between the data misfit and the components of + the regularization based on the derivative of the l2-norm measure. And it assists the + convergence by ensuring the model does not deviate + aggressively from the global 2-norm solution during the first few IRLS iterations. + + To apply elementwise scaling, let + + .. math:: + f_{max} = \big \| \, \mathbf{f_m} \, \big \|_\infty + + And define a vector array :math:`\mathbf{\tilde{f}_{\! max}}` such that: + + .. math:: + \tilde{f}_{\! i,max} = \begin{cases} + f_{max} \;\;\; for \; p_i \geq 1 \\ + \frac{\epsilon}{\sqrt{1 - p_i}} \;\;\;\;\;\;\, for \; p_i < 1 + \end{cases} + + The elementwise scaling vector :math:`\boldsymbol{\lambda}` is: + + .. math:: + \boldsymbol{\lambda} = \bigg [ \frac{f_{max}}{\mathbf{\tilde{f}_{max}}} \bigg ] + \odot \bigg [ \mathbf{f_{\! max}}^{\!\! 2} + \epsilon^2} \bigg ]^{1 - \mathbf{p}/2} + """ + lp_scale = np.ones_like(f_m) + if self.irls_scaled: + # Scale on l2-norm gradient: f_m.max() + l2_max = np.ones_like(f_m) * np.abs(f_m).max() + # Compute theoretical maximum gradients for p < 1 + l2_max[self.norm < 1] = self.irls_threshold / np.sqrt( + 1.0 - self.norm[self.norm < 1] + ) + lp_values = l2_max / (l2_max**2.0 + self.irls_threshold**2.0) ** ( + 1.0 - self.norm / 2.0 + ) + lp_scale[lp_values != 0] = np.abs(f_m).max() / lp_values[lp_values != 0] + + return lp_scale / (f_m**2.0 + self.irls_threshold**2.0) ** ( + 1.0 - self.norm / 2.0 + ) + + +class SparseSmallness(BaseSparse, Smallness): + r"""Sparse smallness (compactness) regularization. + + ``SparseSmallness`` is used to recover models comprised of compact structures. + The level of compactness is controlled by the norm within the regularization + function; with more compact structures being recovered when a smaller norm is used. + Optionally, custom cell weights can be included to control the degree of compactness + being enforced throughout different regions the model. + + See the *Notes* section below for a comprehensive description. + + Parameters + ---------- + mesh : .regularization.RegularizationMesh + Mesh on which the regularization is discretized. Not the mesh used to + define the simulation. + norm : float, (n_cells, ) array_like + The norm defining sparseness in the regularization function. Use a ``float`` to define + the same norm for all mesh cells, or define an independent norm for each cell. All norm + values must be within the interval [0, 2]. + active_cells : None, (n_cells, ) numpy.ndarray of bool + 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. + reference_model : None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. + units : None, str + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to + a (n_cells, ) numpy.ndarray that is defined on the + :py:class:`regularization.RegularizationMesh`. + irls_scaled : bool + If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. + If ``False``, do not scale. + irls_threshold : float + Constant added to IRLS weights to ensures stability in the algorithm. + + Notes + ----- + We define the regularization function (objective function) for sparse smallness (compactness) as: + + .. math:: + \phi (m) = \int_\Omega \, w(r) \, + \Big | \, m(r) - m^{(ref)}(r) \, \Big |^{p(r)} \, dv + + where :math:`m(r)` is the model, :math:`m^{(ref)}(r)` is the reference model, :math:`w(r)` + is a user-defined weighting function and :math:`p(r) \in [0,2]` is a parameter which imposes + sparseness throughout the recovered model. More compact structures are recovered in regions + where :math:`p` is small. If the same level of sparseness is being imposed everywhere, + the exponent becomes a constant. + + 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: + + .. math:: + \phi (\mathbf{m}) = \sum_i + \tilde{w}_i \, \Big | m_i - m_i^{(ref)} \Big |^{p_i} + + where :math:`m_i \in \mathbf{m}` are the discrete model parameters defined on the mesh. + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account + for cell dimensions in the discretization and 2) apply user-defined weighting. + :math:`p_i \in \mathbf{p}` define the norm for each cell (set using `norm`). + + It is impractical to work with the general form directly, as its derivatives with respect + to the model are non-linear and discontinuous. Instead, the iteratively re-weighted + least-squares (IRLS) approach is used to approximate the sparse norm by iteratively solving + a set of convex least-squares problems. For IRLS iteration :math:`k`, we define: + + .. math:: + \phi \big (\mathbf{m}^{(k)} \big ) + = \sum_i \tilde{w}_i \, \Big | m_i^{(k)} - m_i^{(ref)} \Big |^{p_i} + \approx \sum_i \tilde{w}_i \, r_i^{(k)} \Big | m_i^{(k)} - m_i^{(ref)} \Big |^2 + + where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: + + .. math:: + r_i^{(k)} = \bigg [ \Big ( m_i^{(k-1)} - m_i^{(ref)} \Big )^2 + + \epsilon^2 \; \bigg ]^{{p_i}/2 - 1} + + and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). + For the set of model parameters :math:`\mathbf{m}` defined at cell centers, the objective + function for IRLS iteration :math:`k` can be expressed as follows: + + .. math:: + \phi \big ( \mathbf{m}^{(k)} \big ) \approx \Big \| \, + \mathbf{W}^{\! (k)} \big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + + where + + - :math:`\mathbf{m}^{(k)}` are the discrete model parameters at iteration :math:`k`, + - :math:`\mathbf{m}^{(ref)}` is a reference model (optional, set with `reference_model`), + - :math:`\mathbf{W}^{(k)}` is the weighting matrix for iteration :math:`k`. It applies the IRLS weights, user-defined weighting, and accounts for cell dimensions when the regularization function is discretized. + + **IRLS weights, user-defined weighting and the weighting matrix:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of + custom cell weights. And let :math:`\mathbf{r_s}^{\!\! (k)}` represent the IRLS weights + for iteration :math:`k`. The net weighting applied within the objective function + is given by: + + .. math:: + \mathbf{w}^{(k)} = \mathbf{r_s}^{\!\! (k)} \odot \mathbf{v} \odot \prod_j \mathbf{w_j} + + where :math:`\mathbf{v}` are the cell volumes. + For a description of how IRLS weights are updated at every iteration, see the documentation + for :py:meth:`update_weights`. + + The weighting matrix used to apply the weights is given by: + + .. math:: + \mathbf{W}^{(k)} = \textrm{diag} \Big ( \sqrt{\mathbf{w}^{(k)} \, } \Big ) + + Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + + >>> reg = SparseSmallness(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) + + """ + + _multiplier_pair = "alpha_s" + + def update_weights(self, m): + r"""Update the IRLS weights for sparse smallness regularization. + + Parameters + ---------- + m : numpy.ndarray + The model used to update the IRLS weights. + + Notes + ----- + For the model :math:`\mathbf{m}` provided, the regularization kernel function + for sparse smallness is given by: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{m} - \mathbf{m}^{(ref)} + + where :math:`\mathbf{m}^{(ref)}` is the reference model; see :py:meth:`Smallness.f_m` + for a more comprehensive definition. + + The IRLS weights are computed via: + + .. math:: + \mathbf{w_r} = \boldsymbol{\lambda} \oslash + \Big [ \mathbf{f_m}^{\!\! 2} + \epsilon^2 \Big ]^{1 - \mathbf{p}/2} + + where :math:`\oslash` represents elementwise division, :math:`\epsilon` is a small + constant added for stability of the algorithm (set using the `irls_threshold` property), + and :math:`\mathbf{p}` defines the norm for each cell (defined using the `norm` property). + + :math:`\boldsymbol{\lambda}` applies optional scaling to the IRLS weights + (when the `irls_scaled` property is ``True``). + The scaling acts to preserve the balance between the data misfit and the components of + the regularization based on the derivative of the l2-norm measure. And it assists the + convergence by ensuring the model does not deviate + aggressively from the global 2-norm solution during the first few IRLS iterations. + + To compute the scaling, let + + .. math:: + f_{max} = \big \| \, \mathbf{f_m} \, \big \|_\infty + + and define a vector array :math:`\mathbf{\tilde{f}_{\! max}}` such that: + + .. math:: + \tilde{f}_{\! i,max} = \begin{cases} + f_{max} \;\;\;\;\; for \; p_i \geq 1 \\ + \frac{\epsilon}{\sqrt{1 - p_i}} \;\;\; for \; p_i < 1 + \end{cases} + + The scaling quantity :math:`\boldsymbol{\lambda}` is: + + .. math:: + \boldsymbol{\lambda} = \Bigg [ \frac{f_{max}}{\mathbf{\tilde{f}_{max}}} \Bigg ] + \odot \Big [ \mathbf{\tilde{f}_{max}}^{\!\! 2} + \epsilon^2 \Big ]^{1 - \mathbf{p}/2} + """ + f_m = self.f_m(m) + self.set_weights(irls=self.get_lp_weights(f_m)) + + +class SparseSmoothness(BaseSparse, SmoothnessFirstOrder): + r"""Sparse smoothness (blockiness) regularization. + + ``SparseSmoothness`` is used to recover models comprised of blocky structures. + The level of blockiness is controlled by the choice in norm within the regularization + function; with more blocky structures being recovered when a smaller norm is used. + Optionally, custom cell weights can be included to control the degree of blockiness being + enforced throughout different regions the model. + + See the *Notes* section below for a comprehensive description. + + Parameters + ---------- + mesh : .regularization.RegularizationMesh + Mesh on which the regularization is discretized. Not the mesh used to + define the simulation. + orientation : {'x','y','z'} + The direction along which sparse smoothness is applied. + norm : float, array_like + The norm defining sparseness thoughout the regularization function. Must be within the + interval [0,2]. There are several options: + + - ``float``: constant sparse norm throughout the domain. + - (n_faces, ) ``array_like``: define the sparse norm independently at each face set by `orientation` (e.g. x-faces). + - (n_cells, ) ``array_like``: define the sparse norm independently for each cell. Will be averaged to faces specified by `orientation` (e.g. x-faces). + + active_cells : None, (n_cells, ) numpy.ndarray of bool + 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. + reference_model : None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. To include the reference model in the regularization, the + `reference_model_in_smooth` property must be set to ``True``. + reference_model_in_smooth : bool, optional + Whether to include the reference model in the smoothness terms. + units : None, str + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. + weights : None, dict + Custom weights for the least-squares function. Each ``key`` points to + a ``numpy.ndarray`` that is defined on the :py:class:`regularization.RegularizationMesh`. + A (n_cells, ) ``numpy.ndarray`` is used to define weights at cell centers, which are + averaged to the appropriate faces internally when weighting is applied. + A (n_faces, ) ``numpy.ndarray`` is used to define weights directly on the faces specified + by the `orientation` input argument. + irls_scaled : bool + If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. + If ``False``, do not scale. + irls_threshold : float + Constant added to IRLS weights to ensures stability in the algorithm. + gradient_type : {"total", "component"} + Gradient measure used in the IRLS re-weighting. Whether to re-weight using the total + gradient or components of the gradient. + + Notes + ----- + The regularization function (objective function) for sparse smoothness (blockiness) + along the x-direction as: + + .. math:: + \phi (m) = \int_\Omega \, w(r) \, + \Bigg | \, \frac{\partial m}{\partial x} \, \Bigg |^{p(r)} \, dv + + where :math:`m(r)` is the model, :math:`w(r)` + is a user-defined weighting function and :math:`p(r) \in [0,2]` is a parameter which imposes + sparseness throughout the recovered model. Sharper boundaries are recovered in regions + where :math:`p(r)` is small. If the same level of sparseness is being imposed everywhere, + the exponent becomes a constant. + + For implementation within SimPEG, the regularization function and its variables + must be discretized onto a `mesh`. The discrete 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 |^{p_i} + + where :math:`m_i \in \mathbf{m}` are the discrete model parameters defined on the mesh. + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account + for cell dimensions in the discretization and 2) apply user-defined weighting. + :math:`p_i \in \mathbf{p}` define the norm for each face (set using `norm`). + + It is impractical to work with the general form directly, as its derivatives with respect + to the model are non-linear and discontinuous. Instead, the iteratively re-weighted + least-squares (IRLS) approach is used to approximate the sparse norm by iteratively solving + a set of convex least-squares problems. For IRLS iteration :math:`k`, we define: + + .. math:: + \phi \big (\mathbf{m}^{(k)} \big ) + = \sum_i + \tilde{w}_i \, \Bigg | \, \frac{\partial m_i^{(k)}}{\partial x} \Bigg |^{p_i} + \approx \sum_i \tilde{w}_i \, r_i^{(k)} + \Bigg | \, \frac{\partial m_i^{(k)}}{\partial x} \Bigg |^2 + + where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: + + .. math:: + r_i^{(k)} = \Bigg [ \Bigg ( \frac{\partial m_i^{(k-1)}}{\partial x} \Bigg )^2 + + \epsilon^2 \; \Bigg ]^{{p_i}/2 - 1} + + and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). + For the set of model parameters :math:`\mathbf{m}` defined at cell centers, the objective + function for IRLS iteration :math:`k` can be expressed as follows: + + .. math:: + \phi \big ( \mathbf{m}^{(k)} \big ) \approx \Big \| \, + \mathbf{W}^{(k)} \, \mathbf{G_x} \, \mathbf{m}^{(k)} \Big \|^2 + + where + + - :math:`\mathbf{m}^{(k)}` are the discrete model parameters at iteration :math:`k`, + - :math:`\mathbf{G_x}` is the partial cell-gradient operator along x (x-derivative), + - :math:`\mathbf{W}^{(k)}` is the weighting matrix for iteration :math:`k`. It applies the IRLS weights, user-defined weighting, and accounts for cell dimensions when the regularization function is discretized. + + Note that since :math:`\mathbf{G_x}` maps from cell centers to x-faces, the weighting matrix + 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 smoothness regularization. + In this case, the least-squares problem for IRLS iteration :math:`k` becomes: + + .. math:: + \phi \big ( \mathbf{m}^{(k)} \big ) \approx \Big \| \, + \mathbf{W}^{(k)} \mathbf{G_x} + \big [ \mathbf{m}^{(k)} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + + This functionality is used by setting :math:`\mathbf{m}^{(ref)}` with the + `reference_model` property, and by setting the `reference_model_in_smooth` parameter + to ``True``. + + **IRLS weights, user-defined weighting and the weighting matrix:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of + custom weights defined on the faces specified by the `orientation` property; + i.e. x-faces for smoothness along the x-direction. Each set of weights were either defined + directly on the faces or have been averaged from cell centers. And let + :math:`\mathbf{r_x}^{\!\! (k)}` represent the IRLS weights for iteration :math:`k`. + The net weighting applied within the objective function is given by: + + .. math:: + \mathbf{w}^{(k)} = \mathbf{r_x}^{\!\! (k)} \odot \mathbf{v_x} \odot \prod_j \mathbf{w_j} + + where :math:`\mathbf{v_x}` are cell volumes projected to x-faces; i.e. where the + x-derivative lives. For a description of how IRLS weights are updated at every iteration, + see the documentation for :py:meth:`update_weights`. + + The weighting matrix used to apply the weights is given by: + + .. math:: + \mathbf{W}^{(k)} = \textrm{diag} \Big ( \sqrt{\mathbf{w}^{(k)} \, } \Big ) + + Each set of custom weights is stored within a ``dict`` as an ``numpy.ndarray``. + A (n_cells, ) ``numpy.ndarray`` is used to define weights at cell centers, which are + averaged to the appropriate faces internally when weighting is applied. + A (n_faces, ) ``numpy.ndarray`` is used to define weights directly on the faces specified + by the `orientation` input argument. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + + >>> array_1 = np.ones(mesh.n_cells) # weights at cell centers + >>> array_2 = np.ones(mesh.n_faces_x) # weights directly on x-faces + >>> reg = SparseSmoothness( + >>> mesh, orientation='x', weights={'weights_1': array_1, 'weights_2': array_2} + >>> ) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) + + """ + + 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) + + def update_weights(self, m): + r"""Update the IRLS weights for sparse smoothness regularization. + + Parameters + ---------- + m : numpy.ndarray + The model used to update the IRLS weights. + + Notes + ----- + Let us consider the IRLS weights for sparse smoothness along the x-direction. + When the class property `gradient_type`=`'components'`, IRLS weights are computed + using the regularization kernel function and we define: + + .. math:: + \mathbf{f_m} = \mathbf{G_x} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] + + where :math:`\mathbf{m}` is the model provided, :math:`\mathbf{G_x}` is the partial cell + gradient operator along x (i.e. x-derivative), and :math:`\mathbf{m}^{(ref)}` is a + reference model (optional, activated using `reference_model_in_smooth`). + See :py:meth:`SmoothnessFirstOrder.f_m` for a more comprehensive definition of the + regularization kernel function. + + However, when the class property `gradient_type`=`'total'`, IRLS weights are computed + using the magnitude of the total gradient and we define: + + .. math:: + \mathbf{{f}_m} = \mathbf{A_{cx}} \sum_{j=x,y,z} \Big | \mathbf{A_j G_j} + \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big | + + where :math:`\mathbf{A_j}` for :math:`j=x,y,z` averages the partial gradients from their + respective faces to cell centers, and :math:`\mathbf{A_{cx}}` averages the sum of the + absolute values back to the appropriate faces. + + Once :math:`\mathbf{f_m}` is obtained, the IRLS weights are computed via: + + .. math:: + \mathbf{w_r} = \boldsymbol{\lambda} \oslash + \Big [ \mathbf{f_m}^{\!\! 2} + \epsilon^2 \Big ]^{1 - \mathbf{p}/2} + + where :math:`\oslash` represents elementwise division, :math:`\epsilon` is a small + constant added for stability of the algorithm (set using the `irls_threshold` property), + and :math:`\mathbf{p}` defines the norm for each element (set using the `norm` property). + + :math:`\boldsymbol{\lambda}` applies optional scaling to the IRLS weights + (when the `irls_scaled` property is ``True``). + The scaling acts to preserve the balance between the data misfit and the components of + the regularization based on the derivative of the l2-norm measure. And it assists the + convergence by ensuring the model does not deviate + aggressively from the global 2-norm solution during the first few IRLS iterations. + + To apply the scaling, let + + .. math:: + f_{max} = \big \| \, \mathbf{f_m} \, \big \|_\infty + + and define a vector array :math:`\mathbf{\tilde{f}_{\! max}}` such that: + + .. math:: + \tilde{f}_{\! i,max} = \begin{cases} + f_{max} \;\;\;\;\; for \; p_i \geq 1 \\ + \frac{\epsilon}{\sqrt{1 - p_i}} \;\;\; for \; p_i < 1 + \end{cases} + + The scaling vector :math:`\boldsymbol{\lambda}` is: + + .. math:: + \boldsymbol{\lambda} = \Bigg [ \frac{f_{max}}{\mathbf{\tilde{f}_{max}}} \Bigg ] + \odot \Big [ \mathbf{\tilde{f}_{max}}^{\!\! 2} + \epsilon^2 \Big ]^{1 - \mathbf{p}/2} + """ + if self.gradient_type == "total" and self.parent is not None: + f_m = np.zeros(self.regularization_mesh.nC) + for obj in self.parent.objfcts: + if isinstance(obj, SparseSmoothness): + avg = getattr(self.regularization_mesh, f"aveF{obj.orientation}2CC") + f_m += np.abs(avg * obj.f_m(m)) + + f_m = getattr(self.regularization_mesh, f"aveCC2F{self.orientation}") * f_m + + else: + f_m = self.f_m(m) + + self.set_weights(irls=self.get_lp_weights(f_m)) + + @property + def gradient_type(self) -> str: + """Gradient measure used to update IRLS weights for sparse smoothness. + + This property specifies whether the IRLS weights for sparse smoothness regularization + are updated using the total gradient (*"total"*) or using the partial gradient along + the smoothing orientation (*"components"*). To see how the IRLS weights are computed, + visit the documentation for :py:meth:`update_weights`. + + Returns + ------- + str in {"total", "components"} + Whether to re-weight using the total gradient or partial gradients along + smoothing orientations. + """ + return self._gradient_type + + @gradient_type.setter + def gradient_type(self, value: str): + self._gradient_type = validate_string( + "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. + + Apply regularization for recovering compact and/or blocky structures + using a weighted sum of :class:`SparseSmallness` and :class:`SparseSmoothness` + regularization functions. The level of compactness and blockiness is + controlled by the norms within the respective regularization functions; + with more sparse structures (compact and/or blocky) being recovered when smaller + norms are used. Optionally, custom cell weights can be applied to control + the degree of sparseness being enforced throughout different regions the model. + + See the *Notes* section below for a comprehensive description. + + Parameters + ---------- + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. This is not necessarily + the same as the mesh on which the simulation is defined. + active_cells : None, (n_cells, ) numpy.ndarray of bool + 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. + reference_model : None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. + reference_model_in_smooth : bool, optional + Whether to include the reference model in the smoothness terms. + units : None, str + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) + numpy.ndarray that is defined on the :py:class:`~.regularization.RegularizationMesh`. + alpha_s : float, optional + Scaling constant for the smallness regularization term. + alpha_x, alpha_y, alpha_z : float or None, optional + Scaling constants for the first order smoothness along x, y and z, respectively. + If set to ``None``, the scaling constant is set automatically according to the + value of the `length_scale` parameter. + length_scale_x, length_scale_y, length_scale_z : float, optional + First order smoothness length scales for the respective dimensions. + gradient_type : {"total", "component"} + Gradient measure used in the IRLS re-weighting. Whether to re-weight using the + total gradient or components of the gradient. + norms : (dim+1, ) numpy.ndarray + The respective norms used for the sparse smallness, x-smoothness, (y-smoothness + and z-smoothness) regularization function. Must all be within the interval [0, 2]. + E.g. `np.r_[2, 1, 1, 1]` uses a 2-norm on the smallness term and a 1-norm on all + smoothness terms. + irls_scaled : bool + If ``True``, scale the IRLS weights to preserve magnitude of the regularization + function. If ``False``, do not scale. + irls_threshold : float + Constant added to IRLS weights to ensures stability in the algorithm. + + Notes + ----- + Sparse regularization can be defined by a weighted sum of + :class:`SparseSmallness` and :class:`SparseSmoothness` + regularization functions. This corresponds to a model objective function + :math:`\phi_m (m)` of the form: + + .. math:: + \phi_m (m) = \alpha_s \int_\Omega \, w(r) + \Big | \, m(r) - m^{(ref)}(r) \, \Big |^{p_s(r)} \, dv + + \sum_{j=x,y,z} \alpha_j \int_\Omega \, w(r) + \Bigg | \, \frac{\partial m}{\partial \xi_j} \, \Bigg |^{p_j(r)} \, 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 applied to all terms. + :math:`\xi_j` for :math:`j=x,y,z` are unit directions along :math:`j`. + Parameters :math:`\alpha_s` and :math:`\alpha_j` for :math:`j=x,y,z` are multiplier constants + that weight the respective contributions of the smallness and smoothness terms in the + regularization. :math:`p_s(r) \in [0,2]` is a parameter which imposes sparse smallness + throughout the recovered model; where more compact structures are recovered in regions where + :math:`p_s(r)` is small. And :math:`p_j(r) \in [0,2]` for :math:`j=x,y,z` are parameters which + impose sparse smoothness throughout the recovered model along the specified direction; + where sharper boundaries are recovered in regions where these parameters are small. + + For implementation within SimPEG, regularization functions and their variables + must be discretized onto a `mesh`. For a regularization function whose kernel is given by + :math:`f(r)`, we approximate as follows: + + .. math:: + \int_\Omega w(r) \big [ f(r) \big ]^{p(r)} \, dv \approx \sum_i \tilde{w}_i \, | f_i |^{p_i} + + where :math:`f_i \in \mathbf{f_m}` define the discrete regularization kernel function + on the mesh. For example, the regularization kernel function for smallness regularization + is: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{m - m}^{(ref)} + + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account + for cell dimensions in the discretization and 2) apply user-defined weighting. + :math:`p_i \in \mathbf{p}` define the sparseness throughout the domain (set using `norm`). + + It is impractical to work with sparse norms directly, as their derivatives with respect + to the model are non-linear and discontinuous. Instead, the iteratively re-weighted + least-squares (IRLS) approach is used to approximate sparse norms by iteratively solving + a set of convex least-squares problems. For IRLS iteration :math:`k`, we define: + + .. math:: + \sum_i \tilde{w}_i \, \Big | f_i^{(k)} \Big |^{p_i} + \approx \sum_i \tilde{w}_i \, r_i^{(k)} \Big | f_i^{(k)} \Big |^2 + + where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: + + .. math:: + r_i^{(k)} = \bigg [ \Big ( f_i^{(k-1)} \Big )^2 + \epsilon^2 \; \bigg ]^{p_i/2 - 1} + + and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). + For the set of model parameters :math:`\mathbf{m}` defined at cell centers, the model + objective function for IRLS iteration :math:`k` can be expressed as a weighted sum of + objective functions of the form: + + .. math:: + \phi_m (\mathbf{m}) = \alpha_s + \Big \| \mathbf{W_s}^{\!\! (k)} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + + \sum_{j=x,y,z} \alpha_j \Big \| \mathbf{W_j}^{\! (k)} \mathbf{G_j \, m} \Big \|^2 + + where + + - :math:`\mathbf{m}` are the set of discrete model parameters (i.e. the model), + - :math:`\mathbf{m}^{(ref)}` is the reference model, + - :math:`\mathbf{G_x, \, G_y, \; G_z}` are partial cell gradients operators along x, y and z, and + - :math:`\mathbf{W_s, \, W_x, \, W_y, \; W_z}` are the weighting matrices for iteration :math:`k`. + + The weighting matrices apply the IRLS weights, user-defined weighting, and account for cell + dimensions when the regularization functions are discretized. + + **IRLS weights, user-defined weighting and the weighting matrix:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of custom cell + weights that are applied to all objective functions in the model objective function. + For IRLS iteration :math:`k`, the general form for the weights applied to the sparse smallness + term is given by: + + .. math:: + \mathbf{w_s}^{\!\! (k)} = \mathbf{r_s}^{\!\! (k)} \odot + \mathbf{v} \odot \prod_j \mathbf{w_j} + + And for sparse smoothness along x (likewise for y and z) is given by: + + .. math:: + \mathbf{w_x}^{\!\! (k)} = \mathbf{r_x}^{\!\! (k)} \odot \big ( \mathbf{P_x \, v} \big ) + \odot \prod_j \mathbf{P_x \, w_j} + + The IRLS weights at iteration :math:`k` are defined as :math:`\mathbf{r_\ast}^{\!\! (k)}` + for :math:`\ast = s,x,y,z`. :math:`\mathbf{v}` are the cell volumes. + Operators :math:`\mathbf{P_\ast}` for :math:`\ast = x,y,z` + project to the appropriate faces. + + Once the net weights for all objective functions are computed, + their weighting matrices can be constructed via: + + .. math:: + \mathbf{W}_\ast^{(k)} = \textrm{diag} \Big ( \, \sqrt{\mathbf{w_\ast}^{\!\! (k)} \, } \Big ) + + Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + + >>> reg = Sparse(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) + + **Reference model in smoothness:** + + Gradients/interfaces within a discrete reference model can be preserved by including the + reference model the smoothness regularization. In this case, + the objective function becomes: + + .. math:: + \phi_m (\mathbf{m}) = \alpha_s + \Big \| \mathbf{W_s}^{\! (k)} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + + \sum_{j=x,y,z} \alpha_j \Big \| + \mathbf{W_j}^{\! (k)} \mathbf{G_j} \big [ \mathbf{m} - \mathbf{m}^{(ref)} \big ] \Big \|^2 + + This functionality is used by setting the `reference_model_in_smooth` parameter + to ``True``. + + **Alphas and length scales:** + + The :math:`\alpha` parameters scale the relative contributions of the smallness and smoothness + terms in the model objective function. Each :math:`\alpha` parameter can be set directly as an + appropriate property of the ``WeightedLeastSquares`` class; e.g. :math:`\alpha_x` is set + using the `alpha_x` property. Note that unless the parameters are set manually, second-order + smoothness is not included in the model objective function. That is, the `alpha_xx`, `alpha_yy` + and `alpha_zz` parameters are set to 0 by default. + + The relative contributions of smallness and smoothness terms on the recovered model can also + be set by leaving `alpha_s` as its default value of 1, and setting the smoothness scaling + constants based on length scales. The model objective function has been formulated such that + smallness and smoothness terms contribute equally when the length scales are equal; i.e. when + properties `length_scale_x = length_scale_y = length_scale_z`. When the `length_scale_x` + property is set, the `alpha_x` and `alpha_xx` properties are set internally as: + + >>> reg.alpha_x = (reg.length_scale_x * reg.regularization_mesh.base_length) ** 2.0 + + and + + >>> reg.alpha_xx = (ref.length_scale_x * reg.regularization_mesh.base_length) ** 4.0 + + Likewise for y and z. + """ + + def __init__( + self, + mesh, + active_cells=None, + norms=None, + gradient_type="total", + irls_scaled=True, + irls_threshold=1e-8, + objfcts=None, + **kwargs, + ): + if not isinstance(mesh, RegularizationMesh): + mesh = RegularizationMesh(mesh) + + if not isinstance(mesh, RegularizationMesh): + TypeError( + f"'regularization_mesh' must be of type {RegularizationMesh} or {BaseMesh}. " + 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 + + if objfcts is None: + objfcts = [ + SparseSmallness(mesh=self.regularization_mesh), + SparseSmoothness(mesh=self.regularization_mesh, orientation="x"), + ] + + if mesh.dim > 1: + objfcts.append( + SparseSmoothness(mesh=self.regularization_mesh, orientation="y") + ) + + if mesh.dim > 2: + objfcts.append( + SparseSmoothness(mesh=self.regularization_mesh, orientation="z") + ) + + super().__init__( + self.regularization_mesh, + objfcts=objfcts, + **kwargs, + ) + if norms is None: + norms = [1] * (mesh.dim + 1) + self.norms = norms + self.gradient_type = gradient_type + self.irls_scaled = irls_scaled + self.irls_threshold = irls_threshold + + @property + def gradient_type(self) -> str: + """Gradient measure used to update IRLS weights for sparse smoothness. + + This property specifies whether the IRLS weights for sparse smoothness regularization(s) + terms are updated using the total gradient (*"total"*) or using the partial gradients along + their smoothing orientations (*"components"*). To see how the IRLS weights are computed, + visit the documentation for :py:meth:`~SparseSmoothness.update_weights`. + + Returns + ------- + str in {"total", "components"} + Whether to re-weight using the total gradient or partial gradients along + smoothing orientations. + """ + return self._gradient_type + + @gradient_type.setter + def gradient_type(self, value: str): + for fct in self.objfcts: + if hasattr(fct, "gradient_type"): + fct.gradient_type = value + + 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. + + Norms for the smallness and all smoothness terms in the ``Sparse`` regularization. + + Returns + ------- + list of float or numpy.ndarray + Norms for the child regularization classes. + """ + return self._norms + + @norms.setter + def norms(self, values: list | np.ndarray | None): + if values is not None: + if len(values) != len(self.objfcts): + raise ValueError( + f"The number of values provided for 'norms', {len(values)}, does not " + f"match the number of regularization functions, {len(self.objfcts)}." + ) + else: + values = [None] * len(self.objfcts) + previous_norms = getattr(self, "_norms", [None] * len(self.objfcts)) + try: + for val, fct in zip(values, self.objfcts): + fct.norm = val + self._norms = values + except Exception as err: + # reset the norms if failed + for val, fct in zip(previous_norms, self.objfcts): + fct.norm = val + raise err + + @property + def irls_scaled(self) -> bool: + """Scale IRLS weights. + + Returns + ------- + bool + Scale the IRLS weights. + """ + return self._irls_scaled + + @irls_scaled.setter + def irls_scaled(self, value: bool): + value = validate_type("irls_scaled", value, bool, cast=False) + for fct in self.objfcts: + fct.irls_scaled = value + self._irls_scaled = value + + @property + def irls_threshold(self): + """IRLS stabilization constant. + + Constant added to the denominator of the IRLS weights for stability. + See documentation for the :class:`Sparse` class for a comprehensive description. + + Returns + ------- + float + IRLS stabilization constant. + """ + return self._irls_threshold + + @irls_threshold.setter + def irls_threshold(self, value): + value = validate_float( + "irls_threshold", value, min_val=0.0, inclusive_min=False + ) + for fct in self.objfcts: + fct.irls_threshold = value + self._irls_threshold = value + + def update_weights(self, model): + """Update IRLS weights for all child regularization objects. + + For an instance of the `Sparse` regularization class, this method re-computes and updates + the IRLS for all child regularization objects using the model provided. + To see how IRLS weights are recomputed for :class:`SparseSmallness` objects, visit the + documentation for :py:meth:`SparseSmallness.update_weights`. And for + :class:`SparseSmoothness` objects, visit the documentation for + :py:meth:`SparseSmoothness.update_weights`. + + Parameters + ---------- + model : (n_params, ) numpy.ndarray + The model used to recompute the IRLS weights. + """ + for fct in self.objfcts: + fct.update_weights(model) diff --git a/simpeg/regularization/vector.py b/simpeg/regularization/vector.py new file mode 100644 index 0000000000..cc2f628c44 --- /dev/null +++ b/simpeg/regularization/vector.py @@ -0,0 +1,1266 @@ +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 + + +class BaseVectorRegularization(BaseRegularization): + """Base regularization class for models defined by vector quantities. + + The ``BaseVectorRegularization`` class defines properties and methods used + by regularization classes for inversion to recover vector quantities. + It is not directly used to constrain inversions. + """ + + @property + def n_comp(self): + """Number of components in the model.""" + if self.mapping.shape[0] == "*": + return self.regularization_mesh.dim + return int(self.mapping.shape[0] / self.regularization_mesh.nC) + + @property + def _weights_shapes(self) -> list[tuple[int]]: + """Acceptable lengths for the weights + + Returns + ------- + list of tuple + Each tuple represents accetable shapes for the weights + """ + mesh = self.regularization_mesh + + return [(mesh.nC,), (self.n_comp * mesh.nC,), (mesh.nC, self.n_comp)] + + +class CrossReferenceRegularization(Smallness, BaseVectorRegularization): + r"""Cross reference regularization for models representing vector quantities. + + ``CrossReferenceRegularization`` encourages the vectors in the recovered model to + be oriented in the same directions as the vector in a reference vector model. + The regularization function (objective function) constrains the inversion by penalizing + the magnitude of the-cross product of the vector model with a reference vector model. + The cross product, and therefore the objective function, is minimized when vectors + in the model and reference vector model are parallel (or anti-parallel) to each other. + And it is maximized when the vectors are perpendicular to each other. + The reference vector model can be set using a single vector, or by defining a + vector for each mesh cell. + + Parameters + ---------- + mesh : discretize.base.BaseMesh, .RegularizationMesh + The mesh defining the model discretization. + ref_dir : (mesh.dim,) array_like or (mesh.dim, n_active) array_like + The reference direction model. This can be either a constant vector applied + to every model cell, or different for every active model cell. + active_cells : index_array, optional + Boolean array or an array of active indices indicating the active cells of the + inversion domain mesh. + mapping : simpeg.maps.IdentityMap, optional + An optional linear mapping that would go from the model space to the space where + the cross-product is enforced. + weights : dict of [str: array_like], optional + Any cell based weights for the regularization. Note if given a weight that is + (n_cells, dim), meaning it is dependent on the vector component, it will compute + the geometric mean of the component weights per cell and use that as a weight. + **kwargs + Arguments passed on to the parent classes: :py:class:`.Smallness` and + :py:class:`.BaseVectorRegularization`. + + Notes + ----- + Consider the case where the model is a vector quantity :math:`\vec{m}(r)`. + The regularization function (objective function) for cross-reference + regularization is given by: + + .. math:: + \phi (\vec{m}) = \int_\Omega \, \vec{w}(r) \, \cdot \, + \Big [ \vec{m}(r) \, \times \, \vec{m}^{(ref)}(r) \Big ]^2 \, dv + + where :math:`\vec{m}^{(ref)}(r)` is the reference model vector and :math:`\vec{w}(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 given by: + + .. math:: + \phi (\vec{m}) \approx \sum_i \tilde{w}_i \, \cdot \, + \Big | \vec{m}_i \, \times \, \vec{m}_i^{(ref)} \Big |^2 + + where :math:`\tilde{m}_i \in \mathbf{m}` are the model vectors at cell centers 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. + + In practice, the model is a discrete vector of the form: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m_p} \\ \mathbf{m_s} \\ \mathbf{m_t} \end{bmatrix} + + where :math:`\mathbf{m_p}`, :math:`\mathbf{m_s}` and :math:`\mathbf{m_t}` represent vector + components in the primary, secondary and tertiary directions at cell centers, respectively. + The cross product between :math:`\mathbf{m}` and a similar reference vector + :math:`\mathbf{m^{(ref)}}` (set with `ref_dir`) is a linear operation of the form: + + .. math:: + \mathbf{m} \times \mathbf{m^{ref}} = \mathbf{X m} = + \begin{bmatrix} + \mathbf{0} & -\boldsymbol{\Lambda_s} & \boldsymbol{\Lambda_t} \\ + \boldsymbol{\Lambda_p} & \mathbf{0} & -\boldsymbol{\Lambda_t} \\ + -\boldsymbol{\Lambda_p} & \boldsymbol{\Lambda_s} & \mathbf{0} + \end{bmatrix} \! + \begin{bmatrix} \mathbf{m_p} \\ \mathbf{m_s} \\ \mathbf{m_t} \end{bmatrix} + + where :math:`\mathbf{X}` is a linear operator that applies the cross-product on :math:`\mathbf{m}`, + :math:`\mathbf{W}` is the weighting matrix, and: + + .. math:: + \boldsymbol{\Lambda_j} = \textrm{diag} \Big ( \mathbf{m_j^{(ref)}} \Big ) + \;\;\;\; \textrm{for} \; j=p,s,t + + The discrete regularization function in linear form can ultimately be expressed as: + + .. math:: + \phi (\mathbf{m}) = + \Big \| \mathbf{W X m} \, \Big \|^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. Each individual set of cell weights acts independently on the + the directional components of the cross-product and is a discrete vector of the form: + + .. math:: + \mathbf{w_j} = \begin{bmatrix} \mathbf{w_p} \\ \mathbf{w_s} \\ \mathbf{w_t} \end{bmatrix} + + The weighting applied within the objective function is given by: + + .. math:: + \mathbf{\tilde{w}} = \big ( \mathbf{e_3 \otimes v} ) \odot \prod_j \mathbf{w_j} + + where + + - :math:`\mathbf{e_3}` is a vector of ones of length 3, + - :math:`\otimes` is the Kronecker product, and + - :math:`\mathbf{v}` are the cell volumes. + + The weighting matrix used to apply the weights in the regularization function is given by: + + .. math:: + \mathbf{W} = \textrm{diag} \Big ( \, \mathbf{\tilde{w}}^{1/2} \Big ) + + Each set of custom cell weights is stored within a ``dict`` as a ``list`` of (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + + >>> reg = CrossReferenceRegularization( + >>> mesh, weights={'weights_1': array_1, 'weights_2': array_2} + >>> ) + + where `array_1` and `array_2` are (n_cells, dim) ``numpy.ndarray``. + Weights can also be set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) + + The default weights that account for cell dimensions in the regularization are accessed via: + + >>> reg.get_weights('volume') + + """ + + def __init__( + self, mesh, ref_dir, active_cells=None, mapping=None, weights=None, **kwargs + ): + kwargs.pop("reference_model", None) + super().__init__( + mesh=mesh, + active_cells=active_cells, + mapping=mapping, + weights=weights, + **kwargs, + ) + self.ref_dir = ref_dir + self.reference_model = 0.0 + + @property + def _nC_residual(self): + return np.prod(self.ref_dir.shape) + + @property + def ref_dir(self): + """The reference direction model. + + Returns + ------- + (n_active, dim) numpy.ndarray + The reference direction model. + """ + return self._ref_dir + + @ref_dir.setter + def ref_dir(self, value): + mesh = self.regularization_mesh + nC = mesh.nC + value = np.asarray(value) + if value.shape != (nC, mesh.dim): + if value.shape == (mesh.dim,): + # expand it out for each mesh cell + value = np.tile(value, (nC, 1)) + else: + raise ValueError(f"ref_dir must be shape {(nC, mesh.dim)}") + self._ref_dir = value + + R0 = sp.diags(value[:, 0]) + R1 = sp.diags(value[:, 1]) + if value.shape[1] == 2: + X = sp.bmat([[R1, -R0]]) + elif value.shape[1] == 3: + Z = sp.csr_matrix((nC, nC)) + R2 = sp.diags(value[:, 2]) + X = sp.bmat( + [ + [Z, R2, -R1], + [-R2, Z, R0], + [R1, -R0, Z], + ] + ) + self._X = X + + def f_m(self, m): + r"""Evaluate the regularization kernel function. + + For cross reference regularization, the regularization kernel function is given by: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{X m} + + where :math:`\mathbf{m}` are the discrete model parameters and :math:`\mathbf{X}` + carries out the cross-product with a reference vector model. + For a more detailed description, see the *Notes* section below. + + Parameters + ---------- + m : numpy.ndarray + The vector model. + + Returns + ------- + numpy.ndarray + The regularization kernel function evaluated for the model provided. + + Notes + ----- + The objective function for cross reference regularization is given by: + + .. math:: + \phi_m (\mathbf{m}) = + \Big \| \mathbf{W X m} \, \Big \|^2 + + where :math:`\mathbf{m}` are the discrete vector model parameters defined on the mesh (model), + :math:`\mathbf{X}` carries out the cross-product with a reference vector model, and :math:`\mathbf{W}` is + the weighting matrix. See the :class:`CrossReferenceRegularization` class documentation for more detail. + + We define the regularization kernel function :math:`\mathbf{f_m}` as: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{X m} + + such that + + .. math:: + \phi_m (\mathbf{m}) = \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 + + """ + return self._X @ (self.mapping * m) + + def f_m_deriv(self, m): + r"""Derivative of the regularization kernel function. + + For ``CrossReferenceRegularization``, 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{X} + + where :math:`\mathbf{X}` is a linear operator that carries out the + cross-product with a reference vector model. + + Parameters + ---------- + m : numpy.ndarray + The vector model. + + Returns + ------- + scipy.sparse.csr_matrix + The derivative of the regularization kernel function. + + Notes + ----- + The objective function for cross reference regularization is given by: + + .. math:: + \phi_m (\mathbf{m}) = + \Big \| \mathbf{W X m} \, \Big \|^2 + + where :math:`\mathbf{m}` are the discrete vector model parameters defined on the mesh (model), + :math:`\mathbf{X}` carries out the cross-product with a reference vector model, and :math:`\mathbf{W}` is + the weighting matrix. See the :class:`CrossReferenceRegularization` class documentation for more detail. + + We define the regularization kernel function :math:`\mathbf{f_m}` as: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{X m} + + such that + + .. math:: + \phi_m (\mathbf{m}) = \Big \| \mathbf{W} \, \mathbf{f_m} \Big \|^2 + + Thus, the derivative with respect to the model is: + + .. math:: + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = \mathbf{X} + + """ + return self._X @ self.mapping.deriv(m) + + @property + def W(self): + r"""Weighting matrix. + + Returns the weighting matrix for the objective function. To see how the + weighting matrix is constructed, see the *Notes* section for the + :class:`CrossReferenceRegularization` class. + + Returns + ------- + scipy.sparse.csr_matrix + The weighting matrix applied in the objective function. + """ + if getattr(self, "_W", None) is None: + mesh = self.regularization_mesh + nC = mesh.nC + + weights = np.ones( + nC, + ) + for value in self._weights.values(): + if value.shape == (nC,): + weights *= value + elif value.size == mesh.dim * nC: + weights *= np.linalg.norm( + value.reshape((nC, mesh.dim), order="F"), axis=1 + ) + weights = np.sqrt(weights) + if mesh.dim == 2: + diag = weights + else: + diag = np.r_[weights, weights, weights] + self._W = sp.diags(diag, format="csr") + return self._W + + +class BaseAmplitude(BaseVectorRegularization): + """Base amplitude regularization class for models defined by vector quantities. + + The ``BaseAmplitude`` class defines properties and methods used + by amplitude regularization classes for vector quantities. + It is not directly used to constrain inversions. + """ + + def amplitude(self, m): + """Return vector amplitudes for the model provided. + + Where the model `m` defines a vector quantity for each active cell in the + inversion, the `amplitude` method returns the amplitudes of these vectors. + + Parameters + ---------- + m : (n_param ) numpy.ndarray + The model. + + Returns + ------- + (n_cells, ) numpy.ndarray + The amplitudes of the vectors for the model provided. + """ + return np.linalg.norm( + (self.mapping * self._delta_m(m)).reshape( + (self.regularization_mesh.nC, self.n_comp), order="F" + ), + axis=1, + ) + + def deriv(self, m) -> np.ndarray: + r"""Gradient of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method evaluates and returns the derivative with respect to the model parameters; + i.e. the gradient: + + .. math:: + \frac{\partial \phi}{\partial \mathbf{m}} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the gradient is evaluated. + + Returns + ------- + (n_param, ) numpy.ndarray + Gradient of the regularization function evaluated for the model provided. + """ + d_m = self._delta_m(m) + + return ( + 2 + * self.f_m_deriv(m).T + * ( + self.W.T + @ self.W + @ (self.f_m_deriv(m) @ d_m).reshape((-1, self.n_comp), order="F") + ).flatten(order="F") + ) + + def deriv2(self, m, v=None) -> csr_matrix: + r"""Hessian of the regularization function evaluated for the model provided. + + Where :math:`\phi (\mathbf{m})` is the discrete regularization function (objective function), + this method returns the second-derivative (Hessian) with respect to the model parameters: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} + + or the second-derivative (Hessian) multiplied by a vector :math:`(\mathbf{v})`: + + .. math:: + \frac{\partial^2 \phi}{\partial \mathbf{m}^2} \, \mathbf{v} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model for which the Hessian is evaluated. + v : None, (n_param, ) numpy.ndarray (optional) + A vector. + + Returns + ------- + (n_param, n_param) scipy.sparse.csr_matrix | (n_param, ) numpy.ndarray + If the input argument *v* is ``None``, the Hessian of the regularization + function for the model provided is returned. If *v* is not ``None``, + the Hessian multiplied by the vector provided is returned. + """ + f_m_deriv = self.f_m_deriv(m) + + if v is None: + return ( + 2 + * f_m_deriv.T + * (sp.block_diag([self.W.T * self.W] * self.n_comp) * f_m_deriv) + ) + + return ( + 2 + * f_m_deriv.T + * ( + self.W.T + @ self.W + @ (f_m_deriv * v).reshape((-1, self.n_comp), order="F") + ).flatten(order="F") + ) + + +class AmplitudeSmallness(SparseSmallness, BaseAmplitude): + r"""Sparse smallness regularization on vector amplitudes. + + ``AmplitudeSmallness`` is a sparse norm smallness regularization that acts on the + amplitudes of the vectors defining the model. Sparse norm functionality allows the + use to recover more compact regions of higher amplitude vectors. + The level of compactness is controlled by the norm within the regularization + function; with more compact structures being recovered when a smaller norm is used. + Optionally, custom cell weights can be included to control the degree of compactness + being enforced throughout different regions the model. + + See the *Notes* section below for a comprehensive description. + + Parameters + ---------- + mesh : .regularization.RegularizationMesh + Mesh on which the regularization is discretized. Not the mesh used to + define the simulation. + norm : float, (n_cells, ) array_like + The norm defining sparseness in the regularization function. Use a ``float`` to define + the same norm for all mesh cells, or define an independent norm for each cell. All norm + values must be within the interval [0, 2]. + active_cells : None, (n_cells, ) numpy.ndarray of bool + 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. + reference_model : None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. + units : None, str + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to + a (n_cells, ) numpy.ndarray that is defined on the + :py:class:`regularization.RegularizationMesh`. + irls_scaled : bool + If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. + If ``False``, do not scale. + irls_threshold : float + Constant added to IRLS weights to ensures stability in the algorithm. + + Notes + ----- + We define the regularization function (objective function) for sparse amplitude smallness + (compactness) as: + + .. math:: + \phi (\vec{m}) = \int_\Omega \, w(r) \, + \Big | \, \vec{m}(r) - \vec{m}^{(ref)}(r) \, \Big |^{p(r)} \, dv + + where :math:`\vec{m}(r)` is the model, :math:`\vec{m}^{(ref)}(r)` is the reference model, :math:`w(r)` + is a user-defined weighting function and :math:`p(r) \in [0,2]` is a parameter which imposes + sparseness throughout the recovered model. More compact structures are recovered in regions + where :math:`p` is small. If the same level of sparseness is being imposed everywhere, + the exponent becomes a constant. + + 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: + + .. math:: + \phi (\mathbf{m}) = \sum_i + \tilde{w}_i \, \Big | \vec{m}_i - \vec{m}_i^{(ref)} \Big |^{p_i} + + where :math:`\mathbf{m}` are the model parameters, :math:`\vec{m}_i` represents the vector + defined for mesh cell :math:`i`, and :math:`\vec{m}_i^{(ref)}` defines the reference model + vector for cell :math:`i`. :math:`\tilde{w}_i` are amalgamated weighting constants that + 1) account for cell dimensions in the discretization and 2) apply user-defined weighting. + :math:`p_i \in \mathbf{p}` define the norm for each cell (set using `norm`). + + It is impractical to work with the general form directly, as its derivatives with respect + to the model are non-linear and discontinuous. Instead, the iteratively re-weighted + least-squares (IRLS) approach is used to approximate the sparse norm by iteratively solving + a set of convex least-squares problems. For IRLS iteration :math:`k`, we define: + + .. math:: + \phi \big (\mathbf{m}^{(k)} \big ) + = \sum_i \tilde{w}_i \, \Big | \, \vec{m}_i^{(k)} - \vec{m}_i^{(ref)} \, \Big |^{p_i} + \approx \sum_i \tilde{w}_i \, r_i^{(k)} + \Big | \, \vec{m}_i^{(k)} - \vec{m}_i^{(ref)} \, \Big |^2 + + where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: + + .. math:: + r_i^{(k)} = \bigg [ \, \Big | \vec{m}_i^{(k-1)} - \vec{m}_i^{(ref)} \Big |^2 + + \epsilon^2 \; \bigg ]^{{p_i}/2 - 1} + + and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). + + The global set of model parameters :math:`\mathbf{m}` defined at cell centers is ordered according + to its primary (:math:`p`), secondary (:math:`s`) and tertiary (:math:`t`) directions as follows: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m}_p \\ \mathbf{m}_s \\ \mathbf{m}_t \end{bmatrix} + + We define the amplitudes of the residual between the model and reference model for all cells as: + + .. math:: + \mathbf{\bar{m}} = \bigg ( + \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_t - \mathbf{m}_t^{(ref)} \Big ]^2 \bigg )^{1/2} + + The objective function for IRLS iteration :math:`k` is given by: + + .. math:: + \phi \big ( \mathbf{\bar{m}}^{(k)} \big ) \approx \Big \| \, + \mathbf{W}^{(k)} \, \mathbf{\bar{m}}^{(k)} \; \Big \|^2 + + where + + - :math:`\mathbf{\bar{m}}^{(k)}` are the absolute values of the residual at iteration :math:`k`, and + - :math:`\mathbf{W}^{(k)}` is the weighting matrix for iteration :math:`k`. It applies the IRLS weights, user-defined weighting, and accounts for cell dimensions when the regularization function is discretized. + + **IRLS weights, user-defined weighting and the weighting matrix:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of + custom cell weights. And let :math:`\mathbf{r_s}^{\!\! (k)}` represent the IRLS weights + for iteration :math:`k`. The net weighting applied within the objective function + is given by: + + .. math:: + \mathbf{w}^{(k)} = \mathbf{r_s}^{\!\! (k)} \odot \mathbf{v} \odot \prod_j \mathbf{w_j} + + where :math:`\mathbf{v}` are the cell volumes. + For a description of how IRLS weights are updated at every iteration, see the documentation + for :py:meth:`update_weights`. + + The weighting matrix used to apply the weights is given by: + + .. math:: + \mathbf{W}^{(k)} = \textrm{diag} \Big ( \sqrt{\mathbf{w}^{(k)} \, } \Big ) + + Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + + >>> reg = AmplitudeSmallness(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) + """ + + def f_m(self, m): + r"""Evaluate the regularization kernel function. + + For smallness vector amplitude regularization, the regularization kernel function is: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{\bar{m}} = \bigg ( + \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_t - \mathbf{m}_t^{(ref)} \Big ]^2 \bigg )^{1/2} + + where the global set of model parameters :math:`\mathbf{m}` defined at cell centers is + ordered according to its primary (:math:`p`), secondary (:math:`s`) and tertiary (:math:`t`) + directions as follows: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m}_p \\ \mathbf{m}_s \\ \mathbf{m}_t \end{bmatrix} + + Likewise for the vector components of the reference model. + + Parameters + ---------- + m : numpy.ndarray + The model. + + Returns + ------- + numpy.ndarray + The regularization kernel function evaluated at the model provided. + + """ + + return self.amplitude(m) + + @property + def W(self): + ### Inherited from Smallness regularization class + if getattr(self, "_W", None) is None: + mesh = self.regularization_mesh + nC = mesh.nC + + weights = np.ones( + nC, + ) + for value in self._weights.values(): + if value.shape == (nC,): + weights *= value + elif value.shape == (self.n_comp * nC,): + weights *= np.linalg.norm( + value.reshape((nC, self.n_comp), order="F"), axis=1 + ) + + self._W = sp.diags(np.sqrt(weights), format="csr") + + return self._W + + +class AmplitudeSmoothnessFirstOrder(SparseSmoothness, BaseAmplitude): + r"""Sparse amplitude smoothness (blockiness) regularization. + + ``AmplitudeSmallness`` is a sparse norm smoothness regularization that acts on the + amplitudes of the vectors defining the model. Sparse norm functionality allows the + use to recover more blocky regions of higher amplitude vectors. + The level of blockiness is controlled by the norm within the regularization + function; with more blocky structures being recovered when a smaller norm is used. + Optionally, custom cell weights can be included to control the degree of blockiness + being enforced throughout different regions the model. + + See the *Notes* section below for a comprehensive description. + + Parameters + ---------- + mesh : .regularization.RegularizationMesh + Mesh on which the regularization is discretized. Not the mesh used to + define the simulation. + orientation : {'x','y','z'} + The direction along which sparse smoothness is applied. + norm : float, array_like + The norm defining sparseness thoughout the regularization function. Must be within the + interval [0,2]. There are several options: + + - ``float``: constant sparse norm throughout the domain. + - (n_faces, ) ``array_like``: define the sparse norm independently at each face set by `orientation` (e.g. x-faces). + - (n_cells, ) ``array_like``: define the sparse norm independently for each cell. Will be averaged to faces specified by `orientation` (e.g. x-faces). + + active_cells : None, (n_cells, ) numpy.ndarray of bool + 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. + reference_model : None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. To include the reference model in the regularization, the + `reference_model_in_smooth` property must be set to ``True``. + reference_model_in_smooth : bool, optional + Whether to include the reference model in the smoothness terms. + units : None, str + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. + weights : None, dict + Custom weights for the least-squares function. Each ``key`` points to + a ``numpy.ndarray`` that is defined on the :py:class:`regularization.RegularizationMesh`. + A (n_cells, ) ``numpy.ndarray`` is used to define weights at cell centers, which are + averaged to the appropriate faces internally when weighting is applied. + A (n_faces, ) ``numpy.ndarray`` is used to define weights directly on the faces specified + by the `orientation` input argument. + irls_scaled : bool + If ``True``, scale the IRLS weights to preserve magnitude of the regularization function. + If ``False``, do not scale. + irls_threshold : float + Constant added to IRLS weights to ensures stability in the algorithm. + gradient_type : {"total", "component"} + Gradient measure used in the IRLS re-weighting. Whether to re-weight using the total + gradient or components of the gradient. + + Notes + ----- + The regularization function (objective function) for sparse amplitude smoothness (blockiness) + along the x-direction as: + + .. math:: + \phi (m) = \int_\Omega \, w(r) \, + \Bigg | \, \frac{\partial |\vec{m}|}{\partial x} \, \Bigg |^{p(r)} \, dv + + where :math:`\vec{m}(r)` is the model, :math:`w(r)` + is a user-defined weighting function and :math:`p(r) \in [0,2]` is a parameter which imposes + sparseness throughout the recovered model. Sharper boundaries are recovered in regions + where :math:`p(r)` is small. If the same level of sparseness is being imposed everywhere, + the exponent becomes a constant. + + For implementation within SimPEG, the regularization function and its variables + must be discretized onto a `mesh`. The discrete 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 |\vec{m}_i|}{\partial x} \, \Bigg |^{p_i} + + where :math:`\vec{m}_i` is the vector defined for mesh cell :math:`i`. + :math:`\tilde{w}_i` are amalgamated weighting constants that 1) account + for cell dimensions in the discretization and 2) apply user-defined weighting. + :math:`p_i \in \mathbf{p}` define the norm for each cell (set using `norm`). + + It is impractical to work with the general form directly, as its derivatives with respect + to the model are non-linear and discontinuous. Instead, the iteratively re-weighted + least-squares (IRLS) approach is used to approximate the sparse norm by iteratively solving + a set of convex least-squares problems. For IRLS iteration :math:`k`, we define: + + .. math:: + \phi \big (\mathbf{m}^{(k)} \big ) + = \sum_i + \tilde{w}_i \, \left | \, \frac{\partial \big | \vec{m}_i^{(k)} \big | }{\partial x} \right |^{p_i} + \approx \sum_i \tilde{w}_i \, r_i^{(k)} + \left | \, \frac{\partial \big | \vec{m}_i^{(k)} \big | }{\partial x} \right |^2 + + where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: + + .. math:: + r_i^{(k)} = \Bigg [ \Bigg ( \frac{\partial \big | \vec{m}_i^{(k-1)} \big | }{\partial x} \Bigg )^2 + + \epsilon^2 \; \Bigg ]^{{p_i}/2 - 1} + + and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). + + The global set of model parameters :math:`\mathbf{m}` defined at cell centers is ordered according + to its primary (:math:`p`), secondary (:math:`s`) and tertiary (:math:`t`) directions as follows: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m}_p \\ \mathbf{m}_s \\ \mathbf{m}_t \end{bmatrix} + + We define the amplitudes of the vectors for all cells as: + + .. math:: + \mathbf{\bar{m}} = \Big [ \, \mathbf{m}_p^2 + \mathbf{m}_s^2 + \mathbf{m}_t^2 \Big ]^{1/2} + + The objective function for IRLS iteration :math:`k` is given by: + + .. math:: + \phi \big ( \mathbf{m}^{(k)} \big ) \approx \Big \| \, + \mathbf{W}^{(k)} \, \mathbf{G_x} \, \mathbf{\bar{m}}^{(k)} \Big \|^2 + + where + + - :math:`\bar{\mathbf{m}}^{(k)}` are the discrete vector model amplitudes at iteration :math:`k`, + - :math:`\mathbf{G_x}` is the partial cell-gradient operator along x (x-derivative), + - :math:`\mathbf{W}^{(k)}` is the weighting matrix for iteration :math:`k`. It applies the IRLS weights, user-defined weighting, and accounts for cell dimensions when the regularization function is discretized. + + Note that since :math:`\mathbf{G_x}` maps from cell centers to x-faces, the weighting matrix + 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 smoothness regularization. + In this case, the least-squares problem for IRLS iteration :math:`k` becomes: + + .. math:: + \phi \big ( \mathbf{m}^{(k)} \big ) \approx \Big \| \, + \mathbf{W}^{(k)} \, \mathbf{G_x} \, \mathbf{\bar{m}}^{(k)} \Big \|^2 + + where + + .. math:: + \mathbf{\bar{m}} = \bigg ( + \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_t - \mathbf{m}_t^{(ref)} \Big ]^2 \bigg )^{1/2} + + This functionality is used by setting :math:`\mathbf{m}^{(ref)}` with the + `reference_model` property, and by setting the `reference_model_in_smooth` parameter + to ``True``. + + **IRLS weights, user-defined weighting and the weighting matrix:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of + custom weights defined on the faces specified by the `orientation` property; + i.e. x-faces for smoothness along the x-direction. Each set of weights were either defined + directly on the faces or have been averaged from cell centers. And let + :math:`\mathbf{r_x}^{\!\! (k)}` represent the IRLS weights for iteration :math:`k`. + The net weighting applied within the objective function is given by: + + .. math:: + \mathbf{w}^{(k)} = \mathbf{r_x}^{\!\! (k)} \odot \mathbf{v_x} \odot \prod_j \mathbf{w_j} + + where :math:`\mathbf{v_x}` are cell volumes projected to x-faces; i.e. where the + x-derivative lives. For a description of how IRLS weights are updated at every iteration, + see the documentation for :py:meth:`update_weights`. + + The weighting matrix used to apply the weights is given by: + + .. math:: + \mathbf{W}^{(k)} = \textrm{diag} \Big ( \sqrt{\mathbf{w}^{(k)} \, } \Big ) + + Each set of custom weights is stored within a ``dict`` as an ``numpy.ndarray``. + A (n_cells, ) ``numpy.ndarray`` is used to define weights at cell centers, which are + averaged to the appropriate faces internally when weighting is applied. + A (n_faces, ) ``numpy.ndarray`` is used to define weights directly on the faces specified + by the `orientation` input argument. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + """ + + @property + def _weights_shapes(self) -> list[tuple[int]]: + """Acceptable lengths for the weights + + Returns + ------- + list of tuple + Each tuple represents accetable shapes for the weights + """ + nC = self.regularization_mesh.nC + nF = getattr( + self.regularization_mesh, "aveCC2F{}".format(self.orientation) + ).shape[0] + return [ + (nF,), + (self.n_comp * nF,), + (nF, self.n_comp), + (nC,), + (self.n_comp * nC,), + (nC, self.n_comp), + ] + + def f_m(self, m): + r"""Evaluate the regularization kernel function. + + For first-order smoothness regularization in the x-direction, + the regularization kernel function is given by: + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{G_x \, \bar{m}} + + where :math:`\mathbf{G_x}` is the partial cell gradient operator along the x-direction + (i.e. x-derivative), and + + .. math:: + \mathbf{f_m}(\mathbf{m}) = \mathbf{\bar{m}} = \bigg ( + \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_t - \mathbf{m}_t^{(ref)} \Big ]^2 \bigg )^{1/2} + + The global set of model parameters :math:`\mathbf{m}` defined at cell centers is + ordered according to its primary (:math:`p`), secondary (:math:`s`) and tertiary (:math:`t`) + directions as follows: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m}_p \\ \mathbf{m}_s \\ \mathbf{m}_t \end{bmatrix} + + Likewise for the reference model vector. The expression has the same form for smoothness + along y and z. + + Parameters + ---------- + m : numpy.ndarray + The model. + + Returns + ------- + numpy.ndarray + The regularization kernel function evaluated for the model provided. + """ + fm = self.cell_gradient * (self.mapping * self._delta_m(m)).reshape( + (self.regularization_mesh.nC, self.n_comp), order="F" + ) + + return np.linalg.norm(fm, axis=1) + + 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: + + .. math:: + \frac{\partial \mathbf{f_m}}{\partial \mathbf{m}} = + \begin{bmatrix} \mathbf{G_x} & \mathbf{0} \\ \mathbf{0} & \mathbf{G_x} \end{bmatrix} + + where :math:`\mathbf{G_x}` is the partial cell gradient operator along x + (i.e. the x-derivative). + + Parameters + ---------- + m : numpy.ndarray + The model. + + Returns + ------- + scipy.sparse.csr_matrix + The derivative of the regularization kernel function. + """ + return sp.block_diag([self.cell_gradient] * self.n_comp) @ self.mapping.deriv( + self._delta_m(m) + ) + + @property + def W(self): + ### Inherited + if getattr(self, "_W", None) is None: + average_cell_2_face = getattr( + self.regularization_mesh, "aveCC2F{}".format(self.orientation) + ) + nC = self.regularization_mesh.nC + nF = average_cell_2_face.shape[0] + weights = 1.0 + for values in self._weights.values(): + if values.shape[0] == nC: + values = average_cell_2_face * values + elif not values.shape == (nF,): + values = np.linalg.norm( + values.reshape((-1, self.n_comp), order="F"), axis=1 + ) + if values.size == nC: + values = average_cell_2_face * values + + weights *= values + + self._W = sp.diags(np.sqrt(weights), format="csr") + + return self._W + + +class VectorAmplitude(Sparse): + r"""Sparse vector amplitude regularization. + + Apply vector amplitude regularization for recovering compact and/or blocky structures + using a weighted sum of :class:`AmplitudeSmallness` and :class:`AmplitudeSmoothnessFirstOrder` + regularization functions. The level of compactness and blockiness is + controlled by the norms within the respective regularization functions; + with more sparse structures (compact and/or blocky) being recovered when smaller + norms are used. Optionally, custom cell weights can be applied to control + the degree of sparseness being enforced throughout different regions the model. + + See the *Notes* section below for a comprehensive description. + + Parameters + ---------- + mesh : simpeg.regularization.RegularizationMesh, discretize.base.BaseMesh + Mesh on which the regularization is discretized. This is not necessarily + the same as the mesh on which the simulation is defined. + active_cells : None, (n_cells, ) numpy.ndarray of bool + 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. + reference_model : None, (n_param, ) numpy.ndarray + Reference model. If ``None``, the reference model in the inversion is set to + the starting model. + reference_model_in_smooth : bool, optional + Whether to include the reference model in the smoothness terms. + units : None, str + Units for the model parameters. Some regularization classes behave + differently depending on the units; e.g. 'radian'. + weights : None, dict + Weight multipliers to customize the least-squares function. Each key points to a (n_cells, ) + numpy.ndarray that is defined on the :py:class:`~.regularization.RegularizationMesh`. + alpha_s : float, optional + Scaling constant for the smallness regularization term. + alpha_x, alpha_y, alpha_z : float or None, optional + Scaling constants for the first order smoothness along x, y and z, respectively. + If set to ``None``, the scaling constant is set automatically according to the + value of the `length_scale` parameter. + length_scale_x, length_scale_y, length_scale_z : float, optional + First order smoothness length scales for the respective dimensions. + gradient_type : {"total", "component"} + Gradient measure used in the IRLS re-weighting. Whether to re-weight using the + total gradient or components of the gradient. + norms : (dim+1, ) numpy.ndarray + The respective norms used for the sparse smallness, x-smoothness, (y-smoothness + and z-smoothness) regularization function. Must all be within the interval [0, 2]. + E.g. `np.r_[2, 1, 1, 1]` uses a 2-norm on the smallness term and a 1-norm on all + smoothness terms. + irls_scaled : bool + If ``True``, scale the IRLS weights to preserve magnitude of the regularization + function. If ``False``, do not scale. + irls_threshold : float + Constant added to IRLS weights to ensures stability in the algorithm. + + Notes + ----- + Sparse vector amplitude regularization can be defined by a weighted sum of + :class:`AmplitudeSmallness` and :class:`AmplitudeSmoothnessFirstOrder` + regularization functions. This corresponds to a model objective function + :math:`\phi_m (m)` of the form: + + .. math:: + \phi_m (m) = \alpha_s \int_\Omega \, w(r) + \Big | \, \vec{m}(r) - \vec{m}^{(ref)}(r) \, \Big |^{p_s(r)} \, dv + + \sum_{j=x,y,z} \alpha_j \int_\Omega \, w(r) + \Bigg | \, \frac{\partial |\vec{m}|}{\partial \xi_j} \, \bigg |^{p_j(r)} \, dv + + where :math:`\vec{m}(r)` is the model, :math:`\vec{m}^{(ref)}(r)` is the reference model, + and :math:`w(r)` is a user-defined weighting function applied to all terms. + :math:`\xi_j` for :math:`j=x,y,z` are unit directions along :math:`j`. + Parameters :math:`\alpha_s` and :math:`\alpha_j` for :math:`j=x,y,z` are multiplier constants + that weight the respective contributions of the smallness and smoothness terms in the + regularization. :math:`p_s(r) \in [0,2]` is a parameter which imposes sparse smallness + throughout the recovered model; where more compact structures are recovered in regions where + :math:`p_s(r)` is small. And :math:`p_j(r) \in [0,2]` for :math:`j=x,y,z` are parameters which + impose sparse smoothness throughout the recovered model along the specified direction; + where sharper boundaries are recovered in regions where these parameters are small. + + For implementation within SimPEG, regularization functions and their variables + must be discretized onto a `mesh`. For a regularization function whose kernel is given by + :math:`f(r)`, we approximate as follows: + + .. math:: + \int_\Omega w(r) \big [ f(r) \big ]^{p(r)} \, dv \approx \sum_i \tilde{w}_i \, | f_i |^{p_i} + + where :math:`f_i \in \mathbf{f_m}` define the discrete regularization kernel function + on the mesh such that: + + .. math:: + f_i = \begin{cases} + | \, \vec{m}_i \, | \;\;\;\;\;\;\; (no \; reference \; model)\\ + | \, \vec{m}_i - \vec{m}_i^{(ref)} \, | \;\;\;\; (reference \; model) + \end{cases} + + :math:`\tilde{w}_i \in \mathbf{\tilde{w}}` are amalgamated weighting constants that 1) account + for cell dimensions in the discretization and 2) apply user-defined weighting. + :math:`p_i \in \mathbf{p}` define the sparseness throughout the domain (set using `norm`). + + It is impractical to work with sparse norms directly, as their derivatives with respect + to the model are non-linear and discontinuous. Instead, the iteratively re-weighted + least-squares (IRLS) approach is used to approximate sparse norms by iteratively solving + a set of convex least-squares problems. For IRLS iteration :math:`k`, we define: + + .. math:: + \sum_i \tilde{w}_i \, \Big | f_i^{(k)} \Big |^{p_i} + \approx \sum_i \tilde{w}_i \, r_i^{(k)} \Big | f_i^{(k)} \Big |^2 + + where the IRLS weight :math:`r_i` for iteration :math:`k` is given by: + + .. math:: + r_i^{(k)} = \bigg [ \Big ( f_i^{(k-1)} \Big )^2 + \epsilon^2 \; \bigg ]^{p_i/2 - 1} + + and :math:`\epsilon` is a small constant added for stability (set using `irls_threshold`). + + The global set of model parameters :math:`\mathbf{m}` defined at cell centers is ordered according + to its primary (:math:`p`), secondary (:math:`s`) and tertiary (:math:`t`) directions as follows: + + .. math:: + \mathbf{m} = \begin{bmatrix} \mathbf{m}_p \\ \mathbf{m}_s \\ \mathbf{m}_t \end{bmatrix} + + The objective function for IRLS iteration :math:`k` can be expressed as a weighted sum of + objective functions of the form: + + .. math:: + \phi_m (\mathbf{m}) = \alpha_s + \Big \| \, \mathbf{W_s}^{\! (k)} \, \Delta \mathbf{\bar{m}} \, \Big \|^2 + + \sum_{j=x,y,z} \alpha_j \Big \| \, \mathbf{W_j}^{\! (k)} \mathbf{G_j \, \bar{m}} \, \Big \|^2 + + where + + .. math:: + \Delta \mathbf{\bar{m}} = \bigg ( + \Big [ \mathbf{m}_p - \mathbf{m}_p^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_s - \mathbf{m}_s^{(ref)} \Big ]^2 + + \Big [ \mathbf{m}_t - \mathbf{m}_t^{(ref)} \Big ]^2 \bigg )^{1/2} + + and + + .. math:: + \mathbf{\bar{m}} = \Big [ \, \mathbf{m}_p^2 + \mathbf{m}_s^2 + \mathbf{m}_t^2 \Big ]^{1/2} + + :math:`\mathbf{G_x, \, G_y, \; G_z}` are partial cell gradients operators along x, y and z, and + :math:`\mathbf{W_s, \, W_x, \, W_y, \; W_z}` are the weighting matrices for iteration :math:`k`. + The weighting matrices apply the IRLS weights, user-defined weighting, and account for cell + dimensions when the regularization functions are discretized. + + **IRLS weights, user-defined weighting and the weighting matrix:** + + Let :math:`\mathbf{w_1, \; w_2, \; w_3, \; ...}` each represent an optional set of custom cell + weights that are applied to all objective functions in the model objective function. + For IRLS iteration :math:`k`, the general form for the weights applied to the sparse smallness + term is given by: + + .. math:: + \mathbf{w_s}^{\!\! (k)} = \mathbf{r_s}^{\!\! (k)} \odot + \mathbf{v} \odot \prod_j \mathbf{w_j} + + And for sparse smoothness along x (likewise for y and z) is given by: + + .. math:: + \mathbf{w_x}^{\!\! (k)} = \mathbf{r_x}^{\!\! (k)} \odot \big ( \mathbf{P_x \, v} \big ) + \odot \prod_j \mathbf{P_x \, w_j} + + The IRLS weights at iteration :math:`k` are defined as :math:`\mathbf{r_\ast}^{\!\! (k)}` + for :math:`\ast = s,x,y,z`. :math:`\mathbf{v}` are the cell volumes. + Operators :math:`\mathbf{P_\ast}` for :math:`\ast = x,y,z` + project to the appropriate faces. + + Once the net weights for all objective functions are computed, + their weighting matrices can be constructed via: + + .. math:: + \mathbf{W}_\ast^{(k)} = \textrm{diag} \Big ( \, \sqrt{\mathbf{w_\ast}^{\!\! (k)} \, } \Big ) + + Each set of custom cell weights is stored within a ``dict`` as an (n_cells, ) + ``numpy.ndarray``. The weights can be set all at once during instantiation + with the `weights` keyword argument as follows: + + >>> reg = AmplitudeVector(mesh, weights={'weights_1': array_1, 'weights_2': array_2}) + + or set after instantiation using the `set_weights` method: + + >>> reg.set_weights(weights_1=array_1, weights_2=array_2}) + + **Reference model in smoothness:** + + Gradients/interfaces within a discrete reference model can be preserved by including the + reference model the smoothness regularization. In this case, + the objective function becomes: + + .. math:: + \phi_m (\mathbf{m}) = \alpha_s + \Big \| \, \mathbf{W_s}^{\! (k)} \, \Delta \mathbf{\bar{m}} \, \Big \|^2 + + \sum_{j=x,y,z} \alpha_j \Big \| \, \mathbf{W_j}^{\! (k)} \mathbf{G_j \, \Delta \bar{m}} \, \Big \|^2 + + This functionality is used by setting the `reference_model_in_smooth` parameter + to ``True``. + + **Alphas and length scales:** + + The :math:`\alpha` parameters scale the relative contributions of the smallness and smoothness + terms in the model objective function. Each :math:`\alpha` parameter can be set directly as an + appropriate property of the ``WeightedLeastSquares`` class; e.g. :math:`\alpha_x` is set + using the `alpha_x` property. Note that unless the parameters are set manually, second-order + smoothness is not included in the model objective function. That is, the `alpha_xx`, `alpha_yy` + and `alpha_zz` parameters are set to 0 by default. + + The relative contributions of smallness and smoothness terms on the recovered model can also + be set by leaving `alpha_s` as its default value of 1, and setting the smoothness scaling + constants based on length scales. The model objective function has been formulated such that + smallness and smoothness terms contribute equally when the length scales are equal; i.e. when + properties `length_scale_x = length_scale_y = length_scale_z`. When the `length_scale_x` + property is set, the `alpha_x` and `alpha_xx` properties are set internally as: + + >>> reg.alpha_x = (reg.length_scale_x * reg.regularization_mesh.base_length) ** 2.0 + + and + + >>> reg.alpha_xx = (ref.length_scale_x * reg.regularization_mesh.base_length) ** 4.0 + + Likewise for y and z. + + """ + + def __init__( + self, + mesh, + mapping=None, + active_cells=None, + **kwargs, + ): + if not isinstance(mesh, (BaseMesh, RegularizationMesh)): + raise TypeError( + f"'regularization_mesh' must be of type {RegularizationMesh} or {BaseMesh}. " + f"Value of type {type(mesh)} provided." + ) + + if not isinstance(mesh, RegularizationMesh): + mesh = RegularizationMesh(mesh) + + self._regularization_mesh = mesh + + if active_cells is not None: + self._regularization_mesh.active_cells = active_cells + + objfcts = [ + AmplitudeSmallness(mesh=self.regularization_mesh, mapping=mapping), + AmplitudeSmoothnessFirstOrder( + mesh=self.regularization_mesh, orientation="x", mapping=mapping + ), + ] + + if mesh.dim > 1: + objfcts.append( + AmplitudeSmoothnessFirstOrder( + mesh=self.regularization_mesh, orientation="y", mapping=mapping + ) + ) + + if mesh.dim > 2: + objfcts.append( + AmplitudeSmoothnessFirstOrder( + mesh=self.regularization_mesh, orientation="z", mapping=mapping + ) + ) + + super().__init__( + self.regularization_mesh, + objfcts=objfcts, + mapping=mapping, + **kwargs, + ) diff --git a/SimPEG/seismic/__init__.py b/simpeg/seismic/__init__.py similarity index 100% rename from SimPEG/seismic/__init__.py rename to simpeg/seismic/__init__.py diff --git a/SimPEG/seismic/straight_ray_tomography/__init__.py b/simpeg/seismic/straight_ray_tomography/__init__.py similarity index 83% rename from SimPEG/seismic/straight_ray_tomography/__init__.py rename to simpeg/seismic/straight_ray_tomography/__init__.py index 68edbf39c3..c447198b10 100644 --- a/SimPEG/seismic/straight_ray_tomography/__init__.py +++ b/simpeg/seismic/straight_ray_tomography/__init__.py @@ -1,8 +1,8 @@ """ ================================================================================== -Straight Ray Tomography (:mod:`SimPEG.seismic.straight_ray_tomography`) +Straight Ray Tomography (:mod:`simpeg.seismic.straight_ray_tomography`) ================================================================================== -.. currentmodule:: SimPEG.seismic.straight_ray_tomography +.. currentmodule:: simpeg.seismic.straight_ray_tomography About ``straight_ray_tomography`` @@ -24,6 +24,7 @@ """ + from .simulation import Simulation2DIntegral as Simulation from .survey import StraightRaySurvey as Survey from ...survey import BaseSrc as Src diff --git a/SimPEG/seismic/straight_ray_tomography/simulation.py b/simpeg/seismic/straight_ray_tomography/simulation.py similarity index 100% rename from SimPEG/seismic/straight_ray_tomography/simulation.py rename to simpeg/seismic/straight_ray_tomography/simulation.py diff --git a/SimPEG/seismic/straight_ray_tomography/survey.py b/simpeg/seismic/straight_ray_tomography/survey.py similarity index 100% rename from SimPEG/seismic/straight_ray_tomography/survey.py rename to simpeg/seismic/straight_ray_tomography/survey.py diff --git a/simpeg/simulation.py b/simpeg/simulation.py new file mode 100644 index 0000000000..0158205bb3 --- /dev/null +++ b/simpeg/simulation.py @@ -0,0 +1,1079 @@ +""" +Define simulation classes. +""" + +from __future__ import annotations # needed to use type operands in Python 3.8 +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 . import props +from .typing import RandomSeed +from .data import SyntheticData, Data +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 + +__all__ = ["LinearSimulation", "ExponentialSinusoidSimulation"] + + +############################################################################## +# # +# Simulation Base Classes # +# # +############################################################################## + + +class BaseSimulation(props.HasModel): + r"""Base class for all geophysical forward simulations in SimPEG. + + The ``BaseSimulation`` class defines properties and methods inherited by + practical simulation classes in SimPEG. + + .. important:: + This class is not meant to be instantiated. You should inherit from it to + create your own simulation class. + + 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 + SimPEG ``Counter`` object to store iterations and run-times. + verbose : bool, optional + Verbose progress printout. + """ + + _REGISTRY = {} + + def __init__( + self, + mesh=None, + survey=None, + solver=None, + solver_opts=None, + sensitivity_path=None, + counter=None, + verbose=False, + **kwargs, + ): + self._store_sensitivities: str | None = None + 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`. + + 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 + + @property + def survey(self): + """The survey for the simulation. + + Returns + ------- + simpeg.survey.BaseSurvey + The survey for the simulation. + """ + return self._survey + + @survey.setter + def survey(self, value): + if value is not None: + value = validate_type("survey", value, BaseSurvey, cast=False) + self._survey = value + + @property + def counter(self): + """SimPEG ``Counter`` object to store iterations and run-times. + + Returns + ------- + None or simpeg.utils.Counter + SimPEG ``Counter`` object to store iterations and run-times. + """ + return self._counter + + @counter.setter + def counter(self, value): + if value is not None: + value = validate_type("counter", value, Counter, cast=False) + self._counter = value + + @property + def sensitivity_path(self): + """Path to directory where sensitivity file is stored. + + Returns + ------- + str + Path to directory where sensitivity file is stored. + """ + return self._sensitivity_path + + @sensitivity_path.setter + 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. + + Returns + ------- + bool + Verbose progress printout status. + """ + return self._verbose + + @verbose.setter + def verbose(self, value): + self._verbose = validate_type("verbose", value, bool) + + def fields(self, m=None): + r"""Return the computed geophysical fields for the model provided. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + + Returns + ------- + simpeg.fields.Fields + Computed geophysical fields for the model provided. + + """ + raise NotImplementedError("fields has not been implemented for this ") + + def dpred(self, m=None, f=None): + r"""Predicted data for the model provided. + + Parameters + ---------- + m : (n_param,) numpy.ndarray + The model parameters. + f : simpeg.fields.Fields, optional + If provided, will be used to compute the predicted data + without recalculating the fields. + + Returns + ------- + (n_data, ) numpy.ndarray + The predicted data vector. + """ + if self.survey is None: + raise AttributeError( + "The survey has not yet been set and is required to compute " + "data. Please set the survey for the simulation: " + "simulation.survey = survey" + ) + + if f is None: + if m is None: + m = self.model + + f = self.fields(m) + + data = Data(self.survey) + 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) + + @timeIt + def Jvec(self, m, v, f=None): + r"""Compute the Jacobian times a vector for the model provided. + + The Jacobian defines the derivative of the predicted data vector with respect to the + model parameters. For a data vector :math:`\mathbf{d}` predicted for a set of model parameters + :math:`\mathbf{m}`, the Jacobian is an (n_data, n_param) matrix whose elements + are given by: + + .. math:: + J_{ij} = \frac{\partial d_i}{\partial m_j} + + For a model `m` and vector `v`, the ``Jvec`` method computes the matrix-vector product + + .. math:: + \mathbf{u} = \mathbf{J \, v} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model parameters. + v : (n_param, ) numpy.ndarray + Vector we are multiplying. + f : simpeg.field.Fields, optional + If provided, fields will not need to be recomputed for the + current model to compute `Jvec`. + + Returns + ------- + (n_data, ) numpy.ndarray + The Jacobian times a vector for the model and vector provided. + """ + raise NotImplementedError("Jvec is not yet implemented.") + + @timeIt + def Jtvec(self, m, v, f=None): + r"""Compute the Jacobian transpose times a vector for the model provided. + + The Jacobian defines the derivative of the predicted data vector with respect to the + model parameters. For a data vector :math:`\mathbf{d}` predicted for a set of model parameters + :math:`\mathbf{m}`, the Jacobian is an ``(n_data, n_param)`` matrix whose elements + are given by: + + .. math:: + J_{ij} = \frac{\partial d_i}{\partial m_j} + + For a model `m` and vector `v`, the ``Jtvec`` method computes the matrix-vector product with the adjoint-sensitivity + + .. math:: + \mathbf{u} = \mathbf{J^T \, v} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model parameters. + v : (n_data, ) numpy.ndarray + Vector we are multiplying. + f : simpeg.field.Fields, optional + If provided, fields will not need to be recomputed for the + current model to compute `Jtvec`. + + Returns + ------- + (n_param, ) numpy.ndarray + The Jacobian transpose times a vector for the model and vector provided. + """ + raise NotImplementedError("Jtvec is not yet implemented.") + + @timeIt + def Jvec_approx(self, m, v, f=None): + r"""Approximation of the Jacobian times a vector for the model provided. + + The Jacobian defines the derivative of the predicted data vector with respect to the + model parameters. For a data vector :math:`\mathbf{d}` predicted for a set of model parameters + :math:`\mathbf{m}`, the Jacobian is an ``(n_data, n_param)`` matrix whose elements + are given by: + + .. math:: + J_{ij} = \frac{\partial d_i}{\partial m_j} + + For a model `m` and vector `v`, the ``Jvec_approx`` method **approximates** + the matrix-vector product: + + .. math:: + \mathbf{u} = \mathbf{J \, v} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model parameters. + v : (n_data, ) numpy.ndarray + Vector we are multiplying. + f : simpeg.field.Fields, optional + If provided, fields will not need to be recomputed for the + current model to compute `Jtvec`. + + Returns + ------- + (n_param, ) numpy.ndarray + Approximation of the Jacobian times a vector for the model provided. + """ + return self.Jvec(m, v, f) + + @timeIt + def Jtvec_approx(self, m, v, f=None): + r"""Approximation of the Jacobian transpose times a vector for the model provided. + + The Jacobian defines the derivative of the predicted data vector with respect to the + model parameters. For a data vector :math:`\mathbf{d}` predicted for a set of model parameters + :math:`\mathbf{m}`, the Jacobian is an ``(n_data, n_param)`` matrix whose elements + are given by: + + .. math:: + J_{ij} = \frac{\partial d_i}{\partial m_j} + + For a model `m` and vector `v`, the ``Jtvec_approx`` method **approximates** + the matrix-vector product: + + .. math:: + \mathbf{u} = \mathbf{J^T \, v} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model parameters. + v : (n_data, ) numpy.ndarray + Vector we are multiplying. + f : simpeg.field.Fields, optional + If provided, fields will not need to be recomputed for the + current model to compute `Jtvec`. + + Returns + ------- + (n_param, ) numpy.ndarray + Approximation of the Jacobian transpose times a vector for the model provided. + """ + return self.Jtvec(m, v, f) + + @count + def residual(self, m, dobs, f=None): + r"""The data residual. + + This method computes and returns the data residual for the model provided. + Where :math:`\mathbf{d}_\text{obs}` are the observed data values, and :math:`\mathbf{d}_\text{pred}` + are the predicted data values for model parameters :math:`\mathbf{m}`, the data + residual is given by: + + .. math:: + \mathbf{r}(\mathbf{m}) = \mathbf{d}_\text{pred} - \mathbf{d}_\text{obs} + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model parameters. + dobs : (n_data, ) numpy.ndarray + The observed data values. + f : simpeg.fields.Fields, optional + If provided, fields will not need to be recomputed when solving the forward problem. + + Returns + ------- + (n_data, ) numpy.ndarray + The data residual. + + """ + return mkvc(self.dpred(m, f=f) - dobs) + + def make_synthetic_data( + self, + m, + relative_error=0.05, + noise_floor=0.0, + f=None, + add_noise=False, + random_seed: RandomSeed | None = None, + **kwargs, + ): + r"""Make synthetic data for the model and Gaussian noise provided. + + This method generates and returns a :py:class:`simpeg.data.SyntheticData` object + for the model and standard deviation of Gaussian noise provided. + + Parameters + ---------- + m : (n_param, ) numpy.ndarray + The model parameters. + relative_error : float, numpy.ndarray + Assign relative uncertainties to the data using relative error; sometimes + referred to as percent uncertainties. For each datum, we assume the + standard deviation of Gaussian noise is the relative error times the + absolute value of the datum; i.e. :math:`C_\text{err} \times |d|`. + noise_floor : float, numpy.ndarray + Assign floor/absolute uncertainties to the data. For each datum, we assume + standard deviation of Gaussian noise is equal to `noise_floor`. + f : simpeg.fields.Fields, optional + If provided, fields will not need to be recomputed when solving the + forward problem to obtain noiseless data. + add_noise : bool + Whether to add gaussian noise to the synthetic data or not. + 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 + ------- + simpeg.data.SyntheticData + A SimPEG synthetic data object, which organizes both clean and noisy data. + """ + + std = kwargs.pop("std", None) + if std is not None: + raise TypeError( + "The std parameter has been removed. " "Please use relative_error." + ) + + if f is None: + f = self.fields(m) + + dclean = self.dpred(m, f=f) + + if add_noise is True: + 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: + dobs = dclean + + return SyntheticData( + survey=self.survey, + dobs=dobs, + dclean=dclean, + relative_error=relative_error, + noise_floor=noise_floor, + ) + + @property + def store_sensitivities(self): + """Options for storing sensitivities. + + There are 3 options: + + - 'ram': sensitivity matrix stored in RAM + - 'disk': sensitivities written and stored to disk + - 'forward_only': sensitivities are not store (only use for forward simulation) + + Returns + ------- + {'disk', 'ram', 'forward_only'} + A string defining the model type for the simulation. + """ + if self._store_sensitivities is None: + self._store_sensitivities = "ram" + return self._store_sensitivities + + @store_sensitivities.setter + def store_sensitivities(self, value): + self._store_sensitivities = validate_string( + "store_sensitivities", value, ["disk", "ram", "forward_only"] + ) + + +class BaseTimeSimulation(BaseSimulation): + r"""Base class for time domain simulations. + + The ``BaseTimeSimulation`` defines properties and methods that are required + when the finite volume approach is used to solve time-dependent forward simulations. + Presently, SimPEG discretizes in time using the backward Euler approach. + And as such, the user must now define the step lengths for the forward simulation. + + 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 + The time step lengths, in seconds, for the time domain simulation. + This property can be also be set using a compact form; see *Notes*. + + Notes + ----- + There are two ways in which the user can set the ``time_steps`` property + for the forward simulation. The most basic approach is to use a ``(n_steps, )`` + :py:class:`numpy.ndarray` that explicitly defines the step lengths in order. + I.e.: + + >>> sim.time_steps = np.r_[1e-6, 1e-6, 1e-6, 1e-5, 1e-5, 1e-4, 1e-4] + + We can define also define the step lengths in compact for when the same + step length is reused multiple times in succession. In this case, the + ``time_steps`` property is set using a ``list`` of ``tuple``. Each + ``tuple`` contains the step length and number of times that step is repeated. + The time stepping defined above can be set equivalently with: + + >>> sim.time_steps = [(1e-6, 3), (1e-5, 2), (1e-4, 2)] + + When set, the :py:func:`discretize.utils.unpack_widths` utility is + used to convert the ``list`` of ``tuple`` to its (n_steps, ) :py:class:`numpy.ndarray` + representation. + """ + + def __init__(self, mesh=None, t0=0.0, time_steps=None, **kwargs): + self.t0 = t0 + self.time_steps = time_steps + super().__init__(mesh=mesh, **kwargs) + + @property + def time_steps(self): + """Time step lengths, in seconds, for the time domain simulation. + + There are two ways in which the user can set the ``time_steps`` property + for the forward simulation. The most basic approach is to use a ``(n_steps, )`` + :py:class:`numpy.ndarray` that explicitly defines the step lengths in order. + I.e.: + + >>> sim.time_steps = np.r_[1e-6, 1e-6, 1e-6, 1e-5, 1e-5, 1e-4, 1e-4] + + We can define also define the step lengths in compact for when the same + step length is reused multiple times in succession. In this case, the + ``time_steps`` property is set using a ``list`` of ``tuple``. Each + ``tuple`` contains the step length and number of times that step is repeated. + The time stepping defined above can be set equivalently with: + + >>> sim.time_steps = [(1e-6, 3), (1e-5, 2), (1e-4, 2)] + + When set, the :py:func:`discretize.utils.unpack_widths` utility is + used to convert the ``list`` of ``tuple`` to its ``(n_steps, )`` :py:class:`numpy.ndarray` + representation. + + Returns + ------- + (n_steps, ) numpy.ndarray + The time step lengths for the time domain simulation. + """ + return self._time_steps + + @time_steps.setter + def time_steps(self, value): + if value is not None: + if isinstance(value, list): + value = unpack_widths(value) + value = validate_ndarray_with_shape("time_steps", value, shape=("*",)) + self._time_steps = value + del self.time_mesh + + @property + def t0(self): + """Initial time, in seconds, for the time-dependent forward simulation. + + Returns + ------- + float + Initial time, in seconds, for the time-dependent forward simulation. + """ + return self._t0 + + @t0.setter + def t0(self, value): + self._t0 = validate_float("t0", value) + del self.time_mesh + + @property + def time_mesh(self): + r"""Time mesh for easy interpolation to observation times. + + The time mesh is constructed internally from the :py:attr:`t0` and + :py:attr:`time_steps` properties using the :py:class:`discretize.TensorMesh` class. + The ``time_mesh`` property allows for easy interpolation from fields computed at + discrete time-steps, to an arbitrary set of observation + times within the continuous interval (:math:`t_0 , t_\text{end}`). + + Returns + ------- + discretize.TensorMesh + The time mesh. + """ + if getattr(self, "_time_mesh", None) is None: + self._time_mesh = TensorMesh( + [ + self.time_steps, + ], + x0=[self.t0], + ) + return self._time_mesh + + @time_mesh.deleter + def time_mesh(self): + if hasattr(self, "_time_mesh"): + del self._time_mesh + + @property + def nT(self): + """Total number of time steps. + + Returns + ------- + int + Total number of time steps. + """ + return self.time_mesh.n_cells + + @property + def times(self): + """Evaluation times. + + Returns the discrete set of times at which the fields are computed for + the forward simulation. + + Returns + ------- + (nT, ) numpy.ndarray + The discrete set of times at which the fields are computed for + the forward simulation. + """ + return self.time_mesh.nodes_x + + def dpred(self, m=None, f=None): + # Docstring inherited from BaseSimulation. + if self.survey is None: + raise AttributeError( + "The survey has not yet been set and is required to compute " + "data. Please set the survey for the simulation: " + "simulation.survey = survey" + ) + + if f is None: + f = self.fields(m) + + data = Data(self.survey) + 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 + + +############################################################################## +# # +# Linear Simulation # +# # +############################################################################## + + +class LinearSimulation(BaseSimulation): + r"""Linear forward simulation class. + + The ``LinearSimulation`` class is used to define forward simulations of the form: + + .. math:: + \mathbf{d} = \mathbf{G \, f}(\mathbf{m}) + + where :math:`\mathbf{m}` are the model parameters, :math:`\mathbf{f}` is a + mapping operator (optional) from the model space to a user-defined parameter space, + :math:`\mathbf{d}` is the predicted data vector, and :math:`\mathbf{G}` is an + ``(n_data, n_param)`` linear operator. + + The ``LinearSimulation`` class is generally used as a base class that is inherited by + other simulation classes within SimPEG. However, it can be used directly as a + simulation class if the :py:attr:`G` property is used to set the linear forward + operator directly. + + By default, we assume the mapping operator :math:`\mathbf{f}` is the identity map, + and that the forward simulation reduces to: + + .. math:: + \mathbf{d} = \mathbf{G \, m} + + 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 + The linear operator. For a ``model_map`` that maps within the same vector space + (e.g. the identity map), the dimension ``n_param`` equals the number of model parameters. + If not, the dimension ``n_param`` of the linear operator will depend on the mapping. + """ + + linear_model, model_map, model_deriv = props.Invertible( + "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) + self.linear_model = linear_model + self.model_map = model_map + self.solver = None + if G is not None: + self.G = G + + if self.survey is None: + # Give it an empty survey + self.survey = BaseSurvey([]) + if self.survey.nD == 0: + # try seting the number of data to G + if getattr(self, "G", None) is not None: + self.survey._vnD = np.r_[self.G.shape[0]] + + @property + def G(self): + """The linear operator. + + Returns + ------- + (n_data, n_param) numpy.ndarray or scipy.sparse.csr_matrix + The linear operator. For a :py:attr:`model_map` that maps within the same vector space + (e.g. the identity map), the dimension ``n_param`` equals the number of model parameters. + If not, the dimension ``n_param`` of the linear operator will depend on the mapping. + """ + if getattr(self, "_G", None) is not None: + return self._G + else: + warnings.warn("G has not been implemented for the simulation", stacklevel=2) + return None + + @G.setter + def G(self, G): + # Allows setting G in a LinearSimulation. + # TODO should be validated + self._G = G + + def fields(self, m): + # Docstring inherited from BaseSimulation. + self.model = m + return self.G.dot(self.linear_model) + + def dpred(self, m=None, f=None): + # Docstring inherited from BaseSimulation + if m is not None: + self.model = m + if f is not None: + return f + return self.fields(self.model) + + def getJ(self, m, f=None): + r"""Returns the full Jacobian. + + The general definition of the linear forward simulation is: + + .. math:: + \mathbf{d} = \mathbf{G \, f}(\mathbf{m}) + + where :math:`\mathbf{f}` is a mapping operator (optional) from the model space + to a user-defined parameter space, and :math:`\mathbf{G}` is an (n_data, n_param) + linear operator. The ``getJ`` method forms and returns the full Jacobian: + + .. math:: + \mathbf{J}(\mathbf{m}) = \mathbf{G} \frac{\partial \mathbf{f}}{\partial \mathbf{m}} + + for the model :math:`\mathbf{m}` provided. When :math:`\mathbf{f}` is the identity map + (default), the Jacobian is no longer model-dependent and reduces to: + + .. math:: + \mathbf{J} = \mathbf{G} + + Parameters + ---------- + m : numpy.ndarray + The model vector. + f : None + Precomputed fields are not used to speed up the computation of the + Jacobian for linear problems. + + Returns + ------- + J : (n_data, n_param) numpy.ndarray + :math:`J = G\frac{\partial f}{\partial\mathbf{m}}`. + Where :math:`f` is :attr:`model_map`. + """ + self.model = m + # self.model_deriv is likely a sparse matrix + # and G is possibly dense, thus we need to do.. + return (self.model_deriv.T.dot(self.G.T)).T + + def Jvec(self, m, v, f=None): + # Docstring inherited from BaseSimulation + self.model = m + return self.G.dot(self.model_deriv * v) + + def Jtvec(self, m, v, f=None): + # Docstring inherited from BaseSimulation + self.model = m + return self.model_deriv.T * self.G.T.dot(v) + + +class ExponentialSinusoidSimulation(LinearSimulation): + r"""Simulation class for exponentially decaying sinusoidal kernel functions. + + This is the simulation class for the linear problem consisting of + exponentially decaying sinusoids. The entries of the linear operator + :math:`\mathbf{G}` are: + + .. math:: + + G_{ik} = \int_\Omega e^{p \, j_i \, x_k} \cos(\pi \, q \, j_i \, x_k) \, dx + + The model is defined on a 1D :py:class:`discretize.TensorMesh`, and :math:`x_k` + are the cell center locations. :math:`p \leq 0` defines the rate of exponential + decay of the kernel functions. :math:`q` defines the rate of oscillation of + the kernel functions. And :math:`j_i \in [j_0, ... , j_n]` controls the spread + of the kernel functions; the number of which is set using the ``n_kernels`` + property. + + .. tip:: + + For proper scaling, we advise defining the 1D tensor mesh to + discretize the interval [0, 1]. + + The kernel functions take the form: + + .. math:: + + \int_x e^{p j_k x} \cos(\pi q j_k x) \quad, j_k \in [j_0, ..., j_n] + + The model is defined at cell centers while the kernel functions are defined on nodes. + The trapezoid rule is used to evaluate the integral + + .. math:: + + d_j = \int g_j(x) m(x) dx + + to define our data. + + Parameters + ---------- + 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 + in the linear forward operator. + p : float + Exponent specifying the decay (`p \leq 0`) or growth (`p \geq 0`) of the kernel. For decay, set :math:`p \leq 0`. + q : float + Rate of oscillation of the kernel. + j0 : float + Minimum value for the spread of the kernel factors. + jn : float + 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): + self.n_kernels = n_kernels + self.p = p + self.q = q + self.j0 = j0 + self.jn = jn + super(ExponentialSinusoidSimulation, self).__init__(**kwargs) + + @property + def n_kernels(self): + r"""The number of kernel factors for the linear problem. + + Where :math:`j_0` represents the minimum value for the spread of + kernel factors and :math:`j_n` represents the maximum, ``n_kernels`` + defines the number of kernel factors :math:`j_i \in [j_0, ... , j_n]`. + This ultimately sets the number of rows in the linear forward operator. + + Returns + ------- + int + The number of kernel factors for the linear problem. + """ + return self._n_kernels + + @n_kernels.setter + def n_kernels(self, value): + self._n_kernels = validate_integer("n_kernels", value, min_val=1) + + @property + def p(self): + """Rate of exponential decay of the kernel. + + Returns + ------- + float + Rate of exponential decay of the kernel. + """ + return self._p + + @p.setter + def p(self, value): + self._p = validate_float("p", value) + + @property + def q(self): + """Rate of oscillation of the kernel. + + Returns + ------- + float + Rate of oscillation of the kernel. + """ + return self._q + + @q.setter + def q(self, value): + self._q = validate_float("q", value) + + @property + def j0(self): + """Minimum value for the spread of the kernel factors. + + Returns + ------- + float + Minimum value for the spread of the kernel factors. + """ + return self._j0 + + @j0.setter + def j0(self, value): + self._j0 = validate_float("j0", value) + + @property + def jn(self): + """Maximum value for the spread of the kernel factors. + + Returns + ------- + float + Maximum value for the spread of the kernel factors. + """ + return self._jn + + @jn.setter + def jn(self, value): + self._jn = validate_float("jn", value) + + @property + def jk(self): + """The set of kernel factors controlling the spread of the kernel functions. + + Returns + ------- + (n_kernels, ) numpy.ndarray + The set of kernel factors controlling the spread of the kernel functions. + """ + if getattr(self, "_jk", None) is None: + self._jk = np.linspace(self.j0, self.jn, self.n_kernels) + return self._jk + + def g(self, k): + """Kernel functions evaluated for kernel factor :math:`j_k`. + + This method computes the row of the linear forward operator for + the kernel functions for kernel factor :math:`j_k`, given :math:`k` + + Parameters + ---------- + k : int + Kernel functions for kernel factor *k* + + Returns + ------- + (n_param, ) numpy.ndarray + Kernel functions evaluated for kernel factor *k*. + """ + return np.exp(self.p * self.jk[k] * self.mesh.nodes_x) * np.cos( + np.pi * self.q * self.jk[k] * self.mesh.nodes_x + ) + + @property + def G(self): + """The linear forward operator. + + Returns + ------- + (n_kernels, n_param) numpy.ndarray + The linear forward operator. + """ + if getattr(self, "_G", None) is None: + G_nodes = np.empty((self.mesh.n_nodes, self.n_kernels)) + + for i in range(self.n_kernels): + G_nodes[:, i] = self.g(i) + + self._G = (self.mesh.average_node_to_cell @ G_nodes).T @ sdiag( + self.mesh.cell_volumes + ) + return self._G diff --git a/SimPEG/survey.py b/simpeg/survey.py similarity index 97% rename from SimPEG/survey.py rename to simpeg/survey.py index 731952089d..65335b43a5 100644 --- a/SimPEG/survey.py +++ b/simpeg/survey.py @@ -40,7 +40,8 @@ def __init__(self, locations, storeProjections=False, **kwargs): if projGLoc is not None: warnings.warn( "'projGLoc' is no longer of property of the receiver class. It is set automatically " - "based on the receiver and simulation class. Will be remove in SimPEG 0.18.0" + "based on the receiver and simulation class. Will be remove in SimPEG 0.18.0", + stacklevel=2, ) # ideally there shouldn't be any kwargs left to hit, but this will throw an # error if kwargs hasn't been emptied properly. This also will allow proper @@ -283,7 +284,7 @@ class BaseSrc: Parameters ---------- - receiver_list : list of SimPEG.survey.BaseRx objects + receiver_list : list of simpeg.survey.BaseRx objects Sets the receivers associated with the source location : (n_dim) numpy.ndarray Location of the source @@ -327,7 +328,7 @@ def receiver_list(self): Returns ------- - list of SimPEG.survey.BaseRx + list of simpeg.survey.BaseRx List of receivers associated with the source """ return self._receiver_list @@ -366,7 +367,7 @@ def get_receiver_indices(self, receivers): Parameters ---------- - receivers : list of SimPEG.survey.BaseRx + receivers : list of simpeg.survey.BaseRx A subset list of receivers within the source's receivers list Returns @@ -418,9 +419,9 @@ class BaseSurvey: Parameters ---------- - source_list : list of SimPEG.survey.BaseSrc objects + source_list : list of simpeg.survey.BaseSrc objects Sets the sources (and their receivers) - counter : SimPEG.utils.Counter + counter : simpeg.utils.Counter A SimPEG counter object """ @@ -442,7 +443,7 @@ def source_list(self): Returns ------- - list of SimPEG.survey.BaseSrc + list of simpeg.survey.BaseSrc List of sources associated with the survey """ return self._source_list @@ -487,7 +488,7 @@ def counter(self): Returns ------- - SimPEG.utils.counter_utils.Counter + simpeg.utils.counter_utils.Counter A SimPEG counter object """ return self._counter diff --git a/simpeg/typing/__init__.py b/simpeg/typing/__init__.py new file mode 100644 index 0000000000..07975782bd --- /dev/null +++ b/simpeg/typing/__init__.py @@ -0,0 +1,61 @@ +""" +============================= +Typing (:mod:`simpeg.typing`) +============================= + +This module provides additional `PEP 484 `_ +type aliases used in ``simpeg``'s codebase. + +API +--- + +.. autosummary:: + :toctree: generated/ + + RandomSeed + +""" + +from __future__ import annotations +import numpy as np +import numpy.typing as npt +from typing import Union + +# Use try and except to support Python<3.10 +try: + from typing import TypeAlias + + RandomSeed: TypeAlias = Union[ + int, + npt.NDArray[np.int_], + np.random.SeedSequence, + np.random.BitGenerator, + np.random.Generator, + ] +except ImportError: + RandomSeed = Union[ + int, + npt.NDArray[np.int_], + np.random.SeedSequence, + np.random.BitGenerator, + np.random.Generator, + ] + +RandomSeed.__doc__ = """ +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) +... ... +""" diff --git a/SimPEG/utils/__init__.py b/simpeg/utils/__init__.py similarity index 86% rename from SimPEG/utils/__init__.py rename to simpeg/utils/__init__.py index 2b6b3c7ba7..d5506d510c 100644 --- a/SimPEG/utils/__init__.py +++ b/simpeg/utils/__init__.py @@ -1,8 +1,8 @@ """ ======================================================== -Utility Classes and Functions (:mod:`SimPEG.utils`) +Utility Classes and Functions (:mod:`simpeg.utils`) ======================================================== -.. currentmodule:: SimPEG.utils +.. currentmodule:: simpeg.utils The ``utils`` package contains utilities for helping with common operations involving SimPEG. @@ -11,15 +11,6 @@ documentation for many details on items. -Coordinates Utility Functions -============================= - -.. autosummary:: - :toctree: generated/ - - rotation_matrix_from_normals - rotate_points_from_normals - Counter Utility Functions ========================= @@ -30,17 +21,6 @@ count timeIt -Curvilinear Utility Functions -============================= - -.. autosummary:: - :toctree: generated/ - - example_curvilinear_grid - face_info - index_cube - volume_tetrahedron - IO Utility Functions ==================== @@ -71,30 +51,12 @@ .. autosummary:: :toctree: generated/ - av - av_extrap cartesian2spherical coterminal - ddx define_plane_from_points - diagEst eigenvalue_by_power_iteration estimate_diagonal - get_subarray - kron3 - ind2sub - inverse_2x2_block_diagonal - inverse_3x3_block_diagonal - inverse_property_tensor - make_property_tensor - mkvc - ndgrid - sdiag - sdinv - speye spherical2cartesian - spzeros - sub2ind unique_rows @@ -104,9 +66,6 @@ .. autosummary:: :toctree: generated/ - closest_points_index - extract_core_mesh - unpack_widths surface2inds @@ -117,7 +76,6 @@ :toctree: generated/ depth_weighting - surface2ind_topo model_builder.add_block model_builder.create_2_layer_model model_builder.create_block_in_wholespace @@ -157,7 +115,6 @@ .. autosummary:: :toctree: generated/ - as_array_n_by_dim call_hooks check_stoppers mem_profile_class @@ -169,7 +126,6 @@ deprecate_property hook print_done - printDone print_line print_stoppers print_titles @@ -186,6 +142,7 @@ validate_active_indices """ + from discretize.utils.interpolation_utils import interpolation_matrix from .code_utils import ( @@ -268,7 +225,7 @@ rotation_matrix_from_normals, rotate_points_from_normals, ) -from .model_utils import surface2ind_topo, depth_weighting +from .model_utils import depth_weighting from .plot_utils import plot2Ddata, plotLayer, plot_1d_layer_model from .io_utils import download from .pgi_utils import ( @@ -281,7 +238,7 @@ # Deprecated imports interpmat = deprecate_function( - interpolation_matrix, "interpmat", removal_version="0.19.0", future_warn=True + interpolation_matrix, "interpmat", removal_version="0.19.0", error=True ) from .code_utils import ( diff --git a/SimPEG/utils/code_utils.py b/simpeg/utils/code_utils.py similarity index 95% rename from SimPEG/utils/code_utils.py rename to simpeg/utils/code_utils.py index 0241990316..8c2014b216 100644 --- a/SimPEG/utils/code_utils.py +++ b/simpeg/utils/code_utils.py @@ -6,7 +6,7 @@ from discretize.utils import as_array_n_by_dim # noqa: F401 -# scooby is a soft dependency for SimPEG +# scooby is a soft dependency for simpeg try: from scooby import Report as ScoobyReport except ImportError: @@ -14,7 +14,7 @@ class ScoobyReport: def __init__(self, additional, core, optional, ncol, text_width, sort): print( - "\n *ERROR*: `SimPEG.Report` requires `scooby`." + "\n *ERROR*: `simpeg.Report` requires `scooby`." "\n Install it via `pip install scooby` or" "\n `conda install -c conda-forge scooby`.\n" ) @@ -322,7 +322,7 @@ def call_hooks(match, mainFirst=False): Use the following syntax:: - @callHooks('doEndIteration') + @call_hooks('doEndIteration') def doEndIteration(self): pass @@ -454,7 +454,7 @@ class Report(ScoobyReport): >>> import pytest >>> import dateutil - >>> from SimPEG import Report + >>> from simpeg import Report >>> Report() # Default values >>> Report(pytest) # Provide additional package >>> Report([pytest, dateutil], ncol=5) # Define nr of columns @@ -466,7 +466,7 @@ def __init__(self, add_pckg=None, ncol=3, text_width=80, sort=False): # Mandatory packages. core = [ - "SimPEG", + "simpeg", "discretize", "pymatsolver", "numpy", @@ -491,6 +491,7 @@ def __init__(self, add_pckg=None, ncol=3, text_width=80, sort=False): "vtk", "utm", "memory_profiler", + "choclo", ] super().__init__( @@ -546,11 +547,11 @@ def decorator(cls): def __init__(self, *args, **kwargs): if future_warn: - warnings.warn(message, FutureWarning) + warnings.warn(message, FutureWarning, stacklevel=2) elif error: raise NotImplementedError(message) else: - warnings.warn(message, DeprecationWarning) + warnings.warn(message, DeprecationWarning, stacklevel=2) self._old__init__(*args, **kwargs) cls.__init__ = __init__ @@ -589,11 +590,11 @@ def deprecate_module( message += " It will be removed in a future version of SimPEG." message += " Please update your code accordingly." if future_warn: - warnings.warn(message, FutureWarning) + warnings.warn(message, FutureWarning, stacklevel=2) elif error: raise NotImplementedError(message) else: - warnings.warn(message, DeprecationWarning) + warnings.warn(message, DeprecationWarning, stacklevel=2) def deprecate_property( @@ -639,20 +640,20 @@ def deprecate_property( def get_dep(self): if future_warn: - warnings.warn(message, FutureWarning) + warnings.warn(message, FutureWarning, stacklevel=2) elif error: raise NotImplementedError(message) else: - warnings.warn(message, DeprecationWarning) + warnings.warn(message, DeprecationWarning, stacklevel=2) return prop.fget(self) def set_dep(self, other): if future_warn: - warnings.warn(message, FutureWarning) + warnings.warn(message, FutureWarning, stacklevel=2) elif error: raise NotImplementedError(message) else: - warnings.warn(message, DeprecationWarning) + warnings.warn(message, DeprecationWarning, stacklevel=2) prop.fset(self, other) doc = f"`{old_name}` has been deprecated. See `{new_name}` for documentation" @@ -698,11 +699,11 @@ def deprecate_method( def new_method(*args, **kwargs): if future_warn: - warnings.warn(message, FutureWarning) + warnings.warn(message, FutureWarning, stacklevel=2) elif error: raise NotImplementedError(message) else: - warnings.warn(message, DeprecationWarning) + warnings.warn(message, DeprecationWarning, stacklevel=2) return method(*args, **kwargs) doc = f"`{old_name}` has been deprecated. See `{new_name}` for documentation" @@ -752,11 +753,11 @@ def deprecate_function( def dep_function(*args, **kwargs): if future_warn: - warnings.warn(message, FutureWarning) + warnings.warn(message, FutureWarning, stacklevel=2) elif error: raise NotImplementedError(message) else: - warnings.warn(message, DeprecationWarning) + warnings.warn(message, DeprecationWarning, stacklevel=2) return new_function(*args, **kwargs) doc = f""" @@ -1109,7 +1110,7 @@ def validate_type(property_name, obj, obj_type, cast=True, strict=False): f"{type(obj).__name__} cannot be converted to type {obj_type.__name__} " f"required for {property_name}." ) from err - if strict and type(obj) != obj_type: + if strict and type(obj) is not obj_type: raise TypeError( f"Object must be exactly a {obj_type.__name__} for {property_name}" ) @@ -1232,22 +1233,32 @@ def validate_active_indices(property_name, index_arr, n_cells): # DEPRECATIONS ############################################################### memProfileWrapper = deprecate_function( - mem_profile_class, "memProfileWrapper", removal_version="0.18.0" + 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 ) -setKwargs = deprecate_function(set_kwargs, "setKwargs", removal_version="0.18.0") -printTitles = deprecate_function(print_titles, "printTitles", removal_version="0.18.0") -printLine = deprecate_function(print_line, "printLine", removal_version="0.18.0") printStoppers = deprecate_function( - print_stoppers, "printStoppers", removal_version="0.18.0" + print_stoppers, "printStoppers", removal_version="0.18.0", error=True ) checkStoppers = deprecate_function( - check_stoppers, "checkStoppers", removal_version="0.18.0" + 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 ) -printDone = deprecate_function(print_done, "printDone", removal_version="0.18.0") -callHooks = deprecate_function(call_hooks, "callHooks", removal_version="0.18.0") dependentProperty = deprecate_function( - dependent_property, "dependentProperty", removal_version="0.18.0" + 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", future_warn=True + 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 similarity index 91% rename from SimPEG/utils/coord_utils.py rename to simpeg/utils/coord_utils.py index bb46021ba9..e1d17c5dbf 100644 --- a/SimPEG/utils/coord_utils.py +++ b/simpeg/utils/coord_utils.py @@ -9,11 +9,11 @@ rotation_matrix_from_normals, "rotationMatrixFromNormals", removal_version="0.19.0", - future_warn=True, + error=True, ) rotatePointsFromNormals = deprecate_function( rotate_points_from_normals, "rotatePointsFromNormals", removal_version="0.19.0", - future_warn=True, + error=True, ) diff --git a/SimPEG/utils/counter_utils.py b/simpeg/utils/counter_utils.py similarity index 98% rename from SimPEG/utils/counter_utils.py rename to simpeg/utils/counter_utils.py index c570de0a65..ded65c2cd8 100644 --- a/SimPEG/utils/counter_utils.py +++ b/simpeg/utils/counter_utils.py @@ -16,7 +16,7 @@ class Counter(object): decorators on class methods. - >>> from SimPEG.utils import Counter, count, timeIt + >>> from simpeg.utils import Counter, count, timeIt >>> class MyClass(object): ... def __init__(self, url): diff --git a/SimPEG/utils/curv_utils.py b/simpeg/utils/curv_utils.py similarity index 63% rename from SimPEG/utils/curv_utils.py rename to simpeg/utils/curv_utils.py index 6f516db1c9..71e764ce60 100644 --- a/SimPEG/utils/curv_utils.py +++ b/simpeg/utils/curv_utils.py @@ -8,17 +8,17 @@ # deprecated functions volTetra = deprecate_function( - volume_tetrahedron, "volTetra", removal_version="0.19.0", future_warn=True + volume_tetrahedron, "volTetra", removal_version="0.19.0", error=True ) indexCube = deprecate_function( - index_cube, "indexCube", removal_version="0.19.0", future_warn=True + index_cube, "indexCube", removal_version="0.19.0", error=True ) faceInfo = deprecate_function( - face_info, "faceInfo", removal_version="0.19.0", future_warn=True + face_info, "faceInfo", removal_version="0.19.0", error=True ) exampleLrmGrid = deprecate_function( example_curvilinear_grid, "exampleLrmGrid", removal_version="0.19.0", - future_warn=True, + error=True, ) diff --git a/SimPEG/utils/drivers/__init__.py b/simpeg/utils/drivers/__init__.py similarity index 100% rename from SimPEG/utils/drivers/__init__.py rename to simpeg/utils/drivers/__init__.py diff --git a/SimPEG/utils/drivers/gravity_driver.py b/simpeg/utils/drivers/gravity_driver.py similarity index 97% rename from SimPEG/utils/drivers/gravity_driver.py rename to simpeg/utils/drivers/gravity_driver.py index b63354bfe6..77c6fcfde4 100644 --- a/SimPEG/utils/drivers/gravity_driver.py +++ b/simpeg/utils/drivers/gravity_driver.py @@ -3,7 +3,7 @@ from discretize import TensorMesh from discretize.utils import active_from_xyz -from SimPEG import utils +from simpeg import utils import numpy as np @@ -110,7 +110,7 @@ def readDriverFile(self, input_file): l_input = re.split(r"[!\s]", line) if l_input[0] == "VALUE": val = np.array(l_input[1:5]) - alphas = val.astype(np.float) + alphas = val.astype(float) elif l_input[0] == "DEFAULT": alphas = np.ones(4) @@ -120,7 +120,7 @@ def readDriverFile(self, input_file): l_input = re.split(r"[!\s]", line) if l_input[0] == "VALUE": val = np.array(l_input[1:3]) - bounds = val.astype(np.float) + bounds = val.astype(float) elif l_input[0] == "FILE": bounds = l_input[1].rstrip() @@ -133,7 +133,7 @@ def readDriverFile(self, input_file): l_input = re.split(r"[!\s]", line) if l_input[0] == "VALUE": val = np.array(l_input[1:6]) - lpnorms = val.astype(np.float) + lpnorms = val.astype(float) elif l_input[0] == "FILE": lpnorms = l_input[1].rstrip() @@ -143,7 +143,7 @@ def readDriverFile(self, input_file): l_input = re.split(r"[!\s]", line) if l_input[0] == "VALUE": val = np.array(l_input[1:3]) - eps = val.astype(np.float) + eps = val.astype(float) elif l_input[0] == "DEFAULT": eps = None diff --git a/SimPEG/utils/drivers/magnetics_driver.py b/simpeg/utils/drivers/magnetics_driver.py similarity index 97% rename from SimPEG/utils/drivers/magnetics_driver.py rename to simpeg/utils/drivers/magnetics_driver.py index 4f7b884866..60b799afe8 100644 --- a/SimPEG/utils/drivers/magnetics_driver.py +++ b/simpeg/utils/drivers/magnetics_driver.py @@ -3,7 +3,7 @@ from discretize import TensorMesh from discretize.utils import active_from_xyz -from SimPEG import utils +from simpeg import utils import numpy as np @@ -119,7 +119,7 @@ def readDriverFile(self, input_file): l_input = re.split(r"[!\s]", line) if l_input[0] == "VALUE": val = np.array(l_input[1:5]) - alphas = val.astype(np.float) + alphas = val.astype(float) elif l_input[0] == "DEFAULT": alphas = np.ones(4) @@ -129,7 +129,7 @@ def readDriverFile(self, input_file): l_input = re.split(r"[!\s]", line) if l_input[0] == "VALUE": val = np.array(l_input[1:3]) - bounds = val.astype(np.float) + bounds = val.astype(float) elif l_input[0] == "FILE": bounds = l_input[1].rstrip() @@ -142,7 +142,7 @@ def readDriverFile(self, input_file): l_input = re.split(r"[!\s]", line) if l_input[0] == "VALUE": val = np.array(l_input[1:6]) - lpnorms = val.astype(np.float) + lpnorms = val.astype(float) elif l_input[0] == "FILE": lpnorms = l_input[1].rstrip() @@ -152,7 +152,7 @@ def readDriverFile(self, input_file): l_input = re.split(r"[!\s]", line) if l_input[0] == "VALUE": val = np.array(l_input[1:3]) - eps = val.astype(np.float) + eps = val.astype(float) elif l_input[0] == "DEFAULT": eps = None @@ -307,7 +307,7 @@ def magnetizationModel(self): # Convert list to 2d array M = np.vstack(M) - # Cycle through three components and permute from UBC to SimPEG + # Cycle through three components and permute from UBC to simpeg for ii in range(3): m = np.reshape( M[:, ii], diff --git a/SimPEG/utils/io_utils/__init__.py b/simpeg/utils/io_utils/__init__.py similarity index 70% rename from SimPEG/utils/io_utils/__init__.py rename to simpeg/utils/io_utils/__init__.py index b3226b2c2e..14d628ab3d 100644 --- a/SimPEG/utils/io_utils/__init__.py +++ b/simpeg/utils/io_utils/__init__.py @@ -18,11 +18,3 @@ write_dcipoctree_ubc, write_dcip_xyz, ) - -# Deprecated -from .io_utils_pf import ( - readUBCmagneticsObservations, - writeUBCmagneticsObservations, - readUBCgravityObservations, - writeUBCgravityObservations, -) diff --git a/SimPEG/utils/io_utils/io_utils_electromagnetics.py b/simpeg/utils/io_utils/io_utils_electromagnetics.py similarity index 95% rename from SimPEG/utils/io_utils/io_utils_electromagnetics.py rename to simpeg/utils/io_utils/io_utils_electromagnetics.py index 783c1abf36..e0306a17ef 100644 --- a/SimPEG/utils/io_utils/io_utils_electromagnetics.py +++ b/simpeg/utils/io_utils/io_utils_electromagnetics.py @@ -28,7 +28,7 @@ def read_dcip_xyz( locations provided. This function is versatile enough to load 2D or 3D data. The data file may include elevations for the electrodes or be surface formatted. Columns containing data which are not defined as part of a - :class:`SimPEG.data.Data` object may be loaded and output to a dictionary. + :class:`simpeg.data.Data` object may be loaded and output to a dictionary. Parameters ---------- @@ -62,10 +62,10 @@ def read_dcip_xyz( Returns ------- - SimPEG.data.Data + simpeg.data.Data DC or IP data. The survey attribute associated with the data object will be an - instance of :class:`SimPEG.electromagnetics.static.resistivity.survey.Survey` - or :class:`SimPEG.electromagnetics.static.induced_polarization.survey.Survey` + instance of :class:`simpeg.electromagnetics.static.resistivity.survey.Survey` + or :class:`simpeg.electromagnetics.static.induced_polarization.survey.Survey` dict If additional columns are loaded and output to a dictionary using the keyward argument `dict_headers`, the output of this function has the form `(out_data, out_dict)`. @@ -148,7 +148,8 @@ def read_dcip_xyz( warnings.warn( "Loaded data are in surface format. Elevations automatically set to 9999 m. " "Use the project_to_discretized_topography method of the survey to project " - "electrode locations to the discretized surface." + "electrode locations to the discretized surface.", + stacklevel=2, ) else: locations_a = data_array[:, a_cols] @@ -206,11 +207,11 @@ def read_dcip2d_ubc(file_name, data_type, format_type): Returns ------- - SimPEG.data.Data + simpeg.data.Data A SimPEG data object. The data from the input file is loaded and parsed into three attributes of the data object: - - `survey`: the survey geometry as defined by an instance of :class`SimPEG.electromagnetics.static.resistivity.survey.Survey` or :class`SimPEG.electromagnetics.static.induced_polarization.survey.Survey` + - `survey`: the survey geometry as defined by an instance of :class`simpeg.electromagnetics.static.resistivity.survey.Survey` or :class`simpeg.electromagnetics.static.induced_polarization.survey.Survey` - `dobs`: observed/predicted data if present in the data file - `standard_deviations`: uncertainties (if observed data file) or apparent resistivities (if predicted data file) @@ -315,7 +316,8 @@ def read_dcip2d_ubc(file_name, data_type, format_type): warnings.warn( "Loaded data did not have elevations. Elevations automatically set to 9999 m. " "Use the project_to_discretized_topography method of the survey to project " - "electrode locations to the discretized surface." + "electrode locations to the discretized surface.", + stacklevel=2, ) else: @@ -410,7 +412,8 @@ def read_dcip2d_ubc(file_name, data_type, format_type): warnings.warn( "Loaded data were in surface format. Elevations automatically set to 9999 m. " "Use the project_to_discretized_topography method of the survey to project " - "electrode locations to the discretized surface." + "electrode locations to the discretized surface.", + stacklevel=2, ) return data_out @@ -432,11 +435,11 @@ def read_dcip3d_ubc(file_name, data_type): Returns ------- - SimPEG.data.Data + simpeg.data.Data A SimPEG data object. The data from the input file is loaded and parsed into three attributes of the data object: - - `survey`: the survey geometry as defined by an instance of :class`SimPEG.electromagnetics.static.resitivity.survey.Survey` or :class`SimPEG.electromagnetics.static.induced_polarization.survey.Survey` + - `survey`: the survey geometry as defined by an instance of :class`simpeg.electromagnetics.static.resitivity.survey.Survey` or :class`simpeg.electromagnetics.static.induced_polarization.survey.Survey` - `dobs`: observed/predicted data if present in the data file - `standard_deviations`: uncertainties (if observed data file) or apparent resistivities (if predicted data file) @@ -569,7 +572,8 @@ def read_dcip3d_ubc(file_name, data_type): warnings.warn( "Loaded data were in surface format. Elevations automatically set to 9999 m. " "Use the project_to_discretized_topography method of the survey to project " - "electrode locations to the discretized surface." + "electrode locations to the discretized surface.", + stacklevel=2, ) return data_out @@ -591,11 +595,11 @@ def read_dcipoctree_ubc(file_name, data_type): Returns ------- - SimPEG.data.Data + simpeg.data.Data A SimPEG data object. The data from the input file is loaded and parsed into three attributes of the data object: - - `survey`: the survey geometry as defined by an instance of :class`SimPEG.electromagnetics.static.resistivity.survey.Survey` or :class`SimPEG.electromagnetics.static.induced_polarization.survey.Survey` + - `survey`: the survey geometry as defined by an instance of :class`simpeg.electromagnetics.static.resistivity.survey.Survey` or :class`simpeg.electromagnetics.static.induced_polarization.survey.Survey` - `dobs`: observed/predicted data if present in the data file - `standard_deviations`: uncertainties (if observed data file) or apparent resistivities (if predicted data file) @@ -623,9 +627,9 @@ def write_dcip2d_ubc( file_name : str file path for output file data_object : - SimPEG.data.Data object. The `survey` attribute of this data object must be - an instance of :class`SimPEG.electromagnetics.static.resistivity.survey.Survey` or - :class`SimPEG.electromagnetics.static.induced_polarization.survey.Survey` + simpeg.data.Data object. The `survey` attribute of this data object must be + an instance of :class`simpeg.electromagnetics.static.resistivity.survey.Survey` or + :class`simpeg.electromagnetics.static.induced_polarization.survey.Survey` data_type : {'volt', 'apparent_chargeability', 'secondary_potential'} The type of data. file_type : {'survey', 'dpred', 'dobs'} @@ -799,9 +803,9 @@ def write_dcip3d_ubc( file_name : str file path for output file data_object : - SimPEG.data.Data object. The `survey` attribute of this data object must be - an instance of :class`SimPEG.electromagnetics.static.resistivity.survey.Survey` or - :class`SimPEG.electromagnetics.static.induced_polarization.survey.Survey` + simpeg.data.Data object. The `survey` attribute of this data object must be + an instance of :class`simpeg.electromagnetics.static.resistivity.survey.Survey` or + :class`simpeg.electromagnetics.static.induced_polarization.survey.Survey` data_type : {'volt', 'apparent_chargeability', 'secondary_potential'} file_type : {'survey', 'dpred', 'dobs'} format_type : {'general', 'surface'} @@ -954,9 +958,9 @@ def write_dcipoctree_ubc( file_name : str file path for output file data_object : - SimPEG.data.Data object. The `survey` attribute of this data object must be - an instance of :class`SimPEG.electromagnetics.static.resistivity.survey.Survey` or - :class`SimPEG.electromagnetics.static.induced_polarization.survey.Survey` + simpeg.data.Data object. The `survey` attribute of this data object must be + an instance of :class`simpeg.electromagnetics.static.resistivity.survey.Survey` or + :class`simpeg.electromagnetics.static.induced_polarization.survey.Survey` data_type : {'volt', 'apparent_chargeability', 'secondary_potential'} file_type : {'survey', 'dpred', 'dobs'} format_type : {'general', 'surface'} @@ -988,10 +992,10 @@ def write_dcip_xyz( ---------- file_name : str Path to the file - data_object : SimPEG.data.Data - SimPEG.data.Data object. The `survey` attribute of this data object must be - an instance of :class`SimPEG.electromagnetics.static.resistivity.survey.Survey` or - :class`SimPEG.electromagnetics.static.induced_polarization.survey.Survey` + data_object : simpeg.data.Data + simpeg.data.Data object. The `survey` attribute of this data object must be + an instance of :class`simpeg.electromagnetics.static.resistivity.survey.Survey` or + :class`simpeg.electromagnetics.static.induced_polarization.survey.Survey` data_header: str Header for the data column; i.e. the header for the data defined in the `dobs` attibute of the data object. If ``None``, these data are not written to file @@ -1031,7 +1035,7 @@ def write_dcip_xyz( out_columns = np.c_[out_columns, data_object.standard_deviation] # Append additional columns from dictionary - if out_dict != None: + if out_dict is not None: for k in list(out_dict.keys()): out_headers += " " + k out_columns = np.c_[out_columns, out_dict[k]] diff --git a/SimPEG/utils/io_utils/io_utils_general.py b/simpeg/utils/io_utils/io_utils_general.py similarity index 98% rename from SimPEG/utils/io_utils/io_utils_general.py rename to simpeg/utils/io_utils/io_utils_general.py index c167bc4594..2fddb56243 100644 --- a/SimPEG/utils/io_utils/io_utils_general.py +++ b/simpeg/utils/io_utils/io_utils_general.py @@ -35,7 +35,7 @@ def read_GOCAD_ts(tsfile): while re.match("VRTX", line): l_input = re.split(r"[\s*]", line) temp = np.array(l_input[2:5]) - vrtx.append(temp.astype(np.float)) + vrtx.append(temp.astype(float)) # Read next line line = fid.readline() @@ -53,7 +53,7 @@ def read_GOCAD_ts(tsfile): while re.match("TRGL", line): l_input = re.split(r"[\s*]", line) temp = np.array(l_input[1:4]) - trgl.append(temp.astype(np.int)) + trgl.append(temp.astype(int)) # Read next line line = fid.readline() diff --git a/SimPEG/utils/io_utils/io_utils_pf.py b/simpeg/utils/io_utils/io_utils_pf.py similarity index 89% rename from SimPEG/utils/io_utils/io_utils_pf.py rename to simpeg/utils/io_utils/io_utils_pf.py index 5e9533f5e3..ff689c1902 100644 --- a/SimPEG/utils/io_utils/io_utils_pf.py +++ b/simpeg/utils/io_utils/io_utils_pf.py @@ -1,6 +1,5 @@ import numpy as np from discretize.utils import mkvc -from ...utils.code_utils import deprecate_method def read_mag3d_ubc(obs_file): @@ -17,9 +16,9 @@ def read_mag3d_ubc(obs_file): Returns ------- - SimPEG.data.Data + simpeg.data.Data Instance of a SimPEG data class. The `survey` attribute associated with - the data object is an instance of :class`SimPEG.potential_fields.magnetics.survey.Survey`. + the data object is an instance of :class`simpeg.potential_fields.magnetics.survey.Survey`. """ # Prevent circular import @@ -90,9 +89,9 @@ def write_mag3d_ubc(filename, data_object): ---------- filename : str File path for the output file - data_object : SimPEG.data.Data + data_object : simpeg.data.Data An instance of SimPEG data class. The `survey` attribute associate with the - data object must be an instance of :class:`SimPEG.potential_fields.magnetics.survey.Survey` + data object must be an instance of :class:`simpeg.potential_fields.magnetics.survey.Survey` """ survey = data_object.survey @@ -137,9 +136,9 @@ def read_grav3d_ubc(obs_file): Returns ------- - SimPEG.data.Data + simpeg.data.Data Instance of a SimPEG data class. The `survey` attribute associated with - the data object is an instance of :class`SimPEG.potential_fields.gravity.survey.Survey`. + the data object is an instance of :class`simpeg.potential_fields.gravity.survey.Survey`. """ # Prevent circular import @@ -202,9 +201,9 @@ def write_grav3d_ubc(filename, data_object): ---------- filename : str File path for the output file - data_object : SimPEG.data.Data + data_object : simpeg.data.Data An instance of SimPEG data class. The `survey` attribute associate with the - data object must be an instance of :class:`SimPEG.potential_fields.gravity.survey.Survey` + data object must be an instance of :class:`simpeg.potential_fields.gravity.survey.Survey` """ survey = data_object.survey src = survey.source_field @@ -245,9 +244,9 @@ def read_gg3d_ubc(obs_file): Returns ------- - SimPEG.data.Data + simpeg.data.Data Instance of a SimPEG data class. The `survey` attribute associated with - the data object is an instance of :class`SimPEG.potential_fields.gravity.survey.Survey`. + the data object is an instance of :class`simpeg.potential_fields.gravity.survey.Survey`. """ # Prevent circular import @@ -263,7 +262,7 @@ def read_gg3d_ubc(obs_file): n_comp = len(components) factor = np.zeros(n_comp) - # Convert component types from UBC to SimPEG + # Convert component types from UBC to simpeg ubc_types = ["xx", "xy", "xz", "yy", "yz", "zz", "uv"] simpeg_types = ["gyy", "gxy", "gyz", "gxx", "gxz", "gzz", "guv"] factor_list = [1.0, 1.0, -1.0, 1.0, -1.0, 1.0, 1.0] @@ -329,14 +328,14 @@ def write_gg3d_ubc(filename, data_object): ---------- filename : str File path for the output file - data_object : SimPEG.data.Data + data_object : simpeg.data.Data An instance of SimPEG data class. The `survey` attribute associate with the - data object must be an instance of :class:`SimPEG.potential_fields.gravity.survey.Survey` + data object must be an instance of :class:`simpeg.potential_fields.gravity.survey.Survey` """ survey = data_object.survey src = survey.source_field - # Convert component types from UBC to SimPEG + # Convert component types from UBC to simpeg if len(src.receiver_list) > 1: raise NotImplementedError( "Writing of ubc format only supported for a single receiver." @@ -379,22 +378,3 @@ def write_gg3d_ubc(filename, data_object): ) print("Observation file saved to: " + filename) - - -# ====================================================== -# Depricated Methods -# ====================================================== - - -readUBCmagneticsObservations = deprecate_method( - read_mag3d_ubc, "readUBCmagneticsObservations", removal_version="0.14.4" -) -writeUBCmagneticsObservations = deprecate_method( - write_mag3d_ubc, "writeUBCmagneticsObservations", removal_version="0.14.4" -) -readUBCgravityObservations = deprecate_method( - read_grav3d_ubc, "readUBCgravityObservations", removal_version="0.14.4" -) -writeUBCgravityObservations = deprecate_method( - write_grav3d_ubc, "writeUBCgravityObservations", removal_version="0.14.4" -) diff --git a/SimPEG/utils/mat_utils.py b/simpeg/utils/mat_utils.py similarity index 91% rename from SimPEG/utils/mat_utils.py rename to simpeg/utils/mat_utils.py index 5019a42bea..346a999993 100644 --- a/SimPEG/utils/mat_utils.py +++ b/simpeg/utils/mat_utils.py @@ -1,5 +1,7 @@ +from __future__ import annotations # needed to use type operands in Python 3.8 import numpy as np from .code_utils import deprecate_function +from ..typing import RandomSeed from discretize.utils import ( # noqa: F401 Zero, Identity, @@ -39,7 +41,7 @@ def estimate_diagonal(matrix_arg, n, k=None, approach="Probing"): and a vector. For background information on this method, see - `Bekas (et al., 2005) `__ + `Bekas (et al., 2005) `__ and `Selig (et al., 2012) `__ Parameters @@ -129,17 +131,21 @@ def unique_rows(M): 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, + seed: RandomSeed | None = None, ): r"""Estimate largest eigenvalue in absolute value using power iteration. Uses the power iteration approach to estimate the largest eigenvalue in absolute - value for a single :class:`SimPEG.BaseObjectiveFunction` or a combination of - objective functions stored in a :class:`SimPEG.ComboObjectiveFunction`. + value for a single :class:`simpeg.BaseObjectiveFunction` or a combination of + objective functions stored in a :class:`simpeg.ComboObjectiveFunction`. Parameters ---------- - combo_objfct : SimPEG.BaseObjectiveFunction + combo_objfct : simpeg.BaseObjectiveFunction Objective function or a combo objective function model : numpy.ndarray Current model @@ -148,10 +154,12 @@ def eigenvalue_by_power_iteration( fields_list : list (optional) ``list`` of fields objects for each data misfit term in combo_objfct. If none given, 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 + 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. + 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 ------- @@ -176,12 +184,10 @@ def eigenvalue_by_power_iteration( selected from a uniform distribution. """ - - if seed is not None: - np.random.seed(seed) + rng = np.random.default_rng(seed=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 @@ -307,17 +313,6 @@ def cartesian2amplitude_dip_azimuth(m): return atp -def spherical2cartesian(m): - """Convert from spherical to cartesian""" - m = m.reshape((-1, 3), order="F") - a = m[:, 0] + 1e-8 - t = m[:, 1] - p = m[:, 2] - m_xyz = np.r_[a * np.cos(t) * np.cos(p), a * np.cos(t) * np.sin(p), a * np.sin(t)] - - return m_xyz - - def spherical2cartesian(m): r""" Converts a set of 3D vectors from spherical to Catesian coordinates. @@ -420,7 +415,7 @@ def coterminal(theta): \theta = 2\pi N + \gamma and *N* is an integer, the function returns the value of :math:`\gamma`. - The coterminal angle :math:`\gamma` is within the range :math:`[-\pi , \pi]`. + The coterminal angle :math:`\gamma` is within the range :math:`[-\pi , \pi)`. Parameters ---------- @@ -475,33 +470,37 @@ def define_plane_from_points(xyz1, xyz2, xyz3): ################################################ -diagEst = deprecate_function(estimate_diagonal, "diagEst", removal_version="0.19.0") -uniqueRows = deprecate_function(unique_rows, "uniqueRows", removal_version="0.19.0") -sdInv = deprecate_function(sdinv, "sdInv", removal_version="0.19.0", future_warn=True) +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", future_warn=True + get_subarray, "getSubArray", removal_version="0.19.0", error=True ) inv3X3BlockDiagonal = deprecate_function( inverse_3x3_block_diagonal, "inv3X3BlockDiagonal", removal_version="0.19.0", - future_warn=True, + error=True, ) inv2X2BlockDiagonal = deprecate_function( inverse_2x2_block_diagonal, "inv2X2BlockDiagonal", removal_version="0.19.0", - future_warn=True, + error=True, ) makePropertyTensor = deprecate_function( make_property_tensor, "makePropertyTensor", removal_version="0.19.0", - future_warn=True, + error=True, ) invPropertyTensor = deprecate_function( inverse_property_tensor, "invPropertyTensor", removal_version="0.19.0", - future_warn=True, + error=True, ) diff --git a/SimPEG/utils/mesh_utils.py b/simpeg/utils/mesh_utils.py similarity index 95% rename from SimPEG/utils/mesh_utils.py rename to simpeg/utils/mesh_utils.py index 30a7e52143..1fc3a8d580 100644 --- a/SimPEG/utils/mesh_utils.py +++ b/simpeg/utils/mesh_utils.py @@ -11,8 +11,8 @@ def surface2inds(vrtx, trgl, mesh, boundaries=True, internal=True): """Takes a triangulated surface and determine which mesh cells it intersects. - Paramters - --------- + 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 @@ -101,11 +101,11 @@ def surface2inds(vrtx, trgl, mesh, boundaries=True, internal=True): # DEPRECATED FUNCTIONS ################################################ meshTensor = deprecate_function( - unpack_widths, "meshTensor", removal_version="0.19.0", future_warn=True + unpack_widths, "meshTensor", removal_version="0.19.0", error=True ) closestPoints = deprecate_function( - closest_points_index, "closestPoints", removal_version="0.19.0", future_warn=True + closest_points_index, "closestPoints", removal_version="0.19.0", error=True ) ExtractCoreMesh = deprecate_function( - extract_core_mesh, "ExtractCoreMesh", removal_version="0.19.0", future_warn=True + extract_core_mesh, "ExtractCoreMesh", removal_version="0.19.0", error=True ) diff --git a/SimPEG/utils/model_builder.py b/simpeg/utils/model_builder.py similarity index 88% rename from SimPEG/utils/model_builder.py rename to simpeg/utils/model_builder.py index dfcd0fd930..c3a68abcec 100644 --- a/SimPEG/utils/model_builder.py +++ b/simpeg/utils/model_builder.py @@ -1,11 +1,13 @@ +from __future__ import annotations # needed to use type operands in Python 3.8 import numpy as np import scipy.ndimage as ndi import scipy.sparse as sp from .mat_utils import mkvc from scipy.spatial import Delaunay -from .code_utils import deprecate_function from discretize.base import BaseMesh +from ..typing import RandomSeed + def add_block(cell_centers, model, p0, p1, prop_value): """Add a homogeneous block to an existing cell centered model @@ -144,7 +146,7 @@ def create_block_in_wholespace( pass sigma = np.zeros(cell_centers.shape[0]) + background_value - ind = getIndicesBlock(p0, p1, cell_centers) + ind = get_indices_block(p0, p1, cell_centers) sigma[ind] = block_value @@ -317,7 +319,7 @@ def create_2_layer_model(cell_centers, depth, top_value=1.0, bottom_value=0.0): # The depth is always defined on the last one. p1[len(p1) - 1] -= depth - ind = getIndicesBlock(p0, p1, cell_centers) + ind = get_indices_block(p0, p1, cell_centers) sigma[ind] = top_value @@ -415,15 +417,24 @@ 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, + seed: RandomSeed | None = 1000, + anisotropy=None, + its=100, + bounds=None, +): + """ + 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. + 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 @@ -441,7 +452,7 @@ def create_random_model(shape, seed=1000, anisotropy=None, its=100, bounds=None) -------- >>> import matplotlib.pyplot as plt - >>> from SimPEG.utils.model_builder import create_random_model + >>> from simpeg.utils.model_builder import create_random_model >>> m = create_random_model((50,50), bounds=[-4,0]) >>> plt.colorbar(plt.imshow(m)) >>> plt.title('A very cool, yet completely random model.') @@ -451,14 +462,11 @@ def create_random_model(shape, seed=1000, anisotropy=None, its=100, bounds=None) 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=seed) + mr = rng.random(size=shape) if anisotropy is None: if len(shape) == 1: smth = np.array([1, 10.0, 1], dtype=float) @@ -510,47 +518,3 @@ def get_indices_polygon(mesh, pts): hull = Delaunay(pts) inds = hull.find_simplex(mesh.cell_centers) >= 0 return inds - - -################################################ -# DEPRECATED FUNCTIONS -################################################ - - -addBlock = deprecate_function(add_block, "addBlock", removal_version="0.19.0") - -getIndicesBlock = deprecate_function( - get_indices_block, "getIndicesBlock", removal_version="0.19.0" -) - -defineBlock = deprecate_function( - create_block_in_wholespace, "defineBlock", removal_version="0.19.0" -) - -defineEllipse = deprecate_function( - create_ellipse_in_wholespace, "defineEllipse", removal_version="0.19.0" -) - -getIndicesSphere = deprecate_function( - get_indices_sphere, "getIndicesSphere", removal_version="0.19.0" -) - -defineTwoLayers = deprecate_function( - create_2_layer_model, "defineTwoLayers", removal_version="0.19.0" -) - -layeredModel = deprecate_function( - create_layers_model, "layeredModel", removal_version="0.19.0" -) - -randomModel = deprecate_function( - create_random_model, "randomModel", removal_version="0.19.0" -) - -polygonInd = deprecate_function( - get_indices_polygon, "polygonInd", removal_version="0.19.0" -) - -scalarConductivity = deprecate_function( - create_from_function, "scalarConductivity", removal_version="0.19.0" -) diff --git a/SimPEG/utils/model_utils.py b/simpeg/utils/model_utils.py similarity index 72% rename from SimPEG/utils/model_utils.py rename to simpeg/utils/model_utils.py index 2933c9180d..91df15da71 100644 --- a/SimPEG/utils/model_utils.py +++ b/simpeg/utils/model_utils.py @@ -3,46 +3,6 @@ from scipy.interpolate import griddata from scipy.spatial import cKDTree import scipy.sparse as sp -from discretize.utils import active_from_xyz -import warnings - - -def surface2ind_topo(mesh, topo, gridLoc="CC", method="nearest", fill_value=np.nan): - """Get indices of active cells from topography. - - For a mesh and surface topography, this function returns the indices of cells - lying below the discretized surface topography. - - Parameters - ---------- - mesh : discretize.TensorMesh or discretize.TreeMesh - Mesh on which you want to identify active cells - topo : (n, 3) numpy.ndarray - Topography data as a ``numpyndarray`` with columns [x,y,z]; can use [x,z] for 2D meshes. - Topography data can be unstructured. - gridLoc : str {'CC', 'N'} - If 'CC', all cells whose centers are below the topography are active cells. - If 'N', then cells must lie entirely below the topography in order to be active cells. - method : str {'nearest','linear'} - Interpolation method for approximating topography at cell's horizontal position. - Default is 'nearest'. - fill_value : float - Defines the elevation for cells outside the horizontal extent of the topography data. - Default is :py:class:`numpy.nan`. - - Returns - ------- - (n_active) numpy.ndarray of int - Indices of active cells below xyz. - """ - warnings.warn( - "The surface2ind_topo function has been deprecated, please import " - "discretize.utils.active_from_xyz. This will be removed in SimPEG 0.20.0", - FutureWarning, - ) - - active_cells = active_from_xyz(mesh, topo, gridLoc, method) - return np.arange(mesh.n_cells)[active_cells] def surface_layer_index(mesh, topo, index=0): @@ -159,13 +119,10 @@ def depth_weighting( value. """ - if "indActive" in kwargs: - warnings.warn( - "The indActive keyword argument has been deprecated, please use active_cells. " - "This will be removed in SimPEG 0.19.0", - FutureWarning, + if (key := "indActive") in kwargs: + raise TypeError( + f"'{key}' argument has been removed. " "Please use 'active_cells' instead." ) - active_cells = kwargs["indActive"] # Default threshold value if threshold is None: diff --git a/SimPEG/utils/pgi_utils.py b/simpeg/utils/pgi_utils.py similarity index 98% rename from SimPEG/utils/pgi_utils.py rename to simpeg/utils/pgi_utils.py index d0019eeead..638b94cfb3 100644 --- a/SimPEG/utils/pgi_utils.py +++ b/simpeg/utils/pgi_utils.py @@ -19,7 +19,7 @@ ) from sklearn.mixture._base import check_random_state, ConvergenceWarning import warnings -from SimPEG.maps import IdentityMap +from simpeg.maps import IdentityMap ############################################################################### @@ -158,13 +158,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,6 +194,7 @@ 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) @@ -271,6 +272,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 +305,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 +330,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 +338,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 +355,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 +366,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 +392,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 +435,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 +448,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 +495,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) @@ -1157,7 +1170,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 @@ -1171,6 +1184,7 @@ def fit_predict(self, X, y=None, debug=False): "or increase max_iter, tol " "or check for degenerate data." % (init + 1), ConvergenceWarning, + stacklevel=2, ) self._set_parameters(best_params) @@ -1179,6 +1193,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. @@ -1253,6 +1283,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) @@ -1294,6 +1325,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) @@ -1305,6 +1337,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) @@ -1559,7 +1592,7 @@ def __init__( warm_start=warm_start, weights_init=weights_init, update_covariances=update_covariances, - fixed_membership=fixed_membership + fixed_membership=fixed_membership, # **kwargs ) diff --git a/SimPEG/utils/plot_utils.py b/simpeg/utils/plot_utils.py similarity index 99% rename from SimPEG/utils/plot_utils.py rename to simpeg/utils/plot_utils.py index f847e3dfe7..fc50246fd8 100644 --- a/SimPEG/utils/plot_utils.py +++ b/simpeg/utils/plot_utils.py @@ -368,6 +368,7 @@ def plotLayer( warnings.warn( "plotLayer has been deprecated, please use plot_1d_layer_model", DeprecationWarning, + stacklevel=2, ) thicknesses = np.diff(LocSigZ) z0 = LocSigZ[0] diff --git a/SimPEG/utils/solver_utils.py b/simpeg/utils/solver_utils.py similarity index 95% rename from SimPEG/utils/solver_utils.py rename to simpeg/utils/solver_utils.py index 820c07fc64..e5b4f343c4 100644 --- a/SimPEG/utils/solver_utils.py +++ b/simpeg/utils/solver_utils.py @@ -15,7 +15,7 @@ def _checkAccuracy(A, b, X, accuracyTol): nrm, accuracyTol ) print(msg) - warnings.warn(msg, RuntimeWarning) + warnings.warn(msg, RuntimeWarning, stacklevel=2) def SolverWrapD(fun, factorize=True, checkAccuracy=True, accuracyTol=1e-6, name=None): @@ -44,7 +44,7 @@ def SolverWrapD(fun, factorize=True, checkAccuracy=True, accuracyTol=1e-6, name= -------- A solver that does not have a factorize method. - >>> from SimPEG.utils.solver_utils import SolverWrapD + >>> 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)) @@ -87,7 +87,8 @@ def __init__(self, A, **kwargs): culled_args[item] = kwargs[item] else: warnings.warn( - f"{item} is not a valid keyword for {fun.__name__} and will be ignored" + f"{item} is not a valid keyword for {fun.__name__} and will be ignored", + stacklevel=2, ) kwargs = culled_args @@ -169,7 +170,7 @@ def SolverWrapI(fun, checkAccuracy=True, accuracyTol=1e-5, name=None): -------- >>> import scipy.sparse as sp - >>> from SimPEG.utils.solver_utils import SolverWrapI + >>> from simpeg.utils.solver_utils import SolverWrapI >>> SolverCG = SolverWrapI(sp.linalg.cg) >>> A = sp.diags([-1, 2, -1], [-1, 0, 1], shape=(10, 10)) @@ -204,7 +205,8 @@ def __init__(self, A, **kwargs): culled_args[item] = kwargs[item] else: warnings.warn( - f"{item} is not a valid keyword for {fun.__name__} and will be ignored" + f"{item} is not a valid keyword for {fun.__name__} and will be ignored", + stacklevel=2, ) kwargs = culled_args @@ -276,7 +278,7 @@ class SolverDiag(object): Examples -------- >>> import scipy.sparse as sp - >>> from SimPEG.utils.solver_utils import SolverDiag + >>> from simpeg.utils.solver_utils import SolverDiag >>> A = sp.diags(np.linspace(1, 2, 10)) >>> b = np.arange(10) >>> Ainv = SolverDiag(A) @@ -289,7 +291,9 @@ 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") + warnings.warn( + f"{kwarg} is not recognized and will be ignored", stacklevel=2 + ) def __mul__(self, rhs): n = self.A.shape[0] diff --git a/simpeg/version.py b/simpeg/version.py new file mode 100644 index 0000000000..7beb522965 --- /dev/null +++ b/simpeg/version.py @@ -0,0 +1,16 @@ +# file generated by setuptools_scm +# don't change, don't track in version control +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple, Union + VERSION_TUPLE = Tuple[Union[int, str], ...] +else: + VERSION_TUPLE = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE + +__version__ = version = '0.21.2.dev55+g68a92075b.d20240520' +__version_tuple__ = version_tuple = (0, 21, 2, 'dev55', 'g68a92075b.d20240520') diff --git a/tests/base/test_cross_gradient.py b/tests/base/regularizations/test_cross_gradient.py similarity index 84% rename from tests/base/test_cross_gradient.py rename to tests/base/regularizations/test_cross_gradient.py index 66e84082ab..65f21ea4b7 100644 --- a/tests/base/test_cross_gradient.py +++ b/tests/base/regularizations/test_cross_gradient.py @@ -3,13 +3,11 @@ import numpy as np from discretize import TensorMesh, TreeMesh -from SimPEG import ( +from simpeg import ( maps, regularization, ) -np.random.seed(10) - class CrossGradientTensor2D(unittest.TestCase): def setUp(self): @@ -30,7 +28,7 @@ def setUp(self): cros_grad = regularization.CrossGradient( mesh, wire_map=wires, - indActive=actv, + active_cells=actv, ) self.mesh = mesh @@ -44,7 +42,7 @@ def test_order_approximate_hessian(self): """ 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): """ @@ -54,10 +52,11 @@ def test_order_full_hessian(self): """ 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) m = np.random.randn(2 * len(self.mesh)) cross_grad = self.cross_grad @@ -96,7 +95,7 @@ def test_cross_grad_calc(self): cross_grad = self.cross_grad - v1 = 0.5 * np.sum(np.abs(cross_grad.calculate_cross_gradient(m))) + v1 = np.sum(np.abs(cross_grad.calculate_cross_gradient(m))) v2 = cross_grad(m) self.assertEqual(v1, v2) @@ -122,7 +121,7 @@ def setUp(self): cros_grad = regularization.CrossGradient( mesh, wire_map=wires, - indActive=actv, + active_cells=actv, ) self.mesh = mesh @@ -136,7 +135,7 @@ def test_order_approximate_hessian(self): """ 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,10 +145,11 @@ def test_order_full_hessian(self): """ 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) m = np.random.randn(2 * len(self.mesh)) cross_grad = self.cross_grad @@ -167,6 +167,7 @@ def test_deriv2_no_arg(self): np.testing.assert_allclose(Wv, W @ v) def test_cross_grad_calc(self): + np.random.seed(10) m = np.random.randn(2 * len(self.mesh)) cross_grad = self.cross_grad @@ -196,7 +197,9 @@ def setUp(self): # maps wires = maps.Wires(("m1", mesh.nC), ("m2", mesh.nC)) - cross_grad = regularization.CrossGradient(mesh, wire_map=wires, indActive=actv) + cross_grad = regularization.CrossGradient( + mesh, wire_map=wires, active_cells=actv + ) self.mesh = mesh self.cross_grad = cross_grad @@ -209,7 +212,7 @@ def test_order_approximate_hessian(self): """ 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): """ @@ -219,10 +222,11 @@ def test_order_full_hessian(self): """ 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) m = np.random.randn(2 * len(self.mesh)) cross_grad = self.cross_grad @@ -259,7 +263,9 @@ def setUp(self): # maps wires = maps.Wires(("m1", mesh.nC), ("m2", mesh.nC)) - cross_grad = regularization.CrossGradient(mesh, wire_map=wires, indActive=actv) + cross_grad = regularization.CrossGradient( + mesh, wire_map=wires, active_cells=actv + ) self.mesh = mesh self.cross_grad = cross_grad @@ -272,7 +278,7 @@ def test_order_approximate_hessian(self): """ 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): """ @@ -282,10 +288,11 @@ def test_order_full_hessian(self): """ 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) m = np.random.randn(2 * len(self.mesh)) cross_grad = self.cross_grad diff --git a/tests/base/regularizations/test_full_gradient.py b/tests/base/regularizations/test_full_gradient.py new file mode 100644 index 0000000000..ae3b51e27f --- /dev/null +++ b/tests/base/regularizations/test_full_gradient.py @@ -0,0 +1,236 @@ +from discretize.tests import assert_expected_order, check_derivative +from discretize.utils import example_simplex_mesh +import discretize +import numpy as np +from simpeg.regularization import SmoothnessFullGradient +import pytest + + +def f_2d(x, y): + return (1 - np.cos(2 * x * np.pi)) * (1 - np.cos(4 * y * np.pi)) + + +def f_3d(x, y, z): + return f_2d(x, y) * (1 - np.cos(8 * z * np.pi)) + + +dir_2d = np.array([[1.0, 1.0], [-1.0, 1.0]]).T +dir_2d /= np.linalg.norm(dir_2d, axis=0) +dir_3d = np.array([[1, 1, 1], [-1, 1, 0], [-1, -1, 2]]).T +dir_3d = dir_3d / np.linalg.norm(dir_3d, axis=0) + +# a list of argument tuples to pass to pytest parameterize +# each is a tuple of (function, dim, true_value, alphas, reg_dirs) +parameterized_args = [ + (f_2d, 2, 15 * np.pi**2, [1, 1], None), # assumes reg_dirs aligned with axes + ( + f_2d, + 2, + 15 * np.pi**2, + [1, 1], + np.eye(2), + ), # test for explicitly aligned with axes + ( + f_2d, + 2, + 15 * np.pi**2, + [1, 1], + dir_2d, + ), # circular regularization should be invariant to rotation + ( + f_2d, + 2, + 27 * np.pi**2, + [1, 2], + None, + ), # elliptic regularization aligned with axes + (f_2d, 2, 111.033049512255 * 2, [1, 2], dir_2d), # rotated elliptic regularization + ( + f_3d, + 3, + 189 * np.pi**2 / 2, + [1, 1, 1], + None, + ), # test for explicitly aligned with axes + ( + f_3d, + 3, + 189 * np.pi**2 / 2, + [1, 1, 1], + np.eye(3), + ), # test for explicitly aligned with axes + ( + f_3d, + 3, + 189 * np.pi**2 / 2, + [1, 1, 1], + dir_3d, + ), # circular regularization should be invariant to rotation + ( + f_3d, + 3, + 513 * np.pi**2 / 2, + [1, 2, 3], + None, + ), # elliptic regularization aligned with axes + ( + f_3d, + 3, + 1065.91727531765 * 2, + [1, 2, 3], + dir_3d, + ), # rotated elliptic regularization +] + + +@pytest.mark.parametrize("mesh_class", [discretize.TensorMesh, discretize.TreeMesh]) +@pytest.mark.parametrize("func,dim,true_value,alphas,reg_dirs", parameterized_args) +def test_regulariation_order(mesh_class, func, dim, true_value, alphas, reg_dirs): + """This function is testing for the accuracy of the regularization. + Basically, is it actually measuring what we say it's measuring. + """ + n_hs = [8, 16, 32] + + def reg_error(n): + h = [n] * dim + mesh = mesh_class(h) + if mesh_class is discretize.TreeMesh: + mesh.refine(-1) + # cell widths will be the same in each dimension + dh = mesh.h[0][0] + + f_eval = func(*mesh.cell_centers.T) + + reg = SmoothnessFullGradient(mesh, alphas=alphas, reg_dirs=reg_dirs) + + numerical_eval = reg(f_eval) + err = np.abs(numerical_eval - true_value) + return err, dh + + assert_expected_order(reg_error, n_hs) + + +@pytest.mark.parametrize("dim", [2, 3]) +def test_simplex_mesh(dim): + """Test to make sure it works with a simplex mesh + + We can't make as strong of an accuracy claim for this mesh type because the cell gradient + operator is not actually defined for it (it uses an approximation to the cell gradient). + It is close, but we should at least test that it works.. + """ + h = [10] * dim + points, simplices = example_simplex_mesh(h) + mesh = discretize.SimplexMesh(points, simplices) + reg = SmoothnessFullGradient(mesh) + + # multiply it by a vector to make sure we can construct everything internally + # at the very least, we should be able to confirm it evaluates to 0 for a flat model. + out = reg(np.ones(mesh.n_cells)) + np.testing.assert_allclose(out, 0) + + +@pytest.mark.parametrize( + "dim,alphas,reg_dirs", [(2, [1, 2], dir_2d), (3, [1, 2, 3], dir_3d)] +) +def test_first_derivatives(dim, alphas, reg_dirs): + """Perform a derivative test.""" + h = [10] * dim + mesh = discretize.TensorMesh(h) + reg = SmoothnessFullGradient(mesh, alphas=alphas, reg_dirs=reg_dirs) + + def func(x): + return reg(x), reg.deriv(x) + + check_derivative(func, np.ones(mesh.n_cells), plotIt=False) + + +@pytest.mark.parametrize( + "dim,alphas,reg_dirs", [(2, [1, 2], dir_2d), (3, [1, 2, 3], dir_3d)] +) +def test_second_derivatives(dim, alphas, reg_dirs): + """Perform a derivative test.""" + h = [10] * dim + mesh = discretize.TensorMesh(h) + reg = SmoothnessFullGradient(mesh, alphas=alphas, reg_dirs=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) + + +@pytest.mark.parametrize("with_active_cells", [True, False]) +def test_operations(with_active_cells, dim=3): + # Here we just make sure operations at least work + h = [10] * dim + mesh = discretize.TensorMesh(h) + if with_active_cells: + active_cells = mesh.cell_centers[:, -1] <= 0.75 + n_cells = active_cells.sum() + else: + active_cells = None + n_cells = mesh.n_cells + reg = SmoothnessFullGradient(mesh, active_cells=active_cells) + # create a model + m = np.arange(n_cells) + # create a vector + v = np.random.rand(n_cells) + # test the second derivative evaluates + # and gives same results with and without a vector + v1 = reg.deriv2(m, v) + v2 = reg.deriv2(m) @ v + np.testing.assert_allclose(v1, v2) + + W1 = reg.W + + # test assigning n_cells + reg.set_weights(temp_weight=np.random.rand(n_cells)) + + # setting a weight should've erased W + assert reg._W is None + + # test assigning n_total_faces face weight + reg.set_weights(temp_weight=np.random.rand(mesh.n_faces)) + + # and test it all works! + W2 = reg.W + assert W1 is not W2 + + +def test_errors(): + # bad dimension mesh + mesh1d = discretize.TensorMesh([5]) + with pytest.raises(TypeError): + SmoothnessFullGradient(mesh1d) + mesh2d = discretize.TensorMesh([5, 5]) + # test some bad alphas + with pytest.raises(ValueError): + # 3D alpha passed to 2D operator + SmoothnessFullGradient(mesh2d, [1, 2, 3]) + + with pytest.raises(IndexError): + # incorrect number cell dependent alphas + alphas = np.random.rand(mesh2d.n_cells - 5, 2) + SmoothnessFullGradient(mesh2d, alphas=alphas) + + with pytest.raises(ValueError): + # negative alphas + SmoothnessFullGradient(mesh2d, [-1, 1, 1]) + + alphas = [1, 2] + # test some bad reg dirs + with pytest.raises(ValueError): + # 3D reg dirs to 2D reg + reg_dirs = np.random.rand(3, 3) + SmoothnessFullGradient(mesh2d, alphas=alphas, reg_dirs=reg_dirs) + + with pytest.raises(IndexError): + # incorrect number of cell dependent reg_dirs + reg_dirs = np.random.rand(mesh2d.n_cells - 5, 2, 2) + SmoothnessFullGradient(mesh2d, alphas=alphas, reg_dirs=reg_dirs) + + with pytest.raises(ValueError): + # non orthnormal reg_dirs + # incorrect number of cell dependent reg_dirs + reg_dirs = np.random.rand(2, 2) + SmoothnessFullGradient(mesh2d, alphas=alphas, reg_dirs=reg_dirs) diff --git a/tests/base/test_jtv.py b/tests/base/regularizations/test_jtv.py similarity index 87% rename from tests/base/test_jtv.py rename to tests/base/regularizations/test_jtv.py index 9d93d26c66..e30d570c5e 100644 --- a/tests/base/test_jtv.py +++ b/tests/base/regularizations/test_jtv.py @@ -4,7 +4,7 @@ import numpy as np from discretize import TensorMesh, TreeMesh -from SimPEG import ( +from simpeg import ( maps, regularization, ) @@ -31,7 +31,7 @@ def setUp(self): jtv = regularization.JointTotalVariation( mesh, wire_map=wires, - indActive=actv, + active_cells=actv, ) self.mesh = mesh @@ -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 @@ -81,7 +81,7 @@ def setUp(self): jtv = regularization.JointTotalVariation( mesh, wire_map=wires, - indActive=actv, + active_cells=actv, ) self.mesh = mesh @@ -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 @@ -127,7 +127,9 @@ def setUp(self): # maps wires = maps.Wires(("m1", mesh.nC), ("m2", mesh.nC)) - jtv = regularization.JointTotalVariation(mesh, wire_map=wires, indActive=actv) + jtv = regularization.JointTotalVariation( + mesh, wire_map=wires, active_cells=actv + ) self.mesh = mesh self.jtv = jtv @@ -141,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 @@ -174,7 +176,9 @@ def setUp(self): # maps wires = maps.Wires(("m1", mesh.nC), ("m2", mesh.nC)) - jtv = regularization.JointTotalVariation(mesh, wire_map=wires, indActive=actv) + jtv = regularization.JointTotalVariation( + mesh, wire_map=wires, active_cells=actv + ) self.mesh = mesh self.jtv = jtv @@ -188,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 @@ -221,7 +225,7 @@ def test_bad_wires(): regularization.JointTotalVariation( mesh, wire_map=wires, - indActive=actv, + active_cells=actv, ) diff --git a/tests/base/test_pgi_regularization.py b/tests/base/regularizations/test_pgi_regularization.py similarity index 94% rename from tests/base/test_pgi_regularization.py rename to tests/base/regularizations/test_pgi_regularization.py index b8db90f00e..b1f08e905f 100644 --- a/tests/base/test_pgi_regularization.py +++ b/tests/base/regularizations/test_pgi_regularization.py @@ -1,12 +1,14 @@ +import pytest import unittest 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 import regularization +from simpeg.maps import Wires +from simpeg.utils import WeightedGaussianMixture, mkvc class TestPGI(unittest.TestCase): @@ -85,9 +87,7 @@ def test_full_covariances(self): dm = self.model - mref score_approx0 = reg(self.model) score_approx1 = 0.5 * dm.dot(reg.deriv2(self.model, dm)) - passed_score_approx = np.allclose(score_approx0, score_approx1) - self.assertTrue(passed_score_approx) - + np.testing.assert_allclose(score_approx0, score_approx1) reg.objfcts[0].approx_eval = False score = reg(self.model) - reg(mref) passed_score = np.allclose(score_approx0, score, rtol=1e-4) @@ -193,8 +193,7 @@ def test_tied_covariances(self): dm = self.model - mref score_approx0 = reg(self.model) score_approx1 = 0.5 * dm.dot(reg.deriv2(self.model, dm)) - passed_score_approx = np.allclose(score_approx0, score_approx1) - self.assertTrue(passed_score_approx) + np.testing.assert_allclose(score_approx0, score_approx1) reg.objfcts[0].approx_eval = False score = reg(self.model) - reg(mref) passed_score = np.allclose(score_approx0, score, rtol=1e-4) @@ -297,8 +296,7 @@ def test_diag_covariances(self): dm = self.model - mref score_approx0 = reg(self.model) score_approx1 = 0.5 * dm.dot(reg.deriv2(self.model, dm)) - passed_score_approx = np.allclose(score_approx0, score_approx1) - self.assertTrue(passed_score_approx) + np.testing.assert_allclose(score_approx0, score_approx1) reg.objfcts[0].approx_eval = False score = reg(self.model) - reg(mref) passed_score = np.allclose(score_approx0, score, rtol=1e-4) @@ -401,8 +399,7 @@ def test_spherical_covariances(self): dm = self.model - mref score_approx0 = reg(self.model) score_approx1 = 0.5 * dm.dot(reg.deriv2(self.model, dm)) - passed_score_approx = np.allclose(score_approx0, score_approx1) - self.assertTrue(passed_score_approx) + np.testing.assert_allclose(score_approx0, score_approx1) reg.objfcts[0].approx_eval = False score = reg(self.model) - reg(mref) passed_score = np.allclose(score_approx0, score, rtol=1e-4) @@ -473,5 +470,19 @@ 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 + + if __name__ == "__main__": unittest.main() diff --git a/tests/base/test_regularization.py b/tests/base/regularizations/test_regularization.py similarity index 60% rename from tests/base/test_regularization.py rename to tests/base/regularizations/test_regularization.py index ce8b511114..257dca9dba 100644 --- a/tests/base/test_regularization.py +++ b/tests/base/regularizations/test_regularization.py @@ -5,7 +5,17 @@ import inspect import discretize -from SimPEG import maps, objective_function, regularization, utils +from simpeg import maps, objective_function, regularization, utils +from simpeg.regularization import ( + BaseRegularization, + WeightedLeastSquares, + Sparse, + SparseSmoothness, + Smallness, + SmoothnessFirstOrder, + SmoothnessSecondOrder, +) +from simpeg.objective_function import ComboObjectiveFunction TOL = 1e-7 @@ -20,6 +30,7 @@ "BaseSimilarityMeasure", "SimpleComboRegularization", "BaseSparse", + "BaseVectorRegularization", "PGI", "PGIwithRelationships", "PGIwithNonlinearRelationshipsSmallness", @@ -27,6 +38,20 @@ "CrossGradient", "LinearCorrespondence", "JointTotalVariation", + "BaseAmplitude", + "SmoothnessFullGradient", + "VectorAmplitude", + "CrossReferenceRegularization", + # Removed regularization classes that raise error on instantiation + "PGIwithNonlinearRelationshipsSmallness", + "PGIwithRelationships", + "Simple", + "SimpleSmall", + "SimpleSmoothDeriv", + "Small", + "SmoothDeriv", + "SmoothDeriv2", + "Tikhonov", ] @@ -69,7 +94,7 @@ def test_regularization(self): else: m = np.random.rand(mesh.nC) mref = np.ones_like(m) * np.mean(m) - reg.mref = mref + reg.reference_model = mref # test derivs passed = reg.test(m, eps=TOL) @@ -157,7 +182,7 @@ def test_property_mirroring(self): active_cells = mesh.gridCC[:, 2] < 0.6 reg = getattr(regularization, regType)(mesh, active_cells=active_cells) - self.assertTrue(reg.nP == reg.regularization_mesh.nC) + self.assertTrue(reg.nP == reg.regularization_mesh.n_cells) [ self.assertTrue(np.all(fct.active_cells == active_cells)) @@ -277,7 +302,8 @@ def test_mappings_and_cell_weights(self): wires = maps.Wires(("sigma", mesh.nC), ("mu", mesh.nC)) - reg = regularization.Smallness(mesh, mapping=wires.sigma, weights=cell_weights) + reg = regularization.Smallness(mesh, mapping=wires.sigma) + reg.set_weights(cell_weights=cell_weights) objfct = objective_function.L2ObjectiveFunction( W=utils.sdiag(np.sqrt(cell_weights * mesh.cell_volumes)), @@ -312,8 +338,7 @@ def test_update_of_sparse_norms(self): v = np.random.rand(mesh.nC) cell_weights = np.random.rand(mesh.nC) - - reg = regularization.Sparse(mesh, weights=cell_weights) + reg = regularization.Sparse(mesh, weights={"cell_weights": cell_weights}) np.testing.assert_equal(reg.norms, [1, 1, 1, 1]) @@ -374,14 +399,14 @@ def test_linked_properties(self): ] [self.assertTrue(reg.mapping is fct.mapping) for fct in reg.objfcts] - D = reg.regularization_mesh.cellDiffx + D = reg.regularization_mesh.cell_gradient_x reg.regularization_mesh._cell_gradient_x = 4 * D v = np.random.rand(D.shape[1]) [ self.assertTrue( np.all( reg.regularization_mesh._cell_gradient_x * v - == fct.regularization_mesh.cellDiffx * v + == fct.regularization_mesh.cell_gradient_x * v ) ) for fct in reg.objfcts @@ -445,7 +470,7 @@ def test_nC_residual(self): mapping = maps.ExpMap(mesh) * maps.SurjectVertical1D(mesh) * actMap regMesh = discretize.TensorMesh([mesh.h[2][mapping.maps[-1].indActive]]) - reg = regularization.Simple(regMesh) + reg = regularization.WeightedLeastSquares(regMesh) self.assertTrue(reg._nC_residual == regMesh.nC) self.assertTrue(all([fct._nC_residual == regMesh.nC for fct in reg.objfcts])) @@ -549,6 +574,27 @@ def test_sparse_properties(self): assert reg.gradient_type == "total" # Check default + def test_vector_amplitude(self): + n_comp = 4 + mesh = discretize.TensorMesh([8, 7]) + model = np.random.randn(mesh.nC, n_comp) + + with pytest.raises(TypeError, match="'regularization_mesh' must be of type"): + regularization.VectorAmplitude("abc") + + reg = regularization.VectorAmplitude( + mesh, maps.IdentityMap(nP=n_comp * mesh.nC) + ) + + with pytest.raises(ValueError, match="'weights' must be one of"): + reg.set_weights(abc=(1.0, 1.0)) + + np.testing.assert_almost_equal( + reg.objfcts[0].f_m(model.flatten(order="F")), np.linalg.norm(model, axis=1) + ) + + reg.test(model.flatten(order="F")) + def test_WeightedLeastSquares(): mesh = discretize.TensorMesh([3, 4, 5]) @@ -565,5 +611,305 @@ def test_WeightedLeastSquares(): np.testing.assert_allclose(reg.length_scale_z, 0.8) +@pytest.mark.parametrize("dim", [2, 3]) +def test_cross_ref_reg(dim): + mesh = discretize.TensorMesh([3, 4, 5][:dim]) + actives = mesh.cell_centers[:, -1] < 0.6 + n_active = actives.sum() + + ref_dir = dim * [1] + + cross_reg = regularization.CrossReferenceRegularization( + mesh, ref_dir, active_cells=actives + ) + + assert cross_reg.ref_dir.shape == (n_active, 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) + cross_reg.set_weights(cell_weights=cell_weights) + cross_reg.set_weights(vec_weights=cell_vec_weights) + + if dim == 3: + assert cross_reg.W.shape == (3 * n_active, 3 * n_active) + else: + assert cross_reg.W.shape == (n_active, n_active) + + m = np.random.rand(dim * n_active) + cross_reg.test(m) + + +def test_cross_reg_reg_errors(): + mesh = discretize.TensorMesh([3, 4, 5]) + + # bad ref_dir shape + ref_dir = np.random.rand(mesh.n_cells - 1, mesh.dim) + + with pytest.raises(ValueError, match="ref_dir"): + regularization.CrossReferenceRegularization(mesh, ref_dir) + + +@pytest.mark.parametrize("orientation", ("x", "y", "z")) +def test_smoothness_first_order_coterminal_angle(orientation): + """ + Test smoothness first order regularizations of angles on a treemesh + """ + mesh = discretize.TreeMesh([16, 16, 16]) + mesh.insert_cells([100, 100, 100], mesh.max_level, finalize=True) + + reg = regularization.SmoothnessFirstOrder( + mesh, units="radian", orientation=orientation + ) + angles = np.ones(mesh.n_cells) * np.pi + angles[5] = -np.pi + assert np.all(reg.f_m(angles) == 0) + + +class TestParent: + """Test parent property of regularizations.""" + + @pytest.fixture + def regularization(self): + """Sample regularization instance.""" + mesh = discretize.TensorMesh([3, 4, 5]) + return BaseRegularization(mesh) + + def test_parent(self, regularization): + """Test setting a parent class to a BaseRegularization.""" + combo = ComboObjectiveFunction() + regularization.parent = combo + assert regularization.parent == combo + + def test_invalid_parent(self, regularization): + """Test setting an invalid parent class to a BaseRegularization.""" + + class Dummy: + pass + + invalid_parent = Dummy() + msg = "Invalid parent of type 'Dummy'." + with pytest.raises(TypeError, match=msg): + regularization.parent = invalid_parent + + def test_default_parent(self, regularization): + """Test setting default parent class to a BaseRegularization.""" + mesh = discretize.TensorMesh([3, 4, 5]) + parent = WeightedLeastSquares(mesh, objfcts=[regularization]) + assert regularization.parent is parent + + +class TestWeightsKeys: + """ + Test weights_keys property of regularizations + """ + + @pytest.fixture + def mesh(self): + """Sample mesh.""" + return discretize.TensorMesh([8, 7, 6]) + + def test_empty_weights(self, mesh): + """ + Test weights_keys when no weight is defined + """ + reg = BaseRegularization(mesh) + assert reg.weights_keys == [] + + def test_user_defined_weights_as_dict(self, mesh): + """ + Test weights_keys after user defined weights as dictionary + """ + weights = dict(dummy_weight=np.ones(mesh.n_cells)) + reg = BaseRegularization(mesh, weights=weights) + assert reg.weights_keys == ["dummy_weight"] + + @pytest.mark.parametrize( + "regularization_class", (Smallness, SmoothnessFirstOrder, SmoothnessSecondOrder) + ) + def test_volume_weights(self, mesh, regularization_class): + """ + Test weights_keys has "volume" by default on some regularizations + """ + reg = regularization_class(mesh) + assert reg.weights_keys == ["volume"] + + @pytest.mark.parametrize( + "regularization_class", + (BaseRegularization, Smallness, SmoothnessFirstOrder, SmoothnessSecondOrder), + ) + def test_multiple_weights(self, mesh, regularization_class): + """ + Test weights_keys has "volume" by default on some regularizations + """ + weights = dict( + dummy_weight=np.ones(mesh.n_cells), other_weights=np.ones(mesh.n_cells) + ) + reg = regularization_class(mesh, weights=weights) + if regularization_class == BaseRegularization: + assert reg.weights_keys == ["dummy_weight", "other_weights"] + else: + assert reg.weights_keys == ["dummy_weight", "other_weights", "volume"] + + +class TestRemovedObjects: + """ + Test if errors are raised after passing removed arguments or trying to + access removed properties. + + * ``indActive`` (replaced by ``active_cells``) + * ``gradientType`` (replaced by ``gradient_type``) + * ``mref`` (replaced by ``reference_model``) + * ``regmesh`` (replaced by ``regularization_mesh``) + * ``cell_weights`` (replaced by ``weights``) + + """ + + @pytest.fixture(params=["1D", "2D", "3D"]) + def mesh(self, request): + """Sample mesh.""" + if request.param == "1D": + hx = np.random.rand(10) + h = [hx / hx.sum()] + elif request.param == "2D": + hx, hy = np.random.rand(10), np.random.rand(9) + h = [h_i / h_i.sum() for h_i in (hx, hy)] + elif request.param == "3D": + hx, hy, hz = np.random.rand(10), np.random.rand(9), np.random.rand(8) + h = [h_i / h_i.sum() for h_i in (hx, hy, hz)] + return discretize.TensorMesh(h) + + @pytest.mark.parametrize( + "regularization_class", (BaseRegularization, WeightedLeastSquares) + ) + 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 + + 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 + + @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." + ) + with pytest.raises(TypeError, match=msg): + regularization_class(mesh, gradientType="total") + + @pytest.mark.parametrize( + "regularization_class", + (BaseRegularization, WeightedLeastSquares), + ) + 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." + ) + with pytest.raises(TypeError, match=msg): + regularization_class(mesh, indActive=active_cells) + + @pytest.mark.parametrize( + "regularization_class", + (BaseRegularization, WeightedLeastSquares), + ) + 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 + + @pytest.mark.parametrize( + "regularization_class", + (BaseRegularization, WeightedLeastSquares), + ) + 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." + with pytest.raises(TypeError, match=msg): + regularization_class(mesh, cell_weights=weights) + + @pytest.mark.parametrize( + "regularization_class", (BaseRegularization, WeightedLeastSquares) + ) + 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() + + +@pytest.mark.parametrize( + "regularization_class", (BaseRegularization, WeightedLeastSquares) +) +def test_invalid_weights_type(regularization_class): + """Test error after passing weights as invalid type.""" + mesh = discretize.TensorMesh([[(2, 2)]]) + msg = "Invalid 'weights' of type ''" + with pytest.raises(TypeError, match=msg): + regularization_class(mesh, weights=np.array([1.0])) + + if __name__ == "__main__": unittest.main() diff --git a/tests/base/test_Fields.py b/tests/base/test_Fields.py index 3c90616e1f..b9450bfc5f 100644 --- a/tests/base/test_Fields.py +++ b/tests/base/test_Fields.py @@ -1,7 +1,7 @@ import unittest import discretize -from SimPEG import survey, simulation, utils, fields, data +from simpeg import survey, simulation, utils, fields, data import numpy as np import sys diff --git a/tests/base/test_Props.py b/tests/base/test_Props.py index ff78a50dc1..bb7de0602d 100644 --- a/tests/base/test_Props.py +++ b/tests/base/test_Props.py @@ -1,12 +1,13 @@ import unittest import numpy as np import inspect +import pytest import discretize -from SimPEG import maps -from SimPEG import utils -from SimPEG import props +from simpeg import maps +from simpeg import utils +from simpeg import props class SimpleExample(props.HasModel): @@ -124,137 +125,157 @@ def __init__(self, nest_model=None, **kwargs): self.nest_model = nest_model -class TestPropMaps(unittest.TestCase): - def setUp(self): - pass - - def test_basic(self): - expMap = maps.ExpMap(discretize.TensorMesh((3,))) - assert expMap.nP == 3 - - for Example in [SimpleExample, ShortcutExample]: - PM = Example(sigmaMap=expMap) - assert PM.sigmaMap is not None - assert PM.sigmaMap is expMap - - # There is currently no model, so sigma, which is mapped, fails - self.assertRaises(AttributeError, getattr, PM, "sigma") - - PM.model = np.r_[1.0, 2.0, 3.0] - assert np.all(PM.sigma == np.exp(np.r_[1.0, 2.0, 3.0])) - # PM = pickle.loads(pickle.dumps(PM)) - # PM = maps.ExpMap.deserialize(PM.serialize()) - - assert np.all( - PM.sigmaDeriv.todense() - == utils.sdiag(np.exp(np.r_[1.0, 2.0, 3.0])).todense() - ) - - # If we set sigma, we should delete the mapping - PM.sigma = np.r_[1.0, 2.0, 3.0] - assert np.all(PM.sigma == np.r_[1.0, 2.0, 3.0]) - # PM = pickle.loads(pickle.dumps(PM)) - assert PM.sigmaMap is None - assert PM.sigmaDeriv == 0 - - del PM.model - # sigma is not changed - assert np.all(PM.sigma == np.r_[1.0, 2.0, 3.0]) - - def test_reciprocal(self): - expMap = maps.ExpMap(discretize.TensorMesh((3,))) - - PM = ReciprocalMappingExample() - - self.assertRaises(AttributeError, getattr, PM, "sigma") - PM.sigmaMap = expMap - PM.model = np.r_[1.0, 2.0, 3.0] - assert np.all(PM.sigma == np.exp(np.r_[1.0, 2.0, 3.0])) - assert np.all(PM.rho == 1.0 / np.exp(np.r_[1.0, 2.0, 3.0])) - - PM.rho = np.r_[1.0, 2.0, 3.0] - assert PM.rhoMap is None - assert PM.sigmaMap is None - assert PM.rhoDeriv == 0 - assert PM.sigmaDeriv == 0 - assert np.all(PM.sigma == 1.0 / np.r_[1.0, 2.0, 3.0]) - - PM.sigmaMap = expMap - # change your mind? - # PM = pickle.loads(pickle.dumps(PM)) - PM.rhoMap = expMap - assert PM._sigmaMap is None - assert len(PM.rhoMap) == 1 - assert len(PM.sigmaMap) == 2 - # PM = pickle.loads(pickle.dumps(PM)) - assert np.all(PM.rho == np.exp(np.r_[1.0, 2.0, 3.0])) - assert np.all(PM.sigma == 1.0 / np.exp(np.r_[1.0, 2.0, 3.0])) - # PM = pickle.loads(pickle.dumps(PM)) - assert isinstance(PM.sigmaDeriv.todense(), np.ndarray) - - def test_reciprocal_no_map(self): - expMap = maps.ExpMap(discretize.TensorMesh((3,))) - - PM = ReciprocalExample() - self.assertRaises(AttributeError, getattr, PM, "sigma") - - PM.sigmaMap = expMap - # PM = pickle.loads(pickle.dumps(PM)) - PM.model = np.r_[1.0, 2.0, 3.0] - assert np.all(PM.sigma == np.exp(np.r_[1.0, 2.0, 3.0])) - assert np.all(PM.rho == 1.0 / np.exp(np.r_[1.0, 2.0, 3.0])) - - PM.rho = np.r_[1.0, 2.0, 3.0] - assert PM.sigmaMap is None - assert PM.sigmaDeriv == 0 - assert np.all(PM.sigma == 1.0 / np.r_[1.0, 2.0, 3.0]) - - PM.sigmaMap = expMap - assert len(PM.sigmaMap) == 1 - # PM = pickle.loads(pickle.dumps(PM)) - assert np.all(PM.rho == 1.0 / np.exp(np.r_[1.0, 2.0, 3.0])) - assert np.all(PM.sigma == np.exp(np.r_[1.0, 2.0, 3.0])) - assert isinstance(PM.sigmaDeriv.todense(), np.ndarray) - - def test_reciprocal_no_maps(self): - PM = ReciprocalPropExample() - self.assertRaises(AttributeError, getattr, PM, "sigma") - - # PM = pickle.loads(pickle.dumps(PM)) - PM.sigma = np.r_[1.0, 2.0, 3.0] - # PM = pickle.loads(pickle.dumps(PM)) - - assert np.all(PM.sigma == np.r_[1.0, 2.0, 3.0]) - # PM = pickle.loads(pickle.dumps(PM)) - assert np.all(PM.rho == 1.0 / np.r_[1.0, 2.0, 3.0]) - - PM.rho = np.r_[1.0, 2.0, 3.0] - assert np.all(PM.sigma == 1.0 / np.r_[1.0, 2.0, 3.0]) - - def test_reciprocal_defaults(self): - PM = ReciprocalPropExampleDefaults() - assert np.all(PM.sigma == np.r_[1.0, 2.0, 3.0]) - assert np.all(PM.rho == 1.0 / np.r_[1.0, 2.0, 3.0]) - - rho = np.r_[2.0, 4.0, 6.0] - PM.rho = rho - assert np.all(PM.rho == rho) - assert np.all(PM.sigma == 1.0 / rho) - - def test_multi_parameter_inversion(self): - """The setup of the defaults should not invalidated the - mappings or other defaults. - """ - PM = ComplicatedInversion() - params = inspect.signature(ComplicatedInversion).parameters - - np.testing.assert_equal(PM.Ks, params["Ks"].default) - np.testing.assert_equal(PM.gamma, params["gamma"].default) - np.testing.assert_equal(PM.A, params["A"].default) - - def test_nested(self): - PM = NestedModels() - assert PM._has_nested_models is True +class OptionalInvertible(props.HasModel): + sigma, sigmaMap, sigmaDeriv = props.Invertible( + "Electrical conductivity (S/m)", optional=True + ) + + +@pytest.mark.parametrize("example", [SimpleExample, ShortcutExample]) +def test_basic(example): + expMap = maps.ExpMap(discretize.TensorMesh((3,))) + assert expMap.nP == 3 + + PM = example(sigmaMap=expMap) + assert PM.sigmaMap is not None + assert PM.sigmaMap is expMap + + # There is currently no model, so sigma, which is mapped, fails + with pytest.raises(AttributeError): + PM.sigma + + PM.model = np.r_[1.0, 2.0, 3.0] + assert np.all(PM.sigma == np.exp(np.r_[1.0, 2.0, 3.0])) + # PM = pickle.loads(pickle.dumps(PM)) + # PM = maps.ExpMap.deserialize(PM.serialize()) + + assert np.all( + PM.sigmaDeriv.todense() == utils.sdiag(np.exp(np.r_[1.0, 2.0, 3.0])).todense() + ) + + # If we set sigma, we should delete the mapping + PM.sigma = np.r_[1.0, 2.0, 3.0] + assert np.all(PM.sigma == np.r_[1.0, 2.0, 3.0]) + # PM = pickle.loads(pickle.dumps(PM)) + assert PM.sigmaMap is None + assert PM.sigmaDeriv == 0 + + del PM.model + # sigma is not changed + assert np.all(PM.sigma == np.r_[1.0, 2.0, 3.0]) + + +def test_reciprocal(): + expMap = maps.ExpMap(discretize.TensorMesh((3,))) + + PM = ReciprocalMappingExample() + + with pytest.raises(AttributeError): + PM.sigma + PM.sigmaMap = expMap + PM.model = np.r_[1.0, 2.0, 3.0] + assert np.all(PM.sigma == np.exp(np.r_[1.0, 2.0, 3.0])) + assert np.all(PM.rho == 1.0 / np.exp(np.r_[1.0, 2.0, 3.0])) + + PM.rho = np.r_[1.0, 2.0, 3.0] + assert PM.rhoMap is None + assert PM.sigmaMap is None + assert PM.rhoDeriv == 0 + assert PM.sigmaDeriv == 0 + assert np.all(PM.sigma == 1.0 / np.r_[1.0, 2.0, 3.0]) + + PM.sigmaMap = expMap + # change your mind? + # PM = pickle.loads(pickle.dumps(PM)) + PM.rhoMap = expMap + assert PM._sigmaMap is None + assert len(PM.rhoMap) == 1 + assert len(PM.sigmaMap) == 2 + # PM = pickle.loads(pickle.dumps(PM)) + assert np.all(PM.rho == np.exp(np.r_[1.0, 2.0, 3.0])) + assert np.all(PM.sigma == 1.0 / np.exp(np.r_[1.0, 2.0, 3.0])) + # PM = pickle.loads(pickle.dumps(PM)) + assert isinstance(PM.sigmaDeriv.todense(), np.ndarray) + + +def test_reciprocal_no_map(): + expMap = maps.ExpMap(discretize.TensorMesh((3,))) + + PM = ReciprocalExample() + with pytest.raises(AttributeError): + PM.sigma + + PM.sigmaMap = expMap + # PM = pickle.loads(pickle.dumps(PM)) + PM.model = np.r_[1.0, 2.0, 3.0] + assert np.all(PM.sigma == np.exp(np.r_[1.0, 2.0, 3.0])) + assert np.all(PM.rho == 1.0 / np.exp(np.r_[1.0, 2.0, 3.0])) + + PM.rho = np.r_[1.0, 2.0, 3.0] + assert PM.sigmaMap is None + assert PM.sigmaDeriv == 0 + assert np.all(PM.sigma == 1.0 / np.r_[1.0, 2.0, 3.0]) + + PM.sigmaMap = expMap + assert len(PM.sigmaMap) == 1 + # PM = pickle.loads(pickle.dumps(PM)) + assert np.all(PM.rho == 1.0 / np.exp(np.r_[1.0, 2.0, 3.0])) + assert np.all(PM.sigma == np.exp(np.r_[1.0, 2.0, 3.0])) + assert isinstance(PM.sigmaDeriv.todense(), np.ndarray) + + +def test_reciprocal_no_maps(): + PM = ReciprocalPropExample() + with pytest.raises(AttributeError): + PM.sigma + + # PM = pickle.loads(pickle.dumps(PM)) + PM.sigma = np.r_[1.0, 2.0, 3.0] + # PM = pickle.loads(pickle.dumps(PM)) + + assert np.all(PM.sigma == np.r_[1.0, 2.0, 3.0]) + # PM = pickle.loads(pickle.dumps(PM)) + assert np.all(PM.rho == 1.0 / np.r_[1.0, 2.0, 3.0]) + + PM.rho = np.r_[1.0, 2.0, 3.0] + assert np.all(PM.sigma == 1.0 / np.r_[1.0, 2.0, 3.0]) + + +def test_reciprocal_defaults(): + PM = ReciprocalPropExampleDefaults() + assert np.all(PM.sigma == np.r_[1.0, 2.0, 3.0]) + assert np.all(PM.rho == 1.0 / np.r_[1.0, 2.0, 3.0]) + + rho = np.r_[2.0, 4.0, 6.0] + PM.rho = rho + assert np.all(PM.rho == rho) + assert np.all(PM.sigma == 1.0 / rho) + + +def test_multi_parameter_inversion(): + """The setup of the defaults should not invalidated the + mappings or other defaults. + """ + PM = ComplicatedInversion() + params = inspect.signature(ComplicatedInversion).parameters + + np.testing.assert_equal(PM.Ks, params["Ks"].default) + np.testing.assert_equal(PM.gamma, params["gamma"].default) + np.testing.assert_equal(PM.A, params["A"].default) + + +def test_nested(): + PM = NestedModels() + assert PM._has_nested_models is True + + +def test_optional_inverted(): + modeler = OptionalInvertible() + assert modeler.sigmaMap is None + assert modeler.sigma is None + + modeler.sigma = 10 + assert modeler.sigma == 10 if __name__ == "__main__": diff --git a/tests/base/test_Solver.py b/tests/base/test_Solver.py index 6c3423df3d..adb753f6c8 100644 --- a/tests/base/test_Solver.py +++ b/tests/base/test_Solver.py @@ -1,8 +1,8 @@ import unittest -from SimPEG import Solver, SolverDiag, SolverCG, SolverLU +from simpeg import Solver, SolverDiag, SolverCG, SolverLU from discretize import TensorMesh -from SimPEG.utils import sdiag +from simpeg.utils import sdiag import numpy as np TOLD = 1e-10 diff --git a/tests/base/test_coordutils.py b/tests/base/test_coordutils.py index 6ccd28ec43..601110ba2e 100644 --- a/tests/base/test_coordutils.py +++ b/tests/base/test_coordutils.py @@ -1,6 +1,6 @@ import unittest import numpy as np -from SimPEG import utils +from simpeg import utils tol = 1e-15 diff --git a/tests/base/test_correspondance.py b/tests/base/test_correspondance.py index 4e9fc71442..8e92fdb848 100644 --- a/tests/base/test_correspondance.py +++ b/tests/base/test_correspondance.py @@ -3,7 +3,7 @@ import numpy as np from discretize import TensorMesh -from SimPEG import ( +from simpeg import ( maps, regularization, ) @@ -30,7 +30,7 @@ def setUp(self): corr = regularization.LinearCorrespondence( mesh, wire_map=wires, - indActive=actv, + active_cells=actv, ) self.mesh = mesh @@ -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.py b/tests/base/test_data.py index 75947c8b06..b62d6cc9c0 100644 --- a/tests/base/test_data.py +++ b/tests/base/test_data.py @@ -3,9 +3,9 @@ import numpy as np import discretize -from SimPEG import maps -from SimPEG import simulation, survey -from SimPEG import Data +from simpeg import maps +from simpeg import simulation, survey +from simpeg import Data class DataTest(unittest.TestCase): diff --git a/tests/base/test_data_misfit.py b/tests/base/test_data_misfit.py index c83fea1291..2e23131da5 100644 --- a/tests/base/test_data_misfit.py +++ b/tests/base/test_data_misfit.py @@ -3,8 +3,8 @@ import numpy as np import discretize -from SimPEG import maps -from SimPEG import data_misfit, simulation, survey +from simpeg import maps +from simpeg import data_misfit, simulation, survey np.random.seed(17) diff --git a/tests/base/test_directives.py b/tests/base/test_directives.py index 8637e633af..f6450eb586 100644 --- a/tests/base/test_directives.py +++ b/tests/base/test_directives.py @@ -3,7 +3,7 @@ import numpy as np import discretize -from SimPEG import ( +from simpeg import ( maps, directives, regularization, @@ -13,7 +13,7 @@ inverse_problem, simulation, ) -from SimPEG.potential_fields import magnetics as mag +from simpeg.potential_fields import magnetics as mag import shutil @@ -64,11 +64,16 @@ def setUp(self): mesh = discretize.TensorMesh([4, 4, 4]) # Magnetic inducing field parameter (A,I,D) - B = [50000, 90, 0] + h0_amplitude, h0_inclination, h0_declination = (50000, 90, 0) # Create a MAGsurvey rx = mag.Point(np.vstack([[0.25, 0.25, 0.25], [-0.25, -0.25, 0.25]])) - srcField = mag.UniformBackgroundField([rx], parameters=(B[0], B[1], B[2])) + srcField = mag.UniformBackgroundField( + receiver_list=[rx], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) survey = mag.Survey(srcField) # Create the forward model operator @@ -128,21 +133,12 @@ def test_validation_in_inversion(self): inv = inversion.BaseInversion(invProb) inv.directiveList = [update_Jacobi, sensitivity_weights] - def test_sensitivity_weighting_warnings(self): - # Test setter warnings - d_temp = directives.UpdateSensitivityWeights() - d_temp.normalization_method = True - self.assertTrue(d_temp.normalization_method == "maximum") - - d_temp.normalization_method = False - self.assertTrue(d_temp.normalization_method is None) - def test_sensitivity_weighting_global(self): test_inputs = { - "everyIter": False, - "threshold": 1e-12, + "every_iteration": False, + "threshold_value": 1e-12, "threshold_method": "global", - "normalization": False, + "normalization_method": None, } # Compute test weights @@ -150,7 +146,7 @@ def test_sensitivity_weighting_global(self): np.sqrt(np.sum((self.dmis.W * self.sim.G) ** 2, axis=0)) / self.mesh.cell_volumes ) - test_weights = sqrt_diagJtJ + test_inputs["threshold"] + test_weights = sqrt_diagJtJ + test_inputs["threshold_value"] test_weights *= self.mesh.cell_volumes # Test directive @@ -165,7 +161,11 @@ def test_sensitivity_weighting_global(self): test_directive.update() for reg_i in reg.objfcts: - self.assertTrue(np.all(np.isclose(test_weights, reg_i.cell_weights))) + # Get all weights in regularization + weights = [reg_i.get_weights(key) for key in reg_i.weights_keys] + # Compute the product of all weights + weights = np.prod(weights, axis=0) + self.assertTrue(np.all(np.isclose(test_weights, weights))) reg_i.remove_weights("sensitivity") # self.test_sensitivity_weighting_subroutine(test_weights, test_directive) @@ -177,7 +177,7 @@ def test_sensitivity_weighting_percentile_maximum(self): "every_iteration": True, "threshold_value": 1, "threshold_method": "percentile", - "normalization": True, + "normalization_method": "maximum", } # Compute test weights @@ -205,7 +205,11 @@ def test_sensitivity_weighting_percentile_maximum(self): test_directive.update() for reg_i in reg.objfcts: - self.assertTrue(np.all(np.isclose(test_weights, reg_i.cell_weights))) + # Get all weights in regularization + weights = [reg_i.get_weights(key) for key in reg_i.weights_keys] + # Compute the product of all weights + weights = np.prod(weights, axis=0) + self.assertTrue(np.all(np.isclose(test_weights, weights))) reg_i.remove_weights("sensitivity") # self.test_sensitivity_weighting_subroutine(test_weights, test_directive) @@ -245,7 +249,11 @@ def test_sensitivity_weighting_amplitude_minimum(self): test_directive.update() for reg_i in reg.objfcts: - self.assertTrue(np.all(np.isclose(test_weights, reg_i.cell_weights))) + # Get all weights in regularization + weights = [reg_i.get_weights(key) for key in reg_i.weights_keys] + # Compute the product of all weights + weights = np.prod(weights, axis=0) + self.assertTrue(np.all(np.isclose(test_weights, weights))) reg_i.remove_weights("sensitivity") # self.test_sensitivity_weighting_subroutine(test_weights, test_directive) @@ -298,5 +306,157 @@ 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` + """ + + @pytest.mark.parametrize("normalization_method", (None, "maximum", "minimum")) + def test_normalization_method_setter_valid(self, normalization_method): + """ + Test if the setter method for normalization_method in + `UpdateSensitivityWeights` works as expected on valid values. + + The `normalization_method` must be a string or a None. This test was + included as part of the removal process of the old `normalization` + property. + """ + d_temp = directives.UpdateSensitivityWeights() + # Use the setter method to assign a value to normalization_method + d_temp.normalization_method = normalization_method + assert d_temp.normalization_method == normalization_method + + @pytest.mark.parametrize("normalization_method", (True, False, "an invalid method")) + def test_normalization_method_setter_invalid(self, normalization_method): + """ + Test if the setter method for normalization_method in + `UpdateSensitivityWeights` raises error on invalid values. + + The `normalization_method` must be a string or a None. This test was + included as part of the removal process of the old `normalization` + property. + """ + d_temp = directives.UpdateSensitivityWeights() + if isinstance(normalization_method, bool): + error_type = TypeError + msg = "'normalization_method' must be a str. Got" + else: + error_type = ValueError + msg = ( + r"'normalization_method' must be in \['minimum', 'maximum'\]. " + f"Got '{normalization_method}'" + ) + with pytest.raises(error_type, match=msg): + d_temp.normalization_method = normalization_method + + +class TestSeedProperty: + """ + Test ``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( + "seed", + (42, np.random.default_rng(seed=1), np.array([1, 2])), + ids=("int", "rng", "array"), + ) + def test_valid_seed(self, directive_class, seed): + "Test if seed setter works as expected on valid seed arguments." + directive = directive_class(seed=seed) + assert directive.seed is seed + + @pytest.mark.parametrize("directive_class", directive_classes) + @pytest.mark.parametrize("seed", (42.1, np.array([1.0, 2.0]))) + def test_invalid_seed(self, directive_class, 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(seed=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 + seed = 42 + directive = directives.BetaEstimate_ByEig( + beta0_ratio=beta0_ratio, n_pw_iter=n_pw_iter, seed=seed + ) + assert directive.beta0_ratio == beta0_ratio + assert directive.n_pw_iter == n_pw_iter + assert directive.seed == seed + + def test_beta_estimate_max_derivative(self): + """Test on directives.BetaEstimateMaxDerivative.""" + beta0_ratio = 3.0 + seed = 42 + directive = directives.BetaEstimateMaxDerivative( + beta0_ratio=beta0_ratio, seed=seed + ) + assert directive.beta0_ratio == beta0_ratio + assert directive.seed == seed + + if __name__ == "__main__": unittest.main() diff --git a/tests/base/test_joint.py b/tests/base/test_joint.py index 30033d3aa7..3d269cb141 100644 --- a/tests/base/test_joint.py +++ b/tests/base/test_joint.py @@ -3,7 +3,7 @@ import numpy as np import discretize -from SimPEG import ( +from simpeg import ( data_misfit, maps, utils, @@ -13,7 +13,7 @@ directives, inversion, ) -from SimPEG.electromagnetics import resistivity as DC +from simpeg.electromagnetics import resistivity as DC np.random.seed(82) @@ -72,8 +72,8 @@ def setUp(self): self.dmiscombo = self.dmis0 + self.dmis1 def test_multiDataMisfit(self): - self.dmis0.test() - self.dmis1.test() + self.dmis0.test(random_seed=42) + self.dmis1.test(random_seed=42) self.dmiscombo.test(x=self.model) def test_inv(self): diff --git a/tests/base/test_maps.py b/tests/base/test_maps.py index f037b2d5f4..4957f8db28 100644 --- a/tests/base/test_maps.py +++ b/tests/base/test_maps.py @@ -4,7 +4,7 @@ import pytest import scipy.sparse as sp -from SimPEG import maps, models, utils +from simpeg import maps, models, utils from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz import inspect @@ -564,6 +564,51 @@ def test_Tile(self): self.assertTrue((local_mass - total_mass) / total_mass < 1e-8) + def test_logit_errors(self): + nP = 10 + scalar_lower = -2 + scalar_upper = 2 + good_vector_lower = np.random.rand(nP) - 2 + good_vector_upper = np.random.rand(nP) + 2 + + bad_vector_lower = np.random.rand(nP - 2) - 2 + bad_vector_upper = np.random.rand(nP - 2) + 2 + + # test that lower is not equal to nP + with pytest.raises( + ValueError, + match="Lower bound does not broadcast to the number of parameters.*", + ): + maps.LogisticSigmoidMap( + nP=10, lower_bound=bad_vector_lower, upper_bound=scalar_upper + ) + + # test that bad is not equal to nP + with pytest.raises( + ValueError, + match="Upper bound does not broadcast to the number of parameters.*", + ): + maps.LogisticSigmoidMap( + nP=10, lower_bound=scalar_lower, upper_bound=bad_vector_upper + ) + + # test that two upper and lower arrays will not broadcast when not specifying the number of parameters + with pytest.raises( + ValueError, match="Upper bound does not broadcast to the lower bound.*" + ): + maps.LogisticSigmoidMap( + lower_bound=good_vector_lower, upper_bound=bad_vector_upper + ) + + # test that passing a lower bound higher than an upper bound) + with pytest.raises( + ValueError, + match="A lower bound is greater than or equal to the upper bound.", + ): + maps.LogisticSigmoidMap( + lower_bound=good_vector_upper, upper_bound=good_vector_lower + ) + class TestWires(unittest.TestCase): def test_basic(self): @@ -589,7 +634,7 @@ 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.abs(np.random.rand(mesh.nC)) + 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) def test_spheroidalInclusions(self): @@ -706,6 +751,7 @@ def test_linearity(): maps.SphericalSystem(mesh2), maps.SelfConsistentEffectiveMedium(mesh2, sigma0=1, sigma1=2), maps.ExpMap(), + maps.LogisticSigmoidMap(), maps.ReciprocalMap(), maps.LogMap(), maps.ParametricCircleMap(mesh2), diff --git a/tests/base/test_mass_matrices.py b/tests/base/test_mass_matrices.py index 396a75cd29..90dc04ea75 100644 --- a/tests/base/test_mass_matrices.py +++ b/tests/base/test_mass_matrices.py @@ -1,11 +1,13 @@ -from SimPEG.base import with_property_mass_matrices, BasePDESimulation -from SimPEG import props, maps +from simpeg.base import with_property_mass_matrices, BasePDESimulation +from simpeg import props, maps import unittest import discretize import numpy as np from scipy.constants import mu_0 from discretize.tests import check_derivative from discretize.utils import Zero +import scipy.sparse as sp +import pytest # define a very simple class... @@ -41,9 +43,24 @@ def setUp(self): self.mesh = discretize.TensorMesh([5, 6, 7]) self.sim = SimpleSim(self.mesh, sigmaMap=maps.ExpMap()) - self.start_mod = np.log(1e-2 * np.ones(self.mesh.n_cells)) + np.random.randn( - self.mesh.n_cells - ) + n_cells = self.mesh.n_cells + self.start_mod = np.log(np.full(n_cells, 1e-2)) + np.random.randn(n_cells) + self.start_diag_mod = np.r_[ + np.log(np.full(n_cells, 1e-2)), + np.log(np.full(n_cells, 2e-2)), + np.log(np.full(n_cells, 3e-2)), + ] + np.random.randn(3 * n_cells) + + self.sim_full_aniso = SimpleSim(self.mesh, sigmaMap=maps.IdentityMap()) + + self.start_full_mod = np.r_[ + np.full(n_cells, 1), + np.full(n_cells, 2), + np.full(n_cells, 3), + np.full(n_cells, -1), + np.full(n_cells, 1), + np.full(n_cells, -2), + ] def test_zero_returns(self): n_c = self.mesh.n_cells @@ -180,6 +197,66 @@ def test_forward_expected_shapes(self): UM @ v, sim.MfSigmaDeriv(u2, v).reshape(-1, order="F") ) + def test_forward_anis_expected_shapes(self): + sim = self.sim + sim.model = self.start_full_mod + + n_f = self.mesh.n_faces + n_p = sim.model.size + # if U.shape (*, ) + u = np.random.rand(n_f) + v = np.random.randn(n_p) + u2 = np.random.rand(n_f, 2) + v2 = np.random.randn(n_p, 4) + + # These cases should all return an array of shape (n_f, ) + # if V.shape (*, ) + out = sim.MfSigmaDeriv(u, v) + assert out.shape == (n_f,) + out = sim.MfSigmaDeriv(u, v[:, None]) + assert out.shape == (n_f,) + out = sim.MfSigmaDeriv(u[:, None], v) + assert out.shape == (n_f,) + out = sim.MfSigmaDeriv(u[:, None], v[:, None]) + assert out.shape == (n_f,) + + # now check passing multiple V's + out = sim.MfSigmaDeriv(u, v2) + assert out.shape == (n_f, 4) + out = sim.MfSigmaDeriv(u[:, None], v2) + assert out.shape == (n_f, 4) + + # also ensure it properly broadcasted the operation.... + out_2 = np.empty_like(out) + for i in range(v2.shape[1]): + out_2[:, i] = sim.MfSigmaDeriv(u[:, None], v2[:, i]) + np.testing.assert_equal(out, out_2) + + # now check for multiple source polarizations + out = sim.MfSigmaDeriv(u2, v) + assert out.shape == (n_f, 2) + out = sim.MfSigmaDeriv(u2, v[:, None]) + assert out.shape == (n_f, 2) + + # and with multiple RHS + out = sim.MfSigmaDeriv(u2, v2) + assert out.shape == (n_f, v2.shape[1], 2) + + # and test broadcasting here... + out_2 = np.empty_like(out) + for i in range(v2.shape[1]): + out_2[:, i, :] = sim.MfSigmaDeriv(u2, v2[:, i]) + np.testing.assert_equal(out, out_2) + + # test None as v + UM = sim.MfSigmaDeriv(u) + np.testing.assert_allclose(UM @ v, sim.MfSigmaDeriv(u, v)) + + UM = sim.MfSigmaDeriv(u2) + np.testing.assert_allclose( + UM @ v, sim.MfSigmaDeriv(u2, v).reshape(-1, order="F") + ) + def test_adjoint_expected_shapes(self): sim = self.sim sim.model = self.start_mod @@ -242,7 +319,69 @@ def test_adjoint_expected_shapes(self): UMT @ v2_2.reshape(-1, order="F"), sim.MfSigmaDeriv(u2, v2_2, adjoint=True) ) - def test_adjoint_opp_shapes(self): + def test_adjoint_anis_expected_shapes(self): + sim = self.sim + sim.model = self.start_full_mod + + n_f = self.mesh.n_faces + n_p = sim.model.size + + u = np.random.rand(n_f) + v = np.random.randn(n_f) + v2 = np.random.randn(n_f, 4) + u2 = np.random.rand(n_f, 2) + v2_2 = np.random.randn(n_f, 2) + v3 = np.random.rand(n_f, 4, 2) + + # These cases should all return an array of shape (n_c, ) + # if V.shape (n_f, ) + out = sim.MfSigmaDeriv(u, v, adjoint=True) + assert out.shape == (n_p,) + out = sim.MfSigmaDeriv(u, v[:, None], adjoint=True) + assert out.shape == (n_p,) + out = sim.MfSigmaDeriv(u[:, None], v, adjoint=True) + assert out.shape == (n_p,) + out = sim.MfSigmaDeriv(u[:, None], v[:, None], adjoint=True) + assert out.shape == (n_p,) + + # now check passing multiple V's + out = sim.MfSigmaDeriv(u, v2, adjoint=True) + assert out.shape == (n_p, 4) + out = sim.MfSigmaDeriv(u[:, None], v2, adjoint=True) + assert out.shape == (n_p, 4) + + # also ensure it properly broadcasted the operation.... + out_2 = np.empty_like(out) + for i in range(v2.shape[1]): + out_2[:, i] = sim.MfSigmaDeriv(u, v2[:, i], adjoint=True) + np.testing.assert_equal(out, out_2) + + # now check for multiple source polarizations + out = sim.MfSigmaDeriv(u2, v2_2, adjoint=True) + assert out.shape == (n_p,) + out = sim.MfSigmaDeriv(u2, v2_2, adjoint=True) + assert out.shape == (n_p,) + + # and with multiple RHS + out = sim.MfSigmaDeriv(u2, v3, adjoint=True) + assert out.shape == (n_p, v3.shape[1]) + + # and test broadcasting here... + out_2 = np.empty_like(out) + for i in range(v2.shape[1]): + out_2[:, i] = sim.MfSigmaDeriv(u2, v3[:, i, :], adjoint=True) + np.testing.assert_equal(out, out_2) + + # test None as v + UMT = sim.MfSigmaDeriv(u, adjoint=True) + np.testing.assert_allclose(UMT @ v, sim.MfSigmaDeriv(u, v, adjoint=True)) + + UMT = sim.MfSigmaDeriv(u2, adjoint=True) + np.testing.assert_allclose( + UMT @ v2_2.reshape(-1, order="F"), sim.MfSigmaDeriv(u2, v2_2, adjoint=True) + ) + + def test_adjoint_opp(self): sim = self.sim sim.model = self.start_mod @@ -301,6 +440,44 @@ def test_adjoint_opp_shapes(self): yJtv = np.sum(y2 * sim.MfSigmaIDeriv(u2, v3, adjoint=True)) np.testing.assert_allclose(vJy, yJtv) + def test_anis_adjoint_opp(self): + sim = self.sim + sim.model = self.start_full_mod + + n_f = self.mesh.n_faces + n_p = sim.model.size + + u = np.random.rand(n_f) + u2 = np.random.rand(n_f, 2) + + y = np.random.rand(n_p) + y2 = np.random.rand(n_p, 4) + + v = np.random.randn(n_f) + v2 = np.random.randn(n_f, 4) + v2_2 = np.random.randn(n_f, 2) + v3 = np.random.rand(n_f, 4, 2) + + # u1, y1 -> v1 + vJy = v @ sim.MfSigmaDeriv(u, y) + yJtv = y @ sim.MfSigmaDeriv(u, v, adjoint=True) + np.testing.assert_allclose(vJy, yJtv) + + # u1, y2 -> v2 + vJy = np.sum(v2 * sim.MfSigmaDeriv(u, y2)) + yJtv = np.sum(y2 * sim.MfSigmaDeriv(u, v2, adjoint=True)) + np.testing.assert_allclose(vJy, yJtv) + + # u2, y1 -> v2_2 + vJy = np.sum(v2_2 * sim.MfSigmaDeriv(u2, y)) + yJtv = np.sum(y * sim.MfSigmaDeriv(u2, v2_2, adjoint=True)) + np.testing.assert_allclose(vJy, yJtv) + + # u2, y2 -> v3 + vJy = np.sum(v3 * sim.MfSigmaDeriv(u2, y2)) + yJtv = np.sum(y2 * sim.MfSigmaDeriv(u2, v3, adjoint=True)) + np.testing.assert_allclose(vJy, yJtv) + def test_Mcc_deriv(self): u = np.random.randn(self.mesh.n_cells) sim = self.sim @@ -352,6 +529,40 @@ def Jvec(v): assert check_derivative(f, x0=x0, num=3, plotIt=False) + def test_Me_diagonal_anisotropy_deriv(self): + u = np.random.randn(self.mesh.n_edges) + sim = self.sim + x0 = self.start_diag_mod + + def f(x): + sim.model = x + d = sim.MeSigma @ u + + def Jvec(v): + sim.model = x0 + return sim.MeSigmaDeriv(u, v) + + return d, Jvec + + assert check_derivative(f, x0=x0, num=3, plotIt=False) + + def test_Me_full_anisotropy_deriv(self): + u = np.random.randn(self.mesh.n_edges) + sim = self.sim_full_aniso + x0 = self.start_full_mod + + def f(x): + sim.model = x + d = sim.MeSigma @ u + + def Jvec(v): + sim.model = x0 + return sim.MeSigmaDeriv(u, v) + + return d, Jvec + + assert check_derivative(f, x0=x0, num=3, plotIt=False) + def test_Mf_deriv(self): u = np.random.randn(self.mesh.n_faces) sim = self.sim @@ -369,6 +580,40 @@ def Jvec(v): assert check_derivative(f, x0=x0, num=3, plotIt=False) + def test_Mf_diagonal_anisotropy_deriv(self): + u = np.random.randn(self.mesh.n_faces) + sim = self.sim + x0 = self.start_diag_mod + + def f(x): + sim.model = x + d = sim.MfSigma @ u + + def Jvec(v): + sim.model = x0 + return sim.MfSigmaDeriv(u, v) + + return d, Jvec + + assert check_derivative(f, x0=x0, num=3, plotIt=False) + + def test_Mf_full_anisotropy_deriv(self): + u = np.random.randn(self.mesh.n_faces) + sim = self.sim_full_aniso + x0 = self.start_full_mod + + def f(x): + sim.model = x + d = sim.MfSigma @ u + + def Jvec(v): + sim.model = x0 + return sim.MfSigmaDeriv(u, v) + + return d, Jvec + + assert check_derivative(f, x0=x0, num=3, plotIt=False) + def test_MccI_deriv(self): u = np.random.randn(self.mesh.n_cells) sim = self.sim @@ -540,3 +785,24 @@ def test_MfI_adjoint(self): yJv = y @ sim.MfSigmaIDeriv(u, v) vJty = v @ sim.MfSigmaIDeriv(u, y, adjoint=True) np.testing.assert_allclose(yJv, vJty) + + +def test_bad_derivative_stash(): + mesh = discretize.TensorMesh([5, 6, 7]) + sim = SimpleSim(mesh, sigmaMap=maps.ExpMap()) + sim.model = np.random.rand(mesh.n_cells) + + u = np.random.rand(mesh.n_edges) + v = np.random.rand(mesh.n_cells) + + # This should work + sim.MeSigmaDeriv(u, v) + # stashed derivative operation is a sparse matrix + assert sp.issparse(sim._Me_Sigma_deriv) + + # Let's set the stashed item as a bad value which would error + # The user shouldn't cause this to happen, but a developer might. + sim._Me_Sigma_deriv = [40, 10, 30] + + with pytest.raises(TypeError): + sim.MeSigmaDeriv(u, v) diff --git a/tests/base/test_model_utils.py b/tests/base/test_model_utils.py index d7d57f6d80..97802fbf2d 100644 --- a/tests/base/test_model_utils.py +++ b/tests/base/test_model_utils.py @@ -1,10 +1,11 @@ +import pytest import unittest import numpy as np from discretize import TensorMesh -from SimPEG import ( +from simpeg import ( utils, ) @@ -22,7 +23,9 @@ def test_depth_weighting_3D(self): r_loc = 0.1 # Depth weighting - wz = utils.depth_weighting(mesh, r_loc, indActive=actv, exponent=5, threshold=0) + 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)) @@ -31,14 +34,14 @@ def test_depth_weighting_3D(self): reference_locs[:, -1] = r_loc wz2 = utils.depth_weighting( - mesh, reference_locs, indActive=actv, exponent=5, threshold=0 + mesh, reference_locs, active_cells=actv, exponent=5, threshold=0 ) np.testing.assert_allclose(wz, wz2) # testing default params all_active = np.ones(mesh.n_cells, dtype=bool) wz = utils.depth_weighting( - mesh, r_loc, indActive=all_active, exponent=2, threshold=0.5 * dh + mesh, r_loc, active_cells=all_active, exponent=2, threshold=0.5 * dh ) wz2 = utils.depth_weighting(mesh, r_loc) @@ -58,7 +61,9 @@ def test_depth_weighting_2D(self): r_loc = 0.1 # Depth weighting - wz = utils.depth_weighting(mesh, r_loc, indActive=actv, exponent=5, threshold=0) + wz = utils.depth_weighting( + 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)) @@ -67,10 +72,30 @@ def test_depth_weighting_2D(self): reference_locs[:, -1] = r_loc wz2 = utils.depth_weighting( - mesh, reference_locs, indActive=actv, exponent=5, threshold=0 + mesh, reference_locs, active_cells=actv, exponent=5, threshold=0 ) np.testing.assert_allclose(wz, wz2) +@pytest.fixture +def mesh(): + """Sample mesh.""" + dh = 5.0 + hx = [(dh, 5, -1.3), (dh, 40), (dh, 5, 1.3)] + hz = [(dh, 15)] + mesh = TensorMesh([hx, hz], "CN") + return mesh + + +def test_removed_indactive(mesh): + """ + Test if error is raised after passing removed indActive argument + """ + active_cells = np.ones(mesh.nC, dtype=bool) + 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 07d750a55d..ad4a3d22ac 100644 --- a/tests/base/test_objective_function.py +++ b/tests/base/test_objective_function.py @@ -1,9 +1,12 @@ import numpy as np import scipy.sparse as sp +import pytest import unittest -from SimPEG import utils, maps -from SimPEG import objective_function +from simpeg import utils, maps +from simpeg import objective_function +from simpeg.objective_function import _validate_multiplier +from simpeg.utils import Zero np.random.seed(130) @@ -54,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) @@ -123,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 @@ -163,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 @@ -192,6 +195,18 @@ def test_updateMultipliers(self): self.assertTrue(phi(m) == phi1(m)) + def test_invalid_mapping(self): + """Test if setting mapping of wrong type raises errors.""" + + class Dummy: + pass + + phi = objective_function.L2ObjectiveFunction() + invalid_mapping = Dummy() + msg = "Invalid mapping of class 'Dummy'." + with pytest.raises(TypeError, match=msg): + phi.mapping = invalid_mapping + def test_early_exits(self): nP = 10 @@ -242,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 @@ -263,13 +279,11 @@ def test_ComboW(self): r1 = phi1.W * m r2 = phi2.W * m - print(phi(m), 0.5 * np.inner(r, r)) + print(phi(m), np.inner(r, r)) - self.assertTrue(np.allclose(phi(m), 0.5 * np.inner(r, r))) + self.assertTrue(np.allclose(phi(m), np.inner(r, r))) self.assertTrue( - np.allclose( - phi(m), 0.5 * (alpha1 * np.inner(r1, r1) + alpha2 * np.inner(r2, r2)) - ) + np.allclose(phi(m), (alpha1 * np.inner(r1, r1) + alpha2 * np.inner(r2, r2))) ) def test_ComboConstruction(self): @@ -313,6 +327,158 @@ def test_updating_multipliers(self): with self.assertRaises(Exception): phi3.multipliers = ["a", "b"] + def test_inconsistent_nparams_and_weights(self): + """ + Test if L2ObjectiveFunction raises error after nP != columns in W + """ + n_params = 9 + weights = np.zeros((5, n_params + 1)) + with pytest.raises(ValueError, match="Number of parameters nP"): + objective_function.L2ObjectiveFunction(nP=n_params, W=weights) + + +class TestOperationsComboObjectiveFunctions: + """Test arithmetic operations involving ComboObjectiveFunction""" + + @pytest.mark.parametrize("unpack_on_add", (True, False)) + def test_mul(self, unpack_on_add): + """Test if ComboObjectiveFunction multiplication works as expected""" + n_params = 10 + phi1 = objective_function.L2ObjectiveFunction(nP=n_params) + phi2 = objective_function.L2ObjectiveFunction(nP=n_params) + combo = objective_function.ComboObjectiveFunction( + [phi1, phi2], [2, 3], unpack_on_add=unpack_on_add + ) + combo_mul = 3.5 * combo + assert len(combo_mul) == 1 + assert combo_mul.multipliers == [3.5] + assert combo_mul.objfcts == [combo] + + @pytest.mark.parametrize("unpack_on_add", (True, False)) + def test_add(self, unpack_on_add): + """Test if ComboObjectiveFunction addition works as expected""" + n_params = 10 + phi1 = objective_function.L2ObjectiveFunction(nP=n_params) + phi2 = objective_function.L2ObjectiveFunction(nP=n_params) + phi3 = objective_function.L2ObjectiveFunction(nP=n_params) + combo_1 = objective_function.ComboObjectiveFunction( + [phi1, phi2], [2, 3], unpack_on_add=unpack_on_add + ) + combo_2 = phi3 + combo_1 + if unpack_on_add: + assert len(combo_2) == 3 + assert combo_2.multipliers == [1, 2, 3] + assert combo_2.objfcts == [phi3, phi1, phi2] + else: + assert len(combo_2) == 2 + assert combo_2.multipliers == [1, 1] + assert combo_2.objfcts == [phi3, combo_1] + combo_1 = combo_2.objfcts[1] + assert combo_1.multipliers == [2, 3] + + def test_add_multiple_terms(self): + """Test addition of multiple BaseObjectiveFunctions""" + n_params = 10 + phi1 = objective_function.L2ObjectiveFunction(nP=n_params) + phi2 = objective_function.L2ObjectiveFunction(nP=n_params) + phi3 = objective_function.L2ObjectiveFunction(nP=n_params) + combo = 1.1 * phi1 + 1.2 * phi2 + 1.3 * phi3 + assert len(combo) == 3 + assert combo.multipliers == [1.1, 1.2, 1.3] + assert combo.objfcts == [phi1, phi2, phi3] + + @pytest.mark.parametrize("unpack_on_add", (True, False)) + def test_add_and_mul(self, unpack_on_add): + """ + Test ComboObjectiveFunction addition with multiplication + + After multiplying a Combo with a scalar, the `__mul__` method creates + another Combo for it. + """ + n_params = 10 + phi1 = objective_function.L2ObjectiveFunction(nP=n_params) + phi2 = objective_function.L2ObjectiveFunction(nP=n_params) + phi3 = objective_function.L2ObjectiveFunction(nP=n_params) + combo_1 = objective_function.ComboObjectiveFunction( + [phi1, phi2], [2, 3], unpack_on_add=unpack_on_add + ) + combo_2 = 5 * phi3 + 1.2 * combo_1 + assert len(combo_2) == 2 + assert combo_2.multipliers == [5, 1.2] + assert combo_2.objfcts == [phi3, combo_1] + + +@pytest.mark.parametrize( + "objfcts, multipliers", + ( + (None, None), + ([objective_function.L2ObjectiveFunction()], None), + ([objective_function.L2ObjectiveFunction()], [2.5]), + ), +) +def test_empty_combo(objfcts, multipliers): + """Test defining an empty ComboObjectiveFunction.""" + combo = objective_function.ComboObjectiveFunction( + objfcts=objfcts, multipliers=multipliers + ) + if objfcts is None and multipliers is None: + assert combo.objfcts == [] + assert combo.multipliers == [] + if objfcts is not None: + assert combo.objfcts == objfcts + if multipliers is None: + assert combo.multipliers == [1] + else: + assert combo.multipliers == [2.5] + + +def test_invalid_objfcts_in_combo(): + """Test invalid objective function class in ComboObjectiveFunction.""" + + class Dummy: + pass + + phi = objective_function.L2ObjectiveFunction() + invalid_phi = Dummy() + msg = "Unrecognized objective function type Dummy in 'objfcts'." + with pytest.raises(TypeError, match=msg): + objective_function.ComboObjectiveFunction(objfcts=[phi, invalid_phi]) + + +class TestMultiplierValidation: + """ + Test the _validate_multiplier private function. + """ + + @pytest.mark.parametrize( + "multiplier", + ( + 3.14, + 1, + np.float64(-15.3), + np.float32(-10.2), + np.int64(10), + np.int32(33), + Zero(), + ), + ) + def test_valid_multipliers(self, multiplier): + """ + Test function against valid multipliers + """ + _validate_multiplier(multiplier) + + @pytest.mark.parametrize( + "multiplier", + (np.array([1, 3.14]), np.array(3), [1, 2, 3], "string", True, None), + ) + def test_invalid_multipliers(self, multiplier): + """ + Test function against invalid multipliers + """ + with pytest.raises(TypeError, match="Invalid multiplier"): + _validate_multiplier(multiplier) + if __name__ == "__main__": unittest.main() diff --git a/tests/base/test_optimizers.py b/tests/base/test_optimizers.py index 4bdfd2cafa..45ef588f25 100644 --- a/tests/base/test_optimizers.py +++ b/tests/base/test_optimizers.py @@ -1,8 +1,8 @@ import unittest -from SimPEG.utils import sdiag +from simpeg.utils import sdiag import numpy as np import scipy.sparse as sp -from SimPEG import optimization +from simpeg import optimization from discretize.tests import get_quadratic, rosenbrock TOL = 1e-2 @@ -49,11 +49,11 @@ def test_ProjGradient_quadratic1Bound(self): self.assertTrue(np.linalg.norm(xopt - x_true, 2) < TOL, True) def test_NewtonRoot(self): - fun = ( - lambda x, return_g=True: np.sin(x) - if not return_g - else (np.sin(x), sdiag(np.cos(x))) - ) + 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]) diff --git a/tests/base/test_problem.py b/tests/base/test_problem.py index d5a0f074ae..a6afd10479 100644 --- a/tests/base/test_problem.py +++ b/tests/base/test_problem.py @@ -1,7 +1,6 @@ import unittest import discretize -import discretize -from SimPEG import simulation +from simpeg import simulation import numpy as np diff --git a/tests/base/test_simulation.py b/tests/base/test_simulation.py index a1271d96b2..4f49d4c9fa 100644 --- a/tests/base/test_simulation.py +++ b/tests/base/test_simulation.py @@ -1,7 +1,7 @@ import unittest import numpy as np import discretize -from SimPEG import maps, simulation +from simpeg import maps, simulation class TestLinearSimulation(unittest.TestCase): @@ -19,32 +19,6 @@ def setUp(self): self.mtrue = mtrue - def test_forward(self): - data = np.r_[ - 7.50000000e-02, - 5.34102961e-02, - 5.26315566e-03, - -3.92235199e-02, - -4.22361894e-02, - -1.29419602e-02, - 1.30060891e-02, - 1.73572943e-02, - 7.78056876e-03, - -1.49689823e-03, - -4.50212858e-03, - -3.14559131e-03, - -9.55761370e-04, - 3.53963158e-04, - 7.24902205e-04, - 6.06022770e-04, - 3.36635644e-04, - 7.48637479e-05, - -1.10094573e-04, - -1.84905476e-04, - ] - - assert np.allclose(data, self.sim.dpred(self.mtrue)) - def test_make_synthetic_data(self): dclean = self.sim.dpred(self.mtrue) data = self.sim.make_synthetic_data(self.mtrue) diff --git a/tests/base/test_stub.py b/tests/base/test_stub.py new file mode 100644 index 0000000000..1b830df195 --- /dev/null +++ b/tests/base/test_stub.py @@ -0,0 +1,14 @@ +import pytest + + +def test_SimPEG_import(): + with pytest.warns( + FutureWarning, + match="Importing `SimPEG` is deprecated. please import from `simpeg`.", + ): + from SimPEG import data + import SimPEG + import simpeg + + assert SimPEG is simpeg + assert data.__file__.endswith("simpeg/data.py") diff --git a/tests/base/test_survey_data.py b/tests/base/test_survey_data.py index 1056cfee26..9b06649e07 100644 --- a/tests/base/test_survey_data.py +++ b/tests/base/test_survey_data.py @@ -1,6 +1,6 @@ import unittest import numpy as np -from SimPEG import survey, utils, data +from simpeg import survey, utils, data np.random.seed(100) diff --git a/tests/base/test_utils.py b/tests/base/test_utils.py index c8bc597638..1fa7ad6d39 100644 --- a/tests/base/test_utils.py +++ b/tests/base/test_utils.py @@ -1,9 +1,10 @@ import unittest +import pytest import numpy as np import scipy.sparse as sp import os import shutil -from SimPEG.utils import ( +from simpeg.utils import ( sdiag, sub2ind, ndgrid, @@ -16,44 +17,20 @@ ind2sub, as_array_n_by_dim, TensorType, - diagEst, + estimate_diagonal, count, timeIt, Counter, download, - surface2ind_topo, + coterminal, ) import discretize -from discretize.tests import check_derivative TOL = 1e-8 np.random.seed(25) -class TestCheckDerivative(unittest.TestCase): - def test_simplePass(self): - def simplePass(x): - return np.sin(x), sdiag(np.cos(x)) - - passed = check_derivative(simplePass, np.random.randn(5), plotIt=False) - self.assertTrue(passed, True) - - def test_simpleFunction(self): - def simpleFunction(x): - return np.sin(x), lambda xi: sdiag(np.cos(x)) * xi - - passed = check_derivative(simpleFunction, np.random.randn(5), plotIt=False) - self.assertTrue(passed, True) - - def test_simpleFail(self): - def simpleFail(x): - return np.sin(x), -sdiag(np.cos(x)) - - passed = check_derivative(simpleFail, np.random.randn(5), plotIt=False) - self.assertTrue(not passed, True) - - class TestCounter(unittest.TestCase): def test_simpleFail(self): class MyClass(object): @@ -298,38 +275,15 @@ def test_as_array_n_by_dim(self): self.assertTrue(np.all(true == listArray)) self.assertTrue(true.shape == listArray.shape) - def test_surface2ind_topo(self): - file_url = ( - "https://storage.googleapis.com/simpeg/tests/utils/vancouver_topo.xyz" - ) - file2load = download(file_url) - vancouver_topo = np.loadtxt(file2load) - mesh_topo = discretize.TensorMesh( - [[(500.0, 24)], [(500.0, 20)], [(10.0, 30)]], x0="CCC" - ) - - # To keep consistent with result from deprecated function - vancouver_topo[:, 2] = vancouver_topo[:, 2] + 1e-8 - - indtopoCC = surface2ind_topo( - mesh_topo, vancouver_topo, gridLoc="CC", method="nearest" - ) - indtopoN = surface2ind_topo( - mesh_topo, vancouver_topo, gridLoc="N", method="nearest" - ) - assert len(np.where(indtopoCC)[0]) == 8728 - assert len(np.where(indtopoN)[0]) == 8211 - - -class TestDiagEst(unittest.TestCase): +class TestEstimateDiagonal(unittest.TestCase): def setUp(self): self.n = 1000 self.A = np.random.rand(self.n, self.n) self.Adiag = np.diagonal(self.A) def getTest(self, testType): - Adiagtest = diagEst(self.A, self.n, self.n, testType) + Adiagtest = estimate_diagonal(self.A, self.n, self.n, testType) r = np.abs(Adiagtest - self.Adiag) err = r.dot(r) return err @@ -366,5 +320,35 @@ def test_downloads(self): shutil.rmtree(os.path.expanduser("./test_url")) +class TestCoterminalAngle: + """ + Tests for the coterminal function + """ + + @pytest.mark.parametrize( + "coterminal_angle", + (1 / 4 * np.pi, 3 / 4 * np.pi, -3 / 4 * np.pi, -1 / 4 * np.pi), + ids=("pi/4", "3/4 pi", "-3/4 pi", "-pi/4"), + ) + def test_angles_in_quadrants(self, coterminal_angle): + """ + Test coterminal for angles in each quadrant + """ + angles = np.array([2 * n * np.pi + coterminal_angle for n in range(-3, 4)]) + np.testing.assert_allclose(coterminal(angles), coterminal_angle) + + @pytest.mark.parametrize( + "coterminal_angle", + (0, np.pi / 2, -np.pi, -np.pi / 2), + ids=("0", "pi/2", "-pi", "-pi/2"), + ) + def test_right_angles(self, coterminal_angle): + """ + Test coterminal for right angles + """ + angles = np.array([2 * n * np.pi + coterminal_angle for n in range(-3, 4)]) + np.testing.assert_allclose(coterminal(angles), coterminal_angle) + + if __name__ == "__main__": unittest.main() diff --git a/tests/base/test_validators.py b/tests/base/test_validators.py index 5d35a2c5a2..2d2df0eb87 100644 --- a/tests/base/test_validators.py +++ b/tests/base/test_validators.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from SimPEG.utils import ( +from simpeg.utils import ( validate_string, validate_integer, validate_float, diff --git a/tests/dask/test_DC_jvecjtvecadj_dask.py b/tests/dask/test_DC_jvecjtvecadj_dask.py index 55722e9f67..a2f32c2e16 100644 --- a/tests/dask/test_DC_jvecjtvecadj_dask.py +++ b/tests/dask/test_DC_jvecjtvecadj_dask.py @@ -1,8 +1,8 @@ import unittest import numpy as np import discretize -import SimPEG.dask # noqa: F401 -from SimPEG import ( +import simpeg.dask # noqa: F401 +from simpeg import ( maps, data_misfit, regularization, @@ -11,8 +11,8 @@ inverse_problem, tests, ) -from SimPEG.utils import mkvc -from SimPEG.electromagnetics import resistivity as dc +from simpeg.utils import mkvc +from simpeg.electromagnetics import resistivity as dc import shutil np.random.seed(40) diff --git a/tests/dask/test_IP_jvecjtvecadj_dask.py b/tests/dask/test_IP_jvecjtvecadj_dask.py index c989089db1..73ac660054 100644 --- a/tests/dask/test_IP_jvecjtvecadj_dask.py +++ b/tests/dask/test_IP_jvecjtvecadj_dask.py @@ -1,23 +1,143 @@ +import os +import shutil +import tarfile import unittest -import discretize + +import discretize as ds import numpy as np -import SimPEG.dask # noqa: F401 -from SimPEG import maps -from SimPEG import data_misfit -from SimPEG import regularization -from SimPEG import optimization -from SimPEG import inversion -from SimPEG import inverse_problem -from SimPEG import tests - -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.electromagnetics import induced_polarization as ip -import shutil +import simpeg.dask # noqa: F401 +from simpeg import ( + data_misfit, + inverse_problem, + inversion, + maps, + optimization, + regularization, + tests, + utils, +) +from simpeg.electromagnetics import induced_polarization as ip +from simpeg.electromagnetics import resistivity as dc +from simpeg.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc np.random.seed(30) +class IPProblemTests2DN(unittest.TestCase): + """ + This test builds upon the 2D files used in the IP2D tutorial, with a much smaller mesh. + + It tests IP 2D with dask without calling `make_synthetic_data` first to simulate a real data case. + """ + + def setUp(self): + # storage bucket where we have the data + data_source = "https://storage.googleapis.com/simpeg/doc-assets/dcip2d.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.obs" + ip_data_filename = dir_path + "ip_data.obs" + + # Load topo + topo_xyz = np.loadtxt(str(topo_filename)) + # define the 2D topography along the survey line. + topo_2d = np.unique(topo_xyz[:, [0, 2]], axis=0) + # Load datas + dc_data = read_dcip2d_ubc(dc_data_filename, "volt", "general") + ip_data = read_dcip2d_ubc(ip_data_filename, "apparent_chargeability", "general") + # assign uncertainties + dc_data.standard_deviation = 0.05 * np.abs(dc_data.dobs) + ip_data.standard_deviation = 5e-3 * np.ones_like(ip_data.dobs) + # mesh + cs = 10.0 + mesh = ds.TensorMesh( + [ + [(cs, 20, -1.3), (cs, 20, 1.3)], + [(cs, 20, -1.3), (cs, 20, 1.3)], + ], + "CN", + ) + # 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") + + # Define conductivity model in S/m (or resistivity model in Ohm m) + air_conductivity = 1e-8 + background_conductivity = 1e-2 + active_map = maps.InjectActiveCells(mesh, ind_active, air_conductivity) + nC = int(ind_active.sum()) + # Define model + conductivity_model = background_conductivity * np.ones(nC) + + simulation = ip.simulation.Simulation2DNodal( + mesh=mesh, + survey=ip_data.survey, + sigma=conductivity_model, + etaMap=active_map, + ) + mSynth = np.ones(nC) * 0.1 + # test without calling make_synthetic_data first to simulate real data case + dobs = read_dcip2d_ubc(ip_data_filename, "apparent_chargeability", "general") + # 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=5, maxIter=1, tolF=1e-6, tolX=1e-6, tolG=1e-6, maxIterCG=5 + ) + invProb = inverse_problem.BaseInvProblem(dmis, reg, opt, beta=1e4) + inv = inversion.BaseInversion(invProb) + + self.inv = inv + self.reg = reg + self.p = simulation + self.mesh = mesh + self.m0 = mSynth + self.survey = ip_data.survey + self.dmis = dmis + self.conductivity_model = conductivity_model + + def test_misfit(self): + passed = tests.check_derivative( + lambda m: [self.p.dpred(m), lambda mx: self.p.Jvec(self.m0, mx)], + self.m0, + plotIt=False, + num=3, + ) + self.assertTrue(passed) + + def test_adjoint(self): + # Adjoint Test + v = np.random.rand(len(self.m0)) + w = np.random.rand(self.survey.nD) + # J = self.p.getJ(self.m0) + 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 + print("Adjoint Test", np.abs(wtJv - vtJtw), passed) + self.assertTrue(passed) + + def test_dataObj(self): + passed = tests.check_derivative( + lambda m: [self.dmis(m), self.dmis.deriv(m)], self.m0, plotIt=False, num=3 + ) + self.assertTrue(passed) + + class IPProblemTestsCC(unittest.TestCase): def setUp(self): aSpacing = 2.5 @@ -26,7 +146,7 @@ def setUp(self): surveySize = nElecs * aSpacing - aSpacing cs = surveySize / nElecs / 4 - mesh = discretize.TensorMesh( + mesh = ds.TensorMesh( [ [(cs, 10, -1.3), (cs, surveySize / cs), (cs, 10, 1.3)], [(cs, 3, -1.3), (cs, 3, 1.3)], @@ -96,7 +216,7 @@ def setUp(self): surveySize = nElecs * aSpacing - aSpacing cs = surveySize / nElecs / 4 - mesh = discretize.TensorMesh( + mesh = ds.TensorMesh( [ [(cs, 10, -1.3), (cs, surveySize / cs), (cs, 10, 1.3)], [(cs, 3, -1.3), (cs, 3, 1.3)], @@ -165,7 +285,7 @@ def setUp(self): surveySize = nElecs * aSpacing - aSpacing cs = surveySize / nElecs / 4 - mesh = discretize.TensorMesh( + mesh = ds.TensorMesh( [ [(cs, 10, -1.3), (cs, surveySize / cs), (cs, 10, 1.3)], [(cs, 3, -1.3), (cs, 3, 1.3)], @@ -245,7 +365,7 @@ def setUp(self): surveySize = nElecs * aSpacing - aSpacing cs = surveySize / nElecs / 4 - mesh = discretize.TensorMesh( + mesh = ds.TensorMesh( [ [(cs, 10, -1.3), (cs, surveySize / cs), (cs, 10, 1.3)], [(cs, 3, -1.3), (cs, 3, 1.3)], diff --git a/tests/dask/test_grav_inversion_linear.py b/tests/dask/test_grav_inversion_linear.py index 9e91433d36..df68680167 100644 --- a/tests/dask/test_grav_inversion_linear.py +++ b/tests/dask/test_grav_inversion_linear.py @@ -3,8 +3,8 @@ import discretize from discretize.utils import active_from_xyz import dask -import SimPEG.dask # noqa: F401 -from SimPEG import ( +import simpeg.dask # noqa: F401 +from simpeg import ( utils, maps, regularization, @@ -14,7 +14,7 @@ directives, inversion, ) -from SimPEG.potential_fields import gravity +from simpeg.potential_fields import gravity import shutil @@ -105,7 +105,7 @@ def setUp(self): # Here is where the norms are applied IRLS = directives.Update_IRLS(max_irls_iterations=20, chifact_start=2.0) update_Jacobi = directives.UpdatePreconditioner() - sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) + sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) self.inv = inversion.BaseInversion( invProb, directiveList=[IRLS, sensitivity_weights, update_Jacobi] ) diff --git a/tests/dask/test_mag_MVI_Octree.py b/tests/dask/test_mag_MVI_Octree.py index f34e210287..cc3984f2ab 100644 --- a/tests/dask/test_mag_MVI_Octree.py +++ b/tests/dask/test_mag_MVI_Octree.py @@ -1,6 +1,6 @@ import unittest -import SimPEG.dask # noqa: F401 -from SimPEG import ( +import simpeg.dask # noqa: F401 +from simpeg import ( directives, maps, inverse_problem, @@ -14,14 +14,14 @@ from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz import numpy as np -from SimPEG.potential_fields import magnetics as mag +from simpeg.potential_fields import magnetics as mag import shutil class MVIProblemTest(unittest.TestCase): def setUp(self): np.random.seed(0) - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # The magnetization is set along a different # direction (induced + remanence) @@ -47,7 +47,12 @@ def setUp(self): # Create a MAGsurvey xyzLoc = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] rxLoc = mag.Point(xyzLoc) - srcField = mag.SourceField([rxLoc], parameters=H0) + srcField = mag.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) survey = mag.Survey(srcField) # Create a mesh @@ -71,7 +76,7 @@ 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.getIndicesBlock( + ind = utils.model_builder.get_indices_block( np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, @@ -112,17 +117,17 @@ def setUp(self): # Create three regularization for the different components # of magnetization - reg_p = regularization.Sparse(mesh, indActive=actv, mapping=wires.p) - reg_p.mref = np.zeros(3 * nC) + reg_p = regularization.Sparse(mesh, active_cells=actv, mapping=wires.p) + reg_p.reference_model = np.zeros(3 * nC) - reg_s = regularization.Sparse(mesh, indActive=actv, mapping=wires.s) - reg_s.mref = np.zeros(3 * nC) + reg_s = regularization.Sparse(mesh, active_cells=actv, mapping=wires.s) + reg_s.reference_model = np.zeros(3 * nC) - reg_t = regularization.Sparse(mesh, indActive=actv, mapping=wires.t) - reg_t.mref = np.zeros(3 * nC) + reg_t = regularization.Sparse(mesh, active_cells=actv, mapping=wires.t) + reg_t.reference_model = np.zeros(3 * nC) reg = reg_p + reg_s + reg_t - reg.mref = np.zeros(3 * nC) + reg.reference_model = np.zeros(3 * nC) # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=sim, data=data) @@ -147,7 +152,7 @@ def setUp(self): # Pre-conditioner update_Jacobi = directives.UpdatePreconditioner() - sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) + sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) inv = inversion.BaseInversion( invProb, directiveList=[sensitivity_weights, IRLS, update_Jacobi, betaest] ) @@ -166,24 +171,24 @@ def setUp(self): # Create a Combo Regularization # Regularize the amplitude of the vectors - reg_a = regularization.Sparse(mesh, indActive=actv, mapping=wires.amp) + reg_a = regularization.Sparse(mesh, active_cells=actv, mapping=wires.amp) reg_a.norms = [0.0, 0.0, 0.0, 0.0] # Sparse on the model and its gradients - reg_a.mref = np.zeros(3 * nC) + reg_a.reference_model = np.zeros(3 * nC) # Regularize the vertical angle of the vectors - reg_t = regularization.Sparse(mesh, indActive=actv, mapping=wires.theta) + reg_t = regularization.Sparse(mesh, active_cells=actv, mapping=wires.theta) reg_t.alpha_s = 0.0 # No reference angle reg_t.space = "spherical" reg_t.norms = [2.0, 0.0, 0.0, 0.0] # Only norm on gradients used # Regularize the horizontal angle of the vectors - reg_p = regularization.Sparse(mesh, indActive=actv, mapping=wires.phi) + reg_p = regularization.Sparse(mesh, active_cells=actv, mapping=wires.phi) reg_p.alpha_s = 0.0 # No reference angle reg_p.space = "spherical" reg_p.norms = [2.0, 0.0, 0.0, 0.0] # Only norm on gradients used reg = reg_a + reg_t + reg_p - reg.mref = np.zeros(3 * nC) + reg.reference_model = np.zeros(3 * nC) Lbound = np.kron(np.asarray([0, -np.inf, -np.inf]), np.ones(nC)) Ubound = np.kron(np.asarray([10, np.inf, np.inf]), np.ones(nC)) diff --git a/tests/dask/test_mag_inversion_linear_Octree.py b/tests/dask/test_mag_inversion_linear_Octree.py index 8e9115982d..0bf5c4b6e0 100644 --- a/tests/dask/test_mag_inversion_linear_Octree.py +++ b/tests/dask/test_mag_inversion_linear_Octree.py @@ -1,6 +1,6 @@ import unittest -import SimPEG.dask # noqa: F401 -from SimPEG import ( +import simpeg.dask # noqa: F401 +from simpeg import ( directives, maps, inverse_problem, @@ -15,7 +15,7 @@ import shutil -from SimPEG.potential_fields import magnetics as mag +from simpeg.potential_fields import magnetics as mag import numpy as np @@ -29,7 +29,7 @@ def setUp(self): # From old convention, field orientation is given as an # azimuth from North (positive clockwise) # and dip from the horizontal (positive downward). - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # Create a mesh h = [5, 5, 5] @@ -58,7 +58,12 @@ def setUp(self): # Create a MAGsurvey xyzLoc = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] rxLoc = mag.Point(xyzLoc) - srcField = mag.SourceField([rxLoc], parameters=H0) + srcField = mag.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) survey = mag.Survey(srcField) self.mesh = mesh_utils.mesh_builder_xyz( @@ -83,7 +88,7 @@ def setUp(self): # We can now create a susceptibility model and generate data # Lets start with a simple block in half-space - self.model = utils.model_builder.addBlock( + self.model = utils.model_builder.add_block( self.mesh.gridCC, np.zeros(self.mesh.nC), np.r_[-20, -20, -15], diff --git a/tests/dask/test_mag_nonLinear_Amplitude.py b/tests/dask/test_mag_nonLinear_Amplitude.py index a83216b7ad..eb1775c032 100644 --- a/tests/dask/test_mag_nonLinear_Amplitude.py +++ b/tests/dask/test_mag_nonLinear_Amplitude.py @@ -1,6 +1,6 @@ import numpy as np -import SimPEG.dask # noqa: F401 -from SimPEG import ( +import simpeg.dask # noqa: F401 +from simpeg import ( data, data_misfit, directives, @@ -11,9 +11,9 @@ regularization, ) -from SimPEG.potential_fields import magnetics -from SimPEG import utils -from SimPEG.utils import mkvc +from simpeg.potential_fields import magnetics +from simpeg import utils +from simpeg.utils import mkvc from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz import unittest import shutil @@ -22,7 +22,7 @@ class AmpProblemTest(unittest.TestCase): def setUp(self): # We will assume a vertical inducing field - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # The magnetization is set along a different direction (induced + remanence) M = np.array([45.0, 90.0]) @@ -47,8 +47,11 @@ def setUp(self): # Create a MAGsurvey rxLoc = np.c_[mkvc(X.T), mkvc(Y.T), mkvc(Z.T)] receiver_list = magnetics.receivers.Point(rxLoc) - srcField = magnetics.sources.SourceField( - receiver_list=[receiver_list], parameters=H0 + srcField = magnetics.sources.UniformBackgroundField( + receiver_list=[receiver_list], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, ) survey = magnetics.survey.Survey(srcField) @@ -76,14 +79,14 @@ def setUp(self): ) # Get the indicies of the magnetized block - ind = utils.model_builder.getIndicesBlock( + ind = utils.model_builder.get_indices_block( 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 + # be applied in by the :class:`simpeg.PF.Magnetics` problem model = np.zeros(mesh.nC) model[ind] = chi_e @@ -139,9 +142,9 @@ def setUp(self): # Create a regularization function, in this case l2l2 reg = regularization.Sparse( - mesh, indActive=surf, mapping=maps.IdentityMap(nP=nC), alpha_z=0 + mesh, active_cells=surf, mapping=maps.IdentityMap(nP=nC), alpha_z=0 ) - reg.mref = np.zeros(nC) + reg.reference_model = np.zeros(nC) # Specify how the optimization will proceed, set susceptibility bounds to inf opt = optimization.ProjectedGNCG( @@ -186,8 +189,11 @@ def setUp(self): # receiver_list = magnetics.receivers.Point(rxLoc, components=["bx", "by", "bz"]) - srcField = magnetics.sources.SourceField( - receiver_list=[receiver_list], parameters=H0 + srcField = magnetics.sources.UniformBackgroundField( + receiver_list=[receiver_list], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, ) surveyAmp = magnetics.survey.Survey(srcField) @@ -229,9 +235,9 @@ def setUp(self): data_obj = data.Data(survey, dobs=bAmp, noise_floor=wd) # Create a sparse regularization - reg = regularization.Sparse(mesh, indActive=actv, mapping=idenMap) + reg = regularization.Sparse(mesh, active_cells=actv, mapping=idenMap) reg.norms = [1, 0, 0, 0] - reg.mref = np.zeros(nC) + reg.reference_model = np.zeros(nC) # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_obj) diff --git a/tests/em/em1d/test_EM1D_FD_fwd.py b/tests/em/em1d/test_EM1D_FD_fwd.py index f7c79ebaa5..8a62a1398f 100644 --- a/tests/em/em1d/test_EM1D_FD_fwd.py +++ b/tests/em/em1d/test_EM1D_FD_fwd.py @@ -1,6 +1,6 @@ import unittest -import SimPEG.electromagnetics.frequency_domain as fdem -from SimPEG import maps +import simpeg.electromagnetics.frequency_domain as fdem +from simpeg import maps import numpy as np from scipy.constants import mu_0 from geoana.em.fdem import ( diff --git a/tests/em/em1d/test_EM1D_FD_jac_layers.py b/tests/em/em1d/test_EM1D_FD_jac_layers.py index ee9221a0a6..432b647a64 100644 --- a/tests/em/em1d/test_EM1D_FD_jac_layers.py +++ b/tests/em/em1d/test_EM1D_FD_jac_layers.py @@ -1,7 +1,7 @@ import unittest -from SimPEG import maps +from simpeg import maps from discretize import tests, TensorMesh -import SimPEG.electromagnetics.frequency_domain as fdem +import simpeg.electromagnetics.frequency_domain as fdem import numpy as np from scipy.constants import mu_0 @@ -112,8 +112,11 @@ 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 - derChk = lambda m: [fwdfun(m), lambda mx: jacfun(m, mx)] + passed = tests.check_derivative( derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 ) @@ -152,11 +155,15 @@ def test_EM1DFDJtvec_Layers(self): def misfit(m, dobs): dpred = self.sim.dpred(m) - misfit = 0.5 * np.linalg.norm(dpred - dobs) ** 2 - dmisfit = self.sim.Jtvec(m, dr) + 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 - derChk = lambda m: misfit(m, dobs) + def derChk(m): + return misfit(m, dobs) + passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) self.assertTrue(passed) if passed: @@ -266,8 +273,11 @@ 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 - derChk = lambda m: [fwdfun(m), lambda mx: jacfun(m, mx)] + passed = tests.check_derivative( derChk, m_1D, num=4, dx=dm, plotIt=False, eps=1e-15 ) @@ -306,11 +316,15 @@ def test_EM1DFDJtvec_Layers(self): def misfit(m, dobs): dpred = self.sim.dpred(m) - misfit = 0.5 * np.linalg.norm(dpred - dobs) ** 2 - dmisfit = self.sim.Jtvec(m, dr) + 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 - derChk = lambda m: misfit(m, dobs) + def derChk(m): + return misfit(m, dobs) + passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) self.assertTrue(passed) if passed: @@ -401,7 +415,10 @@ def jacfun(m, dm): return Jvec dm = m_1D * 0.5 - derChk = lambda m: [fwdfun(m), lambda mx: jacfun(m, mx)] + + 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 ) @@ -437,11 +454,15 @@ def test_EM1DFDJtvec_Layers(self): def misfit(m, dobs): dpred = self.sim.dpred(m) - misfit = 0.5 * np.linalg.norm(dpred - dobs) ** 2 - dmisfit = self.sim.Jtvec(m, dr) + 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 - derChk = lambda m: misfit(m, dobs) + def derChk(m): + return misfit(m, dobs) + passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) self.assertTrue(passed) if passed: diff --git a/tests/em/em1d/test_EM1D_TD_dual_moment_fwd.py b/tests/em/em1d/test_EM1D_TD_dual_moment_fwd.py index f457765c26..f5e3252857 100644 --- a/tests/em/em1d/test_EM1D_TD_dual_moment_fwd.py +++ b/tests/em/em1d/test_EM1D_TD_dual_moment_fwd.py @@ -1,8 +1,7 @@ import unittest -from SimPEG import maps -import SimPEG.electromagnetics.time_domain as tdem -import numpy as np -from SimPEG.electromagnetics.utils import convolve_with_waveform +from simpeg import maps +import simpeg.electromagnetics.time_domain as tdem +from simpeg.electromagnetics.utils import convolve_with_waveform from geoana.em.tdem import ( vertical_magnetic_flux_time_deriv_horizontal_loop as dbdt_loop, ) diff --git a/tests/em/em1d/test_EM1D_TD_general_fwd.py b/tests/em/em1d/test_EM1D_TD_general_fwd.py index 6ea353b9cb..1d4daf5c6d 100644 --- a/tests/em/em1d/test_EM1D_TD_general_fwd.py +++ b/tests/em/em1d/test_EM1D_TD_general_fwd.py @@ -1,7 +1,7 @@ import unittest -from SimPEG import maps -import SimPEG.electromagnetics.time_domain as tdem -from SimPEG.electromagnetics.utils import convolve_with_waveform +from simpeg import maps +import simpeg.electromagnetics.time_domain as tdem +from simpeg.electromagnetics.utils import convolve_with_waveform from geoana.em.tdem import ( vertical_magnetic_flux_horizontal_loop as b_loop, vertical_magnetic_flux_time_deriv_horizontal_loop as dbdt_loop, 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 78bff95ecb..6cce1655aa 100644 --- a/tests/em/em1d/test_EM1D_TD_general_jac_layers.py +++ b/tests/em/em1d/test_EM1D_TD_general_jac_layers.py @@ -1,8 +1,8 @@ import unittest -from SimPEG import maps +from simpeg import maps from discretize import tests import numpy as np -import SimPEG.electromagnetics.time_domain as tdem +import simpeg.electromagnetics.time_domain as tdem class EM1D_TD_general_Jac_layers_ProblemTests(unittest.TestCase): @@ -80,7 +80,10 @@ def jacfun(m, dm): return Jvec dm = m_1D * 0.5 - derChk = lambda m: [fwdfun(m), lambda mx: jacfun(m, mx)] + + 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 ) @@ -112,7 +115,9 @@ def misfit(m, dobs): dmisfit = sim.Jtvec(m, dr) return misfit, dmisfit - derChk = lambda m: misfit(m, dobs) + def derChk(m): + return misfit(m, dobs) + passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-26) self.assertTrue(passed) @@ -255,7 +260,10 @@ def jacfun(m, dm): return Jvec dm = m_1D * 0.5 - derChk = lambda m: [fwdfun(m), lambda mx: jacfun(m, mx)] + + 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 ) @@ -286,7 +294,9 @@ def misfit(m, dobs): dmisfit = sim.Jtvec(m, dr) return misfit, dmisfit - derChk = lambda m: misfit(m, dobs) + def derChk(m): + return misfit(m, dobs) + passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-26) self.assertTrue(passed) diff --git a/tests/em/em1d/test_EM1D_TD_off_fwd.py b/tests/em/em1d/test_EM1D_TD_off_fwd.py index 78b45f61ab..8a1078b02a 100644 --- a/tests/em/em1d/test_EM1D_TD_off_fwd.py +++ b/tests/em/em1d/test_EM1D_TD_off_fwd.py @@ -1,7 +1,7 @@ import unittest import numpy as np -from SimPEG import maps -import SimPEG.electromagnetics.time_domain as tdem +from simpeg import maps +import simpeg.electromagnetics.time_domain as tdem from scipy.constants import mu_0 from geoana.em.tdem import ( magnetic_flux_vertical_magnetic_dipole as b_dipole, 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 56d4a02b60..c985b8e758 100644 --- a/tests/em/em1d/test_EM1D_TD_off_jac_layers.py +++ b/tests/em/em1d/test_EM1D_TD_off_jac_layers.py @@ -1,7 +1,7 @@ import unittest -from SimPEG import maps +from simpeg import maps from discretize import tests, TensorMesh -import SimPEG.electromagnetics.time_domain as tdem +import simpeg.electromagnetics.time_domain as tdem import numpy as np from scipy.constants import mu_0 @@ -111,7 +111,10 @@ def jacfun(m, dm): return Jvec dm = m_1D * 0.5 - derChk = lambda m: [fwdfun(m), lambda mx: jacfun(m, mx)] + + 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 ) @@ -154,7 +157,9 @@ def misfit(m, dobs): dmisfit = self.sim.Jtvec(m, dr) return misfit, dmisfit - derChk = lambda m: misfit(m, dobs) + def derChk(m): + return misfit(m, dobs) + passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) self.assertTrue(passed) if passed: @@ -266,7 +271,10 @@ def jacfun(m, dm): return Jvec dm = m_1D * 0.5 - derChk = lambda m: [fwdfun(m), lambda mx: jacfun(m, mx)] + + 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 ) @@ -309,7 +317,9 @@ def misfit(m, dobs): dmisfit = self.sim.Jtvec(m, dr) return misfit, dmisfit - derChk = lambda m: misfit(m, dobs) + def derChk(m): + return misfit(m, dobs) + passed = tests.check_derivative(derChk, m_ini, num=4, plotIt=False, eps=1e-27) self.assertTrue(passed) if passed: diff --git a/tests/em/fdem/forward/test_FDEM_analytics.py b/tests/em/fdem/forward/test_FDEM_analytics.py index c021251b5c..afda665b43 100644 --- a/tests/em/fdem/forward/test_FDEM_analytics.py +++ b/tests/em/fdem/forward/test_FDEM_analytics.py @@ -5,9 +5,9 @@ import numpy as np import scipy.sparse as sp from scipy.constants import mu_0 -from SimPEG import SolverLU, utils -from SimPEG.electromagnetics import analytics -from SimPEG.electromagnetics import frequency_domain as fdem +from simpeg import SolverLU, utils +from simpeg.electromagnetics import analytics +from simpeg.electromagnetics import frequency_domain as fdem # import matplotlib # matplotlib.use('Agg') diff --git a/tests/em/fdem/forward/test_FDEM_casing.py b/tests/em/fdem/forward/test_FDEM_casing.py index 549b66109e..a0ceffc3c8 100644 --- a/tests/em/fdem/forward/test_FDEM_casing.py +++ b/tests/em/fdem/forward/test_FDEM_casing.py @@ -1,6 +1,6 @@ -from SimPEG import tests, utils +from simpeg import tests, utils import numpy as np -import SimPEG.electromagnetics.analytics.FDEMcasing as Casing +import simpeg.electromagnetics.analytics.FDEMcasing as Casing import unittest from scipy.constants import mu_0 @@ -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,24 @@ def CasingMagDipole2Deriv_z_z(z): class Casing_DerivTest(unittest.TestCase): def test_derivs(self): + rng = np.random.default_rng(seed=42) + + np.random.seed(1983) # set a random seed for check_derivative tests.check_derivative( - CasingMagDipoleDeriv_r, np.ones(n) * 10 + np.random.randn(n), plotIt=False + CasingMagDipoleDeriv_r, np.ones(n) * 10 + rng.normal(size=n), plotIt=False ) - tests.check_derivative(CasingMagDipoleDeriv_z, np.random.randn(n), plotIt=False) + + np.random.seed(1983) # set a random seed for check_derivative + tests.check_derivative(CasingMagDipoleDeriv_z, rng.normal(size=n), plotIt=False) + + np.random.seed(1983) # set a random seed for check_derivative tests.check_derivative( CasingMagDipole2Deriv_z_r, - np.ones(n) * 10 + np.random.randn(n), + np.ones(n) * 10 + rng.normal(size=n), plotIt=False, ) + + np.random.seed(1983) # set a random seed for check_derivative tests.check_derivative( - CasingMagDipole2Deriv_z_z, np.random.randn(n), plotIt=False + CasingMagDipole2Deriv_z_z, rng.normal(size=n), plotIt=False ) diff --git a/tests/em/fdem/forward/test_FDEM_forward.py b/tests/em/fdem/forward/test_FDEM_forward.py index 111edac6df..be81c2891e 100644 --- a/tests/em/fdem/forward/test_FDEM_forward.py +++ b/tests/em/fdem/forward/test_FDEM_forward.py @@ -1,9 +1,9 @@ import numpy as np import pytest -from SimPEG.electromagnetics import frequency_domain as fdem -from SimPEG.electromagnetics import time_domain as tdem -from SimPEG.electromagnetics.utils.testing_utils import crossCheckTest +from simpeg.electromagnetics import frequency_domain as fdem +from simpeg.electromagnetics import time_domain as tdem +from simpeg.electromagnetics.utils.testing_utils import crossCheckTest testEB = True testHJ = True diff --git a/tests/em/fdem/forward/test_FDEM_forwardEJHB.py b/tests/em/fdem/forward/test_FDEM_forwardEJHB.py index e7dd8aeb7f..c8c21957cf 100644 --- a/tests/em/fdem/forward/test_FDEM_forwardEJHB.py +++ b/tests/em/fdem/forward/test_FDEM_forwardEJHB.py @@ -1,5 +1,5 @@ import unittest -from SimPEG.electromagnetics.utils.testing_utils import crossCheckTest +from simpeg.electromagnetics.utils.testing_utils import crossCheckTest testEJ = True testBH = True diff --git a/tests/em/fdem/forward/test_FDEM_forwardHB.py b/tests/em/fdem/forward/test_FDEM_forwardHB.py index ae98a1ec3e..51c04608c2 100644 --- a/tests/em/fdem/forward/test_FDEM_forwardHB.py +++ b/tests/em/fdem/forward/test_FDEM_forwardHB.py @@ -1,5 +1,5 @@ import unittest -from SimPEG.electromagnetics.utils.testing_utils import crossCheckTest +from simpeg.electromagnetics.utils.testing_utils import crossCheckTest testEB = True testHJ = True @@ -306,7 +306,7 @@ def test_BH_CrossCheck_hzi(self): if testBH: - def test_BH_CrossCheck_jxr(self): + def test_BH_CrossCheck_jxr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -318,7 +318,7 @@ def test_BH_CrossCheck_jxr(self): ) ) - def test_BH_CrossCheck_jyr(self): + def test_BH_CrossCheck_jyr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -330,7 +330,7 @@ def test_BH_CrossCheck_jyr(self): ) ) - def test_BH_CrossCheck_jzr(self): + def test_BH_CrossCheck_jzr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -342,7 +342,7 @@ def test_BH_CrossCheck_jzr(self): ) ) - def test_BH_CrossCheck_jxi(self): + def test_BH_CrossCheck_jxi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -354,7 +354,7 @@ def test_BH_CrossCheck_jxi(self): ) ) - def test_BH_CrossCheck_jyi(self): + def test_BH_CrossCheck_jyi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -366,7 +366,7 @@ def test_BH_CrossCheck_jyi(self): ) ) - def test_BH_CrossCheck_jzi(self): + def test_BH_CrossCheck_jzi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -378,7 +378,7 @@ def test_BH_CrossCheck_jzi(self): ) ) - def test_BH_CrossCheck_exr(self): + def test_BH_CrossCheck_exr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -390,7 +390,7 @@ def test_BH_CrossCheck_exr(self): ) ) - def test_BH_CrossCheck_eyr(self): + def test_BH_CrossCheck_eyr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -402,7 +402,7 @@ def test_BH_CrossCheck_eyr(self): ) ) - def test_BH_CrossCheck_ezr(self): + def test_BH_CrossCheck_ezr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -414,7 +414,7 @@ def test_BH_CrossCheck_ezr(self): ) ) - def test_BH_CrossCheck_exi(self): + def test_BH_CrossCheck_exi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -426,7 +426,7 @@ def test_BH_CrossCheck_exi(self): ) ) - def test_BH_CrossCheck_eyi(self): + def test_BH_CrossCheck_eyi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -438,7 +438,7 @@ def test_BH_CrossCheck_eyi(self): ) ) - def test_BH_CrossCheck_ezi(self): + def test_BH_CrossCheck_ezi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -450,7 +450,7 @@ def test_BH_CrossCheck_ezi(self): ) ) - def test_BH_CrossCheck_bxr(self): + def test_BH_CrossCheck_bxr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -462,7 +462,7 @@ def test_BH_CrossCheck_bxr(self): ) ) - def test_BH_CrossCheck_byr(self): + def test_BH_CrossCheck_byr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -474,7 +474,7 @@ def test_BH_CrossCheck_byr(self): ) ) - def test_BH_CrossCheck_bzr(self): + def test_BH_CrossCheck_bzr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -486,7 +486,7 @@ def test_BH_CrossCheck_bzr(self): ) ) - def test_BH_CrossCheck_bxi(self): + def test_BH_CrossCheck_bxi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -498,7 +498,7 @@ def test_BH_CrossCheck_bxi(self): ) ) - def test_BH_CrossCheck_byi(self): + def test_BH_CrossCheck_byi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -510,7 +510,7 @@ def test_BH_CrossCheck_byi(self): ) ) - def test_BH_CrossCheck_bzi(self): + def test_BH_CrossCheck_bzi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -522,7 +522,7 @@ def test_BH_CrossCheck_bzi(self): ) ) - def test_BH_CrossCheck_hxr(self): + def test_BH_CrossCheck_hxr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -534,7 +534,7 @@ def test_BH_CrossCheck_hxr(self): ) ) - def test_BH_CrossCheck_hyr(self): + def test_BH_CrossCheck_hyr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -546,7 +546,7 @@ def test_BH_CrossCheck_hyr(self): ) ) - def test_BH_CrossCheck_hzr(self): + def test_BH_CrossCheck_hzr(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -558,7 +558,7 @@ def test_BH_CrossCheck_hzr(self): ) ) - def test_BH_CrossCheck_hxi(self): + def test_BH_CrossCheck_hxi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -570,7 +570,7 @@ def test_BH_CrossCheck_hxi(self): ) ) - def test_BH_CrossCheck_hyi(self): + def test_BH_CrossCheck_hyi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, @@ -582,7 +582,7 @@ def test_BH_CrossCheck_hyi(self): ) ) - def test_BH_CrossCheck_hzi(self): + def test_BH_CrossCheck_hzi(self): # noqa F811 self.assertTrue( crossCheckTest( SrcList, diff --git a/tests/em/fdem/forward/test_FDEM_primsec.py b/tests/em/fdem/forward/test_FDEM_primsec.py index ce8a432b1d..a4e4494871 100644 --- a/tests/em/fdem/forward/test_FDEM_primsec.py +++ b/tests/em/fdem/forward/test_FDEM_primsec.py @@ -2,8 +2,8 @@ # matplotlib.use('Agg') import discretize -from SimPEG import maps, tests, utils -from SimPEG.electromagnetics import frequency_domain as fdem +from simpeg import maps, tests, utils +from simpeg.electromagnetics import frequency_domain as fdem from pymatsolver import Pardiso as Solver @@ -16,8 +16,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,6 +126,7 @@ def fun(x): lambda x: self.secondarySimulation.Jvec(x0, x, f=self.fields_primsec), ] + np.random.seed(1983) # set a random seed for check_derivative return tests.check_derivative(fun, x0, num=2, plotIt=False) def AdjointTest(self): @@ -135,8 +134,9 @@ def AdjointTest(self): 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)) diff --git a/tests/em/fdem/forward/test_FDEM_sources.py b/tests/em/fdem/forward/test_FDEM_sources.py index a5eb74d305..7ba8de8170 100644 --- a/tests/em/fdem/forward/test_FDEM_sources.py +++ b/tests/em/fdem/forward/test_FDEM_sources.py @@ -6,8 +6,8 @@ import numpy as np from geoana.em.static import MagneticDipoleWholeSpace from scipy.constants import mu_0 -from SimPEG import maps, utils -from SimPEG.electromagnetics import frequency_domain as fdem +from simpeg import maps, utils +from simpeg.electromagnetics import frequency_domain as fdem TOL = 0.5 # relative tolerance (to norm of soln) plotIt = False @@ -225,26 +225,6 @@ def test_MagDipole_bPrimaryMu50_b(self): ) assert self.bPrimaryTest(src, "b") - def test_MagDipole_bPrimaryMu0_h(self): - src = fdem.sources.MagDipole( - [], - frequency=self.frequency, - location=self.location, - orientation="Z", - mu=mu_0, - ) - assert self.bPrimaryTest(src, "h") - - def test_MagDipole_bPrimaryMu50_h(self): - src = fdem.sources.MagDipole( - [], - frequency=self.frequency, - location=self.location, - orientation="Z", - mu=50.0 * mu_0, - ) - assert self.bPrimaryTest(src, "h") - def test_MagDipole_bPrimaryMu0_h(self): src = fdem.sources.MagDipole( [], @@ -307,26 +287,6 @@ def test_MagDipole_Bfield_bPrimaryMu50_b(self): ) assert self.bPrimaryTest(src, "b") - def test_MagDipole_Bfield_bPrimaryMu0_h(self): - src = fdem.sources.MagDipole_Bfield( - [], - frequency=self.frequency, - location=self.location, - orientation="Z", - mu=mu_0, - ) - assert self.bPrimaryTest(src, "h") - - def test_MagDipole_Bfield_bPrimaryMu50_h(self): - src = fdem.sources.MagDipole_Bfield( - [], - frequency=self.frequency, - location=self.location, - orientation="Z", - mu=50.0 * mu_0, - ) - assert self.bPrimaryTest(src, "h") - def test_MagDipole_Bfield_bPrimaryMu0_h(self): src = fdem.sources.MagDipole_Bfield( [], @@ -393,28 +353,6 @@ def test_CircularLoop_bPrimaryMu50_b(self): ) assert self.bPrimaryTest(src, "b") - def test_CircularLoop_bPrimaryMu0_h(self): - src = fdem.sources.CircularLoop( - [], - frequency=self.frequency, - radius=np.sqrt(1 / np.pi), - location=self.location, - orientation="Z", - mu=mu_0, - ) - assert self.bPrimaryTest(src, "h") - - def test_CircularLoop_bPrimaryMu50_h(self): - src = fdem.sources.CircularLoop( - [], - frequency=self.frequency, - radius=np.sqrt(1 / np.pi), - location=self.location, - orientation="Z", - mu=50.0 * mu_0, - ) - assert self.bPrimaryTest(src, "h") - def test_CircularLoop_bPrimaryMu0_h(self): src = fdem.sources.CircularLoop( [], @@ -438,21 +376,22 @@ def test_CircularLoop_bPrimaryMu50_h(self): assert self.bPrimaryTest(src, "j") -def test_CircularLoop_test_N_assign(): +def test_removal_circular_loop_n(): """ - Test depreciation of the N argument (now n_turns) + Test if passing the N argument to CircularLoop raises an error """ - src = 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, - ) - assert src.n_turns == 2 + 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(): diff --git a/tests/em/fdem/forward/test_permittivity.py b/tests/em/fdem/forward/test_permittivity.py new file mode 100644 index 0000000000..2ef4abd272 --- /dev/null +++ b/tests/em/fdem/forward/test_permittivity.py @@ -0,0 +1,370 @@ +import pytest + +import numpy as np +from scipy.constants import epsilon_0 + +import geoana +import discretize +from simpeg.electromagnetics import frequency_domain as fdem +from pymatsolver import Pardiso + + +# set up the mesh +hx = 1 +hz = 1 +nx = 50 +nz = int(2 * nx) + 1 +npad = 20 +pf = 1.3 + +mesh = discretize.CylindricalMesh( + [[(hx, nx), (hx, npad, pf)], 1, [(hz, npad, -pf), (hz, nz), (hz, npad, pf)]], + x0="00C", +) + +sigma = 1e-2 +conductivity = sigma * np.ones(mesh.n_cells) +epsilon_r_list = [0, 1, 1e3, 1e4, 1e5, 1e6] +epsilon_list = [epsilon_0 * e_r for e_r in epsilon_r_list] +frequency_list = [50, 100] + +threshold = 1e-1 + + +def get_inds(val, x, z): + if len(val) == mesh.n_faces: + grid = mesh.faces + elif len(val) == mesh.n_edges: + grid = mesh.edges + + return ( + (grid[:, 0] > x.min()) + & (grid[:, 0] < x.max()) + & (grid[:, 2] > z.min()) + & (grid[:, 2] < z.max()) + ) + + +def print_comparison( + numeric, + analytic, + x=np.r_[50, 100], + z=np.r_[-100, 100], + threshold=threshold, + name1="numeric", + name2="analytic", +): + inds = get_inds(numeric, x, z) + results = [] + for component in ["real", "imag"]: + numeric_norm = np.linalg.norm(np.abs(getattr(numeric[inds], component))) + analytic_norm = np.linalg.norm(np.abs(getattr(analytic[inds], component))) + difference = np.linalg.norm( + np.abs( + getattr(analytic[inds], component) - getattr(numeric[inds], component) + ) + ) + print(f"{component} {name1:12s} : {numeric_norm:1.4e}") + print(f"{component} {name2:12s} : {analytic_norm:1.4e}") + print(f"{component} {'difference':12s} : {difference:1.4e}\n") + results.append(difference / np.mean([numeric_norm, analytic_norm]) < threshold) + print(results) + return results + + +@pytest.mark.parametrize("epsilon", epsilon_list) +@pytest.mark.parametrize("frequency", frequency_list) +@pytest.mark.parametrize( + "simulation", + [ + lambda survey, epsilon: fdem.Simulation3DElectricField( + mesh, + survey=survey, + forward_only=True, + sigma=conductivity, + permittivity=epsilon, + solver=Pardiso, + ), + lambda survey, epsilon: fdem.Simulation3DMagneticFluxDensity( + mesh, + survey=survey, + forward_only=True, + sigma=conductivity, + permittivity=epsilon, + solver=Pardiso, + ), + ], +) +def test_mag_dipole(epsilon, frequency, simulation): + sources = [fdem.sources.MagDipole([], frequency, location=np.r_[0, 0, 0])] + survey = fdem.Survey(sources) + sim = simulation(survey, epsilon) + fields = sim.fields() + + analytic_bdipole = geoana.em.fdem.MagneticDipoleWholeSpace( + sigma=sigma, epsilon=epsilon, frequency=frequency, orientation="Z" + ) + analytics = { + "b": np.hstack( + [ + analytic_bdipole.magnetic_flux_density(mesh.faces_x)[:, 0], + analytic_bdipole.magnetic_flux_density(mesh.faces_z)[:, 2], + ] + ), + "e": analytic_bdipole.electric_field(mesh.edges_y)[:, 1], + "h": np.hstack( + [ + analytic_bdipole.magnetic_field(mesh.faces_x)[:, 0], + analytic_bdipole.magnetic_field(mesh.faces_z)[:, 2], + ] + ), + "j": analytic_bdipole.current_density(mesh.edges_y)[:, 1], + } + + for f, analytic in analytics.items(): + print(f"Testing Mag dipole: {f}") + test = print_comparison(fields[:, f].squeeze(), analytic) + assert np.all(test) + + +@pytest.mark.parametrize("epsilon", epsilon_list) +@pytest.mark.parametrize("frequency", frequency_list) +@pytest.mark.parametrize( + "simulation", + [ + lambda survey, epsilon: fdem.Simulation3DCurrentDensity( + mesh, + survey=survey, + forward_only=True, + sigma=conductivity, + permittivity=epsilon, + solver=Pardiso, + ), + lambda survey, epsilon: fdem.Simulation3DMagneticField( + mesh, + survey=survey, + forward_only=True, + sigma=conductivity, + permittivity=epsilon, + solver=Pardiso, + ), + ], +) +def test_e_dipole(epsilon, frequency, simulation): + sources = [ + fdem.sources.LineCurrent( + [], frequency, location=np.array([[0, 0, 1], [0, 0, -1]]), current=1 / 2 + ) + ] + survey = fdem.Survey(sources) + sim = simulation(survey, epsilon) + fields = sim.fields() + + analytic_edipole = geoana.em.fdem.ElectricDipoleWholeSpace( + sigma=sigma, epsilon=epsilon, frequency=frequency, orientation="Z" + ) + analytics = { + "j": np.hstack( + [ + analytic_edipole.current_density(mesh.faces_x)[:, 0], + analytic_edipole.current_density(mesh.faces_z)[:, 2], + ] + ), + "h": analytic_edipole.magnetic_field(mesh.edges_y)[:, 1], + "e": np.hstack( + [ + analytic_edipole.electric_field(mesh.faces_x)[:, 0], + analytic_edipole.electric_field(mesh.faces_z)[:, 2], + ] + ), + "b": analytic_edipole.magnetic_flux_density(mesh.edges_y)[:, 1], + } + + for f, analytic in analytics.items(): + print(f"Testing E dipole: {f}") + test = print_comparison(fields[:, f].squeeze(), analytic) + assert np.all(test) + + +@pytest.mark.parametrize("epsilon_r", epsilon_r_list) +@pytest.mark.parametrize("frequency", frequency_list) +def test_cross_check_e_dipole(epsilon_r, frequency): + sigma_back = 1e-2 + epsilon_r_back = 1 + + target_z = np.r_[-20, -40] + target_x = np.r_[0, 60] + + frequencies = [frequency] + + sigma = np.ones(mesh.n_cells) * sigma_back + rel_permittivity = np.ones(mesh.n_cells) * epsilon_r_back + + target_inds = ( + (mesh.cell_centers[:, 0] >= target_x.min()) + & (mesh.cell_centers[:, 0] <= target_x.max()) + & (mesh.cell_centers[:, 2] >= target_z.min()) + & (mesh.cell_centers[:, 2] <= target_z.max()) + ) + + rel_permittivity[target_inds] = epsilon_r + + # J-Formulation + sources_j_target = [ + fdem.sources.LineCurrent( + [], freq, location=np.array([[0, 0, 1], [0, 0, -1]]), current=1 / 2 + ) + for freq in frequencies + ] + survey_j_target = fdem.Survey(sources_j_target) + sim_j_target = fdem.Simulation3DCurrentDensity( + mesh, + survey=survey_j_target, + forward_only=True, + sigma=sigma, + permittivity=rel_permittivity * epsilon_0, + solver=Pardiso, + ) + + # H-formulation + sources_h_target = [ + fdem.sources.LineCurrent( + [], freq, location=np.array([[0, 0, 1], [0, 0, -1]]), current=1 / 2 + ) + for freq in frequencies + ] + survey_h_target = fdem.Survey(sources_h_target) + sim_h_target = fdem.Simulation3DMagneticField( + mesh, + survey=survey_h_target, + forward_only=True, + sigma=sigma, + permittivity=rel_permittivity * epsilon_0, + solver=Pardiso, + ) + + # compute fields + fields_j_target = sim_j_target.fields() + fields_h_target = sim_h_target.fields() + + j_comparison = print_comparison( + fields_j_target[:, "j"].squeeze(), + fields_h_target[:, "j"].squeeze(), + name1="J-formulation", + name2="H-formulation", + ) + assert np.all(j_comparison) + + h_comparison = print_comparison( + fields_j_target[:, "h"].squeeze(), + fields_h_target[:, "h"].squeeze(), + name1="J-formulation", + name2="H-formulation", + ) + assert np.all(h_comparison) + + e_comparison = print_comparison( + fields_j_target[:, "e"].squeeze(), + fields_h_target[:, "e"].squeeze(), + name1="J-formulation", + name2="H-formulation", + ) + assert np.all(e_comparison) + + b_comparison = print_comparison( + fields_j_target[:, "b"].squeeze(), + fields_h_target[:, "b"].squeeze(), + name1="J-formulation", + name2="H-formulation", + ) + assert np.all(b_comparison) + + +@pytest.mark.parametrize("epsilon_r", epsilon_r_list) +@pytest.mark.parametrize("frequency", frequency_list) +def test_cross_check_b_dipole(epsilon_r, frequency): + sigma_back = 1e-2 + epsilon_r_back = 1 + + target_z = np.r_[-20, -40] + target_x = np.r_[0, 60] + + frequencies = [frequency] + + sigma = np.ones(mesh.n_cells) * sigma_back + rel_permittivity = np.ones(mesh.n_cells) * epsilon_r_back + + target_inds = ( + (mesh.cell_centers[:, 0] >= target_x.min()) + & (mesh.cell_centers[:, 0] <= target_x.max()) + & (mesh.cell_centers[:, 2] >= target_z.min()) + & (mesh.cell_centers[:, 2] <= target_z.max()) + ) + + rel_permittivity[target_inds] = epsilon_r + + # B-Formulation + sources_b_target = [ + fdem.sources.MagDipole([], freq, location=np.r_[0, 0, 0]) + for freq in frequencies + ] + survey_b_target = fdem.Survey(sources_b_target) + sim_b_target = fdem.Simulation3DMagneticFluxDensity( + mesh, + survey=survey_b_target, + forward_only=True, + sigma=sigma, + permittivity=rel_permittivity * epsilon_0, + solver=Pardiso, + ) + + # E-formulation + sources_e_target = [ + fdem.sources.MagDipole([], freq, location=np.r_[0, 0, 0]) + for freq in frequencies + ] + survey_e_target = fdem.Survey(sources_e_target) + sim_e_target = fdem.Simulation3DElectricField( + mesh, + survey=survey_e_target, + forward_only=True, + sigma=sigma, + permittivity=rel_permittivity * epsilon_0, + solver=Pardiso, + ) + + # compute fields + fields_b_target = sim_b_target.fields() + fields_e_target = sim_e_target.fields() + + b_comparison = print_comparison( + fields_b_target[:, "b"].squeeze(), + fields_e_target[:, "b"].squeeze(), + name1="B-formulation", + name2="E-formulation", + ) + assert np.all(b_comparison) + + e_comparison = print_comparison( + fields_b_target[:, "e"].squeeze(), + fields_e_target[:, "e"].squeeze(), + name1="B-formulation", + name2="E-formulation", + ) + assert np.all(e_comparison) + + h_comparison = print_comparison( + fields_b_target[:, "h"].squeeze(), + fields_e_target[:, "h"].squeeze(), + name1="B-formulation", + name2="E-formulation", + ) + assert np.all(h_comparison) + + j_comparison = print_comparison( + fields_b_target[:, "j"].squeeze(), + fields_e_target[:, "j"].squeeze(), + name1="B-formulation", + name2="E-formulation", + ) + assert np.all(j_comparison) diff --git a/tests/em/fdem/forward/test_properties.py b/tests/em/fdem/forward/test_properties.py index aca1cca714..c583907f7e 100644 --- a/tests/em/fdem/forward/test_properties.py +++ b/tests/em/fdem/forward/test_properties.py @@ -1,22 +1,16 @@ import numpy as np import pytest -from SimPEG.electromagnetics import frequency_domain as fdem -from SimPEG.electromagnetics import time_domain as tdem +from simpeg.electromagnetics import frequency_domain as fdem +from simpeg.electromagnetics import time_domain as tdem -def test_receiver_properties_validation(): +def test_removed_projcomp(): + """Test if passing the removed `projComp` argument raises an error.""" xyz = np.c_[0.0, 0.0, 0.0] - projComp = "Fx" - rx = fdem.receivers.BaseRx(xyz, projComp=projComp) - - assert rx.projComp == projComp - - with pytest.raises(ValueError): - fdem.receivers.BaseRx(xyz, component="potato") - - with pytest.raises(TypeError): - fdem.receivers.BaseRx(xyz, component=2.0) + msg = "'projComp' property has been removed." + with pytest.raises(TypeError, match=msg): + fdem.receivers.BaseRx(xyz, projComp="foo") def test_source_properties_validation(): @@ -50,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 814cefebaa..260227a860 100644 --- a/tests/em/fdem/inverse/adjoint/test_FDEM_adjointEB.py +++ b/tests/em/fdem/inverse/adjoint/test_FDEM_adjointEB.py @@ -1,7 +1,7 @@ import unittest import numpy as np from scipy.constants import mu_0 -from SimPEG.electromagnetics.utils.testing_utils import getFDEMProblem +from simpeg.electromagnetics.utils.testing_utils import getFDEMProblem testE = True testB = True @@ -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 6225671fd2..bb0dcac83b 100644 --- a/tests/em/fdem/inverse/adjoint/test_FDEM_adjointHJ.py +++ b/tests/em/fdem/inverse/adjoint/test_FDEM_adjointHJ.py @@ -1,7 +1,7 @@ import unittest import numpy as np from scipy.constants import mu_0 -from SimPEG.electromagnetics.utils.testing_utils import getFDEMProblem +from simpeg.electromagnetics.utils.testing_utils import getFDEMProblem testJ = True testH = True @@ -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 dc02014c59..c30005ba36 100644 --- a/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py +++ b/tests/em/fdem/inverse/derivs/test_FDEM_derivs.py @@ -1,8 +1,8 @@ import unittest import numpy as np -from SimPEG import tests +from simpeg import tests from scipy.constants import mu_0 -from SimPEG.electromagnetics.utils.testing_utils import getFDEMProblem +from simpeg.electromagnetics.utils.testing_utils import getFDEMProblem testE = False testB = True @@ -29,19 +29,18 @@ 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) + np.random.seed(1983) # set a random seed for check_derivative return tests.check_derivative(fun, x0, num=2, plotIt=False, eps=FLR) diff --git a/tests/em/fdem/muinverse/test_muinverse.py b/tests/em/fdem/muinverse/test_muinverse.py index 0ebd99fd40..7e3841d396 100644 --- a/tests/em/fdem/muinverse/test_muinverse.py +++ b/tests/em/fdem/muinverse/test_muinverse.py @@ -1,6 +1,6 @@ import discretize -from SimPEG import maps, utils, tests -from SimPEG.electromagnetics import frequency_domain as fdem +from simpeg import maps, utils, tests +from simpeg.electromagnetics import frequency_domain as fdem import numpy as np import unittest @@ -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,11 @@ 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) + np.random.seed(1983) # set a random seed for check_derivative + return tests.check_derivative(fun, mod, dx=dx, num=4, plotIt=False) def JtvecTest( self, prbtype="ElectricField", sigmaInInversion=False, invertMui=False @@ -187,9 +190,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 68940b68cc..4af4dae02f 100644 --- a/tests/em/nsem/forward/test_1D_finite_volume.py +++ b/tests/em/nsem/forward/test_1D_finite_volume.py @@ -1,7 +1,7 @@ import numpy as np from discretize import TensorMesh -from SimPEG.electromagnetics import natural_source as nsem -from SimPEG import maps +from simpeg.electromagnetics import natural_source as nsem +from simpeg import maps from pymatsolver import Pardiso import unittest diff --git a/tests/em/nsem/forward/test_AnalyticFunctionVsAppResPhs.py b/tests/em/nsem/forward/test_AnalyticFunctionVsAppResPhs.py index df50db86fe..cab4338952 100644 --- a/tests/em/nsem/forward/test_AnalyticFunctionVsAppResPhs.py +++ b/tests/em/nsem/forward/test_AnalyticFunctionVsAppResPhs.py @@ -1,6 +1,6 @@ import unittest -from SimPEG.electromagnetics import natural_source as nsem -from SimPEG import discretize +from simpeg.electromagnetics import natural_source as nsem +from simpeg import discretize import numpy as np diff --git a/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py b/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py index 2568f37078..a2432f9135 100644 --- a/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py +++ b/tests/em/nsem/forward/test_Problem1D_AnalyticVsNumeric.py @@ -1,6 +1,6 @@ import unittest -from SimPEG import mkvc -from SimPEG.electromagnetics import natural_source as nsem +from simpeg import mkvc +from simpeg.electromagnetics import natural_source as nsem import numpy as np # Define the tolerances diff --git a/tests/em/nsem/forward/test_Problem1D_VsAnalyticHalfspace.py b/tests/em/nsem/forward/test_Problem1D_VsAnalyticHalfspace.py index 5e4e1cea8f..af847a3b7f 100644 --- a/tests/em/nsem/forward/test_Problem1D_VsAnalyticHalfspace.py +++ b/tests/em/nsem/forward/test_Problem1D_VsAnalyticHalfspace.py @@ -1,5 +1,5 @@ import unittest -from SimPEG.electromagnetics import natural_source as nsem +from simpeg.electromagnetics import natural_source as nsem import numpy as np # Define the tolerances diff --git a/tests/em/nsem/forward/test_Problem3D_VsAnalyticSolution.py b/tests/em/nsem/forward/test_Problem3D_VsAnalyticSolution.py index ea089c884a..5086c62d43 100644 --- a/tests/em/nsem/forward/test_Problem3D_VsAnalyticSolution.py +++ b/tests/em/nsem/forward/test_Problem3D_VsAnalyticSolution.py @@ -2,7 +2,7 @@ from scipy.constants import mu_0 import numpy as np -from SimPEG.electromagnetics import natural_source as nsem +from simpeg.electromagnetics import natural_source as nsem np.random.seed(1100) diff --git a/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py b/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py index 43936faf02..16395302a5 100644 --- a/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py +++ b/tests/em/nsem/forward/test_Recursive1D_VsAnalyticHalfspace.py @@ -1,6 +1,6 @@ import unittest -from SimPEG.electromagnetics import natural_source as nsem -from SimPEG import maps +from simpeg.electromagnetics import natural_source as nsem +from simpeg import maps import numpy as np from scipy.constants import mu_0 diff --git a/tests/em/nsem/inversion/test_BC_Sims.py b/tests/em/nsem/inversion/test_BC_Sims.py index 42602a2f32..10edb4ce3d 100644 --- a/tests/em/nsem/inversion/test_BC_Sims.py +++ b/tests/em/nsem/inversion/test_BC_Sims.py @@ -3,8 +3,8 @@ from scipy.constants import mu_0 from discretize.tests import check_derivative -from SimPEG.electromagnetics import natural_source as nsem -from SimPEG import maps +from simpeg.electromagnetics import natural_source as nsem +from simpeg import maps from discretize import TensorMesh, TreeMesh, CylindricalMesh from pymatsolver import Pardiso @@ -163,7 +163,7 @@ def create_simulation_2d(sim_type, deriv_type, mesh_type, fixed_boundary=False): right = np.where(b_e[:, 0] == mesh.nodes_x[-1]) h_bc = {} for src in survey_1d.source_list: - h_bc_freq = np.zeros(mesh.boundary_edges.shape[0], dtype=np.complex) + h_bc_freq = np.zeros(mesh.boundary_edges.shape[0], dtype=complex) h_bc_freq[top] = 1.0 h_bc_freq[right] = f_right[src, "h"][:, 0] h_bc_freq[left] = f_left[src, "h"][:, 0] @@ -214,7 +214,7 @@ def create_simulation_2d(sim_type, deriv_type, mesh_type, fixed_boundary=False): right = np.where(b_e[:, 0] == mesh.nodes_x[-1]) e_bc = {} for src in survey_1d.source_list: - e_bc_freq = np.zeros(mesh.boundary_edges.shape[0], dtype=np.complex) + e_bc_freq = np.zeros(mesh.boundary_edges.shape[0], dtype=complex) e_bc_freq[top] = 1.0 e_bc_freq[right] = f_right[src, "e"][:, 0] e_bc_freq[left] = f_left[src, "e"][:, 0] diff --git a/tests/em/nsem/inversion/test_Problem1D_Adjoint.py b/tests/em/nsem/inversion/test_Problem1D_Adjoint.py index 1dc3bed7c8..d3a9aaaafc 100644 --- a/tests/em/nsem/inversion/test_Problem1D_Adjoint.py +++ b/tests/em/nsem/inversion/test_Problem1D_Adjoint.py @@ -2,8 +2,8 @@ import unittest from scipy.constants import mu_0 -from SimPEG.electromagnetics import natural_source as nsem -from SimPEG import maps +from simpeg.electromagnetics import natural_source as nsem +from simpeg import maps TOL = 1e-4 diff --git a/tests/em/nsem/inversion/test_Problem1D_Derivs.py b/tests/em/nsem/inversion/test_Problem1D_Derivs.py index e9ef878392..733e5bda1f 100644 --- a/tests/em/nsem/inversion/test_Problem1D_Derivs.py +++ b/tests/em/nsem/inversion/test_Problem1D_Derivs.py @@ -1,8 +1,8 @@ import unittest import numpy as np from scipy.constants import mu_0 -from SimPEG import maps, tests -from SimPEG.electromagnetics import natural_source as nsem +from simpeg import maps, tests +from simpeg.electromagnetics import natural_source as nsem TOL = 1e-4 FLR = 1e-20 # "zero", so if residual below this --> pass regardless of order diff --git a/tests/em/nsem/inversion/test_Problem3D_Adjoint.py b/tests/em/nsem/inversion/test_Problem3D_Adjoint.py index 7351fc9ef0..ecafadd8d0 100644 --- a/tests/em/nsem/inversion/test_Problem3D_Adjoint.py +++ b/tests/em/nsem/inversion/test_Problem3D_Adjoint.py @@ -1,6 +1,6 @@ import numpy as np import unittest -from SimPEG.electromagnetics import natural_source as nsem +from simpeg.electromagnetics import natural_source as nsem from scipy.constants import mu_0 diff --git a/tests/em/nsem/inversion/test_Problem3D_Derivs.py b/tests/em/nsem/inversion/test_Problem3D_Derivs.py index 9d1be9da3a..9d4b332a06 100644 --- a/tests/em/nsem/inversion/test_Problem3D_Derivs.py +++ b/tests/em/nsem/inversion/test_Problem3D_Derivs.py @@ -1,8 +1,9 @@ # Test functions +import pytest import unittest import numpy as np -from SimPEG import tests, mkvc -from SimPEG.electromagnetics import natural_source as nsem +from simpeg import tests, mkvc +from simpeg.electromagnetics import natural_source as nsem from scipy.constants import mu_0 TOLr = 5e-2 @@ -12,6 +13,58 @@ MU = mu_0 +@pytest.fixture() +def model_simulation_tuple(): + return nsem.utils.test_utils.setupSimpegNSEM_PrimarySecondary( + nsem.utils.test_utils.halfSpace(1e-2), [0.1], comp="All", singleFreq=False + ) + + +# Test the Jvec derivative +@pytest.mark.parametrize("weights", [True, False]) +def test_Jtjdiag(model_simulation_tuple, weights): + model, simulation = model_simulation_tuple + W = None + if weights: + W = np.eye(simulation.survey.nD) + + J = simulation.getJ(model) + if weights: + J = W @ J + + Jtjdiag = simulation.getJtJdiag(model, W=W) + np.testing.assert_allclose(Jtjdiag, np.sum(J * J, axis=0)) + + +def test_Jtjdiag_clearing(model_simulation_tuple): + model, simulation = model_simulation_tuple + J1 = simulation.getJ(model) + Jtjdiag1 = simulation.getJtJdiag(model) + + m2 = model + 2 + J2 = simulation.getJ(m2) + Jtjdiag2 = simulation.getJtJdiag(m2) + + assert J1 is not J2 + assert Jtjdiag1 is not Jtjdiag2 + + +def test_Jmatrix(model_simulation_tuple): + model, simulation = model_simulation_tuple + rng = np.random.default_rng(4421) + # create random vector + vec = rng.standard_normal(simulation.survey.nD) + + # create the J matrix + J1 = simulation.getJ(model) + Jmatrix_vec = J1.T @ vec + + # compare to JTvec function + jtvec = simulation.Jtvec(model, v=vec) + + np.testing.assert_allclose(Jmatrix_vec, jtvec) + + # Test the Jvec derivative def DerivJvecTest(inputSetup, comp="All", freq=False, expMap=True): m, simulation = nsem.utils.test_utils.setupSimpegNSEM_PrimarySecondary( diff --git a/tests/em/nsem/inversion/test_complex_resistivity.py b/tests/em/nsem/inversion/test_complex_resistivity.py index 209cac986a..45cc6ef6cc 100644 --- a/tests/em/nsem/inversion/test_complex_resistivity.py +++ b/tests/em/nsem/inversion/test_complex_resistivity.py @@ -1,10 +1,10 @@ import unittest -# import SimPEG.dask as simpeg -from SimPEG import maps, tests +# import simpeg.dask as simpeg +from simpeg import maps, tests import discretize from discretize.utils import mkvc -from SimPEG.electromagnetics import natural_source as ns +from simpeg.electromagnetics import natural_source as ns import numpy as np from pymatsolver import Pardiso as Solver from discretize.utils import volume_average @@ -261,7 +261,7 @@ def check_deriv_adjoint(self, component, orientation): self.check_adjoint(sim3) self.check_deriv(sim4) self.check_adjoint(sim4) - print(f"... done") + print("... done") def test_apparent_resistivity_xx(self): self.check_deriv_adjoint("apparent_resistivity", "xx") diff --git a/tests/em/nsem/survey/test_nsem_data.py b/tests/em/nsem/survey/test_nsem_data.py index 35f15f51da..61babc3818 100644 --- a/tests/em/nsem/survey/test_nsem_data.py +++ b/tests/em/nsem/survey/test_nsem_data.py @@ -1,5 +1,5 @@ import numpy as np -from SimPEG.electromagnetics.natural_source.survey import Data +from simpeg.electromagnetics.natural_source.survey import Data class TestNSEMData: diff --git a/tests/em/nsem/utils/test_data_utils.py b/tests/em/nsem/utils/test_data_utils.py index f3ecf4a62d..533f327495 100644 --- a/tests/em/nsem/utils/test_data_utils.py +++ b/tests/em/nsem/utils/test_data_utils.py @@ -1,5 +1,5 @@ import numpy as np -from SimPEG.electromagnetics.natural_source.utils.data_utils import rec_to_ndarr +from simpeg.electromagnetics.natural_source.utils.data_utils import rec_to_ndarr def test_rec_to_ndarr(): diff --git a/tests/em/static/test_DCIP_io_utils.py b/tests/em/static/test_DCIP_io_utils.py index 76bdd99319..12bd6c6beb 100644 --- a/tests/em/static/test_DCIP_io_utils.py +++ b/tests/em/static/test_DCIP_io_utils.py @@ -3,10 +3,10 @@ import unittest import numpy as np -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.electromagnetics.static import utils -from SimPEG import data -from SimPEG.utils.io_utils import io_utils_electromagnetics as io_utils +from simpeg.electromagnetics import resistivity as dc +from simpeg.electromagnetics.static import utils +from simpeg import data +from simpeg.utils.io_utils import io_utils_electromagnetics as io_utils import shutil import os diff --git a/tests/em/static/test_DC_1D_jvecjtvecadj.py b/tests/em/static/test_DC_1D_jvecjtvecadj.py index d867543164..87ff631371 100644 --- a/tests/em/static/test_DC_1D_jvecjtvecadj.py +++ b/tests/em/static/test_DC_1D_jvecjtvecadj.py @@ -1,10 +1,10 @@ from discretize.tests import check_derivative, assert_isadjoint import numpy as np import pytest -from SimPEG import ( +from simpeg import ( maps, ) -from SimPEG.electromagnetics import resistivity as dc +from simpeg.electromagnetics import resistivity as dc TOL = 1e-5 FLR = 1e-20 # "zero", so if residual below this --> pass regardless of order diff --git a/tests/em/static/test_DC_2D_analytic.py b/tests/em/static/test_DC_2D_analytic.py index ecc2b1eea3..9edbf3ca0f 100644 --- a/tests/em/static/test_DC_2D_analytic.py +++ b/tests/em/static/test_DC_2D_analytic.py @@ -3,9 +3,9 @@ from discretize import TensorMesh -from SimPEG import utils, SolverLU -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.electromagnetics import analytics +from simpeg import utils, SolverLU +from simpeg.electromagnetics import resistivity as dc +from simpeg.electromagnetics import analytics class DCProblemAnalyticTests_DPDP(unittest.TestCase): @@ -324,14 +324,14 @@ def setUp(self): # determine comparison locations ROI_large_BNW = np.array([-200, -100]) ROI_large_TSE = np.array([200, 0]) - ROI_largeInds = utils.model_builder.getIndicesBlock( + 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.getIndicesBlock( + ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, mesh.gridN )[0] # print(ROI_smallInds.shape) diff --git a/tests/em/static/test_DC_2D_jvecjtvecadj.py b/tests/em/static/test_DC_2D_jvecjtvecadj.py index a0fb2d18fd..c25e06711f 100644 --- a/tests/em/static/test_DC_2D_jvecjtvecadj.py +++ b/tests/em/static/test_DC_2D_jvecjtvecadj.py @@ -1,7 +1,7 @@ import unittest import numpy as np import discretize -from SimPEG import ( +from simpeg import ( maps, utils, data_misfit, @@ -11,14 +11,12 @@ inversion, inverse_problem, ) -from SimPEG.electromagnetics import resistivity as dc +from simpeg.electromagnetics import resistivity as dc try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver - -np.random.seed(41) + from simpeg import SolverLU as Solver class DCProblem_2DTests(unittest.TestCase): @@ -85,9 +83,9 @@ def test_misfit(self): 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.data.nD) + rng = np.random.default_rng(seed=41) + v = rng.random(self.mesh.nC) + w = rng.random(self.data.nD) wtJv = w.dot(self.p.Jvec(self.m0, v)) vtJtw = v.dot(self.p.Jtvec(self.m0, w)) passed = np.abs(wtJv - vtJtw) < self.adjoint_tol diff --git a/tests/em/static/test_DC_Boundaries.py b/tests/em/static/test_DC_Boundaries.py index 713730e3df..6bffbbd9ef 100644 --- a/tests/em/static/test_DC_Boundaries.py +++ b/tests/em/static/test_DC_Boundaries.py @@ -3,7 +3,7 @@ import discretize from discretize.utils import example_simplex_mesh -import SimPEG.electromagnetics.static.resistivity as dc +import simpeg.electromagnetics.static.resistivity as dc tens_2d = discretize.TensorMesh([8, 9]) diff --git a/tests/em/static/test_DC_FieldsDipoleFullspace.py b/tests/em/static/test_DC_FieldsDipoleFullspace.py index acc37e5a59..ac33a2f2b9 100644 --- a/tests/em/static/test_DC_FieldsDipoleFullspace.py +++ b/tests/em/static/test_DC_FieldsDipoleFullspace.py @@ -1,14 +1,14 @@ import unittest from discretize import TensorMesh -from SimPEG import utils +from simpeg import utils import numpy as np -from SimPEG.electromagnetics import resistivity as dc +from simpeg.electromagnetics import resistivity as dc try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver from geoana.em import fdem from scipy.constants import mu_0, epsilon_0 @@ -76,14 +76,14 @@ def setUp(self): ROI_large_BNW = np.array([-75, 75, -75]) ROI_large_TSE = np.array([75, -75, 75]) - ROI_largeInds = utils.model_builder.getIndicesBlock( + 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.getIndicesBlock( + ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, faceGrid )[0] # print(ROI_smallInds.shape) @@ -249,14 +249,14 @@ def setUp(self): ROI_large_BNW = np.array([-75, 75, -75]) ROI_large_TSE = np.array([75, -75, 75]) - ROI_largeInds = utils.model_builder.getIndicesBlock( + 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.getIndicesBlock( + ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, edgeGrid )[0] # print(ROI_smallInds.shape) diff --git a/tests/em/static/test_DC_FieldsMultipoleFullspace.py b/tests/em/static/test_DC_FieldsMultipoleFullspace.py index 19d8f5eacd..d5e9b8ebfc 100644 --- a/tests/em/static/test_DC_FieldsMultipoleFullspace.py +++ b/tests/em/static/test_DC_FieldsMultipoleFullspace.py @@ -1,14 +1,14 @@ import unittest from discretize import TensorMesh -from SimPEG import utils +from simpeg import utils import numpy as np -from SimPEG.electromagnetics import resistivity as dc +from simpeg.electromagnetics import resistivity as dc try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver from geoana.em import fdem from scipy.constants import mu_0, epsilon_0 @@ -104,14 +104,14 @@ def setUp(self): ROI_large_BNW = np.array([-75, 75, -75]) ROI_large_TSE = np.array([75, -75, 75]) - ROI_largeInds = utils.model_builder.getIndicesBlock( + 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.getIndicesBlock( + ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, faceGrid )[0] # print(ROI_smallInds.shape) @@ -278,14 +278,14 @@ def setUp(self): ROI_large_BNW = np.array([-75, 75, -75]) ROI_large_TSE = np.array([75, -75, 75]) - ROI_largeInds = utils.model_builder.getIndicesBlock( + 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.getIndicesBlock( + ROI_smallInds = utils.model_builder.get_indices_block( ROI_small_BNW, ROI_small_TSE, edgeGrid )[0] # print(ROI_smallInds.shape) diff --git a/tests/em/static/test_DC_Utils.py b/tests/em/static/test_DC_Utils.py index 1b5edb914b..5e98513a81 100644 --- a/tests/em/static/test_DC_Utils.py +++ b/tests/em/static/test_DC_Utils.py @@ -1,20 +1,21 @@ # import matplotlib # matplotlib.use('Agg') +import pytest import unittest import numpy as np import discretize -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.electromagnetics.static import utils -from SimPEG import maps, mkvc -from SimPEG.utils import io_utils +from simpeg.electromagnetics import resistivity as dc +from simpeg.electromagnetics.static import utils +from simpeg import maps, mkvc +from simpeg.utils import io_utils import shutil import os try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver class DCUtilsTests_halfspace(unittest.TestCase): @@ -131,7 +132,7 @@ def test_io_rhoa(self): rhoA_GIF = np.loadtxt(rhoA_GIF_file) passed = np.allclose(rhoapp, rhoA_GIF) self.assertTrue(passed) - print(" ... ok \n".format(survey_type)) + print(f" ... ok ({survey_type})\n") def tearDown(self): # Clean up the working directory @@ -225,27 +226,36 @@ def setUp(self): survey_type = ["dipole-dipole", "pole-pole", "pole-dipole", "dipole-pole"] data_type = "volt" dimension_type = "3D" - end_locations = np.r_[-1000.0, 1000.0, 0.0, 0.0] + end_locations = [ + np.r_[-1000.0, 1000.0, 0.0, 0.0], + np.r_[0.0, 0.0, -1000.0, 1000.0], + ] station_separation = 200.0 num_rx_per_src = 5 # 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(survey_type)): - source_list += utils.generate_dcip_sources_line( - survey_type[ii], - data_type, - dimension_type, - end_locations, - 0.0, - num_rx_per_src, - station_separation, - ) + lineID = [] + for end_location in end_locations: + for survey_type_i in survey_type: + source_list += utils.generate_dcip_sources_line( + survey_type_i, + data_type, + dimension_type, + end_location, + 0.0, + num_rx_per_src, + station_separation, + ) # Define the survey self.survey = dc.survey.Survey(source_list) + lineID = np.ones(len(self.survey.locations_a)) + lineID[int(len(self.survey.locations_a) / 2) :] = 2 + self.lineID = lineID + def test_generate_survey_from_abmn_locations(self): survey_new, sorting_index = utils.generate_survey_from_abmn_locations( locations_a=self.survey.locations_a, @@ -281,7 +291,7 @@ def test_get_source_locations(self): is_rx = np.array(is_rx, dtype=float) _, idx = np.unique( - np.c_[self.survey.locations_a, self.survey.locations_b, is_rx], + np.c_[self.survey.locations_a, self.survey.locations_b, is_rx, self.lineID], axis=0, return_index=True, ) @@ -299,13 +309,14 @@ def test_get_source_locations(self): self.assertTrue(passed) def test_convert_to_2d(self): - # Only 1 line of 3D data along x direction starting from (-1000,0,0) - lineID = np.ones(self.survey.nD, dtype=int) - survey_2d, IND = utils.convert_survey_3d_to_2d_lines( - self.survey, lineID, data_type="volt", output_indexing=True + # 3D survey has two lines of data, one E-W and the other N-S. + survey_2d_list, IND = utils.convert_survey_3d_to_2d_lines( + self.survey, self.lineID, data_type="volt", output_indexing=True ) + + # First, check that coordinates remain the same even after the transformation for the first line IND = IND[0] - survey_2d = survey_2d[0] + survey_2d = survey_2d_list[0] ds = np.c_[-1000.0, 0.0, 0.0] @@ -326,9 +337,79 @@ def test_convert_to_2d(self): survey_2d.locations_n, ] + # Coordinates should be roughly the same passed = np.allclose(loc3d[:, 0::2], loc2d) self.assertTrue(passed) + # Check that the first x-coordinate for electrode A is zero for both surveys + for survey in survey_2d_list: + 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 8c1d169135..d26c1a1cca 100644 --- a/tests/em/static/test_DC_analytic.py +++ b/tests/em/static/test_DC_analytic.py @@ -1,15 +1,15 @@ import unittest import discretize -from SimPEG import utils +from simpeg import utils import numpy as np -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.electromagnetics import analytics +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 + from simpeg import SolverLU as Solver class DCProblemAnalyticTests(unittest.TestCase): diff --git a/tests/em/static/test_DC_jvecjtvecadj.py b/tests/em/static/test_DC_jvecjtvecadj.py index 70634d8c3f..25c08fc8e2 100644 --- a/tests/em/static/test_DC_jvecjtvecadj.py +++ b/tests/em/static/test_DC_jvecjtvecadj.py @@ -1,7 +1,7 @@ import unittest import numpy as np import discretize -from SimPEG import ( +from simpeg import ( maps, data_misfit, regularization, @@ -11,8 +11,8 @@ tests, utils, ) -from SimPEG.utils import mkvc -from SimPEG.electromagnetics import resistivity as dc +from simpeg.utils import mkvc +from simpeg.electromagnetics import resistivity as dc from pymatsolver import Pardiso import shutil @@ -155,9 +155,8 @@ def test_e_adjoint(self): wJtv = w.dot(self.prob.Jtvec(m, v, u)) tol = np.max([TOL * (10 ** int(np.log10(np.abs(vJw)))), FLR]) print( - "vJw: {:1.2e}, wJTv: {:1.2e}, tol: {:1.0e}, passed: {}\n".format( - vJw, wJtv, vJw - wJtv, tol, np.abs(vJw - wJtv) < tol - ) + f"vJw: {vJw:1.2e}, wJTv: {wJtv:1.2e}, tol: {tol:1.0e}, " + f"passed: {np.abs(vJw - wJtv) < tol}\n" ) return np.abs(vJw - wJtv) < tol diff --git a/tests/em/static/test_DC_miniaturize.py b/tests/em/static/test_DC_miniaturize.py index 95ec1d80b6..95a2fb67a7 100644 --- a/tests/em/static/test_DC_miniaturize.py +++ b/tests/em/static/test_DC_miniaturize.py @@ -1,6 +1,6 @@ -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.electromagnetics.static.utils.static_utils import generate_dcip_sources_line -from SimPEG import maps +from simpeg.electromagnetics.static import resistivity as dc +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 diff --git a/tests/em/static/test_IO.py b/tests/em/static/test_IO.py index 791381af11..b6816ac067 100644 --- a/tests/em/static/test_IO.py +++ b/tests/em/static/test_IO.py @@ -1,8 +1,8 @@ # import matplotlib # matplotlib.use('Agg') -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.electromagnetics.static import utils +from simpeg.electromagnetics import resistivity as dc +from simpeg.electromagnetics.static import utils import matplotlib.pyplot as plt import numpy as np import unittest diff --git a/tests/em/static/test_IP_2D_fwd.py b/tests/em/static/test_IP_2D_fwd.py index 10beaf71da..93c22b41e9 100644 --- a/tests/em/static/test_IP_2D_fwd.py +++ b/tests/em/static/test_IP_2D_fwd.py @@ -1,15 +1,15 @@ import unittest import discretize -from SimPEG import utils, maps +from simpeg import utils, maps import numpy as np -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.electromagnetics import induced_polarization as ip +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 + from simpeg import SolverLU as Solver class IPProblemAnalyticTests(unittest.TestCase): @@ -39,7 +39,7 @@ def setUp(self): surveyDC = dc.Survey([src0, src1]) sigmaInf = np.ones(mesh.nC) * 1.0 - blkind = utils.model_builder.getIndicesSphere(np.r_[0, -150], 40, mesh.gridCC) + blkind = utils.model_builder.get_indices_sphere(np.r_[0, -150], 40, mesh.gridCC) eta = np.zeros(mesh.nC) eta[blkind] = 0.1 @@ -148,7 +148,7 @@ def setUp(self): survey_ip = ip.Survey([src0_ip, src1_ip]) sigmaInf = np.ones(mesh.nC) * 1.0 - blkind = utils.model_builder.getIndicesSphere(np.r_[0, -150], 40, mesh.gridCC) + blkind = utils.model_builder.get_indices_sphere(np.r_[0, -150], 40, mesh.gridCC) eta = np.zeros(mesh.nC) eta[blkind] = 0.05 diff --git a/tests/em/static/test_IP_2D_jvecjtvecadj.py b/tests/em/static/test_IP_2D_jvecjtvecadj.py index a247f52981..c95da77982 100644 --- a/tests/em/static/test_IP_2D_jvecjtvecadj.py +++ b/tests/em/static/test_IP_2D_jvecjtvecadj.py @@ -2,17 +2,17 @@ import discretize import numpy as np -from SimPEG import utils -from SimPEG import maps -from SimPEG import data_misfit -from SimPEG import regularization -from SimPEG import optimization -from SimPEG import inversion -from SimPEG import inverse_problem -from SimPEG import tests - -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.electromagnetics import induced_polarization as ip +from simpeg import utils +from simpeg import maps +from simpeg import data_misfit +from simpeg import regularization +from simpeg import optimization +from simpeg import inversion +from simpeg import inverse_problem +from simpeg import tests + +from simpeg.electromagnetics import resistivity as dc +from simpeg.electromagnetics import induced_polarization as ip np.random.seed(30) diff --git a/tests/em/static/test_IP_fwd.py b/tests/em/static/test_IP_fwd.py index fbc1003180..3d722f931c 100644 --- a/tests/em/static/test_IP_fwd.py +++ b/tests/em/static/test_IP_fwd.py @@ -3,14 +3,14 @@ import numpy as np import discretize -from SimPEG import utils, maps -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.electromagnetics import induced_polarization as ip +from simpeg import utils, maps +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 + from simpeg import SolverLU as Solver class IPProblemAnalyticTests(unittest.TestCase): @@ -34,7 +34,7 @@ def setUp(self): N = utils.ndgrid(x + 12.5, y, np.r_[0.0]) radius = 50.0 xc = np.r_[0.0, 0.0, -100] - blkind = utils.model_builder.getIndicesSphere(xc, radius, mesh.gridCC) + blkind = utils.model_builder.get_indices_sphere(xc, radius, mesh.gridCC) sigmaInf = np.ones(mesh.nC) * 1e-2 eta = np.zeros(mesh.nC) eta[blkind] = 0.1 @@ -131,7 +131,7 @@ def setUp(self): N = utils.ndgrid(x + 12.5, y, np.r_[0.0]) radius = 50.0 xc = np.r_[0.0, 0.0, -100] - blkind = utils.model_builder.getIndicesSphere(xc, radius, mesh.gridCC) + blkind = utils.model_builder.get_indices_sphere(xc, radius, mesh.gridCC) sigmaInf = np.ones(mesh.nC) * 1e-2 eta = np.zeros(mesh.nC) eta[blkind] = 0.1 diff --git a/tests/em/static/test_IP_jvecjtvecadj.py b/tests/em/static/test_IP_jvecjtvecadj.py index b1163280d1..8884b3b385 100644 --- a/tests/em/static/test_IP_jvecjtvecadj.py +++ b/tests/em/static/test_IP_jvecjtvecadj.py @@ -2,16 +2,16 @@ import discretize import numpy as np -from SimPEG import maps -from SimPEG import data_misfit -from SimPEG import regularization -from SimPEG import optimization -from SimPEG import inversion -from SimPEG import inverse_problem -from SimPEG import tests - -from SimPEG.electromagnetics import resistivity as dc -from SimPEG.electromagnetics import induced_polarization as ip +from simpeg import maps +from simpeg import data_misfit +from simpeg import regularization +from simpeg import optimization +from simpeg import inversion +from simpeg import inverse_problem +from simpeg import tests + +from simpeg.electromagnetics import resistivity as dc +from simpeg.electromagnetics import induced_polarization as ip import shutil np.random.seed(30) diff --git a/tests/em/static/test_SIP_2D_jvecjtvecadj.py b/tests/em/static/test_SIP_2D_jvecjtvecadj.py index 153792856a..751aa7ea98 100644 --- a/tests/em/static/test_SIP_2D_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_2D_jvecjtvecadj.py @@ -1,7 +1,7 @@ import unittest import discretize -from SimPEG import ( +from simpeg import ( utils, maps, data_misfit, @@ -12,12 +12,12 @@ tests, ) import numpy as np -from SimPEG.electromagnetics import spectral_induced_polarization as sip +from simpeg.electromagnetics import spectral_induced_polarization as sip try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver np.random.seed(38) @@ -28,10 +28,10 @@ def setUp(self): hx = [(cs, 0, -1.3), (cs, 21), (cs, 0, 1.3)] hz = [(cs, 0, -1.3), (cs, 20)] mesh = discretize.TensorMesh([hx, hz], x0="CN") - blkind0 = utils.model_builder.getIndicesSphere( + blkind0 = utils.model_builder.get_indices_sphere( np.r_[-100.0, -200.0], 75.0, mesh.gridCC ) - blkind1 = utils.model_builder.getIndicesSphere( + blkind1 = utils.model_builder.get_indices_sphere( np.r_[100.0, -200.0], 75.0, mesh.gridCC ) @@ -120,10 +120,10 @@ def setUp(self): hx = [(cs, 0, -1.3), (cs, 21), (cs, 0, 1.3)] hz = [(cs, 0, -1.3), (cs, 20)] mesh = discretize.TensorMesh([hx, hz], x0="CN") - blkind0 = utils.model_builder.getIndicesSphere( + blkind0 = utils.model_builder.get_indices_sphere( np.r_[-100.0, -200.0], 75.0, mesh.gridCC ) - blkind1 = utils.model_builder.getIndicesSphere( + blkind1 = utils.model_builder.get_indices_sphere( np.r_[100.0, -200.0], 75.0, mesh.gridCC ) @@ -211,10 +211,10 @@ def setUp(self): hx = [(cs, 0, -1.3), (cs, 21), (cs, 0, 1.3)] hz = [(cs, 0, -1.3), (cs, 20)] mesh = discretize.TensorMesh([hx, hz], x0="CN") - blkind0 = utils.model_builder.getIndicesSphere( + blkind0 = utils.model_builder.get_indices_sphere( np.r_[-100.0, -200.0], 75.0, mesh.gridCC ) - blkind1 = utils.model_builder.getIndicesSphere( + blkind1 = utils.model_builder.get_indices_sphere( np.r_[100.0, -200.0], 75.0, mesh.gridCC ) @@ -264,9 +264,15 @@ def setUp(self): dobs = problem.make_synthetic_data(mSynth, add_noise=True) # Now set up the problem to do some minimization dmis = data_misfit.L2DataMisfit(data=dobs, simulation=problem) - reg_eta = regularization.Simple(mesh, mapping=wires.eta, indActive=~airind) - reg_taui = regularization.Simple(mesh, mapping=wires.taui, indActive=~airind) - reg_c = regularization.Simple(mesh, mapping=wires.c, indActive=~airind) + reg_eta = regularization.WeightedLeastSquares( + mesh, mapping=wires.eta, active_cells=~airind + ) + reg_taui = regularization.WeightedLeastSquares( + mesh, mapping=wires.taui, active_cells=~airind + ) + reg_c = regularization.WeightedLeastSquares( + 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 diff --git a/tests/em/static/test_SIP_jvecjtvecadj.py b/tests/em/static/test_SIP_jvecjtvecadj.py index dfafb7b83f..d9400f6f1e 100644 --- a/tests/em/static/test_SIP_jvecjtvecadj.py +++ b/tests/em/static/test_SIP_jvecjtvecadj.py @@ -1,6 +1,6 @@ import unittest import discretize -from SimPEG import ( +from simpeg import ( utils, maps, data_misfit, @@ -11,12 +11,12 @@ tests, ) import numpy as np -from SimPEG.electromagnetics import spectral_induced_polarization as sip +from simpeg.electromagnetics import spectral_induced_polarization as sip try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver np.random.seed(38) @@ -28,10 +28,10 @@ def setUp(self): hy = [(cs, 0, -1.3), (cs, 21), (cs, 0, 1.3)] hz = [(cs, 0, -1.3), (cs, 20)] mesh = discretize.TensorMesh([hx, hy, hz], x0="CCN") - blkind0 = utils.model_builder.getIndicesSphere( + blkind0 = utils.model_builder.get_indices_sphere( np.r_[-100.0, -100.0, -200.0], 75.0, mesh.gridCC ) - blkind1 = utils.model_builder.getIndicesSphere( + blkind1 = utils.model_builder.get_indices_sphere( np.r_[100.0, 100.0, -200.0], 75.0, mesh.gridCC ) sigma = np.ones(mesh.nC) * 1e-2 @@ -127,10 +127,10 @@ def setUp(self): hy = [(cs, 0, -1.3), (cs, 21), (cs, 0, 1.3)] hz = [(cs, 0, -1.3), (cs, 20)] mesh = discretize.TensorMesh([hx, hy, hz], x0="CCN") - blkind0 = utils.model_builder.getIndicesSphere( + blkind0 = utils.model_builder.get_indices_sphere( np.r_[-100.0, -100.0, -200.0], 75.0, mesh.gridCC ) - blkind1 = utils.model_builder.getIndicesSphere( + blkind1 = utils.model_builder.get_indices_sphere( np.r_[100.0, 100.0, -200.0], 75.0, mesh.gridCC ) sigma = np.ones(mesh.nC) * 1e-2 @@ -224,10 +224,10 @@ def setUp(self): hy = [(cs, 0, -1.3), (cs, 21), (cs, 0, 1.3)] hz = [(cs, 0, -1.3), (cs, 20), (cs, 0, 1.3)] mesh = discretize.TensorMesh([hx, hy, hz], x0="CCC") - blkind0 = utils.model_builder.getIndicesSphere( + blkind0 = utils.model_builder.get_indices_sphere( np.r_[-100.0, -100.0, -200.0], 75.0, mesh.gridCC ) - blkind1 = utils.model_builder.getIndicesSphere( + blkind1 = utils.model_builder.get_indices_sphere( np.r_[100.0, 100.0, -200.0], 75.0, mesh.gridCC ) sigma = np.ones(mesh.nC) * 1e-2 @@ -282,9 +282,9 @@ def setUp(self): dobs = problem.make_synthetic_data(mSynth, add_noise=True) # 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, indActive=~airind) - reg_taui = regularization.Sparse(mesh, mapping=wires.taui, indActive=~airind) - reg_c = regularization.Sparse(mesh, mapping=wires.c, indActive=~airind) + reg_eta = regularization.Sparse(mesh, mapping=wires.eta, active_cells=~airind) + reg_taui = regularization.Sparse(mesh, mapping=wires.taui, active_cells=~airind) + 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 diff --git a/tests/em/static/test_SPjvecjtvecadj.py b/tests/em/static/test_SPjvecjtvecadj.py index d884334404..8d806a3de3 100644 --- a/tests/em/static/test_SPjvecjtvecadj.py +++ b/tests/em/static/test_SPjvecjtvecadj.py @@ -1,10 +1,10 @@ import pytest import numpy as np -import SimPEG.electromagnetics.static.spontaneous_potential as sp -import SimPEG.electromagnetics.static.resistivity as dc +import simpeg.electromagnetics.static.self_potential as sp +import simpeg.electromagnetics.static.resistivity as dc import discretize -from SimPEG import utils -from SimPEG import maps +from simpeg import utils +from simpeg import maps from discretize.tests import check_derivative, assert_isadjoint @@ -117,3 +117,29 @@ def test_clears(): sim.qMap = maps.ExpMap() assert sim.deleteTheseOnModelUpdate == ["_Jmatrix", "_gtgdiag"] assert sim.clean_on_model_update == [] + + +def test_deprecations(): + """ + Test warning 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): + 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_properties.py b/tests/em/static/test_properties.py index 3b4fb9202e..5a9c6419bb 100644 --- a/tests/em/static/test_properties.py +++ b/tests/em/static/test_properties.py @@ -1,8 +1,8 @@ import numpy as np import pytest -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.electromagnetics.static import spectral_induced_polarization as sip +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.electromagnetics.static import spectral_induced_polarization as sip def test_receiver_properties(): diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint.py b/tests/em/tdem/test_TDEM_DerivAdjoint.py index 5433ea9a4a..49d0b4476d 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint.py @@ -2,8 +2,8 @@ import numpy as np import time import discretize -from SimPEG import maps, tests -from SimPEG.electromagnetics import time_domain as tdem +from simpeg import maps, tests +from simpeg.electromagnetics import time_domain as tdem from pymatsolver import Pardiso as Solver @@ -133,11 +133,7 @@ def JvecVsJtvecTest(self, rxcomp): tol = TOL * (np.abs(V1) + np.abs(V2)) / 2.0 passed = np.abs(V1 - V2) < tol - print( - " {v1} {v2} {passed}".format( - prbtype=self.formulation, v1=V1, v2=V2, passed=passed - ) - ) + print(f"{self.formulation} {V1} {V2} {passed}") self.assertTrue(passed) @@ -238,9 +234,6 @@ def test_Jvec_b_dbdtx(self): def test_Jvec_b_dbdtz(self): self.JvecTest("MagneticFluxTimeDerivativez") - def test_Jvec_b_jy(self): - self.JvecTest("CurrentDensityy") - def test_Jvec_b_hx(self): self.JvecTest("MagneticFieldx") @@ -282,10 +275,10 @@ def test_Jvec_adjoint_b_hz(self): def test_Jvec_adjoint_b_dhdtx(self): self.JvecVsJtvecTest("MagneticFieldTimeDerivativex") - def test_Jvec_adjoint_b_dhdtx(self): + def test_Jvec_adjoint_b_dhdtz(self): self.JvecVsJtvecTest("MagneticFieldTimeDerivativez") - def test_Jvec_adjoint_b_ey(self): + def test_Jvec_adjoint_b_jy(self): self.JvecVsJtvecTest("CurrentDensityy") diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py index 6e5325b3cf..e650dde269 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_RawWaveform.py @@ -2,11 +2,12 @@ import numpy as np import time import discretize -from SimPEG import maps, tests -from SimPEG.electromagnetics import time_domain as tdem -from SimPEG.electromagnetics import utils +from simpeg import maps, tests +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 @@ -18,11 +19,11 @@ def get_mesh(): - cs = 5.0 - ncx = 8 - ncy = 8 - ncz = 8 - npad = 4 + cs = 10.0 + ncx = 4 + ncy = 4 + ncz = 4 + npad = 2 return discretize.TensorMesh( [ @@ -71,12 +72,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) 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 print("Solving Fields for problem {}".format(self.formulation)) t = time.time() @@ -101,7 +104,7 @@ def get_rx(self, rxcomp): timerx = self.t0 + np.logspace(-5, -3, 20) return getattr(tdem.Rx, "Point{}".format(rxcomp[:-1]))( - np.array([[rxOffset, 0.0, 0.0]]), timerx, rxcomp[-1] + np.array([[rxOffset, 0.0, 0.0]]), timerx, orientation=rxcomp[-1] ) def set_receiver_list(self, rxcomp): @@ -131,7 +134,7 @@ 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=3, eps=1e-20) def JvecVsJtvecTest(self, rxcomp): np.random.seed(4) @@ -147,11 +150,7 @@ def JvecVsJtvecTest(self, rxcomp): tol = TOL * (np.abs(V1) + np.abs(V2)) / 2.0 passed = np.abs(V1 - V2) < tol - print( - " {v1} {v2} {passed}".format( - prbtype=self.formulation, v1=V1, v2=V2, passed=passed - ) - ) + print(f"{self.formulation} {V1} {V2} {passed}") self.assertTrue(passed) @@ -171,13 +170,13 @@ def test_Jvec_e_ey(self): if testAdjoint: - def test_Jvec_adjoint_e_ey(self): + def test_Jvec_adjoint_e_ey(self): # noqa F811 self.JvecVsJtvecTest("MagneticFluxTimeDerivativex") - def test_Jvec_adjoint_e_ey(self): + def test_Jvec_adjoint_e_ey(self): # noqa F811 self.JvecVsJtvecTest("MagneticFluxTimeDerivativez") - def test_Jvec_adjoint_e_ey(self): + def test_Jvec_adjoint_e_ey(self): # noqa F811 self.JvecVsJtvecTest("ElectricFieldy") @@ -186,9 +185,11 @@ class DerivAdjoint_B(Base_DerivAdjoint_Test): if testDeriv: + @pytest.mark.xfail def test_Jvec_b_bx(self): self.JvecTest("MagneticFluxDensityx") + @pytest.mark.xfail def test_Jvec_b_bz(self): self.JvecTest("MagneticFluxDensityz") @@ -224,9 +225,11 @@ class DerivAdjoint_H(Base_DerivAdjoint_Test): if testDeriv: + @pytest.mark.xfail def test_Jvec_h_hx(self): self.JvecTest("MagneticFieldx") + @pytest.mark.xfail def test_Jvec_h_hz(self): self.JvecTest("MagneticFieldz") diff --git a/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py b/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py index 145a011b80..fb764d924b 100644 --- a/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py +++ b/tests/em/tdem/test_TDEM_DerivAdjoint_galvanic.py @@ -1,8 +1,8 @@ import unittest import numpy as np import discretize -from SimPEG import maps, tests -from SimPEG.electromagnetics import time_domain as tdem +from simpeg import maps, tests +from simpeg.electromagnetics import time_domain as tdem from pymatsolver import Pardiso as Solver plotIt = False diff --git a/tests/em/tdem/test_TDEM_crosscheck.py b/tests/em/tdem/test_TDEM_crosscheck.py index 853d4e5c70..ec0742b066 100644 --- a/tests/em/tdem/test_TDEM_crosscheck.py +++ b/tests/em/tdem/test_TDEM_crosscheck.py @@ -1,9 +1,9 @@ import unittest import discretize -from SimPEG import maps -from SimPEG.electromagnetics import time_domain as tdem -from SimPEG.electromagnetics import utils +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 @@ -17,11 +17,11 @@ def setUp_TDEM( ): # set a seed so that the same conductivity model is used for all runs np.random.seed(25) - cs = 5.0 - ncx = 8 - ncy = 8 - ncz = 8 - npad = 4 + cs = 10.0 + ncx = 4 + ncy = 4 + ncz = 4 + npad = 2 # hx = [(cs, ncx), (cs, npad, 1.3)] # hz = [(cs, npad, -1.3), (cs, ncy), (cs, npad, 1.3)] mesh = discretize.TensorMesh( @@ -145,14 +145,6 @@ def test_HJ_j_stepoff(self): waveform="stepoff", ) - def test_HJ_j_stepoff(self): - CrossCheck( - prbtype1="MagneticField", - prbtype2="CurrentDensity", - rxcomp="CurrentDensityy", - waveform="stepoff", - ) - def test_HJ_dhdtx_stepoff(self): CrossCheck( prbtype1="MagneticField", @@ -219,14 +211,6 @@ def test_HJ_j_vtem(self): waveform="vtem", ) - def test_HJ_j_vtem(self): - CrossCheck( - prbtype1="MagneticField", - prbtype2="CurrentDensity", - rxcomp="CurrentDensityy", - waveform="vtem", - ) - def test_HJ_dhdtx_vtem(self): CrossCheck( prbtype1="MagneticField", diff --git a/tests/em/tdem/test_TDEM_forward_Analytic.py b/tests/em/tdem/test_TDEM_forward_Analytic.py index 9594e3de86..1c6e85c18e 100644 --- a/tests/em/tdem/test_TDEM_forward_Analytic.py +++ b/tests/em/tdem/test_TDEM_forward_Analytic.py @@ -5,9 +5,9 @@ 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 -from SimPEG.electromagnetics import time_domain as tdem +from simpeg import maps +from simpeg.electromagnetics import analytics +from simpeg.electromagnetics import time_domain as tdem def analytic_wholespace_dipole_comparison( diff --git a/tests/em/tdem/test_TDEM_forward_Analytic_RawWaveform.py b/tests/em/tdem/test_TDEM_forward_Analytic_RawWaveform.py index da6b4a3935..c5a8e9ba63 100644 --- a/tests/em/tdem/test_TDEM_forward_Analytic_RawWaveform.py +++ b/tests/em/tdem/test_TDEM_forward_Analytic_RawWaveform.py @@ -6,10 +6,10 @@ from pymatsolver import Pardiso as Solver from scipy.constants import mu_0 from scipy.interpolate import interp1d -from SimPEG import maps -from SimPEG.electromagnetics import analytics -from SimPEG.electromagnetics import time_domain as tdem -from SimPEG.electromagnetics import utils +from simpeg import maps +from simpeg.electromagnetics import analytics +from simpeg.electromagnetics import time_domain as tdem +from simpeg.electromagnetics import utils def halfSpaceProblemAnaDiff( diff --git a/tests/em/tdem/test_TDEM_grounded.py b/tests/em/tdem/test_TDEM_grounded.py index 116cfb00e2..c85a8807cc 100644 --- a/tests/em/tdem/test_TDEM_grounded.py +++ b/tests/em/tdem/test_TDEM_grounded.py @@ -2,10 +2,10 @@ from scipy.constants import mu_0 import unittest -# SimPEG, discretize +# simpeg, discretize import discretize -from SimPEG.electromagnetics import time_domain as tdem -from SimPEG import maps, tests +from simpeg.electromagnetics import time_domain as tdem +from simpeg import maps, tests from pymatsolver import Pardiso diff --git a/tests/em/tdem/test_TDEM_inductive_permeable.py b/tests/em/tdem/test_TDEM_inductive_permeable.py index 254769daec..8bf243554d 100644 --- a/tests/em/tdem/test_TDEM_inductive_permeable.py +++ b/tests/em/tdem/test_TDEM_inductive_permeable.py @@ -1,15 +1,14 @@ import unittest import discretize -from discretize import utils import numpy as np import matplotlib.pyplot as plt from matplotlib.colors import LogNorm from scipy.constants import mu_0 import time -from SimPEG.electromagnetics import time_domain as tdem -from SimPEG import utils, maps +from simpeg.electromagnetics import time_domain as tdem +from simpeg import utils, maps from pymatsolver import Pardiso diff --git a/tests/em/tdem/test_TDEM_sources.py b/tests/em/tdem/test_TDEM_sources.py index 7332c49db3..3d4fff3896 100644 --- a/tests/em/tdem/test_TDEM_sources.py +++ b/tests/em/tdem/test_TDEM_sources.py @@ -1,10 +1,11 @@ +import pytest import unittest 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 ( +from simpeg.electromagnetics.time_domain.sources import ( CircularLoop, ExponentialWaveform, HalfSineWaveform, @@ -527,16 +528,17 @@ def test_simple_source(): assert waveform.eval(0.0) == 1.0 -def test_CircularLoop_test_N_assignment(): +def test_removal_circular_loop_n(): """ - Test depreciation of the N property + Test if passing the N argument to CircularLoop raises an error """ - loop = CircularLoop( - [], - waveform=StepOffWaveform(), - location=np.array([0.0, 0.0, 0.0]), - radius=1.0, - current=0.5, - N=2, - ) - assert loop.n_turns == 2 + 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_properties.py b/tests/em/tdem/test_properties.py index 5aa443b45b..e1305b52da 100644 --- a/tests/em/tdem/test_properties.py +++ b/tests/em/tdem/test_properties.py @@ -1,16 +1,16 @@ import numpy as np import pytest -from SimPEG.electromagnetics import time_domain as tdem +from simpeg.electromagnetics import time_domain as tdem -def test_receiver_properties(): +def test_removed_projcomp(): + """Test if passing the removed `projComp` argument raises an error.""" xyz = np.c_[0.0, 0.0, 0.0] times = np.logspace(-5, -2, 4) - projComp = "Fx" - rx = tdem.receivers.BaseRx(xyz, times, projComp=projComp) - - assert rx.projComp == projComp + msg = "'projComp' property has been removed." + with pytest.raises(TypeError, match=msg): + tdem.receivers.BaseRx(xyz, times, projComp="foo") def test_source_properties(): diff --git a/tests/em/utils/test_linecurrents.py b/tests/em/utils/test_linecurrents.py index e824dc76a1..f0d94f3cb3 100644 --- a/tests/em/utils/test_linecurrents.py +++ b/tests/em/utils/test_linecurrents.py @@ -1,12 +1,12 @@ import numpy as np -from SimPEG.electromagnetics.utils import ( +from simpeg.electromagnetics.utils import ( getStraightLineCurrentIntegral, segmented_line_current_source_term, line_through_faces, ) import discretize import unittest -from SimPEG.utils import download +from simpeg.utils import download class LineCurrentTests(unittest.TestCase): diff --git a/tests/em/vrm/test_vrmfwd.py b/tests/em/vrm/test_vrmfwd.py index 89f1fc54f9..5e45cdcb6f 100644 --- a/tests/em/vrm/test_vrmfwd.py +++ b/tests/em/vrm/test_vrmfwd.py @@ -1,11 +1,10 @@ import unittest import numpy as np import discretize -from SimPEG.electromagnetics import viscous_remanent_magnetization as vrm +from simpeg.electromagnetics import viscous_remanent_magnetization as vrm class VRM_fwd_tests(unittest.TestCase): - """ Computed vs analytic dipole field """ diff --git a/tests/em/vrm/test_vrminv.py b/tests/em/vrm/test_vrminv.py index 8511f0a704..1b64627490 100644 --- a/tests/em/vrm/test_vrminv.py +++ b/tests/em/vrm/test_vrminv.py @@ -3,16 +3,16 @@ import discretize -from SimPEG import mkvc +from simpeg import mkvc -from SimPEG import data_misfit -from SimPEG import optimization -from SimPEG import regularization -from SimPEG import inverse_problem -from SimPEG import inversion -from SimPEG.directives import BetaSchedule, TargetMisfit +from simpeg import data_misfit +from simpeg import optimization +from simpeg import regularization +from simpeg import inverse_problem +from simpeg import inversion +from simpeg.directives import BetaSchedule, TargetMisfit -from SimPEG.electromagnetics import viscous_remanent_magnetization as vrm +from simpeg.electromagnetics import viscous_remanent_magnetization as vrm class VRM_inversion_tests(unittest.TestCase): @@ -63,13 +63,16 @@ def test_basic_inversion(self): dmis = data_misfit.L2DataMisfit(data=dobs, simulation=Problem) W = ( - mkvc( - (np.sum(np.array(Problem.A) ** 2, axis=0)) / meshObj.cell_volumes**2.0 - ) + mkvc((np.sum(np.array(Problem.A) ** 2, axis=0)) / meshObj.cell_volumes**2.0) ** 0.25 ) reg = regularization.WeightedLeastSquares( - meshObj, alpha_s=0.01, alpha_x=1.0, alpha_y=1.0, alpha_z=1.0, weights=W + meshObj, + alpha_s=0.01, + alpha_x=1.0, + alpha_y=1.0, + alpha_z=1.0, + weights={"weights": W}, ) opt = optimization.ProjectedGNCG( maxIter=20, lower=0.0, upper=1e-2, maxIterLS=20, tolCG=1e-4 diff --git a/tests/em/vrm/test_waveform.py b/tests/em/vrm/test_waveform.py index 15f30b9132..a005fc744f 100644 --- a/tests/em/vrm/test_waveform.py +++ b/tests/em/vrm/test_waveform.py @@ -1,7 +1,7 @@ import unittest import numpy as np -from SimPEG.electromagnetics import viscous_remanent_magnetization as vrm +from simpeg.electromagnetics import viscous_remanent_magnetization as vrm class VRM_waveform_tests(unittest.TestCase): diff --git a/tests/flow/test_Richards.py b/tests/flow/test_Richards.py index 81c3d8ccfc..71e9cc31b0 100644 --- a/tests/flow/test_Richards.py +++ b/tests/flow/test_Richards.py @@ -4,14 +4,14 @@ from discretize.tests import check_derivative import discretize -from SimPEG import maps -from SimPEG import utils -from SimPEG.flow import richards +from simpeg import maps +from simpeg import utils +from simpeg.flow import richards try: from pymatsolver import Pardiso as Solver except Exception: - from SimPEG import Solver + from simpeg import Solver TOL = 1e-8 @@ -108,7 +108,7 @@ 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=4, plotIt=False + lambda m: [self.prob.dpred(m), J], self.mtrue, num=3, plotIt=False ) self.assertTrue(passed, True) diff --git a/tests/flow/test_Richards_empirical.py b/tests/flow/test_Richards_empirical.py index 44bcf00d8f..4361621e3f 100644 --- a/tests/flow/test_Richards_empirical.py +++ b/tests/flow/test_Richards_empirical.py @@ -5,8 +5,8 @@ import discretize from discretize.tests import check_derivative -from SimPEG import maps -from SimPEG.flow import richards +from simpeg import maps +from simpeg.flow import richards TOL = 1e-8 diff --git a/tests/meta/test_dask_meta.py b/tests/meta/test_dask_meta.py new file mode 100644 index 0000000000..5feb5f6c75 --- /dev/null +++ b/tests/meta/test_dask_meta.py @@ -0,0 +1,393 @@ +import numpy as np +from simpeg.potential_fields import gravity +from simpeg.electromagnetics.static import resistivity as dc +from simpeg import maps +from discretize import TensorMesh +import scipy.sparse as sp +import pytest + +from simpeg.meta import ( + MetaSimulation, + SumMetaSimulation, + RepeatedSimulation, + DaskMetaSimulation, + DaskSumMetaSimulation, + DaskRepeatedSimulation, +) + +from distributed import Client, LocalCluster + + +@pytest.fixture(scope="module") +def cluster(): + dask_cluster = LocalCluster( + n_workers=2, threads_per_worker=2, dashboard_address=None, processes=True + ) + yield dask_cluster + dask_cluster.close() + + +def test_meta_correctness(cluster): + with Client(cluster) as client: + mesh = TensorMesh([16, 16, 16], origin="CCN") + + rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j] + rx_locs = rx_locs.reshape(3, -1).T + rxs = dc.receivers.Pole(rx_locs) + source_locs = np.mgrid[-0.5:0.5:10j, 0:1:1j, 0:1:1j].reshape(3, -1).T + src_list = [ + dc.sources.Pole( + [ + rxs, + ], + location=loc, + ) + for loc in source_locs + ] + m_test = np.arange(mesh.n_cells) / mesh.n_cells + 0.1 + # split by chunks of sources + chunk_size = 3 + sims = [] + mappings = [] + for i in range(0, len(src_list) + 1, chunk_size): + end = min(i + chunk_size, len(src_list)) + if i == end: + break + survey_chunk = dc.Survey(src_list[i:end]) + sims.append( + dc.Simulation3DNodal( + mesh, survey=survey_chunk, sigmaMap=maps.IdentityMap() + ) + ) + mappings.append(maps.IdentityMap()) + + serial_sim = MetaSimulation(sims, mappings) + dask_sim = DaskMetaSimulation(sims, mappings, client) + + # test fields objects + f_meta = serial_sim.fields(m_test) + f_dask = dask_sim.fields(m_test) + # Can't serialize DC nodal fields here, so can't directly test them. + # sol_meta = np.concatenate([f[:, "phiSolution"] for f in f_meta], axis=1) + # sol_dask = np.concatenate([f.result()[:, "phiSolution"] for f in f_dask], axis=1) + # np.testing.assert_allclose(sol_meta, sol_dask) + + # test data output + d_meta = serial_sim.dpred(m_test, f=f_meta) + d_dask = dask_sim.dpred(m_test, f=f_dask) + np.testing.assert_allclose(d_dask, d_meta) + + # test Jvec + rng = np.random.default_rng(seed=0) + u = rng.random(mesh.n_cells) + jvec_meta = serial_sim.Jvec(m_test, u, f=f_meta) + jvec_dask = dask_sim.Jvec(m_test, u, f=f_dask) + + np.testing.assert_allclose(jvec_dask, jvec_meta) + + # test Jtvec + v = rng.random(serial_sim.survey.nD) + jtvec_meta = serial_sim.Jtvec(m_test, v, f=f_meta) + jtvec_dask = dask_sim.Jtvec(m_test, v, f=f_dask) + + np.testing.assert_allclose(jtvec_dask, jtvec_meta) + + # test get diag + diag_meta = serial_sim.getJtJdiag(m_test, f=f_meta) + diag_dask = dask_sim.getJtJdiag(m_test, f=f_dask) + + np.testing.assert_allclose(diag_dask, diag_meta) + + # test things also works without passing optional fields + dask_sim.model = m_test + d_dask2 = dask_sim.dpred() + np.testing.assert_allclose(d_dask, d_dask2) + + jvec_dask2 = dask_sim.Jvec(m_test, u) + np.testing.assert_allclose(jvec_dask, jvec_dask2) + + jtvec_dask2 = dask_sim.Jtvec(m_test, v) + np.testing.assert_allclose(jtvec_dask, jtvec_dask2) + + # also pass a diagonal matrix here for testing. + dask_sim._jtjdiag = None + W = sp.eye(dask_sim.survey.nD) + diag_dask2 = dask_sim.getJtJdiag(m_test, W=W) + np.testing.assert_allclose(diag_dask, diag_dask2) + + +def test_sum_sim_correctness(cluster): + with Client(cluster) as client: + mesh = TensorMesh([16, 16, 16], origin="CCN") + # Create gravity sum sims + rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T + rx = gravity.Point(rx_locs, components=["gz"]) + survey = gravity.Survey(gravity.SourceField(rx)) + + mesh_bot = TensorMesh([mesh.h[0], mesh.h[1], mesh.h[2][:8]], origin=mesh.origin) + mesh_top = TensorMesh( + [mesh.h[0], mesh.h[1], mesh.h[2][8:]], origin=["C", "C", mesh.nodes_z[8]] + ) + + g_mappings = [ + maps.Mesh2Mesh((mesh_bot, mesh)), + maps.Mesh2Mesh((mesh_top, mesh)), + ] + g_sims = [ + gravity.Simulation3DIntegral( + mesh_bot, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 + ), + gravity.Simulation3DIntegral( + mesh_top, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 + ), + ] + + serial_sim = SumMetaSimulation(g_sims, g_mappings) + parallel_sim = DaskSumMetaSimulation(g_sims, g_mappings, client) + + m_test = np.arange(mesh.n_cells) / mesh.n_cells + 0.1 + + # test fields objects + f_full = serial_sim.fields(m_test) + f_meta = parallel_sim.fields(m_test) + # Again don't serialize and collect the fields on the main + # process directly. + # np.testing.assert_allclose(f_full, sum(f_meta)) + + # test data output + d_full = serial_sim.dpred(m_test, f=f_full) + d_meta = parallel_sim.dpred(m_test, f=f_meta) + np.testing.assert_allclose(d_full, d_meta, rtol=1e-6) + + rng = np.random.default_rng(0) + + # test Jvec + u = rng.random(mesh.n_cells) + jvec_full = serial_sim.Jvec(m_test, u, f=f_full) + jvec_meta = parallel_sim.Jvec(m_test, u, f=f_meta) + + np.testing.assert_allclose(jvec_full, jvec_meta, rtol=1e-6) + + # test Jtvec + v = rng.random(survey.nD) + jtvec_full = serial_sim.Jtvec(m_test, v, f=f_full) + jtvec_meta = parallel_sim.Jtvec(m_test, v, f=f_meta) + + np.testing.assert_allclose(jtvec_full, jtvec_meta, rtol=1e-6) + + # test get diag + diag_full = serial_sim.getJtJdiag(m_test, f=f_full) + diag_meta = parallel_sim.getJtJdiag(m_test, f=f_meta) + + np.testing.assert_allclose(diag_full, diag_meta, rtol=1e-6) + + # test things also works without passing optional kwargs + parallel_sim.model = m_test + d_meta2 = parallel_sim.dpred() + np.testing.assert_allclose(d_meta, d_meta2) + + jvec_meta2 = parallel_sim.Jvec(m_test, u) + np.testing.assert_allclose(jvec_meta, jvec_meta2) + + jtvec_meta2 = parallel_sim.Jtvec(m_test, v) + np.testing.assert_allclose(jtvec_meta, jtvec_meta2) + + parallel_sim._jtjdiag = None + diag_meta2 = parallel_sim.getJtJdiag(m_test) + np.testing.assert_allclose(diag_meta, diag_meta2) + + +def test_repeat_sim_correctness(cluster): + with Client(cluster) as client: + # meta sim is tested for correctness + # so can test the repeat against the meta sim + mesh = TensorMesh([8, 8, 8], origin="CCN") + rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T + rx = gravity.Point(rx_locs, components=["gz"]) + survey = gravity.Survey(gravity.SourceField(rx)) + grav_sim = gravity.Simulation3DIntegral( + mesh, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 + ) + + time_mesh = TensorMesh([8], origin=[0]) + sim_ts = np.linspace(0, 1, 6) + + repeat_mappings = [] + eye = sp.eye(mesh.n_cells, mesh.n_cells) + for t in sim_ts: + ave_time = time_mesh.get_interpolation_matrix([t]) + ave_full = sp.kron(ave_time, eye, format="csr") + repeat_mappings.append(maps.LinearMap(ave_full)) + + serial_sim = RepeatedSimulation(grav_sim, repeat_mappings) + parallel_sim = DaskRepeatedSimulation(grav_sim, repeat_mappings, client) + + rng = np.random.default_rng(0) + model = rng.random((time_mesh.n_cells, mesh.n_cells)).reshape(-1) + + # test field things + f_full = serial_sim.fields(model) + f_meta = parallel_sim.fields(model) + # np.testing.assert_equal(np.c_[f_full], np.c_[f_meta]) + + d_full = serial_sim.dpred(model, f_full) + d_repeat = parallel_sim.dpred(model, f_meta) + np.testing.assert_allclose(d_full, d_repeat, rtol=1e-6) + + # test Jvec + u = rng.random(len(model)) + jvec_full = serial_sim.Jvec(model, u, f=f_full) + jvec_meta = parallel_sim.Jvec(model, u, f=f_meta) + np.testing.assert_allclose(jvec_full, jvec_meta, rtol=1e-6) + + # test Jtvec + v = rng.random(len(sim_ts) * survey.nD) + jtvec_full = serial_sim.Jtvec(model, v, f=f_full) + jtvec_meta = parallel_sim.Jtvec(model, v, f=f_meta) + np.testing.assert_allclose(jtvec_full, jtvec_meta, rtol=1e-6) + + # test get diag + diag_full = serial_sim.getJtJdiag(model, f=f_full) + diag_meta = parallel_sim.getJtJdiag(model, f=f_meta) + np.testing.assert_allclose(diag_full, diag_meta, rtol=1e-6) + + +def test_dask_meta_errors(cluster): + with Client(cluster) as client: + mesh = TensorMesh([16, 16, 16], origin="CCN") + + rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j] + rx_locs = rx_locs.reshape(3, -1).T + rxs = dc.receivers.Pole(rx_locs) + source_locs = np.mgrid[-0.5:0.5:10j, 0:1:1j, 0:1:1j].reshape(3, -1).T + src_list = [ + dc.sources.Pole( + [ + rxs, + ], + location=loc, + ) + for loc in source_locs + ] + + # split by chunks of sources + chunk_size = 3 + sims = [] + mappings = [] + for i in range(0, len(src_list) + 1, chunk_size): + end = min(i + chunk_size, len(src_list)) + if i == end: + break + survey_chunk = dc.Survey(src_list[i:end]) + sims.append( + dc.Simulation3DNodal( + mesh, survey=survey_chunk, sigmaMap=maps.IdentityMap(mesh) + ) + ) + mappings.append(maps.IdentityMap(mesh)) + + # incompatible length of mappings and simulations lists + with pytest.raises(ValueError): + DaskMetaSimulation(sims[:-1], mappings, client) + + # Bad Simulation type? + with pytest.raises(TypeError): + DaskRepeatedSimulation( + len(sims) + * [ + lambda x: x * 2, + ], + mappings, + client, + ) + + # 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) + + # incompatible mapping and simulation + mappings[0] = maps.Projection(mesh.n_cells, [0, 1, 3, 5, 10]) + with pytest.raises(ValueError): + DaskMetaSimulation(sims, mappings, client) + + +def test_sum_errors(cluster): + with Client(cluster) as client: + mesh = TensorMesh([16, 16, 16], origin="CCN") + + mesh_bot = TensorMesh([mesh.h[0], mesh.h[1], mesh.h[2][:8]], origin=mesh.origin) + mesh_top = TensorMesh( + [mesh.h[0], mesh.h[1], mesh.h[2][8:]], origin=["C", "C", mesh.nodes_z[8]] + ) + + mappings = [ + maps.Mesh2Mesh((mesh_bot, mesh)), + maps.Mesh2Mesh((mesh_top, mesh)), + ] + + rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T + + rx1 = gravity.Point(rx_locs, components=["gz"]) + survey1 = gravity.Survey(gravity.SourceField(rx1)) + rx2 = gravity.Point(rx_locs[1:], components=["gz"]) + survey2 = gravity.Survey(gravity.SourceField(rx2)) + + sims = [ + gravity.Simulation3DIntegral( + mesh_bot, + survey=survey1, + rhoMap=maps.IdentityMap(mesh_bot), + n_processes=1, + ), + gravity.Simulation3DIntegral( + mesh_top, + survey=survey2, + rhoMap=maps.IdentityMap(mesh_top), + n_processes=1, + ), + ] + + # Test simulations with different numbers of data. + with pytest.raises(ValueError): + DaskSumMetaSimulation(sims, mappings, client) + + +def test_repeat_errors(cluster): + with Client(cluster) as client: + mesh = TensorMesh([16, 16, 16], origin="CCN") + + rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j] + rx_locs = rx_locs.reshape(3, -1).T + rxs = dc.receivers.Pole(rx_locs) + source_locs = np.mgrid[-0.5:0.5:10j, 0:1:1j, 0:1:1j].reshape(3, -1).T + src_list = [ + dc.sources.Pole( + [ + rxs, + ], + location=loc, + ) + for loc in source_locs + ] + survey = dc.Survey(src_list) + sim = dc.Simulation3DNodal(mesh, survey=survey, sigmaMap=maps.IdentityMap(mesh)) + + # split by chunks of sources + mappings = [] + for _i in range(10): + mappings.append(maps.IdentityMap(mesh)) + + # mappings have incompatible input lengths: + mappings[0] = maps.Projection(mesh.n_cells + 1, np.arange(mesh.n_cells) + 1) + with pytest.raises(ValueError): + DaskRepeatedSimulation(sim, mappings, client) + + # incompatible mappings and simulations + mappings[0] = maps.Projection(mesh.n_cells, [0, 1, 3, 5, 10]) + with pytest.raises(ValueError): + DaskRepeatedSimulation(sim, mappings, client) + + # Bad Simulation type? + with pytest.raises(TypeError): + DaskRepeatedSimulation(lambda x: x * 2, mappings, client) diff --git a/tests/meta/test_meta_sim.py b/tests/meta/test_meta_sim.py index f5df798439..bb88591156 100644 --- a/tests/meta/test_meta_sim.py +++ b/tests/meta/test_meta_sim.py @@ -1,12 +1,12 @@ import numpy as np -from SimPEG.potential_fields import gravity -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG import maps +from simpeg.potential_fields import gravity +from simpeg.electromagnetics.static import resistivity as dc +from simpeg import maps from discretize import TensorMesh import scipy.sparse as sp import pytest -from SimPEG.meta import MetaSimulation, SumMetaSimulation, RepeatedSimulation +from simpeg.meta import MetaSimulation, SumMetaSimulation, RepeatedSimulation def test_multi_sim_correctness(): @@ -61,14 +61,15 @@ def test_multi_sim_correctness(): np.testing.assert_allclose(d_full, d_mult) # test Jvec - u = np.random.rand(mesh.n_cells) + rng = np.random.default_rng(seed=0) + u = rng.random(mesh.n_cells) jvec_full = full_sim.Jvec(m_test, u, f=f_full) jvec_mult = multi_sim.Jvec(m_test, u, f=f_mult) np.testing.assert_allclose(jvec_full, jvec_mult) # test Jtvec - v = np.random.rand(survey_full.nD) + v = rng.random(survey_full.nD) jtvec_full = full_sim.Jtvec(m_test, v, f=f_full) jtvec_mult = multi_sim.Jtvec(m_test, v, f=f_mult) @@ -133,26 +134,27 @@ def test_sum_sim_correctness(): # test fields objects f_full = full_sim.fields(m_test) f_mult = sum_sim.fields(m_test) - np.testing.assert_allclose(f_full, sum(f_mult)) + np.testing.assert_allclose(f_full, sum(f_mult), rtol=1e-6) # test data output d_full = full_sim.dpred(m_test, f=f_full) d_mult = sum_sim.dpred(m_test, f=f_mult) - np.testing.assert_allclose(d_full, d_mult) + np.testing.assert_allclose(d_full, d_mult, rtol=1e-6) # test Jvec - u = np.random.rand(mesh.n_cells) + rng = np.random.default_rng(seed=0) + u = rng.random(mesh.n_cells) jvec_full = full_sim.Jvec(m_test, u, f=f_full) jvec_mult = sum_sim.Jvec(m_test, u, f=f_mult) - np.testing.assert_allclose(jvec_full, jvec_mult) + np.testing.assert_allclose(jvec_full, jvec_mult, rtol=1e-6) # test Jtvec - v = np.random.rand(survey.nD) + v = rng.random(survey.nD) jtvec_full = full_sim.Jtvec(m_test, v, f=f_full) jtvec_mult = sum_sim.Jtvec(m_test, v, f=f_mult) - np.testing.assert_allclose(jtvec_full, jtvec_mult) + np.testing.assert_allclose(jtvec_full, jtvec_mult, rtol=1e-6) # test get diag diag_full = full_sim.getJtJdiag(m_test, f=f_full) @@ -218,7 +220,8 @@ def test_repeat_sim_correctness(): multi_sim = MetaSimulation(simulations, mappings) repeat_sim = RepeatedSimulation(sim, mappings) - model = np.random.rand(time_mesh.n_cells, mesh.n_cells).reshape(-1) + rng = np.random.default_rng(seed=0) + model = rng.random((time_mesh.n_cells, mesh.n_cells)).reshape(-1) # test field things f_full = multi_sim.fields(model) @@ -230,13 +233,13 @@ def test_repeat_sim_correctness(): np.testing.assert_equal(d_full, d_repeat) # test Jvec - u = np.random.rand(len(model)) + u = rng.random(len(model)) jvec_full = multi_sim.Jvec(model, u, f=f_full) jvec_mult = repeat_sim.Jvec(model, u, f=f_mult) np.testing.assert_allclose(jvec_full, jvec_mult) # test Jtvec - v = np.random.rand(len(sim_ts) * survey.nD) + v = rng.random(len(sim_ts) * survey.nD) jtvec_full = multi_sim.Jtvec(model, v, f=f_full) jtvec_mult = repeat_sim.Jtvec(model, v, f=f_mult) np.testing.assert_allclose(jtvec_full, jtvec_mult) diff --git a/tests/meta/test_multiprocessing_sim.py b/tests/meta/test_multiprocessing_sim.py new file mode 100644 index 0000000000..eaabf64f6f --- /dev/null +++ b/tests/meta/test_multiprocessing_sim.py @@ -0,0 +1,257 @@ +import numpy as np +import multiprocessing as mp +import sys + +from simpeg.potential_fields import gravity +from simpeg.electromagnetics.static import resistivity as dc +from simpeg import maps +from discretize import TensorMesh +import scipy.sparse as sp + +from simpeg.meta import ( + MetaSimulation, + SumMetaSimulation, + RepeatedSimulation, + MultiprocessingMetaSimulation, + MultiprocessingSumMetaSimulation, + 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") + + rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j] + rx_locs = rx_locs.reshape(3, -1).T + rxs = dc.receivers.Pole(rx_locs) + source_locs = np.mgrid[-0.5:0.5:10j, 0:1:1j, 0:1:1j].reshape(3, -1).T + src_list = [ + dc.sources.Pole( + [ + rxs, + ], + location=loc, + ) + for loc in source_locs + ] + + m_test = np.arange(mesh.n_cells) / mesh.n_cells + 0.1 + + # split by chunks of sources + chunk_size = 3 + dc_sims = [] + dc_sims2 = [] + dc_mappings = [] + for i in range(0, len(src_list) + 1, chunk_size): + end = min(i + chunk_size, len(src_list)) + if i == end: + break + survey_chunk = dc.Survey(src_list[i:end]) + dc_sims.append( + dc.Simulation3DNodal(mesh, survey=survey_chunk, sigmaMap=maps.IdentityMap()) + ) + dc_sims2.append( + dc.Simulation3DNodal(mesh, survey=survey_chunk, sigmaMap=maps.IdentityMap()) + ) + dc_mappings.append(maps.IdentityMap()) + + serial_sim = MetaSimulation(dc_sims, dc_mappings) + parallel_sim = MultiprocessingMetaSimulation(dc_sims2, dc_mappings, n_processes=12) + + rng = np.random.default_rng(seed=0) + + try: + # create fields objects + f_serial = serial_sim.fields(m_test) + f_parallel = parallel_sim.fields(m_test) + + # test data output + d_full = serial_sim.dpred(m_test, f=f_serial) + d_mult = parallel_sim.dpred(m_test, f=f_parallel) + np.testing.assert_allclose(d_full, d_mult) + + # test Jvec + u = rng.random(mesh.n_cells) + jvec_full = serial_sim.Jvec(m_test, u, f=f_serial) + jvec_mult = parallel_sim.Jvec(m_test, u, f=f_parallel) + np.testing.assert_allclose(jvec_full, jvec_mult) + + # test Jtvec + v = rng.random(serial_sim.survey.nD) + jtvec_full = serial_sim.Jtvec(m_test, v, f=f_serial) + jtvec_mult = parallel_sim.Jtvec(m_test, v, f=f_parallel) + + np.testing.assert_allclose(jtvec_full, jtvec_mult) + + # test get diag + diag_full = serial_sim.getJtJdiag(m_test, f=f_serial) + diag_mult = parallel_sim.getJtJdiag(m_test, f=f_parallel) + + np.testing.assert_allclose(diag_full, diag_mult) + + # test things also works without passing optional fields + parallel_sim.model = m_test + d_mult2 = parallel_sim.dpred() + np.testing.assert_allclose(d_mult, d_mult2) + + jvec_mult2 = parallel_sim.Jvec(m_test, u) + np.testing.assert_allclose(jvec_mult, jvec_mult2) + + jtvec_mult2 = parallel_sim.Jtvec(m_test, v) + np.testing.assert_allclose(jtvec_mult, jtvec_mult2) + + # also pass a diagonal matrix here for testing. + parallel_sim._jtjdiag = None + W = sp.eye(parallel_sim.survey.nD) + diag_mult2 = parallel_sim.getJtJdiag(m_test, W=W) + np.testing.assert_allclose(diag_mult, diag_mult2) + except Exception as err: + raise err + finally: + parallel_sim.join() + + +def test_sum_correctness(): + mesh = TensorMesh([16, 16, 16], origin="CCN") + # Create gravity sum sims + rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T + rx = gravity.Point(rx_locs, components=["gz"]) + survey = gravity.Survey(gravity.SourceField(rx)) + + mesh_bot = TensorMesh([mesh.h[0], mesh.h[1], mesh.h[2][:8]], origin=mesh.origin) + mesh_top = TensorMesh( + [mesh.h[0], mesh.h[1], mesh.h[2][8:]], origin=["C", "C", mesh.nodes_z[8]] + ) + + g_mappings = [ + maps.Mesh2Mesh((mesh_bot, mesh)), + maps.Mesh2Mesh((mesh_top, mesh)), + ] + g_sims = [ + gravity.Simulation3DIntegral( + mesh_bot, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 + ), + gravity.Simulation3DIntegral( + mesh_top, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 + ), + ] + + m_test = np.arange(mesh.n_cells) / mesh.n_cells + 0.1 + + serial_sim = SumMetaSimulation(g_sims, g_mappings) + parallel_sim = MultiprocessingSumMetaSimulation(g_sims, g_mappings, n_processes=2) + + rng = np.random.default_rng(0) + try: + # test fields objects + f_serial = serial_sim.fields(m_test) + f_parallel = parallel_sim.fields(m_test) + # np.testing.assert_allclose(f_serial, sum(f_parallel)) + + # test data output + d_full = serial_sim.dpred(m_test, f=f_serial) + d_mult = parallel_sim.dpred(m_test, f=f_parallel) + np.testing.assert_allclose(d_full, d_mult, rtol=1e-06) + + # test Jvec + u = rng.random(mesh.n_cells) + jvec_full = serial_sim.Jvec(m_test, u, f=f_serial) + jvec_mult = parallel_sim.Jvec(m_test, u, f=f_parallel) + + np.testing.assert_allclose(jvec_full, jvec_mult, rtol=1e-06) + + # test Jtvec + v = rng.random(survey.nD) + jtvec_full = serial_sim.Jtvec(m_test, v, f=f_serial) + jtvec_mult = parallel_sim.Jtvec(m_test, v, f=f_parallel) + + np.testing.assert_allclose(jtvec_full, jtvec_mult, rtol=1e-06) + + # test get diag + diag_full = serial_sim.getJtJdiag(m_test, f=f_serial) + diag_mult = parallel_sim.getJtJdiag(m_test, f=f_parallel) + + np.testing.assert_allclose(diag_full, diag_mult, rtol=1e-06) + + # test things also works without passing optional kwargs + parallel_sim.model = m_test + d_mult2 = parallel_sim.dpred() + np.testing.assert_allclose(d_mult, d_mult2, rtol=1e-06) + + jvec_mult2 = parallel_sim.Jvec(m_test, u) + np.testing.assert_allclose(jvec_mult, jvec_mult2, rtol=1e-06) + + jtvec_mult2 = parallel_sim.Jtvec(m_test, v) + np.testing.assert_allclose(jtvec_mult, jtvec_mult2, rtol=1e-06) + + parallel_sim._jtjdiag = None + diag_mult2 = parallel_sim.getJtJdiag(m_test) + np.testing.assert_allclose(diag_mult, diag_mult2, rtol=1e-06) + + except Exception as err: + raise err + finally: + parallel_sim.join() + + +def test_repeat_correctness(): + mesh = TensorMesh([16, 16, 16], origin="CCN") + rx_locs = np.mgrid[-0.25:0.25:5j, -0.25:0.25:5j, 0:1:1j].reshape(3, -1).T + rx = gravity.Point(rx_locs, components=["gz"]) + survey = gravity.Survey(gravity.SourceField(rx)) + grav_sim = gravity.Simulation3DIntegral( + mesh, survey=survey, rhoMap=maps.IdentityMap(), n_processes=1 + ) + + time_mesh = TensorMesh([8], origin=[0]) + sim_ts = np.linspace(0, 1, 6) + + repeat_mappings = [] + eye = sp.eye(mesh.n_cells, mesh.n_cells) + for t in sim_ts: + ave_time = time_mesh.get_interpolation_matrix([t]) + ave_full = sp.kron(ave_time, eye, format="csr") + repeat_mappings.append(maps.LinearMap(ave_full)) + + serial_sim = RepeatedSimulation(grav_sim, repeat_mappings) + parallel_sim = MultiprocessingRepeatedSimulation( + grav_sim, repeat_mappings, n_processes=2 + ) + + rng = np.random.default_rng(0) + + t_model = rng.random((time_mesh.n_cells, mesh.n_cells)).reshape(-1) + + try: + # test field things + f_serial = serial_sim.fields(t_model) + f_parallel = parallel_sim.fields(t_model) + # np.testing.assert_equal(np.c_[f_serial], np.c_[f_parallel]) + + d_full = serial_sim.dpred(t_model, f_serial) + d_repeat = parallel_sim.dpred(t_model, f_parallel) + np.testing.assert_allclose(d_full, d_repeat, rtol=1e-6) + + # test Jvec + u = rng.random(len(t_model)) + jvec_full = serial_sim.Jvec(t_model, u, f=f_serial) + jvec_mult = parallel_sim.Jvec(t_model, u, f=f_parallel) + np.testing.assert_allclose(jvec_full, jvec_mult, rtol=1e-6) + + # test Jtvec + v = rng.random(len(sim_ts) * survey.nD) + jtvec_full = serial_sim.Jtvec(t_model, v, f=f_serial) + jtvec_mult = parallel_sim.Jtvec(t_model, v, f=f_parallel) + np.testing.assert_allclose(jtvec_full, jtvec_mult, rtol=1e-6) + + # test get diag + diag_full = serial_sim.getJtJdiag(t_model, f=f_serial) + diag_mult = parallel_sim.getJtJdiag(t_model, f=f_parallel) + np.testing.assert_allclose(diag_full, diag_mult, rtol=1e-6) + except Exception as err: + raise err + finally: + parallel_sim.join() diff --git a/tests/pf/test_base_pf_simulation.py b/tests/pf/test_base_pf_simulation.py new file mode 100644 index 0000000000..9cf7964ee4 --- /dev/null +++ b/tests/pf/test_base_pf_simulation.py @@ -0,0 +1,306 @@ +""" +Test BasePFSimulation class +""" + +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 = 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_invalid_mesh(self, tensor_mesh, mock_simulation_class): + """ + Test error on invalid mesh class + """ + # Initialize base simulation with valid mesh (so we don't trigger + # errors in the constructor) + simulation = mock_simulation_class(tensor_mesh) + # Assign an invalid mesh to the simulation + simulation.mesh = CylindricalMesh(tensor_mesh.h) + msg = "Invalid mesh of type CylindricalMesh." + with pytest.raises(TypeError, match=msg): + simulation._get_active_nodes() + + 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, ind_active=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, ind_active=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 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 = TensorMesh(h, "CC") + else: + mesh = TreeMesh(h, origin="CC") + mesh.finalize() + return mesh + + def test_invalid_mesh_with_choclo(self, mesh, mock_simulation_class): + """ + Test if simulation raises error when passing an invalid mesh and using choclo + """ + msg = ( + "Invalid mesh with 2 dimensions. " + "Only 3D meshes are supported when using 'choclo' as engine." + ) + with pytest.raises(ValueError, match=msg): + mock_simulation_class(mesh, engine="choclo") diff --git a/tests/pf/test_forward_Grav_Linear.py b/tests/pf/test_forward_Grav_Linear.py index ac86d23e20..32a964710a 100644 --- a/tests/pf/test_forward_Grav_Linear.py +++ b/tests/pf/test_forward_Grav_Linear.py @@ -1,28 +1,63 @@ -import unittest +import pytest import discretize -from SimPEG import maps -from SimPEG.potential_fields import gravity +import simpeg +from simpeg import maps +from simpeg.potential_fields import gravity from geoana.gravity import Prism import numpy as np -import os -def test_ana_grav_forward(tmp_path): - nx = 5 - ny = 5 - - # 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): +class TestsGravitySimulation: + """ + Test gravity simulation. + """ + + @pytest.fixture + def blocks(self): + """Synthetic blocks to build the sample model.""" + block1 = np.array([[-1.6, 1.6], [-1.6, 1.6], [-1.6, 1.6]]) + block2 = np.array([[-0.8, 0.8], [-0.8, 0.8], [-0.8, 0.8]]) + rho1 = 1.0 + rho2 = 2.0 + return (block1, block2), (rho1, rho2) + + @pytest.fixture(params=("tensormesh", "treemesh")) + def mesh(self, blocks, request): + """Sample mesh.""" + cs = 0.2 + (block1, _), _ = blocks + if request.param == "tensormesh": + hxind, hyind, hzind = tuple([(cs, 42)] for _ in range(3)) + mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") + else: + h = cs * np.ones(64) + mesh = discretize.TreeMesh([h, h, h], origin="CCC") + x0, x1 = block1[:, 0], block1[:, 1] + mesh.refine_box(x0, x1, levels=9) + return mesh + + @pytest.fixture + def simple_mesh(self): + """Simpler sample mesh, just to use it as a placeholder in some tests.""" + return discretize.TensorMesh([5, 5, 5], "CCC") + + @pytest.fixture + def density_and_active_cells(self, mesh, blocks): + """Sample density and active_cells arrays for the sample mesh.""" + # create a model of two blocks, 1 inside the other + (block1, block2), (rho1, rho2) = blocks + block1_inds = self.get_block_inds(mesh.cell_centers, block1) + block2_inds = self.get_block_inds(mesh.cell_centers, block2) + # Define densities for each block + model = np.zeros(mesh.n_cells) + model[block1_inds] = rho1 + model[block2_inds] = rho2 + # Define active cells and reduce model + active_cells = model != 0.0 + model_reduced = model[active_cells] + return model_reduced, active_cells + + def get_block_inds(self, grid, block): return np.where( (grid[:, 0] > block[0, 0]) & (grid[:, 0] < block[0, 1]) @@ -32,151 +67,404 @@ def get_block_inds(grid, block): & (grid[:, 2] < block[2, 1]) ) - block1_inds = get_block_inds(mesh.cell_centers, block1) - block2_inds = get_block_inds(mesh.cell_centers, block2) - - rho1 = 1.0 - rho2 = 2.0 - - model = np.zeros(mesh.n_cells) - model[block1_inds] = rho1 - model[block2_inds] = rho2 - - active_cells = model != 0.0 - model_reduced = model[active_cells] - - # Create reduced identity map for Linear Pproblem - 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)] - - receivers = gravity.Point(locXyz, components=["gx", "gy", "gz"]) - sources = gravity.SourceField([receivers]) - survey = gravity.Survey(sources) - - sim = gravity.Simulation3DIntegral( + @pytest.fixture + def receivers_locations(self): + nx = 5 + ny = 5 + # 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 + receivers_locations = np.vstack([a.ravel() for a in (x, y, z)]).T + return receivers_locations + + def get_analytic_solution(self, blocks, survey): + """Compute analytical response from dense prism.""" + (block1, block2), (rho1, rho2) = blocks + # Build prisms (convert densities from g/cc to kg/m3) + prisms = [ + Prism(block1[:, 0], block1[:, 1], rho1 * 1000), + Prism(block2[:, 0], block2[:, 1], -rho1 * 1000), + Prism(block2[:, 0], block2[:, 1], rho2 * 1000), + ] + # Forward model the prisms + components = survey.source_field.receiver_list[0].components + receivers_locations = survey.source_field.receiver_list[0].locations + if "gx" in components or "gy" in components or "gz" in components: + fields = sum( + prism.gravitational_field(receivers_locations) for prism in prisms + ) + fields *= 1e5 # convert to mGal from m/s^2 + else: + fields = sum( + prism.gravitational_gradient(receivers_locations) for prism in prisms + ) + fields *= 1e9 # convert to Eotvos from 1/s^2 + return fields + + @pytest.mark.parametrize( + "engine, parallelism", + [("geoana", None), ("geoana", 1), ("choclo", False), ("choclo", True)], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], + ) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_accelerations_vs_analytic( + self, + engine, + parallelism, + store_sensitivities, + tmp_path, + blocks, mesh, - survey=survey, - rhoMap=idenMap, - ind_active=active_cells, - store_sensitivities="disk", - sensitivity_path=str(tmp_path) + os.sep, + density_and_active_cells, + receivers_locations, + ): + """ + Test gravity acceleration components against analytic solutions of prisms. + """ + components = ["gx", "gy", "gz"] + # Unpack fixtures + density, active_cells = density_and_active_cells + # Create survey + receivers = gravity.Point(receivers_locations, components=components) + sources = gravity.SourceField([receivers]) + survey = gravity.Survey(sources) + # Create reduced identity map for Linear Problem + idenMap = maps.IdentityMap(nP=int(sum(active_cells))) + # Create simulation + if engine == "choclo": + sensitivity_path = tmp_path / "sensitivity_choclo" + kwargs = dict(numba_parallel=parallelism) + else: + sensitivity_path = tmp_path + kwargs = dict(n_processes=parallelism) + sim = gravity.Simulation3DIntegral( + mesh, + survey=survey, + rhoMap=idenMap, + ind_active=active_cells, + store_sensitivities=store_sensitivities, + engine=engine, + sensitivity_path=str(sensitivity_path), + sensitivity_dtype=np.float64, + **kwargs, + ) + data = sim.dpred(density) + g_x, g_y, g_z = data[0::3], data[1::3], data[2::3] + solution = self.get_analytic_solution(blocks, survey) + # Check results + rtol, atol = 1e-9, 1e-6 + np.testing.assert_allclose(g_x, solution[:, 0], rtol=rtol, atol=atol) + np.testing.assert_allclose(g_y, solution[:, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(g_z, solution[:, 2], rtol=rtol, atol=atol) + + @pytest.mark.parametrize( + "engine, parallelism", + [("geoana", None), ("geoana", 1), ("choclo", False), ("choclo", True)], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], ) - - data = sim.dpred(model_reduced) - d_x = data[0::3] - d_y = data[1::3] - d_z = data[2::3] - - # Compute analytical response from dense prism - prism_1 = Prism(block1[:, 0], block1[:, 1], rho1 * 1000) # g/cc to kg/m**3 - prism_2 = Prism(block2[:, 0], block2[:, 1], -rho1 * 1000) - prism_3 = Prism(block2[:, 0], block2[:, 1], rho2 * 1000) - - d = ( - prism_1.gravitational_field(locXyz) - + prism_2.gravitational_field(locXyz) - + prism_3.gravitational_field(locXyz) - ) * 1e5 # convert to mGal from m/s^2 - np.testing.assert_allclose(d_x, d[:, 0], rtol=1e-10, atol=1e-14) - np.testing.assert_allclose(d_y, d[:, 1], rtol=1e-10, atol=1e-14) - np.testing.assert_allclose(d_z, d[:, 2], rtol=1e-10, atol=1e-14) - - -def test_ana_gg_forward(): - nx = 5 - ny = 5 - - # 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]) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_tensor_vs_analytic( + self, + engine, + parallelism, + store_sensitivities, + tmp_path, + blocks, + mesh, + density_and_active_cells, + receivers_locations, + ): + """ + Test tensor components against analytic solutions of prisms. + """ + components = ["gxx", "gxy", "gxz", "gyy", "gyz", "gzz"] + # Unpack fixtures + density, active_cells = density_and_active_cells + # Create survey + receivers = gravity.Point(receivers_locations, components=components) + sources = gravity.SourceField([receivers]) + survey = gravity.Survey(sources) + # Create reduced identity map for Linear Problem + idenMap = maps.IdentityMap(nP=int(sum(active_cells))) + # Create simulation + if engine == "choclo": + sensitivity_path = tmp_path / "sensitivity_choclo" + kwargs = dict(numba_parallel=parallelism) + else: + sensitivity_path = tmp_path + kwargs = dict(n_processes=parallelism) + sim = gravity.Simulation3DIntegral( + mesh, + survey=survey, + rhoMap=idenMap, + ind_active=active_cells, + store_sensitivities=store_sensitivities, + engine=engine, + sensitivity_path=str(sensitivity_path), + sensitivity_dtype=np.float64, + **kwargs, ) - - block1_inds = get_block_inds(mesh.cell_centers, block1) - block2_inds = get_block_inds(mesh.cell_centers, block2) - - rho1 = 1.0 - rho2 = 2.0 - - model = np.zeros(mesh.n_cells) - model[block1_inds] = rho1 - model[block2_inds] = rho2 - - active_cells = model != 0.0 - model_reduced = model[active_cells] - - # Create reduced identity map for Linear Pproblem - 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)] - - receivers = gravity.Point( - locXyz, components=["gxx", "gxy", "gxz", "gyy", "gyz", "gzz"] + data = sim.dpred(density) + g_xx, g_xy, g_xz = data[0::6], data[1::6], data[2::6] + g_yy, g_yz, g_zz = data[3::6], data[4::6], data[5::6] + solution = self.get_analytic_solution(blocks, survey) + # Check results + rtol, atol = 2e-6, 1e-6 + np.testing.assert_allclose(g_xx, solution[..., 0, 0], rtol=rtol, atol=atol) + np.testing.assert_allclose(g_xy, solution[..., 0, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(g_xz, solution[..., 0, 2], rtol=rtol, atol=atol) + np.testing.assert_allclose(g_yy, solution[..., 1, 1], rtol=rtol, atol=atol) + np.testing.assert_allclose(g_yz, solution[..., 1, 2], rtol=rtol, atol=atol) + np.testing.assert_allclose(g_zz, solution[..., 2, 2], rtol=rtol, atol=atol) + + @pytest.mark.parametrize( + "engine, parallelism", + [("geoana", 1), ("geoana", None), ("choclo", False), ("choclo", True)], + ids=["geoana_serial", "geoana_parallel", "choclo_serial", "choclo_parallel"], ) - sources = gravity.SourceField([receivers]) - survey = gravity.Survey(sources) - - sim = gravity.Simulation3DIntegral( + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_guv_vs_analytic( + self, + engine, + parallelism, + store_sensitivities, + tmp_path, + blocks, mesh, - survey=survey, - rhoMap=idenMap, - ind_active=active_cells, - store_sensitivities="forward_only", - n_processes=None, + density_and_active_cells, + receivers_locations, + ): + """ + Test guv tensor component against analytic solutions of prisms. + """ + components = ["guv"] + # Unpack fixtures + density, active_cells = density_and_active_cells + # Create survey + receivers = gravity.Point(receivers_locations, components=components) + sources = gravity.SourceField([receivers]) + survey = gravity.Survey(sources) + # Create reduced identity map for Linear Problem + idenMap = maps.IdentityMap(nP=int(sum(active_cells))) + # Create simulation + if engine == "choclo": + sensitivity_path = tmp_path / "sensitivity_choclo" + kwargs = dict(numba_parallel=parallelism) + else: + sensitivity_path = tmp_path + kwargs = dict(n_processes=parallelism) + sim = gravity.Simulation3DIntegral( + mesh, + survey=survey, + rhoMap=idenMap, + ind_active=active_cells, + store_sensitivities=store_sensitivities, + engine=engine, + sensitivity_path=str(sensitivity_path), + sensitivity_dtype=np.float64, + **kwargs, + ) + g_uv = sim.dpred(density) + solution = self.get_analytic_solution(blocks, survey) + g_xx_solution = solution[..., 0, 0] + g_yy_solution = solution[..., 1, 1] + g_uv_solution = 0.5 * (g_yy_solution - g_xx_solution) + # Check results + rtol, atol = 2e-6, 1e-6 + np.testing.assert_allclose(g_uv, g_uv_solution, rtol=rtol, atol=atol) + + @pytest.mark.parametrize("engine", ("choclo", "geoana")) + @pytest.mark.parametrize("store_sensitivities", ("ram", "disk", "forward_only")) + def test_sensitivity_dtype( + self, + engine, + store_sensitivities, + simple_mesh, + receivers_locations, + tmp_path, + ): + """Test sensitivity_dtype.""" + # Create survey + receivers = gravity.Point(receivers_locations, components="gz") + sources = gravity.SourceField([receivers]) + survey = gravity.Survey(sources) + # Create reduced identity map for Linear Problem + active_cells = np.ones(simple_mesh.n_cells, dtype=bool) + idenMap = maps.IdentityMap(nP=simple_mesh.n_cells) + # Create simulation + sensitivity_path = tmp_path + if engine == "choclo": + sensitivity_path /= "dummy" + simulation = gravity.Simulation3DIntegral( + simple_mesh, + survey=survey, + rhoMap=idenMap, + ind_active=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, simple_mesh, invalid_dtype): + """ + Test invalid sensitivity_dtype assignment + """ + simulation = gravity.Simulation3DIntegral( + simple_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, simple_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): + gravity.Simulation3DIntegral(simple_mesh, engine=engine) + + def test_choclo_and_n_proceesses(self, simple_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 = gravity.Simulation3DIntegral( + simple_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, simple_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 = f"The passed sensitivity_path '{str(sensitivity_path)}' is a directory" + with pytest.raises(ValueError, match=msg): + gravity.Simulation3DIntegral( + simple_mesh, + store_sensitivities="disk", + sensitivity_path=str(sensitivity_path), + engine="choclo", + ) + + def test_sensitivities_on_disk(self, simple_mesh, receivers_locations, tmp_path): + """ + Test if sensitivity matrix is correctly being stored in disk when asked + """ + # Build survey + receivers = gravity.Point(receivers_locations, components="gz") + sources = gravity.SourceField([receivers]) + survey = gravity.Survey(sources) + # Build simulation + sensitivities_path = tmp_path / "sensitivities" + simulation = gravity.Simulation3DIntegral( + mesh=simple_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, simple_mesh, receivers_locations, tmp_path): + """ + Test if sensitivity matrix is correctly being allocated in memory when asked + """ + # Build survey + receivers = gravity.Point(receivers_locations, components="gz") + sources = gravity.SourceField([receivers]) + survey = gravity.Survey(sources) + # Build simulation + simulation = gravity.Simulation3DIntegral( + mesh=simple_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, 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.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 TestConversionFactor: + """Test _get_conversion_factor function.""" + + @pytest.mark.parametrize( + "component", + ("gx", "gy", "gz", "gxx", "gyy", "gzz", "gxy", "gxz", "gyz", "guv"), ) - - 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 dense prism - prism_1 = Prism(block1[:, 0], block1[:, 1], rho1 * 1000) # g/cc to kg/m**3 - prism_2 = Prism(block2[:, 0], block2[:, 1], -rho1 * 1000) - prism_3 = Prism(block2[:, 0], block2[:, 1], rho2 * 1000) - - d = ( - prism_1.gravitational_gradient(locXyz) - + prism_2.gravitational_gradient(locXyz) - + prism_3.gravitational_gradient(locXyz) - ) * 1e9 # convert to Eotvos from 1/s^2 - - 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) - - -if __name__ == "__main__": - unittest.main() + def test_conversion_factor(self, component): + """ + Test _get_conversion_factor function with valid components + """ + conversion_factor = gravity.simulation._get_conversion_factor(component) + if len(component) == 2: + assert conversion_factor == 1e5 * 1e3 # SI to mGal and g/cc to kg/m3 + else: + assert conversion_factor == 1e9 * 1e3 # SI to Eotvos and g/cc to kg/m3 + + def test_invalid_conversion_factor(self): + """ + Test invalid conversion factor _get_conversion_factor function + """ + 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 3f412651db..9cf3498238 100644 --- a/tests/pf/test_forward_Mag_Linear.py +++ b/tests/pf/test_forward_Mag_Linear.py @@ -1,112 +1,667 @@ -import unittest +from __future__ import annotations + import discretize -from SimPEG import utils, maps -from SimPEG.potential_fields import magnetics as mag +import numpy as np +import pytest from geoana.em.static import MagneticPrism from scipy.constants import mu_0 -import numpy as np - -def test_ana_mag_forward(): - nx = 5 - ny = 5 - - 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") +import simpeg +from simpeg import maps, utils +from simpeg.potential_fields import magnetics as mag + + +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]) + ) - # 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]) +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, + ) + return mag.Survey(source_field) + + +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 + + 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([[-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]]) + 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"], + ) + @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, ) - 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 Pproblem - 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.SourceField([rxLoc], parameters=H0) - survey = mag.Survey(srcField) + sim = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=identity_map, + ind_active=active_cells, + sensitivity_path=str(tmp_path / f"{engine}"), + store_sensitivities=store_sensitivities, + engine=engine, + **parallel_kwargs, + ) - # Creat reduced identity map for Linear Pproblem - idenMap = maps.IdentityMap(nP=int(sum(active_cells))) + 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], -chi1 * b0 / mu_0) + prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) + + d = ( + prism_1.magnetic_flux_density(receiver_locations) + + prism_2.magnetic_flux_density(receiver_locations) + + prism_3.magnetic_flux_density(receiver_locations) + ) - sim = mag.Simulation3DIntegral( - mesh, - survey=survey, - chiMap=idenMap, - ind_active=active_cells, - store_sensitivities="forward_only", - n_processes=None, + # TMI projection + tmi = sim.tmi_projection + d_t2 = d_x * tmi[0] + d_y * tmi[1] + d_z * tmi[2] + + # Check results + rtol, atol = 1e-7, 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"], + ) + @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, + ind_active=active_cells, + sensitivity_path=str(tmp_path / f"{engine}"), + store_sensitivities=store_sensitivities, + engine=engine, + **parallel_kwargs, + ) + if engine == "choclo": + # gradient simulation not implemented for choclo yet + with pytest.raises(NotImplementedError): + data = sim.dpred(model_reduced) + else: + 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], -chi1 * b0 / mu_0) + prism_3 = MagneticPrism(block2[:, 0], block2[:, 1], chi2 * b0 / mu_0) + + d = ( + prism_1.magnetic_field_gradient(receiver_locations) + + prism_2.magnetic_field_gradient(receiver_locations) + + prism_3.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"], ) + @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, + ) - 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] + sim = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=identity_map, + ind_active=active_cells, + sensitivity_path=str(tmp_path / f"{engine}"), + store_sensitivities=store_sensitivities, + model_type="vector", + engine=engine, + **parallel_kwargs, + ) - 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 + data = sim.dpred(model_reduced).reshape(-1, 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) + # 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], -M1 * np.linalg.norm(b0) / mu_0 + ) + prism_3 = 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_flux_density(receiver_locations) + + prism_2.magnetic_flux_density(receiver_locations) + + prism_3.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"], ) + @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, + ) - 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) + sim = mag.Simulation3DIntegral( + mag_mesh, + survey=survey, + chiMap=identity_map, + ind_active=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) -def test_ana_mag_grad_forward(): - nx = 5 - ny = 5 + # 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], -M1 * np.linalg.norm(b0) / mu_0 + ) + prism_3 = MagneticPrism( + block2[:, 0], block2[:, 1], M2 * np.linalg.norm(b0) / mu_0 + ) - H0 = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-H0[1], H0[2], H0[0]) + d = ( + prism_1.magnetic_flux_density(receiver_locations) + + prism_2.magnetic_flux_density(receiver_locations) + + prism_3.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", ("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, + ind_active=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 = 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, tmp_path): + """ + 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_ana_mag_tmi_grad_forward(): + """ + Test TMI gradiometry using susceptibilities as model + """ + nx = 61 + ny = 61 + + 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 @@ -141,22 +696,29 @@ def get_block_inds(grid, block): active_cells = model != 0.0 model_reduced = model[active_cells] - # Create reduced identity map for Linear Pproblem + # 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 = ["bxx", "bxy", "bxz", "byy", "byz", "bzz"] + components = ["tmi", "tmi_x", "tmi_y", "tmi_z"] rxLoc = mag.Point(locXyz, components=components) - srcField = mag.SourceField([rxLoc], parameters=H0) + srcField = mag.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) survey = mag.Survey(srcField) - # Creat reduced identity map for Linear Pproblem + # Create reduced identity map for Linear Problem idenMap = maps.IdentityMap(nP=int(sum(active_cells))) sim = mag.Simulation3DIntegral( @@ -169,12 +731,10 @@ def get_block_inds(grid, block): ) 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] + 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) @@ -186,184 +746,83 @@ def get_block_inds(grid, block): + 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 = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-H0[1], H0[2], H0[0]) - - 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.SourceField([rxLoc], parameters=H0) - survey = mag.Survey(srcField) - - # Create reduced identity map for Linear Pproblem - idenMap = maps.IdentityMap(nP=int(sum(active_cells)) * 3) - - sim = mag.Simulation3DIntegral( - mesh, - survey=survey, - chiMap=idenMap, - ind_active=active_cells, - store_sensitivities="forward_only", - model_type="vector", - n_processes=None, + tmi_x = ( + d[:, 0, 0] * b0[0] + d[:, 0, 1] * b0[1] + d[:, 0, 2] * b0[2] + ) / h0_amplitude + tmi_y = ( + d[:, 1, 0] * b0[0] + d[:, 1, 1] * b0[1] + d[:, 1, 2] * b0[2] + ) / h0_amplitude + tmi_z = ( + d[:, 2, 0] * b0[0] + d[:, 2, 1] * b0[1] + d[:, 2, 2] * b0[2] + ) / h0_amplitude + 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, ) - - data = sim.dpred(model_reduced).reshape(-1, 4) - - # 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) - - d = ( - prism_1.magnetic_flux_density(locXyz) - + prism_2.magnetic_flux_density(locXyz) - + prism_3.magnetic_flux_density(locXyz) + # 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, ) - 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 = (50000.0, 60.0, 250.0) - b0 = mag.analytics.IDTtoxyz(-H0[1], H0[2], H0[0]) - - 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]) +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 = mag.Point(receivers_locations) + sources = mag.UniformBackgroundField( + receiver_list=[receivers], + amplitude=50_000, + inclination=45.0, + declination=12.0, ) - - 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.SourceField([rxLoc], parameters=H0) - survey = mag.Survey(srcField) - - # Create reduced identity map for Linear Pproblem - idenMap = maps.IdentityMap(nP=int(sum(active_cells)) * 3) - - 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, - ) - - 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) - - d = ( - prism_1.magnetic_flux_density(locXyz) - + prism_2.magnetic_flux_density(locXyz) - + prism_3.magnetic_flux_density(locXyz) + survey = mag.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): + mag.Simulation3DIntegral(mesh, survey, engine="choclo") + + +def test_removed_modeltype(): + """Test if accesing 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], amplitude=50_000, inclination=90, declination=0 ) - d_amp = np.linalg.norm(d, axis=1) - - np.testing.assert_allclose(data, d_amp) - - -if __name__ == "__main__": - unittest.main() + 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): + sim.modelType diff --git a/tests/pf/test_forward_PFproblem.py b/tests/pf/test_forward_PFproblem.py index 53cc321e63..660af07d3f 100644 --- a/tests/pf/test_forward_PFproblem.py +++ b/tests/pf/test_forward_PFproblem.py @@ -1,8 +1,8 @@ import unittest import discretize -from SimPEG import utils, maps -from SimPEG.utils.model_builder import getIndicesSphere -from SimPEG.potential_fields import magnetics as mag +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 @@ -12,7 +12,6 @@ def setUp(self): Inc = 45.0 Dec = 45.0 Btot = 51000 - H0 = (Btot, Inc, Dec) self.b0 = mag.analytics.IDTtoxyz(-Inc, Dec, Btot) @@ -28,7 +27,7 @@ def setUp(self): self.rad = 100 self.sphere_center = [0.0, 0.0, 0.0] - sph_ind = getIndicesSphere(self.sphere_center, self.rad, M.gridCC) + sph_ind = get_indices_sphere(self.sphere_center, self.rad, M.gridCC) chi[sph_ind] = self.chiblk xr = np.linspace(-300, 300, 41) @@ -40,7 +39,12 @@ def setUp(self): 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.SourceField([receivers], parameters=H0) + srcField = mag.UniformBackgroundField( + receiver_list=[receivers], + amplitude=Btot, + inclination=Inc, + declination=Dec, + ) self.survey = mag.Survey(srcField) diff --git a/tests/pf/test_grav_inversion_linear.py b/tests/pf/test_grav_inversion_linear.py index 733cec9bfe..3657c4668d 100644 --- a/tests/pf/test_grav_inversion_linear.py +++ b/tests/pf/test_grav_inversion_linear.py @@ -1,10 +1,9 @@ -import shutil -import unittest +import pytest import numpy as np import discretize from discretize.utils import active_from_xyz -from SimPEG import ( +from simpeg import ( utils, maps, regularization, @@ -14,124 +13,108 @@ directives, inversion, ) -from SimPEG.potential_fields import gravity - - -class GravInvLinProblemTest(unittest.TestCase): - def setUp(self): - # Create a self.mesh - dx = 5.0 - hxind = [(dx, 5, -1.3), (dx, 5), (dx, 5, 1.3)] - hyind = [(dx, 5, -1.3), (dx, 5), (dx, 5, 1.3)] - hzind = [(dx, 5, -1.3), (dx, 6)] - self.mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") - - # Get index of the center - midx = int(self.mesh.shape_cells[0] / 2) - midy = int(self.mesh.shape_cells[1] / 2) - - # Lets create a simple Gaussian topo and set the active cells - [xx, yy] = np.meshgrid(self.mesh.nodes_x, self.mesh.nodes_y) - zz = -np.exp((xx**2 + yy**2) / 75**2) + self.mesh.nodes_z[-1] - - # Go from topo to actv cells - topo = np.c_[utils.mkvc(xx), utils.mkvc(yy), utils.mkvc(zz)] - actv = active_from_xyz(self.mesh, topo, "N") - - # Create active map to go from reduce space to full - self.actvMap = maps.InjectActiveCells(self.mesh, actv, -100) - nC = int(actv.sum()) - - # Create and array of observation points - xr = np.linspace(-20.0, 20.0, 20) - yr = np.linspace(-20.0, 20.0, 20) - X, Y = np.meshgrid(xr, yr) - - # Move the observation points 5m above the topo - Z = -np.exp((X**2 + Y**2) / 75**2) + self.mesh.nodes_z[-1] + 5.0 - - # Create a MAGsurvey - locXYZ = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] - rxLoc = gravity.Point(locXYZ) - srcField = gravity.SourceField([rxLoc]) - survey = gravity.Survey(srcField) - - # We can now create a density model and generate data - # Here a simple block in half-space - model = np.zeros( - ( - self.mesh.shape_cells[0], - self.mesh.shape_cells[1], - self.mesh.shape_cells[2], - ) +from simpeg.potential_fields import gravity + + +@pytest.mark.parametrize("engine", ("geoana", "choclo")) +def test_gravity_inversion_linear(engine): + """Test gravity inversion.""" + # Create a mesh + dx = 5.0 + hxind = [(dx, 5, -1.3), (dx, 5), (dx, 5, 1.3)] + hyind = [(dx, 5, -1.3), (dx, 5), (dx, 5, 1.3)] + hzind = [(dx, 5, -1.3), (dx, 6)] + mesh = discretize.TensorMesh([hxind, hyind, hzind], "CCC") + + # Get index of the center + midx = int(mesh.shape_cells[0] / 2) + midy = int(mesh.shape_cells[1] / 2) + + # Lets create a simple Gaussian topo and set the active cells + [xx, yy] = np.meshgrid(mesh.nodes_x, mesh.nodes_y) + zz = -np.exp((xx**2 + yy**2) / 75**2) + mesh.nodes_z[-1] + + # Go from topo to actv cells + topo = np.c_[utils.mkvc(xx), utils.mkvc(yy), utils.mkvc(zz)] + actv = active_from_xyz(mesh, topo, "N") + nC = int(actv.sum()) + + # Create and array of observation points + xr = np.linspace(-20.0, 20.0, 20) + yr = np.linspace(-20.0, 20.0, 20) + X, Y = np.meshgrid(xr, yr) + + # Move the observation points 5m above the topo + Z = -np.exp((X**2 + Y**2) / 75**2) + mesh.nodes_z[-1] + 5.0 + + # Create a MAGsurvey + locXYZ = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] + rxLoc = gravity.Point(locXYZ) + srcField = gravity.SourceField([rxLoc]) + survey = gravity.Survey(srcField) + + # We can now create a density model and generate data + # Here a simple block in half-space + model = np.zeros( + ( + mesh.shape_cells[0], + mesh.shape_cells[1], + mesh.shape_cells[2], ) - model[(midx - 2) : (midx + 2), (midy - 2) : (midy + 2), -6:-2] = 0.5 - model = utils.mkvc(model) - self.model = model[actv] - - # Create reduced identity map - idenMap = maps.IdentityMap(nP=nC) - - # Create the forward model operator - sim = gravity.Simulation3DIntegral( - self.mesh, - survey=survey, - rhoMap=idenMap, - ind_active=actv, - store_sensitivities="ram", - n_processes=None, - ) - - # Compute linear forward operator and compute some data - data = sim.make_synthetic_data( - self.model, relative_error=0.0, noise_floor=0.0005, add_noise=True - ) - - # Create a regularization - reg = regularization.Sparse(self.mesh, active_cells=actv, mapping=idenMap) - reg.norms = [0, 0, 0, 0] - reg.gradientType = "components" - - # Data misfit function - dmis = data_misfit.L2DataMisfit(simulation=sim, data=data) - - # Add directives to the inversion - opt = optimization.ProjectedGNCG( - maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=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() - update_Jacobi = directives.UpdatePreconditioner() - sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) - self.inv = inversion.BaseInversion( - invProb, - directiveList=[IRLS, sensitivity_weights, starting_beta, update_Jacobi], - ) - self.sim = sim - - def test_grav_inverse(self): - # Run the inversion - mrec = self.inv.run(self.model) - residual = np.linalg.norm(mrec - self.model) / np.linalg.norm(self.model) - - # import matplotlib.pyplot as plt - # plt.figure() - # ax = plt.subplot(1, 2, 1) - # self.mesh.plot_slice(self.actvMap*mrec, ax=ax, clim=(0, 0.5), normal="Y") - # ax = plt.subplot(1, 2, 2) - # self.mesh.plot_slice(self.actvMap*self.model, ax=ax, clim=(0, 0.5), normal="Y") - # plt.show() - - self.assertTrue(residual < 0.05) - - def tearDown(self): - # Clean up the working directory - if self.sim.store_sensitivities == "disk": - shutil.rmtree(self.sim.sensitivity_path) - - -if __name__ == "__main__": - unittest.main() + ) + model[(midx - 2) : (midx + 2), (midy - 2) : (midy + 2), -6:-2] = 0.5 + model = utils.mkvc(model) + model = model[actv] + + # Create reduced identity map + idenMap = maps.IdentityMap(nP=nC) + + # Create the forward model operator + kwargs = dict() + if engine == "geoana": + kwargs["n_processes"] = None + sim = gravity.Simulation3DIntegral( + mesh, + survey=survey, + rhoMap=idenMap, + ind_active=actv, + store_sensitivities="ram", + engine=engine, + **kwargs, + ) + + # Compute linear forward operator and compute some data + data = sim.make_synthetic_data( + model, relative_error=0.0, noise_floor=0.0005, add_noise=True, random_seed=2 + ) + + # Create a regularization + reg = regularization.Sparse(mesh, active_cells=actv, mapping=idenMap) + reg.norms = [0, 0, 0, 0] + reg.gradient_type = "components" + + # Data misfit function + dmis = data_misfit.L2DataMisfit(simulation=sim, data=data) + + # Add directives to the inversion + opt = optimization.ProjectedGNCG( + maxIter=100, lower=-1.0, upper=1.0, maxIterLS=20, maxIterCG=10, tolCG=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() + update_Jacobi = directives.UpdatePreconditioner() + sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) + inv = inversion.BaseInversion( + invProb, + directiveList=[IRLS, sensitivity_weights, starting_beta, update_Jacobi], + ) + + # Run the inversion + mrec = inv.run(model) + residual = np.linalg.norm(mrec - model) / np.linalg.norm(model) + + # Assert result + assert np.all(residual < 0.05) diff --git a/tests/pf/test_gravity_IO.py b/tests/pf/test_gravity_IO.py index 972018f64b..0431047e3d 100644 --- a/tests/pf/test_gravity_IO.py +++ b/tests/pf/test_gravity_IO.py @@ -1,9 +1,9 @@ import unittest import numpy as np -# from SimPEG.potential_fields import gravity -from SimPEG.utils.drivers import GravityDriver_Inv -from SimPEG.utils import io_utils +# from simpeg.potential_fields import gravity +from simpeg.utils.drivers import GravityDriver_Inv +from simpeg.utils import io_utils import shutil import os diff --git a/tests/pf/test_mag_MVI_Octree.py b/tests/pf/test_mag_MVI_Octree.py index 8a4bfc27e0..48aa8e26bb 100644 --- a/tests/pf/test_mag_MVI_Octree.py +++ b/tests/pf/test_mag_MVI_Octree.py @@ -1,5 +1,5 @@ import unittest -from SimPEG import ( +from simpeg import ( directives, maps, inverse_problem, @@ -13,14 +13,13 @@ from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz import numpy as np -from SimPEG.potential_fields import magnetics as mag +from simpeg.potential_fields import magnetics as mag import shutil class MVIProblemTest(unittest.TestCase): def setUp(self): - np.random.seed(0) - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # The magnetization is set along a different # direction (induced + remanence) @@ -46,7 +45,12 @@ def setUp(self): # Create a MAGsurvey xyzLoc = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] rxLoc = mag.Point(xyzLoc) - srcField = mag.SourceField([rxLoc], parameters=H0) + srcField = mag.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) survey = mag.Survey(srcField) # Create a mesh @@ -70,7 +74,7 @@ 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.getIndicesBlock( + ind = utils.model_builder.get_indices_block( np.r_[-20, -20, -10], np.r_[20, 20, 25], mesh.gridCC, @@ -101,7 +105,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=0, ) # This Mapping connects the regularizations for the three-component @@ -144,7 +152,7 @@ def setUp(self): # Pre-conditioner update_Jacobi = directives.UpdatePreconditioner() - sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) + sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) inv = inversion.BaseInversion( invProb, directiveList=[sensitivity_weights, IRLS, update_Jacobi, betaest] ) diff --git a/tests/pf/test_mag_inversion_linear.py b/tests/pf/test_mag_inversion_linear.py index ff8adfdfd7..47d8df2321 100644 --- a/tests/pf/test_mag_inversion_linear.py +++ b/tests/pf/test_mag_inversion_linear.py @@ -1,7 +1,7 @@ import unittest import discretize from discretize.utils import active_from_xyz -from SimPEG import ( +from simpeg import ( utils, maps, regularization, @@ -13,17 +13,15 @@ ) import numpy as np -# import SimPEG.PF as PF -from SimPEG.potential_fields import magnetics as mag +# import simpeg.PF as PF +from simpeg.potential_fields import magnetics as mag import shutil class MagInvLinProblemTest(unittest.TestCase): def setUp(self): - np.random.seed(0) - # Define the inducing field parameter - H0 = (50000, 90, 0) + h0_amplitude, h0_inclination, h0_declination = (50000, 90, 0) # Create a mesh dx = 5.0 @@ -59,7 +57,12 @@ def setUp(self): # Create a MAGsurvey rxLoc = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] rxLoc = mag.Point(rxLoc) - srcField = mag.SourceField([rxLoc], parameters=H0) + srcField = mag.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) survey = mag.Survey(srcField) # We can now create a susceptibility model and generate data @@ -88,13 +91,17 @@ def setUp(self): # Compute linear forward operator and compute some data 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=2, ) # Create a regularization - reg = regularization.Sparse(self.mesh, indActive=actv, mapping=idenMap) + reg = regularization.Sparse(self.mesh, active_cells=actv, mapping=idenMap) reg.norms = [0, 0, 0, 0] - reg.gradientType = "components" + reg.gradient_type = "components" # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=sim, data=data) @@ -110,7 +117,7 @@ def setUp(self): # Here is where the norms are applied IRLS = directives.Update_IRLS(f_min_change=1e-4, minGNiter=1) update_Jacobi = directives.UpdatePreconditioner() - sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) + sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) self.inv = inversion.BaseInversion( invProb, directiveList=[IRLS, sensitivity_weights, betaest, update_Jacobi] ) diff --git a/tests/pf/test_mag_inversion_linear_Octree.py b/tests/pf/test_mag_inversion_linear_Octree.py index 8f4c70e4e3..d754d64e5c 100644 --- a/tests/pf/test_mag_inversion_linear_Octree.py +++ b/tests/pf/test_mag_inversion_linear_Octree.py @@ -2,8 +2,8 @@ import unittest import numpy as np -from discretize.utils import meshutils, active_from_xyz -from SimPEG import ( +from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz +from simpeg import ( directives, maps, inverse_problem, @@ -13,20 +13,18 @@ utils, regularization, ) -from SimPEG.potential_fields import magnetics as mag +from simpeg.potential_fields import magnetics as mag 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. # From old convention, field orientation is given as an # azimuth from North (positive clockwise) # and dip from the horizontal (positive downward). - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # Create a mesh h = [5, 5, 5] @@ -55,18 +53,23 @@ def setUp(self): # Create a MAGsurvey xyzLoc = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] rxLoc = mag.Point(xyzLoc) - srcField = mag.SourceField([rxLoc], parameters=H0) + srcField = mag.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) survey = mag.Survey(srcField) # self.mesh.finalize() - self.mesh = meshutils.mesh_builder_xyz( + self.mesh = mesh_builder_xyz( xyzLoc, h, padding_distance=padDist, mesh_type="TREE", ) - self.mesh = meshutils.refine_tree_xyz( + self.mesh = refine_tree_xyz( self.mesh, topo, method="surface", @@ -81,7 +84,7 @@ def setUp(self): # We can now create a susceptibility model and generate data # Lets start with a simple block in half-space - self.model = utils.model_builder.addBlock( + self.model = utils.model_builder.add_block( self.mesh.gridCC, np.zeros(self.mesh.nC), np.r_[-20, -20, -15], @@ -106,13 +109,17 @@ def setUp(self): ) 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 reg = regularization.Sparse(self.mesh, active_cells=actv, mapping=idenMap) reg.norms = [0, 0, 0, 0] - reg.mref = np.zeros(nC) + reg.reference_model = np.zeros(nC) # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=sim, data=data) diff --git a/tests/pf/test_mag_nonLinear_Amplitude.py b/tests/pf/test_mag_nonLinear_Amplitude.py index cf7f3615ee..d4f11f6ce8 100644 --- a/tests/pf/test_mag_nonLinear_Amplitude.py +++ b/tests/pf/test_mag_nonLinear_Amplitude.py @@ -1,5 +1,5 @@ import numpy as np -from SimPEG import ( +from simpeg import ( data, data_misfit, directives, @@ -10,9 +10,9 @@ regularization, ) -from SimPEG.potential_fields import magnetics -from SimPEG import utils -from SimPEG.utils import mkvc +from simpeg.potential_fields import magnetics +from simpeg import utils +from simpeg.utils import mkvc from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz import unittest import shutil @@ -21,7 +21,7 @@ class AmpProblemTest(unittest.TestCase): def setUp(self): # We will assume a vertical inducing field - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) # The magnetization is set along a different direction (induced + remanence) M = np.array([45.0, 90.0]) @@ -46,8 +46,11 @@ def setUp(self): # Create a MAGsurvey rxLoc = np.c_[mkvc(X.T), mkvc(Y.T), mkvc(Z.T)] receiver_list = magnetics.receivers.Point(rxLoc) - srcField = magnetics.sources.SourceField( - receiver_list=[receiver_list], parameters=H0 + srcField = magnetics.sources.UniformBackgroundField( + receiver_list=[receiver_list], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, ) survey = magnetics.survey.Survey(srcField) @@ -75,14 +78,14 @@ def setUp(self): ) # Get the indicies of the magnetized block - ind = utils.model_builder.getIndicesBlock( + ind = utils.model_builder.get_indices_block( 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 + # be applied in by the :class:`simpeg.PF.Magnetics` problem model = np.zeros(mesh.nC) model[ind] = chi_e @@ -138,9 +141,9 @@ def setUp(self): # Create a regularization function, in this case l2l2 reg = regularization.Sparse( - mesh, indActive=surf, mapping=maps.IdentityMap(nP=nC), alpha_z=0 + mesh, active_cells=surf, mapping=maps.IdentityMap(nP=nC), alpha_z=0 ) - reg.mref = np.zeros(nC) + reg.reference_model = np.zeros(nC) # Specify how the optimization will proceed, set susceptibility bounds to inf opt = optimization.ProjectedGNCG( @@ -185,8 +188,11 @@ def setUp(self): # receiver_list = magnetics.receivers.Point(rxLoc, components=["bx", "by", "bz"]) - srcField = magnetics.sources.SourceField( - receiver_list=[receiver_list], parameters=H0 + srcField = magnetics.sources.UniformBackgroundField( + receiver_list=[receiver_list], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, ) surveyAmp = magnetics.survey.Survey(srcField) @@ -228,9 +234,9 @@ def setUp(self): data_obj = data.Data(survey, dobs=bAmp, noise_floor=wd) # Create a sparse regularization - reg = regularization.Sparse(mesh, indActive=actv, mapping=idenMap) + reg = regularization.Sparse(mesh, active_cells=actv, mapping=idenMap) reg.norms = [1, 0, 0, 0] - reg.mref = np.zeros(nC) + reg.reference_model = np.zeros(nC) # Data misfit function dmis = data_misfit.L2DataMisfit(simulation=simulation, data=data_obj) diff --git a/tests/pf/test_mag_uniform_background_field.py b/tests/pf/test_mag_uniform_background_field.py new file mode 100644 index 0000000000..feeb65e909 --- /dev/null +++ b/tests/pf/test_mag_uniform_background_field.py @@ -0,0 +1,69 @@ +""" +Test the UniformBackgroundField class +""" + +import pytest +import numpy as np +from simpeg.potential_fields.magnetics import UniformBackgroundField, SourceField, Point + + +def test_invalid_parameters_argument(): + """ + Test if error is raised after passing 'parameters' as argument + """ + parameters = (1, 35, 60) + msg = r"__init__\(\) got an unexpected keyword argument 'parameters'" + with pytest.raises(TypeError, match=msg): + UniformBackgroundField(parameters=parameters) + + +def test_deprecated_source_field(): + """ + Test if instantiating a magnetics.source.SourceField object raises an error + """ + msg = "SourceField has been removed, please use UniformBackgroundField." + with pytest.raises(NotImplementedError, match=msg): + SourceField() + + +@pytest.mark.parametrize("receiver_as_list", (True, False)) +def test_invalid_receiver_type(receiver_as_list): + """ + Test if error is raised after passing invalid type of receivers + """ + 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 new file mode 100644 index 0000000000..5115e4a22a --- /dev/null +++ b/tests/pf/test_mag_vector_amplitude.py @@ -0,0 +1,192 @@ +import unittest +from simpeg import ( + directives, + maps, + inverse_problem, + optimization, + data_misfit, + inversion, + utils, + regularization, +) + + +from discretize.utils import mesh_builder_xyz, refine_tree_xyz, active_from_xyz +import numpy as np +from simpeg.potential_fields import magnetics as mag +import shutil + + +class MVIProblemTest(unittest.TestCase): + def setUp(self): + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) + + # The magnetization is set along a different + # direction (induced + remanence) + M = np.array([45.0, 90.0]) + + # Create grid of points for topography + # Lets create a simple Gaussian topo + # and set the active cells + [xx, yy] = np.meshgrid(np.linspace(-200, 200, 50), np.linspace(-200, 200, 50)) + b = 100 + A = 50 + zz = A * np.exp(-0.5 * ((xx / b) ** 2.0 + (yy / b) ** 2.0)) + + # We would usually load a topofile + topo = np.c_[utils.mkvc(xx), utils.mkvc(yy), utils.mkvc(zz)] + + # Create and array of observation points + xr = np.linspace(-100.0, 100.0, 20) + yr = np.linspace(-100.0, 100.0, 20) + X, Y = np.meshgrid(xr, yr) + Z = A * np.exp(-0.5 * ((X / b) ** 2.0 + (Y / b) ** 2.0)) + 5 + + # Create a MAGsurvey + xyzLoc = np.c_[utils.mkvc(X.T), utils.mkvc(Y.T), utils.mkvc(Z.T)] + rxLoc = mag.Point(xyzLoc) + srcField = mag.UniformBackgroundField( + receiver_list=[rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) + survey = mag.Survey(srcField) + + # Create a mesh + h = [5, 5, 5] + padDist = np.ones((3, 2)) * 100 + + 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 + ) + self.mesh = mesh + # Define an active cells from topo + actv = active_from_xyz(mesh, topo) + nC = int(actv.sum()) + + model = np.zeros((mesh.nC, 3)) + + # 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( + 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) + + # Remove air cells + self.model = model[actv, :] + + # Create active map to go from reduce set to full + self.actvMap = maps.InjectActiveCells(mesh, actv, np.nan) + + # Creat reduced identity map + idenMap = maps.IdentityMap(nP=nC * 3) + + # Create the forward model operator + sim = mag.Simulation3DIntegral( + self.mesh, + survey=survey, + model_type="vector", + chiMap=idenMap, + ind_active=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, + random_seed=0, + ) + + reg = regularization.VectorAmplitude( + mesh, + mapping=idenMap, + active_cells=actv, + reference_model_in_smooth=True, + norms=[0.0, 0.0, 0.0, 0.0], + gradient_type="components", + ) + + # Data misfit function + dmis = data_misfit.L2DataMisfit(simulation=sim, data=data) + # dmis.W = 1./survey.std + + # Add directives to the inversion + opt = optimization.ProjectedGNCG( + maxIter=10, lower=-10, upper=10.0, maxIterLS=5, maxIterCG=5, tolCG=1e-4 + ) + + invProb = inverse_problem.BaseInvProblem(dmis, reg, opt) + + # A list of directive to control the inverson + betaest = directives.BetaEstimate_ByEig(beta0_ratio=1e1) + + # 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 + ) + + # Pre-conditioner + update_Jacobi = directives.UpdatePreconditioner() + sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) + self.inv = inversion.BaseInversion( + invProb, directiveList=[sensitivity_weights, IRLS, update_Jacobi, betaest] + ) + + # Run the inversion + self.mstart = np.ones(3 * nC) * 1e-4 # Starting model + + def test_vector_amplitude_inverse(self): + # Run the inversion + mrec = self.inv.run(self.mstart) + + 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) + + # 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() + + self.assertLess(residual, 1) + # self.assertTrue(residual < 0.05) + + def tearDown(self): + # Clean up the working directory + if self.sim.store_sensitivities == "disk": + shutil.rmtree(self.sim.sensitivity_path) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/pf/test_magnetics_IO.py b/tests/pf/test_magnetics_IO.py index 2d686453ce..aa0e386619 100644 --- a/tests/pf/test_magnetics_IO.py +++ b/tests/pf/test_magnetics_IO.py @@ -1,9 +1,9 @@ import unittest import numpy as np -# from SimPEG import Mesh, PF -from SimPEG.utils.drivers import MagneticsDriver_Inv -from SimPEG.utils import io_utils +# from simpeg import Mesh, PF +from simpeg.utils.drivers import MagneticsDriver_Inv +from simpeg.utils import io_utils # from scipy.constants import mu_0 import shutil diff --git a/tests/pf/test_magnetics_analytics.py b/tests/pf/test_magnetics_analytics.py index 2711b76398..c93ac3c27e 100644 --- a/tests/pf/test_magnetics_analytics.py +++ b/tests/pf/test_magnetics_analytics.py @@ -1,9 +1,9 @@ import unittest -# from SimPEG import Mesh, PF +# from simpeg import Mesh, PF import discretize -from SimPEG.potential_fields import magnetics as mag -from SimPEG.utils.model_builder import getIndicesSphere +from simpeg.potential_fields import magnetics as mag +from simpeg.utils.model_builder import get_indices_sphere import numpy as np from scipy.constants import mu_0 @@ -21,7 +21,7 @@ def test_ana_boundary_computation(self): chibkg = 0.0 chiblk = 0.01 chi = np.ones(M3.nC) * chibkg - sph_ind = getIndicesSphere([0, 0, 0], 100, M3.gridCC) + sph_ind = get_indices_sphere([0, 0, 0], 100, M3.gridCC) chi[sph_ind] = chiblk Bbc, const = mag.analytics.CongruousMagBC(M3, np.array([1.0, 0.0, 0.0]), chi) diff --git a/tests/pf/test_pf_quadtree_inversion_linear.py b/tests/pf/test_pf_quadtree_inversion_linear.py index 028966c819..c6c7f64d8f 100644 --- a/tests/pf/test_pf_quadtree_inversion_linear.py +++ b/tests/pf/test_pf_quadtree_inversion_linear.py @@ -5,7 +5,7 @@ from discretize import TensorMesh from discretize.utils import mesh_builder_xyz, mkvc, refine_tree_xyz -from SimPEG import ( +from simpeg import ( data_misfit, directives, inverse_problem, @@ -15,9 +15,7 @@ regularization, utils, ) -from SimPEG.potential_fields import gravity, magnetics - -np.random.seed(44) +from simpeg.potential_fields import gravity, magnetics class QuadTreeLinProblemTest(unittest.TestCase): @@ -101,13 +99,19 @@ def create_gravity_sim_flat(self, block_value=1.0, noise_floor=0.01): relative_error=0.0, noise_floor=noise_floor, add_noise=True, + random_seed=44, ) def create_magnetics_sim_flat(self, block_value=1.0, noise_floor=0.01): # Create a magnetic survey - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) mag_rxLoc = magnetics.Point(data_xyz_flat) - mag_srcField = magnetics.SourceField([mag_rxLoc], parameters=H0) + mag_srcField = magnetics.UniformBackgroundField( + [mag_rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) mag_survey = magnetics.Survey(mag_srcField) # Create the magnetics forward model operator @@ -128,6 +132,7 @@ def create_magnetics_sim_flat(self, block_value=1.0, noise_floor=0.01): relative_error=0.0, noise_floor=noise_floor, add_noise=True, + random_seed=44, ) def create_gravity_sim(self, block_value=1.0, noise_floor=0.01): @@ -154,13 +159,19 @@ def create_gravity_sim(self, block_value=1.0, noise_floor=0.01): relative_error=0.0, noise_floor=noise_floor, add_noise=True, + random_seed=1, ) def create_magnetics_sim(self, block_value=1.0, noise_floor=0.01): # Create a magnetic survey - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) mag_rxLoc = magnetics.Point(data_xyz) - mag_srcField = magnetics.SourceField([mag_rxLoc], parameters=H0) + mag_srcField = magnetics.UniformBackgroundField( + [mag_rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) mag_survey = magnetics.Survey(mag_srcField) # Create the magnetics forward model operator @@ -181,6 +192,7 @@ def create_magnetics_sim(self, block_value=1.0, noise_floor=0.01): relative_error=0.0, noise_floor=noise_floor, add_noise=True, + random_seed=1, ) def create_gravity_sim_active(self, block_value=1.0, noise_floor=0.01): @@ -189,9 +201,6 @@ def create_gravity_sim_active(self, block_value=1.0, noise_floor=0.01): grav_srcField = gravity.SourceField([grav_rxLoc]) grav_survey = gravity.Survey(grav_srcField) - # Set only non-zero cells as active - self.active_cells = ~(self.model == 0.0) - # Create the gravity forward model operator self.grav_sim_active = gravity.SimulationEquivalentSourceLayer( self.mesh, @@ -211,13 +220,19 @@ def create_gravity_sim_active(self, block_value=1.0, noise_floor=0.01): relative_error=0.0, noise_floor=noise_floor, add_noise=True, + random_seed=1, ) def create_magnetics_sim_active(self, block_value=1.0, noise_floor=0.01): # Create a magnetic survey - H0 = (50000.0, 90.0, 0.0) + h0_amplitude, h0_inclination, h0_declination = (50000.0, 90.0, 0.0) mag_rxLoc = magnetics.Point(data_xyz) - mag_srcField = magnetics.SourceField([mag_rxLoc], parameters=H0) + mag_srcField = magnetics.UniformBackgroundField( + receiver_list=[mag_rxLoc], + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, + ) mag_survey = magnetics.Survey(mag_srcField) # Create the magnetics forward model operator @@ -239,6 +254,7 @@ def create_magnetics_sim_active(self, block_value=1.0, noise_floor=0.01): relative_error=0.0, noise_floor=noise_floor, add_noise=True, + random_seed=1, ) def create_inversion(self, sim, data, beta=1e3, all_active=True): @@ -268,7 +284,7 @@ def create_inversion(self, sim, data, beta=1e3, all_active=True): lower=-1.0, upper=1.0, maxIterLS=5, - maxIterCG=10, + maxIterCG=20, tolCG=1e-4, ) @@ -315,7 +331,7 @@ def create_inversion(self, sim, data, beta=1e3, all_active=True): # Create a density model and generate data, # with a block in a half space - self.model = utils.model_builder.addBlock( + self.model = utils.model_builder.add_block( self.mesh.cell_centers, np.zeros(self.mesh.nC), np.r_[-20, -20], @@ -323,9 +339,17 @@ def create_inversion(self, sim, data, beta=1e3, all_active=True): 1.0, ) + self.active_cells = utils.model_builder.add_block( + self.mesh.cell_centers, + np.zeros(self.mesh.nC, dtype=bool), + np.r_[-40, -40], + np.r_[40, 40], + True, + ) + # Set only non-zero cells as active. Some tests use all cells # (by not using `self.active_cells`), and others use the active cells - self.active_cells = ~(self.model == 0.0) + # self.active_cells = ~(self.model == 0.0) # Create reduced identity maps. Two versions: for the all-active # and the active-subset models @@ -440,8 +464,6 @@ def create_xyz_points_flat(x_range, y_range, spacing, altitude=0.0): print("Z_TOP OR Z_BOTTOM LENGTH MATCHING NACTIVE-CELLS ERROR TEST PASSED.") def test_quadtree_grav_inverse(self): - np.random.seed(44) - # Run the inversion from a zero starting model mrec = self.grav_inv.run(np.zeros(self.mesh.nC)) @@ -456,12 +478,10 @@ def test_quadtree_grav_inverse(self): self.assertAlmostEqual(model_residual, 0.1, delta=0.1) # Check data converged to less than 10% of target misfit - data_misfit = 2.0 * self.grav_inv.invProb.dmisfit(self.grav_model) + data_misfit = self.grav_inv.invProb.dmisfit(self.grav_model) self.assertLess(data_misfit, dpred.shape[0] * 1.15) def test_quadtree_mag_inverse(self): - np.random.seed(44) - # Run the inversion from a zero starting model mrec = self.mag_inv.run(np.zeros(self.mesh.nC)) @@ -476,12 +496,10 @@ def test_quadtree_mag_inverse(self): self.assertAlmostEqual(model_residual, 0.01, delta=0.05) # Check data converged to less than 10% of target misfit - data_misfit = 2.0 * self.mag_inv.invProb.dmisfit(self.mag_model) + data_misfit = self.mag_inv.invProb.dmisfit(self.mag_model) self.assertLess(data_misfit, dpred.shape[0] * 1.1) def test_quadtree_grav_inverse_activecells(self): - np.random.seed(44) - # Run the inversion from a zero starting model mrec = self.grav_inv_active.run(np.zeros(int(self.active_cells.sum()))) @@ -495,20 +513,27 @@ def test_quadtree_grav_inverse_activecells(self): # Wide difference in results run locally (0.04) versus the pipeline # (0.21), so seems to need unusually large tolerance. print("MODEL RESIDUAL: {}".format(model_residual)) - self.assertAlmostEqual(model_residual, 0.14, delta=0.1) + self.assertAlmostEqual(model_residual, 0.1, delta=0.1) # Check data converged to less than 10% of target misfit - data_misfit = 2.0 * self.grav_inv_active.invProb.dmisfit( + data_misfit = self.grav_inv_active.invProb.dmisfit( self.grav_model[self.active_cells] ) self.assertLess(data_misfit, dpred.shape[0] * 1.1) def test_quadtree_mag_inverse_activecells(self): - np.random.seed(44) - # Run the inversion from a zero starting model mrec = self.mag_inv_active.run(np.zeros(int(self.active_cells.sum()))) + # import matplotlib.pyplot as plt + # + # fig = plt.figure() + # ax = plt.subplot() + # m_out = np.zeros(self.mesh.nC) * np.nan + # m_out[self.active_cells] = mrec + # self.mesh.plot_image(m_out, ax=ax) + # fig.savefig("mrec.png") + # Compute predicted data dpred = self.mag_sim_active.dpred(self.mag_model[self.active_cells]) @@ -517,10 +542,10 @@ def test_quadtree_mag_inverse_activecells(self): mrec - self.mag_model[self.active_cells] ) / np.linalg.norm(self.mag_model[self.active_cells]) print("MODEL RESIDUAL: {}".format(model_residual)) - self.assertAlmostEqual(model_residual, 0.11, delta=0.05) + self.assertAlmostEqual(model_residual, 0.01, delta=0.05) # Check data converged to less than 10% of target misfit - data_misfit = 2.0 * self.mag_inv_active.invProb.dmisfit( + data_misfit = self.mag_inv_active.invProb.dmisfit( self.mag_model[self.active_cells] ) self.assertLess(data_misfit, dpred.shape[0] * 1.1) diff --git a/tests/pf/test_sensitivity_PFproblem.py b/tests/pf/test_sensitivity_PFproblem.py index e5bbf0d705..eb39c97485 100644 --- a/tests/pf/test_sensitivity_PFproblem.py +++ b/tests/pf/test_sensitivity_PFproblem.py @@ -6,9 +6,9 @@ # import discretize # from pymatsolver import Pardiso # #import simpeg.PF as PF -# from SimPEG import maps, utils -# from SimPEG.potential_fields import magnetics as mag -# from SimPEG.utils.model_builder import getIndicesSphere +# from simpeg import maps, utils +# from simpeg.potential_fields import magnetics as mag +# from simpeg.utils.model_builder import get_indices_sphere # from scipy.constants import mu_0 # # @@ -30,7 +30,7 @@ # H0 = (Btot, Inc, Dec) # # b0 = mag.analytics.IDTtoxyz(-Inc, Dec, Btot) -# sph_ind = getIndicesSphere([0., 0., 0.], 100, M.gridCC) +# sph_ind = get_indices_sphere([0., 0., 0.], 100, M.gridCC) # chi[sph_ind] = chiblk # # xr = np.linspace(-300, 300, 41) @@ -41,7 +41,7 @@ # # components = ['bx', 'by', 'bz'] # receivers = mag.Point(rxLoc, components=components) -# srcField = mag.SourceField([receivers], parameters=H0) +# srcField = mag.UniformBackgroundField([receivers], parameters=H0) # # self.survey = mag.Survey(srcField) # diff --git a/tests/pf/test_survey_counting.py b/tests/pf/test_survey_counting.py index cea965eb0b..d0e0d71002 100644 --- a/tests/pf/test_survey_counting.py +++ b/tests/pf/test_survey_counting.py @@ -1,6 +1,6 @@ import numpy as np -from SimPEG.potential_fields import gravity as grav -from SimPEG.potential_fields import magnetics as mag +from simpeg.potential_fields import gravity as grav +from simpeg.potential_fields import magnetics as mag def test_gravity_survey(): @@ -27,7 +27,9 @@ def test_magnetics_survey(): rx1 = mag.Point(rx_locs, components=rx_components) rx2 = mag.Point(rx_locs, components="tmi") - src = mag.UniformBackgroundField([rx1, rx2]) + src = mag.UniformBackgroundField( + receiver_list=[rx1, rx2], amplitude=50_000, inclination=90, declination=0 + ) survey = mag.Survey(src) assert rx1.nD == 60 diff --git a/tests/seis/test_tomo.py b/tests/seis/test_tomo.py index 97eff392fb..e7125d70fa 100644 --- a/tests/seis/test_tomo.py +++ b/tests/seis/test_tomo.py @@ -2,8 +2,8 @@ import unittest import discretize -from SimPEG.seismic import straight_ray_tomography as tomo -from SimPEG import tests, maps, utils +from simpeg.seismic import straight_ray_tomography as tomo +from simpeg import tests, maps, utils TOL = 1e-5 FLR = 1e-14 @@ -30,7 +30,7 @@ def setUp(self): self.survey = survey def test_deriv(self): - s = utils.mkvc(utils.model_builder.randomModel(self.M.vnC)) + 1.0 + s = utils.mkvc(utils.model_builder.create_random_model(self.M.vnC)) + 1.0 def fun(x): return self.problem.dpred(x), lambda x: self.problem.Jvec(s, x) diff --git a/tests/utils/test_deprecate.py b/tests/utils/test_deprecate.py index cd41344e2e..924ee56e87 100644 --- a/tests/utils/test_deprecate.py +++ b/tests/utils/test_deprecate.py @@ -8,98 +8,98 @@ locs = np.array([[1.0, 2.0, 3.0]]) deprecated_modules = [ - # "SimPEG.utils.codeutils", - # "SimPEG.utils.coordutils", - # "SimPEG.utils.CounterUtils", - # "SimPEG.utils.curvutils", - # "SimPEG.utils.matutils", - # "SimPEG.utils.meshutils", - # "SimPEG.utils.ModelBuilder", - # "SimPEG.utils.PlotUtils", - # "SimPEG.utils.SolverUtils", - # "SimPEG.electromagnetics.utils.EMUtils", - # "SimPEG.electromagnetics.utils.AnalyticUtils", - # "SimPEG.electromagnetics.utils.CurrentUtils", - # "SimPEG.electromagnetics.utils.testingUtils", - # "SimPEG.electromagnetics.static.utils.StaticUtils", - # "SimPEG.electromagnetics.natural_source.utils.dataUtils", - # "SimPEG.electromagnetics.natural_source.utils.ediFilesUtils", - # "SimPEG.electromagnetics.natural_source.utils.MT1Danalytic", - # "SimPEG.electromagnetics.natural_source.utils.MT1Dsolutions", - # "SimPEG.electromagnetics.natural_source.utils.plotDataTypes", - # "SimPEG.electromagnetics.natural_source.utils.plotUtils", - # "SimPEG.electromagnetics.natural_source.utils.sourceUtils", - # "SimPEG.electromagnetics.natural_source.utils.testUtils", + # "simpeg.utils.codeutils", + # "simpeg.utils.coordutils", + # "simpeg.utils.CounterUtils", + # "simpeg.utils.curvutils", + # "simpeg.utils.matutils", + # "simpeg.utils.meshutils", + # "simpeg.utils.ModelBuilder", + # "simpeg.utils.PlotUtils", + # "simpeg.utils.SolverUtils", + # "simpeg.electromagnetics.utils.EMUtils", + # "simpeg.electromagnetics.utils.AnalyticUtils", + # "simpeg.electromagnetics.utils.CurrentUtils", + # "simpeg.electromagnetics.utils.testingUtils", + # "simpeg.electromagnetics.static.utils.StaticUtils", + # "simpeg.electromagnetics.natural_source.utils.dataUtils", + # "simpeg.electromagnetics.natural_source.utils.ediFilesUtils", + # "simpeg.electromagnetics.natural_source.utils.MT1Danalytic", + # "simpeg.electromagnetics.natural_source.utils.MT1Dsolutions", + # "simpeg.electromagnetics.natural_source.utils.plotDataTypes", + # "simpeg.electromagnetics.natural_source.utils.plotUtils", + # "simpeg.electromagnetics.natural_source.utils.sourceUtils", + # "simpeg.electromagnetics.natural_source.utils.testUtils", ] deprecated_problems = [ # [ - # "SimPEG.electromagnetics.frequency_domain", + # "simpeg.electromagnetics.frequency_domain", # ("Problem3D_e", "Problem3D_b", "Problem3D_h", "Problem3D_j"), # ], # [ - # "SimPEG.electromagnetics.time_domain", + # "simpeg.electromagnetics.time_domain", # ("Problem3D_e", "Problem3D_b", "Problem3D_h", "Problem3D_j"), # ], # [ - # "SimPEG.electromagnetics.natural_source", + # "simpeg.electromagnetics.natural_source", # ("Problem3D_ePrimSec", "Problem1D_ePrimSec"), # ], # [ - # "SimPEG.electromagnetics.static.induced_polarization", + # "simpeg.electromagnetics.static.induced_polarization", # ("Problem3D_CC", "Problem3D_N", "Problem2D_CC", "Problem2D_N"), # ], # [ - # "SimPEG.electromagnetics.static.resistivity", + # "simpeg.electromagnetics.static.resistivity", # ("Problem3D_CC", "Problem3D_N", "Problem2D_CC", "Problem2D_N"), # ], # [ - # "SimPEG.electromagnetics.static.spectral_induced_polarization", + # "simpeg.electromagnetics.static.spectral_induced_polarization", # ("Problem3D_CC", "Problem3D_N", "Problem2D_CC", "Problem2D_N"), # ], # [ - # "SimPEG.electromagnetics.viscous_remanent_magnetization", + # "simpeg.electromagnetics.viscous_remanent_magnetization", # ("Problem_Linear", "Problem_LogUnifrom"), # ], ] deprecated_fields = [ # [ - # "SimPEG.electromagnetics.frequency_domain", + # "simpeg.electromagnetics.frequency_domain", # ("Fields3D_e", "Fields3D_b", "Fields3D_h", "Fields3D_j"), # ], # [ - # "SimPEG.electromagnetics.time_domain", + # "simpeg.electromagnetics.time_domain", # ("Fields3D_e", "Fields3D_b", "Fields3D_h", "Fields3D_j"), # ], # [ - # "SimPEG.electromagnetics.natural_source", + # "simpeg.electromagnetics.natural_source", # ("Fields1D_ePrimSec", "Fields3D_ePrimSec"), # ], # [ - # "SimPEG.electromagnetics.static.resistivity", + # "simpeg.electromagnetics.static.resistivity", # ("Fields_CC", "Fields_N", "Fields_ky", "Fields_ky_CC", "Fields_ky_N"), # ], ] deprecated_receivers = [ # [ - # "SimPEG.electromagnetics.frequency_domain.receivers", + # "simpeg.electromagnetics.frequency_domain.receivers", # ("Point_e", "Point_b", "Point_bSecondary", "Point_h", "Point_j"), # ], # [ - # "SimPEG.electromagnetics.time_domain.receivers", + # "simpeg.electromagnetics.time_domain.receivers", # ("Point_e", "Point_b", "Point_h", "Point_j", "Point_dbdt", "Point_dhdt"), # ], # [ - # "SimPEG.electromagnetics.natural_source.receivers", + # "simpeg.electromagnetics.natural_source.receivers", # ("Point_impedance1D", "Point_impedance3D", "Point_tipper3D"), # ], - # ["SimPEG.electromagnetics.static.resistivity.receivers", ("Dipole_ky", "Pole_ky")], + # ["simpeg.electromagnetics.static.resistivity.receivers", ("Dipole_ky", "Pole_ky")], ] deprcated_surveys = [ - # "SimPEG.electromagnetics.static.resistivity", ("Survey") + # "simpeg.electromagnetics.static.resistivity", ("Survey") ] diff --git a/tests/utils/test_gmm_utils.py b/tests/utils/test_gmm_utils.py index a262d46a4e..f6e6c93d18 100644 --- a/tests/utils/test_gmm_utils.py +++ b/tests/utils/test_gmm_utils.py @@ -1,8 +1,9 @@ +import pytest import numpy as np import unittest import discretize -from SimPEG.maps import Wires -from SimPEG.utils import ( +from simpeg.maps import Wires +from simpeg.utils import ( mkvc, WeightedGaussianMixture, GaussianMixtureWithPrior, @@ -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 54e6282fe0..105ade7451 100644 --- a/tests/utils/test_io_utils.py +++ b/tests/utils/test_io_utils.py @@ -1,10 +1,10 @@ import unittest import numpy as np -from SimPEG.data import Data -from SimPEG.potential_fields import gravity, magnetics -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.utils.io_utils import ( +from simpeg.data import Data +from simpeg.potential_fields import gravity, magnetics +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.utils.io_utils import ( write_grav3d_ubc, read_grav3d_ubc, write_gg3d_ubc, @@ -242,9 +242,12 @@ def setUp(self): xyz = np.c_[x, y, z] rx = magnetics.receivers.Point(xyz, components="tmi") - inducing_field = (50000.0, 60.0, 15.0) - source_field = magnetics.sources.SourceField( - receiver_list=rx, parameters=inducing_field + h0_amplitude, h0_inclination, h0_declination = (50000.0, 60.0, 15.0) + source_field = magnetics.sources.UniformBackgroundField( + receiver_list=rx, + amplitude=h0_amplitude, + inclination=h0_inclination, + declination=h0_declination, ) survey = magnetics.survey.Survey(source_field) @@ -253,7 +256,9 @@ def setUp(self): self.std = std rx2 = magnetics.receivers.Point(xyz, components="tmi") - src_bad = magnetics.sources.SourceField([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 diff --git a/tests/utils/test_mat_utils.py b/tests/utils/test_mat_utils.py index 9fc7018435..5d85f51804 100644 --- a/tests/utils/test_mat_utils.py +++ b/tests/utils/test_mat_utils.py @@ -2,10 +2,10 @@ import numpy as np from scipy.sparse.linalg import eigsh from discretize import TensorMesh -from SimPEG import simulation, data_misfit -from SimPEG.maps import IdentityMap -from SimPEG.regularization import WeightedLeastSquares -from SimPEG.utils.mat_utils import eigenvalue_by_power_iteration +from simpeg import simulation, data_misfit +from simpeg.maps import IdentityMap +from simpeg.regularization import WeightedLeastSquares +from simpeg.utils.mat_utils import eigenvalue_by_power_iteration class TestEigenvalues(unittest.TestCase): @@ -39,11 +39,11 @@ def g(k): true_model[mesh.cell_centers_x > 0.6] = 0 self.true_model = true_model - # Create a SimPEG simulation + # Create a simpeg simulation model_map = IdentityMap(mesh) sim = simulation.LinearSimulation(mesh, G=G, model_map=model_map) - # Create a SimPEG data object + # Create a simpeg data object relative_error = 0.1 noise_floor = 1e-4 data_obj = sim.make_synthetic_data( @@ -75,11 +75,11 @@ def g(k): def test_dm_eigenvalue_by_power_iteration(self): # Test for a single data misfit - dmis_matrix = self.G.T.dot((self.dmis.W**2).dot(self.G)) + dmis_matrix = 2 * self.G.T.dot((self.dmis.W**2).dot(self.G)) 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, seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) @@ -89,10 +89,10 @@ def test_dm_eigenvalue_by_power_iteration(self): WtW = 0.0 for mult, dm in zip(self.dmiscombo.multipliers, self.dmiscombo.objfcts): WtW += mult * dm.W**2 - dmiscombo_matrix = self.G.T.dot(WtW.dot(self.G)) + 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, seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) @@ -102,7 +102,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, seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) @@ -110,11 +110,11 @@ def test_reg_eigenvalue_by_power_iteration(self): def test_combo_eigenvalue_by_power_iteration(self): reg_maxtrix = self.reg.deriv2(self.true_model) - dmis_matrix = self.G.T.dot((self.dmis.W**2).dot(self.G)) + dmis_matrix = 2 * self.G.T.dot((self.dmis.W**2).dot(self.G)) 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, seed=42 ) passed = np.isclose(max_eigenvalue_numpy, max_eigenvalue_directive, rtol=1e-2) self.assertTrue(passed, True) diff --git a/tests/utils/test_report.py b/tests/utils/test_report.py index e9ebe09bb3..828d06ec3f 100644 --- a/tests/utils/test_report.py +++ b/tests/utils/test_report.py @@ -1,6 +1,6 @@ import scooby import unittest -from SimPEG import Report +from simpeg import Report class TestReport(unittest.TestCase): @@ -10,7 +10,7 @@ def test_version_defaults(self): out1 = Report() out2 = scooby.Report( core=[ - "SimPEG", + "simpeg", "discretize", "pymatsolver", "numpy", @@ -34,6 +34,7 @@ def test_version_defaults(self): "vtk", "utm", "memory_profiler", + "choclo", ], ncol=3, text_width=80, diff --git a/tests/utils/test_solverwrap.py b/tests/utils/test_solverwrap.py index 9a56192a7b..126f4466ee 100644 --- a/tests/utils/test_solverwrap.py +++ b/tests/utils/test_solverwrap.py @@ -1,5 +1,5 @@ import unittest -from SimPEG.utils.solver_utils import Solver, SolverLU, SolverCG, SolverBiCG, SolverDiag +from simpeg.utils.solver_utils import Solver, SolverLU, SolverCG, SolverBiCG, SolverDiag import scipy.sparse as sp import numpy as np diff --git a/tutorials/01-models_mapping/plot_1_tensor_models.py b/tutorials/01-models_mapping/plot_1_tensor_models.py index 6728ba2a1d..ebcf496e95 100644 --- a/tutorials/01-models_mapping/plot_1_tensor_models.py +++ b/tutorials/01-models_mapping/plot_1_tensor_models.py @@ -20,8 +20,8 @@ from discretize import TensorMesh from discretize.utils import active_from_xyz -from SimPEG.utils import mkvc, model_builder -from SimPEG import maps +from simpeg.utils import mkvc, model_builder +from simpeg import maps import numpy as np import matplotlib.pyplot as plt @@ -210,7 +210,9 @@ def make_example_mesh(): model = background_value * np.ones(ind_active.sum()) # Add a sphere -ind_sphere = model_builder.getIndicesSphere(np.r_[-25.0, 0.0, -15.0], 20.0, mesh.gridCC) +ind_sphere = model_builder.get_indices_sphere( + np.r_[-25.0, 0.0, -15.0], 20.0, mesh.gridCC +) ind_sphere = ind_sphere[ind_active] # So it's same size and order as model model[ind_sphere] = sphere_value @@ -313,7 +315,9 @@ def make_example_mesh(): model = np.kron(np.ones((N, 1)), np.c_[background_sigma, background_myu]) # Add a conductive and permeable sphere -ind_sphere = model_builder.getIndicesSphere(np.r_[-25.0, 0.0, -15.0], 20.0, mesh.gridCC) +ind_sphere = model_builder.get_indices_sphere( + np.r_[-25.0, 0.0, -15.0], 20.0, mesh.gridCC +) ind_sphere = ind_sphere[ind_active] # So same size and order as model model[ind_sphere, :] = np.c_[sphere_sigma, sphere_mu] diff --git a/tutorials/01-models_mapping/plot_2_cyl_models.py b/tutorials/01-models_mapping/plot_2_cyl_models.py index 09b24b54ad..cb118b43f9 100644 --- a/tutorials/01-models_mapping/plot_2_cyl_models.py +++ b/tutorials/01-models_mapping/plot_2_cyl_models.py @@ -21,8 +21,8 @@ # from discretize import CylindricalMesh -from SimPEG.utils import mkvc -from SimPEG import maps +from simpeg.utils import mkvc +from simpeg import maps import numpy as np import matplotlib.pyplot as plt diff --git a/tutorials/01-models_mapping/plot_3_tree_models.py b/tutorials/01-models_mapping/plot_3_tree_models.py index 069e969bb8..2a8549aa6a 100644 --- a/tutorials/01-models_mapping/plot_3_tree_models.py +++ b/tutorials/01-models_mapping/plot_3_tree_models.py @@ -21,8 +21,8 @@ from discretize import TreeMesh from discretize.utils import refine_tree_xyz, active_from_xyz -from SimPEG.utils import mkvc, model_builder -from SimPEG import maps +from simpeg.utils import mkvc, model_builder +from simpeg import maps import numpy as np import matplotlib.pyplot as plt @@ -221,7 +221,9 @@ def refine_box(mesh): model = background_value * np.ones(ind_active.sum()) # Add a sphere -ind_sphere = model_builder.getIndicesSphere(np.r_[-25.0, 0.0, -15.0], 20.0, mesh.gridCC) +ind_sphere = model_builder.get_indices_sphere( + np.r_[-25.0, 0.0, -15.0], 20.0, mesh.gridCC +) ind_sphere = ind_sphere[ind_active] # So same size and order as model model[ind_sphere] = sphere_value @@ -334,7 +336,9 @@ def refine_box(mesh): model = np.kron(np.ones((N, 1)), np.c_[background_sigma_value, background_mu_value]) # Add a conductive and permeable sphere -ind_sphere = model_builder.getIndicesSphere(np.r_[-20.0, 0.0, -15.0], 20.0, mesh.gridCC) +ind_sphere = model_builder.get_indices_sphere( + np.r_[-20.0, 0.0, -15.0], 20.0, mesh.gridCC +) ind_sphere = ind_sphere[ind_active] # So same size and order as model model[ind_sphere, :] = np.c_[sphere_sigma_value, sphere_mu_value] diff --git a/tutorials/02-linear_inversion/plot_inv_1_inversion_lsq.py b/tutorials/02-linear_inversion/plot_inv_1_inversion_lsq.py index 6173c95c4c..2d284b4264 100644 --- a/tutorials/02-linear_inversion/plot_inv_1_inversion_lsq.py +++ b/tutorials/02-linear_inversion/plot_inv_1_inversion_lsq.py @@ -25,7 +25,7 @@ from discretize import TensorMesh -from SimPEG import ( +from simpeg import ( simulation, maps, data_misfit, 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 19ae156e89..4715d3937b 100644 --- a/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py +++ b/tutorials/02-linear_inversion/plot_inv_2_inversion_irls.py @@ -16,13 +16,12 @@ """ - import numpy as np import matplotlib.pyplot as plt from discretize import TensorMesh -from SimPEG import ( +from simpeg import ( simulation, maps, data_misfit, @@ -172,7 +171,7 @@ def g(k): # # Add sensitivity weights but don't update at each beta -sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) +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) diff --git a/tutorials/03-gravity/plot_1a_gravity_anomaly.py b/tutorials/03-gravity/plot_1a_gravity_anomaly.py index a017eed964..d2d8aacfd6 100644 --- a/tutorials/03-gravity/plot_1a_gravity_anomaly.py +++ b/tutorials/03-gravity/plot_1a_gravity_anomaly.py @@ -2,7 +2,7 @@ Forward Simulation of Gravity Anomaly Data on a Tensor Mesh =========================================================== -Here we use the module *SimPEG.potential_fields.gravity* to predict gravity +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: @@ -28,9 +28,9 @@ 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 +from simpeg.utils import plot2Ddata, model_builder +from simpeg import maps +from simpeg.potential_fields import gravity save_output = False @@ -138,7 +138,9 @@ model[ind_block] = block_density # You can also use SimPEG utilities to add structures to the model more concisely -ind_sphere = model_builder.getIndicesSphere(np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC) +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 @@ -178,19 +180,35 @@ # formulation. # -# Define the forward simulation. By setting the 'store_sensitivities' keyword -# argument to "forward_only", we simulate the data without storing the sensitivities +############################################################################### +# 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 @@ -232,6 +250,6 @@ np.random.seed(737) maximum_anomaly = np.max(np.abs(dpred)) - noise = 0.01 * maximum_anomaly * np.random.rand(len(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 8c4dd756c8..84e4227cf3 100644 --- a/tutorials/03-gravity/plot_1b_gravity_gradiometry.py +++ b/tutorials/03-gravity/plot_1b_gravity_gradiometry.py @@ -2,7 +2,7 @@ Forward Simulation of Gradiometry Data on a Tree Mesh ===================================================== -Here we use the module *SimPEG.potential_fields.gravity* to predict gravity +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: @@ -26,9 +26,9 @@ 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 +from simpeg.utils import plot2Ddata, model_builder +from simpeg import maps +from simpeg.potential_fields import gravity # sphinx_gallery_thumbnail_number = 2 @@ -160,7 +160,9 @@ model[ind_block] = block_density # You can also use SimPEG utilities to add structures to the model more concisely -ind_sphere = model_builder.getIndicesSphere(np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC) +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 @@ -199,17 +201,33 @@ # formulation. # -# Define the forward simulation. By setting the 'store_sensitivities' keyword -# argument to "forward_only", we simulate the data without storing the sensitivities +############################################################################### +# 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) @@ -265,6 +283,6 @@ cbar = mpl.colorbar.ColorbarBase( ax4, norm=norm, orientation="vertical", cmap=mpl.cm.bwr ) -cbar.set_label("$mgal/m$", rotation=270, labelpad=15, size=12) +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 2b3d2fc55d..89d043d65d 100644 --- a/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py +++ b/tutorials/03-gravity/plot_inv_1a_gravity_anomaly.py @@ -32,9 +32,9 @@ 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 ( +from simpeg.utils import plot2Ddata, model_builder +from simpeg.potential_fields import gravity +from simpeg import ( maps, data, data_misfit, @@ -206,9 +206,20 @@ # 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 + survey=survey, + mesh=mesh, + rhoMap=model_map, + ind_active=ind_active, + engine="choclo", ) @@ -230,7 +241,9 @@ dmis = data_misfit.L2DataMisfit(data=data_object, simulation=simulation) # Define the regularization (model objective function). -reg = regularization.WeightedLeastSquares(mesh, indActive=ind_active, mapping=model_map) +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. @@ -268,7 +281,7 @@ target_misfit = directives.TargetMisfit(chifact=1) # Add sensitivity weights -sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) +sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) # The directives are defined as a list. directives_list = [ @@ -321,7 +334,9 @@ true_model[ind_block] = block_density # You can also use SimPEG utilities to add structures to the model more concisely -ind_sphere = model_builder.getIndicesSphere(np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC) +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 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 1d79479e47..7bfd6da31f 100644 --- a/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py +++ b/tutorials/03-gravity/plot_inv_1b_gravity_anomaly_irls.py @@ -33,9 +33,9 @@ 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 ( +from simpeg.utils import plot2Ddata, model_builder +from simpeg.potential_fields import gravity +from simpeg import ( maps, data, data_misfit, @@ -208,9 +208,20 @@ # 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 + survey=survey, + mesh=mesh, + rhoMap=model_map, + ind_active=ind_active, + engine="choclo", ) @@ -267,10 +278,6 @@ beta_tol=1e-2, ) -# 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) @@ -278,14 +285,13 @@ update_jacobi = directives.UpdatePreconditioner() # Add sensitivity weights -sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) +sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) # The directives are defined as a list. directives_list = [ update_IRLS, sensitivity_weights, starting_beta, - beta_schedule, save_iteration, update_jacobi, ] @@ -331,7 +337,9 @@ true_model[ind_block] = block_density # You can also use SimPEG utilities to add structures to the model more concisely -ind_sphere = model_builder.getIndicesSphere(np.r_[35.0, 0.0, -40.0], 15.0, mesh.gridCC) +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 diff --git a/tutorials/04-magnetics/plot_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_2a_magnetics_induced.py index eaab3b8a9b..67889ba62a 100644 --- a/tutorials/04-magnetics/plot_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_2a_magnetics_induced.py @@ -2,7 +2,7 @@ Forward Simulation of Total Magnetic Intensity Data =================================================== -Here we use the module *SimPEG.potential_fields.magnetics* to predict magnetic +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: @@ -27,9 +27,9 @@ 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 +from simpeg.utils import plot2Ddata, model_builder +from simpeg import maps +from simpeg.potential_fields import magnetics write_output = False @@ -82,10 +82,12 @@ inclination = 90 declination = 0 strength = 50000 -inducing_field = (strength, inclination, declination) -source_field = magnetics.sources.SourceField( - receiver_list=receiver_list, parameters=inducing_field +source_field = magnetics.sources.UniformBackgroundField( + receiver_list=receiver_list, + amplitude=strength, + inclination=inclination, + declination=declination, ) # Define the survey @@ -128,7 +130,7 @@ # Define model. Models in SimPEG are vector arrays model = background_susceptibility * np.ones(ind_active.sum()) -ind_sphere = model_builder.getIndicesSphere( +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] @@ -167,8 +169,10 @@ # 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, @@ -176,9 +180,21 @@ chiMap=model_map, ind_active=ind_active, store_sensitivities="forward_only", + engine="choclo", ) +############################################################################### +# .. tip:: +# +# Since SimPEG v0.22.0 we can use `Choclo +# `_ as the engine for running the magnetic +# simulations, which results in faster and more memory efficient runs. Just +# pass ``engine="choclo"`` when constructing the simulation. +# + +############################################################################### # Compute predicted data for a susceptibility model + dpred = simulation.dpred(model) # Plot @@ -228,6 +244,6 @@ np.random.seed(211) maximum_anomaly = np.max(np.abs(dpred)) - noise = 0.02 * maximum_anomaly * np.random.rand(len(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 a97cbc9462..70c9621221 100644 --- a/tutorials/04-magnetics/plot_2b_magnetics_mvi.py +++ b/tutorials/04-magnetics/plot_2b_magnetics_mvi.py @@ -2,7 +2,7 @@ Forward Simulation of Gradiometry Data for Magnetic Vector Models ================================================================= -Here we use the module *SimPEG.potential_fields.magnetics* to predict magnetic +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: @@ -27,9 +27,9 @@ 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 +from simpeg.utils import plot2Ddata, model_builder, mat_utils +from simpeg import maps +from simpeg.potential_fields import magnetics # sphinx_gallery_thumbnail_number = 2 @@ -81,10 +81,12 @@ field_inclination = 60 field_declination = 30 field_strength = 50000 -inducing_field = (field_strength, field_inclination, field_declination) -source_field = magnetics.sources.SourceField( - receiver_list=receiver_list, parameters=inducing_field +source_field = magnetics.sources.UniformBackgroundField( + receiver_list=receiver_list, + amplitude=field_strength, + inclination=field_inclination, + declination=field_declination, ) # Define the survey @@ -161,7 +163,7 @@ # Define susceptibility for each cell susceptibility_model = background_susceptibility * np.ones(ind_active.sum()) -ind_sphere = model_builder.getIndicesSphere(np.r_[0.0, 0.0, -45.0], 15.0, mesh.gridCC) +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 @@ -223,8 +225,10 @@ # 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, @@ -234,7 +238,10 @@ store_sensitivities="forward_only", ) + +############################################################################### # Compute predicted data for some model + dpred = simulation.dpred(model) n_data = len(dpred) diff --git a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py index 69fc1932f0..92ab0691e0 100644 --- a/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py +++ b/tutorials/04-magnetics/plot_inv_2a_magnetics_induced.py @@ -34,9 +34,9 @@ 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 ( +from simpeg.potential_fields import magnetics +from simpeg.utils import plot2Ddata, model_builder +from simpeg import ( maps, data, inverse_problem, @@ -160,10 +160,12 @@ inclination = 90 declination = 0 strength = 50000 -inducing_field = (strength, inclination, declination) -source_field = magnetics.sources.SourceField( - receiver_list=receiver_list, parameters=inducing_field +source_field = magnetics.sources.UniformBackgroundField( + receiver_list=receiver_list, + amplitude=strength, + inclination=inclination, + declination=declination, ) # Define the survey @@ -227,15 +229,27 @@ # 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, + engine="choclo", ) +############################################################################### +# .. tip:: +# +# Since SimPEG v0.22.0 we can use `Choclo +# `_ as the engine for running the magnetic +# simulations, which results in faster and more memory efficient runs. Just +# pass ``engine="choclo"`` when constructing the simulation. +# + ####################################################################### # Define Inverse Problem @@ -307,7 +321,7 @@ target_misfit = directives.TargetMisfit(chifact=1) # Add sensitivity weights -sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) +sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) # The directives are defined as a list. directives_list = [ @@ -345,7 +359,7 @@ sphere_susceptibility = 0.01 true_model = background_susceptibility * np.ones(nC) -ind_sphere = model_builder.getIndicesSphere( +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] diff --git a/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py b/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py index 62590a5fa5..7ea20813c7 100644 --- a/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py +++ b/tutorials/05-dcr/plot_fwd_1_dcr_sounding.py @@ -3,7 +3,7 @@ Simulate a 1D Sounding over a Layered Earth =========================================== -Here we use the module *SimPEG.electromangetics.static.resistivity* to predict +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: - General definition of sources and receivers @@ -28,9 +28,9 @@ 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 +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}) @@ -157,7 +157,7 @@ os.mkdir(dir_path) np.random.seed(145) - noise = 0.025 * dpred * np.random.rand(len(dpred)) + noise = 0.025 * dpred * np.random.randn(len(dpred)) data_array = np.c_[ survey.locations_a, diff --git a/tutorials/05-dcr/plot_fwd_2_dcr2d.py b/tutorials/05-dcr/plot_fwd_2_dcr2d.py index 3d58f7872a..8095504f90 100644 --- a/tutorials/05-dcr/plot_fwd_2_dcr2d.py +++ b/tutorials/05-dcr/plot_fwd_2_dcr2d.py @@ -3,7 +3,7 @@ DC Resistivity Forward Simulation in 2.5D ========================================= -Here we use the module *SimPEG.electromagnetics.static.resistivity* to predict +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: @@ -22,13 +22,13 @@ # from discretize import TreeMesh -from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz +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 ( +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, @@ -43,7 +43,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver write_output = False mpl.rcParams.update({"font.size": 16}) @@ -123,11 +123,9 @@ mesh = TreeMesh([hx, hz], x0="CN") # Mesh refinement based on topography -mesh = refine_tree_xyz( - mesh, +mesh.refine_surface( topo_xyz[:, [0, 2]], - octree_levels=[0, 0, 4, 4], - method="surface", + padding_cells_by_level=[0, 0, 4, 4], finalize=False, ) @@ -144,16 +142,12 @@ np.reshape(electrode_locations, (4 * survey.nD, 2)), axis=0 ) -mesh = refine_tree_xyz( - mesh, unique_locations, octree_levels=[4, 4], method="radial", finalize=False -) +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_tree_xyz( - mesh, xyz, octree_levels=[0, 0, 2, 8], method="box", finalize=False -) +mesh.refine_bounding_box(xyz, padding_cells_by_level=[0, 0, 2, 8], finalize=False) mesh.finalize() @@ -183,11 +177,13 @@ # Define model conductivity_model = background_conductivity * np.ones(nC) -ind_conductor = model_builder.getIndicesSphere(np.r_[-120.0, -160.0], 60.0, mesh.gridCC) +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.getIndicesSphere(np.r_[120.0, -100.0], 60.0, mesh.gridCC) +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 @@ -307,7 +303,7 @@ # Add 10% Gaussian noise to each datum np.random.seed(225) std = 0.05 * np.abs(dpred) - dc_noise = std * np.random.rand(len(dpred)) + dc_noise = std * np.random.randn(len(dpred)) dobs = dpred + dc_noise # Create a survey with the original electrode locations diff --git a/tutorials/05-dcr/plot_fwd_3_dcr3d.py b/tutorials/05-dcr/plot_fwd_3_dcr3d.py index f02757a50f..ea7293d267 100644 --- a/tutorials/05-dcr/plot_fwd_3_dcr3d.py +++ b/tutorials/05-dcr/plot_fwd_3_dcr3d.py @@ -3,7 +3,7 @@ DC Resistivity Forward Simulation in 3D ======================================= -Here we use the module *SimPEG.electromagnetics.static.resistivity* to predict +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: - How to define the survey @@ -35,11 +35,11 @@ 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 ( +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, ) @@ -47,7 +47,7 @@ # 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 + from simpeg.electromagnetics.static.utils.static_utils import plot_3d_pseudosection has_plotly = True except ImportError: @@ -57,7 +57,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) write_output = False @@ -190,12 +190,12 @@ # Define model conductivity_model = background_value * np.ones(nC) -ind_conductor = model_builder.getIndicesSphere( +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.getIndicesSphere( +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 @@ -347,7 +347,7 @@ # Add 5% Gaussian noise to each datum np.random.seed(433) std = 0.1 * np.abs(dpred) - noise = std * np.random.rand(len(dpred)) + noise = std * np.random.randn(len(dpred)) dobs = dpred + noise # Create dictionary that stores line IDs diff --git a/tutorials/05-dcr/plot_gen_3_3d_to_2d.py b/tutorials/05-dcr/plot_gen_3_3d_to_2d.py index 6d7a87fbc9..8200698207 100644 --- a/tutorials/05-dcr/plot_gen_3_3d_to_2d.py +++ b/tutorials/05-dcr/plot_gen_3_3d_to_2d.py @@ -24,9 +24,9 @@ # -from SimPEG import utils -from SimPEG.utils.io_utils.io_utils_electromagnetics import read_dcip_xyz -from SimPEG.electromagnetics.static.utils.static_utils import ( +from simpeg import utils +from simpeg.utils.io_utils.io_utils_electromagnetics import read_dcip_xyz +from simpeg.electromagnetics.static.utils.static_utils import ( apparent_resistivity_from_voltage, convert_survey_3d_to_2d_lines, plot_pseudosection, @@ -42,7 +42,7 @@ try: import plotly - from SimPEG.electromagnetics.static.utils.static_utils import plot_3d_pseudosection + from simpeg.electromagnetics.static.utils.static_utils import plot_3d_pseudosection has_plotly = True except ImportError: diff --git a/tutorials/05-dcr/plot_inv_1_dcr_sounding.py b/tutorials/05-dcr/plot_inv_1_dcr_sounding.py index 03f950cee8..75deb01d18 100644 --- a/tutorials/05-dcr/plot_inv_1_dcr_sounding.py +++ b/tutorials/05-dcr/plot_inv_1_dcr_sounding.py @@ -3,7 +3,7 @@ Least-Squares 1D Inversion of Sounding Data =========================================== -Here we use the module *SimPEG.electromangetics.static.resistivity* to invert +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: @@ -30,7 +30,7 @@ from discretize import TensorMesh -from SimPEG import ( +from simpeg import ( maps, data, data_misfit, @@ -41,8 +41,8 @@ directives, utils, ) -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.utils import plot_1d_layer_model +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.utils import plot_1d_layer_model mpl.rcParams.update({"font.size": 16}) 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 34e4989641..f461b716dd 100644 --- a/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py +++ b/tutorials/05-dcr/plot_inv_1_dcr_sounding_irls.py @@ -3,7 +3,7 @@ Sparse 1D Inversion of Sounding Data ==================================== -Here we use the module *SimPEG.electromangetics.static.resistivity* to invert +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: @@ -30,7 +30,7 @@ from discretize import TensorMesh -from SimPEG import ( +from simpeg import ( maps, data, data_misfit, @@ -41,8 +41,8 @@ directives, utils, ) -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.utils import plot_1d_layer_model +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.utils import plot_1d_layer_model mpl.rcParams.update({"font.size": 16}) 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 ff8618dbc9..63936d4e9d 100644 --- a/tutorials/05-dcr/plot_inv_1_dcr_sounding_parametric.py +++ b/tutorials/05-dcr/plot_inv_1_dcr_sounding_parametric.py @@ -3,7 +3,7 @@ Parametric 1D Inversion of Sounding Data ======================================== -Here we use the module *SimPEG.electromangetics.static.resistivity* to invert +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: @@ -31,7 +31,7 @@ from discretize import TensorMesh -from SimPEG import ( +from simpeg import ( maps, data, data_misfit, @@ -42,8 +42,8 @@ directives, utils, ) -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.utils import plot_1d_layer_model +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.utils import plot_1d_layer_model mpl.rcParams.update({"font.size": 16}) diff --git a/tutorials/05-dcr/plot_inv_2_dcr2d.py b/tutorials/05-dcr/plot_inv_2_dcr2d.py index 1ec103157f..f28bb8d26d 100644 --- a/tutorials/05-dcr/plot_inv_2_dcr2d.py +++ b/tutorials/05-dcr/plot_inv_2_dcr2d.py @@ -29,10 +29,10 @@ import tarfile from discretize import TreeMesh -from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz +from discretize.utils import mkvc, active_from_xyz -from SimPEG.utils import model_builder -from SimPEG import ( +from simpeg.utils import model_builder +from simpeg import ( maps, data_misfit, regularization, @@ -42,16 +42,16 @@ directives, utils, ) -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.electromagnetics.static.utils.static_utils import ( +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 +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 + from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) # sphinx_gallery_thumbnail_number = 4 @@ -173,11 +173,9 @@ mesh = TreeMesh([hx, hz], x0="CN") # Mesh refinement based on topography -mesh = refine_tree_xyz( - mesh, +mesh.refine_surface( topo_xyz[:, [0, 2]], - octree_levels=[0, 0, 4, 4], - method="surface", + padding_cells_by_level=[0, 0, 4, 4], finalize=False, ) @@ -194,16 +192,12 @@ np.reshape(electrode_locations, (4 * dc_data.survey.nD, 2)), axis=0 ) -mesh = refine_tree_xyz( - mesh, unique_locations, octree_levels=[4, 4], method="radial", finalize=False -) +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_tree_xyz( - mesh, xyz, octree_levels=[0, 0, 2, 8], method="box", finalize=False -) +mesh.refine_bounding_box(xyz, padding_cells_by_level=[0, 0, 2, 8], finalize=False) mesh.finalize() @@ -369,10 +363,12 @@ true_conductivity_model = true_background_conductivity * np.ones(len(mesh)) -ind_conductor = model_builder.getIndicesSphere(np.r_[-120.0, -180.0], 60.0, mesh.gridCC) +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.getIndicesSphere(np.r_[120.0, -180.0], 60.0, mesh.gridCC) +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 diff --git a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py index e30b7e0e77..f84891fb15 100644 --- a/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py +++ b/tutorials/05-dcr/plot_inv_2_dcr2d_irls.py @@ -29,10 +29,10 @@ import tarfile from discretize import TreeMesh -from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz +from discretize.utils import mkvc, active_from_xyz -from SimPEG.utils import model_builder -from SimPEG import ( +from simpeg.utils import model_builder +from simpeg import ( maps, data_misfit, regularization, @@ -42,17 +42,17 @@ directives, utils, ) -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.electromagnetics.static.utils.static_utils import ( +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 +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 + from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) # sphinx_gallery_thumbnail_number = 3 @@ -179,11 +179,9 @@ mesh = TreeMesh([hx, hz], x0="CN") # Mesh refinement based on topography -mesh = refine_tree_xyz( - mesh, +mesh.refine_surface( topo_xyz[:, [0, 2]], - octree_levels=[0, 0, 4, 4], - method="surface", + padding_cells_by_level=[0, 0, 4, 4], finalize=False, ) @@ -200,16 +198,12 @@ np.reshape(electrode_locations, (4 * dc_data.survey.nD, 2)), axis=0 ) -mesh = refine_tree_xyz( - mesh, unique_locations, octree_levels=[4, 4], method="radial", finalize=False -) +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_tree_xyz( - mesh, xyz, octree_levels=[0, 0, 2, 8], method="box", finalize=False -) +mesh.refine_bounding_box(xyz, padding_cells_by_level=[0, 0, 2, 8], finalize=False) mesh.finalize() @@ -302,10 +296,10 @@ reg = regularization.Sparse( mesh, - indActive=ind_active, + active_cells=ind_active, reference_model=starting_conductivity_model, mapping=regmap, - gradientType="total", + gradient_type="total", alpha_s=0.01, alpha_x=1, alpha_y=1, @@ -385,10 +379,12 @@ true_conductivity_model = true_background_conductivity * np.ones(len(mesh)) -ind_conductor = model_builder.getIndicesSphere(np.r_[-120.0, -180.0], 60.0, mesh.gridCC) +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.getIndicesSphere(np.r_[120.0, -180.0], 60.0, mesh.gridCC) +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 diff --git a/tutorials/05-dcr/plot_inv_3_dcr3d.py b/tutorials/05-dcr/plot_inv_3_dcr3d.py index 3ff0ad4676..4ca93a6106 100644 --- a/tutorials/05-dcr/plot_inv_3_dcr3d.py +++ b/tutorials/05-dcr/plot_inv_3_dcr3d.py @@ -34,9 +34,9 @@ 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 ( +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, @@ -46,15 +46,15 @@ directives, utils, ) -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.electromagnetics.static.utils.static_utils import ( +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 + from simpeg.electromagnetics.static.utils.static_utils import plot_3d_pseudosection has_plotly = True except ImportError: @@ -64,7 +64,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) @@ -300,7 +300,7 @@ # Define the regularization (model objective function) dc_regularization = regularization.WeightedLeastSquares( mesh, - indActive=ind_active, + active_cells=ind_active, reference_model=starting_conductivity_model, ) @@ -388,12 +388,12 @@ # Define model true_conductivity_model = background_value * np.ones(nC) -ind_conductor = model_builder.getIndicesSphere( +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.getIndicesSphere( +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 diff --git a/tutorials/06-ip/plot_fwd_2_dcip2d.py b/tutorials/06-ip/plot_fwd_2_dcip2d.py index ad19acb46b..4e1add360e 100644 --- a/tutorials/06-ip/plot_fwd_2_dcip2d.py +++ b/tutorials/06-ip/plot_fwd_2_dcip2d.py @@ -3,8 +3,8 @@ 2.5D Forward Simulation of a DCIP Line ====================================== -Here we use the module *SimPEG.electromagnetics.static.resistivity* to predict -DC resistivity data and the module *SimPEG.electromagnetics.static.induced_polarization* +Here we use the module *simpeg.electromagnetics.static.resistivity* to predict +DC resistivity data and the module *simpeg.electromagnetics.static.induced_polarization* to predict IP data for a dipole-dipole survey. In this tutorial, we focus on the following: @@ -30,14 +30,14 @@ # from discretize import TreeMesh -from discretize.utils import mkvc, refine_tree_xyz, 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 import induced_polarization as ip -from SimPEG.electromagnetics.static.utils.static_utils import ( +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 import induced_polarization as ip +from simpeg.electromagnetics.static.utils.static_utils import ( generate_dcip_sources_line, plot_pseudosection, apparent_resistivity_from_voltage, @@ -52,7 +52,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) write_output = False @@ -135,11 +135,9 @@ mesh = TreeMesh([hx, hz], x0="CN") # Mesh refinement based on topography -mesh = refine_tree_xyz( - mesh, +mesh.refine_surface( topo_xyz[:, [0, 2]], - octree_levels=[0, 0, 4, 4], - method="surface", + padding_cells_by_level=[0, 0, 4, 4], finalize=False, ) @@ -156,16 +154,12 @@ np.reshape(electrode_locations, (4 * dc_survey.nD, 2)), axis=0 ) -mesh = refine_tree_xyz( - mesh, unique_locations, octree_levels=[4, 4], method="radial", finalize=False -) +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_tree_xyz( - mesh, xyz, octree_levels=[0, 0, 2, 8], method="box", finalize=False -) +mesh.refine_bounding_box(xyz, padding_cells_by_level=[0, 0, 2, 8], finalize=False) mesh.finalize() @@ -195,11 +189,13 @@ # Define model conductivity_model = background_conductivity * np.ones(nC) -ind_conductor = model_builder.getIndicesSphere(np.r_[-120.0, -160.0], 60.0, mesh.gridCC) +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.getIndicesSphere(np.r_[120.0, -100.0], 60.0, mesh.gridCC) +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 @@ -353,7 +349,7 @@ # Define chargeability model chargeability_model = background_chargeability * np.ones(nC) -ind_chargeable = model_builder.getIndicesSphere( +ind_chargeable = model_builder.get_indices_sphere( np.r_[-120.0, -160.0], 60.0, mesh.gridCC ) ind_chargeable = ind_chargeable[ind_active] @@ -473,7 +469,7 @@ # Add 5% Gaussian noise to each DC datum np.random.seed(225) std = 0.05 * np.abs(dpred_dc) - dc_noise = std * np.random.rand(len(dpred_dc)) + dc_noise = std * np.random.randn(len(dpred_dc)) dobs = dpred_dc + dc_noise # Create a survey with the original electrode locations @@ -497,7 +493,7 @@ # Add Gaussian noise equal to 5e-3 V/V std = 5e-3 * np.ones_like(dpred_ip) - ip_noise = std * np.random.rand(len(dpred_ip)) + ip_noise = std * np.random.randn(len(dpred_ip)) dobs = dpred_ip + ip_noise # Create a survey with the original electrode locations diff --git a/tutorials/06-ip/plot_fwd_3_dcip3d.py b/tutorials/06-ip/plot_fwd_3_dcip3d.py index f147ff64ed..dc5a07db96 100644 --- a/tutorials/06-ip/plot_fwd_3_dcip3d.py +++ b/tutorials/06-ip/plot_fwd_3_dcip3d.py @@ -3,9 +3,9 @@ DC/IP Forward Simulation in 3D ============================== -Here we use the module *SimPEG.electromagnetics.static.resistivity* to predict +Here we use the module *simpeg.electromagnetics.static.resistivity* to predict DC resistivity data on an OcTree mesh. Then we use the module -*SimPEG.electromagnetics.static.induced_polarization* to predict IP data. +*simpeg.electromagnetics.static.induced_polarization* to predict IP data. In this tutorial, we focus on the following: - How to define the survey @@ -37,12 +37,12 @@ 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 import induced_polarization as ip -from SimPEG.electromagnetics.static.utils.static_utils import ( +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 import induced_polarization as ip +from simpeg.electromagnetics.static.utils.static_utils import ( generate_dcip_sources_line, apparent_resistivity_from_voltage, ) @@ -50,7 +50,7 @@ # 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 + from simpeg.electromagnetics.static.utils.static_utils import plot_3d_pseudosection has_plotly = True except ImportError: @@ -60,7 +60,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) write_output = False @@ -196,12 +196,12 @@ # Define model conductivity_model = background_value * np.ones(nC) -ind_conductor = model_builder.getIndicesSphere( +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.getIndicesSphere( +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 @@ -388,7 +388,7 @@ # Define model chargeability_model = background_value * np.ones(nC) -ind_chargeable = model_builder.getIndicesSphere( +ind_chargeable = model_builder.get_indices_sphere( np.r_[-350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] ) @@ -505,7 +505,7 @@ # Add 10% Gaussian noise to each datum np.random.seed(433) std = 0.1 * np.abs(dpred_dc) - noise = std * np.random.rand(len(dpred_dc)) + noise = std * np.random.randn(len(dpred_dc)) dobs = dpred_dc + noise # Create dictionary that stores line IDs @@ -543,7 +543,7 @@ # Add Gaussian noise with a standard deviation of 5e-3 V/V np.random.seed(444) std = 5e-3 * np.ones_like(dpred_ip) - noise = std * np.random.rand(len(dpred_ip)) + noise = std * np.random.randn(len(dpred_ip)) dobs = dpred_ip + noise # Create a survey with the original electrode locations diff --git a/tutorials/06-ip/plot_inv_2_dcip2d.py b/tutorials/06-ip/plot_inv_2_dcip2d.py index f34d6996b2..72cf5c2639 100644 --- a/tutorials/06-ip/plot_inv_2_dcip2d.py +++ b/tutorials/06-ip/plot_inv_2_dcip2d.py @@ -33,10 +33,10 @@ import tarfile from discretize import TreeMesh -from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz +from discretize.utils import mkvc, active_from_xyz -from SimPEG.utils import model_builder -from SimPEG import ( +from simpeg.utils import model_builder +from simpeg import ( maps, data_misfit, regularization, @@ -46,18 +46,18 @@ directives, utils, ) -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.electromagnetics.static import induced_polarization as ip -from SimPEG.electromagnetics.static.utils.static_utils import ( +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.electromagnetics.static import induced_polarization as ip +from simpeg.electromagnetics.static.utils.static_utils import ( apparent_resistivity_from_voltage, plot_pseudosection, ) -from SimPEG.utils.io_utils.io_utils_electromagnetics import read_dcip2d_ubc +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 + from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) # sphinx_gallery_thumbnail_number = 7 @@ -188,11 +188,9 @@ mesh = TreeMesh([hx, hz], x0="CN") # Mesh refinement based on topography -mesh = refine_tree_xyz( - mesh, +mesh.refine_surface( topo_xyz[:, [0, 2]], - octree_levels=[0, 0, 4, 4], - method="surface", + padding_cells_by_level=[0, 0, 4, 4], finalize=False, ) @@ -209,16 +207,12 @@ np.reshape(electrode_locations, (4 * dc_data.survey.nD, 2)), axis=0 ) -mesh = refine_tree_xyz( - mesh, unique_locations, octree_levels=[4, 4], method="radial", finalize=False -) +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_tree_xyz( - mesh, xyz, octree_levels=[0, 0, 2, 8], method="box", finalize=False -) +mesh.refine_bounding_box(xyz, padding_cells_by_level=[0, 0, 2, 8], finalize=False) mesh.finalize() @@ -310,7 +304,7 @@ # Define the regularization (model objective function) dc_regularization = regularization.WeightedLeastSquares( mesh, - indActive=ind_active, + active_cells=ind_active, reference_model=starting_conductivity_model, alpha_s=0.01, alpha_x=1, @@ -393,10 +387,12 @@ true_conductivity_model = true_background_conductivity * np.ones(len(mesh)) -ind_conductor = model_builder.getIndicesSphere(np.r_[-120.0, -180.0], 60.0, mesh.gridCC) +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.getIndicesSphere(np.r_[120.0, -180.0], 60.0, mesh.gridCC) +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 @@ -540,7 +536,7 @@ # Define the regularization (model objective function) ip_regularization = regularization.WeightedLeastSquares( mesh, - indActive=ind_active, + active_cells=ind_active, mapping=maps.IdentityMap(nP=nC), alpha_s=0.01, alpha_x=1, @@ -565,7 +561,7 @@ # Here we define the directives in the same manner as the DC inverse problem. # -update_sensitivity_weighting = directives.UpdateSensitivityWeights(threshold=1e-3) +update_sensitivity_weighting = directives.UpdateSensitivityWeights(threshold_value=1e-3) starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e1) beta_schedule = directives.BetaSchedule(coolingFactor=2, coolingRate=1) save_iteration = directives.SaveOutputEveryIteration(save_txt=False) diff --git a/tutorials/06-ip/plot_inv_3_dcip3d.py b/tutorials/06-ip/plot_inv_3_dcip3d.py index fa4fb33440..94b4cd9407 100644 --- a/tutorials/06-ip/plot_inv_3_dcip3d.py +++ b/tutorials/06-ip/plot_inv_3_dcip3d.py @@ -35,9 +35,9 @@ 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 ( +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, @@ -47,16 +47,16 @@ directives, utils, ) -from SimPEG.electromagnetics.static import resistivity as dc -from SimPEG.electromagnetics.static import induced_polarization as ip -from SimPEG.electromagnetics.static.utils.static_utils import ( +from simpeg.electromagnetics.static import resistivity as dc +from simpeg.electromagnetics.static import induced_polarization as ip +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 + from simpeg.electromagnetics.static.utils.static_utils import plot_3d_pseudosection has_plotly = True except ImportError: @@ -66,7 +66,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver mpl.rcParams.update({"font.size": 16}) @@ -348,7 +348,7 @@ # Define the regularization (model objective function) dc_regularization = regularization.WeightedLeastSquares( mesh, - indActive=ind_active, + active_cells=ind_active, reference_model=starting_conductivity_model, ) @@ -430,12 +430,12 @@ resistor_value = 1e-3 true_conductivity_model = background_value * np.ones(nC) -ind_conductor = model_builder.getIndicesSphere( +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.getIndicesSphere( +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 @@ -608,7 +608,7 @@ # Define the regularization (model objective function) ip_regularization = regularization.WeightedLeastSquares( mesh, - indActive=ind_active, + active_cells=ind_active, mapping=maps.IdentityMap(nP=nC), alpha_s=0.01, alpha_x=1, @@ -633,7 +633,7 @@ # Here we define the directives in the same manner as the DC inverse problem. # -update_sensitivity_weighting = directives.UpdateSensitivityWeights(threshold=1e-3) +update_sensitivity_weighting = directives.UpdateSensitivityWeights(threshold_value=1e-3) starting_beta = directives.BetaEstimate_ByEig(beta0_ratio=1e2) beta_schedule = directives.BetaSchedule(coolingFactor=2.5, coolingRate=1) save_iteration = directives.SaveOutputEveryIteration(save_txt=False) @@ -672,7 +672,7 @@ chargeable_value = 1e-1 true_chargeability_model = background_value * np.ones(nC) -ind_chargeable = model_builder.getIndicesSphere( +ind_chargeable = model_builder.get_indices_sphere( np.r_[-350.0, 0.0, -300.0], 160.0, mesh.cell_centers[ind_active, :] ) true_chargeability_model[ind_chargeable] = chargeable_value diff --git a/tutorials/07-fdem/plot_fwd_1_em1dfm.py b/tutorials/07-fdem/plot_fwd_1_em1dfm.py index 6abd237d0c..87afd6f83e 100644 --- a/tutorials/07-fdem/plot_fwd_1_em1dfm.py +++ b/tutorials/07-fdem/plot_fwd_1_em1dfm.py @@ -2,7 +2,7 @@ 1D Forward Simulation for a Single Sounding =========================================== -Here we use the module *SimPEG.electromangetics.frequency_domain_1d* to predict +Here we use the module *simpeg.electromangetics.frequency_domain_1d* to predict frequency domain data for a single sounding over a 1D layered Earth. In this tutorial, we focus on the following: @@ -28,9 +28,9 @@ from matplotlib import pyplot as plt from discretize import TensorMesh -from SimPEG import maps -from SimPEG.electromagnetics import frequency_domain as fdem -from SimPEG.utils import plot_1d_layer_model +from simpeg import maps +from simpeg.electromagnetics import frequency_domain as fdem +from simpeg.utils import plot_1d_layer_model plt.rcParams.update({"font.size": 16}) write_output = False @@ -142,7 +142,7 @@ # The simulation requires the user define the survey, the layer thicknesses # and a mapping from the model to the conductivities of the layers. # -# When using the *SimPEG.electromagnetics.frequency_domain_1d* module, +# When using the *simpeg.electromagnetics.frequency_domain_1d* module, # predicted data are organized by source, then by receiver, then by frequency. # @@ -182,7 +182,7 @@ os.mkdir(dir_path) np.random.seed(222) - noise = 0.05 * np.abs(dpred) * np.random.rand(len(dpred)) + noise = 0.05 * np.abs(dpred) * np.random.randn(len(dpred)) dpred += noise fname = dir_path + "em1dfm_data.txt" diff --git a/tutorials/07-fdem/plot_fwd_1_em1dfm_dispersive.py b/tutorials/07-fdem/plot_fwd_1_em1dfm_dispersive.py index ded6972add..d154916d7f 100644 --- a/tutorials/07-fdem/plot_fwd_1_em1dfm_dispersive.py +++ b/tutorials/07-fdem/plot_fwd_1_em1dfm_dispersive.py @@ -2,7 +2,7 @@ 1D Forward Simulation for a Susceptible and Chargeable Earth ============================================================ -Here we use the module *SimPEG.electromangetics.frequency_domain_1d* to compare +Here we use the module *simpeg.electromangetics.frequency_domain_1d* to compare predicted frequency domain data for a single sounding when the Earth is purely conductive, conductive and magnetically susceptible, and when it is chargeable. In this tutorial, we focus on: @@ -26,9 +26,9 @@ import numpy as np from matplotlib import pyplot as plt -from SimPEG import maps -import SimPEG.electromagnetics.frequency_domain as fdem -from SimPEG.electromagnetics.utils.em1d_utils import ColeCole +from simpeg import maps +import simpeg.electromagnetics.frequency_domain as fdem +from simpeg.electromagnetics.utils.em1d_utils import ColeCole plt.rcParams.update({"font.size": 16}) @@ -161,7 +161,7 @@ # parameters used to define the physical properties are permanently set when # defining the simulation. # -# When using the *SimPEG.electromagnetics.frequency_domain_1d* module, note that +# When using the *simpeg.electromagnetics.frequency_domain_1d* module, note that # predicted data are organized by source, then by receiver, then by frequency. # # diff --git a/tutorials/07-fdem/plot_fwd_2_fem_cyl.py b/tutorials/07-fdem/plot_fwd_2_fem_cyl.py index 8ff4307448..9bace19d92 100644 --- a/tutorials/07-fdem/plot_fwd_2_fem_cyl.py +++ b/tutorials/07-fdem/plot_fwd_2_fem_cyl.py @@ -2,7 +2,7 @@ 3D Forward Simulation on a Cylindrical Mesh =========================================== -Here we use the module *SimPEG.electromagnetics.frequency_domain* to simulate the +Here we use the module *simpeg.electromagnetics.frequency_domain* to simulate the FDEM response for a borehole survey using a cylindrical mesh and radially symmetric conductivity model. For this tutorial, we focus on the following: @@ -27,8 +27,8 @@ from discretize import CylindricalMesh from discretize.utils import mkvc -from SimPEG import maps -import SimPEG.electromagnetics.frequency_domain as fdem +from simpeg import maps +import simpeg.electromagnetics.frequency_domain as fdem import numpy as np import matplotlib as mpl @@ -37,7 +37,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver write_file = False @@ -50,7 +50,7 @@ # # Here we define a x-offset borehole survey that consists of a single vertical line # of source-receiver pairs which measred the secondary magnetic flux density -# over a range of frequencies. +# over a range of frequencies. # # Frequencies being predicted (10 Hz to 10000 Hz) diff --git a/tutorials/07-fdem/plot_fwd_3_fem_3d.py b/tutorials/07-fdem/plot_fwd_3_fem_3d.py index d2bd523730..f49bb96056 100644 --- a/tutorials/07-fdem/plot_fwd_3_fem_3d.py +++ b/tutorials/07-fdem/plot_fwd_3_fem_3d.py @@ -2,7 +2,7 @@ 3D Forward Simulation on a Tree Mesh ==================================== -Here we use the module *SimPEG.electromagnetics.frequency_domain* to simulate the +Here we use the module *simpeg.electromagnetics.frequency_domain* to simulate the FDEM response for an airborne survey using an OcTree mesh and a conductivity/resistivity model. To limit computational demant, we simulate airborne data at a single frequency @@ -31,9 +31,9 @@ from discretize import TreeMesh from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz -from SimPEG.utils import plot2Ddata -from SimPEG import maps -import SimPEG.electromagnetics.frequency_domain as fdem +from simpeg.utils import plot2Ddata +from simpeg import maps +import simpeg.electromagnetics.frequency_domain as fdem import os import numpy as np @@ -43,7 +43,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver save_file = False @@ -312,8 +312,8 @@ # Write data with 2% noise added fname = dir_path + "fdem_data.obs" - bz_real = bz_real + 1e-14 * np.random.rand(len(bz_real)) - bz_imag = bz_imag + 1e-14 * np.random.rand(len(bz_imag)) + bz_real = bz_real + 1e-14 * np.random.randn(len(bz_real)) + bz_imag = bz_imag + 1e-14 * np.random.randn(len(bz_imag)) f_vec = np.kron(frequencies, np.ones(ntx)) receiver_locations = np.kron(np.ones((len(frequencies), 1)), receiver_locations) diff --git a/tutorials/07-fdem/plot_inv_1_em1dfm.py b/tutorials/07-fdem/plot_inv_1_em1dfm.py index 8c58fc7adc..f5e5366765 100644 --- a/tutorials/07-fdem/plot_inv_1_em1dfm.py +++ b/tutorials/07-fdem/plot_inv_1_em1dfm.py @@ -2,7 +2,7 @@ 1D Inversion of for a Single Sounding ===================================== -Here we use the module *SimPEG.electromangetics.frequency_domain_1d* to invert +Here we use the module *simpeg.electromangetics.frequency_domain_1d* to invert frequency domain data and recover a 1D electrical conductivity model. In this tutorial, we focus on the following: @@ -18,7 +18,6 @@ """ - ######################################################################### # Import modules # -------------- @@ -31,9 +30,9 @@ from discretize import TensorMesh -import SimPEG.electromagnetics.frequency_domain as fdem -from SimPEG.utils import mkvc, plot_1d_layer_model -from SimPEG import ( +import simpeg.electromagnetics.frequency_domain as fdem +from simpeg.utils import mkvc, plot_1d_layer_model +from simpeg import ( maps, data, data_misfit, @@ -238,7 +237,7 @@ reg = regularization.Sparse(mesh, mapping=reg_map, alpha_s=0.025, alpha_x=1.0) # reference model -reg.mref = starting_model +reg.reference_model = starting_model # Define sparse and blocky norms p, q reg.norms = [0, 0] diff --git a/tutorials/08-tdem/plot_fwd_1_em1dtm.py b/tutorials/08-tdem/plot_fwd_1_em1dtm.py index d217835dad..afe0db1550 100644 --- a/tutorials/08-tdem/plot_fwd_1_em1dtm.py +++ b/tutorials/08-tdem/plot_fwd_1_em1dtm.py @@ -2,7 +2,7 @@ 1D Forward Simulation for a Single Sounding =========================================== -Here we use the module *SimPEG.electromangetics.time_domain_1d* to predict +Here we use the module *simpeg.electromangetics.time_domain_1d* to predict the stepoff response for a single sounding over a 1D layered Earth. In this tutorial, we focus on the following: @@ -26,9 +26,9 @@ import os from matplotlib import pyplot as plt -from SimPEG import maps -import SimPEG.electromagnetics.time_domain as tdem -from SimPEG.utils import plot_1d_layer_model +from simpeg import maps +import simpeg.electromagnetics.time_domain as tdem +from simpeg.utils import plot_1d_layer_model write_output = False plt.rcParams.update({"font.size": 16}) @@ -131,7 +131,7 @@ # The simulation requires the user define the survey, the layer thicknesses # and a mapping from the model to the conductivities of the layers. # -# When using the *SimPEG.electromagnetics.time_domain_1d* module, +# When using the *simpeg.electromagnetics.time_domain_1d* module, # predicted data are organized by source, then by receiver, then by time channel. # @@ -168,7 +168,7 @@ os.mkdir(dir_path) np.random.seed(347) - noise = 0.05 * np.abs(dpred) * np.random.rand(len(dpred)) + noise = 0.05 * np.abs(dpred) * np.random.randn(len(dpred)) dpred += noise fname = dir_path + "em1dtm_data.txt" np.savetxt(fname, np.c_[times, dpred], fmt="%.4e", header="TIME BZ") diff --git a/tutorials/08-tdem/plot_fwd_1_em1dtm_dispersive.py b/tutorials/08-tdem/plot_fwd_1_em1dtm_dispersive.py index 2ce79e7c70..a3dc087cf3 100644 --- a/tutorials/08-tdem/plot_fwd_1_em1dtm_dispersive.py +++ b/tutorials/08-tdem/plot_fwd_1_em1dtm_dispersive.py @@ -2,7 +2,7 @@ 1D Forward Simulation with Chargeable and/or Magnetic Viscosity =============================================================== -Here we use the module *SimPEG.electromangetics.time_domain_1d* to compare +Here we use the module *simpeg.electromangetics.time_domain_1d* to compare predicted time domain data for a single sounding when the Earth is purely conductive, chargeable and/or magnetically viscous. In this tutorial, we focus on: @@ -26,9 +26,9 @@ import numpy as np from matplotlib import pyplot as plt -from SimPEG import maps -import SimPEG.electromagnetics.time_domain as tdem -from SimPEG.electromagnetics.utils.em1d_utils import ColeCole, LogUniform +from simpeg import maps +import simpeg.electromagnetics.time_domain as tdem +from simpeg.electromagnetics.utils.em1d_utils import ColeCole, LogUniform # sphinx_gallery_thumbnail_number = 3 @@ -186,7 +186,7 @@ # parameters used to define the physical properties are permanently set when # defining the simulation. # -# When using the *SimPEG.electromagnetics.time_domain_1d* module, note that +# When using the *simpeg.electromagnetics.time_domain_1d* module, note that # predicted data are organized by source, then by receiver, then by time channel. # # diff --git a/tutorials/08-tdem/plot_fwd_1_em1dtm_waveforms.py b/tutorials/08-tdem/plot_fwd_1_em1dtm_waveforms.py index 945b22ffa6..bd307c843f 100644 --- a/tutorials/08-tdem/plot_fwd_1_em1dtm_waveforms.py +++ b/tutorials/08-tdem/plot_fwd_1_em1dtm_waveforms.py @@ -5,8 +5,8 @@ For time-domain electromagnetic problems, the response depends strongly on the souce waveforms. In this tutorial, we construct a set of waveforms of different types and simulate the response for a halfspace. Many types of waveforms can -be constructed within *SimPEG.electromagnetics.time_domain_1d*. These include: - +be constructed within *simpeg.electromagnetics.time_domain_1d*. These include: + - the unit step off waveform - a set of basic waveforms: rectangular, triangular, quarter sine, etc... - a set of system-specific waveforms: SkyTEM, VTEM, GeoTEM, etc... @@ -26,8 +26,8 @@ mpl.rcParams.update({"font.size": 16}) -from SimPEG import maps -import SimPEG.electromagnetics.time_domain as tdem +from simpeg import maps +import simpeg.electromagnetics.time_domain as tdem ##################################################################### diff --git a/tutorials/08-tdem/plot_fwd_2_tem_cyl.py b/tutorials/08-tdem/plot_fwd_2_tem_cyl.py index bb9c76fed1..f3b81c0361 100644 --- a/tutorials/08-tdem/plot_fwd_2_tem_cyl.py +++ b/tutorials/08-tdem/plot_fwd_2_tem_cyl.py @@ -2,8 +2,8 @@ 3D Forward Simulation for Transient Response on a Cylindrical Mesh ================================================================== -Here we use the module *SimPEG.electromagnetics.time_domain* to simulate the -transient response for borehole survey using a cylindrical mesh and a +Here we use the module *simpeg.electromagnetics.time_domain* to simulate the +transient response for borehole survey using a cylindrical mesh and a radially symmetric conductivity. For this tutorial, we focus on the following: - How to define the transmitters and receivers @@ -29,8 +29,8 @@ from discretize import CylindricalMesh from discretize.utils import mkvc -from SimPEG import maps -import SimPEG.electromagnetics.time_domain as tdem +from simpeg import maps +import simpeg.electromagnetics.time_domain as tdem import numpy as np import matplotlib as mpl @@ -39,7 +39,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver write_file = False @@ -50,7 +50,7 @@ # Defining the Waveform # --------------------- # -# Under *SimPEG.electromagnetic.time_domain.sources* +# Under *simpeg.electromagnetic.time_domain.sources* # there are a multitude of waveforms that can be defined (VTEM, Ramp-off etc...). # Here we simulate the response due to a step off waveform where the off-time # begins at t=0. Other waveforms are discuss in the OcTree simulation example. diff --git a/tutorials/08-tdem/plot_fwd_3_tem_3d.py b/tutorials/08-tdem/plot_fwd_3_tem_3d.py index b711465f86..9b9aecbf79 100644 --- a/tutorials/08-tdem/plot_fwd_3_tem_3d.py +++ b/tutorials/08-tdem/plot_fwd_3_tem_3d.py @@ -2,7 +2,7 @@ 3D Forward Simulation with User-Defined Waveforms ================================================= -Here we use the module *SimPEG.electromagnetics.time_domain* to predict the +Here we use the module *simpeg.electromagnetics.time_domain* to predict the TDEM response for a trapezoidal waveform. We consider an airborne survey which uses a horizontal coplanar geometry. For this tutorial, we focus on the following: @@ -31,9 +31,9 @@ from discretize import TreeMesh from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz -from SimPEG.utils import plot2Ddata -from SimPEG import maps -import SimPEG.electromagnetics.time_domain as tdem +from simpeg.utils import plot2Ddata +from simpeg import maps +import simpeg.electromagnetics.time_domain as tdem import numpy as np import matplotlib as mpl @@ -43,7 +43,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver save_file = False @@ -68,7 +68,7 @@ # Defining the Waveform # --------------------- # -# Under *SimPEG.electromagnetic.time_domain.sources* +# Under *simpeg.electromagnetic.time_domain.sources* # there are a multitude of waveforms that can be defined (VTEM, Ramp-off etc...). # Here, we consider a trapezoidal waveform, which consists of a # linear ramp-on followed by a linear ramp-off. For each waveform, it @@ -364,7 +364,7 @@ # Write data with 2% noise added fname = dir_path + "tdem_data.obs" - dpred = dpred + 0.02 * np.abs(dpred) * np.random.rand(len(dpred)) + dpred = dpred + 0.02 * np.abs(dpred) * np.random.randn(len(dpred)) t_vec = np.kron(np.ones(ntx), time_channels) receiver_locations = np.kron(receiver_locations, np.ones((len(time_channels), 1))) diff --git a/tutorials/08-tdem/plot_inv_1_em1dtm.py b/tutorials/08-tdem/plot_inv_1_em1dtm.py index ae188747c0..e0dcede58a 100644 --- a/tutorials/08-tdem/plot_inv_1_em1dtm.py +++ b/tutorials/08-tdem/plot_inv_1_em1dtm.py @@ -2,7 +2,7 @@ 1D Inversion of Time-Domain Data for a Single Sounding ====================================================== -Here we use the module *SimPEG.electromangetics.time_domain_1d* to invert +Here we use the module *simpeg.electromangetics.time_domain_1d* to invert time domain data and recover a 1D electrical conductivity model. In this tutorial, we focus on the following: @@ -18,7 +18,6 @@ """ - ######################################################################### # Import modules # -------------- @@ -31,10 +30,10 @@ from discretize import TensorMesh -import SimPEG.electromagnetics.time_domain as tdem +import simpeg.electromagnetics.time_domain as tdem -from SimPEG.utils import mkvc, plot_1d_layer_model -from SimPEG import ( +from simpeg.utils import mkvc, plot_1d_layer_model +from simpeg import ( maps, data, data_misfit, @@ -227,7 +226,7 @@ reg = regularization.Sparse(mesh, mapping=reg_map, alpha_s=0.01, alpha_x=1.0) # set reference model -reg.mref = starting_model +reg.reference_model = starting_model # Define sparse and blocky norms p, q reg.norms = [1, 0] diff --git a/tutorials/10-vrm/plot_fwd_1_vrm_layer.py b/tutorials/10-vrm/plot_fwd_1_vrm_layer.py index 8e27997da9..b783113a15 100644 --- a/tutorials/10-vrm/plot_fwd_1_vrm_layer.py +++ b/tutorials/10-vrm/plot_fwd_1_vrm_layer.py @@ -2,7 +2,7 @@ Response from a Homogeneous Layer for Different Waveforms ========================================================= -Here we use the module *SimPEG.electromagnetics.viscous_remanent_magnetization* +Here we use the module *simpeg.electromagnetics.viscous_remanent_magnetization* to predict the characteristic VRM response over magnetically viscous layer. We consider a small-loop, ground-based survey which uses a coincident loop geometry. For this tutorial, we focus on the following: @@ -25,7 +25,7 @@ # -------------- # -import SimPEG.electromagnetics.viscous_remanent_magnetization as vrm +import simpeg.electromagnetics.viscous_remanent_magnetization as vrm from discretize import TensorMesh from discretize.utils import mkvc @@ -40,7 +40,7 @@ # Define Waveforms # ---------------- # -# Under *SimPEG.electromagnetic.viscous_remanent_magnetization.waveform* +# Under *simpeg.electromagnetic.viscous_remanent_magnetization.waveform* # there are a multitude of waveforms that can be defined (Step-off, square-pulse, # piecewise linear, ...). Here we define a specific waveform for each transmitter. # Each waveform is defined with a diferent set of parameters. diff --git a/tutorials/10-vrm/plot_fwd_2_vrm_topsoil.py b/tutorials/10-vrm/plot_fwd_2_vrm_topsoil.py index 4c1444f426..a1a60bec98 100644 --- a/tutorials/10-vrm/plot_fwd_2_vrm_topsoil.py +++ b/tutorials/10-vrm/plot_fwd_2_vrm_topsoil.py @@ -2,7 +2,7 @@ Forward Simulation of VRM Response on a Tree Mesh ================================================= -Here we use the module *SimPEG.electromagnetics.viscous_remanent_magnetization* +Here we use the module *simpeg.electromagnetics.viscous_remanent_magnetization* to predict the characteristic VRM response over magnetically viscous top soil. We consider a small-loop, ground-based survey which uses a coincident loop geometry. For this tutorial, we focus on the following: @@ -26,9 +26,9 @@ # -------------- # -from SimPEG.electromagnetics import viscous_remanent_magnetization as vrm -from SimPEG.utils import plot2Ddata -from SimPEG import maps +from simpeg.electromagnetics import viscous_remanent_magnetization as vrm +from simpeg.utils import plot2Ddata +from simpeg import maps from discretize import TreeMesh from discretize.utils import mkvc, refine_tree_xyz, active_from_xyz diff --git a/tutorials/10-vrm/plot_fwd_3_vrm_tem.py b/tutorials/10-vrm/plot_fwd_3_vrm_tem.py index 4189cfbf67..fece2a72ec 100644 --- a/tutorials/10-vrm/plot_fwd_3_vrm_tem.py +++ b/tutorials/10-vrm/plot_fwd_3_vrm_tem.py @@ -2,8 +2,8 @@ Forward Simulation Including Inductive Response =============================================== -Here we use the modules *SimPEG.electromagnetics.viscous_remanent_magnetization* -and *SimPEG.electromagnetics.time_domain* to simulation the transient response +Here we use the modules *simpeg.electromagnetics.viscous_remanent_magnetization* +and *simpeg.electromagnetics.time_domain* to simulation the transient response over a conductive and magnetically viscous Earth. We consider a small-loop, ground-based survey which uses a coincident loop geometry. Earth is comprised of a conductive pipe and resistive surface layer as well as a magnetically @@ -28,9 +28,9 @@ # -------------- # -import SimPEG.electromagnetics.viscous_remanent_magnetization as vrm -import SimPEG.electromagnetics.time_domain as tdem -from SimPEG import maps +import simpeg.electromagnetics.viscous_remanent_magnetization as vrm +import simpeg.electromagnetics.time_domain as tdem +from simpeg import maps from discretize import TensorMesh, CylindricalMesh from discretize.utils import mkvc @@ -42,7 +42,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver # sphinx_gallery_thumbnail_number = 3 diff --git a/tutorials/12-seismic/plot_fwd_1_tomography_2D.py b/tutorials/12-seismic/plot_fwd_1_tomography_2D.py index c5a1fdff7a..ed82636588 100644 --- a/tutorials/12-seismic/plot_fwd_1_tomography_2D.py +++ b/tutorials/12-seismic/plot_fwd_1_tomography_2D.py @@ -2,7 +2,7 @@ Forward Simulation for Straight Ray Tomography in 2D ==================================================== -Here we module *SimPEG.seismic.straight_ray_tomography* to predict arrival +Here we module *simpeg.seismic.straight_ray_tomography* to predict arrival time data for a synthetic velocity/slowness model. In this tutorial, we focus on the following: - How to define the survey @@ -24,9 +24,9 @@ from discretize import TensorMesh -from SimPEG import maps -from SimPEG.seismic import straight_ray_tomography as tomo -from SimPEG.utils import model_builder +from simpeg import maps +from simpeg.seismic import straight_ray_tomography as tomo +from simpeg.utils import model_builder save_file = False @@ -94,7 +94,7 @@ # Define the model. Models in SimPEG are vector arrays. model = background_velocity * np.ones(mesh.nC) -ind_block = model_builder.getIndicesBlock(np.r_[-50, 20], np.r_[50, -20], mesh.gridCC) +ind_block = model_builder.get_indices_block(np.r_[-50, 20], np.r_[50, -20], mesh.gridCC) model[ind_block] = block_velocity # Define a mapping from the model (velocity) to the slowness. If your model @@ -170,7 +170,7 @@ dir_path.extend(["tutorials", "seismic", "assets"]) dir_path = os.path.sep.join(dir_path) + os.path.sep - noise = 0.05 * dpred * np.random.rand(len(dpred)) + noise = 0.05 * dpred * np.random.randn(len(dpred)) data_array = np.c_[ np.kron(x, np.ones(n_receiver)), diff --git a/tutorials/12-seismic/plot_inv_1_tomography_2D.py b/tutorials/12-seismic/plot_inv_1_tomography_2D.py index cfa786638a..896def24fd 100644 --- a/tutorials/12-seismic/plot_inv_1_tomography_2D.py +++ b/tutorials/12-seismic/plot_inv_1_tomography_2D.py @@ -29,7 +29,7 @@ from discretize import TensorMesh -from SimPEG import ( +from simpeg import ( data, maps, regularization, @@ -41,7 +41,7 @@ utils, ) -from SimPEG.seismic import straight_ray_tomography as tomo +from simpeg.seismic import straight_ray_tomography as tomo # sphinx_gallery_thumbnail_number = 3 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 56ef62c72a..951807633a 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 @@ -36,9 +36,9 @@ from discretize import TensorMesh from discretize.utils import active_from_xyz -from SimPEG.utils import plot2Ddata -from SimPEG.potential_fields import gravity, magnetics -from SimPEG import ( +from simpeg.utils import plot2Ddata +from simpeg.potential_fields import gravity, magnetics +from simpeg import ( maps, data, data_misfit, @@ -196,11 +196,13 @@ inclination = 90 declination = 0 strength = 50000 -inducing_field = (strength, inclination, declination) # Define the source field and survey for gravity data -source_field_mag = magnetics.sources.SourceField( - receiver_list=[receiver_mag], parameters=inducing_field +source_field_mag = magnetics.sources.UniformBackgroundField( + receiver_list=[receiver_mag], + amplitude=strength, + inclination=inclination, + declination=declination, ) survey_mag = magnetics.survey.Survey(source_field_mag) @@ -273,9 +275,21 @@ # Here, we define the physics of the gravity and magnetic problems 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_grav = gravity.simulation.Simulation3DIntegral( - survey=survey_grav, mesh=mesh, rhoMap=wires.density, ind_active=ind_active + survey=survey_grav, + mesh=mesh, + rhoMap=wires.density, + ind_active=ind_active, + engine="choclo", ) simulation_mag = magnetics.simulation.Simulation3DIntegral( @@ -308,15 +322,15 @@ # Define the regularization (model objective function). reg_grav = regularization.WeightedLeastSquares( - mesh, indActive=ind_active, mapping=wires.density + mesh, active_cells=ind_active, mapping=wires.density ) reg_mag = regularization.WeightedLeastSquares( - mesh, indActive=ind_active, mapping=wires.susceptibility + mesh, active_cells=ind_active, mapping=wires.susceptibility ) # Define the coupling term to connect two different physical property models lamda = 2e12 # weight for coupling term -cross_grad = regularization.CrossGradient(mesh, wires, indActive=ind_active) +cross_grad = regularization.CrossGradient(mesh, wires, active_cells=ind_active) # combo dmis = dmis_grav + dmis_mag @@ -363,7 +377,7 @@ stopping = directives.MovingAndMultiTargetStopping(tol=1e-6) -sensitivity_weights = directives.UpdateSensitivityWeights(everyIter=False) +sensitivity_weights = directives.UpdateSensitivityWeights(every_iteration=False) # Updating the preconditionner if it is model dependent. update_jacobi = directives.UpdatePreconditioner() 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 59e842354d..d4a1e3cc13 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 @@ -22,6 +22,7 @@ `_. """ + ######################################################################### # Import modules # -------------- @@ -31,8 +32,8 @@ from discretize.utils import active_from_xyz import matplotlib.pyplot as plt import numpy as np -import SimPEG.potential_fields as pf -from SimPEG import ( +import simpeg.potential_fields as pf +from simpeg import ( data_misfit, directives, inverse_problem, @@ -42,7 +43,7 @@ regularization, utils, ) -from SimPEG.utils import io_utils +from simpeg.utils import io_utils # Reproducible science np.random.seed(518936) @@ -233,6 +234,7 @@ mesh=mesh, rhoMap=wires.den, ind_active=actv, + engine="choclo", ) dmis_grav = data_misfit.L2DataMisfit(data=data_grav, simulation=simulation_grav) # Mag problem 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 6880a7c15c..78582411f1 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 @@ -23,6 +23,7 @@ `_. """ + ######################################################################### # Import modules # -------------- @@ -32,8 +33,8 @@ from discretize.utils import active_from_xyz import matplotlib.pyplot as plt import numpy as np -import SimPEG.potential_fields as pf -from SimPEG import ( +import simpeg.potential_fields as pf +from simpeg import ( data_misfit, directives, inverse_problem, @@ -43,7 +44,7 @@ regularization, utils, ) -from SimPEG.utils import io_utils +from simpeg.utils import io_utils # Reproducible science np.random.seed(518936) @@ -234,6 +235,7 @@ mesh=mesh, rhoMap=wires.den, ind_active=actv, + engine="choclo", ) dmis_grav = data_misfit.L2DataMisfit(data=data_grav, simulation=simulation_grav) # Mag problem diff --git a/tutorials/_temporary/plot_4c_fdem3d_inversion.py b/tutorials/_temporary/plot_4c_fdem3d_inversion.py index 5ac26bff26..c035e10caf 100644 --- a/tutorials/_temporary/plot_4c_fdem3d_inversion.py +++ b/tutorials/_temporary/plot_4c_fdem3d_inversion.py @@ -32,9 +32,9 @@ from discretize import TreeMesh from discretize.utils import refine_tree_xyz, active_from_xyz -from SimPEG.utils import plot2Ddata, mkvc -from SimPEG.electromagnetics import frequency_domain as fdem -from SimPEG import ( +from simpeg.utils import plot2Ddata, mkvc +from simpeg.electromagnetics import frequency_domain as fdem +from simpeg import ( maps, data, data_misfit, @@ -49,7 +49,7 @@ try: from pymatsolver import Pardiso as Solver except ImportError: - from SimPEG import SolverLU as Solver + from simpeg import SolverLU as Solver # sphinx_gallery_thumbnail_number = 3 @@ -313,7 +313,7 @@ # Define the regularization (model objective function) reg = regularization.WeightedLeastSquares( mesh, - indActive=ind_active, + active_cells=ind_active, reference_model=starting_model, alpha_s=1e-2, alpha_x=1, diff --git a/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py b/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py index 24400ea467..f4694830de 100644 --- a/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py +++ b/tutorials/_temporary/plot_fwd_1_em1dtm_stitched_skytem.py @@ -20,13 +20,13 @@ from discretize import TensorMesh from pymatsolver import PardisoSolver -from SimPEG import maps -from SimPEG.utils import mkvc -import SimPEG.electromagnetics.time_domain_1d as em1d -from SimPEG.electromagnetics.utils.em1d_utils import ( +from simpeg import maps +from simpeg.utils import mkvc +import simpeg.electromagnetics.time_domain_1d as em1d +from simpeg.electromagnetics.utils.em1d_utils import ( get_vertical_discretization_time, ) -from SimPEG.electromagnetics.time_domain_1d.known_waveforms import ( +from simpeg.electromagnetics.time_domain_1d.known_waveforms import ( skytem_HM_2015, skytem_LM_2015, ) @@ -304,7 +304,7 @@ def PolygonInd(mesh, pts): dir_path.extend(["tutorials", "08-tdem", "em1dtm_stitched_skytem"]) dir_path = os.path.sep.join(dir_path) + os.path.sep - noise = 0.1 * np.abs(dpred) * np.random.rand(len(dpred)) + noise = 0.1 * np.abs(dpred) * np.random.randn(len(dpred)) dpred += noise fname = dir_path + "em1dtm_stitched_skytem_data.obs" diff --git a/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py b/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py index 7cdb188cba..936b238b3c 100644 --- a/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py +++ b/tutorials/_temporary/plot_inv_1_em1dtm_stitched_skytem.py @@ -20,8 +20,8 @@ from discretize import TensorMesh from pymatsolver import PardisoSolver -from SimPEG.utils import mkvc -from SimPEG import ( +from simpeg.utils import mkvc +from simpeg import ( maps, data, data_misfit, @@ -33,12 +33,12 @@ utils, ) -import SimPEG.electromagnetics.time_domain_1d as em1d -from SimPEG.electromagnetics.utils.em1d_utils import ( +import simpeg.electromagnetics.time_domain_1d as em1d +from simpeg.electromagnetics.utils.em1d_utils import ( get_2d_mesh, get_vertical_discretization_time, ) -from SimPEG.electromagnetics.time_domain_1d.known_waveforms import ( +from simpeg.electromagnetics.time_domain_1d.known_waveforms import ( skytem_HM_2015, skytem_LM_2015, ) @@ -455,7 +455,7 @@ def PolygonInd(mesh, pts): ax2 = fig.add_axes([0.85, 0.12, 0.05, 0.78]) norm = mpl.colors.Normalize( vmin=np.log10(true_model.min()), - vmax=np.log10(true_model.max()) + vmax=np.log10(true_model.max()), # vmin=np.log10(0.1), vmax=np.log10(1) ) cbar = mpl.colorbar.ColorbarBase(