diff --git a/.gitignore b/.gitignore index 9573a99..790d6ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,167 @@ -*.egg-info -*.pyc -*/__pycache__ -/doc/index.rst -/doc/example_dataset.rst -/doc/adjoint_sources/ -/doc/_build/* -/doc/index_files/* -/doc/example_dataset_files/* -*.ipynb_checkpoints* -dist/* +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ .coverage -htmlcov +.coverage.* .cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Misc. +pyadjoint/tests/**png +pyadjoint/tests/baseline_images/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Mac OS +.DS_Store + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ diff --git a/.travis-update-gh-pages.sh b/.travis-update-gh-pages.sh index 0108992..b19864e 100644 --- a/.travis-update-gh-pages.sh +++ b/.travis-update-gh-pages.sh @@ -9,12 +9,12 @@ if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then git config --global user.name "Travis" # Using token clone gh-pages branch. Pipe to /dev/null to avoid printing the decrypted key. - git clone --quiet --branch=gh-pages https://${GH_TOKEN}@github.com/krischer/pyadjoint gh-pages > /dev/null + git clone --quiet --branch=gh-pages https://${GH_TOKEN}@github.com/adjtomo/pyadjoint gh-pages > /dev/null # Go there, and overwrite everything with the freshly built contents. cd gh-pages rm -rf * - cp -Rf $TRAVIS_BUILD_DIR/doc/_build/html/* . + cp -Rf $TRAVIS_BUILD_DIR/docs/_build/html/* . # add, commit and push files git add -f . diff --git a/.travis.yml b/.travis.yml index 58f19da..0bf1c2d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,7 @@ install: script: - coverage run --source=src/pyadjoint -m pyadjoint.tests - - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then cd $TRAVIS_BUILD_DIR/doc; make html ; fi + - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then cd $TRAVIS_BUILD_DIR/docs; make html ; fi after_success: - cd $TRAVIS_BUILD_DIR; coveralls diff --git a/CHANGELOG.md b/CHANGELOG.md index e1ceefd..1a7b1c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,81 @@ -Changelog ---------- +# Change Log +## v0.1.0 +### Package structure +- Renamed 'pyadjoint/doc/' -> 'pyadjoint/docs' +- Renamed 'pyadjoint/src/' -> 'pydjoint/pyadjoint' +- Removed 'setup.py' in favor of new 'pyproject.toml' +- Updated setup URLs for dev team and website +- Reorganized main dir structure to include a utils/ directory which now + houses `dpss` and `utils`. Utils script split into multiple files for + increased organization. +- Revamped documentation, removed the old notebook and source-code bundled + documentation, moved all docs-related info out of source code and into the + upper-level docs/ directory. +- New documentation pages for each of the double difference misfit functions, and exponentiated phase +- New 'furo' theme for documentation + +### Bug fixes +- Plotting: changed `ylim` of figure to max of waveform and not whole plot + because windows were plotted much larger than waveforms + +### New features +- Double difference capabilities added. + - These are placed directly into the respective adjoint sources rather than + separately (as in Princenton/Mines version) to cut down on redundant code + - Controlled with string flag ``choice``=='double_difference'. Requires + additional waveforms and windows (``observed_dd``, ``synthetic_dd``, + ``windows_dd``) + - Main caculate function will return two adjoint sources if + ``double_difference`` is set True +- Waveform misfit has four choices which include: ``waveform``, ``convolution``, ``waveform_dd`` and ``convolution_dd`` +- New Misfit Functions: + - Exponentiated phase + - Convolved waveform difference + - Waveform double difference + - Convolved waveform double difference + - Multitaper double difference +- Config class: + - Introduces individual Configs for each misfit function (as in Princeton v) + - New `get_config` function which provides single entry point to grab + Config objects by name + +### Code architecture +- New ``main.py`` script which now holds all the main processing scripts for + discovering, calculating and plotting adjoint sources +- Changed `window` -> `windows` for list of tuples carrying window information +- Moved plotting functions out of individual adjoint sources and into the + main call function +- All adjoint sources baselined with test data against Princeton/Mines version + of code +- Removed plot capability for User to provide their own Figure object +- Removed boolean return flag for adjoint source and measurements because these + are calculated regardless so might as well return them. May re-introduce if requested. +- Config now carries information on `adjsrc_type`, removed this from the main + calculate_adjoint_source function, as it was redundant to have it in multiple places. + +### Class/Function specific +- AdjointSource class: + - New `window` attribute which holds information about input windows used + to build the adjoint source, and their individual misfit values + - Print statement now deals with `window attribute` + - Write function cleaned up to include both SPECFEM and ASDF write functions +- Waveform Misfit adjoint source: + - Edited calculation of Simpsons rule integration to match Princeton ver. + Now has a factor of 1/2 introduced so misfit is 1/2 original value. + - Baselined with test data against Priceton/Mines version of code +- Cross-correlation Traveltime Misfit: + - Moves bulk of utility/calculation functions to ``pyadjoint.utils.cctm`` + so that MTM can use them allowing for less redundant code +- Multitaper Misfit: + - Overhauled function into a class to avoid the excessive parameter passing + between functions, removed unncessary parameter renaming in-function + - Renamed many of the internal variables and functions for clarity + - Removed redundant CCTM misfit definitions, instead switched to import of + functions from CCTM utility + - Moved 'frequency_limit' function into 'mtm' utils and cleaned up. Removed + unncessary functions which were passing too many variables, and included + doc string. Changed some internal parameter names to match the Config + - Bugfix frequency limit search was not returning a `search` parameter flag + which is meant to stop the search (from Princeton version) + + diff --git a/LICENSE.txt b/LICENSE.txt index 12ebc2d..a992e14 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2015, Lion Krischer +Copyright (c) 2022, adjTomo Dev Team All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index c171688..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -include src/pyadjoint/tests/data/* -include src/pyadjoint/tests/baseline_images/* -include *.md diff --git a/README.md b/README.md index 69f5d63..3f3d4ea 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ -Pyadjoint -========= +# Pyadjoint -PyAdjoint is a Python tool for calculating a variety of adjoint sources for the full waveform inversion problem. - -[Documentation](http://adjtomo.github.io/pyadjoint/) contains details on installation, usage and API. +### Misfit measurement and adjoint source generation +- Pyadjoint is a small misfit measurement package that calculates misfit and adjoint sources on time series +- [Documentation](https://adjtomo.github.io/pyadjoint) for Pyadjoint can be found on GitHub +- Can be used standlone, but is also wrapped in [Pyatoa](https://github.com/adjtomo/pyatoa) (alongside + [Pyflex](https://github.com/adjtomo/pyflex)) +- Pyadjoint is part of the [adjTomo ecosystem](https://github.com/adjtomo) of tools for + adjoint tomography and full waveform inversion diff --git a/doc/README.md b/doc/README.md deleted file mode 100644 index 4cbabb4..0000000 --- a/doc/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Notes on Documentation Building - -**This is only the readme for how to build the documentation. For a rendered - view of the documentation go [here](http://krischer.github.io/pyadjoint/).** - -The complete documentation is contained in the `index.ipynb` IPython notebook. -Always edit that; the `index.rst` file will be overwritten by the sphinx -makefile. - -Please don't commit the notebook with filled cells! Running `make html` will -also strip the output from the notebook. Make sure to run it at least once -before commiting changes to the notebook! - - -To actually build the documentation run - -```bash -make html -``` - -This will execute the notebook and convert it to an rst file which will then -be used by sphinx to build the documentation. It will also strip the output -and some other noise from the notebook. - -Install everything needed to build the documentation (in addition to pyadjoint) -with - -```bash -$ conda install sphinx ipython -$ pip install sphinx-readable-theme runipy -``` - -or only using `pip`: - -```bash -$ pip install sphinx sphinx-readable-theme ipython runipy -``` diff --git a/doc/adjoint_source.rst b/doc/adjoint_source.rst deleted file mode 100644 index 7018feb..0000000 --- a/doc/adjoint_source.rst +++ /dev/null @@ -1,9 +0,0 @@ -======================== -pyadjoint.adjoint_source -======================== - -.. autoclass:: pyadjoint.adjoint_source.AdjointSource - :members: - - -.. autofunction:: pyadjoint.adjoint_source.calculate_adjoint_source diff --git a/doc/conf.py b/doc/conf.py deleted file mode 100644 index 2c0d494..0000000 --- a/doc/conf.py +++ /dev/null @@ -1,286 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Pyadjoint documentation build configuration file, created by -# sphinx-quickstart on Thu Nov 6 21:16:35 2014. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os - -import sphinx_readable_theme - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - - -html_theme_path = [sphinx_readable_theme.get_html_theme_path()] -html_theme = 'readable' - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.viewcode', - 'sphinx.ext.mathjax', - 'sphinx.ext.doctest', - 'matplotlib.sphinxext.plot_directive' -] - -# Configuration options for the plot directive -plot_include_source = False -plot_html_show_source_link = False -plot_html_show_formats = False - - -autoclass_content = 'both' - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'Pyadjoint' -copyright = u'2015, Lion Krischer' - -# 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.0' -# The full version, including alpha/beta/rc tags. -release = '0.0.1a' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = "logo.svg" - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# html_favicon = "favicon.ico" - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = { -#} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -html_use_index = False - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -html_show_sourcelink = False - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -html_show_sphinx = False - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'Pyadjointdoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ('index', 'Pyadjoint.tex', u'Pyadjoint Documentation', - u'Lion Krischer', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'pyadjoint', u'Pyadjoint Documentation', - [u'Lion Krischer'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'Pyadjoint', u'Pyadjoint Documentation', - u'Lion Krischer', 'Pyadjoint', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False - -intersphinx_mapping = { - 'python': ('http://docs.python.org/', None), - 'obspy': ('http://docs.obspy.org/', None), - 'numpy': ('http://docs.scipy.org/doc/numpy/', None), - 'matplotlib': ('http://matplotlib.sourceforge.net/', None) -} diff --git a/doc/convert.py b/doc/convert.py deleted file mode 100644 index e0f9878..0000000 --- a/doc/convert.py +++ /dev/null @@ -1,80 +0,0 @@ -#! /usr/bin/env python -""" -Convert empty IPython notebook to a sphinx doc page. -""" -import io -import os -import sys - -from IPython.nbformat import current - - -def clean_for_doc(nb): - """ - Cleans the notebook to be suitable for inclusion in the docs. - """ - new_cells = [] - for cell in nb.worksheets[0].cells: - # Remove the pylab inline line cells. - if "input" in cell and \ - cell["input"].strip().startswith("%pylab inline"): - continue - - # Make sure all cells are padded at the top and bottom. - if "source" in cell: - cell["source"] = "\n" + cell["source"].strip() + "\n\n" - - # Remove output resulting from the stream/trace method chaining. - if "outputs" in cell: - outputs = [_i for _i in cell["outputs"] if "text" not in _i or - not _i["text"].startswith("= os.path.getmtime(nbname): - print("\t%s is up to date; nothing to do." % rst_name) - return - - os.system("runipy --o %s --matplotlib --quiet" % nbname) - - with io.open(nbname, 'r', encoding='utf8') as f: - nb = current.read(f, 'json') - nb = clean_for_doc(nb) - print("Writing to", nbname) - with io.open(nbname, 'w', encoding='utf8') as f: - current.write(nb, f, 'json') - - # Convert to rst. - os.system("jupyter nbconvert --to rst %s" % nbname) - - with io.open(nbname, 'r', encoding='utf8') as f: - nb = current.read(f, 'json') - nb = strip_output(nb) - print("Writing to", nbname) - with io.open(nbname, 'w', encoding='utf8') as f: - current.write(nb, f, 'json') - -if __name__ == "__main__": - for nbname in sys.argv[1:]: - convert_nb(nbname) diff --git a/doc/create_sphinx_files_for_adjoint_sources.py b/doc/create_sphinx_files_for_adjoint_sources.py deleted file mode 100644 index db632a9..0000000 --- a/doc/create_sphinx_files_for_adjoint_sources.py +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env python -# -*- encoding: utf8 -*- -""" -This will create the sphinx input files for the various defined adjoint -sources. - -:copyright: - Lion Krischer (krischer@geophysik.uni-muenchen.de), 2015 -:license: - BSD 3-Clause ("BSD New" or "BSD Simplified") -""" -import os - - -folder = "adjoint_sources" -if not os.path.exists(folder): - os.makedirs(folder) - - -import pyadjoint - - -TEMPLATE = """ -{upper} -{name} -{lower} - -{description} - -{additional_parameters} - -Usage ------ - -.. doctest:: - - >>> import pyadjoint - >>> obs, syn = pyadjoint.utils.get_example_data() - >>> obs = obs.select(component="Z")[0] - >>> syn = syn.select(component="Z")[0] - >>> start, end = pyadjoint.utils.EXAMPLE_DATA_PDIFF - >>> adj_src = pyadjoint.calculate_adjoint_source( - ... adj_src_type="{short_name}", observed=obs, synthetic=syn, - ... min_period=20.0, max_period=100.0, left_window_border=start, - ... right_window_border=end) - >>> print(adj_src) - {name} Adjoint Source for component Z at station SY.DBO - Misfit: 4.26e-11 - Adjoint source available with 3600 samples - -Example Plots -------------- - -The following shows plots of the :doc:`../example_dataset` for some phases. - -Pdif Phase on Vertical Component -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This example contains *Pdif* and some surface reflected diffracted phases -recorded on the vertical component. - -.. plot:: - - import pyadjoint - import matplotlib.pylab as plt - fig = plt.figure(figsize=(12, 7)) - obs, syn = pyadjoint.utils.get_example_data() - obs = obs.select(component="Z")[0] - syn = syn.select(component="Z")[0] - start, end = pyadjoint.utils.EXAMPLE_DATA_PDIFF - pyadjoint.calculate_adjoint_source("{short_name}", obs, syn, 20.0, 100.0, - start, end, adjoint_src=True, plot=fig) - plt.show() - - -Sdif Phase on Transverse Component -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This example contains *Sdif* and some surface reflected diffracted phases -recorded on the transverse component. - -.. plot:: - - import pyadjoint - import matplotlib.pylab as plt - fig = plt.figure(figsize=(12, 7)) - obs, syn = pyadjoint.utils.get_example_data() - obs = obs.select(component="T")[0] - syn = syn.select(component="T")[0] - start, end = pyadjoint.utils.EXAMPLE_DATA_SDIFF - pyadjoint.calculate_adjoint_source("{short_name}", obs, syn, 20.0, 100.0, - start, end, adjoint_src=True, plot=fig) - plt.show() -""".lstrip() - - - -ADDITIONAL_PARAMETERS_TEMPLATE = """ -Additional Parameters ---------------------- - -Additional parameters in addition to the default ones in the central -:func:`~pyadjoint.adjoint_source.calculate_adjoint_source` function: - -{params} -""".strip() - - -srcs = pyadjoint.AdjointSource._ad_srcs - -srcs = [(key, value) for key, value in srcs.items()] -srcs = sorted(srcs, key=lambda x: x[1][1]) - -for key, value in srcs: - filename = os.path.join(folder, "%s.rst" % key) - - additional_params = "" - if value[3]: - additional_params = ADDITIONAL_PARAMETERS_TEMPLATE.format( - params=value[3]) - - with open(filename, "wt") as fh: - fh.write(TEMPLATE.format( - upper="=" * len(value[1].strip()), - name=value[1].strip(), - lower="=" * len(value[1].strip()), - description=value[2].lstrip(), - short_name=key, - additional_parameters=additional_params - )) - -INDEX = """ -=============== -Adjoint Sources -=============== - -``Pyadjoint`` can currently calculate the following misfits measurements and -associated adjoint sources: - -.. toctree:: - :maxdepth: 1 - - {contents} - -Comparative Plots of All Available Adjoint Sources --------------------------------------------------- - -Pdif Phase on Vertical Component -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This example contains *Pdif* and some surface reflected diffracted phases -recorded on the vertical component. - -.. plot:: plots/all_adjoint_sources_pdif.py - -Sdif Phase on Transverse Component -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This example contains *Sdif* and some surface reflected diffracted phases -recorded on the transverse component. - -.. plot:: plots/all_adjoint_sources_sdif.py - -""".lstrip() - -index_filename = os.path.join(folder, "index.rst") -with open(index_filename, "wt") as fh: - fh.write(INDEX.format( - contents="\n ".join([_i[0] for _i in srcs]))) diff --git a/doc/example_dataset.ipynb b/doc/example_dataset.ipynb deleted file mode 100644 index 6d4463b..0000000 --- a/doc/example_dataset.ipynb +++ /dev/null @@ -1,427 +0,0 @@ -{ - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.4.3" - }, - "name": "" - }, - "nbformat": 3, - "nbformat_minor": 0, - "worksheets": [ - { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "# Example Data Set used in Pyadjoint\n", - "\n", - "This document illustrates where the example data used in Pyadjoint originates from. It uses a set of 3D synthetics from the Shakemovie project and the same event extraced from a 2 second Instaseis database with the AK135 Earth model. Thus we effectively compare the results of a 3D simulation including topography, ellipticity, ... versus a simulation on a 1D background model with a spherical Earth. We will compare data in a period band from 20 to 100 seconds.\n", - "\n", - "To establish a more practical terminology, the Shakemovie seismograms will serve as our observed data, whereas the ones from Instaseis will be considered synthetics.\n", - "\n", - "## Source and Receiver\n", - "\n", - "We use an event from the GCMT catalog:\n", - "\n", - "```\n", - "Event name: 201411150231A\n", - "CMT origin time: 2014-11-15T02:31:50.260000Z\n", - "Assumed half duration: 8.2\n", - "Mw = 7.0 Scalar Moment = 4.71e+19\n", - "Latitude: 1.97\n", - "Longitude: 126.42\n", - "Depth in km: 37.3\n", - "\n", - "Exponent for moment tensor: 19 units: N-m\n", - " Mrr Mtt Mpp Mrt Mrp Mtp\n", - "CMT 3.970 -0.760 -3.210 0.173 -2.220 -1.970\n", - "```\n", - "\n", - "recorded at station `SY.DBO` (`SY` denotes the synthetic data network from the Shakemovie project):\n", - "\n", - "```\n", - "Latitude: 43.12, Longitude: -123.24, Elevation: 984.0 m\n", - "```\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "### Setup Variables\n", - "\n", - "Sets up some values we'll need throughout this document.\n", - "\n" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "import obspy\n", - "import numpy as np\n", - "\n", - "event_longitude = 126.42\n", - "event_latitude = 1.97\n", - "event_depth_in_km = 37.3\n", - "\n", - "station_longitude = -123.24\n", - "station_latitude = 43.12\n", - "\n", - "max_period = 100.0\n", - "min_period = 20.0\n", - "\n", - "cmt_time = obspy.UTCDateTime(2014, 11, 15, 2, 31, 50.26)\n", - "\n", - "# Desired properties after the data processing.\n", - "sampling_rate = 1.0\n", - "npts = 3600" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": null - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "### Map of Source and Receiver\n", - "\n" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "import matplotlib.pyplot as plt\n", - "plt.style.use(\"ggplot\")\n", - "from mpl_toolkits.basemap import Basemap\n", - "\n", - "plt.figure(figsize=(12, 6))\n", - "\n", - "# Equal area mollweide projection.\n", - "m = Basemap(projection=\"moll\", lon_0=180.0, resolution=\"c\")\n", - "m.drawmapboundary(fill_color=\"#cccccc\")\n", - "m.fillcontinents(color=\"white\", lake_color=\"#cccccc\", zorder=0)\n", - "\n", - "m.drawgreatcircle(event_longitude, event_latitude, station_longitude,\n", - " station_latitude, lw=2, color=\"green\")\n", - "m.scatter(event_longitude, event_latitude, color=\"red\", s=500, marker=\"*\",\n", - " latlon=True, zorder=5)\n", - "m.scatter(station_longitude, station_latitude, color=\"blue\", s=400, marker=\"v\",\n", - " latlon=True, zorder=5)\n", - "plt.show()" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": null - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "## Data\n", - "\n", - "*\"Raw\"* data.\n", - "\n" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "shakemovie_data = obspy.read(\"../src/pyadjoint/example_data/shakemovie_data.mseed\")\n", - "instaseis_data = obspy.read(\"../src/pyadjoint/example_data/instaseis_data.mseed\")\n", - "\n", - "print(shakemovie_data)\n", - "print(instaseis_data)" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": null - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "## Data Processing\n", - "\n", - "Both data and synthetics are processed to have similar spectral content and to ensure they are sampled at the same points in time. The processing applied is similar to the typical preprocessing workflow applied to data in full waveform inversions using adjoint techniques. This example lacks instrument removal as both data samples are synthetics.\n", - "\n" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "from obspy.signal.invsim import c_sac_taper\n", - "from obspy.core.util.geodetics import gps2DistAzimuth\n", - "\n", - "f2 = 1.0 / max_period\n", - "f3 = 1.0 / min_period\n", - "f1 = 0.8 * f2\n", - "f4 = 1.2 * f3\n", - "pre_filt = (f1, f2, f3, f4)\n", - "\n", - "def process_function(st):\n", - " st.detrend(\"linear\")\n", - " st.detrend(\"demean\")\n", - " st.taper(max_percentage=0.05, type=\"hann\")\n", - "\n", - " # Perform a frequency domain taper like during the response removal\n", - " # just without an actual response...\n", - " for tr in st:\n", - " data = tr.data.astype(np.float64)\n", - "\n", - " # smart calculation of nfft dodging large primes\n", - " from obspy.signal.util import _npts2nfft\n", - " nfft = _npts2nfft(len(data))\n", - "\n", - " fy = 1.0 / (tr.stats.delta * 2.0)\n", - " freqs = np.linspace(0, fy, nfft // 2 + 1)\n", - "\n", - " # Transform data to Frequency domain\n", - " data = np.fft.rfft(data, n=nfft)\n", - " data *= c_sac_taper(freqs, flimit=pre_filt)\n", - " data[-1] = abs(data[-1]) + 0.0j\n", - " # transform data back into the time domain\n", - " data = np.fft.irfft(data)[0:len(data)]\n", - " # assign processed data and store processing information\n", - " tr.data = data\n", - "\n", - " st.detrend(\"linear\")\n", - " st.detrend(\"demean\")\n", - " st.taper(max_percentage=0.05, type=\"hann\")\n", - "\n", - " st.interpolate(sampling_rate=sampling_rate, starttime=cmt_time,\n", - " npts=npts)\n", - "\n", - " _, baz, _ = gps2DistAzimuth(station_latitude, station_longitude,\n", - " event_latitude, event_longitude)\n", - "\n", - " components = [tr.stats.channel[-1] for tr in st]\n", - " if \"N\" in components and \"E\" in components:\n", - " st.rotate(method=\"NE->RT\", back_azimuth=baz)\n", - "\n", - " return st" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": null - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "# From now one we will refer to them as observed data and synthetics.\n", - "observed = process_function(shakemovie_data.copy())\n", - "synthetic = process_function(instaseis_data.copy())\n", - "\n", - "print(observed)\n", - "print(synthetic)" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": null - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "## Data Plots\n", - "\n", - "We first define a function to plot both data sets.\n", - "\n" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "from obspy.core.util import geodetics\n", - "from obspy.taup import getTravelTimes\n", - "\n", - "def plot_data(start=0, end=1.0 / sampling_rate * npts, show_tts=False):\n", - " start, end = int(start), int(end)\n", - " plt.figure(figsize=(12, 6))\n", - " plt.subplot(311)\n", - "\n", - " obs_z = observed.select(component=\"Z\")[0]\n", - " syn_z = synthetic.select(component=\"Z\")[0]\n", - " obs_r = observed.select(component=\"R\")[0]\n", - " syn_r = synthetic.select(component=\"R\")[0]\n", - " obs_t = observed.select(component=\"T\")[0]\n", - " syn_t = synthetic.select(component=\"T\")[0]\n", - " \n", - " y_range = [obs_z.data[start: end].min(), obs_z.data[start: end].max(),\n", - " syn_z.data[start: end].min(), syn_z.data[start: end].max(),\n", - " obs_r.data[start: end].min(), obs_r.data[start: end].max(),\n", - " syn_r.data[start: end].min(), syn_r.data[start: end].max(),\n", - " obs_t.data[start: end].min(), obs_t.data[start: end].max(),\n", - " syn_t.data[start: end].min(), syn_t.data[start: end].max()]\n", - " y_range = max(map(abs, y_range))\n", - " y_range *= 1.1\n", - " \n", - " dist_in_deg = geodetics.locations2degrees(\n", - " station_latitude, station_longitude,\n", - " event_latitude, event_longitude)\n", - " tts = getTravelTimes(dist_in_deg, event_depth_in_km, model=\"ak135\")\n", - " x_range = end - start\n", - " tts = [_i for _i in tts\n", - " if (start + 0.05 * x_range) < _i[\"time\"] < (end - 0.05 * x_range)]\n", - " \n", - " def plot_tts():\n", - " for _i, tt in enumerate(tts):\n", - " f = 1 if _i % 2 else -1\n", - " va = \"top\" if f is 1 else \"bottom\"\n", - " plt.text(tt[\"time\"], f * y_range * 0.96, tt[\"phase_name\"],\n", - " color=\"0.2\", ha=\"center\", va=va, weight=\"900\",\n", - " fontsize=8)\n", - " \n", - " plt.plot(obs_z.times(), obs_z.data, color=\"black\", label=\"observed\")\n", - " plt.plot(syn_z.times(), syn_z.data, color=\"red\", label=\"synthetic\")\n", - " plt.legend(loc=\"lower left\")\n", - " if show_tts:\n", - " plot_tts()\n", - " plt.xlim(start, end)\n", - " plt.ylim(-y_range, y_range)\n", - " plt.ylabel(\"Displacement in m\")\n", - " plt.title(\"Vertical component\")\n", - "\n", - "\n", - " plt.subplot(312)\n", - " plt.plot(obs_r.times(), obs_r.data, color=\"black\", label=\"observed\")\n", - " plt.plot(syn_r.times(), syn_r.data, color=\"red\", label=\"synthetic\")\n", - " plt.legend(loc=\"lower left\")\n", - " if show_tts:\n", - " plot_tts()\n", - " plt.xlim(start, end)\n", - " plt.ylim(-y_range, y_range)\n", - " plt.ylabel(\"Displacement in m\")\n", - " plt.title(\"Radial component\")\n", - "\n", - " plt.subplot(313)\n", - "\n", - " plt.plot(obs_t.times(), obs_t.data, color=\"black\", label=\"observed\")\n", - " plt.plot(syn_t.times(), syn_t.data, color=\"red\", label=\"synthetic\")\n", - " plt.legend(loc=\"lower left\")\n", - " if show_tts:\n", - " plot_tts()\n", - " plt.ylabel(\"Displacement in m\")\n", - " plt.xlim(start, end)\n", - " plt.ylim(-y_range, y_range)\n", - " plt.xlabel(\"Seconds since event\")\n", - " plt.title(\"Transverse component\")\n", - "\n", - " plt.tight_layout()\n", - "\n", - " plt.show();" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": null - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "### Plot of All Data\n", - "\n" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "plot_data()" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": null - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "### Plot of First Arrivals\n", - "\n" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "plot_data(700, 1200, show_tts=True)" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": null - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "### Plot of Some Later Arrivals\n", - "\n" - ] - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "plot_data(1400, 1900, show_tts=True)" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": null - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "plot_data(2000, 3000, show_tts=True)" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": null - } - ], - "metadata": {} - } - ] -} \ No newline at end of file diff --git a/doc/how_to_add_a_new_adjoint_source.rst b/doc/how_to_add_a_new_adjoint_source.rst deleted file mode 100644 index cb95676..0000000 --- a/doc/how_to_add_a_new_adjoint_source.rst +++ /dev/null @@ -1,81 +0,0 @@ -How to Add a New Adjoint Source Type -===================================== - -A large part of ``Pyadjoint``'s architecture is due to the desire to make it -as easy as possible to add new types of measurements and adjoint sources in -a stable and clean fashion. - -To add a new type of adjoint source just copy the -``src/pyadjoint/adjoint_source_types/waveform_misfit.py`` file to a new -file in the same folder. View the most recent version of the file -`here `_. - -This particular file has been written in a very verbose way to double as a -template and tutorial for new adjoint source types. The new adjoint source -is defined by 5 things: - -**filename** - The filename with the ``.py`` stripped will be the name of the adjoint - source used in the - :func:`~pyadjoint.adjoint_source.calculate_adjoint_source` function. - Keep it lowercase and use underscores to delimit multiple words. - -``VERBOSE_NAME`` variable in the file - This determines the verbose and pretty name of the adjoint source and - misfit. This is used for plots and string representations. - -``DESCRIPTION`` variable in the file - Long and detailed description of the misfit and adjoint source including - formulas, citations, use cases, ... This will be used in the - auto-generated documentation of the adjoint source and should be as - detailed as necessary. - -``ADDITIONAL_PARAMETERS`` variable in the file, optional - If the particular adjoint requires additional parameters in addition to - the default ones, please document them here. - - -``calculate_adjoint_source()`` function in the file - - Function that will be called when actually calculation the misfit and - adjoint source. It must a function with the following signature: - - .. code-block:: python - - def calculate_adjoint_source( - observed, synthetic, min_period, max_period, - left_window_border, right_window_border, - adjoint_src, figure, **kwargs): - pass - - A couple of things to keep in mind: - - 1. Calculate the adjoint source if and only if ``adjoint_src`` is ``True``. - 2. Create a plot if and only if ``figure`` is not ``None``. - 3. Don't ``plt.show()`` the plot. This is handled by a higher-level - function. - 4. Always calculate the misfit value. - 5. The function is responsible for tapering the data and cutting the - window. - 6. The final adjoint source must have the same number of samples and - sampling rate as the input data. - 7. Return the already time-reversed adjoint source. - 8. Return a dictionary with the following structure. - - .. code-block:: python - - return { - "misfit": 1.234, - "adjoint_source": np.array(..., dtype=np.float64) - } - - The ``"adjoint_source"`` key-value pair is optional and depends on - the aforementioned ``adjoint_src`` parameter. Make sure it returns a - ``float64`` numpy array. - - - -This is all you need to do. ``Pyadjoint`` will discover it and connect with -the rest of its architecture. It will generate documentation and perform a -few basic tests fully automatically. At one point you might want to add -additional test to the new adjoint source. \ No newline at end of file diff --git a/doc/index.ipynb b/doc/index.ipynb deleted file mode 100644 index 0c90392..0000000 --- a/doc/index.ipynb +++ /dev/null @@ -1,312 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "# Pyadjoint\n", - "\n", - "`Pyadjoint` is a Python package to measure misfits and calculate adjoint sources. It aims to provide a comprehensive package and incorporate various types of measurements and associated adjoint sources.\n", - "\n", - "## Installation\n", - "\n", - "`Pyadjoint` utilizes [ObsPy](http://obspy.org) (and some of its dependencies) for the processing and data handling. As a first step, please follow the [installation instructions of ObsPy](https://github.com/obspy/obspy/wiki#installation) for your given platform (we recommend the installation with [Anaconda](https://github.com/obspy/obspy/wiki/Installation-via-Anaconda) as it will most likely result in the least amount of problems). `Pyadjoint` should work with Python versions 2.7, 3.4, and 3.5. To install it, best use `pip` (not working yet!):\n", - "\n", - "```bash\n", - "$ pip install pyadjoint\n", - "```\n", - "\n", - "If you want the latest development version, or want to work on the code, you will have to install with the help of `git`.\n", - "\n", - "```bash\n", - "$ git clone https://github.com/krischer/pyadjoint.git\n", - "$ cd pyadjoint\n", - "$ pip install -v -e .\n", - "```\n", - "\n", - "## Tests\n", - "\n", - "To assure the installation is valid and everything works as expected, run the tests with\n", - "\n", - "```bash\n", - "$ python -m pyadjoint.tests\n", - "```\n", - "\n", - "## Usage\n", - "\n", - "### Basic Usage\n", - "\n", - "The first step is to import ObsPy and `Pyadjoint`." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "import obspy\n", - "import pyadjoint" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "`Pyadjoint` expects the data to be fully preprocessed thus both observed and synthetic data are expected to have exactly the same length, sampling rate, and spectral content. `Pyadjoint` furthermore does not care about the actual components in question; it will use two traces and calculate misfit values and adjoint sources for them. To provide a familiar nomenclature we will always talk about observed and synthetic data `Pyadjoint` is independent of what the data actually represents.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# Helper function to get some example data used for\n", - "# illustrative purposes.\n", - "obs, syn = pyadjoint.utils.get_example_data()\n", - "# Select the vertical components of both.\n", - "obs = obs.select(component=\"Z\")[0]\n", - "syn = syn.select(component=\"Z\")[0]" - ] - }, - { - "cell_type": "raw", - "metadata": {}, - "source": [ - "\n", - "Essentially all of ``Pyadjoint``'s functionality is accessed through its central :func:`~pyadjoint.adjoint_source.calculate_adjoint_source` function. A list of available\n", - "adjoint source types can be found in :doc:`adjoint_sources/index`.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "adj_src = pyadjoint.calculate_adjoint_source(\n", - " # The type of misfit measurement and adjoint source.\n", - " adj_src_type=\"waveform_misfit\",\n", - " # Pass observed and synthetic data traces.\n", - " observed=obs, synthetic=syn,\n", - " # The spectral content of the data.\n", - " min_period=20.0, max_period=100.0,\n", - " # The window borders in seconds since the first sample.\n", - " left_window_border=800.0,\n", - " right_window_border=900.0)\n", - "print(adj_src)" - ] - }, - { - "cell_type": "raw", - "metadata": {}, - "source": [ - "\n", - "It returns an :class:`~pyadjoint.adjoint_source.AdjointSource` object.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "# Access misfit and adjoint sources. The misfit is a floating point number.\n", - "print(adj_src.misfit)\n", - "# The adjoint source is a a numpy array.\n", - "print(adj_src.adjoint_source)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "### Usage Options\n", - "\n", - "In case one just wants to calculate the misfit value, pass `adjoint_src=False` in which case the adjoint source will not be calculated. This sometimes is much faster and useful for line searches or similar endeavors.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "print(pyadjoint.calculate_adjoint_source(\n", - " adj_src_type=\"waveform_misfit\", observed=obs, synthetic=syn,\n", - " min_period=20.0, max_period=100.0,\n", - " left_window_border=800.0, right_window_border=900.0, adjoint_src=False))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "All adjoint source types can also be plotted during the calculation. The type of plot produced depends on the type of misfit measurement and adjoint source.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "pyadjoint.calculate_adjoint_source(\n", - " adj_src_type=\"waveform_misfit\", observed=obs, synthetic=syn,\n", - " min_period=20.0, max_period=100.0,\n", - " left_window_border=800.0, right_window_border=900.0, plot=True);" - ] - }, - { - "cell_type": "raw", - "metadata": {}, - "source": [ - "\n", - "Many types of adjoint sources have additional arguments that can be passed to it. The waveform misfit adjoint source for example allows to specifiy the width and type of the taper applied to the data. Please see the documentation of the different :doc:`adjoint_sources/index` for details.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "print(pyadjoint.calculate_adjoint_source(\n", - " adj_src_type=\"waveform_misfit\", observed=obs, synthetic=syn,\n", - " min_period=20.0, max_period=100.0,\n", - " left_window_border=800.0, right_window_border=900.0,\n", - " taper_percentage=0.3, taper_type=\"cosine\"))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "### Saving to Disc\n", - "\n" - ] - }, - { - "cell_type": "raw", - "metadata": {}, - "source": [ - "\n", - "One of course wants to serialize the calculated adjoint sources to disc at one point in time. You need to pass the filename and the desired format as well as some format specific parameters to the :meth:`~pyadjoint.adjoint_source.AdjointSource.write` method of the :class:`~pyadjoint.adjoint_source.AdjointSource` object. Instead of a filename you can also pass an open file or a file-like object. Please refer to its documentation for more details.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "adj_src.write(filename=\"NET.STA.CHA.adj_src\",\n", - " format=\"SPECFEM\", time_offset=-10)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "!head NET.STA.CHA.adj_src\n", - "!rm NET.STA.CHA.adj_src" - ] - }, - { - "cell_type": "raw", - "metadata": {}, - "source": [ - "\n", - "Detailed Documentation\n", - "----------------------\n", - "\n", - "Further Pages\n", - "~~~~~~~~~~~~~\n", - "\n", - "\n", - ".. toctree::\n", - " :maxdepth: 2\n", - "\n", - " adjoint_sources/index\n", - " example_dataset\n", - " citations\n", - " how_to_add_a_new_adjoint_source\n", - " \n", - "\n", - "\n", - "API\n", - "~~~\n", - "\n", - ".. toctree::\n", - " :maxdepth: 2\n", - " \n", - " adjoint_source\n", - " utils\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.5.1" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/doc/plots/all_adjoint_sources_pdif.py b/doc/plots/all_adjoint_sources_pdif.py deleted file mode 100644 index 6786e2a..0000000 --- a/doc/plots/all_adjoint_sources_pdif.py +++ /dev/null @@ -1,72 +0,0 @@ -import pyadjoint -import numpy as np -import matplotlib.pylab as plt -from matplotlib.patches import Rectangle - -srcs = pyadjoint.AdjointSource._ad_srcs - -srcs = [(key, value) for key, value in srcs.items()] -srcs = sorted(srcs, key=lambda x: x[1][1]) - -plt.figure(figsize=(12, 2 * (len(srcs) + 1) )) - - -observed, synthetic = pyadjoint.utils.get_example_data() - -obs = observed.select(component="Z")[0] -syn = synthetic.select(component="Z")[0] - -start, end = pyadjoint.utils.EXAMPLE_DATA_PDIFF - -left_window_border,right_window_border = start, end -x_range = obs.stats.endtime - obs.stats.starttime -buf = (right_window_border - left_window_border) * 1.0 -left_window_border -= buf -right_window_border += buf -left_window_border = max(0, left_window_border) -right_window_border = min(x_range, right_window_border) - -ylim = np.abs(obs.slice(start, x_range - end).data).max() * 1.15 - -plt.subplot(len(srcs) + 1, 1, 1) -plt.plot(obs.times(), obs.data, color="0.2", label="observed", lw=2) -plt.plot(syn.times(), syn.data, color="#B26063", label="synthetic", lw=2) -plt.legend(fancybox=True, framealpha=0.5) -plt.grid() - -plt.ylim(-ylim, ylim) -plt.xlim(left_window_border, right_window_border) - -re = Rectangle((start, plt.ylim()[0]), end - start, - plt.ylim()[1] - plt.ylim()[0], color="blue", - alpha=0.25, zorder=-5) -plt.gca().add_patch(re) -plt.text(x=end - 0.02 * (end - start), - y=plt.ylim()[1] - 0.01 * (plt.ylim()[1] - plt.ylim()[0]), - s="Chosen window", - color="0.2", - fontweight=900, - horizontalalignment="right", - verticalalignment="top", - size="small", multialignment="right") - - -pretty_colors = ["#5B76A1", "#619C6F", "#867CA8", "#BFB281", "#74ACBD"] - -_i = 0 -for key, value in srcs: - _i += 1 - plt.subplot(len(srcs) + 1, 1, _i + 1) - - adj_src = pyadjoint.calculate_adjoint_source(key, obs, syn, 20, 100, - start, end, adjoint_src=True) - - plt.plot(obs.times(), adj_src.adjoint_source, - color=pretty_colors[(_i - 1) % len(pretty_colors)], lw=2, - label=value[1]) - plt.xlim(x_range - right_window_border, x_range - left_window_border) - plt.legend(fancybox=True, framealpha=0.5) - plt.grid() - -plt.tight_layout() -plt.show() diff --git a/doc/plots/all_adjoint_sources_sdif.py b/doc/plots/all_adjoint_sources_sdif.py deleted file mode 100644 index 7e965ee..0000000 --- a/doc/plots/all_adjoint_sources_sdif.py +++ /dev/null @@ -1,72 +0,0 @@ -import pyadjoint -import numpy as np -import matplotlib.pylab as plt -from matplotlib.patches import Rectangle - -srcs = pyadjoint.AdjointSource._ad_srcs - -srcs = [(key, value) for key, value in srcs.items()] -srcs = sorted(srcs, key=lambda x: x[1][1]) - -plt.figure(figsize=(12, 2 * (len(srcs) + 1) )) - - -observed, synthetic = pyadjoint.utils.get_example_data() - -obs = observed.select(component="T")[0] -syn = synthetic.select(component="T")[0] - -start, end = pyadjoint.utils.EXAMPLE_DATA_SDIFF - -left_window_border,right_window_border = start, end -x_range = obs.stats.endtime - obs.stats.starttime -buf = (right_window_border - left_window_border) * 1.0 -left_window_border -= buf -right_window_border += buf -left_window_border = max(0, left_window_border) -right_window_border = min(x_range, right_window_border) - -ylim = np.abs(obs.slice(start, x_range - end).data).max() * 1.15 - -plt.subplot(len(srcs) + 1, 1, 1) -plt.plot(obs.times(), obs.data, color="0.2", label="observed", lw=2) -plt.plot(syn.times(), syn.data, color="#B26063", label="synthetic", lw=2) -plt.legend(fancybox=True, framealpha=0.5) -plt.grid() - -plt.ylim(-ylim, ylim) -plt.xlim(left_window_border, right_window_border) - -re = Rectangle((start, plt.ylim()[0]), end - start, - plt.ylim()[1] - plt.ylim()[0], color="blue", - alpha=0.25, zorder=-5) -plt.gca().add_patch(re) -plt.text(x=end - 0.02 * (end - start), - y=plt.ylim()[1] - 0.01 * (plt.ylim()[1] - plt.ylim()[0]), - s="Chosen window", - color="0.2", - fontweight=900, - horizontalalignment="right", - verticalalignment="top", - size="small", multialignment="right") - - -pretty_colors = ["#5B76A1", "#619C6F", "#867CA8", "#BFB281", "#74ACBD"] - -_i = 0 -for key, value in srcs: - _i += 1 - plt.subplot(len(srcs) + 1, 1, _i + 1) - - adj_src = pyadjoint.calculate_adjoint_source(key, obs, syn, 20, 100, - start, end, adjoint_src=True) - - plt.plot(obs.times(), adj_src.adjoint_source, - color=pretty_colors[(_i - 1) % len(pretty_colors)], lw=2, - label=value[1]) - plt.xlim(x_range - right_window_border, x_range - left_window_border) - plt.legend(fancybox=True, framealpha=0.5) - plt.grid() - -plt.tight_layout() -plt.show() diff --git a/doc/utils.rst b/doc/utils.rst deleted file mode 100644 index b482ff9..0000000 --- a/doc/utils.rst +++ /dev/null @@ -1,6 +0,0 @@ -=============== -pyadjoint.utils -=============== - -.. automodule:: pyadjoint.utils - :members: diff --git a/doc/Makefile b/docs/Makefile similarity index 96% rename from doc/Makefile rename to docs/Makefile index 2779489..28835dc 100644 --- a/doc/Makefile +++ b/docs/Makefile @@ -48,17 +48,9 @@ help: clean: rm -rf $(BUILDDIR)/* - rm -rf example_dataset.rst - rm -rf example_dataset_files - rm -rf index.rst - rm -rf adjoint_sources html: - python ./convert.py index - python ./convert.py example_dataset - python ./create_sphinx_files_for_adjoint_sources.py $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - rm -f windows.json @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." diff --git a/docs/cctm.rst b/docs/cctm.rst new file mode 100644 index 0000000..ff9a283 --- /dev/null +++ b/docs/cctm.rst @@ -0,0 +1,78 @@ +Cross Correlation Traveltime Misfit +==================================== + +.. warning:: + + Please refer to the original paper [Tromp2005]_ for rigorous mathematical + derivations of this misfit function. This documentation page only serves to + summarize their results for the purpose of explaining the underlying code. + +Traveltime misfits simply measure the squared traveltime difference. +The misfit :math:`\chi(\mathbf{m})` for a given Earth model :math:`\mathbf{m}` +and a single receiver and component is given by + +.. math:: + + \chi (\mathbf{m}) = \frac{1}{2} \left[ T^{obs} - T(\mathbf{m}) \right] ^ 2 + +:math:`T^{obs}` is the observed traveltime, and :math:`T(\mathbf{m})` the +predicted traveltime in Earth model :math:`\mathbf{m}`. + +In practice traveltime are measured by cross correlating observed and +predicted waveforms. This particular implementation here measures cross +correlation time shifts with subsample accuracy with a fitting procedure +explained in [Deichmann1992]_. For more details see the documentation of the +:func:`~obspy.signal.cross_correlation.xcorr_pick_correction` function and the +corresponding +`Tutorial `_. + + +The adjoint source for the same receiver and component is then given by + +.. math:: + + f^{\dagger}(t) = - \left[ T^{obs} - T(\mathbf{m}) \right] ~ \frac{1}{N} ~ + \partial_t \mathbf{s}(T - t, \mathbf{m}) + + +:math:`N` is a normalization factor given by + +.. math:: + + N = \int_0^T ~ \mathbf{s}(t, \mathbf{m}) ~ + \partial^2_t \mathbf{s}(t, \mathbf{m}) dt + +.. note:: + + For the sake of simplicity we omit the spatial Kronecker delta and define + the adjoint source as acting solely at the receiver's location. For more + details, please see [Tromp2005]_ and [Bozdag2011]_. + +.. note:: + + This particular implementation here uses + `Simpson's rule `_ + to evaluate the definite integral. + +Usage +````` + +The following code snippets illustrates the basic usage of the cross correlation +traveltime misfit function. + +.. code:: python + + import pyadjoint + + obs, syn = pyadjoint.get_example_data() + obs = obs.select(component="Z")[0] + syn = syn.select(component="Z")[0] + + config = pyadjoint.get_config(adjsrc_type="cc_traveltime_misfit", + min_period=20., max_period=100., + taper_percentage=0.3, taper_type="cos") + + adj_src = pyadjoint.calculate_adjoint_source(config=config, + observed=obs, synthetic=syn, + windows=[(800., 900.)] + ) \ No newline at end of file diff --git a/doc/citations.rst b/docs/citations.rst similarity index 58% rename from doc/citations.rst rename to docs/citations.rst index ea8883d..b7bdadb 100644 --- a/doc/citations.rst +++ b/docs/citations.rst @@ -9,29 +9,49 @@ Citations :widths: 1 4 - * - .. [Bozdag2011] - - | Bozdağ, E., Trampert, J., & Tromp, J. (2011). - | **Misfit functions for full waveform inversion based on instantaneous phase and envelope measurements.** - | *Geophysical Journal International*, 185(2), 845–870. - | http://dx.doi.org/10.1111/j.1365-246X.2011.04970.x * - .. [Deichmann1992] - | Deichmann, N. and Garcia-Fernandez, M. (1992), - | **Rupture geometry from high-precision relative hypocentre locations of microearthquake clusters**, + | **Rupture geometry from high-precision relative hypocentre locations of microearthquake clusters.**, | *Geophysical Journal International*, 110 (3), 501-517. | http://doi.wiley.com/10.1111/j.1365-246X.1992.tb02088.x + * - .. [Laske1996] + - | Laske, G., & Masters, G. (1996), + | ** Constraints on global phase velocity maps from long‐period polarization data.**, + | *Geophysical Research: Solid Earth, 101(B7), 16059-16075. + | https://doi.org/10.1029/96JB00526 + * - .. [Tromp2005] + - | Tromp, J., Tape, C., & Liu, Q. (2005). + | **Seismic tomography, adjoint methods, time reversal and banana-doughnut kernels.** + | *Geophysical Journal International*, 160(1), 195–216. + | http://dx.doi.org/10.1111/j.1365-246X.2004.02453.x * - .. [Fichtner2009] - | Fichtner, A., Kennett, B. L. N., Igel, H., & Bunge, H.-P. (2009). | **Full seismic waveform tomography for upper-mantle structure in the Australasian region using adjoint methods.** | *Geophysical Journal International*, 179(3), 1703–1725. | http://dx.doi.org/10.1111/j.1365-246X.2009.04368.x + * - .. [Bozdag2011] + - | Bozdağ, E., Trampert, J., & Tromp, J. (2011). + | **Misfit functions for full waveform inversion based on instantaneous phase and envelope measurements.** + | *Geophysical Journal International*, 185(2), 845–870. + | http://dx.doi.org/10.1111/j.1365-246X.2011.04970.x + * - .. [Choi2011] + - | Choi, Y., & Alkhalifah, T. (2011). + | **Source-independent time-domain waveform inversion using convolved wavefields: Application to the encoded multisource waveform inversion.** + | *Geophysics*, 76(5), R125-R134. + | https://doi.org/10.1190/geo2010-0210.1 * - .. [Rickers2012] - | Rickers, F., Fichtner, A., & Trampert, J. (2012). | **Imaging mantle plumes with instantaneous phase measurements of diffracted waves.** | *Geophysical Journal International*, 190(1), 650–664. | http://dx.doi.org/10.1111/j.1365-246X.2012.05515.x - * - .. [Tromp2005] - - | Tromp, J., Tape, C., & Liu, Q. (2005). - | **Seismic tomography, adjoint methods, time reversal and banana-doughnut kernels.** - | *Geophysical Journal International*, 160(1), 195–216. - | http://dx.doi.org/10.1111/j.1365-246X.2004.02453.x + * - .. [Yuan2016] + - | Yuan, Y. O., Simons, F. J., & Tromp, J. (2016). + | **Double-difference adjoint seismic tomography.** + | *Geophysical Journal International*, 206(3), 1599-1618. + | https://doi.org/10.1093/gji/ggw233 + * - .. [Yuan2020] + - | Yuan, Y. O., Bozdağ, E., Ciardelli, C., Gao, F., & Simons, F. J. (2020). + | **The exponentiated phase measurement, and objective-function hybridization for adjoint waveform tomography.** + | *Geophysical Journal International*, 221(2), 1145-1164. + | https://doi.org/10.1093/gji/ggaa063 diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..d8f908f --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +import sphinx_rtd_theme + +# -- Project information ----------------------------------------------------- + +project = 'Pyadjoint' +copyright = '2022, adjTomo' +author = 'adjTomo Dev Team' + +# The short X.Y version +version = '0.1.0' +# The full version, including alpha/beta/rc tags +release = '' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode', + 'sphinx.ext.napoleon', + 'sphinx.ext.autosummary', + 'sphinx_rtd_theme', + 'autoapi.extension', + 'nbsphinx', +] + +# Need to tell the autoapi that our source code is one level up +autoapi_type = "python" +autoapi_dirs = ["../pyadjoint", "../pyadjoint/utils"] +autoapi_add_toctree_entry = True + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +source_suffix = {'.rst': 'restructuredtext'} + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '.ipynb'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'furo' +# html_theme = 'sphinx_rtd_theme' + +# Extra HTML options +# html_favicon = 'images/pyatoa_favicon.png' +# html_logo = 'images/pyatoa_inline_text_bigO.png' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ['_static'] +html_static_path = [] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Pyadjointdoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'Pyadjoint.tex', 'Pyadjoint Documentation', + 'Bryant Chow', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'pyadjoint', 'Pyadjoint Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'Pyadjoint', 'Pyadjoint Documentation', + author, 'Pyadjoint', 'Waveform misfit functions and adjoint sources', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# -- Extension configuration ------------------------------------------------- + +# -- Options for intersphinx extension --------------------------------------- + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/': None} + +# -- Options for todo extension ---------------------------------------------- + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True diff --git a/docs/config.rst b/docs/config.rst new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/config.rst @@ -0,0 +1 @@ + diff --git a/docs/convolution.rst b/docs/convolution.rst new file mode 100644 index 0000000..17e868b --- /dev/null +++ b/docs/convolution.rst @@ -0,0 +1,48 @@ +Convolution Misfit +================== + +Very similar to the :doc:`waveform` misfit, the :doc:`convolution` is +defined as the convolution between data and synthetics. The misfit, +:math:`\chi(\mathbf{m})`, for a given Earth model :math:`\mathbf{m}`, and a +single receiver and component is given by + +.. math:: + + \chi (\mathbf{m}) = \frac{1}{2} \int_0^T ( \mathbf{d}(t) * + \mathbf{s}(t, \mathbf{m}) ) ^ 2 dt, + +where :math:`\mathbf{d}(t)` is the observed data and +:math:`\mathbf{s}(t, \mathbf{m})` the synthetic data. + +The corresponding convolution misfit adjoint source for the same receiver and +component is given by + +.. math:: + + f^{\dagger}(t) = \left[ \mathbf{d}(T - t) * + \mathbf{s}(T - t, \mathbf{m}) \right] + +Usage +````` + +The following code snippets illustrates the basic usage of the convolution +misfit function. The ``convolution`` misfit piggybacks on the waveform misft +source code, and consequently shares the same config object. + +.. code:: python + + import pyadjoint + + obs, syn = pyadjoint.get_example_data() + obs = obs.select(component="Z")[0] + syn = syn.select(component="Z")[0] + + config = pyadjoint.get_config(adjsrc_type="waveform_misfit", min_period=20., + max_period=100., taper_percentage=0.3, + taper_type="cos") + + adj_src = pyadjoint.calculate_adjoint_source(config=config, + choice="convolution", + observed=obs, synthetic=syn, + windows=[(800., 900.)] + ) diff --git a/docs/dd_cctm.rst b/docs/dd_cctm.rst new file mode 100644 index 0000000..539ee78 --- /dev/null +++ b/docs/dd_cctm.rst @@ -0,0 +1,117 @@ +Cross Correlation Traveltime Double Difference Misfit +====================================================== + +.. warning:: + + Please refer to the original paper [Yuan2016]_ for rigorous mathematical + derivations of this misfit function. This documentation page only serves to + summarize their results. + +.. note:: + + Double difference misfit functions, defined in [Yuan2016]_, construct misfit + and adjoint sources from differential measurements between stations to reduce + the influence of systematic errors from source and stations. 'Differential' is + defined as "between pairs of stations, from a common source". + +For two stations, `i` and `j`, the cross correlation traveltime double +difference misfit is defined as the squared difference of cross correlations of +observed and synthetic data. The misfit :math:`\chi(\mathbf{m})` for a given +Earth model :math:`\mathbf{m}` at a given component is: + +.. math:: + + \chi (\mathbf{m}) = \frac{1}{2} \int_0^T \left| + \Delta{s}(t, \mathbf{m})_{ij} - + \Delta{d}(t)_{ij} \right| ^ 2 dt, + +where :math:`\Delta{s}(t, \mathbf{m})_{ij}` is the cross correlation traveltime +time shift of synthetic waveforms `s`: + +.. math:: + + \Delta{s}(t, \mathbf{m})_{ij} = \mathrm{argmax}_{\tau} \int_0^T + s_{i}(t + \tau, \mathbf{m}) s_{j}(t, \mathbf{m})dt, + + +and :math:`\mathbf{d}(t)` is the cross correlation traveltime time shift of +observed waveforms `d`, + +.. math:: + + \Delta{d}(t)_{ij} = \mathrm{argmax}_{\tau} \int_0^T + d_{i}(t + \tau) d_{j}(t)dt. + +Double difference misfit functions result in two adjoint sources, one for each +station in the pair `i`, `j`. The corresponding adjoint sources for the misfit +function :math:`\chi(\mathbf{m})` is defined as the difference of the +differential waveform misfits: + +.. math:: + + f_{i}^{\dagger}(t) = + + \frac{\Delta{s}(t, \mathbf{m})_{ij} - \Delta{d}(t)_{ij}}{N_{ij}} + \partial{s_j}(T-[t-\Delta s_{ij}]) + + f_{j}^{\dagger}(t) = + - \frac{\Delta{s}(t, \mathbf{m})_{ij} - \Delta{d}(t)_{ij}}{N_{ij}} + \partial{s_j}(T-[t+\Delta s_{ij}]) + +Where the normalization factor :math:`N_{ij}` is defined as: + +.. math:: + + N_{ij} = \int_0^T \partial{t}^{2}s_i(t + \Delta s_{ij})s_j(t)dt + +.. note:: + + For the sake of simplicity we omit the spatial Kronecker delta and define + the adjoint source as acting solely at the receiver's location. For more + details, please see [Tromp2005]_ and [Bozdag2011]_. + +.. note:: + + This particular implementation here uses + `Simpson's rule `_ + to evaluate the definite integral. + +Usage +````` + +The following code snippets illustrates the basic usage of the double difference +CCTM misfit function. + +Note that double difference implementations can take a set of windows for the +second set of waveforms, independent of the first set of windows. Windows +are compared in order, so both ``windows`` and ``windows_2`` need to be the same +length. + +.. note:: + + In the following code snippet, we use the 'R' component of the same station + in liue of waveforms from a second station. In practice, the second set of + waveforms should come from a completely different station. + +.. code:: python + + import pyadjoint + + obs, syn = pyadjoint.get_example_data() + obs = obs.select(component="Z")[0] + syn = syn.select(component="Z")[0] + + obs_2, syn_2 = pyadjoint.get_example_data() + obs_2 = obs_2.select(component="R")[0] + syn_2 = syn_2.select(component="R")[0] + + config = pyadjoint.get_config(adjsrc_type="cc_traveltime_misfit", + min_period=20., max_period=100., + taper_percentage=0.3, taper_type="cos") + + # Calculating double-difference adjoint source returns two adjoint sources + adj_src, adj_src_2 = pyadjoint.calculate_adjoint_source( + config=config, observed=obs, synthetic=syn, windows=[(800., 900.)], + choice="double_difference", observed_2=obs_2, synthetic_2=syn_2, + windows_2=[(800., 900.)] + ) + diff --git a/docs/dd_convolution.rst b/docs/dd_convolution.rst new file mode 100644 index 0000000..666db26 --- /dev/null +++ b/docs/dd_convolution.rst @@ -0,0 +1,94 @@ +Convolution Double Difference Misfit +==================================== + +.. note:: + + Double difference misfit functions, defined in [Yuan2016]_, construct misfit + and adjoint sources from differential measurements between stations to reduce + the influence of systematic errors from source and stations. "Differential" is + defined as "between pairs of stations, from a common source". + + +For two stations, `i` and `j`, the convolution double difference misfit is +defined as the squared difference of convolution of observed and synthetic data. +The misfit :math:`\chi(\mathbf{m})` for a given Earth model :math:`\mathbf{m}` at +a given component is: + +.. math:: + + \chi (\mathbf{m}) = \frac{1}{2} \int_0^T \left| + {s}_i(t, \mathbf{m}) * d_j(t) - + {d}_j(t) * s_i(t, \mathbf{m}) + \right| ^ 2 dt, + + +Double difference misfit functions result in two adjoint sources, one for each +station in the pair `i`, `j`. The corresponding adjoint sources for the misfit +function :math:`\chi(\mathbf{m})` is defined as the difference of the +differential waveform misfits: + +.. math:: + + f_{i}^{\dagger}(t) = + + ( {s}_i(t, \mathbf{m}) * d_j(t) - + {d}_j(t) * s_i(t, \mathbf{m})) + + f_{j}^{\dagger}(t) = + - ({s}_i(t, \mathbf{m}) * d_j(t) - + {d}_j(t) * s_i(t, \mathbf{m})) + + +.. note:: + + For the sake of simplicity we omit the spatial Kronecker delta and define + the adjoint source as acting solely at the receiver's location. For more + details, please see [Tromp2005]_ and [Yuan2016]_. + +.. note:: + + This particular implementation here uses + `Simpson's rule `_ + to evaluate the definite integral. + +Usage +````` + +The following code snippets illustrates the basic usage of the waveform +misfit function. + +Note that double difference implementations can take a set of windows for the +second set of waveforms, independent of the first set of windows. Windows +are compared in order, so both ``windows`` and ``windows_2`` need to be the same +length. + +.. note:: + + In the following code snippet, we use the 'R' component of the same station + in liue of waveforms from a second station. In practice, the second set of + waveforms should come from a completely different station. + + +.. code:: python + + import pyadjoint + + obs, syn = pyadjoint.get_example_data() + obs = obs.select(component="Z")[0] + syn = syn.select(component="Z")[0] + + obs_2, syn_2 = pyadjoint.get_example_data() + obs_2 = obs_2.select(component="R")[0] + syn_2 = syn_2.select(component="R")[0] + + config = pyadjoint.get_config(adjsrc_type="waveform_misfit", min_period=20., + max_period=100., taper_percentage=0.3, + taper_type="cos") + + # Calculating double-difference adjoint source returns two adjoint sources + adj_src, adj_src_2 = pyadjoint.calculate_adjoint_source( + config=config, observed=obs, synthetic=syn, windows=[(800., 900.)], + choice="convolution_dd", observed_2=obs_2, synthetic_2=syn_2, + windows_2=[(800., 900.)] + ) + + diff --git a/docs/dd_mtm.rst b/docs/dd_mtm.rst new file mode 100644 index 0000000..5516594 --- /dev/null +++ b/docs/dd_mtm.rst @@ -0,0 +1,55 @@ +Double Difference Multitaper Misfit +=================================== + +.. note:: + + Double difference misfit functions, defined in [Yuan2016]_, construct misfit + and adjoint sources from differential measurements between stations to reduce + the influence of systematic errors from source and stations. 'Differential' is + defined as "between pairs of stations, from a common source". + +Due to the length and complexity of the equations for double difference +multitaper misfit, please see [Yuan2016]_ Appendix A1 and A2 for the +mathematical expressions that define misfit and adjoint source. + + +Usage +````` + +The following code snippets illustrates the basic usage of the double +difference multitaper misfit function. + +Note that double difference implementations can take a set of windows for the +second set of waveforms, independent of the first set of windows. Windows +are compared in order, so both ``windows`` and ``windows_2`` need to be the same +length. + +.. note:: + + In the following code snippet, we use the 'R' component of the same station + in liue of waveforms from a second station. In practice, the second set of + waveforms should come from a completely different station. + +.. code:: python + + import pyadjoint + + obs, syn = pyadjoint.get_example_data() + obs = obs.select(component="Z")[0] + syn = syn.select(component="Z")[0] + + obs_2, syn_2 = pyadjoint.get_example_data() + obs_2 = obs_2.select(component="R")[0] + syn_2 = syn_2.select(component="R")[0] + + config = pyadjoint.get_config(adjsrc_type="multitaper_misfit", min_period=20., + max_period=100., taper_percentage=0.3, + taper_type="cos") + + # Calculating double-difference adjoint source returns two adjoint sources + adj_src, adj_src_2 = pyadjoint.calculate_adjoint_source( + config=config, observed=obs, synthetic=syn, windows=[(800., 900.)], + choice="double_difference", observed_2=obs_2, synthetic_2=syn_2, + windows_2=[(800., 900.)] + ) + diff --git a/docs/dd_waveform.rst b/docs/dd_waveform.rst new file mode 100644 index 0000000..fb65133 --- /dev/null +++ b/docs/dd_waveform.rst @@ -0,0 +1,113 @@ +Waveform Double Difference Misfit +================================= + +.. warning:: + + Please refer to the papers [Tromp2005]_ and [Yuan2016]_ for mathematical + derivations of the wavefoform misfit function and cross correlation + double difference measurement. This misfit function is not explicitely + derived there, but follows as a natural extension from these publications. + + +.. note:: + + Double difference misfit functions, defined in [Yuan2016]_, construct misfit + and adjoint sources from differential measurements between stations to reduce + the influence of systematic errors from source and stations. "Differential" is + defined as "between pairs of stations, from a common source". + +For two stations, `i` and `j`, the waveform double difference misfit is defined +as the squared difference of differences of observed and synthetic data. The +misfit :math:`\chi(\mathbf{m})` for a given Earth model :math:`\mathbf{m}` at +a given component is: + +.. math:: + + \chi (\mathbf{m}) = \frac{1}{2} \int_0^T \left| + \Delta{s}(t, \mathbf{m})_{ij} - + \Delta{d}(t)_{ij} \right| ^ 2 dt, + +where :math:`\Delta{s}(t, \mathbf{m})_{ij}` is the difference of +synthetic waveforms `s`: + +.. math:: + + \Delta{s}(t, \mathbf{m})_{ij} = + s_{j}(t, \mathbf{m}) - s_{i}(t, \mathbf{m}), + + +and :math:`\Delta{d}(t)` is the difference of observed waveforms `d`, + +.. math:: + + \Delta{d}(t)_{ij} = d_{j}(t) - d_{i}(t). + + +Double difference misfit functions result in two adjoint sources, one for each +station in the pair `i`, `j`. The corresponding adjoint sources for the misfit +function :math:`\chi(\mathbf{m})` is defined as the difference of the +differential waveform misfits: + +.. math:: + + f_{i}^{\dagger}(t) = + + (\Delta{s}(t, \mathbf{m})_{ij} - \Delta{d}(t)_{ij}) + + f_{j}^{\dagger}(t) = + - (\Delta{s}(t, \mathbf{m})_{ij} - \Delta{d}(t)_{ij}) + + +.. note:: + + For the sake of simplicity we omit the spatial Kronecker delta and define + the adjoint source as acting solely at the receiver's location. For more + details, please see [Tromp2005]_ and [Yuan2016]_. + +.. note:: + + This particular implementation here uses + `Simpson's rule `_ + to evaluate the definite integral. + +Usage +````` + +The following code snippets illustrates the basic usage of the waveform +misfit function. + +Note that double difference implementations can take a set of windows for the +second set of waveforms, independent of the first set of windows. Windows +are compared in order, so both ``windows`` and ``windows_2`` need to be the same +length. + +.. note:: + + In the following code snippet, we use the 'R' component of the same station + in liue of waveforms from a second station. In practice, the second set of + waveforms should come from a completely different station. + + +.. code:: python + + import pyadjoint + + obs, syn = pyadjoint.get_example_data() + obs = obs.select(component="Z")[0] + syn = syn.select(component="Z")[0] + + obs_2, syn_2 = pyadjoint.get_example_data() + obs_2 = obs_2.select(component="R")[0] + syn_2 = syn_2.select(component="R")[0] + + config = pyadjoint.get_config(adjsrc_type="waveform_misfit", min_period=20., + max_period=100., taper_percentage=0.3, + taper_type="cos") + + # Calculating double-difference adjoint source returns two adjoint sources + adj_src, adj_src_2 = pyadjoint.calculate_adjoint_source( + config=config, observed=obs, synthetic=syn, windows=[(800., 900.)], + choice="waveform_dd", observed_2=obs_2, synthetic_2=syn_2, + windows_2=[(800., 900.)] + ) + + diff --git a/docs/example_dataset.rst b/docs/example_dataset.rst new file mode 100644 index 0000000..8460fc3 --- /dev/null +++ b/docs/example_dataset.rst @@ -0,0 +1,286 @@ +Example Dataset +=============== + +This document illustrates where the example data used in Pyadjoint +originates from. It uses a set of 3D synthetics from the Shakemovie +project and the same event extraced from a 2 second Instaseis database +with the AK135 Earth model. Thus we effectively compare the results of a +3D simulation including topography, ellipticity, … versus a simulation +on a 1D background model with a spherical Earth. We will compare data in +a period band from 20 to 100 seconds. + +To establish a more practical terminology, the Shakemovie seismograms +will serve as our observed data, whereas the ones from Instaseis will be +considered synthetics. + +Source and Receiver +------------------- + +We use an event from the GCMT catalog: + +:: + + Event name: 201411150231A + CMT origin time: 2014-11-15T02:31:50.260000Z + Assumed half duration: 8.2 + Mw = 7.0 Scalar Moment = 4.71e+19 + Latitude: 1.97 + Longitude: 126.42 + Depth in km: 37.3 + + Exponent for moment tensor: 19 units: N-m + Mrr Mtt Mpp Mrt Mrp Mtp + CMT 3.970 -0.760 -3.210 0.173 -2.220 -1.970 + +recorded at station ``SY.DBO`` (``SY`` denotes the synthetic data +network from the Shakemovie project): + +:: + + Latitude: 43.12, Longitude: -123.24, Elevation: 984.0 m + +Setup Variables +~~~~~~~~~~~~~~~ + +Sets up some values we’ll need throughout this document. + +.. code:: python + + import obspy + import numpy as np + + event_longitude = 126.42 + event_latitude = 1.97 + event_depth_in_km = 37.3 + + station_longitude = -123.24 + station_latitude = 43.12 + + max_period = 100.0 + min_period = 20.0 + + cmt_time = obspy.UTCDateTime(2014, 11, 15, 2, 31, 50.26) + + # Desired properties after the data processing. + sampling_rate = 1.0 + npts = 3600 + +Map of Source and Receiver +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + import matplotlib.pyplot as plt + plt.style.use("ggplot") + from mpl_toolkits.basemap import Basemap + + plt.figure(figsize=(12, 6)) + + # Equal area mollweide projection. + m = Basemap(projection="moll", lon_0=180.0, resolution="c") + m.drawmapboundary(fill_color="#cccccc") + m.fillcontinents(color="white", lake_color="#cccccc", zorder=0) + + m.drawgreatcircle(event_longitude, event_latitude, station_longitude, + station_latitude, lw=2, color="green") + m.scatter(event_longitude, event_latitude, color="red", s=500, marker="*", + latlon=True, zorder=5) + m.scatter(station_longitude, station_latitude, color="blue", s=400, marker="v", + latlon=True, zorder=5) + plt.show() + +Data +---- + +*“Raw”* data. + +.. code:: python + + shakemovie_data = obspy.read("../src/pyadjoint/example_data/shakemovie_data.mseed") + instaseis_data = obspy.read("../src/pyadjoint/example_data/instaseis_data.mseed") + + print(shakemovie_data) + print(instaseis_data) + +Data Processing +--------------- + +Both data and synthetics are processed to have similar spectral content +and to ensure they are sampled at the same points in time. The +processing applied is similar to the typical preprocessing workflow +applied to data in full waveform inversions using adjoint techniques. +This example lacks instrument removal as both data samples are +synthetics. + +.. code:: python + + from obspy.signal.invsim import c_sac_taper + from obspy.core.util.geodetics import gps2DistAzimuth + + f2 = 1.0 / max_period + f3 = 1.0 / min_period + f1 = 0.8 * f2 + f4 = 1.2 * f3 + pre_filt = (f1, f2, f3, f4) + + def process_function(st): + st.detrend("linear") + st.detrend("demean") + st.taper(max_percentage=0.05, type="hann") + + # Perform a frequency domain taper like during the response removal + # just without an actual response... + for tr in st: + data = tr.data.astype(np.float64) + + # smart calculation of nfft dodging large primes + from obspy.signal.util import _npts2nfft + nfft = _npts2nfft(len(data)) + + fy = 1.0 / (tr.stats.delta * 2.0) + freqs = np.linspace(0, fy, nfft // 2 + 1) + + # Transform data to Frequency domain + data = np.fft.rfft(data, n=nfft) + data *= c_sac_taper(freqs, flimit=pre_filt) + data[-1] = abs(data[-1]) + 0.0j + # transform data back into the time domain + data = np.fft.irfft(data)[0:len(data)] + # assign processed data and store processing information + tr.data = data + + st.detrend("linear") + st.detrend("demean") + st.taper(max_percentage=0.05, type="hann") + + st.interpolate(sampling_rate=sampling_rate, starttime=cmt_time, + npts=npts) + + _, baz, _ = gps2DistAzimuth(station_latitude, station_longitude, + event_latitude, event_longitude) + + components = [tr.stats.channel[-1] for tr in st] + if "N" in components and "E" in components: + st.rotate(method="NE->RT", back_azimuth=baz) + + return st + +.. code:: python + + # From now one we will refer to them as observed data and synthetics. + observed = process_function(shakemovie_data.copy()) + synthetic = process_function(instaseis_data.copy()) + + print(observed) + print(synthetic) + +Data Plots +---------- + +We first define a function to plot both data sets. + +.. code:: python + + from obspy.core.util import geodetics + from obspy.taup import getTravelTimes + + def plot_data(start=0, end=1.0 / sampling_rate * npts, show_tts=False): + start, end = int(start), int(end) + plt.figure(figsize=(12, 6)) + plt.subplot(311) + + obs_z = observed.select(component="Z")[0] + syn_z = synthetic.select(component="Z")[0] + obs_r = observed.select(component="R")[0] + syn_r = synthetic.select(component="R")[0] + obs_t = observed.select(component="T")[0] + syn_t = synthetic.select(component="T")[0] + + y_range = [obs_z.data[start: end].min(), obs_z.data[start: end].max(), + syn_z.data[start: end].min(), syn_z.data[start: end].max(), + obs_r.data[start: end].min(), obs_r.data[start: end].max(), + syn_r.data[start: end].min(), syn_r.data[start: end].max(), + obs_t.data[start: end].min(), obs_t.data[start: end].max(), + syn_t.data[start: end].min(), syn_t.data[start: end].max()] + y_range = max(map(abs, y_range)) + y_range *= 1.1 + + dist_in_deg = geodetics.locations2degrees( + station_latitude, station_longitude, + event_latitude, event_longitude) + tts = getTravelTimes(dist_in_deg, event_depth_in_km, model="ak135") + x_range = end - start + tts = [_i for _i in tts + if (start + 0.05 * x_range) < _i["time"] < (end - 0.05 * x_range)] + + def plot_tts(): + for _i, tt in enumerate(tts): + f = 1 if _i % 2 else -1 + va = "top" if f is 1 else "bottom" + plt.text(tt["time"], f * y_range * 0.96, tt["phase_name"], + color="0.2", ha="center", va=va, weight="900", + fontsize=8) + + plt.plot(obs_z.times(), obs_z.data, color="black", label="observed") + plt.plot(syn_z.times(), syn_z.data, color="red", label="synthetic") + plt.legend(loc="lower left") + if show_tts: + plot_tts() + plt.xlim(start, end) + plt.ylim(-y_range, y_range) + plt.ylabel("Displacement in m") + plt.title("Vertical component") + + + plt.subplot(312) + plt.plot(obs_r.times(), obs_r.data, color="black", label="observed") + plt.plot(syn_r.times(), syn_r.data, color="red", label="synthetic") + plt.legend(loc="lower left") + if show_tts: + plot_tts() + plt.xlim(start, end) + plt.ylim(-y_range, y_range) + plt.ylabel("Displacement in m") + plt.title("Radial component") + + plt.subplot(313) + + plt.plot(obs_t.times(), obs_t.data, color="black", label="observed") + plt.plot(syn_t.times(), syn_t.data, color="red", label="synthetic") + plt.legend(loc="lower left") + if show_tts: + plot_tts() + plt.ylabel("Displacement in m") + plt.xlim(start, end) + plt.ylim(-y_range, y_range) + plt.xlabel("Seconds since event") + plt.title("Transverse component") + + plt.tight_layout() + + plt.show(); + +Plot of All Data +~~~~~~~~~~~~~~~~ + +.. code:: python + + plot_data() + +Plot of First Arrivals +~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + plot_data(700, 1200, show_tts=True) + +Plot of Some Later Arrivals +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: python + + plot_data(1400, 1900, show_tts=True) + +.. code:: python + + plot_data(2000, 3000, show_tts=True) diff --git a/docs/exp_phase.rst b/docs/exp_phase.rst new file mode 100644 index 0000000..40d9953 --- /dev/null +++ b/docs/exp_phase.rst @@ -0,0 +1,93 @@ +Exponentiated Phase Misfit +========================== + +.. warning:: + + Please refer to the original paper [Yuan2020]_ for rigorous mathematical + derivations of this misfit function. This documentation page only serves to + summarize their results for the purpose of explaining the underlying code. + +The exponentiated phase misfit, defined in [Yuan2020]_, measures misfit +using a complex-valued phase representation that is a good substitute for +instantaneous-phase measurements, which suffer from phase wrapping. + +Exponentiated phase misfit measure the difference between observed and the +synthetic normalized analytic signals. The misfit :math:`\chi(\mathbf{m})` for +a given Earth model :math:`\mathbf{m}` and a single receiver and component is +given by + +.. math:: + + \chi (\mathbf{m}) = + \frac{1}{2} \int_0^T \left[ \left\Vert \Delta R(t)\right\Vert^2 - + \left\Vert\Delta I(t)\right\Vert^2 \right]dt, + +where :math:`\Delta R(t)`, the difference in the real parts is: + +.. math:: + + \Delta R(t) = \frac{d(t)}{E_d(t)} - \frac{s(t)}{E_s(t)}, + +and :math:`\Delta I(t)`, the difference in the imaginary parts: + +.. math:: + + \Delta I(t) = \frac{\mathcal{H}\{d(t)\}}{E_d(t)} - + \frac{\mathcal{H}\{s(t)\}}{E_s(t)}. + +Above, :math:`\mathcal{H}` represents the `Hilbert transform +`__ +of the signal in the curly braces, and :math:`E_s` and :math:`E_d` represent the +instantaneous phase of synthetics and data, respectively: + +.. math:: + + E_s(t) = \sqrt{s^2(t) + \mathcal{H}^2\{s(t)\}} + + E_d(t) = \sqrt{d^2(t) + \mathcal{H}^2\{d(t)\}} + + +The adjoint source for the exponentiated phase misfit function for a given +receiver and component is given by: + +.. math:: + + f^{\dagger}(t) = \left[ + \Delta I(t) \frac{s(t)\mathcal{H}\{s(t)\}}{E^3_s(t)} + - \Delta R(t) \frac{[\mathcal{H}\{s(t)\}]^2}{E^3_s(t)} + + \mathcal{H}\left\{ + \Delta I(t) \frac{s^2(t)}{E^3_s(t)} + - \Delta R(t) \frac{[s(t)\mathcal{H}\{s(t)\}}{E^3_s(t)} + \right\} + \right] + + +.. note:: + + For the sake of simplicity we omit the spatial Kronecker delta and define + the adjoint source as acting solely at the receiver's location. For more + details, please see [Tromp2005]_ and [Yuan2020]_. + + +Usage +````` + +The following code snippets illustrates the basic usage of the cross correlation +traveltime misfit function. + +.. code:: python + + import pyadjoint + + obs, syn = pyadjoint.get_example_data() + obs = obs.select(component="Z")[0] + syn = syn.select(component="Z")[0] + + config = pyadjoint.get_config(adjsrc_type="exponentiated_phase_misfit", + min_period=20., max_period=100., + taper_percentage=0.3, taper_type="cos") + + adj_src = pyadjoint.calculate_adjoint_source(config=config, + observed=obs, synthetic=syn, + windows=[(800., 900.)] + ) \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..d7073a0 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,85 @@ +=================================================== +Pyadjoint +=================================================== +`Pyadjoint `__ is an open-source Python +package for calculating time-dependent time series misfit, offering a number of +different misfit functions. It was designed to generate adjoint sources for +full waveform inversion and adjoint tomography. + +.. note:: + We recommend Pyadjoint within the larger misfit quantification package + `Pyatoa `__, as opposed to standalone. + +Pyadjoint is hosted on `GitHub `__ as +part of the `adjTomo organization `__. + + +Have a look at the `Pyadjoint usage `__ page to learn how +Pyadjoint is used. Browse available adjoint sources using the navigation bar. + +-------------- + +Installation +~~~~~~~~~~~~ + +It is recommended that Pyadjoint be installed inside a `Conda +`__ environment. +The ``devel`` branch provides the latest codebase. + +.. code:: bash + + git clone https://github.com/adjtomo/pyadjoint.git + cd pyadjoint + conda env create -n pyadjoint + conda activate pyadjoint + conda install obspy + pip install -e . + + +Running Tests +````````````` + +Tests ensure Pyadjoint runs as expected after changes are made to the source +code. You can run tests with Pytest. + +.. code:: bash + + cd pyadjoint/tests + pytest + + +.. toctree:: + :maxdepth: 1 + :hidden: + :caption: INTRODUCTION + + usage + example_dataset + +.. toctree:: + :maxdepth: 1 + :hidden: + :caption: MISFIT FUNCTIONS + + waveform + convolution + cctm + mtm + exp_phase + +.. toctree:: + :maxdepth: 1 + :hidden: + :caption: DOUBLE DIFFERENCE MISFIT + + dd_waveform + dd_convolution + dd_cctm + dd_mtm + +.. toctree:: + :maxdepth: 1 + :hidden: + :caption: MISCELLANEOUS + + citations diff --git a/docs/mtm.rst b/docs/mtm.rst new file mode 100644 index 0000000..e823891 --- /dev/null +++ b/docs/mtm.rst @@ -0,0 +1,70 @@ +Multitaper Misfit +================= + +.. warning:: + + Please refer to [Laske1996]_ for a more rigorous mathematical + derivation of this misfit function. This documentation page only serves to + summarize the math for the purpose of explaining the underlying code. + +The misfit :math:`\chi_P(\mathbf{m})` measures frequency-dependent phase +differences estimated with multitaper methods. For a given Earth model +:math:`\mathbf{m}`and a single receiver, :math:`\chi_P(\mathbf{m})` is given by + +.. math:: + + \chi_P (\mathbf{m}) = \frac{1}{2} \int_0^W W_P(w) \left| + \frac{ \tau^{\mathbf{d}}(w) - \tau^{\mathbf{s}}(w, \mathbf{m})} + {\sigma_P(w)} \right|^ 2 dw + +:math:`\tau^\mathbf{d}(w)` is the frequency-dependent +phase measurement of the observed data; +:math:`\tau^\mathbf{s}(w, \mathbf{m})` the frequency-dependent +phase measurement of the synthetic data. +The function :math:`W_P(w)` denotes frequency-domain +taper corresponding to the frequency range over which +the measurements are assumed reliable. +:math:`\sigma_P(w)` is associated with the +traveltime uncertainty introduced in making measurements, +which can be estimated with cross-correlation method, +or Jackknife multitaper approach. + +The adjoint source for the same receiver is given by + +.. math:: + + f_P^{\dagger}(t) = \sum_k h_k(t)P_j(t) + +in which :math:`h_k(t)` is one (the :math:`k` th) of multi-tapers. + +.. math:: + + P_j(t) = 2\pi W_p(t) * \Delta \tau(t) * p_j(t) \\ + P_j(w) = 2\pi W_p(w) \Delta \tau(w) * p_j(w) \\ + p_j(w) = \frac{iw s_j}{\sum_k(iw s_k)(iw s_k)^*} \\ + \Delta \tau(w) = \tau^{\mathbf{d}}(w) - \tau^{\mathbf{s}}(w, \mathbf{m}) + + +Usage +````` + +The following code snippets illustrates the basic usage of the multitaper +misfit function. + +.. code:: python + + import pyadjoint + + obs, syn = pyadjoint.get_example_data() + obs = obs.select(component="Z")[0] + syn = syn.select(component="Z")[0] + + config = pyadjoint.get_config(adjsrc_type="multitaper_misfit", min_period=20., + max_period=100., taper_percentage=0.3, + taper_type="cos") + + adj_src = pyadjoint.calculate_adjoint_source(config=config, + observed=obs, synthetic=syn, + windows=[(800., 900.)] + ) + diff --git a/docs/pyatoa_integration.rst b/docs/pyatoa_integration.rst new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/pyatoa_integration.rst @@ -0,0 +1 @@ + diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 0000000..ba12074 --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,141 @@ +Usage +===== + +The first step is to import ObsPy and ``Pyadjoint``. + +.. code:: python + + import obspy + import pyadjoint + +``Pyadjoint`` expects the data to be fully preprocessed thus both +observed and synthetic data are expected to have exactly the same +length, sampling rate, and spectral content. + +``Pyadjoint`` does not care about the actual components in question; it will +use two traces and calculate misfit values and adjoint sources for them. To +provide a familiar nomenclature we will always talk about observed and +synthetic data even though the workflow is independent of what the data +represents. + +Example Data +~~~~~~~~~~~~ + +The package comes with a helper function to get some example data used for +illustrative and debugging purposes. + + +.. code:: python + + obs, syn = pyadjoint.get_example_data() + # Select the vertical components of both. + obs = obs.select(component="Z")[0] + syn = syn.select(component="Z")[0] + +Config +~~~~~~ + +Each misfit function requires a corresponding :class:`~pyadjoint.config.Config` +class to control optional processing parameters. The +:meth:`~pyadjoint.config.get_config` function provides a wrapper for grabbing +the appropriate Config. + +.. code:: python + + config = pyadjoint.get_config(adjsrc_type="waveform_misfit", min_period=20., + max_period=100.) + +A list of available adjoint source types can be found using the +:meth:`~pyadjoint.discover_adjoint_sources` function. + +.. code:: python + + >>> print(pyadjoint.discover_adjoint_sources().keys()) + dict_keys(['cc_traveltime_misfit', 'exponentiated_phase_misfit', 'multitaper_misfit', 'waveform_misfit']) + + +.. note:: + + Some of these functions allow for modifiers such as the use of + double difference measurements. See individual misfit function pages for + options. + +Many types of adjoint sources have additional arguments that can be passed to +it. See the :mod:`~pyadjoint.config` page for available keyword arguments +and descriptions. + +.. code:: python + + config = pyadjoint.get_config(adjsrc_type="waveform_misfit", min_period=20., + max_period=100., taper_percentage=0.3, + taper_type="cos") + +Calculate Adjoint Source +~~~~~~~~~~~~~~~~~~~~~~~~ + +Essentially all of ``Pyadjoint``'s functionality is accessed through its +central :func:`~pyadjoint.main.calculate_adjoint_source` function. + + +.. code:: python + + adj_src = pyadjoint.calculate_adjoint_source( + config=config, + # Pass observed and synthetic data traces. + observed=obs, synthetic=syn, + # List of window borders in seconds since the first sample. + windows=[(800., 900.)] + ) + +The function returns an :class:`~pyadjoint.adjoint_source.AdjointSource` object. + +.. code:: + + >>> print(adj_src) + 'waveform_misfit' Adjoint Source for channel MXZ at station SY.DBO + misfit: 4.263e-11 + adjoint_source: available with 3600 samples + windows: generated with 1 windows + # Access misfit and adjoint sources. The misfit is a floating point number. + >>> print(adj_src.misfit) + 4.263067857359352e-11 + # The adjoint source is a a numpy array. + >>> print(adj_src.adjoint_source) + [0. 0. 0. ... 0. 0. 0.] + # Time windows used to generate the array are stored + >>> print(adj_src.windows) + [(800.0, 900.0)] + # Misfit stats for each window are also stored + >>> print(adj_src.window_stats) + [{'type': 'waveform', 'left': 800.0, 'right': 901.0, 'misfit': 4.263067857359352e-11, 'difference': 1.519230269510467e-08}] + +Plotting Adjoint Sources +~~~~~~~~~~~~~~~~~~~~~~~~ + +All adjoint source types can also be plotted during the calculation. The +type of plot produced depends on the type of misfit measurement and +adjoint source. + +.. code:: python + + pyadjoint.calculate_adjoint_source(config=config, observed=obs, + synthetic=syn, plot=True, + plot_filename="./waveform_adjsrc.png"); + + +Saving to Disk +~~~~~~~~~~~~~~ + +One of course wants to serialize the calculated adjoint sources to disc at one +point in time. You need to pass the filename and the desired format as well as +some format specific parameters to the +:meth:`~pyadjoint.adjoint_source.AdjointSource.write` method of the +:class:`~pyadjoint.adjoint_source.AdjointSource` object. Instead of a filename +you can also pass an open file or a file-like object. Please refer to its +documentation for more details. + + +.. code:: python + + adj_src.write(filename="NET.STA.CHA.adj_src", + format="SPECFEM", time_offset=-10) diff --git a/docs/waveform.rst b/docs/waveform.rst new file mode 100644 index 0000000..13e0789 --- /dev/null +++ b/docs/waveform.rst @@ -0,0 +1,65 @@ +Waveform Misfit +=============== + +.. warning:: + + Please refer to the original paper [Tromp2005]_ for rigorous mathematical + derivations of this misfit function. This documentation page only serves to + summarize their results for the purpose of explaining the underlying code. + +This is the simplest of all misfits and is defined as the squared difference +between observed and synthetic data. + +The misfit :math:`\chi(\mathbf{m})` for a given Earth model :math:`\mathbf{m}` +and a single receiver and component is given by + +.. math:: + + \chi (\mathbf{m}) = \frac{1}{2} \int_0^T \left| \mathbf{d}(t) - + \mathbf{s}(t, \mathbf{m}) \right| ^ 2 dt + +:math:`\mathbf{d}(t)` is the observed data and +:math:`\mathbf{s}(t, \mathbf{m})` the synthetic data. + +The adjoint source for the same receiver and component is given by + +.. math:: + + f^{\dagger}(t) = - \left[ \mathbf{d}(T - t) - + \mathbf{s}(T - t, \mathbf{m}) \right] + +.. note:: + + For the sake of simplicity we omit the spatial Kronecker delta and define + the adjoint source as acting solely at the receiver's location. For more + details, please see [Tromp2005]_ and [Bozdag2011]_. + +.. note:: + + This particular implementation here uses + `Simpson's rule `_ + to evaluate the definite integral. + +Usage +````` + +The following code snippets illustrates the basic usage of the waveform +misfit function. + +.. code:: python + + import pyadjoint + + obs, syn = pyadjoint.get_example_data() + obs = obs.select(component="Z")[0] + syn = syn.select(component="Z")[0] + + config = pyadjoint.get_config(adjsrc_type="waveform_misfit", min_period=20., + max_period=100., taper_percentage=0.3, + taper_type="cos") + + adj_src = pyadjoint.calculate_adjoint_source(config=config, choice="waveform", + observed=obs, synthetic=syn, + windows=[(800., 900.)] + ) + diff --git a/pyadjoint/__init__.py b/pyadjoint/__init__.py new file mode 100644 index 0000000..ffa4211 --- /dev/null +++ b/pyadjoint/__init__.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +""" +:copyright: + adjTomo Dev Team (adjtomo@gmail.com), 2022 + Lion Krischer (krischer@geophysik.uni-muenchen.de), 2015 +:license: + BSD 3-Clause ("BSD New" or "BSD Simplified") +""" +import inspect +import logging +import os +import pkgutil + +__version__ = "0.1.0" + + +class PyadjointError(Exception): + """ + Base class for all Pyadjoint exceptions. Will probably be used for all + exceptions to not overcomplicate things as the whole package is pretty + small. + """ + pass + + +class PyadjointWarning(UserWarning): + """ + Base class for all Pyadjoint warnings. + """ + pass + + +def discover_adjoint_sources(): + """ + Discovers the available adjoint sources in the package. This should work + regardless of whether Pyadjoint is checked out from git, packaged as .egg + etc. + """ + from pyadjoint import adjoint_source_types + + adjoint_sources = {} + + fct_name = "calculate_adjoint_source" + + path = os.path.join( + os.path.dirname(inspect.getfile(inspect.currentframe())), + "adjoint_source_types") + for importer, modname, _ in pkgutil.iter_modules( + [path], prefix=adjoint_source_types.__name__ + "."): + m = importer.find_module(modname).load_module(modname) + if not hasattr(m, fct_name): + continue + fct = getattr(m, fct_name) + if not callable(fct): + continue + + # Create a dictionary of functions related to the adjsrc name + name = m.__name__.split(".")[-1] + adjoint_sources[name] = fct + + return adjoint_sources + + +# setup the logger +logger = logging.getLogger(__name__) +logger.setLevel(logging.WARNING) +# Prevent propagating to higher loggers. +logger.propagate = 0 +# Console log handler. +ch = logging.StreamHandler() +# Add formatter +FORMAT = "[%(asctime)s] - %(name)s - %(levelname)s: %(message)s" +formatter = logging.Formatter(FORMAT) +ch.setFormatter(formatter) +ch.setLevel(logging.DEBUG) +logger.addHandler(ch) + +# Main objects and functions available at the top level. +from .adjoint_source import AdjointSource # NOQA +from .main import (calculate_adjoint_source, get_example_data, + discover_adjoint_sources, plot_adjoint_source) # NOQA +from .config import get_config # NOQA diff --git a/pyadjoint/adjoint_source.py b/pyadjoint/adjoint_source.py new file mode 100644 index 0000000..71bd054 --- /dev/null +++ b/pyadjoint/adjoint_source.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +""" +Central interfaces for ``Pyadjoint``, misfit measurement package. + +:copyright: + adjTomo Dev Team (adjtomo@gmail.com), 2022 + Lion Krischer (krischer@geophysik.uni-muenchen.de), 2015 +:license: + BSD 3-Clause ("BSD New" or "BSD Simplified") +""" +import numpy as np +import obspy + +from pyadjoint import discover_adjoint_sources + + +class AdjointSource: + """ + Adjoint Source class to hold calculated adjoint sources + """ + def __init__(self, adjsrc_type, misfit, dt, min_period, max_period, + component, adjoint_source=None, windows=None, + network=None, station=None, location=None, starttime=None, + window_stats=None): + """ + Class representing an already calculated adjoint source. + + :param adjsrc_type: The type of adjoint source. + :type adjsrc_type: str + :param misfit: The misfit value. + :type misfit: float + :param dt: The sampling rate of the adjoint source. + :type dt: float + :param min_period: The minimum period of the spectral content + of the data. + :type min_period: float + :param max_period: The maximum period of the spectral content + of the data. + :type max_period: float + :param component: The adjoint source component, usually ``"Z"``, + ``"N"``, ``"E"``, ``"R"``, or ``"T"``. + :type component: str + :param adjoint_source: The actual adjoint source. + :type adjoint_source: :class:`numpy.ndarray` + :type windows: list of dict + :param windows: measurement windows used to generate the adjoint + source, each carrying information about the misfit of the window + :param network: The network code of the station. + :type network: str + :param station: The station code of the station. + :type station: str + :param location: The location code of the station. + :type location: str + :param starttime: starttime of adjoint source + :type starttime: obspy.UTCDateTime + """ + adj_srcs = discover_adjoint_sources() + if adjsrc_type not in adj_srcs.keys(): + raise ValueError(f"Unknown adjoint source type {adjsrc_type}") + + self.adjsrc_type = adjsrc_type + self.adj_src_name = adjsrc_type + self.misfit = misfit + self.dt = dt + self.min_period = min_period + self.max_period = max_period + self.component = component + self.network = network + self.station = station + self.location = location + self.starttime = starttime + self.adjoint_source = adjoint_source + self.windows = windows + self.window_stats = window_stats + + def __str__(self): + if self.network and self.station: + station = f" at station {self.network}.{self.station}" + else: + station = "" + + if self.adjoint_source is not None: + adj_src_status = \ + f"available with {len(self.adjoint_source)} samples" + else: + adj_src_status = "has not been calculated" + + if self.windows is not None: + windows_status = f"generated with {len(self.windows)} windows" + else: + windows_status = "has no windows" + + return (f"'{self.adj_src_name}' Adjoint Source for " + f"channel {self.component}{station}\n" + f"\tmisfit: {self.misfit:.4g}\n" + f"\tadjoint_source: {adj_src_status}\n" + f"\twindows: {windows_status}" + ) + + def write(self, filename, format, **kwargs): + """ + Write the adjoint source to a file. + + :param filename: Determines where the adjoint source is saved. + :type filename: str, open file, or file-like object + :param format: The format of the adjoint source. Currently available + are: ``"SPECFEM"`` + :type format: str + + .. rubric:: SPECFEM + + SPECFEM requires one additional parameter: the temporal offset of the + first sample in seconds. The following example sets the time of the + first sample in the adjoint source to ``-10``. + + >>> adj_src.write("NET.STA.CHAN.adj", format="SPECFEM", + ... time_offset=-10) # doctest: +SKIP + + .. rubric ASDF + + Adjoint sources can be written directly to an ASDFDataSet if provided. + Optional ``coordinates`` parameter specifies the location of the + station that generated the adjoint source + + >>> adj_src.write(ds, format="ASDF", time_offset=-10, + ... coordinates={'latitude':19.2, + ... 'longitude':13.4, + ... 'elevation_in_m':2.0}) + """ + if self.adjoint_source is None: + raise ValueError("Can only write adjoint sources if the adjoint " + "source has been calculated.") + + format = format.upper() + available_formats = ["SPECFEM", "ASDF"] + if format not in available_formats: + raise ValueError("format '%s' not known. Available formats: %s" % + (format, ", ".join(available_formats))) + + if format == "SPECFEM": + self._write_specfem(filename, **kwargs) + elif format == "ASDF": + self._write + + def _write_specfem(self, filename, time_offset): + """ + Write the adjoint source for SPECFEM. + + :param filename: name of file to write adjoint source to + :type filename: str + :param time_offset: time offset of the first time point in array + :type time_offset: float + """ + l = len(self.adjoint_source) + + to_write = np.empty((l, 2)) + + to_write[:, 0] = np.linspace(0, (l - 1) * self.dt, l) + to_write[:, 0] += time_offset + # SPECFEM expects non-time reversed adjoint sources. + to_write[:, 1] = self.adjoint_source[::-1] + + np.savetxt(filename, to_write) + + def _write_asdf(self, ds, time_offset, coordinates=None, **kwargs): + """ + Writes the adjoint source to an ASDF file. + + :param ds: The ASDF data structure read in using pyasdf. + :type ds: str + :param time_offset: The temporal offset of the first sample in seconds. + This is required if using the adjoint source as input to SPECFEM. + :type time_offset: float + :param coordinates: If given, the coordinates of the adjoint source. + The 'latitude', 'longitude', and 'elevation_in_m' of the adjoint + source must be defined. + :type coordinates: list + """ + # Import here to not have a global dependency on pyasdf + from pyasdf.exceptions import NoStationXMLForStation + + # Convert the adjoint source to SPECFEM format + l = len(self.adjoint_source) + specfem_adj_source = np.empty((l, 2)) + specfem_adj_source[:, 0] = np.linspace(0, (l - 1) * self.dt, l) + specfem_adj_source[:, 0] += time_offset + specfem_adj_source[:, 1] = self.adjoint_source[::-1] + + tag = "%s_%s_%s" % (self.network, self.station, self.component) + min_period = self.min_period + max_period = self.max_period + component = self.component + station_id = "%s.%s" % (self.network, self.station) + + if coordinates: + # If given, all three coordinates must be present + if {"latitude", "longitude", "elevation_in_m"}.difference( + set(coordinates.keys())): + raise ValueError( + "'latitude', 'longitude', and 'elevation_in_m'" + " must be given") + else: + try: + coordinates = ds.waveforms[ + "%s.%s" % (self.network, self.station)].coordinates + except NoStationXMLForStation: + raise ValueError("Coordinates must either be given " + "directly or already be part of the " + "ASDF file") + + # Safeguard against funny types in the coordinates dictionary + latitude = float(coordinates["latitude"]) + longitude = float(coordinates["longitude"]) + elevation_in_m = float(coordinates["elevation_in_m"]) + + parameters = {"dt": self.dt, "misfit_value": self.misfit, + "adjoint_source_type": self.adjsrc_type, + "min_period": min_period, "max_period": max_period, + "latitude": latitude, "longitude": longitude, + "elevation_in_m": elevation_in_m, + "station_id": station_id, "component": component, + "units": "m"} + + # Use pyasdf to add auxiliary data to the ASDF file + ds.add_auxiliary_data(data=specfem_adj_source, + data_type="AdjointSource", path=tag, + parameters=parameters) + diff --git a/src/pyadjoint/adjoint_source_types/__init__.py b/pyadjoint/adjoint_source_types/__init__.py similarity index 100% rename from src/pyadjoint/adjoint_source_types/__init__.py rename to pyadjoint/adjoint_source_types/__init__.py diff --git a/pyadjoint/adjoint_source_types/cc_traveltime_misfit.py b/pyadjoint/adjoint_source_types/cc_traveltime_misfit.py new file mode 100644 index 0000000..33b9f41 --- /dev/null +++ b/pyadjoint/adjoint_source_types/cc_traveltime_misfit.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +""" +Cross correlation traveltime misfit and associated adjoint source. + +:authors: + adjtomo Dev Team (adjtomo@gmail.com), 2023 + Yanhua O. Yuan (yanhuay@princeton.edu), 2017 + Youyi Ruan (youyir@princeton.edu) 2016 + Lion Krischer (krischer@geophysik.uni-muenchen.de), 2015 +:license: + BSD 3-Clause ("BSD New" or "BSD Simplified") +""" +import numpy as np + +from pyadjoint import logger +from pyadjoint.utils.signal import get_window_info, window_taper +from pyadjoint.utils.cctm import (calculate_cc_shift, calculate_cc_adjsrc, + calculate_dd_cc_shift, calculate_dd_cc_adjsrc) + + +def calculate_adjoint_source(observed, synthetic, config, windows, + choice=None, observed_2=None, + synthetic_2=None, windows_2=None): + """ + Calculate adjoint source for the cross-correlation traveltime misfit + measurement + + :type observed: obspy.core.trace.Trace + :param observed: observed waveform to calculate adjoint source + :type synthetic: obspy.core.trace.Trace + :param synthetic: synthetic waveform to calculate adjoint source + :type config: pyadjoint.config.ConfigCCTraveltime + :param config: Config class with parameters to control processing + :type windows: list of tuples + :param windows: [(left, right),...] representing left and right window + borders to be used to calculate misfit and adjoint sources + :type choice: str + :param choice: Flag to turn on station pair calculations. Requires + `observed_2`, `synthetic_2`, `windows_2`. Available: + - 'double_difference': Double difference waveform misfit from + Yuan et al. 2016 + - 'convolved': Waveform convolution misfit from Choi & Alkhalifah (2011) + :type observed_2: obspy.core.trace.Trace + :param observed_2: second observed waveform to calculate adjoint sources + from station pairs + :type synthetic_2: obspy.core.trace.Trace + :param synthetic_2: second synthetic waveform to calculate adjoint sources + from station pairs + :type windows_2: list of tuples + :param windows_2: [(left, right),...] representing left and right window + borders to be tapered in units of seconds since first sample in data + array. Used to window `observed_2` and `synthetic_2` + """ + assert(config.__class__.__name__ == "ConfigCCTraveltime"), \ + "Incorrect configuration class passed to CCTraveltime misfit" + + if choice is not None: + assert choice in ["double_difference"], \ + f"if `choice` is set, must be `double_difference` or `convolved`" + logger.info(f"performing waveform caluclation with choice: `{choice}`") + + # Allow for measurement types related to `dt` (p) and `dlna` (q) + ret_val_p = {} + ret_val_q = {} + + # List of windows and some measurement values for each + win_stats = [] + + # Initiate constants and empty return values to fill + nlen_data = len(synthetic.data) + dt = synthetic.stats.delta + + # Initiate empty arrays for memory efficiency + fp = np.zeros(nlen_data) + fq = np.zeros(nlen_data) + if choice == "double_difference": + # Initiate empty arrays for memory efficiency + fp_2 = np.zeros(nlen_data) + fq_2 = np.zeros(nlen_data) + + misfit_sum_p = 0.0 + misfit_sum_q = 0.0 + + # Loop over time windows and calculate misfit for each window range + for i, window in enumerate(windows): + # Convenience variables, quick access to information about time series + dt = synthetic.stats.delta + left_sample, right_sample, nlen_w = get_window_info(window, dt) + + # Pre-allocate arrays for memory efficiency + d = np.zeros(nlen_w) + s = np.zeros(nlen_w) + + # d and s represent the windowed data and synthetic arrays, respectively + d[0: nlen_w] = observed.data[left_sample: right_sample] + s[0: nlen_w] = synthetic.data[left_sample: right_sample] + + # Taper windowed signals in place + window_taper(d, taper_percentage=config.taper_percentage, + taper_type=config.taper_type) + window_taper(s, taper_percentage=config.taper_percentage, + taper_type=config.taper_type) + + if choice == "double_difference": + # Prepare second set of waveforms the same as the first + dt_2 = synthetic.stats.delta + window_2 = windows_2[i] + left_sample_2, right_sample_2, nlen_w_2 = \ + get_window_info(window_2, dt_2) + + # Pre-allocate arrays for memory efficiency + d_2 = np.zeros(nlen_w) + s_2 = np.zeros(nlen_w) + + # d and s represent the windowed data and synthetic arrays + d_2[0: nlen_w_2] = observed_2.data[left_sample_2: right_sample_2] + s_2[0: nlen_w_2] = synthetic_2.data[left_sample_2: right_sample_2] + + # Taper windowed signals in place + window_taper(d_2, taper_percentage=config.taper_percentage, + taper_type=config.taper_type) + window_taper(s_2, taper_percentage=config.taper_percentage, + taper_type=config.taper_type) + + # Calculate double difference time shift + tshift, _, _, dlna, _, sigma_dt, sigma_dlna = \ + calculate_dd_cc_shift(d=d, s=s, d_2=d_2, s_2=s_2, dt=dt, + **vars(config) + ) + # Calculate misfit and adjoint source for the given window + # TODO: Add in dlna misfit and adjoint source in below function + misfit_p, misfit_q, fp_win, fp_win_2, fq_win, fq_win_2 = \ + calculate_dd_cc_adjsrc(s=s, s_2=s_2, tshift=tshift, + dlna=dlna, dt=dt, sigma_dt=sigma_dt, + sigma_dlna=sigma_dlna, **vars(config) + ) + else: + # Calculate cross correlation time shift, amplitude anomaly and + # errors. Config passed as **kwargs to control constants required + # by function + tshift, dlna, sigma_dt, sigma_dlna = calculate_cc_shift( + d=d, s=s, dt=dt, **vars(config) + ) + + # Calculate misfit and adjoint source for the given window + misfit_p, misfit_q, fp_win, fq_win = \ + calculate_cc_adjsrc(s=s, tshift=tshift, dlna=dlna, dt=dt, + sigma_dt=sigma_dt, sigma_dlna=sigma_dlna, + **vars(config) + ) + + # Sum misfit into the overall waveform misfit + misfit_sum_p += misfit_p + misfit_sum_q += misfit_q + + # Add windowed adjoint source to the full adjoint source waveform + window_taper(fp_win, taper_percentage=config.taper_percentage, + taper_type=config.taper_type) + window_taper(fq_win, taper_percentage=config.taper_percentage, + taper_type=config.taper_type) + fp[left_sample:right_sample] = fp_win[:] + fq[left_sample:right_sample] = fq_win[:] + + if choice == "double_difference": + window_taper(fp_win_2, taper_percentage=config.taper_percentage, + taper_type=config.taper_type) + window_taper(fq_win_2, taper_percentage=config.taper_percentage, + taper_type=config.taper_type) + fp_2[left_sample_2:right_sample_2] = fp_win_2[:] + fq_2[left_sample_2:right_sample_2] = fq_win_2[:] + + # Store some information for each window + win_stats.append( + {"left": left_sample * dt, "right": right_sample * dt, + "measurement_type": config.measure_type, "tshift": tshift, + "misfit_dt": misfit_p, "sigma_dt": sigma_dt, "dlna": dlna, + "misfit_dlna": misfit_q, "sigma_dlna": sigma_dlna, + } + ) + + # Keep track of both misfit values but only returning one of them + ret_val_p["misfit"] = misfit_sum_p + ret_val_q["misfit"] = misfit_sum_q + + # Time reverse adjoint sources w.r.t synthetic waveforms + ret_val_p["adjoint_source"] = fp[::-1] + ret_val_q["adjoint_source"] = fq[::-1] + if choice == "double_difference": + ret_val_p["adjoint_source_2"] = fp_2[::-1] + ret_val_q["adjoint_source_2"] = fq_2[::-1] + + if config.measure_type == "dt": + ret_val = ret_val_p + elif config.measure_type == "am": + ret_val = ret_val_q + + ret_val["window_stats"] = win_stats + + return ret_val diff --git a/pyadjoint/adjoint_source_types/exponentiated_phase_misfit.py b/pyadjoint/adjoint_source_types/exponentiated_phase_misfit.py new file mode 100644 index 0000000..fe3ce20 --- /dev/null +++ b/pyadjoint/adjoint_source_types/exponentiated_phase_misfit.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +""" +Exponentiated phase misfit function and adjoint source. + +TODO + - write description + - add citation for bozdag paper + - write additional parameters + +:authors: + adjtomo Dev Team (adjtomo@gmail.com), 2022 + Yanhua O. Yuan (yanhuay@princeton.edu), 2016 +:license: + BSD 3-Clause ("BSD New" or "BSD Simplified") +""" +import numpy as np + +from scipy import signal +from scipy.integrate import simps + +from pyadjoint.utils.signal import get_window_info, window_taper + +VERBOSE_NAME = "Exponentiated Phase Misfit" + +DESCRIPTION = r""" +""" + +ADDITIONAL_PARAMETERS = r""" +""" + + +def calculate_adjoint_source(observed, synthetic, config, windows, + adjoint_src=True, window_stats=True, **kwargs): + """ + Calculate adjoint source for the exponentiated phase misfit. + + :type observed: obspy.core.trace.Trace + :param observed: observed waveform to calculate adjoint source + :type synthetic: obspy.core.trace.Trace + :param synthetic: synthetic waveform to calculate adjoint source + :type config: pyadjoint.config.ConfigCCTraveltime + :param config: Config class with parameters to control processing + :type windows: list of tuples + :param windows: [(left, right),...] representing left and right window + borders to be used to calculate misfit and adjoint sources + :type adjoint_src: bool + :param adjoint_src: flag to calculate adjoint source, if False, will only + calculate misfit + :type window_stats: bool + :param window_stats: flag to return stats for individual misfit windows used + to generate the adjoint source + """ + assert(config.__class__.__name__ == "ConfigExponentiatedPhase"), \ + "Incorrect configuration class passed to Exponentiated Phase misfit" + + if kwargs.get("double_difference", False) is True: + raise NotImplementedError( + "Exponentiated phase misfit does not have double difference " + "capabilities" + ) + + # Dictionary of return values related to exponentiated phase + ret_val = {} + + # List of windows and some measurement values for each + win_stats = [] + + # Initiate constants and empty return values to fill + nlen_w_data = len(synthetic.data) + dt = synthetic.stats.delta + + f = np.zeros(nlen_w_data) # adjoint source + + misfit_sum = 0.0 + + # loop over time windows + for window in windows: + left_sample, right_sample, nlen_w = get_window_info(window, dt) + + # Initiate empty window arrays for memory efficiency + d = np.zeros(nlen_w) + s = np.zeros(nlen_w) + + d[0: nlen_w] = observed.data[left_sample: right_sample] + s[0: nlen_w] = synthetic.data[left_sample: right_sample] + + # Taper window to get rid of kinks at two ends + window_taper(d, taper_percentage=config.taper_percentage, + taper_type=config.taper_type) + window_taper(s, taper_percentage=config.taper_percentage, + taper_type=config.taper_type) + + # Calculate envelope and hilbert transform of data, synthetics + env_s = abs(signal.hilbert(s)) + env_d = abs(signal.hilbert(d)) + hilbt_s = np.imag(signal.hilbert(s)) + hilbt_d = np.imag(signal.hilbert(d)) + + # Determine water level threshold + thresh_s = config.wtr_env * env_s.max() + thresh_d = config.wtr_env * env_d.max() + env_s_wtr = env_s + thresh_s + env_d_wtr = env_d + thresh_d + + # Calculate differences between data and synthetic acct for waterlevel + diff_real = d/env_d_wtr - s/env_s_wtr + diff_imag = hilbt_d/env_d_wtr - hilbt_s/env_s_wtr + + # Integrate with the composite Simpson's rule. + misfit_real = 0.5 * simps(y=diff_real**2, dx=dt) + misfit_imag = 0.5 * simps(y=diff_imag**2, dx=dt) + + misfit_sum += misfit_real + misfit_imag + + env_s_wtr_cubic = env_s_wtr**3 + + adj_real = ( + -1 * (diff_real * hilbt_s ** 2 / env_s_wtr_cubic) - + np.imag(signal.hilbert(diff_real * s * hilbt_s / env_s_wtr_cubic)) + ) + adj_imag = ( + diff_imag * s * hilbt_s / env_s_wtr_cubic + + np.imag(signal.hilbert(diff_imag * s**2 / env_s_wtr_cubic)) + ) + + # Re-taper newly generated adjoint source + window_taper(adj_real, taper_percentage=config.taper_percentage, + taper_type=config.taper_type) + window_taper(adj_imag, taper_percentage=config.taper_percentage, + taper_type=config.taper_type) + + f[left_sample:right_sample] = adj_real[0:nlen_w] + adj_imag[0:nlen_w] + + win_stats.append( + {"left": left_sample * dt, "right": right_sample * dt, + "type": "exponentiated_phase", "measurement_type": "dt", + "diff_real": np.mean(diff_real[0:nlen_w]), + "diff_imag": np.mean(diff_imag[0:nlen_w]), + "misfit_real": misfit_real, "misfit_imag": misfit_imag + } + ) + + # Place return values in output dictionary + ret_val["misfit"] = misfit_sum + + if window_stats: + ret_val["window_stats"] = win_stats + + # Time reverse adjoint sources w.r.t synthetic waveforms + if adjoint_src is True: + ret_val["adjoint_source"] = f[::-1] + + return ret_val diff --git a/pyadjoint/adjoint_source_types/multitaper_misfit.py b/pyadjoint/adjoint_source_types/multitaper_misfit.py new file mode 100644 index 0000000..28e62ca --- /dev/null +++ b/pyadjoint/adjoint_source_types/multitaper_misfit.py @@ -0,0 +1,1396 @@ +#!/usr/bin/env python3 +""" +Multitaper based phase and amplitude misfit and adjoint source. + +:authors: + adjTomo Dev Team (adjtomo@gmail.com), 2022 + Youyi Ruan (youyir@princeton.edu), 2016 + Matthieu Lefebvre (ml15@princeton.edu), 2016 + Yanhua O. Yuan (yanhuay@princeton.edu), 2015 +:license: + BSD 3-Clause ("BSD New" or "BSD Simplified") +""" +import numpy as np +from scipy.integrate import simps + +from pyadjoint import logger +from pyadjoint.utils.dpss import dpss_windows +from pyadjoint.utils.cctm import (calculate_cc_shift, calculate_cc_adjsrc, + calculate_dd_cc_shift, calculate_dd_cc_adjsrc, + ) +from pyadjoint.utils.signal import (window_taper, get_window_info, + process_cycle_skipping) + + +class MultitaperMisfit: + """ + A class to house the machinery of the multitaper misfit calculation. This is + done with a class rather than a function to reduce the amount of unnecessary + parameter passing between functions. + """ + def __init__(self, observed, synthetic, config, windows, + observed_2=None, synthetic_2=None, windows_2=None + ): + """ + Initialize Multitaper Misfit adjoint source creator + + :type observed: obspy.core.trace.Trace + :param observed: observed waveform to calculate adjoint source + :type synthetic: obspy.core.trace.Trace + :param synthetic: synthetic waveform to calculate adjoint source + :type config: pyadjoint.config.Multitaper + :param config: Config class with parameters to control processing + :type windows: list of tuples + :param windows: [(left, right),...] representing left and right window + borders to be used to calculate misfit and adjoint sources + :type observed_2: obspy.core.trace.Trace + :param observed_2: second observed waveform to calculate adjoint sources + from station pairs + :type synthetic_2: obspy.core.trace.Trace + :param synthetic_2: second synthetic waveform to calculate adjoint sources + from station pairs + :type windows_2: list of tuples + :param windows_2: [(left, right),...] representing left and right window + borders to be tapered in units of seconds since first sample in data + array. Used to window `observed_2` and `synthetic_2` + """ + assert (config.__class__.__name__ == "ConfigMultitaper"), \ + "Incorrect configuration class passed to CCTraveltime misfit" + + self.observed = observed + self.synthetic = synthetic + self.config = config + self.windows = windows + + # For optional double-difference measurements + self.observed_2 = observed_2 + self.synthetic_2 = synthetic_2 + self.windows_2 = windows_2 + + # Calculate some information to be used for MTM measurements + # Assumed that second set of waveforms (if provided) have same qualities + self.nlen_f = 2 ** self.config.lnpt + self.nlen_data = len(synthetic.data) # length in samples + self.dt = synthetic.stats.delta # sampling rate + self.tlen_data = self.nlen_data * self.dt # length in time [s] + + def calculate_adjoint_source(self): + """ + Main processing function to calculate adjoint source for MTM. + The logic here is that the function will perform a series of checks/ + calculations to check if MTM is valid for each given window. If + any check/calculation fails, it will fall back to CCTM misfit for the + given window. + """ + # Arrays for adjoint sources w.r.t time shift (p) and amplitude (q) + fp = np.zeros(self.nlen_data) + fq = np.zeros(self.nlen_data) + + misfit_sum_p = 0.0 + misfit_sum_q = 0.0 + win_stats = [] + + # Loop over time windows and calculate misfit for each window range + for window in self.windows: + # `is_mtm` determines whether we use MTM (T) or CC (F) for misfit + is_mtm = True + + left_sample, right_sample, nlen_w = get_window_info(window, self.dt) + fp_t = np.zeros(nlen_w) + fq_t = np.zeros(nlen_w) + misfit_p = 0. + misfit_q = 0. + + # Pre-allocate arrays for memory efficiency + d = np.zeros(nlen_w) + s = np.zeros(nlen_w) + + # d and s represent the windowed data and synthetic arrays + d[0: nlen_w] = self.observed.data[left_sample: right_sample] + s[0: nlen_w] = self.synthetic.data[left_sample: right_sample] + + # Taper windowed signals in place + window_taper(d, taper_percentage=self.config.taper_percentage, + taper_type=self.config.taper_type) + window_taper(s, taper_percentage=self.config.taper_percentage, + taper_type=self.config.taper_type) + + # Calculate cross correlation time shift, amplitude anomaly and + # errors. Config passed as **kwargs to control constants required + # by function + cc_tshift, cc_dlna, sigma_dt_cc, sigma_dlna_cc = calculate_cc_shift( + d=d, s=s, dt=self.dt, **vars(self.config) + ) + + # Perform a series of checks to see if MTM is valid for the data + # This will only loop once, but allows us to break if a check fail + while is_mtm is True: + is_mtm = self.check_time_series_acceptability( + cc_tshift=cc_tshift, nlen_w=nlen_w) + if is_mtm is False: + break + + # Shift and scale observed data 'd' to match synthetics, make + # sure the time shift doesn't go passed time series' bounds + d, is_mtm = self.prepare_data_for_mtm(d=d, tshift=cc_tshift, + dlna=cc_dlna, + window=window) + if is_mtm is False: + logger.info(f"reject MTM: adjusted CC shift: {cc_tshift} is" + f"out of bounds of time series") + logger.debug(f"win = [{left_sample * self.dt}, " + f"{right_sample * self.dt}]") + break + + # Determine FFT information related to frequency bands + # TODO: Sampling rate was set to observed delta, is dt the same? + freq = np.fft.fftfreq(n=self.nlen_f, d=self.dt) + df = freq[1] - freq[0] # delta_f: frequency step + wvec = freq * 2 * np.pi # omega vector: angular frequency + # dw = wvec[1] - wvec[0] # TODO: check to see if dw is not used + logger.debug("delta_f (frequency sampling) = {df}") + + # Check for sufficient frequency range given taper bandwith + nfreq_min, nfreq_max, is_mtm = self.calculate_freq_limits(df) + if is_mtm is False: + logger.info("reject MTM: frequency range narrower than " + "half taper bandwith") + break + + # Determine taper bandwith in frequency domain + tapert, eigens = dpss_windows( + n=nlen_w, half_nbw=self.config.mt_nw, + k_max=self.config.num_taper, low_bias=False + ) + is_mtm = np.isfinite(eigens).all() + if is_mtm is False: + logger.warning("reject MTM: error constructing DPSS tapers") + logger.debug(f"eigenvalues: {eigens}") + break + + # Check if tapers are properly generated. In rare cases + # (e.g., [nw=2.5, nlen=61] or [nw=4.0, nlen=15]) certain + # eigenvalues can not be found and associated eigentaper is NaN + tapers = tapert.T * np.sqrt(nlen_w) + phi_mtm, abs_mtm, dtau_mtm, dlna_mtm = \ + self.calculate_multitaper( + d=d, s=s, tapers=tapers, wvec=wvec, nfreq_min=nfreq_min, + nfreq_max=nfreq_max, cc_tshift=cc_tshift, + cc_dlna=cc_dlna + ) + + # Calculate multi-taper error estimation if requested + if self.config.use_mt_error: + sigma_phi_mt, sigma_abs_mt, sigma_dtau_mt, \ + sigma_dlna_mt = self.calculate_mt_error( + d=d, s=s, tapers=tapers, wvec=wvec, + nfreq_min=nfreq_min, nfreq_max=nfreq_max, + cc_tshift=cc_tshift, cc_dlna=cc_dlna, + phi_mtm=phi_mtm, abs_mtm=abs_mtm, + dtau_mtm=dtau_mtm, dlna_mtm=dlna_mtm) + else: + sigma_dtau_mt = np.zeros(self.nlen_f) + sigma_dlna_mt = np.zeros(self.nlen_f) + + # Check if the multitaper measurements fail selection criteria + is_mtm = self.check_mtm_time_shift_acceptability( + nfreq_min=nfreq_min, nfreq_max=nfreq_max, df=df, + cc_tshift=cc_tshift, dtau_mtm=dtau_mtm, + sigma_dtau_mt=sigma_dtau_mt) + if is_mtm is False: + break + + # We made it! If the loop is still running after this point, + # then we will use MTM for adjoint source calculation + + # Frequency domain taper weighted by measurement error + wp_w, wq_w = self.calculate_freq_domain_taper( + nfreq_min=nfreq_min, nfreq_max=nfreq_max, df=df, + dtau_mtm=dtau_mtm, dlna_mtm=dlna_mtm, err_dt_cc=sigma_dt_cc, + err_dlna_cc=sigma_dlna_cc, err_dtau_mt=sigma_dtau_mt, + err_dlna_mt=sigma_dlna_mt, + ) + + # Misfit is defined as the error-weighted measurements + dtau_mtm_weigh_sqr = dtau_mtm ** 2 * wp_w + dlna_mtm_weigh_sqr = dlna_mtm ** 2 * wq_w + misfit_p = 0.5 * 2.0 * simps(y=dtau_mtm_weigh_sqr, dx=df) + misfit_q = 0.5 * 2.0 * simps(y=dlna_mtm_weigh_sqr, dx=df) + + logger.info("calculating misfit and adjoint source with MTM") + fp_t, fq_t = self.calculate_mt_adjsrc( + s=s, tapers=tapers, nfreq_min=nfreq_min, + nfreq_max=nfreq_max, dtau_mtm=dtau_mtm, dlna_mtm=dlna_mtm, + wp_w=wp_w, wq_w=wq_w + ) + win_stats.append( + {"left": left_sample * self.dt, + "right": right_sample * self.dt, + "type": "multitaper", + "measurement_type": self.config.measure_type, + "misfit_dt": misfit_p, + "misfit_dlna": misfit_q, + "sigma_dt": sigma_dt_cc, + "sigma_dlna": sigma_dlna_cc, + "tshift": np.mean(dtau_mtm[nfreq_min:nfreq_max]), + "dlna": np.mean(dlna_mtm[nfreq_min:nfreq_max]), + } + ) + break + # If at some point MTM broke out of the loop, this code block will + # execute and calculate a CC adjoint source and misfit instead + if is_mtm is False: + logger.info("calculating misfit and adjoint source with CCTM") + misfit_p, misfit_q, fp_t, fq_t = \ + calculate_cc_adjsrc(s=s, tshift=cc_tshift, dlna=cc_dlna, + dt=self.dt, sigma_dt=sigma_dt_cc, + sigma_dlna=sigma_dlna_cc) + win_stats.append( + {"left": left_sample * self.dt, + "right": right_sample * self.dt, + "type": "cross_correlation_traveltime", + "measurement_type": self.config.measure_type, + "misfit_dt": misfit_p, + "misfit_dlna": misfit_q, + "sigma_dt": sigma_dt_cc, + "sigma_dlna": sigma_dlna_cc, + "dt": cc_tshift, + "dlna": cc_dlna, + } + ) + + # Taper windowed adjoint source before including in final array + window_taper(fp_t[0:nlen_w], + taper_percentage=self.config.taper_percentage, + taper_type=self.config.taper_type) + window_taper(fq_t[0:nlen_w], + taper_percentage=self.config.taper_percentage, + taper_type=self.config.taper_type) + + # Place windowed adjoint source within the correct time location + # w.r.t the entire synthetic seismogram + fp_wind = np.zeros(len(self.synthetic.data)) + fq_wind = np.zeros(len(self.synthetic.data)) + fp_wind[left_sample: right_sample] = fp_t[0:nlen_w] + fq_wind[left_sample: right_sample] = fq_t[0:nlen_w] + + # Add the windowed adjoint source to the full adjoint source + fp += fp_wind + fq += fq_wind + + # Increment total misfit value by misfit of windows + misfit_sum_p += misfit_p + misfit_sum_q += misfit_q + + return misfit_sum_p, misfit_sum_q, fp, fq, win_stats + + def calculate_dd_adjoint_source(self): + """ + Process double difference adjoint source. Requires second set of + waveforms and windows. + + .. note:: + + amplitude measurement stuff has been mostly left in the function + (i.e., commented out) even it is not used, so that hopefully it is + easier for someone in the future to implement it if they want. + + :rtype: (float, np.array, np.array, dict) + :return: (misfit_sum_p, fp, fp_2, win_stats) == ( + total phase misfit for the measurement, + adjoint source for first data-synthetic pair, + adjoint source for second data-synthetic pair, + measurement information dictionary + ) + """ + # Arrays for adjoint sources w.r.t time shift (p) and amplitude (q) + fp = np.zeros(self.nlen_data) + fp_2 = np.zeros(self.nlen_data) + + misfit_sum_p = 0.0 + # misfit_sum_q = 0.0 + win_stats = [] + + # Loop over time windows and calculate misfit for each window range + for window, window_2 in zip(self.windows, self.windows_2): + # `is_mtm` determines whether we use MTM (T) or CC (F) for misfit + is_mtm = True + + # Prepare first set of waveforms + left_sample, right_sample, nlen_w = get_window_info(window, self.dt) + fp_t = np.zeros(nlen_w) + misfit_p = 0. + # misfit_q = 0. + # Pre-allocate arrays for memory efficiency + d = np.zeros(nlen_w) + s = np.zeros(nlen_w) + # d and s represent the windowed data and synthetic arrays + d[0: nlen_w] = self.observed.data[left_sample: right_sample] + s[0: nlen_w] = self.synthetic.data[left_sample: right_sample] + + # Prepare second set of waveforms + left_sample_2, right_sample_2, nlen_w_2 = \ + get_window_info(window_2, self.dt) + fp_2_t = np.zeros(nlen_w_2) + + # Pre-allocate arrays for memory efficiency + d_2 = np.zeros(nlen_w) + s_2 = np.zeros(nlen_w) + # d and s represent the windowed data and synthetic arrays + d_2[0: nlen_w_2] = \ + self.observed_2.data[left_sample_2: right_sample_2] + s_2[0: nlen_w_2] = \ + self.synthetic_2.data[left_sample_2: right_sample_2] + + # Taper windowed signals (modify arrays in place) + for arr in [d, s, d_2, s_2]: + window_taper(arr, taper_percentage=self.config.taper_percentage, + taper_type=self.config.taper_type) + + # Calculate double difference cross correlation time shift for + # both sets of waveforms + cc_tshift, cc_tshift_obs, cc_tshift_syn, cc_dlna_obs, cc_dlna_syn, \ + sigma_dt_cc, sigma_dlna_cc = calculate_dd_cc_shift( + d=d, s=s, d_2=d_2, s_2=s_2, dt=self.dt, **vars(self.config) + ) + + # Perform a series of checks to see if MTM is valid for the data + # This will only loop once, but allows us to break if a check fails + while is_mtm is True: + is_mtm = self.check_time_series_acceptability( + cc_tshift=cc_tshift, nlen_w=nlen_w) + if is_mtm is False: + break + + # Shift and scale observed data 'd' to match second set: `d_2` + d, is_mtm_obs = self.prepare_data_for_mtm( + d=d, tshift=cc_tshift_obs, dlna=cc_dlna_obs, window=window + ) + # Shift and scale synthetics 's' to match second set: `s_2` + s, is_mtm_syn = self.prepare_data_for_mtm( + d=s, tshift=cc_tshift_syn, dlna=cc_dlna_syn, window=window + ) + if is_mtm_obs is False or is_mtm_syn is False: + logger.info(f"reject MTM: adjusted CC shift: {cc_tshift} is" + f"out of bounds of time series") + logger.debug(f"win = [{left_sample * self.dt}, " + f"{right_sample * self.dt}]") + break + + # Determine FFT information related to frequency bands + # TODO: Sampling rate was set to observed delta, is dt the same? + freq = np.fft.fftfreq(n=self.nlen_f, d=self.dt) + df = freq[1] - freq[0] # delta_f: frequency step + wvec = freq * 2 * np.pi # omega vector: angular frequency + logger.debug("delta_f (frequency sampling) = {df}") + + # Check for sufficient frequency range given taper bandwith + nfreq_min, nfreq_max, is_mtm = self.calculate_freq_limits(df) + if is_mtm is False: + logger.info("reject MTM: frequency range narrower than " + "half taper bandwith") + break + + # Determine taper bandwith in frequency domain + tapert, eigens = dpss_windows( + n=nlen_w, half_nbw=self.config.mt_nw, + k_max=self.config.num_taper, low_bias=False + ) + is_mtm = np.isfinite(eigens).all() + if is_mtm is False: + logger.warning("reject MTM: error constructing DPSS tapers") + logger.debug(f"eigenvalues: {eigens}") + break + + # Check if tapers are properly generated. In rare cases + # (e.g., [nw=2.5, nlen=61] or [nw=4.0, nlen=15]) certain + # eigenvalues can not be found and associated eigentaper is NaN + tapers = tapert.T * np.sqrt(nlen_w) + phi_mtm_obs, abs_mtm_obs, dtau_mtm_obs, dlna_mtm_obs = \ + self.calculate_multitaper( + d=d, s=d_2, tapers=tapers, wvec=wvec, + nfreq_min=nfreq_min, nfreq_max=nfreq_max, + cc_tshift=cc_tshift_obs, cc_dlna=cc_dlna_obs + ) + phi_mtm_syn, abs_mtm_syn, dtau_mtm_syn, dlna_mtm_syn = \ + self.calculate_multitaper( + d=s, s=s_2, tapers=tapers, wvec=wvec, + nfreq_min=nfreq_min, nfreq_max=nfreq_max, + cc_tshift=cc_tshift_syn, cc_dlna=cc_dlna_syn + ) + + # Measurements are difference between double differences + # phi_mtm = phi_mtm_syn - phi_mtm_obs + # abs_mtm = abs_mtm_syn - abs_mtm_obs + dtau_mtm = dtau_mtm_syn - dtau_mtm_obs + dlna_mtm = dlna_mtm_syn - dlna_mtm_obs + + # Calculate multi-taper error estimation if requested + # FIXME: should dtau_mtm and dlna_mtm be the 'obs' or 'diff' v.? + if self.config.use_mt_error: + sigma_phi_mt, sigma_abs_mt, sigma_dtau_mt, \ + sigma_dlna_mt = self.calculate_mt_error( + d=d, s=s, tapers=tapers, wvec=wvec, + nfreq_min=nfreq_min, nfreq_max=nfreq_max, + cc_tshift=cc_tshift, cc_dlna=cc_dlna_obs, + phi_mtm=phi_mtm_obs, abs_mtm=abs_mtm_obs, + # dtau_mtm=dtau_mtm_obs, dlna_mtm=dlna_mtm_obs # ? + dtau_mtm=dtau_mtm, dlna_mtm=dlna_mtm + ) + else: + sigma_dtau_mt = np.zeros(self.nlen_f) + sigma_dlna_mt = np.zeros(self.nlen_f) + + # Check if the multitaper measurements fail selection criteria + is_mtm = self.check_mtm_time_shift_acceptability( + nfreq_min=nfreq_min, nfreq_max=nfreq_max, df=df, + cc_tshift=cc_tshift, dtau_mtm=dtau_mtm, + sigma_dtau_mt=sigma_dtau_mt) + if is_mtm is False: + break + + # We made it! If the loop is still running after this point, + # then we will use MTM for adjoint source calculation + + # Frequency domain taper weighted by measurement error + wp_w, wq_w = self.calculate_freq_domain_taper( + nfreq_min=nfreq_min, nfreq_max=nfreq_max, df=df, + dtau_mtm=dtau_mtm, dlna_mtm=dlna_mtm, err_dt_cc=sigma_dt_cc, + err_dlna_cc=sigma_dlna_cc, err_dtau_mt=sigma_dtau_mt, + err_dlna_mt=sigma_dlna_mt, + ) + + # Misfit is defined as the error-weighted measurements + # TODO dlna misfit not calculated, only phase (dtau) + dtau_mtm_weigh_sqr = dtau_mtm ** 2 * wp_w + misfit_p = 0.5 * 2.0 * simps(y=dtau_mtm_weigh_sqr, dx=df) + + logger.info("calculate double difference adjoint source w/ MTM") + fp_t, fp_2_t = self.calculate_dd_mt_adjsrc( + s=s, s_2=s_2, tapers=tapers, nfreq_min=nfreq_min, + nfreq_max=nfreq_max, df=df, dtau_mtm=dtau_mtm, + dlna_mtm=dlna_mtm, wp_w=wp_w, wq_w=wq_w + ) + + win_stats.append( + {"left": left_sample * self.dt, + "right": right_sample * self.dt, + "type": "dd_multitaper", + "measurement_type": self.config.measure_type, + "misfit_dt": misfit_p, + "sigma_dt": sigma_dt_cc, + "sigma_dlna": sigma_dlna_cc, + "tshift": np.mean(dtau_mtm[nfreq_min:nfreq_max]), + "dlna": np.mean(dlna_mtm[nfreq_min:nfreq_max]), + } + ) + break + + # If at some point MTM broke out of the loop, this code block will + # execute and calculate a CC adjoint source and misfit instead + if is_mtm is False: + logger.info("calculating adjoint source with double diff. CCTM") + misfit_p, misfit_q, fp_t, fp_2_t, fq_t, fq_2_t = \ + calculate_dd_cc_adjsrc(s=s, s_2=s_2, tshift=cc_tshift, + dlna=cc_dlna_obs, dt=self.dt, + sigma_dt=sigma_dt_cc, + sigma_dlna=sigma_dlna_cc) + win_stats.append( + {"left": left_sample * self.dt, + "right": right_sample * self.dt, + "type": "dd_cross_correlation_traveltime", + "measurement_type": self.config.measure_type, + "misfit_dt": misfit_p, + "misfit_dlna": misfit_q, + "sigma_dt": sigma_dt_cc, + "sigma_dlna": sigma_dlna_cc, + "dt": cc_tshift, + "dlna": cc_dlna_obs, + } + ) + + # Taper windowed adjoint source before including in final array + window_taper(fp_t[0:nlen_w], + taper_percentage=self.config.taper_percentage, + taper_type=self.config.taper_type) + window_taper(fp_2_t[0:nlen_w], + taper_percentage=self.config.taper_percentage, + taper_type=self.config.taper_type) + + # Place windowed adjoint source within the correct time location + # w.r.t the entire synthetic seismogram + fp_wind = np.zeros(len(self.synthetic.data)) + fp_2_wind = np.zeros(len(self.synthetic.data)) + fp_wind[left_sample: right_sample] = fp_t[0:nlen_w] + fp_2_wind[left_sample: right_sample] = fp_2_t[0:nlen_w] + + # Add the windowed adjoint source to the full adjoint source + fp += fp_wind + fp_2 += fp_2_wind + + # Increment total misfit value by misfit of windows + misfit_sum_p += misfit_p + + return misfit_sum_p, fp, fp_2, win_stats + + def calculate_mt_adjsrc(self, s, tapers, nfreq_min, nfreq_max, + dtau_mtm, dlna_mtm, wp_w, wq_w): + """ + Calculate the adjoint source for a multitaper measurement, which + tapers synthetics in various windowed frequency-dependent tapers and + scales them by phase dependent travel time measurements (which + incorporate the observed data). + + :type s: np.array + :param s: synthetic data array + :type tapers: np.array + :param tapers: array of DPPS windows shaped (num_taper, nlen_w) + :type nfreq_min: int + :param nfreq_min: minimum frequency for suitable MTM measurement + :type nfreq_max: int + :param nfreq_max: maximum frequency for suitable MTM measurement + :type dtau_mtm: np.array + :param dtau_mtm: phase dependent travel time measurements from mtm + :type dlna_mtm: np.array + :param dlna_mtm: phase dependent amplitude anomaly + :type wp_w: np.array + :param wp_w: phase-misfit error weighted frequency domain taper + :type wq_w: np.array + :param wq_w: amplitude-misfit error weighted frequency domain taper + """ + nlen_t = len(s) + ntaper = len(tapers[0]) + + # Start pieceing together transfer functions that will be applie to + # the synthetics + bottom_p = np.zeros(self.nlen_f, dtype=complex) + bottom_q = np.zeros(self.nlen_f, dtype=complex) + + s_tw = np.zeros((self.nlen_f, ntaper), dtype=complex) + s_tvw = np.zeros((self.nlen_f, ntaper), dtype=complex) + + # Construct the bottom term of the adjoint formula which requires + # summed contributions from each of the taper bands + for itaper in range(0, ntaper): + taper = np.zeros(self.nlen_f) + taper[0:nlen_t] = tapers[0:nlen_t, itaper] + + # Taper synthetics (s_t) and take the derivative (s_tv) + s_t = s * taper[0:nlen_t] + s_tv = np.gradient(s_t, self.dt) + + # Apply FFT to tapered measurements to get to freq. domain. + s_tw[:, itaper] = np.fft.fft(s_t, self.nlen_f)[:] * self.dt + s_tvw[:, itaper] = np.fft.fft(s_tv, self.nlen_f)[:] * self.dt + + # Calculate bottom term of the adjoint equation + bottom_p[:] = ( + bottom_p[:] + + s_tvw[:, itaper] * s_tvw[:, itaper].conjugate() + ) + bottom_q[:] = ( + bottom_q[:] + + s_tw[:, itaper] * s_tw[:, itaper].conjugate() + ) + + # Now we generate the adjoint sources using each of the tapers + fp_t = np.zeros(nlen_t) + fq_t = np.zeros(nlen_t) + + for itaper in range(0, ntaper): + taper = np.zeros(self.nlen_f) + taper[0: nlen_t] = tapers[0:nlen_t, itaper] + + # Calculate the full adjoint terms pj(w), qj(w) + p_w = np.zeros(self.nlen_f, dtype=complex) + q_w = np.zeros(self.nlen_f, dtype=complex) + + p_w[nfreq_min:nfreq_max] = ( + s_tvw[nfreq_min:nfreq_max, itaper] / + bottom_p[nfreq_min:nfreq_max] + ) + q_w[nfreq_min:nfreq_max] = ( + -1 * s_tw[nfreq_min:nfreq_max, itaper] / + bottom_q[nfreq_min:nfreq_max] + ) + + # weight the adjoint terms by the phase + amplitude measurements + p_w *= dtau_mtm * wp_w # phase + q_w *= dlna_mtm * wq_w # amplitude + + # inverse FFT of weighted adjoint to get back to the time domain + p_wt = np.fft.ifft(p_w, self.nlen_f).real * 2. / self.dt + q_wt = np.fft.ifft(q_w, self.nlen_f).real * 2. / self.dt + + # Taper adjoint term before adding it back to full adj source + fp_t[0:nlen_t] += p_wt[0:nlen_t] * taper[0:nlen_t] + fq_t[0:nlen_t] += q_wt[0:nlen_t] * taper[0:nlen_t] + + return fp_t, fq_t + + def calculate_dd_mt_adjsrc(self, s, s_2, tapers, nfreq_min, nfreq_max, df, + dtau_mtm, dlna_mtm, wp_w, wq_w): + """ + Calculate the double difference adjoint source for multitaper + measurement. Almost the same as `calculate_mt_adjsrc` but only addresses + phase misfit and requres a second set of synthetics `s_2` which is + processed in the same way as the first set `s` + + :type s: np.array + :param s: synthetic data array + :type s_2: np.array + :param s_2: optional 2nd set of synthetics for double difference + measurements only. This will change the output + :type tapers: np.array + :param tapers: array of DPPS windows shaped (num_taper, nlen_w) + :type nfreq_min: int + :param nfreq_min: minimum frequency for suitable MTM measurement + :type nfreq_max: int + :param nfreq_max: maximum frequency for suitable MTM measurement + :type df: floats + :param df: step length of frequency bins for FFT + :type dtau_mtm: np.array + :param dtau_mtm: phase dependent travel time measurements from mtm + :type dlna_mtm: np.array + :param dlna_mtm: phase dependent amplitude anomaly + :type wp_w: np.array + :param wp_w: phase-misfit error weighted frequency domain taper + :type wq_w: np.array + :param wq_w: amplitude-misfit error weighted frequency domain taper + """ + nlen_t = len(s) + ntaper = len(tapers[0]) + + # Set up to piece together transfer functions that will be applied to + # the synthetics. Sets up arrays for memory efficiency + s_tw = np.zeros((self.nlen_f, ntaper), dtype=complex) + s_tvw = np.zeros((self.nlen_f, ntaper), dtype=complex) + + s_2_tw = np.zeros((self.nlen_f, ntaper), dtype=complex) + s_2_tvw = np.zeros((self.nlen_f, ntaper), dtype=complex) + + bottom_p = np.zeros(self.nlen_f, dtype=complex) + bottom_p_2 = np.zeros(self.nlen_f, dtype=complex) + + # Construct the bottom term of the adjoint formula which requires + # summed contributions from each of the taper bands + for itaper in range(0, ntaper): + taper = np.zeros(self.nlen_f) + taper[0:nlen_t] = tapers[0:nlen_t, itaper] + + # Taper synthetics (s_t) and take the derivative (s_tv) + s_t = s * taper[0:nlen_t] + s_tv = np.gradient(s_t, self.dt) + # Apply FFT to tapered measurements to get to freq. domain. + s_tw[:, itaper] = np.fft.fft(s_t, self.nlen_f)[:] * self.dt + s_tvw[:, itaper] = np.fft.fft(s_tv, self.nlen_f)[:] * self.dt + + # Perform same tasks but for second set synthetics + s_2_t = s_2 * taper[0:nlen_t] + s_2_tv = np.gradient(s_2_t, self.dt) + s_2_tw[:, itaper] = np.fft.fft(s_2_t, self.nlen_f)[:] * self.dt + s_2_tvw[:, itaper] = np.fft.fft(s_2_tv, + self.nlen_f)[:] * self.dt + + # Calculate bottom term of the adjoint equation + bottom_p[:] = ( + bottom_p[:] + + s_tvw[:, itaper] * s_2_tvw[:, itaper].conjugate() + ) + bottom_p_2[:] = ( + bottom_p_2[:] + + s_2_tvw[:, itaper] * s_tvw[:, itaper].conjugate() + ) + + # Now we generate the adjoint sources using each of the tapers + fp_t = np.zeros(nlen_t) + fp_2_t = np.zeros(nlen_t) + + for itaper in range(0, ntaper): + taper = np.zeros(self.nlen_f) + taper[0: nlen_t] = tapers[0:nlen_t, itaper] + + # Calculate the full adjoint terms pj(w), qj(w) + p_w = np.zeros(self.nlen_f, dtype=complex) + p_2_w = np.zeros(self.nlen_f, dtype=complex) + + p_w[nfreq_min:nfreq_max] = ( + -1. * s_2_tvw[nfreq_min:nfreq_max, itaper] / + bottom_p_2[nfreq_min:nfreq_max] + ) + p_2_w[nfreq_min:nfreq_max] = ( + 1. * s_tvw[nfreq_min:nfreq_max, itaper] / + bottom_p[nfreq_min:nfreq_max] + ) + + # weight the adjoint terms by the phase measurements + p_w *= dtau_mtm * wp_w # phase + p_2_w *= dtau_mtm * wq_w # amplitude + + # inverse FFT of weighted adjoint to get back to the time domain + p_wt = np.fft.ifft(p_w, self.nlen_f).real * 2. / self.dt + p_2_wt = np.fft.ifft(p_2_w, self.nlen_f).real * 2. / self.dt + + # Taper adjoint term before adding it back to full adj source + fp_t[0:nlen_t] += p_wt[0:nlen_t] * taper[0:nlen_t] + fp_2_t[0:nlen_t] += p_2_wt[0:nlen_t] * taper[0:nlen_t] + + return fp_t, fp_2_t + + def calculate_freq_domain_taper(self, nfreq_min, nfreq_max, df, + dtau_mtm, dlna_mtm, err_dt_cc, + err_dlna_cc, err_dtau_mt, err_dlna_mt): + """ + Calculates frequency domain taper weighted by misfit (either CC or MTM) + + .. note:: + + Frequency-domain tapers are based on adjusted frequency band and + error estimation. They are not one of the filtering processes that + needs to be applied to the adjoint source but rather a frequency + domain weighting function for adjoint source and misfit function. + + :type nfreq_min: int + :param nfreq_min: minimum frequency for suitable MTM measurement + :type nfreq_max: int + :param nfreq_max: maximum frequency for suitable MTM measurement + :type df: floats + :param df: step length of frequency bins for FFT + :type dtau_mtm: np.array + :param dtau_mtm: phase dependent travel time measurements from mtm + :type dlna_mtm: np.array + :param dlna_mtm: phase dependent amplitude anomaly + :type err_dt_cc: float + :param err_dt_cc: cross correlation time shift error + :type err_dlna_cc: float + :param err_dlna_cc: cross correlation amplitude anomaly error + :type err_dtau_mt: np.array + :param err_dtau_mt: phase-dependent timeshift error + :type err_dlna_mt: np.array + :param err_dlna_mt: phase-dependent amplitude error + """ + w_taper = np.zeros(self.nlen_f) + + win_taper_len = nfreq_max - nfreq_min + win_taper = np.ones(win_taper_len) + + # Createsa cosine taper over a range of frequencies in freq. domain + window_taper(win_taper, taper_percentage=1.0, taper_type="cos_p10") + w_taper[nfreq_min: nfreq_max] = win_taper[0:win_taper_len] + + # Normalization factor, factor 2 is needed for integration -inf to inf + ffac = 2.0 * df * np.sum(w_taper[nfreq_min: nfreq_max]) + logger.debug(f"frequency bound (idx): [{nfreq_min}, {nfreq_max - 1}] " + f"(Hz) [{df * (nfreq_min - 1)}, {df * nfreq_max}]" + ) + logger.debug(f"frequency domain taper normalization coeff: {ffac}") + logger.debug(f"frequency domain sampling length df={df}") + if ffac <= 0.0: + logger.warning("frequency band too narrow:") + logger.warning(f"fmin={nfreq_min}, fmax={nfreq_max}, ffac={ffac}") + + # Normalized, tapered window in the frequency domain + wp_w = w_taper / ffac + wq_w = w_taper / ffac + + # Choose whether to scale by CC error or to by calculated MT errors + if self.config.use_cc_error: + wp_w /= err_dt_cc ** 2 + wq_w /= err_dlna_cc ** 2 + elif self.config.use_mt_error: + dtau_wtr = ( + self.config.water_threshold * + np.sum(np.abs(dtau_mtm[nfreq_min: nfreq_max])) / + (nfreq_max - nfreq_min) + ) + dlna_wtr = ( + self.config.water_threshold * + np.sum(np.abs(dlna_mtm[nfreq_min: nfreq_max])) / + (nfreq_max - nfreq_min) + ) + + err_dtau_mt[nfreq_min: nfreq_max] = \ + err_dtau_mt[nfreq_min: nfreq_max] + dtau_wtr * \ + (err_dtau_mt[nfreq_min: nfreq_max] < dtau_wtr) + err_dlna_mt[nfreq_min: nfreq_max] = \ + err_dlna_mt[nfreq_min: nfreq_max] + dlna_wtr * \ + (err_dlna_mt[nfreq_min: nfreq_max] < dlna_wtr) + + wp_w[nfreq_min: nfreq_max] = ( + wp_w[nfreq_min: nfreq_max] / + ((err_dtau_mt[nfreq_min: nfreq_max]) ** 2) + ) + wq_w[nfreq_min: nfreq_max] = ( + wq_w[nfreq_min: nfreq_max] / + ((err_dlna_mt[nfreq_min: nfreq_max]) ** 2) + ) + + return wp_w, wq_w + + def calculate_multitaper(self, d, s, tapers, wvec, nfreq_min, nfreq_max, + cc_tshift, cc_dlna): + """ + Measure phase-dependent time shifts and amplitude anomalies using + the multitaper method + + .. note:: + Formerly `mt_measure`. Renamed for additional clarity and to match + the CCTM function names + + :type d: np.array + :param d: observed data array + :type s: np.array + :param s: synthetic data array + :type tapers: np.array + :param tapers: array of DPPS windows shaped (num_taper, nlen_w) + :type wvec: np.array + :param wvec: angular frequency array generated from Discrete Fourier + Transform sample frequencies + :type nfreq_min: int + :param nfreq_min: minimum frequency for suitable MTM measurement + :type nfreq_max: int + :param nfreq_max: maximum frequency for suitable MTM measurement + :type cc_tshift: float + :param cc_tshift: cross correlation time shift + :type cc_dlna: float + :param cc_dlna: amplitude anomaly from cross correlation + :rtype: tuple of np.array + :return: (phi_w, abs_w, dtau_w, dlna_w); + (frequency dependent phase anomaly, + phase dependent amplitude anomaly, + phase dependent cross-correlation time shift, + phase dependent cross-correlation amplitude anomaly) + """ + # Initialize some constants for convenience + nlen_t = len(d) + ntaper = len(tapers[0]) + fnum = int(self.nlen_f / 2 + 1) + + # Initialize empty arrays to be filled by FFT calculations + top_tf = np.zeros(self.nlen_f, dtype=complex) + bot_tf = np.zeros(self.nlen_f, dtype=complex) + + # Multitaper measurements + for itaper in range(0, ntaper): + taper = np.zeros(nlen_t) + taper[0:nlen_t] = tapers[0:nlen_t, itaper] + + # Apply time-domain multi-tapered measurements + d_t = np.zeros(nlen_t, dtype=complex) + s_t = np.zeros(nlen_t, dtype=complex) + + d_t[0:nlen_t] = d[0:nlen_t] * taper[0:nlen_t] + s_t[0:nlen_t] = s[0:nlen_t] * taper[0:nlen_t] + + d_tw = np.fft.fft(d_t, self.nlen_f) * self.dt + s_tw = np.fft.fft(s_t, self.nlen_f) * self.dt + + # Calculate top and bottom of MT transfer function + top_tf[:] = top_tf[:] + d_tw[:] * s_tw[:].conjugate() + bot_tf[:] = bot_tf[:] + s_tw[:] * s_tw[:].conjugate() + + # Calculate water level for transfer function + wtr_use = (max(abs(bot_tf[0:fnum])) * + self.config.transfunc_waterlevel ** 2) + + # Create transfer function + trans_func = np.zeros(self.nlen_f, dtype=complex) + for i in range(nfreq_min, nfreq_max): + if abs(bot_tf[i]) < wtr_use: + trans_func[i] = top_tf[i] / bot_tf[i] + else: + trans_func[i] = top_tf[i] / (bot_tf[i] + wtr_use) + + # Estimate phase and amplitude anomaly from transfer function + phi_w = np.zeros(self.nlen_f) + abs_w = np.zeros(self.nlen_f) + dtau_w = np.zeros(self.nlen_f) + dlna_w = np.zeros(self.nlen_f) + + # Calculate the phase anomaly + phi_w[nfreq_min:nfreq_max] = np.arctan2( + trans_func[nfreq_min:nfreq_max].imag, + trans_func[nfreq_min:nfreq_max].real + ) + phi_w = process_cycle_skipping(phi_w=phi_w, nfreq_max=nfreq_max, + nfreq_min=nfreq_min, wvec=wvec, + phase_step=self.config.phase_step) + + # Calculate amplitude anomaly + abs_w[nfreq_min:nfreq_max] = np.abs(trans_func[nfreq_min:nfreq_max]) + + # Add the CC measurements to the transfer function + dtau_w[0] = cc_tshift + dtau_w[max(nfreq_min, 1): nfreq_max] = \ + - 1.0 / wvec[max(nfreq_min, 1): nfreq_max] * \ + phi_w[max(nfreq_min, 1): nfreq_max] + cc_tshift + + dlna_w[nfreq_min:nfreq_max] = np.log( + abs_w[nfreq_min:nfreq_max]) + cc_dlna + + return phi_w, abs_w, dtau_w, dlna_w + + def calculate_mt_error(self, d, s, tapers, wvec, nfreq_min, nfreq_max, + cc_tshift, cc_dlna, phi_mtm, abs_mtm, dtau_mtm, + dlna_mtm): + """ + Calculate multitaper error with Jackknife MT estimates. + + The jackknife estimator of a parameter is found by systematically + leaving out each observation from a dataset and calculating the + parameter estimate over the remaining observations and then aggregating + these calculations. + + :type d: np.array + :param d: observed data array + :type s: np.array + :param s: synthetic data array + :type tapers: np.array + :param tapers: array of DPPS windows shaped (num_taper, nlen_w) + :type wvec: np.array + :param wvec: angular frequency array generated from Discrete Fourier + Transform sample frequencies + :type nfreq_min: int + :param nfreq_min: minimum frequency for suitable MTM measurement + :type nfreq_max: int + :param nfreq_max: maximum frequency for suitable MTM measurement + :type cc_tshift: float + :param cc_tshift: cross correlation time shift + :type cc_dlna: float + :param cc_dlna: amplitude anomaly from cross correlation + :type phi_mtm: np.array + :param phi_mtm: frequency dependent phase anomaly + :type abs_mtm: np.array + :param abs_mtm: phase dependent amplitude anomaly + :type dtau_mtm: np.array + :param dtau_mtm: phase dependent cross-correlation time shift + :type dlna_mtm: np.array + :param dlna_mtm: phase dependent cross-correlation amplitude anomaly) + :rtype: tuple of np.array + :return: (err_phi, err_abs, err_dtau, err_dlna), + (error in frequency dependent phase anomaly, + error in phase dependent amplitude anomaly, + error in phase dependent cross-correlation time shift, + error in phase dependent cross-correlation amplitude anomaly) + """ + nlen_t = len(d) + ntaper = len(tapers[0]) + logger.debug("Number of tapers used: %d" % ntaper) + + # Jacknife MT estimates. Initialize arrays for memory efficiency + phi_mul = np.zeros((self.nlen_f, ntaper)) + abs_mul = np.zeros((self.nlen_f, ntaper)) + dtau_mul = np.zeros((self.nlen_f, ntaper)) + dlna_mul = np.zeros((self.nlen_f, ntaper)) + ephi_ave = np.zeros(self.nlen_f) + eabs_ave = np.zeros(self.nlen_f) + edtau_ave = np.zeros(self.nlen_f) + edlna_ave = np.zeros(self.nlen_f) + err_phi = np.zeros(self.nlen_f) + err_abs = np.zeros(self.nlen_f) + err_dtau = np.zeros(self.nlen_f) + err_dlna = np.zeros(self.nlen_f) + + # Loop through all tapers + for itaper in range(0, ntaper): + # Delete one taper at a time + tapers_om = np.zeros((nlen_t, ntaper - 1)) + tapers_om[0:self.nlen_f, 0:ntaper - 1] = \ + np.delete(tapers, itaper, 1) + + # FIXME Recalculate MT measurements with deleted taper list + phi_om, abs_om, dtau_om, dlna_om = self.calculate_multitaper( + d=d, s=s, tapers=tapers_om, wvec=wvec, nfreq_min=nfreq_min, + nfreq_max=nfreq_max, cc_tshift=cc_tshift, cc_dlna=cc_dlna + ) + + phi_mul[0:self.nlen_f, itaper] = phi_om[0:self.nlen_f] + abs_mul[0:self.nlen_f, itaper] = abs_om[0:self.nlen_f] + dtau_mul[0:self.nlen_f, itaper] = dtau_om[0:self.nlen_f] + dlna_mul[0:self.nlen_f, itaper] = dlna_om[0:self.nlen_f] + + # Error estimation + ephi_ave[nfreq_min: nfreq_max] = ( + ephi_ave[nfreq_min: nfreq_max] + + ntaper * phi_mtm[nfreq_min: nfreq_max] - + (ntaper - 1) * phi_mul[nfreq_min: nfreq_max, itaper] + ) + eabs_ave[nfreq_min:nfreq_max] = ( + eabs_ave[nfreq_min: nfreq_max] + + ntaper * abs_mtm[nfreq_min: nfreq_max] - + (ntaper - 1) * abs_mul[nfreq_min: nfreq_max, itaper] + ) + edtau_ave[nfreq_min: nfreq_max] = ( + edtau_ave[nfreq_min: nfreq_max] + + ntaper * dtau_mtm[nfreq_min: nfreq_max] - + (ntaper - 1) * dtau_mul[nfreq_min: nfreq_max, itaper] + ) + edlna_ave[nfreq_min: nfreq_max] = ( + edlna_ave[nfreq_min: nfreq_max] + + ntaper * dlna_mtm[nfreq_min: nfreq_max] - + (ntaper - 1) * dlna_mul[nfreq_min: nfreq_max, itaper] + ) + + # Take average over each taper band + ephi_ave /= ntaper + eabs_ave /= ntaper + edtau_ave /= ntaper + edlna_ave /= ntaper + + # Calculate deviation + for itaper in range(0, ntaper): + err_phi[nfreq_min:nfreq_max] += \ + (phi_mul[nfreq_min: nfreq_max, itaper] - + ephi_ave[nfreq_min: nfreq_max]) ** 2 + err_abs[nfreq_min:nfreq_max] += \ + (abs_mul[nfreq_min: nfreq_max, itaper] - + eabs_ave[nfreq_min: nfreq_max]) ** 2 + err_dtau[nfreq_min:nfreq_max] += \ + (dtau_mul[nfreq_min: nfreq_max, itaper] - + edtau_ave[nfreq_min: nfreq_max]) ** 2 + err_dlna[nfreq_min:nfreq_max] += \ + (dlna_mul[nfreq_min: nfreq_max, itaper] - + edlna_ave[nfreq_min: nfreq_max]) ** 2 + + # Calculate standard deviation + err_phi[nfreq_min: nfreq_max] = np.sqrt( + err_phi[nfreq_min: nfreq_max] / (ntaper * (ntaper - 1))) + err_abs[nfreq_min: nfreq_max] = np.sqrt( + err_abs[nfreq_min: nfreq_max] / (ntaper * (ntaper - 1))) + err_dtau[nfreq_min: nfreq_max] = np.sqrt( + err_dtau[nfreq_min: nfreq_max] / (ntaper * (ntaper - 1))) + err_dlna[nfreq_min: nfreq_max] = np.sqrt( + err_dlna[nfreq_min: nfreq_max] / (ntaper * (ntaper - 1))) + + return err_phi, err_abs, err_dtau, err_dlna + + def calculate_freq_limits(self, df): + """ + Determine if a given window is suitable for multitaper measurements. + If so, finds the maximum frequency range for the measurement using a + spectrum of tapered synthetic waveforms + + First check if the window is suitable for mtm measurements, then + find the maximum frequency point for measurement using the spectrum of + tapered synthetics. + + .. note:: + formerly `frequency_limit`. renamed to be more descriptive. also + split off earlier cycle check from this function into + `check_sufficient_number_of_wavelengths` + + :type df: float + :param df: step length of frequency bins for FFT + :rtype: tuple + :return (float, float, bool); + (minimumum frequency, maximum frequency, continue with MTM?) + """ + # Calculate the frequency limits based on FFT of synthetics + fnum = int(self.nlen_f / 2 + 1) + s_spectra = np.fft.fft(self.synthetic.data, self.nlen_f) * self.dt + + # Calculate the maximum amplitude of the spectra for the given frequency + ampmax = max(abs(s_spectra[0: fnum])) + i_ampmax = np.argmax(abs(s_spectra[0: fnum])) + + # Scale the maximum amplitude by some constant water level + scaled_wl = self.config.water_threshold * ampmax + + # Default starting values for min/max freq. bands + ifreq_min = int(1.0 / (self.config.max_period * df)) # default fmin + ifreq_max = int(1.0 / (self.config.min_period * df)) # default fmax + + # Get the maximum frequency limit by searching valid frequencies + nfreq_max = fnum - 1 + is_search = True + for iw in range(0, fnum): + if iw > i_ampmax: + nfreq_max, is_search = self._search_frequency_limit( + is_search=is_search, index=iw, nfreq_limit=nfreq_max, + spectra=s_spectra, water_threshold=scaled_wl + ) + # Make sure `nfreq_max` does not go beyond the Nyquist frequency + nfreq_max = min(nfreq_max, ifreq_max, int(1.0 / (2 * self.dt) / df) - 1) + + # Get the minimum frequency limit by searchjing valid frequencies + nfreq_min = 0 + is_search = True + for iw in range(fnum - 1, 0, -1): + if iw < i_ampmax: + nfreq_min, is_search = self._search_frequency_limit( + is_search=is_search, index=iw, nfreq_limit=nfreq_min, + spectra=s_spectra, water_threshold=scaled_wl + ) + + # Limit `nfreq_min` by assuming at least N cycles within the window + nfreq_min = max( + nfreq_min, ifreq_min, + int(self.config.min_cycle_in_window / self.tlen_data / df) - 1 + ) + + # Reject mtm if the chosen frequency band is narrower than quarter of + # the multi-taper bandwidth + half_taper_bandwidth = self.config.mt_nw / (4.0 * self.tlen_data) + chosen_bandwidth = (nfreq_max - nfreq_min) * df + + if chosen_bandwidth < half_taper_bandwidth: + logger.debug(f"chosen bandwidth ({chosen_bandwidth}) < " + f"half taper bandwidth ({half_taper_bandwidth})") + nfreq_min = None + nfreq_max = None + is_mtm = False + else: + is_mtm = True + + return nfreq_min, nfreq_max, is_mtm + + def prepare_data_for_mtm(self, d, tshift, dlna, window): + """ + Re-window observed data to center on the optimal time shift, and + scale by amplitude anomaly to get best matching waveforms for MTM + + :return: + """ + left_sample, right_sample, nlen_w = get_window_info(window, self.dt) + ishift = int(tshift / self.dt) # time shift in samples + + left_sample_d = max(left_sample + ishift, 0) + right_sample_d = min(right_sample + ishift, self.nlen_data) + nlen_d = right_sample_d - left_sample_d + + if nlen_d == nlen_w: + # TODO: No need to correct `cc_dlna` in multitaper measurements? + d[0:nlen_w] = self.observed.data[left_sample_d:right_sample_d] + d *= np.exp(-dlna) + window_taper(d, taper_percentage=self.config.taper_percentage, + taper_type=self.config.taper_type) + is_mtm = True + + # If the shifted time window is now out of bounds of the time series + # we will not be able to use MTM + else: + is_mtm = False + + return d, is_mtm + + def check_time_series_acceptability(self, cc_tshift, nlen_w): + """ + Checking acceptability of the time series characteristics for MTM + + :type cc_tshift: float + :param cc_tshift: time shift in unit [s] + :type nlen_w: int + :param nlen_w: window length in samples + :rtype: bool + :return: True if time series OK for MTM, False if fall back to CC + """ + # Check length of the time shift w.r.t time step + if abs(cc_tshift) <= self.dt: + logger.info(f"reject MTM: time shift {cc_tshift} <= " + f"dt ({self.dt})") + return False + + # Check for sufficient number of wavelengths in window + elif bool(self.config.min_cycle_in_window * self.config.min_period > + nlen_w): + logger.info("reject MTM: too few cycles within time window") + logger.debug(f"min_period: {self.config.min_period:.2f}s; " + f"window length: {nlen_w:.2f}s") + return False + else: + return True + + def check_mtm_time_shift_acceptability(self, nfreq_min, nfreq_max, df, + cc_tshift, dtau_mtm, sigma_dtau_mt): + """ + Check MTM time shift measurements to see if they are within allowable + bounds set by the config. If any of the phases used in MTM do not + meet criteria, we will fall back to CC measurement. + + .. note:: + formerly `mt_measure_select`, renamed for clarity + + :type nfreq_max: int + :param nfreq_max: maximum in frequency domain + :type nfreq_min: int + :param nfreq_min: minimum in frequency domain + :type df: floats + :param df: step length of frequency bins for FFT + :type cc_tshift: float + :param cc_tshift: c.c. time shift + :type dtau_mtm: np.array + :param dtau_mtm: phase dependent travel time measurements from mtm + :type sigma_dtau_mt: np.array + :param sigma_dtau_mt: phase-dependent error of multitaper measurement + :rtype: bool + :return: flag for whether any of the MTM phases failed check + """ + # True unless set False + is_mtm = True + + # If any MTM measurements is out of the resonable range, switch to CC + for j in range(nfreq_min, nfreq_max): + # dt larger than 1/dt_fac of the wave period + if np.abs(dtau_mtm[j]) > 1. / (self.config.dt_fac * j * df): + logger.info("reject MTM: `dt` measurements is too large") + is_mtm = False + + # Error larger than 1/err_fac of wave period + if sigma_dtau_mt[j] > 1. / (self.config.err_fac * j * df): + logger.debug("reject MTM: `dt` error is too large") + is_mtm = False + + # dt larger than the maximum allowable time shift + if np.abs(dtau_mtm[j]) > self.config.dt_max_scale * abs(cc_tshift): + logger.debug("reject MTM: dt is larger than the maximum " + "allowable time shift") + is_mtm = False + + return is_mtm + + def rewindow(self, data, left_sample, right_sample, shift): + """ + Align data in a window according to a given time shift. Will not fully + shift if shifted window hits bounds of the data array + + :type data: np.array + :param data: full data array to cut with shifted window + :type left_sample: int + :param left_sample: left window border + :type right_sample: int + :param right_sample: right window border + :type shift: int + :param shift: overall time shift in units of samples + """ + nlen_data = len(data) + nlen = right_sample - left_sample + lindex = 0 + + left_shifted = left_sample + shift + if left_shifted < 0: + logger.warn("Re-windowing due to left shift is out of bounds.") + lindex = -1 * left_shifted + left_shifted = 0 + + rindex = nlen + right_shifted = right_sample + shift + if right_shifted > nlen_data: + logger.warn("Re-windowing due to right shift is out of bounds.") + rindex = rindex - (right_shifted - nlen_data) + right_shifted = nlen_data + + data_shifted = np.zeros(nlen) + data_shifted[lindex:rindex] = data[left_shifted:right_shifted] + + return data_shifted, left_shifted, right_shifted + + @staticmethod + def _search_frequency_limit(is_search, index, nfreq_limit, spectra, + water_threshold, c=10): + """ + Search valid frequency range of spectra. If the spectra larger than + 10 * `water_threshold` it will trigger the search again, works like the + heating thermostat. + + :type is_search: bool + :param is_search: Logic switch + :type index: int + :param index: index of spectra + :type nfreq_limit: int + :param nfreq_limit: index of freqency limit searched + :type spectra: int + :param spectra: spectra of signal + :type water_threshold: float + :param water_threshold: optional triggering value to stop the search, + if not given, defaults to Config value + :type c: int + :param c: constant scaling factor for water threshold. + """ + if abs(spectra[index]) < water_threshold and is_search: + is_search = False + nfreq_limit = index + + if abs(spectra[index]) > c * water_threshold and not is_search: + is_search = True + nfreq_limit = index + + return nfreq_limit, is_search + + +def calculate_adjoint_source(observed, synthetic, config, windows, choice=None, + observed_2=None, synthetic_2=None, windows_2=None): + """ + Convenience wrapper function for MTM class to match the expected format + of Pyadjoint. Contains the logic for what to return to User. + + :type observed: obspy.core.trace.Trace + :param observed: observed waveform to calculate adjoint source + :type synthetic: obspy.core.trace.Trace + :param synthetic: synthetic waveform to calculate adjoint source + :type config: pyadjoint.config.ConfigCCTraveltime + :param config: Config class with parameters to control processing + :type windows: list of tuples + :param windows: [(left, right),...] representing left and right window + borders to be used to calculate misfit and adjoint sources + :type choice: str + :param choice: Flag to turn on station pair calculations. Requires + `observed_2`, `synthetic_2`, `windows_2`. Available: + - 'double_difference': Double difference waveform misfit from + Yuan et al. 2016 + :type observed_2: obspy.core.trace.Trace + :param observed_2: second observed waveform to calculate adjoint sources + from station pairs + :type synthetic_2: obspy.core.trace.Trace + :param synthetic_2: second synthetic waveform to calculate adjoint sources + from station pairs + :type windows_2: list of tuples + :param windows_2: [(left, right),...] representing left and right window + borders to be tapered in units of seconds since first sample in data + array. Used to window `observed_2` and `synthetic_2` + """ + # Standard Multitaper Misfit approach, single waveform set + if choice is None: + ret_val_p = {} + ret_val_q = {} + + # Use the MTM class to generate misfit and adjoint sources + mtm = MultitaperMisfit(observed=observed, synthetic=synthetic, + config=config, windows=windows) + + misfit_sum_p, misfit_sum_q, fp, fq, stats = \ + mtm.calculate_adjoint_source() + + # Append information on the misfit for phase and amplitude + ret_val_p["misfit"] = misfit_sum_p + ret_val_q["misfit"] = misfit_sum_q + + # Reverse adjoint source in time w.r.t synthetics + ret_val_p["adjoint_source"] = fp[::-1] + ret_val_q["adjoint_source"] = fq[::-1] + + if config.measure_type == "dt": + ret_val = ret_val_p + elif config.measure_type == "am": + ret_val = ret_val_q + + ret_val["window_stats"] = stats + + # Double difference multitaper misfit, two sets of waveforms + elif choice == "double_difference": + ret_val = {} + + # Use the MTM class to generate misfit and adjoint sources + mtm = MultitaperMisfit(observed=observed, synthetic=synthetic, + config=config, windows=windows, + observed_2=observed_2, synthetic_2=synthetic_2, + windows_2=windows_2) + misfit_sum_p, fp, fp_2, stats = mtm.calculate_dd_adjoint_source() + + ret_val["misfit"] = misfit_sum_p + ret_val["adjoint_source"] = fp[::-1] + ret_val["adjoint_source_2"] = fp_2[::-1] + ret_val["window_stats"] = stats + else: + raise NotImplementedError + + return ret_val diff --git a/pyadjoint/adjoint_source_types/waveform_misfit.py b/pyadjoint/adjoint_source_types/waveform_misfit.py new file mode 100644 index 0000000..367edfc --- /dev/null +++ b/pyadjoint/adjoint_source_types/waveform_misfit.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +""" +Simple waveform misfit and adjoint source. + +.. note:: + This file serves as the template for generation of new adjoint sources. + Copy-paste file and adjust name, description and underlying calculation + function to generate new adjoint source. + +:authors: + adjTomo Dev Team (adjtomo@gmail.com), 2023 + Yanhua O. Yuan (yanhuay@princeton.edu), 2017 + Lion Krischer (krischer@geophysik.uni-muenchen.de), 2015 +:license: + BSD 3-Clause ("BSD New" or "BSD Simplified") +""" +import numpy as np +from scipy.integrate import simps +from pyadjoint import logger +from pyadjoint.utils.signal import get_window_info, window_taper + + +def calculate_adjoint_source(observed, synthetic, config, windows, + choice="waveform", observed_2=None, + synthetic_2=None, windows_2=None): + """ + Calculate adjoint source for the waveform-based misfit measurements. + + :type observed: obspy.core.trace.Trace + :param observed: observed waveform to calculate adjoint source + :type synthetic: obspy.core.trace.Trace + :param synthetic: synthetic waveform to calculate adjoint source + :type config: pyadjoint.config.ConfigWaveform + :param config: Config class with parameters to control processing + :type windows: list of tuples + :param windows: [(left, right),...] representing left and right window + borders to be tapered in units of seconds since first sample in data + array + :type choice: str + :param choice: Flag to turn on station pair calculations. Requires + `observed_2`, `synthetic_2`, `windows_2`. Available: + + - 'double_difference': Double difference waveform misfit from [Yuan2016]_ + - 'convolution': Waveform convolution misfit from [Choi2011]_ + :type observed_2: obspy.core.trace.Trace + :param observed_2: second observed waveform to calculate adjoint sources + from station pairs + :type synthetic_2: obspy.core.trace.Trace + :param synthetic_2: second synthetic waveform to calculate adjoint sources + from station pairs + :type windows_2: list of tuples + :param windows_2: [(left, right),...] representing left and right window + borders to be tapered in units of seconds since first sample in data + array. Used to window `observed_2` and `synthetic_2` + """ + assert(config.__class__.__name__ == "ConfigWaveform"), \ + "Incorrect configuration class passed to Waveform misfit" + + choices = ["waveform", "convolution", "waveform_dd", "convolution_dd"] + assert choice in choices, f"`choice` must be in {choices}, not {choice}" + logger.info(f"performing waveform caluclation with choice: `{choice}`") + + # Dictionary of values to be used to fill out the adjoint source class + ret_val = {} + + # List of windows and some measurement values for each + win_stats = [] + + # Initiate constants and empty return values to fill + nlen_data = len(synthetic.data) + dt = synthetic.stats.delta + adj = np.zeros(nlen_data) + misfit_sum = 0.0 + + # Sets up for double difference misfit types + if "dd" in choice: + adj_2 = np.zeros(nlen_data) + + # Loop over time windows and calculate misfit for each window range + for i, window in enumerate(windows): + left_sample, right_sample, nlen = get_window_info(window, dt) + + d = np.zeros(nlen) + s = np.zeros(nlen) + + d[0: nlen] = observed.data[left_sample: right_sample] + s[0: nlen] = synthetic.data[left_sample: right_sample] + + # Adjoint sources will need some kind of windowing taper to remove kinks + # at window start and end times + window_taper(d, taper_percentage=config.taper_percentage, + taper_type=config.taper_type) + window_taper(s, taper_percentage=config.taper_percentage, + taper_type=config.taper_type) + + # Prepare double difference waveforms if requested. + # Repeat the steps above for second set of waveforms + if "dd" in choice: + left_sample_2, right_sample_2, nlen_2 = \ + get_window_info(windows_2[i], dt) + + d_2 = np.zeros(nlen) + s_2 = np.zeros(nlen) + + d_2[0: nlen_2] = observed_2.data[left_sample_2: right_sample_2] + s_2[0: nlen_2] = \ + synthetic_2.data[left_sample_2: right_sample_2] + + # Taper DD measurements + window_taper(d_2, taper_percentage=config.taper_percentage, + taper_type=config.taper_type) + window_taper(s_2, taper_percentage=config.taper_percentage, + taper_type=config.taper_type) + + # Diff the two sets of waveforms + if choice == "waveform_dd": + diff = (s - s_2) - (d - d_2) + # Convolve the two sets of waveforms + elif choice == "convolution_dd": + diff = np.convolve(s, d_2, "same") - np.convolve(d, s_2, "same") + # Check at the top of function should avoid this + else: + raise NotImplementedError + # Addressing a single set of waveforms + else: + # Convolve the two waveforms + if choice == "convolution": + diff = np.convolve(s, d, "same") + # Difference the two waveforms + elif choice == "waveform": + diff = s - d + else: + raise NotImplementedError + + # Integrate with the composite Simpson's rule. + misfit_win = 0.5 * simps(y=diff**2, dx=dt) + misfit_sum += misfit_win + + # Taper again for smooth connection of windows adjoint source + # with the full adjoint source + window_taper(diff, taper_percentage=config.taper_percentage, + taper_type=config.taper_type) + + # Include some information about each window's total misfit, + # since its already calculated + win_stats.append( + {"type": choice, "left": left_sample * dt, + "right": right_sample * dt, "misfit": misfit_win, + "difference": np.mean(diff)} + ) + adj[left_sample: right_sample] = diff[0:nlen] + + # If doing differential measurements, add some information about + # second set of waveforms + if "dd" in choice: + win_stats[i]["right_2"] = right_sample_2 * dt + win_stats[i]["left_2"] = left_sample_2 * dt + adj_2[left_sample_2: right_sample_2] = -1 * diff[0:nlen_2] + + # Finally, set the return dictionary + ret_val["misfit"] = misfit_sum + ret_val["window_stats"] = win_stats + ret_val["adjoint_source"] = adj[::-1] + if "dd" in choice: + ret_val["adjoint_source_2"] = adj_2[::-1] + + return ret_val diff --git a/pyadjoint/config.py b/pyadjoint/config.py new file mode 100644 index 0000000..e451fc1 --- /dev/null +++ b/pyadjoint/config.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +""" +Configuration object for Pyadjoint. + +:authors: + adjTomo Dev Team (adjtomo@gmail.com), 2022 + Youyi Ruan (youyir@princeton.edu), 2016 + Lion Krischer (krischer@geophysik.uni-muenchen.de), 2016 +:license: + GNU General Public License, Version 3 + (http://www.gnu.org/copyleft/gpl.html) +""" +from pyadjoint import discover_adjoint_sources +from pyadjoint.utils.signal import TAPER_COLLECTION + + +def get_config(adjsrc_type, min_period, max_period, **kwargs): + """ + Defines two common parameters for all configuration objects and then + reassigns self to a sub Config class which dictates its own required + parameters + """ + adjsrc_type = adjsrc_type.lower() # allow for case-insensitivity + adjsrc_types = discover_adjoint_sources().keys() + + assert(min_period < max_period), f"`min_period` must be < `max_period`" + + if adjsrc_type == "waveform_misfit": + cfg = ConfigWaveform(min_period, max_period, **kwargs) + elif adjsrc_type == "exponentiated_phase_misfit": + cfg = ConfigExponentiatedPhase(min_period, max_period, **kwargs) + elif adjsrc_type == "cc_traveltime_misfit": + cfg = ConfigCCTraveltime(min_period, max_period, **kwargs) + elif adjsrc_type == "multitaper_misfit": + cfg = ConfigMultitaper(min_period, max_period, **kwargs) + else: + raise NotImplementedError(f"adjoint source type must be in " + f"{adjsrc_types}") + + # Set the adjoint source type as an attribute for check functions and plots + cfg.adjsrc_type = adjsrc_type + + # Perform some parameter checks + if "measure_type" in vars(cfg): + assert(cfg.measure_type in ["dt", "am"]), \ + "`measure_type` must be 'dt' or 'am'" + assert(cfg.taper_type in TAPER_COLLECTION), \ + f"`taper_type` must be in {TAPER_COLLECTION}" + + return cfg + + +class ConfigWaveform: + """ + Waveform misfit function required parameters + + :param min_period: Minimum period of the filtered input data in seconds. + :type min_period: float + :param max_period: Maximum period of the filtered input data in seconds. + :type max_period: float + :param taper_percentage: Percentage of a time window needs to be + tapered at two ends, to remove the non-zero values for adjoint + source and for fft. + :type taper_percentage: float + :param taper_type: Taper type, see `pyaadjoint.utils.TAPER_COLLECTION` + for a list of available taper types + :type taper_type: str + """ + def __init__(self, min_period, max_period, taper_type="hann", + taper_percentage=0.3): + self.min_period = min_period + self.max_period = max_period + self.taper_type = taper_type + self.taper_percentage = taper_percentage + + +class ConfigExponentiatedPhase: + """ + Exponentiated Phase misfit function required parameters + + :param min_period: Minimum period of the filtered input data in seconds. + :type min_period: float + :param max_period: Maximum period of the filtered input data in seconds. + :type max_period: float + :param taper_percentage: Percentage of a time window needs to be + tapered at two ends, to remove the non-zero values for adjoint + source and for fft. + :type taper_percentage: float + :param taper_type: Taper type, see `pyaadjoint.utils.TAPER_COLLECTION` + for a list of available taper types + :type taper_type: str + :param wtr_env: float + :param wtr_env: window taper envelope amplitude scaling + """ + def __init__(self, min_period, max_period, taper_type="hann", + taper_percentage=0.3, wtr_env=0.2): + self.min_period = min_period + self.max_period = max_period + self.taper_type = taper_type + self.taper_percentage = taper_percentage + self.wtr_env = wtr_env + + +class ConfigCCTraveltime: + """ + Cross-correlation Traveltime misfit function required parameters + + :param min_period: Minimum period of the filtered input data in seconds. + :type min_period: float + :param max_period: Maximum period of the filtered input data in seconds. + :type max_period: float + :param taper_percentage: Percentage of a time window needs to be + tapered at two ends, to remove the non-zero values for adjoint + source and for fft. + :type taper_percentage: float + :param taper_type: Taper type, see `pyaadjoint.utils.TAPER_COLLECTION` + for a list of available taper types + :type taper_type: str + :param measure_type: measurement type used in calculation of misfit, + dt(travel time), am(dlnA), wf(full waveform) + :param measure_type: string + :param use_cc_error: use cross correlation errors for normalization + :type use_cc_error: bool + :param dt_sigma_min: minimum travel time error allowed + :type dt_sigma_min: float + :param dlna_sigma_min: minimum amplitude error allowed + :type dlna_sigma_min: float + """ + def __init__(self, min_period, max_period, taper_type="hann", + taper_percentage=0.3, measure_type="dt", use_cc_error=True, + dt_sigma_min=1.0, dlna_sigma_min=0.5): + self.min_period = min_period + self.max_period = max_period + self.taper_type = taper_type + self.taper_percentage = taper_percentage + self.measure_type = measure_type + self.use_cc_error = use_cc_error + self.dt_sigma_min = dt_sigma_min + self.dlna_sigma_min = dlna_sigma_min + + +class ConfigMultitaper: + """ + Multitaper misfit function required parameters + + :param min_period: Minimum period of the filtered input data in seconds. + :type min_period: float + :param max_period: Maximum period of the filtered input data in seconds. + :type max_period: float + :param taper_percentage: Percentage of a time window needs to be + tapered at two ends, to remove the non-zero values for adjoint + source and for fft. + :type taper_percentage: float + :param taper_type: Taper type, see `pyaadjoint.utils.TAPER_COLLECTION` + for a list of available taper types + :type taper_type: str + :param measure_type: measurement type used in calculation of misfit, + dt(travel time), am(dlnA), wf(full waveform) + :type measure_type: str + :param use_cc_error: use cross correlation errors for normalization + :type use_cc_error: bool + :param use_mt_error: use multi-taper error for normalization + :type use_mt_error: bool + :param dt_sigma_min: minimum travel time error allowed + :type dt_sigma_min: float + :param dlna_sigma_min: minimum amplitude error allowed + :type dlna_sigma_min: float + :param lnpt: power index to determine the time length use in FFT + (2^lnpt) + :type lnpt: int + :param transfunc_waterlevel: Water level on the transfer function + between data and synthetic. + :type transfunc_waterlevel: float + :param water_threshold: the triggering value to stop the search. If + the spectra is larger than 10*water_threshold it will trigger the + search again, works like the heating thermostat. + :type water_threshold: float + :param ipower_costaper: order of cosine taper, higher the value, + steeper the shoulders. + :type ipower_costaper: int + :param min_cycle_in_window: Minimum cycle of a wave in time window to + determin the maximum period can be reliably measured. + :type min_cycle_in_window: int + :param mt_nw: bin width of multitapers (nw*df is the half + bandwidth of multitapers in frequency domain, + typical values are 2.5, 3., 3.5, 4.0) + :type mt_nw: float + :param num_taper: number of eigen tapers (2*nw - 3 gives tapers + with eigen values larger than 0.96) + :type num_taper: int + :param dt_fac: percentage of wave period at which measurement range is + too large and MTM reverts to CCTM misfit + :type dt_fac: float + :param err_fac: percentange of error at which error is too large + :type err_fac: float + :param dt_max_scale: used to calculate maximum allowable time shift + :type dt_max_scale: float + :param phase_step: maximum step for cycle skip correction (?) + :type phase_step: float + """ + def __init__(self, min_period, max_period, lnpt=15, + transfunc_waterlevel=1.0E-10, water_threshold=0.02, + ipower_costaper=10, min_cycle_in_window=0.5, taper_type="hann", + taper_percentage=0.3, mt_nw=4.0, num_taper=5, dt_fac=2.0, + phase_step=1.5, err_fac=2.5, dt_max_scale=3.5, + measure_type="dt", dt_sigma_min=1.0, dlna_sigma_min=0.5, + use_cc_error=True, use_mt_error=False): + self.min_period = min_period + self.max_period = max_period + self.taper_type = taper_type + self.taper_percentage = taper_percentage + self.measure_type = measure_type + self.use_cc_error = use_cc_error + self.dt_sigma_min = dt_sigma_min + self.dlna_sigma_min = dlna_sigma_min + + self.use_mt_error = use_mt_error + self.lnpt = lnpt + self.transfunc_waterlevel = transfunc_waterlevel + self.water_threshold = water_threshold + self.ipower_costaper = ipower_costaper + self.min_cycle_in_window = min_cycle_in_window + self.mt_nw = mt_nw + self.num_taper = num_taper + self.phase_step = phase_step + self.dt_fac = dt_fac + self.err_fac = err_fac + self.dt_max_scale = dt_max_scale + diff --git a/src/pyadjoint/example_data/instaseis_data.mseed b/pyadjoint/example_data/instaseis_data.mseed similarity index 100% rename from src/pyadjoint/example_data/instaseis_data.mseed rename to pyadjoint/example_data/instaseis_data.mseed diff --git a/src/pyadjoint/example_data/observed_processed.mseed b/pyadjoint/example_data/observed_processed.mseed similarity index 100% rename from src/pyadjoint/example_data/observed_processed.mseed rename to pyadjoint/example_data/observed_processed.mseed diff --git a/src/pyadjoint/example_data/shakemovie_data.mseed b/pyadjoint/example_data/shakemovie_data.mseed similarity index 100% rename from src/pyadjoint/example_data/shakemovie_data.mseed rename to pyadjoint/example_data/shakemovie_data.mseed diff --git a/src/pyadjoint/example_data/synthetic_processed.mseed b/pyadjoint/example_data/synthetic_processed.mseed similarity index 100% rename from src/pyadjoint/example_data/synthetic_processed.mseed rename to pyadjoint/example_data/synthetic_processed.mseed diff --git a/pyadjoint/main.py b/pyadjoint/main.py new file mode 100644 index 0000000..e8afaee --- /dev/null +++ b/pyadjoint/main.py @@ -0,0 +1,299 @@ +""" +Main processing scripts to calculate adjoint sources based on two waveforms +""" + +import inspect +import matplotlib.pyplot as plt +import matplotlib.patches as patches +import numpy as np +import os +import obspy +import warnings + +from pyadjoint import PyadjointError, PyadjointWarning, discover_adjoint_sources +from pyadjoint.adjoint_source import AdjointSource +from pyadjoint.utils.signal import sanity_check_waveforms + + +def calculate_adjoint_source(observed, synthetic, config, windows, plot=False, + plot_filename=None, choice=None, observed_2=None, + synthetic_2=None, windows_2=None, **kwargs): + """ + Central function of Pyadjoint used to calculate adjoint sources and misfit. + + This function uses the notion of observed and synthetic data to offer a + nomenclature most users are familiar with. Please note that it is + nonetheless independent of what the two data arrays actually represent. + + The function tapers the data from ``left_window_border`` to + ``right_window_border``, both in seconds since the first sample in the + data arrays. + + :param observed: The observed data. + :type observed: :class:`obspy.core.trace.Trace` + :param synthetic: The synthetic data. + :type synthetic: :class:`obspy.core.trace.Trace` + :param config: :class:`pyadjoint.config.Config` + :type config: configuration parameters that control measurement + :type windows: list of tuples + :param windows: [(left, right),...] representing left and right window + borders to be tapered in units of seconds since first sample in data + array + :param plot: Also produce a plot of the adjoint source. This will force + the adjoint source to be calculated regardless of the value of + ``adjoint_src``. + :type plot: bool or empty :class:`matplotlib.figure.Figure` instance + :param plot_filename: If given, the plot of the adjoint source will be + saved there. Only used if ``plot`` is ``True``. + :type plot_filename: str + :type choice: str + :param choice: Flag to turn on station pair calculations. Requires + `observed_2`, `synthetic_2`, `windows_2`. Available: + - 'double_difference': Double difference waveform misfit from + Yuan et al. 2016 + - 'convolved': Waveform convolution misfit from Choi & Alkhalifah (2011) + :type observed_2: obspy.core.trace.Trace + :param observed_2: second observed waveform to calculate adjoint sources + from station pairs + :type synthetic_2: obspy.core.trace.Trace + :param synthetic_2: second synthetic waveform to calculate adjoint sources + from station pairs + :type windows_2: list of tuples + :param windows_2: [(left, right),...] representing left and right window + borders to be tapered in units of seconds since first sample in data + array. Used to window `observed_2` and `synthetic_2` + """ + observed, synthetic = sanity_check_waveforms(observed, synthetic) + + # Check to see if we're doing double difference + if choice is not None and ("dd" in choice or "double_difference" in choice): + for check in [observed_2, synthetic_2, windows_2]: + assert(check is not None), ( + f"setting `choice` requires `observed_2`, `synthetic_2`, " + f"and `windows_2`") + observed_2, synthetic_2 = sanity_check_waveforms(observed_2, + synthetic_2) + + # Require adjoint source to be saved if we're plotting + if plot: + assert(plot_filename is not None), f"`plot` requires `plot_filename`" + adjoint_src = True + + # Get number of samples now as the adjoint source calculation function + # are allowed to mess with the trace objects. + npts = observed.stats.npts + adj_srcs = discover_adjoint_sources() + + # From here on out we use this generic function to describe adjoint source + fct = adj_srcs[config.adjsrc_type] + + # Main processing function, calculate adjoint source here + ret_val = fct(observed=observed, synthetic=synthetic, config=config, + windows=windows, choice=choice, observed_2=observed_2, + synthetic_2=synthetic_2, windows_2=windows_2, **kwargs) + + # Generate figure from the adjoint source + if plot: + figure = plt.figure(figsize=(12, 6)) + + # Plot the adjoint source, window and waveforms + plot_adjoint_source(observed, synthetic, ret_val["adjoint_source"], + ret_val["misfit"], windows, config.adjsrc_type) + figure.savefig(plot_filename) + plt.show() + plt.close("all") + + # Plot the double-difference figure if requested + if choice == "double_difference": + figure = plt.figure(figsize=(12, 6)) + plot_adjoint_source(observed_2, synthetic_2, + ret_val["adjoint_source_2"], + ret_val["misfit"], windows_2, + f"{config.adjsrc_type}_2") + fid, ext = os.path.splitext(plot_filename) + figure.savefig(f"{fid}_2{ext}") + plt.show() + plt.close("all") + + # Get misfit and warn for a negative one. + misfit = float(ret_val["misfit"]) + if misfit < 0.0: + warnings.warn("Negative misfit value not expected", PyadjointWarning) + + if "adjoint_source" not in ret_val: + raise PyadjointError("The actual adjoint source was not calculated " + "by the underlying function although it was " + "requested.") + if choice == "double_difference": + try: + assert("adjoint_source_2" in ret_val) + except AssertionError: + raise PyadjointError("The double difference adjoint source was not " + "calculated by the underlying function " + "although it was requested.") + + # Be very defensive and check all the returned parts of the adjoint source. + # This assures future adjoint source types can be integrated smoothly. + adjoint_sources = [ret_val["adjoint_source"]] + # Allow checking double difference adjoint source if present + if "adjoint_source_2" in ret_val: + adjoint_sources.append(ret_val["adjoint_source_2"]) + + for adjoint_source in adjoint_sources: + if not isinstance(adjoint_source, np.ndarray) or \ + adjoint_source.dtype != np.float64: + raise PyadjointError("The adjoint source calculated by the " + "underlying function is no numpy array " + "with a `float64` dtype.") + if len(adjoint_source.shape) != 1: + raise PyadjointError( + "The underlying function returned at adjoint source with " + f"shape {adjoint_source.shape}. It must return a " + "one-dimensional array.") + if len(adjoint_source) != npts: + raise PyadjointError( + f"The underlying function returned an adjoint source with " + f"{len(adjoint_source)} samples. It must return a function " + f"with {npts} samples which is the sample count of the " + f"input data.") + # Make sure the data returned has no infs or NaNs. + if not np.isfinite(adjoint_source).all(): + raise PyadjointError( + "The underlying function returned an adjoint source with " + "either NaNs or Inf values. This must not be.") + + adjsrc = AdjointSource( + config.adjsrc_type, misfit=misfit, dt=observed.stats.delta, + adjoint_source=ret_val["adjoint_source"], windows=windows, + min_period=config.min_period, max_period=config.max_period, + network=observed.stats.network, station=observed.stats.station, + component=observed.stats.channel, location=observed.stats.location, + starttime=observed.stats.starttime, window_stats=ret_val["window_stats"] + ) + if "adjoint_source_2" in ret_val: + adjsrc_2 = AdjointSource( + adjsrc_type=config.adjsrc_type, misfit=misfit, + dt=observed.stats.delta, + adjoint_source=ret_val["adjoint_source_2"], windows=windows_2, + min_period=config.min_period, max_period=config.max_period, + network=observed_2.stats.network, + station=observed_2.stats.station, + component=observed_2.stats.channel, + location=observed_2.stats.location, + starttime=observed.stats.starttime, + window_stats=ret_val["window_stats"] + ) + return adjsrc, adjsrc_2 + else: + return adjsrc + + +def plot_adjoint_source(observed, synthetic, adjoint_source, misfit, windows, + adjoint_source_name): + """ + Generic plotting function for adjoint sources and data. + + Many types of adjoint sources can be represented in the same manner. + This is a convenience function that can be called by different + the implementations for different adjoint sources. + + :param observed: The observed data. + :type observed: :class:`obspy.core.trace.Trace` + :param synthetic: The synthetic data. + :type synthetic: :class:`obspy.core.trace.Trace` + :param adjoint_source: The adjoint source. + :type adjoint_source: `numpy.ndarray` + :param misfit: The associated misfit value. + :type misfit: float + :type windows: list of tuples + :param windows: [(left, right),...] representing left and right window + borders to be tapered in units of seconds since first sample in data + array + :param adjoint_source_name: The name of the adjoint source. + :type adjoint_source_name: str + """ + x_range = observed.stats.endtime - observed.stats.starttime + left_window_border = 60000. + right_window_border = 0. + + for window in windows: + left_window_border = min(left_window_border, window[0]) + right_window_border = max(right_window_border, window[1]) + + buf = (right_window_border - left_window_border) * 0.3 + left_window_border -= buf + right_window_border += buf + left_window_border = max(0, left_window_border) + right_window_border = min(x_range, right_window_border) + + plt.subplot(211) + plt.plot(observed.times(), observed.data, color="0.2", label="Observed", + lw=2) + plt.plot(synthetic.times(), synthetic.data, color="#bb474f", + label="Synthetic", lw=2) + for window in windows: + re = patches.Rectangle((window[0], plt.ylim()[0]), + window[1] - window[0], + plt.ylim()[1] - plt.ylim()[0], + color="blue", alpha=0.1) + plt.gca().add_patch(re) + + plt.grid() + plt.legend(fancybox=True, framealpha=0.5) + plt.xlim(left_window_border, right_window_border) + + # Determine min and max amplitudes within the time window + obs_win = observed.data[int(left_window_border):int(right_window_border)] + syn_win = synthetic.data[int(left_window_border):int(right_window_border)] + ylim = max([max(np.abs(obs_win)), max(np.abs(syn_win))]) + plt.ylim(-ylim, ylim) + + plt.subplot(212) + plt.plot(observed.times(), adjoint_source[::-1], color="#2f8d5b", lw=2, + label="Adjoint Source") + plt.grid() + plt.legend(fancybox=True, framealpha=0.5) + + # No time reversal for comparison with data + plt.xlim(left_window_border, right_window_border) + plt.xlabel("Time (seconds)") + ylim = max(map(abs, plt.ylim())) + plt.ylim(-ylim, ylim) + + plt.suptitle("%s Adjoint Source with a Misfit of %.3g" % ( + adjoint_source_name, misfit)) + + +def get_example_data(): + """ + Helper function returning example data for Pyadjoint. + + The returned data is fully preprocessed and ready to be used with Pyadjoint. + + :returns: Tuple of observed and synthetic streams + :rtype: tuple of :class:`obspy.core.stream.Stream` objects + + .. rubric:: Example + + >>> from pyadjoint import get_example_data + >>> observed, synthetic = get_example_data() + >>> print(observed) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE + 3 Trace(s) in Stream: + SY.DBO.S3.MXR | 2014-11-15T02:31:50.259999Z - ... | 1.0 Hz, 3600 samples + SY.DBO.S3.MXT | 2014-11-15T02:31:50.259999Z - ... | 1.0 Hz, 3600 samples + SY.DBO.S3.MXZ | 2014-11-15T02:31:50.259999Z - ... | 1.0 Hz, 3600 samples + >>> print(synthetic) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE + 3 Trace(s) in Stream: + SY.DBO..LXR | 2014-11-15T02:31:50.259999Z - ... | 1.0 Hz, 3600 samples + SY.DBO..LXT | 2014-11-15T02:31:50.259999Z - ... | 1.0 Hz, 3600 samples + SY.DBO..LXZ | 2014-11-15T02:31:50.259999Z - ... | 1.0 Hz, 3600 samples + """ + path = os.path.join( + os.path.dirname(inspect.getfile(inspect.currentframe())), + "example_data") + observed = obspy.read(os.path.join(path, "observed_processed.mseed")) + observed.sort() + synthetic = obspy.read(os.path.join(path, "synthetic_processed.mseed")) + synthetic.sort() + + return observed, synthetic diff --git a/src/pyadjoint/tests/__init__.py b/pyadjoint/tests/__init__.py similarity index 100% rename from src/pyadjoint/tests/__init__.py rename to pyadjoint/tests/__init__.py diff --git a/pyadjoint/tests/test_adjsrcs.py b/pyadjoint/tests/test_adjsrcs.py new file mode 100644 index 0000000..e98630d --- /dev/null +++ b/pyadjoint/tests/test_adjsrcs.py @@ -0,0 +1,246 @@ +""" +Test generalized adjoint source generation for each type +""" +import pytest +import numpy as np +from pyadjoint import calculate_adjoint_source, get_config +from pyadjoint import get_example_data + + +# plot constant to create figures for comparison +PLOT = False +path = "./" +# Logger useful for debugging, set here +if True: + from pyadjoint import logger + logger.setLevel("DEBUG") + + +@pytest.fixture +def example_data(): + """Return example data to be used to test adjoint sources""" + obs, syn = get_example_data() + obs = obs.select(component="Z")[0] + syn = syn.select(component="Z")[0] + + return obs, syn + + +@pytest.fixture +def example_2_data(): + """ + Return example data to be used to test adjoint source double difference + calculations. Simply grabs the R component to provide a different waveform + """ + obs, syn = get_example_data() + obs = obs.select(component="R")[0] + syn = syn.select(component="R")[0] + + return obs, syn + + +@pytest.fixture +def example_window(): + """Defines an example window where misfit can be quantified""" + return [[2076., 2418.0]] + + +def test_waveform_misfit(example_data, example_window): + """ + Test the waveform misfit function + """ + obs, syn = example_data + cfg = get_config(adjsrc_type="waveform_misfit", min_period=30., + max_period=75.) + adjsrc = calculate_adjoint_source( + observed=obs, synthetic=syn, config=cfg, + windows=example_window, plot=PLOT, choice="waveform", + plot_filename=f"{path}/waveform_misfit.png" + ) + assert adjsrc.adjoint_source.any() + assert adjsrc.misfit >= 0.0 + assert len(adjsrc.windows) == 1 + assert isinstance(adjsrc.adjoint_source, np.ndarray) + + +def test_waveform_dd_misfit(example_data, example_2_data, example_window): + """ + Test the waveform misfit function + """ + obs, syn = example_data + obs_2, syn_2 = example_2_data + cfg = get_config(adjsrc_type="waveform_misfit", min_period=30., + max_period=75.) + adjsrcs = calculate_adjoint_source( + observed=obs, synthetic=syn, config=cfg, + windows=example_window, plot=PLOT, + plot_filename=f"{path}/waveform_2_misfit.png", + choice="waveform_dd", observed_2=obs_2, synthetic_2=syn_2, + windows_2=example_window + ) + assert(len(adjsrcs) == 2) + for adjsrc in adjsrcs: + assert adjsrc.adjoint_source.any() + assert adjsrc.misfit >= 0.0 + assert len(adjsrc.windows) == 1 + assert isinstance(adjsrc.adjoint_source, np.ndarray) + +def test_convolved_waveform_misfit(example_data, example_window): + """ + Test the waveform misfit function + """ + obs, syn = example_data + cfg = get_config(adjsrc_type="waveform_misfit", min_period=30., + max_period=75.) + adjsrc = calculate_adjoint_source( + observed=obs, synthetic=syn, config=cfg, + windows=example_window, plot=PLOT, + plot_filename=f"{path}/conv_misfit.png", + choice="convolution", + ) + assert adjsrc.adjoint_source.any() + assert adjsrc.misfit >= 0.0 + assert len(adjsrc.windows) == 1 + assert isinstance(adjsrc.adjoint_source, np.ndarray) + +def test_dd_convolved_waveform_misfit(example_data, example_2_data, + example_window): + """ + Test the waveform misfit function + """ + obs, syn = example_data + obs_2, syn_2 = example_2_data + cfg = get_config(adjsrc_type="waveform_misfit", min_period=30., + max_period=75.) + adjsrcs = calculate_adjoint_source( + observed=obs, synthetic=syn, config=cfg, + windows=example_window, plot=PLOT, + plot_filename=f"{path}/conv_dd_misfit.png", + choice="convolution_dd", observed_2=obs_2, synthetic_2=syn_2, + windows_2=example_window + ) + + assert(len(adjsrcs) == 2) + for adjsrc in adjsrcs: + assert adjsrc.adjoint_source.any() + assert adjsrc.misfit >= 0.0 + assert len(adjsrc.windows) == 1 + assert isinstance(adjsrc.adjoint_source, np.ndarray) + + +def test_cc_traveltime_misfit(example_data, example_window): + """ + Test the waveform misfit function + """ + obs, syn = example_data + cfg = get_config(adjsrc_type="cc_traveltime_misfit", min_period=30., + max_period=75.) + adjsrc = calculate_adjoint_source( + observed=obs, synthetic=syn, + config=cfg, windows=example_window, plot=PLOT, + plot_filename=f"{path}/cctm.png", + ) + + assert adjsrc.adjoint_source.any() + assert adjsrc.misfit >= 0.0 + assert len(adjsrc.windows) == 1 + assert isinstance(adjsrc.adjoint_source, np.ndarray) + + +def test_dd_cc_traveltime_misfit(example_data, example_2_data, example_window): + """ + Test the waveform misfit function + """ + obs, syn = example_data + obs_2, syn_2 = example_2_data + cfg = get_config(adjsrc_type="cc_traveltime_misfit", min_period=30., + max_period=75.) + adjsrcs = calculate_adjoint_source( + observed=obs, synthetic=syn, + config=cfg, windows=example_window, plot=PLOT, + plot_filename=f"{path}/dd_cctm.png", + choice="double_difference", observed_2=obs_2, synthetic_2=syn_2, + windows_2=example_window + ) + + assert(len(adjsrcs) == 2) + for adjsrc in adjsrcs: + assert adjsrc.adjoint_source.any() + assert adjsrc.misfit >= 0.0 + assert len(adjsrc.windows) == 1 + assert isinstance(adjsrc.adjoint_source, np.ndarray) + + +def test_multitaper_misfit(example_data, example_window): + """ + Test the waveform misfit function + """ + + obs, syn = example_data + cfg = get_config(adjsrc_type="multitaper_misfit", min_period=30., + max_period=75., min_cycle_in_window=3., + use_cc_error=False) + + adjsrc = calculate_adjoint_source( + observed=obs, synthetic=syn, + config=cfg, windows=example_window, plot=False, + plot_filename=f"{path}/multitaper_misfit.png" + ) + + assert adjsrc.adjoint_source.any() + assert adjsrc.misfit >= 0.0 + # Make sure the adj src successfully uses MTM measurement, does not fall + # back to cross correlation traveltime + for stats in adjsrc.window_stats: + assert(stats["type"] == "multitaper") + + assert isinstance(adjsrc.adjoint_source, np.ndarray) + + +def test_dd_multitaper_misfit(example_data, example_2_data, example_window): + """ + Test the waveform misfit function + """ + obs, syn = example_data + obs_2, syn_2 = example_2_data + cfg = get_config(adjsrc_type="multitaper_misfit", min_period=30., + max_period=75., min_cycle_in_window=3., + use_cc_error=False) + + adjsrcs = calculate_adjoint_source( + observed=obs, synthetic=syn, + config=cfg, windows=example_window, plot=PLOT, + plot_filename=f"{path}/dd_multitaper_misfit.png", + choice="double_difference", observed_2=obs_2, synthetic_2=syn_2, + windows_2=example_window + ) + + assert(len(adjsrcs) == 2) + for adjsrc in adjsrcs: + assert adjsrc.adjoint_source.any() + assert adjsrc.misfit >= 0.0 + assert len(adjsrc.windows) == 1 + for stats in adjsrc.window_stats: + assert(stats["type"] == "dd_multitaper") + assert isinstance(adjsrc.adjoint_source, np.ndarray) + + +def test_exponentiated_phase_misfit(example_data, example_window): + """ + Test the waveform misfit function + """ + obs, syn = example_data + cfg = get_config(adjsrc_type="exponentiated_phase_misfit", min_period=30., + max_period=75.) + + adjsrc = calculate_adjoint_source( + observed=obs, synthetic=syn, + config=cfg, windows=example_window, plot=PLOT, + plot_filename=f"{path}/exp_phase_misfit.png" + ) + + assert adjsrc.adjoint_source.any() + assert adjsrc.misfit >= 0.0 + assert len(adjsrc.windows) == 1 + assert isinstance(adjsrc.adjoint_source, np.ndarray) + diff --git a/pyadjoint/tests/test_config.py b/pyadjoint/tests/test_config.py new file mode 100644 index 0000000..de32af3 --- /dev/null +++ b/pyadjoint/tests/test_config.py @@ -0,0 +1,36 @@ +""" +Test suite for Config class +""" +import pytest +from pyadjoint import get_config +from pyadjoint import discover_adjoint_sources + + +def test_all_configs(): + """Test importing all configs based on available types""" + adj_src_types = discover_adjoint_sources().keys() + for adj_src_type in adj_src_types: + get_config(adjsrc_type=adj_src_type, min_period=1, max_period=10) + + +def test_config_set_correctly(): + """Just make sure that choosing a specific adjoint source type exposes + the correct parameters + """ + cfg = get_config(adjsrc_type="multitaper_misfit", min_period=1, + max_period=10) + # Unique parameter for MTM + assert(hasattr(cfg, "phase_step")) + + +def test_incorrect_inputs(): + """ + Make sure that incorrect input parameters are error'd + """ + with pytest.raises(NotImplementedError): + get_config(adjsrc_type="cc_timetraveling_muskrat", min_period=1, + max_period=10) + + with pytest.raises(AssertionError): + get_config(adjsrc_type="cc_traveltime_misfit", min_period=10, + max_period=1) diff --git a/src/pyadjoint/tests/test_utils.py b/pyadjoint/tests/test_utils.py similarity index 92% rename from src/pyadjoint/tests/test_utils.py rename to pyadjoint/tests/test_utils.py index 1b8f53b..f11513e 100644 --- a/src/pyadjoint/tests/test_utils.py +++ b/pyadjoint/tests/test_utils.py @@ -1,19 +1,17 @@ -#!/usr/bin/env pythonG -# -*- coding: utf-8 -*- +#!/usr/bin/env python3 """ Tests for the utility functions. -:copyright: +:authors: + adjTomo Dev Team (adjtomo@gmail.com), 2023 Lion Krischer (krischer@geophysik.uni-muenchen.de), 2015 :license: BSD 3-Clause ("BSD New" or "BSD Simplified") """ -from __future__ import absolute_import, division, print_function - import numpy as np import obspy -from pyadjoint.utils import taper_window +from pyadjoint.utils.signal import taper_window def test_taper_within_window_simple(): diff --git a/pyadjoint/utils/cctm.py b/pyadjoint/utils/cctm.py new file mode 100644 index 0000000..9941ef2 --- /dev/null +++ b/pyadjoint/utils/cctm.py @@ -0,0 +1,369 @@ +""" +General utility functions used to calculate misfit and adjoint sources for the +cross correlation traveltime misfit function +""" +import numpy as np +import warnings +from obspy.signal.cross_correlation import xcorr_pick_correction +from scipy.integrate import simps +from pyadjoint import logger + + +def calculate_cc_shift(d, s, dt, use_cc_error=True, dt_sigma_min=1.0, + dlna_sigma_min=0.5, **kwargs): + """ + Calculate cross-correlation traveltime misfit (time shift, amplitude + anomaly) and associated errors, for a given window. + This is accessed by both the CC and MTM measurement methods. + + .. note:: + Kwargs not used but allows Config class to pass relevant parameters + without explicitely naming them in the function call + + :type d: np.array + :param d: observed data to calculate cc shift and dlna + :type s: np.array + :param s: synthetic data to calculate cc shift and dlna + :type dt: float + :param dt: time sampling rate delta t units seconds + :type use_cc_error: bool + :param use_cc_error: use cross correlation errors for normalization + :type dt_sigma_min: float + :param dt_sigma_min: minimum travel time error allowed + :type dlna_sigma_min: float + :param dlna_sigma_min: minimum amplitude error allowed + :rtype: tuple (float, float, float, float) + :return: (time shift [s], amplitude anomaly, time shift error [s], + amplitude anomaly error) + """ + # Note that CC values may dramatically change with/without the tapering + ishift = xcorr_shift(d, s) # timeshift in unit samples + tshift = ishift * dt # timeshift in unit seconds + dlna = 0.5 * np.log(sum(d[:] * d[:]) / + sum(s[:] * s[:])) # amplitude anomaly + + # Uncertainty estimate based on cross-correlations to be used for norm. + if use_cc_error: + sigma_dt, sigma_dlna = calculate_cc_error(d=d, s=s, dt=dt, + cc_shift=tshift, dlna=dlna, + dt_sigma_min=dt_sigma_min, + dlna_sigma_min=dlna_sigma_min + ) + logger.debug("CC error: " + f"dt={tshift:.2f}+/-{sigma_dt:.2f}s; " + f"dlna = {dlna:.3f}+/-{sigma_dlna:.3f}" + ) + else: + sigma_dt = 1.0 + sigma_dlna = 1.0 + + return tshift, dlna, sigma_dt, sigma_dlna + + +def calculate_cc_adjsrc(s, tshift, dlna, dt, sigma_dt=1., sigma_dlna=0.5, + **kwargs): + """ + Calculate adjoint source and misfit of the cross correlation traveltime + misfit function. This is accessed by both the CC and MTM measurement + methods. + + .. note:: + Kwargs not used but allows Config class to pass relevant parameters + without explicitely naming them in the function call + + :type s: np.array + :param s: synthetic data array + :type tshift: float + :param tshift: measured time shift from `calculate_cc_shift` + :type dlna: float + :param dlna: measured amplitude anomaly from `calculate_cc_shift` + :type dt: float + :param dt: delta t, time sampling rate of `s` + :type sigma_dt: float + :param sigma_dt: traveltime error from `calculate_cc_shift` + :type sigma_dlna: float + :param sigma_dlna: amplitude anomaly error from `calculate_cc_shift` + :rtype: (float, float, np.array, np.array) + :return: (tshift misfit, dlna misfit, tshift adjsrc, dlna adjsrc) + """ + n = len(s) + + # Initialize empty arrays for memory efficiency + fp = np.zeros(n) + fq = np.zeros(n) + + # Calculate the misfit for both time shift and amplitude anomaly + misfit_p = 0.5 * (tshift / sigma_dt) ** 2 + misfit_q = 0.5 * (dlna / sigma_dlna) ** 2 + + # Calculate adjoint sources for both time shift and amplitude anomaly + dsdt = np.gradient(s, dt) + nnorm = simps(y=dsdt * dsdt, dx=dt) + # note: Princeton ver. of code has a -1 on `fp` because they have a '-' on + # `nnorm`. Current format follows original Krischer code implementation + fp[0:n] = dsdt[0:n] * tshift / nnorm / sigma_dt ** 2 + + mnorm = simps(y=s * s, dx=dt) + fq[0:n] = -1.0 * s[0:n] * dlna / mnorm / sigma_dlna ** 2 + + return misfit_p, misfit_q, fp, fq + + +def calculate_dd_cc_shift(d, s, d_2, s_2, dt, use_cc_error=True, + dt_sigma_min=1.0, dlna_sigma_min=0.5, **kwargs): + """ + Calculate double difference cross-correlation traveltime misfit + (time shift, amplitude anomaly) and associated errors, for a given window. + Slight variation on normal CC shift calculation + + TODO + - DD dlna measurement was not properly calculated in the RDNO version + + Assumes d, s, d_2 and s_2 all have the same sampling rate + + .. note:: + Kwargs not used but allows Config class to pass relevant parameters + without explicitely naming them in the function call + + :type d: np.array + :param d: observed data to calculate cc shift and dlna + :type s: np.array + :param s: synthetic data to calculate cc shift and dlna + :type dt: float + :param dt: time sampling rate delta t units seconds + :type d_2: np.array + :param d_2: 2nd pair observed data to calculate cc shift and dlna + :type s_2: np.array + :param s_2: 2nd pair synthetic data to calculate cc shift and dlna + :type use_cc_error: bool + :param use_cc_error: use cross correlation errors for normalization + :type dt_sigma_min: float + :param dt_sigma_min: minimum travel time error allowed + :type dlna_sigma_min: float + :param dlna_sigma_min: minimum amplitude error allowed + :rtype: tuple (float, float, float, float) + :return: (time shift [s], amplitude anomaly, time shift error [s], + amplitude anomaly error) + """ + # Calculate time shift between 'observed' or 'data' waveforms + ishift_obs = xcorr_shift(d, d_2) # timeshift in unit samples + tshift_obs = ishift_obs * dt # timeshift in unit seconds + + # Calculate time shift between 'synthetic' waveforms + ishift_syn = xcorr_shift(s, s_2) # timeshift in unit samples + tshift_syn = ishift_obs * dt # timeshift in unit seconds + + # Overall shift is difference between differential measurements + ishift_dd = ishift_syn - ishift_obs + tshift = ishift_dd * dt + + # FIXME: !!! This is not properly calculated as a differential !!! + dlna_obs = 0.5 * np.log(sum(d[:] * d[:]) / + sum(d_2[:] * d_2[:])) # amplitude anomaly + dlna_syn = 0.5 * np.log(sum(s[:] * s[:]) / + sum(s_2[:] * s_2[:])) # amplitude anomaly + + # Uncertainty is estimated based on DATA cross correlation + if use_cc_error: + sigma_dt, sigma_dlna = calculate_cc_error( + d=d, s=d_2, dt=dt, cc_shift=ishift_obs, dlna=dlna_obs, + dt_sigma_min=dt_sigma_min, dlna_sigma_min=dlna_sigma_min + ) + logger.debug("CC error: " + f"dt={tshift_obs:.2f}+/-{sigma_dt:.2f}s; " + f"dlna = {dlna_obs:.3f}+/-{sigma_dlna:.3f}" + ) + else: + sigma_dt = 1.0 + sigma_dlna = 1.0 + + return tshift, tshift_obs, tshift_syn, dlna_obs, dlna_syn, sigma_dt, \ + sigma_dlna + + +def calculate_dd_cc_adjsrc(s, s_2, tshift, dlna, dt, sigma_dt=1., + sigma_dlna=0.5, **kwargs): + """ + Calculate double difference cross corrrelation adjoint sources. + + TODO + - Add dlna capability to this function + + .. note:: + Kwargs not used but allows Config class to pass relevant parameters + without explicitely naming them in the function call + + :type s: np.array + :param s: synthetic data array + :type s_2: np.array + :param s_2: second synthetic data array + :type tshift: float + :param tshift: measured dd time shift from `calculate_dd_cc_shift` + :type dlna: float + :param dlna: measured dd amplitude anomaly from `calculate_dd_cc_shift` + :type dt: float + :param dt: delta t, time sampling rate of `s` + :type sigma_dt: float + :param sigma_dt: traveltime error from `calculate_cc_shift` + :type sigma_dlna: float + :param sigma_dlna: amplitude anomaly error from `calculate_cc_shift` + :rtype: (float, float, np.array, np.array, np.array, np.array) + :return: (tshift misfit, dlna misfit, tshift adjsrc, dlna adjsrc, + tshift adjsrc 2, dlna adjsrc 2) + """ + # So that we don't have to pass this in as an argument + ishift_dd = int(tshift / dt) # time shift in samples + + n = len(s) + + # Initialize empty arrays for memory efficiency + fp = np.zeros(n) # time shift + fp_2 = np.zeros(n) + + fq = np.zeros(n) # amplitude anomaly + fq_2 = np.zeros(n) + + # Calculate the misfit for both time shift and amplitude anomaly + misfit_p = 0.5 * (tshift / sigma_dt) ** 2 + misfit_q = 0.5 * (dlna / sigma_dlna) ** 2 + + # Calculate adjoint sources for both time shift and amplitude anomaly + dsdt = np.gradient(s, dt) + + # Time shift and gradient the first set of synthetics in reverse time + s_cc_dt, _ = cc_correction(s, -1 * ishift_dd, 0.) + dsdt_cc = np.gradient(s_cc_dt, dt) + + # Time shift and gradient the second of synthetics + s_2_cc_dt, _ = cc_correction(s_2, ishift_dd, 0.) + dsdt_cc_2 = np.gradient(s_2_cc_dt, dt) + + # Integrate the product of gradients + # FIXME: Is `dsdt` supposed to be `dsdt_cc`? Need to check equations + nnorm = simps(y=dsdt * dsdt_cc_2, dx=dt) + + # note: Princeton ver. of code has a -1 on `fp` because they have a '-' on + # `nnorm`. Current format follows original Krischer code implementation + fp[0:n] = -1 * dsdt_cc_2[0:n] * tshift / nnorm / sigma_dt ** 2 # -1 + fp_2[0:n] = +1 * dsdt_cc[0:n] * tshift / nnorm / sigma_dt ** 2 # +1 + + return misfit_p, misfit_q, fp, fp_2, fq, fq_2 + + +def cc_correction(s, cc_shift, dlna): + """ + Apply a correction to synthetics by shifting in time by `cc_shift` samples + and scaling amplitude by `dlna`. Provides the 'best fitting' synthetic + array w.r.t data as realized by the cross correlation misfit function + + :type s: np.array + :param s: synthetic data array + :type cc_shift: int + :param cc_shift: time shift (in samples) as calculated using cross a + cross correlation + :type dlna: float + :param dlna: amplitude anomaly as calculated by amplitude anomaly eq. + :rtype: (np.array, np.array) + :return: (time shifted synthetic array, amplitude scaled synthetic array) + """ + nlen_t = int(len(s)) + s_cc_dt = np.zeros(nlen_t) + s_cc_dtdlna = np.zeros(nlen_t) + + for index in range(0, nlen_t): + index_shift = index - int(cc_shift) + + if 0 <= index_shift < nlen_t: + # corrected by c.c. shift + s_cc_dt[index] = s[index_shift] + + # corrected by c.c. shift and amplitude + s_cc_dtdlna[index] = np.exp(dlna) * s[index_shift] + + return s_cc_dt, s_cc_dtdlna + + +def calculate_cc_error(d, s, dt, cc_shift, dlna, dt_sigma_min=1.0, + dlna_sigma_min=0.5): + """ + Estimate error for `dt` and `dlna` with uncorrelation assumption. Used for + normalization of the traveltime measurement + + :type d: np.array + :param d: observed time series array to calculate error for + :type s: np.array + :param s: synthetic time series array to calculate error for + :type dt: float + :param dt: delta t, time sampling rate + :type cc_shift: int + :param cc_shift: total amount of cross correlation time shift in samples + :type dlna: float + :param dlna: amplitude anomaly calculated for cross-correlation measurement + :type dt_sigma_min: float + :param dt_sigma_min: minimum travel time error allowed + :type dlna_sigma_min: float + :param dlna_sigma_min: minimum amplitude error allowed + """ + # Apply a scaling and time shift to the synthetic data + s_cc_dt, s_cc_dtdlna = cc_correction(s, cc_shift, dlna) + + # time derivative of s_cc (velocity) + s_cc_vel = np.gradient(s_cc_dtdlna, dt) + + # The estimated error for dt and dlna with uncorrelation assumption + sigma_dt_top = np.sum((d - s_cc_dtdlna)**2) + sigma_dt_bot = np.sum(s_cc_vel**2) + + sigma_dlna_top = sigma_dt_top + sigma_dlna_bot = np.sum(s_cc_dt**2) + + sigma_dt = np.sqrt(sigma_dt_top / sigma_dt_bot) + sigma_dlna = np.sqrt(sigma_dlna_top / sigma_dlna_bot) + + # Check that errors do not go below the pre-defined threshold value + if sigma_dt < dt_sigma_min or np.isnan(sigma_dt): + sigma_dt = dt_sigma_min + + if sigma_dlna < dlna_sigma_min or np.isnan(sigma_dlna): + sigma_dlna = dlna_sigma_min + + return sigma_dt, sigma_dlna + + +def xcorr_shift(d, s): + """ + Determine the required time shift for peak cross-correlation of two arrays + + :type d: np.array + :param d: observed time series array + :type s: np.array + :param s: synthetic time series array + """ + cc = np.correlate(d, s, mode="full") + time_shift = cc.argmax() - len(d) + 1 + return time_shift + + +def subsample_xcorr_shift(d, s): + """ + Calculate the correlation time shift around the maximum amplitude of the + synthetic trace `s` with subsample accuracy. + + :type d: obspy.core.trace.Trace + :param d: observed waveform to calculate adjoint source + :type s: obspy.core.trace.Trace + :param s: synthetic waveform to calculate adjoint source + """ + # Estimate shift and use it as a guideline for the subsample accuracy shift. + time_shift = xcorr_shift(d.data, s.data) * d.stats.delta + + # Align on the maximum amplitude of the synthetics. + pick_time = s.stats.starttime + s.data.argmax() * s.stats.delta + + # Will raise a warning if the trace ids don't match which we don't care + # about here. + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + return xcorr_pick_correction( + pick_time, s, pick_time, d, 20.0 * time_shift, + 20.0 * time_shift, 10.0 * time_shift)[0] diff --git a/src/pyadjoint/dpss.py b/pyadjoint/utils/dpss.py similarity index 63% rename from src/pyadjoint/dpss.py rename to pyadjoint/utils/dpss.py index d140764..019257b 100644 --- a/src/pyadjoint/dpss.py +++ b/pyadjoint/utils/dpss.py @@ -1,47 +1,39 @@ -#!/usr/bin/env python -# -*- encoding: utf8 -*- +#!/usr/bin/env python3 """ -# Original Author : Martin Luessi mluessi@nmr.mgh.harvard.edu (2012) -# License : BSD 3-clause - -# Largely copied from the mne-python package so credit goes to them. -https://github.com/mne-tools/mne-python/blob/master/mne/time_frequency/ -multitaper.py +Utility functions for calculating multitaper measurements (MTM). +Mainly contains functions for calculating Discrete Prolate Spheroidal Sequences +(DPSS) + +:copyright: + adjTomo Dev Team (adjtomo@gmail.com), 2022 + Lion Krischer (krischer@geophysik.uni-muenchen.de), 2016 + Martin Luessi (mluessi@nmr.mgh.harvard.edu), 2012 +License : BSD 3-clause """ -from __future__ import absolute_import, division, print_function - -import warnings - import numpy as np from scipy import fftpack, linalg, interpolate +from pyadjoint import logger + def tridisolve(d, e, b, overwrite_b=True): """ Symmetric tridiagonal system solver, from Golub and Van Loan pg 157 - Note: Copied from NiTime - - Parameters - ---------- - - d : ndarray - main diagonal stored in d[:] - e : ndarray - superdiagonal stored in e[:-1] - b : ndarray - RHS vector - - Returns - ------- - - x : ndarray - Solution to Ax = b (if overwrite_b is False). Otherwise solution is - stored in previous RHS vector b - :param b: - :param e: - :param d: - :param overwrite_b: + .. note:: + Copied from the mne-python package so credit goes to them. + https://github.com/mne-tools/mne-python/blob/master/mne/time_frequency/\ + multitaper.py + + :type d: ndarray + :param d: main diagonal stored in d[:] + :type e: ndarray + :param e: superdiagonal stored in e[:-1] + :type b: ndarray + :param b: RHS vector + :rtype x : ndarray + :return: Solution to Ax = b (if overwrite_b is False). Otherwise solution is + stored in previous RHS vector b """ n = len(b) # work vectors @@ -72,28 +64,23 @@ def tridi_inverse_iteration(d, e, w, x0=None, rtol=1e-8): Perform an inverse iteration to find the eigenvector corresponding to the given eigenvalue in a symmetric tridiagonal system. - Note: Copied from NiTime - - Parameters - ---------- - - d : ndarray - main diagonal of the tridiagonal system - e : ndarray - offdiagonal stored in e[:-1] - w : float - eigenvalue of the eigenvector - x0 : ndarray - initial point to start the iteration - rtol : float - tolerance for the norm of the difference of iterates - - Returns - ------- - - e: ndarray - The converged eigenvector - + .. note:: + Copied from the mne-python package so credit goes to them. + https://github.com/mne-tools/mne-python/blob/master/mne/time_frequency/\ + multitaper.py + + :type d: ndarray + :param d: main diagonal stored in d[:] + :type e: ndarray + :param e: off diagonal stored in e[:-1] + :type w: float + :param w: eigenvalue of the eigenvector + :type x0: ndarray + :param x0: initial point to start the iteration + :type rtol : float + :param rtol: tolerance for the norm of the difference of iterates + :rtype: ndarray + :return: The converged eigenvector """ eig_diag = d - w if x0 is None: @@ -115,14 +102,10 @@ def sum_squared(x): """ Compute norm of an array - Parameters - ---------- - x : array - Data whose norm must be found - Returns - ------- - value : float - Sum of squares of the input array X + :type x: array + :param x: Data whose norm must be found + :rtype: float + :return: Sum of squares of the input array X """ x_flat = x.ravel(order='F' if np.isfortran(x) else 'C') return np.dot(x_flat, x_flat) @@ -133,44 +116,40 @@ def dpss_windows(n, half_nbw, k_max, low_bias=True, interp_from=None, """ Returns the Discrete Prolate Spheroidal Sequences of orders [0,Kmax-1] for a given frequency-spacing multiple NW and sequence length N. - - Note: Copied from NiTime - - Parameters - ---------- - n : int - Sequence length - half_nbw : float, unitless - Standardized half bandwidth corresponding to 2 * half_bw = BW*f0 - = BW*N/dt but with dt taken as 1 - k_max : int - Number of DPSS windows to return is Kmax (orders 0 through Kmax-1) - low_bias : Bool - Keep only tapers with eigenvalues > 0.9 - interp_from : int (optional) - The dpss can be calculated using interpolation from a set of dpss - with the same NW and Kmax, but shorter N. This is the length of this - shorter set of dpss windows. - interp_kind : str (optional) - This input variable is passed to scipy.interpolate.interp1d and - specifies the kind of interpolation as a string ('linear', 'nearest', - 'zero', 'slinear', 'quadratic, 'cubic') or as an integer specifying the - order of the spline interpolator to use. - - - Returns - ------- - v, e : tuple, - v is an array of DPSS windows shaped (Kmax, N) + Rayleigh bin parameter typical values of half_nbw/nw are 2.5,3,3.5,4. + + .. note:: + Tridiagonal form of DPSS calculation from: + + Slepian, D. Prolate spheroidal wave functions, Fourier analysis, and + uncertainty V: The discrete case. Bell System Technical Journal, + Volume 57 (1978), 1371430 + + .. note:: + This function was copied from NiTime + + :type n: int + :param n: Sequence length + :type half_nbw: float + :param half_nbw: unitless standardized half bandwidth corresponding to + 2 * half_bw = BW*f0 = BW*N/dt but with dt taken as 1 + :type k_max: int + :param k_max: Number of DPSS windows to return is Kmax + (orders 0 through Kmax-1) + :type low_bias: Bool + :param low_bias: Keep only tapers with eigenvalues > 0.9 + :type interp_from: int (optional) + :param interp_from: The dpss can be calculated using interpolation from a + set of dpss with the same NW and Kmax, but shorter N. This is the + length of this shorter set of dpss windows. + :type interp_kind: str (optional) + :param interp_kind: This input variable is passed to + scipy.interpolate.interp1d and specifies the kind of interpolation as + a string ('linear', 'nearest', 'zero', 'slinear', 'quadratic, 'cubic') + or as an integer specifying the order of the spline interpolator to use. + :rtype: tuple + :return: (v, e), v is an array of DPSS windows shaped (Kmax, N), e are the eigenvalues - - Notes - ----- - Tridiagonal form of DPSS calculation from: - - Slepian, D. Prolate spheroidal wave functions, Fourier analysis, and - uncertainty V: The discrete case. Bell System Technical Journal, - Volume 57 (1978), 1371430 """ k_max = int(k_max) w_bin = float(half_nbw) / n @@ -180,10 +159,9 @@ def dpss_windows(n, half_nbw, k_max, low_bias=True, interp_from=None, # (interp_from) and then interpolate to the larger size (N) if interp_from is not None: if interp_from > n: - e_s = 'In dpss_windows, interp_from is: %s ' % interp_from - e_s += 'and N is: %s. ' % n - e_s += 'Please enter interp_from smaller than N.' - raise ValueError(e_s) + raise ValueError(f"In dpss_windows, interp_from is: {interp_from} " + f"and N is: {n}. Please enter interp_from smaller " + f"than N.") dpss = [] d, e = dpss_windows(interp_from, half_nbw, k_max, low_bias=False) for this_d in d: @@ -266,8 +244,8 @@ def dpss_windows(n, half_nbw, k_max, low_bias=True, interp_from=None, if low_bias: idx = (eigvals > 0.9) if not idx.any(): - warnings.warn('Could not properly use low_bias, ' - 'keeping lowest-bias taper') + logger.warning("Could not properly use low_bias, keeping " + "lowest-bias taper") idx = [np.argmax(eigvals)] dpss, eigvals = dpss[idx], eigvals[idx] assert len(dpss) > 0 # should never happen diff --git a/pyadjoint/utils/signal.py b/pyadjoint/utils/signal.py new file mode 100644 index 0000000..1a90791 --- /dev/null +++ b/pyadjoint/utils/signal.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 +""" +Utility functions for Pyadjoint. + +:copyright: + adjTomo Dev Team (adjtomo@gmail.com), 2022 + Lion Krischer (krischer@geophysik.uni-muenchen.de), 2015 +:license: + BSD 3-Clause ("BSD New" or "BSD Simplified") +""" +import numpy as np +import obspy +import warnings +from pyadjoint import PyadjointError, PyadjointWarning, logger + + +EXAMPLE_DATA_PDIFF = (800, 900) +EXAMPLE_DATA_SDIFF = (1500, 1600) +TAPER_COLLECTION = ('cos', 'cos_p10', 'hann', "hamming") + + +def get_window_info(window, dt): + """ + Convenience function to get window start and end times, and start and end + samples. Repeated a lot throughout package so useful to keep it defined + in one place. + + :type window: tuple, list + :param window: (left sample, right sample) borders of window in sample + :type dt: float + :param dt: delta T, time step of time series + :rtype: tuple (float, float, int) + :return: (left border in sample, right border in sample, length of window + in sample) + """ + assert(window[1] >= window[0]), f"`window` is reversed in time" + + nlen = int(np.floor((window[1] - window[0]) / dt)) + 1 # unit: sample + left_sample = int(np.floor(window[0] / dt)) + right_sample = left_sample + nlen + + return left_sample, right_sample, nlen + + +def sanity_check_waveforms(observed, synthetic): + """ + Perform a number of basic sanity checks to assure the data is valid + in a certain sense. + + It checks the types of both, the start time, sampling rate, number of + samples, etc. + + :param observed: The observed data. + :type observed: :class:`obspy.core.trace.Trace` + :param synthetic: The synthetic data. + :type synthetic: :class:`obspy.core.trace.Trace` + + :raises: :class:`~pyadjoint.PyadjointError` + """ + if not isinstance(observed, obspy.Trace): + # Also accept Stream objects. + if isinstance(observed, obspy.Stream) and \ + len(observed) == 1: + observed = observed[0] + else: + raise PyadjointError( + "Observed data must be an ObsPy Trace object.") + if not isinstance(synthetic, obspy.Trace): + if isinstance(synthetic, obspy.Stream) and \ + len(synthetic) == 1: + synthetic = synthetic[0] + else: + raise PyadjointError( + "Synthetic data must be an ObsPy Trace object.") + + if observed.stats.npts != synthetic.stats.npts: + raise PyadjointError("Observed and synthetic data must have the same " + "number of samples.") + + sr1 = observed.stats.sampling_rate + sr2 = synthetic.stats.sampling_rate + + if abs(sr1 - sr2) / sr1 >= 1E-5: + raise PyadjointError("Observed and synthetic data must have the same " + "sampling rate.") + + # Make sure data and synthetics start within half a sample interval. + if abs(observed.stats.starttime - synthetic.stats.starttime) > \ + observed.stats.delta * 0.5: + raise PyadjointError("Observed and synthetic data must have the same " + "starttime.") + + ptp = sorted([observed.data.ptp(), synthetic.data.ptp()]) + if ptp[1] / ptp[0] >= 5: + warnings.warn("The amplitude difference between data and " + "synthetic is fairly large.", PyadjointWarning) + + # Also check the components of the data to avoid silly mistakes of + # users. + if len(set([observed.stats.channel[-1].upper(), + synthetic.stats.channel[-1].upper()])) != 1: + warnings.warn("The orientation code of synthetic and observed " + "data is not equal.") + + observed = observed.copy() + synthetic = synthetic.copy() + observed.data = np.require(observed.data, dtype=np.float64, + requirements=["C"]) + synthetic.data = np.require(synthetic.data, dtype=np.float64, + requirements=["C"]) + + return observed, synthetic + + +def taper_window(trace, left_border_in_seconds, right_border_in_seconds, + taper_percentage, taper_type, **kwargs): + """ + Helper function to taper a window within a data trace. + This function modifies the passed trace object in-place. + + :param trace: The trace to be tapered. + :type trace: :class:`obspy.core.trace.Trace` + :param left_border_in_seconds: The left window border in seconds since + the first sample. + :type left_border_in_seconds: float + :param right_border_in_seconds: The right window border in seconds since + the first sample. + :type right_border_in_seconds: float + :param taper_percentage: Decimal percentage of taper at one end (ranging + from ``0.0`` (0%) to ``0.5`` (50%)). + :type taper_percentage: float + :param taper_type: The taper type, supports anything + :meth:`obspy.core.trace.Trace.taper` can use. + :type taper_type: str + + Any additional keyword arguments are passed to the + :meth:`obspy.core.trace.Trace.taper` method. + + .. rubric:: Example + + >>> import obspy + >>> tr = obspy.read()[0] + >>> tr.plot() + + .. plot:: + + import obspy + tr = obspy.read()[0] + tr.plot() + + >>> from pyadjoint.utils.signal import taper_window + >>> taper_window(tr, 4, 11, taper_percentage=0.10, taper_type="hann") + >>> tr.plot() + + .. plot:: + + import obspy + from pyadjoint.utils import taper_window + tr = obspy.read()[0] + taper_window(tr, 4, 11, taper_percentage=0.10, taper_type="hann") + tr.plot() + + """ + s, e = trace.stats.starttime, trace.stats.endtime + trace.trim(s + left_border_in_seconds, s + right_border_in_seconds) + trace.taper(max_percentage=taper_percentage, type=taper_type, **kwargs) + trace.trim(s, e, pad=True, fill_value=0.0) + # Enable method chaining. + return trace + + +def window_taper(signal, taper_percentage, taper_type): + """ + Window taper function to taper a time series with various taper functions. + Affect arrays in place but also returns the array. Both will edit the array. + + :param signal: time series + :type signal: ndarray(float) + :param taper_percentage: total percentage of taper in decimal + :type taper_percentage: float + :param taper_type: select available taper type, options are: + cos, cos_p10, hann, hamming + :type taper_type: str + :return: tapered `signal` array + :rtype: ndarray(float) + """ + # Check user inputs + if taper_type not in TAPER_COLLECTION: + raise ValueError(f"Window taper not supported, must be in " + f"{TAPER_COLLECTION}") + if taper_percentage < 0 or taper_percentage > 1: + raise ValueError("taper percentage must be 0 < % < 1") + + npts = len(signal) + if taper_percentage == 0.0 or taper_percentage == 1.0: + frac = int(npts*taper_percentage / 2.0) + else: + frac = int(npts*taper_percentage / 2.0 + 0.5) + + idx1 = frac + idx2 = npts - frac + if taper_type == "hann": + signal[:idx1] *=\ + (0.5 - 0.5 * np.cos(2.0 * np.pi * np.arange(0, frac) / + (2 * frac - 1))) + signal[idx2:] *=\ + (0.5 - 0.5 * np.cos(2.0 * np.pi * np.arange(frac, 2 * frac) / + (2 * frac - 1))) + elif taper_type == "hamming": + signal[:idx1] *=\ + (0.54 - 0.46 * np.cos(2.0 * np.pi * np.arange(0, frac) / + (2 * frac - 1))) + signal[idx2:] *=\ + (0.54 - 0.46 * np.cos(2.0 * np.pi * np.arange(frac, 2 * frac) / + (2 * frac - 1))) + elif taper_type == "cos": + power = 1. + signal[:idx1] *= np.cos(np.pi * np.arange(0, frac) / + (2 * frac - 1) - np.pi / 2.0) ** power + signal[idx2:] *= np.cos(np.pi * np.arange(frac, 2 * frac) / + (2 * frac - 1) - np.pi / 2.0) ** power + elif taper_type == "cos_p10": + power = 10. + signal[:idx1] *= 1. - np.cos(np.pi * np.arange(0, frac) / + (2 * frac - 1)) ** power + signal[idx2:] *= 1. - np.cos(np.pi * np.arange(frac, 2 * frac) / + (2 * frac - 1)) ** power + + return signal + + +def process_cycle_skipping(phi_w, nfreq_max, nfreq_min, wvec, phase_step=1.5): + """ + Check for cycle skipping by looking at the smoothness of phi + + :type phi_w: np.array + :param phi_w: phase anomaly from transfer functions + :type nfreq_min: int + :param nfreq_min: minimum frequency for suitable MTM measurement + :type nfreq_max: int + :param nfreq_max: maximum frequency for suitable MTM measurement + :type phase_step: float + :param phase_step: maximum step for cycle skip correction (?) + :type wvec: np.array + :param wvec: angular frequency array generated from Discrete Fourier + Transform sample frequencies + """ + for iw in range(nfreq_min + 1, nfreq_max - 1): + smth0 = abs(phi_w[iw + 1] + phi_w[iw - 1] - 2.0 * phi_w[iw]) + smth1 = \ + abs((phi_w[iw + 1] + 2 * np.pi) + phi_w[iw - 1] - 2.0 * phi_w[iw]) + smth2 = \ + abs((phi_w[iw + 1] - 2 * np.pi) + phi_w[iw - 1] - 2.0 * phi_w[iw]) + + phase_diff = phi_w[iw] - phi_w[iw + 1] + + if abs(phase_diff) > phase_step: + + temp_period = 2.0 * np.pi / wvec[iw] + + if smth1 < smth0 and smth1 < smth2: + logger.warning(f"2pi phase shift at {iw} T={temp_period} " + f"diff={phase_diff}") + phi_w[iw + 1:nfreq_max] = phi_w[iw + 1:nfreq_max] + 2 * np.pi + + if smth2 < smth0 and smth2 < smth1: + logger.warning(f"-2pi phase shift at {iw} T={temp_period} " + f"diff={phase_diff}") + phi_w[iw + 1:nfreq_max] = phi_w[iw + 1:nfreq_max] - 2 * np.pi + + return phi_w + + + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3fb477a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +requires = ["setuptools>=61.0.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "pyadjoint" +version = "0.1.0" +description = "Misfit measurement and adjoint source calculation" +readme = "README.md" +requires-python = ">=3.7" +license = {file = "LICENSE.txt"} +authors = [ + {name = "adjTomo Dev Team"}, + {email = "adjtomo@gmail.com"} +] +dependencies = [ + "obspy", +] + +[project.optional-dependencies] +test = ["pytest"] +dev = ["pytest", "flake8", "nose"] +doc = ["fumo"] + +[project.urls] +homepage = "https://github.com/adjtomo/" +documentation = "https://adjtomo.github.io/pyadjoint" +repository = "https://github.com/adjtomo/pyadjoint" + diff --git a/setup.py b/setup.py index 90d8156..bac24a4 100644 --- a/setup.py +++ b/setup.py @@ -1,79 +1,6 @@ #!/usr/bin/env python -# -*- encoding: utf8 -*- -""" -Setup script for pyadjoint module. -:copyright: - Lion Krischer (krischer@geophysik.uni-muenchen.de), 2015 -:license: - BSD 3-Clause ("BSD New" or "BSD Simplified") -""" -import glob -import inspect -import io -import os +import setuptools -from setuptools import find_packages -from setuptools import setup - - -changelog = os.path.join(os.path.dirname(os.path.abspath( - inspect.getfile(inspect.currentframe()))), "CHANGELOG.md") -with open(changelog, "rt") as fh: - changelog = fh.read() - -long_description = """ -Source code: https://github.com/krischer/pyadjoint - -Documentation: http://krischer.github.io/pyadjoint - -%s""".strip() % changelog - - -def read(*names, **kwargs): - return io.open( - os.path.join(os.path.dirname(__file__), *names), - encoding=kwargs.get("encoding", "utf8")).read() - -setup( - name="pyadjoint", - version="0.0.1a", - license='BSD 3-Clause', - description="Python package to measure misfit and calculate adjoint " - "sources", - long_description=long_description, - author="Lion Krischer", - author_email="krischer@geophysik.uni-muenchen.de", - url="https://github.com/krischer/pyadjoint", - packages=find_packages("src"), - package_dir={"": "src"}, - py_modules=[os.path.splitext(os.path.basename(i))[0] - for i in glob.glob("src/*.py")], - include_package_data=True, - zip_safe=False, - classifiers=[ - # complete classifier list: - # http://pypi.python.org/pypi?%3Aaction=list_classifiers - "Development Status :: 2 - Pre-Alpha", - "Intended Audience :: Developers", - "Operating System :: Unix", - "Operating System :: POSIX", - "Operating System :: Microsoft :: Windows", - "Programming Language :: Python", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: Implementation :: CPython", - "Topic :: Utilities", - ], - keywords=[ - "seismology", "adjoint", "science", "tomography", "inversion" - ], - install_requires=[ - "obspy>=1.0.0", "flake8", "pytest", "nose", "numpy", "scipy" - ], - extras_require={ - "docs": ["sphinx", "ipython", "runipy"] - } -) +if __name__ == "__main__": + setuptools.setup() diff --git a/src/pyadjoint/__init__.py b/src/pyadjoint/__init__.py deleted file mode 100644 index 3759aa2..0000000 --- a/src/pyadjoint/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -:copyright: - Lion Krischer (krischer@geophysik.uni-muenchen.de), 2015 -:license: - BSD 3-Clause ("BSD New" or "BSD Simplified") -""" -from __future__ import absolute_import, division, print_function - -import logging - - -class PyadjointError(Exception): - """ - Base class for all Pyadjoint exceptions. Will probably be used for all - exceptions to not overcomplicate things as the whole package is pretty - small. - """ - pass - - -class PyadjointWarning(UserWarning): - """ - Base class for all Pyadjoint warnings. - """ - pass - - -__version__ = "0.0.1a" - - -# setup the logger -logger = logging.getLogger(__name__) -logger.setLevel(logging.WARNING) -# Prevent propagating to higher loggers. -logger.propagate = 0 -# Console log handler. -ch = logging.StreamHandler() -# Add formatter -FORMAT = "[%(asctime)s] - %(name)s - %(levelname)s: %(message)s" -formatter = logging.Formatter(FORMAT) -ch.setFormatter(formatter) -ch.setLevel(logging.DEBUG) -logger.addHandler(ch) - -# Main objects and functions available at the top level. -from .adjoint_source import AdjointSource, calculate_adjoint_source # NOQA -from .config import Config # NOQA diff --git a/src/pyadjoint/adjoint_source.py b/src/pyadjoint/adjoint_source.py deleted file mode 100644 index e245b90..0000000 --- a/src/pyadjoint/adjoint_source.py +++ /dev/null @@ -1,485 +0,0 @@ -#!/usr/bin/env python -# -*- encoding: utf8 -*- -""" -Central interfaces for ``Pyadjoint``. - -:copyright: - Lion Krischer (krischer@geophysik.uni-muenchen.de), 2015 -:license: - BSD 3-Clause ("BSD New" or "BSD Simplified") -""" -from __future__ import absolute_import, division, print_function - -import inspect -import matplotlib.pylab as plt -import numpy as np -import obspy -import os -import pkgutil -import warnings - -from . import PyadjointError, PyadjointWarning - - -class AdjointSource(object): - # Dictionary of available adjoint source. The key is the name, the value - # a tuple of function, verbose name, and description. - _ad_srcs = {} - - def __init__(self, adj_src_type, misfit, dt, min_period, max_period, - component, adjoint_source=None, network=None, station=None, - location=None, starttime=None): - """ - Class representing an already calculated adjoint source. - - :param adj_src_type: The type of adjoint source. - :type adj_src_type: str - :param misfit: The misfit value. - :type misfit: float - :param dt: The sampling rate of the adjoint source. - :type dt: float - :param min_period: The minimum period of the spectral content - of the data. - :type min_period: float - :param max_period: The maximum period of the spectral content - of the data. - :type max_period: float - :param component: The adjoint source component, usually ``"Z"``, - ``"N"``, ``"E"``, ``"R"``, or ``"T"``. - :type component: str - :param adjoint_source: The actual adjoint source. - :type adjoint_source: :class:`numpy.ndarray` - :param network: The network code of the station. - :type network: str - :param station: The station code of the station. - :type station: str - :param location: The location code of the station. - :type location: str - :param starttime: starttime of adjoint source - :type starttime: obspy.UTCDateTime - """ - if adj_src_type not in self._ad_srcs: - raise ValueError("Unknown adjoint source type '%s'." % - adj_src_type) - self.adj_src_type = adj_src_type - self.adj_src_name = self._ad_srcs[adj_src_type][1] - self.misfit = misfit - self.dt = dt - self.min_period = min_period - self.max_period = max_period - self.component = component - self.network = network - self.station = station - self.location = location - self.starttime = starttime - self.adjoint_source = adjoint_source - - def __str__(self): - if self.network and self.station: - station = " at station %s.%s" % (self.network, self.station) - else: - station = "" - - if self.adjoint_source is not None: - adj_src_status = "available with %i samples" % (len( - self.adjoint_source)) - else: - adj_src_status = "has not been calculated" - - return ( - "{name} Adjoint Source for component {component}{station}\n" - " Misfit: {misfit:.4g}\n" - " Adjoint source {adj_src_status}" - ).format( - name=self.adj_src_name, - component=self.component, - station=station, - misfit=self.misfit, - adj_src_status=adj_src_status - ) - - def write(self, filename, format, **kwargs): - """ - Write the adjoint source to a file. - - :param filename: Determines where the adjoint source is saved. - :type filename: str, open file, or file-like object - :param format: The format of the adjoint source. Currently available - are: ``"SPECFEM"`` - :type format: str - - .. rubric:: SPECFEM - - SPECFEM requires one additional parameter: the temporal offset of the - first sample in seconds. The following example sets the time of the - first sample in the adjoint source to ``-10``. - - >>> adj_src.write("NET.STA.CHAN.adj", format="SPECFEM", - ... time_offset=-10) # doctest: +SKIP - """ - if self.adjoint_source is None: - raise ValueError("Can only write adjoint sources if the adjoint " - "source has been calculated.") - - format = format.upper() - available_formats = ["SPECFEM"] - if format not in available_formats: - raise ValueError("format '%s' not known. Available formats: %s" % - (format, ", ".join(available_formats))) - - if not hasattr(filename, "write"): - with open(filename, "wb") as fh: - self._write(fh, format=format, **kwargs) - else: - self._write(filename, format=format, **kwargs) - - def _write(self, buf, format, **kwargs): - if format == "SPECFEM": - self._write_specfem(buf=buf, time_offset=kwargs["time_offset"]) - else: - raise NotImplementedError - - def _write_specfem(self, buf, time_offset): - """ - Write the adjoint source for SPECFEM. - """ - l = len(self.adjoint_source) - - to_write = np.empty((l, 2)) - - to_write[:, 0] = np.linspace(0, (l - 1) * self.dt, l) - to_write[:, 0] += time_offset - # SPECFEM expects non-time reversed adjoint sources. - to_write[:, 1] = self.adjoint_source[::-1] - - np.savetxt(buf, to_write) - - def write_to_asdf(self, ds, time_offset, coordinates=None, **kwargs): - """ - Writes the adjoint source to an ASDF file. - Note: For now it is assumed SPECFEM will be using the adjoint source - - :param ds: The ASDF data structure read in using pyasdf. - :type ds: str - :param time_offset: The temporal offset of the first sample in seconds. - This is required if using the adjoint source as input to SPECFEM. - :type time_offset: float - :param coordinates: If given, the coordinates of the adjoint source. - The 'latitude', 'longitude', and 'elevation_in_m' of the adjoint - source must be defined. - :type coordinates: list - - .. rubric:: SPECFEM - - SPECFEM requires one additional parameter: the temporal offset of the - first sample in seconds. The following example sets the time of the - first sample in the adjoint source to ``-10``. - - >>> adj_src.write_to_asdf(ds, time_offset=-10, - ... coordinates={'latitude':19.2, - ... 'longitude':13.4, - ... 'elevation_in_m':2.0}) - """ - # Import here to not have a global dependency on pyasdf - from pyasdf.exceptions import NoStationXMLForStation - - # Convert the adjoint source to SPECFEM format - l = len(self.adjoint_source) - specfem_adj_source = np.empty((l, 2)) - specfem_adj_source[:, 0] = np.linspace(0, (l - 1) * self.dt, l) - specfem_adj_source[:, 0] += time_offset - specfem_adj_source[:, 1] = self.adjoint_source[::-1] - - tag = "%s_%s_%s" % (self.network, self.station, self.component) - min_period = self.min_period - max_period = self.max_period - component = self.component - station_id = "%s.%s" % (self.network, self.station) - - if coordinates: - # If given, all three coordinates must be present - if {"latitude", "longitude", "elevation_in_m"}.difference( - set(coordinates.keys())): - raise ValueError( - "'latitude', 'longitude', and 'elevation_in_m'" - " must be given") - else: - try: - coordinates = ds.waveforms[ - "%s.%s" % (self.network, self.station)].coordinates - except NoStationXMLForStation: - raise ValueError("Coordinates must either be given " - "directly or already be part of the " - "ASDF file") - - # Safeguard against funny types in the coordinates dictionary - latitude = float(coordinates["latitude"]) - longitude = float(coordinates["longitude"]) - elevation_in_m = float(coordinates["elevation_in_m"]) - - parameters = {"dt": self.dt, "misfit_value": self.misfit, - "adjoint_source_type": self.adj_src_type, - "min_period": min_period, "max_period": max_period, - "latitude": latitude, "longitude": longitude, - "elevation_in_m": elevation_in_m, - "station_id": station_id, "component": component, - "units": "m"} - - # Use pyasdf to add auxiliary data to the ASDF file - ds.add_auxiliary_data(data=specfem_adj_source, - data_type="AdjointSource", path=tag, - parameters=parameters) - - -def calculate_adjoint_source(adj_src_type, observed, synthetic, config, - window, adjoint_src=True, - plot=False, plot_filename=None, **kwargs): - """ - Central function of Pyadjoint used to calculate adjoint sources and misfit. - - This function uses the notion of observed and synthetic data to offer a - nomenclature most users are familiar with. Please note that it is - nonetheless independent of what the two data arrays actually represent. - - The function tapers the data from ``left_window_border`` to - ``right_window_border``, both in seconds since the first sample in the - data arrays. - - :param adj_src_type: The type of adjoint source to calculate. - :type adj_src_type: str - :param observed: The observed data. - :type observed: :class:`obspy.core.trace.Trace` - :param synthetic: The synthetic data. - :type synthetic: :class:`obspy.core.trace.Trace` - :param min_period: The minimum period of the spectral content of the data. - :type min_period: float - :param max_period: The maximum period of the spectral content of the data. - :type max_period: float - :param left_window_border: Left border of the window to be tapered in - seconds since the first sample in the data arrays. - :type left_window_border: float - :param right_window_border: Right border of the window to be tapered in - seconds since the first sample in the data arrays. - :type right_window_border: float - :param adjoint_src: Only calculate the misfit or also derive - the adjoint source. - :type adjoint_src: bool - :param plot: Also produce a plot of the adjoint source. This will force - the adjoint source to be calculated regardless of the value of - ``adjoint_src``. - :type plot: bool or empty :class:`matplotlib.figure.Figure` instance - :param plot_filename: If given, the plot of the adjoint source will be - saved there. Only used if ``plot`` is ``True``. - :type plot_filename: str - """ - observed, synthetic = _sanity_checks(observed, synthetic) - - # Get number of samples now as the adjoint source calculation function - # are allowed to mess with the trace objects. - npts = observed.stats.npts - - if adj_src_type not in AdjointSource._ad_srcs: - raise PyadjointError( - "Adjoint Source type '%s' is unknown. Available types: %s" % ( - adj_src_type, ", ".join( - sorted(AdjointSource._ad_srcs.keys())))) - - fct = AdjointSource._ad_srcs[adj_src_type][0] - - if plot: - # The plot kwargs overwrites the adjoint_src kwarg. - adjoint_src = True - if plot is True: - figure = plt.figure(figsize=(12, 6)) - else: - # Assume plot is a preexisting figure instance - figure = plot - else: - figure = None - try: - ret_val = fct(observed=observed, synthetic=synthetic, - config=config, window=window, - adjoint_src=adjoint_src, figure=figure, **kwargs) - - if plot and plot_filename: - figure.savefig(plot_filename) - elif plot is True: - plt.show() - - finally: - # Assure the figure is closed. Otherwise matplotlib will leak - # memory. If the figure has been created outside of Pyadjoint, - # it will not be closed. - if plot is True: - plt.close() - - # Get misfit an warn for a negative one. - misfit = float(ret_val["misfit"]) - if misfit < 0.0: - warnings.warn("The misfit value is negative. Be cautious!", - PyadjointWarning) - - if adjoint_src and "adjoint_source" not in ret_val: - raise PyadjointError("The actual adjoint source was not calculated " - "by the underlying function although it was " - "requested.") - - # Be very defensive. This assures future adjoint source types can be - # integrated smoothly. - if adjoint_src: - adjoint_source = ret_val["adjoint_source"] - # Raise if wrong type. - if not isinstance(adjoint_source, np.ndarray) or \ - adjoint_source.dtype != np.float64: - raise PyadjointError("The adjoint source calculated by the " - "underlying function is no numpy array with " - "a `float64` dtype.") - if len(adjoint_source.shape) != 1: - raise PyadjointError( - "The underlying function returned at adjoint source with " - "shape %s. It must return a one-dimensional array." % str( - adjoint_source.shape)) - if len(adjoint_source) != npts: - raise PyadjointError( - "The underlying function returned an adjoint source with %i " - "samples. It must return a function with %i samples which is " - "the sample count of the input data." % ( - len(adjoint_source), npts)) - # Make sure the data returned has no infs or NaNs. - if not np.isfinite(adjoint_source).all(): - raise PyadjointError( - "The underlying function returned an adjoint source with " - "either NaNs or Inf values. This must not be.") - else: - adjoint_source = None - - return AdjointSource(adj_src_type, misfit=misfit, - adjoint_source=adjoint_source, - dt=observed.stats.delta, - min_period=config.min_period, - max_period=config.max_period, - network=observed.stats.network, - station=observed.stats.station, - component=observed.stats.channel, - location=observed.stats.location, - starttime=observed.stats.starttime) - - -def _sanity_checks(observed, synthetic): - """ - Perform a number of basic sanity checks to assure the data is valid - in a certain sense. - - It checks the types of both, the start time, sampling rate, number of - samples, ... - - :param observed: The observed data. - :type observed: :class:`obspy.core.trace.Trace` - :param synthetic: The synthetic data. - :type synthetic: :class:`obspy.core.trace.Trace` - - :raises: :class:`~pyadjoint.PyadjointError` - """ - if not isinstance(observed, obspy.Trace): - # Also accept Stream objects. - if isinstance(observed, obspy.Stream) and \ - len(observed) == 1: - observed = observed[0] - else: - raise PyadjointError( - "Observed data must be an ObsPy Trace object.") - if not isinstance(synthetic, obspy.Trace): - if isinstance(synthetic, obspy.Stream) and \ - len(synthetic) == 1: - synthetic = synthetic[0] - else: - raise PyadjointError( - "Synthetic data must be an ObsPy Trace object.") - - if observed.stats.npts != synthetic.stats.npts: - raise PyadjointError("Observed and synthetic data must have the same " - "number of samples.") - - sr1 = observed.stats.sampling_rate - sr2 = synthetic.stats.sampling_rate - - if abs(sr1 - sr2) / sr1 >= 1E-5: - raise PyadjointError("Observed and synthetic data must have the same " - "sampling rate.") - - # Make sure data and synthetics start within half a sample interval. - if abs(observed.stats.starttime - synthetic.stats.starttime) > \ - observed.stats.delta * 0.5: - raise PyadjointError("Observed and synthetic data must have the same " - "starttime.") - - ptp = sorted([observed.data.ptp(), synthetic.data.ptp()]) - if ptp[1] / ptp[0] >= 5: - warnings.warn("The amplitude difference between data and " - "synthetic is fairly large.", PyadjointWarning) - - # Also check the components of the data to avoid silly mistakes of - # users. - if len(set([observed.stats.channel[-1].upper(), - synthetic.stats.channel[-1].upper()])) != 1: - warnings.warn("The orientation code of synthetic and observed " - "data is not equal.") - - observed = observed.copy() - synthetic = synthetic.copy() - observed.data = np.require(observed.data, dtype=np.float64, - requirements=["C"]) - synthetic.data = np.require(synthetic.data, dtype=np.float64, - requirements=["C"]) - - return observed, synthetic - - -def _discover_adjoint_sources(): - """ - Discovers the available adjoint sources. This should work no matter if - pyadjoint is checked out from git, packaged as .egg or for any other - possibility. - """ - from . import adjoint_source_types - - AdjointSource._ad_srcs = {} - - fct_name = "calculate_adjoint_source" - name_attr = "VERBOSE_NAME" - desc_attr = "DESCRIPTION" - add_attr = "ADDITIONAL_PARAMETERS" - - path = os.path.join( - os.path.dirname(inspect.getfile(inspect.currentframe())), - "adjoint_source_types") - for importer, modname, _ in pkgutil.iter_modules( - [path], prefix=adjoint_source_types.__name__ + "."): - m = importer.find_module(modname).load_module(modname) - if not hasattr(m, fct_name): - continue - fct = getattr(m, fct_name) - if not callable(fct): - continue - - name = modname.split('.')[-1] - - if not hasattr(m, name_attr): - raise PyadjointError( - "Adjoint source '%s' does not have a variable named %s." % - (name, name_attr)) - - if not hasattr(m, desc_attr): - raise PyadjointError( - "Adjoint source '%s' does not have a variable named %s." % - (name, desc_attr)) - - # Add tuple of name, verbose name, and description. - AdjointSource._ad_srcs[name] = ( - fct, - getattr(m, name_attr), - getattr(m, desc_attr), - getattr(m, add_attr) if hasattr(m, add_attr) else None) - - -_discover_adjoint_sources() diff --git a/src/pyadjoint/adjoint_source_types/cc_traveltime_misfit.py b/src/pyadjoint/adjoint_source_types/cc_traveltime_misfit.py deleted file mode 100644 index 199ff1f..0000000 --- a/src/pyadjoint/adjoint_source_types/cc_traveltime_misfit.py +++ /dev/null @@ -1,240 +0,0 @@ -#!/usr/bin/env python -# -*- encoding: utf8 -*- -""" -Cross correlation traveltime misfit. - -:copyright: - Youyi Ruan (youyir@princeton.edu) 2016 - Lion Krischer (krischer@geophysik.uni-muenchen.de), 2015 -:license: - BSD 3-Clause ("BSD New" or "BSD Simplified") -""" -from __future__ import absolute_import, division, print_function - -import warnings - -import numpy as np -from obspy.signal.cross_correlation import xcorr_pick_correction -from scipy.integrate import simps - -from ..utils import window_taper, generic_adjoint_source_plot - - -VERBOSE_NAME = "Cross Correlation Traveltime Misfit" - -DESCRIPTION = r""" -Traveltime misfits simply measure the squared traveltime difference. The -misfit :math:`\chi(\mathbf{m})` for a given Earth model :math:`\mathbf{m}` -and a single receiver and component is given by - -.. math:: - - \chi (\mathbf{m}) = \frac{1}{2} \left[ T^{obs} - T(\mathbf{m}) \right] ^ 2 - -:math:`T^{obs}` is the observed traveltime, and :math:`T(\mathbf{m})` the -predicted traveltime in Earth model :math:`\mathbf{m}`. - -In practice traveltime are measured by cross correlating observed and -predicted waveforms. This particular implementation here measures cross -correlation time shifts with subsample accuracy with a fitting procedure -explained in [Deichmann1992]_. For more details see the documentation of the -:func:`~obspy.signal.cross_correlation.xcorr_pick_correction` function and the -corresponding -`Tutorial `_. - - -The adjoint source for the same receiver and component is then given by - -.. math:: - - f^{\dagger}(t) = - \left[ T^{obs} - T(\mathbf{m}) \right] ~ \frac{1}{N} ~ - \partial_t \mathbf{s}(T - t, \mathbf{m}) - -For the sake of simplicity we omit the spatial Kronecker delta and define -the adjoint source as acting solely at the receiver's location. For more -details, please see [Tromp2005]_ and [Bozdag2011]_. - - -:math:`N` is a normalization factor given by - - -.. math:: - - N = \int_0^T ~ \mathbf{s}(t, \mathbf{m}) ~ - \partial^2_t \mathbf{s}(t, \mathbf{m}) dt - -This particular implementation here uses -`Simpson's rule `_ -to evaluate the definite integral. -""" # NOQA - -# Optional: document any additional parameters this particular adjoint sources -# receives in addition to the ones passed to the central adjoint source -# calculation function. Make sure to indicate the default values. This is a -# bit redundant but the only way I could figure out to make it work with the -# rest of the architecture. -ADDITIONAL_PARAMETERS = r""" -**taper_percentage** (:class:`float`) - Decimal percentage of taper at one end (ranging from ``0.0`` (0%) to - ``0.5`` (50%)). Defauls to ``0.15``. - -**taper_type** (:class:`float`) - The taper type, supports anything :meth:`obspy.core.trace.Trace.taper` - can use. Defaults to ``"hann"``. -""" - - -def _xcorr_shift(d, s): - cc = np.correlate(d, s, mode="full") - time_shift = cc.argmax() - len(d) + 1 - return time_shift - - -def cc_error(d1, d2, deltat, cc_shift, cc_dlna, sigma_dt_min, sigma_dlna_min): - """ - Estimate error for dt and dlna with uncorrelation assumption - """ - nlen_t = len(d1) - - d2_cc_dt = np.zeros(nlen_t) - d2_cc_dtdlna = np.zeros(nlen_t) - - for index in range(0, nlen_t): - index_shift = index - cc_shift - - if 0 <= index_shift < nlen_t: - # corrected by c.c. shift - d2_cc_dt[index] = d2[index_shift] - - # corrected by c.c. shift and amplitude - d2_cc_dtdlna[index] = np.exp(cc_dlna) * d2[index_shift] - - # time derivative of d2_cc (velocity) - d2_cc_vel = np.gradient(d2_cc_dtdlna, deltat) - - # the estimated error for dt and dlna with uncorrelation assumption - sigma_dt_top = np.sum((d1 - d2_cc_dtdlna)**2) - sigma_dt_bot = np.sum(d2_cc_vel**2) - - sigma_dlna_top = sigma_dt_top - sigma_dlna_bot = np.sum(d2_cc_dt**2) - - sigma_dt = np.sqrt(sigma_dt_top / sigma_dt_bot) - sigma_dlna = np.sqrt(sigma_dlna_top / sigma_dlna_bot) - - if sigma_dt < sigma_dt_min: - sigma_dt = sigma_dt_min - - if sigma_dlna < sigma_dlna_min: - sigma_dlna = sigma_dlna_min - - return sigma_dt, sigma_dlna - - -def subsample_xcorr_shift(d, s): - """ - Calculate the correlation time shift around the maximum amplitude of the - synthetic trace with subsample accuracy. - :param s: - :param d: - """ - # Estimate shift and use it as a guideline for the subsample accuracy - # shift. - time_shift = _xcorr_shift(d.data, s.data) * d.stats.delta - - # Align on the maximum amplitude of the synthetics. - pick_time = s.stats.starttime + s.data.argmax() * s.stats.delta - - # Will raise a warning if the trace ids don't match which we don't care - # about here. - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - return xcorr_pick_correction( - pick_time, s, pick_time, d, 20.0 * time_shift, - 20.0 * time_shift, 10.0 * time_shift)[0] - - -def calculate_adjoint_source(observed, synthetic, config, window, - adjoint_src, figure): # NOQA - - ret_val_p = {} - ret_val_q = {} - - nlen_data = len(synthetic.data) - deltat = synthetic.stats.delta - - fp = np.zeros(nlen_data) - fq = np.zeros(nlen_data) - - misfit_sum_p = 0.0 - misfit_sum_q = 0.0 - - # === - # loop over time windows - # === - for wins in window: - left_window_border = wins[0] - right_window_border = wins[1] - - left_sample = int(np.floor(left_window_border / deltat)) + 1 - nlen = int(np.floor((right_window_border - - left_window_border) / deltat)) + 1 - right_sample = left_sample + nlen - - d = np.zeros(nlen) - s = np.zeros(nlen) - - d[0:nlen] = observed.data[left_sample:right_sample] - s[0:nlen] = synthetic.data[left_sample:right_sample] - - # All adjoint sources will need some kind of windowing taper - window_taper(d, taper_percentage=config.taper_percentage, - taper_type=config.taper_type) - window_taper(s, taper_percentage=config.taper_percentage, - taper_type=config.taper_type) - - i_shift = _xcorr_shift(d, s) - t_shift = i_shift * deltat - - cc_dlna = 0.5 * np.log(sum(d[0:nlen]*d[0:nlen]) / - sum(s[0:nlen]*s[0:nlen])) - - sigma_dt, sigma_dlna = cc_error(d, s, deltat, i_shift, cc_dlna, - config.dt_sigma_min, - config.dlna_sigma_min) - - misfit_sum_p += 0.5 * (t_shift/sigma_dt) ** 2 - misfit_sum_q += 0.5 * (cc_dlna/sigma_dlna) ** 2 - - dsdt = np.gradient(s, deltat) - nnorm = simps(y=dsdt*dsdt, dx=deltat) - fp[left_sample:right_sample] = dsdt[:] * t_shift / nnorm / sigma_dt**2 - - mnorm = simps(y=s*s, dx=deltat) - fq[left_sample:right_sample] =\ - -1.0 * s[:] * cc_dlna / mnorm / sigma_dlna ** 2 - - ret_val_p["misfit"] = misfit_sum_p - ret_val_q["misfit"] = misfit_sum_q - - if adjoint_src is True: - ret_val_p["adjoint_source"] = fp[::-1] - ret_val_q["adjoint_source"] = fq[::-1] - - if config.measure_type == "dt": - if figure: - generic_adjoint_source_plot(observed, synthetic, - ret_val_p["adjoint_source"], - ret_val_p["misfit"], - window, VERBOSE_NAME) - - return ret_val_p - - if config.measure_type == "am": - if figure: - generic_adjoint_source_plot(observed, synthetic, - ret_val_q["adjoint_source"], - ret_val_q["misfit"], - window, VERBOSE_NAME) - - return ret_val_q diff --git a/src/pyadjoint/adjoint_source_types/multitaper_misfit.py b/src/pyadjoint/adjoint_source_types/multitaper_misfit.py deleted file mode 100644 index 2d877fe..0000000 --- a/src/pyadjoint/adjoint_source_types/multitaper_misfit.py +++ /dev/null @@ -1,886 +0,0 @@ -#!/usr/bin/env python -# -*- encoding: utf8 -*- -""" -Multitaper based phase and amplitude misfit and adjoint source. - -:copyright: - Youyi Ruan (youyir@princeton.edu), 2016 - Matthieu Lefebvre (ml15@princeton.edu), 2016 - Yanhua O. Yuan (yanhuay@princeton.edu), 2015 -:license: - BSD 3-Clause ("BSD New" or "BSD Simplified") -""" -from __future__ import absolute_import, division, print_function - -import numpy as np -from scipy.integrate import simps - -from .. import logger -# from . import PyadjointError, PyadjointWarning -from ..utils import generic_adjoint_source_plot -from ..utils import window_taper -from ..dpss import dpss_windows - - -# This is the verbose and pretty name of the adjoint source defined in this -# function. -VERBOSE_NAME = "Multitaper Misfit" - -# Long and detailed description of the adjoint source defined in this file. -# Don't spare any details. This will be rendered as restructured text in the -# documentation. Be careful to escape the string with an ``r`` prefix. -# Otherwise most backslashes will have a special meaning which messes with the -# TeX like formulas. -DESCRIPTION = r""" -The misfit :math:`\chi_P(\mathbf{m})` measures -frequency-dependent phase differences -estimated with multitaper approach. -The misfit :math:`\chi_P(\mathbf{m})` -given Earth model :math:`\mathbf{m}` -and a single receiver is -given by - -.. math:: - - \chi_P (\mathbf{m}) = \frac{1}{2} \int_0^W W_P(w) \left| - \frac{ \tau^{\mathbf{d}}(w) - \tau^{\mathbf{s}}(w, \mathbf{m})} - {\sigma_P(w)} \right|^ 2 dw - -:math:`\tau^\mathbf{d}(w)` is the frequency-dependent -phase measurement of the observed data; -:math:`\tau^\mathbf{s}(w, \mathbf{m})` the frequency-dependent -phase measurement of the synthetic data. -The function :math:`W_P(w)` denotes frequency-domain -taper corresponding to the frequency range over which -the measurements are assumed reliable. -:math:`\sigma_P(w)` is associated with the -traveltime uncertainty introduced in making measurements, -which can be estimated with cross-correlation method, -or Jackknife multitaper approach. - -The adjoint source for the same receiver is given by - -.. math:: - - f_P^{\dagger}(t) = \sum_k h_k(t)P_j(t) - -in which :math:`h_k(t)` is one (the :math:`k`th) of multi-tapers. - -.. math:: - - P_j(t) = 2\pi W_p(t) * \Delta \tau(t) * p_j(t) \\ - P_j(w) = 2\pi W_p(w) \Delta \tau(w) * p_j(w) \\ - p_j(w) = \frac{iw s_j}{\sum_k(iw s_k)(iw s_k)^*} \\ - \Delta \tau(w) = \tau^{\mathbf{d}}(w) - \tau^{\mathbf{s}}(w, \mathbf{m}) - -""" -# Optional: document any additional parameters this particular adjoint sources -# receives in addition to the ones passed to the central adjoint source -# calculation function. Make sure to indicate the default values. This is a -# bit redundant but the only way I could figure out to make it work with the -# rest of the architecture of pyadjoint. -# ADDITIONAL_PARAMETERS = r""" -# **taper_percentage** (:class:`float`) -# Decimal percentage of taper at one end (ranging from ``0.0`` (0%) to -# ``0.5`` (50%)). Defauls to ``0.15``. -# -# **taper_type** (:class:`str`) -# The taper type, supports anything :meth:`obspy.core.trace.Trace.taper` -# can use. Defaults to ``"hann"``. -# -# """ - - -def _xcorr_shift(d, s): - """ - Calculate cross correlation shift in points - """ - cc = np.correlate(d, s, mode="full") - time_shift = cc.argmax() - len(d) + 1 - return time_shift - - -def cc_error(d1, d2, deltat, cc_shift, cc_dlna, sigma_dt_min, sigma_dlna_min): - """ - Estimate error for dt and dlna with uncorrelation assumption - copied from c.c. misfit. should not duplicate the code but keep it for - now and may need to move to utils later - """ - nlen_t = len(d1) - - # make cc-based corrections to d2 - d2_cc_dt = np.zeros(nlen_t) - d2_cc_dtdlna = np.zeros(nlen_t) - - for index in range(0, nlen_t): - index_shift = index - cc_shift - - if 0 <= index_shift < nlen_t: - d2_cc_dt[index] = d2[index_shift] - d2_cc_dtdlna[index] = np.exp(cc_dlna) * d2[index_shift] - - # time derivative of d2_cc (velocity) - d2_cc_vel = np.gradient(d2_cc_dtdlna, deltat) - - # Note: Beware of the first and last value in gradient calculation, - # ignored for safety reason, will be added in future - sigma_dt_top = np.sum((d1 - d2_cc_dtdlna) ** 2) - sigma_dt_bot = np.sum(d2_cc_vel ** 2) - - sigma_dlna_top = sigma_dt_top - sigma_dlna_bot = np.sum(d2_cc_dt ** 2) - - sigma_dt = np.sqrt(sigma_dt_top / sigma_dt_bot) - sigma_dlna = np.sqrt(sigma_dlna_top / sigma_dlna_bot) - - if sigma_dt < sigma_dt_min: - sigma_dt = sigma_dt_min - - if sigma_dlna < sigma_dlna_min: - sigma_dlna = sigma_dlna_min - - return sigma_dt, sigma_dlna - - -def frequency_limit(s, nlen, nlen_f, deltat, df, wtr, ncycle_in_window, - min_period, max_period, nw): - """ - First check if the window is suitable for mtm measurements, then - find the maximum frequency point for measurement using the spectrum of - tapered synthetics. - - :param is_mtm: - :param nw: - :param max_period: - :param min_period: - :param ncycle_in_window: - :param wtr: - :param df: - :param nlen_f: - :param deltat: - :param nlen: - :param s: synthetics - :type s: float ndarray - """ - - ifreq_min = int(1.0 / (max_period * df)) - ifreq_max = int(1.0 / (min_period * df)) - - # reject mtm if wave of min_period experience cycles less than ncycle - # _in_window in the selected window, and switch to c.c. method. - # In this case frequency limits is not needed. - if ncycle_in_window * min_period > nlen * deltat: - logger.debug("min_period: %6.0f window length: %6.0f" % - (min_period, nlen*deltat)) - logger.debug("MTM: rejecting for too few cycles within time window:") - return (ifreq_min, ifreq_max, False) - - fnum = int(nlen_f/2 + 1) - s_spectra = np.fft.fft(s, nlen_f) * deltat - - ampmax = max(abs(s_spectra[0: fnum])) - i_ampmax = np.argmax(abs(s_spectra[0: fnum])) - - water_threshold = ampmax * wtr - - nfreq_max = get_max_frequency_limit(deltat, df, fnum, i_ampmax, ifreq_max, - s_spectra, water_threshold) - - nfreq_min = get_min_frequency_limit(deltat, df, fnum, i_ampmax, ifreq_min, - ncycle_in_window, nlen, s_spectra, - water_threshold) - - # reject mtm if the chosen frequency band is narrower than quater of - # multi-taper bandwidth - if (nfreq_max - nfreq_min) * df < nw / (4.0 * nlen * deltat): - logger.debug("chosen bandwidth: %f" % ((nfreq_max - nfreq_min) * df)) - logger.debug("half taper bandwidth: %f" % (nw / (4.0 * nlen * deltat))) - logger.debug("MTM: rejecting for frequency range" - "narrower than half taper bandwith:") - return (ifreq_min, ifreq_max, False) - - return nfreq_min, nfreq_max, True - - -def get_min_frequency_limit(deltat, df, fnum, i_ampmax, ifreq_min, - ncycle_in_window, nlen, s_spectra, - water_threshold): - nfreq_min = 0 - is_search = True - - for iw in range(fnum - 1, 0, -1): - if iw < i_ampmax: - nfreq_min = search_frequency_limit(is_search, iw, nfreq_min, - s_spectra, water_threshold) - - # assume there are at least N cycles within the window - nfreq_min = max(nfreq_min, int(ncycle_in_window/(nlen*deltat)/df) - 1) - nfreq_min = max(nfreq_min, ifreq_min) - - return nfreq_min - - -def get_max_frequency_limit(deltat, df, fnum, i_ampmax, ifreq_max, s_spectra, - water_threshold): - nfreq_max = fnum - 1 - is_search = True - - for iw in range(0, fnum): - if iw > i_ampmax: - nfreq_max = search_frequency_limit(is_search, iw, nfreq_max, - s_spectra, water_threshold) - # Don't go beyond the Nyquist frequency - nfreq_max = min(nfreq_max, int(1.0/(2*deltat)/df) - 1) - nfreq_max = min(nfreq_max, ifreq_max) - - return nfreq_max - - -def search_frequency_limit(is_search, index, nfreq_limit, spectra, - water_threshold): - """ - Search valid frequency range of spectra - - :param is_search: Logic switch - :param spectra: spectra of signal - :param index: index of spectra - :water_threshold: the triggering value to stop the search - If the spectra larger than 10*water_threshold will trigger the - search again, works like the heating thermostat. - - The constant 10 may need to move outside to allow user choose - different values. - """ - - if abs(spectra[index]) < water_threshold and is_search: - is_search = False - nfreq_limit = index - - if abs(spectra[index]) > 10 * water_threshold and not is_search: - is_search = True - nfreq_limit = index - - return nfreq_limit - - -def mt_measure_select(nfreq_min, nfreq_max, df, nlen, deltat, dtau_w, dt_fac, - err_dt, err_fac, cc_tshift, dt_max_scale): - """ - check mtm measurement see if the measurements are good to keep, - otherwise use c.c. measurement instead - - :param is_mtm: logic, switch of c.c. or mtm - :param dt_max_scale: float, maximum time shift allowed - :param cc_tshift: float, c.c. time shift - :param err_fac: float, percentage of wave period - :param err_dt: float, maximum err allowed - :param dt_fac: float, percentage of wave period - :param dtau_w: float, time dependent travel time measurements from mtm - :param deltat: float, time domain sampling length - :param nlen: integer, lenght of obsd - :param df: float, frequency domain sampling length - :param nfreq_max: maximum in frequency domain - :param nfreq_min: minimum in frequency domain - """ - - # If the c.c. measurements is too small - if abs(cc_tshift) <= deltat: - msg = "C.C. time shift less than time domain sample length %f" % deltat - logger.debug(msg) - return False - - # If any mtm measurements is out of the resonable range, - # switch from mtm to c.c. - for j in range(nfreq_min, nfreq_max): - - # dt larger than 1/dt_fac of the wave period - if np.abs(dtau_w[j]) > 1./(dt_fac*j*df): - msg = "mtm dt measurements is too large" - logger.debug(msg) - return False - - # error larger than 1/err_fac of wave period - if err_dt[j] > 1./(err_fac*j*df): - msg = "mtm dt error is too large" - logger.debug(msg) - return False - - # dt larger than the maximum time shift allowed - if np.abs(dtau_w[j]) > dt_max_scale*abs(cc_tshift): - msg = "dt is larger than the maximum time shift allowed" - logger.debug(msg) - return False - - return True - - -def mt_measure(d1, d2, dt, tapers, wvec, df, nlen_f, waterlevel_mtm, - phase_step, nfreq_min, nfreq_max, cc_tshift, cc_dlna): - - nlen_t = len(d1) - ntaper = len(tapers[0]) - - fnum = int(nlen_f/2 + 1) - - # initialization - top_tf = np.zeros(nlen_f, dtype=complex) - bot_tf = np.zeros(nlen_f, dtype=complex) - - # Multitaper measurements - for itaper in range(0, ntaper): - - taper = np.zeros(nlen_t) - taper[0:nlen_t] = tapers[0:nlen_t, itaper] - - # apply time-domain multi-tapered measurements - # Youyi Ruan 10/29/2015 (no cc shift) change to complex - d1_t = np.zeros(nlen_t, dtype=complex) - d2_t = np.zeros(nlen_t, dtype=complex) - - d1_t[0:nlen_t] = d1[0:nlen_t] * taper[0:nlen_t] - d2_t[0:nlen_t] = d2[0:nlen_t] * taper[0:nlen_t] - - d1_tw = np.fft.fft(d1_t, nlen_f) * dt - d2_tw = np.fft.fft(d2_t, nlen_f) * dt - - # calculate top and bottom of MT transfer function - top_tf[:] = top_tf[:] + d1_tw[:] * d2_tw[:].conjugate() - bot_tf[:] = bot_tf[:] + d2_tw[:] * d2_tw[:].conjugate() - - # === - # Calculate transfer function - # === - - # water level - wtr_use = max(abs(bot_tf[0:fnum])) * waterlevel_mtm ** 2 - - # transfrer function - trans_func = np.zeros(nlen_f, dtype=complex) - for i in range(nfreq_min, nfreq_max): - if abs(bot_tf[i]) < wtr_use: - trans_func[i] = top_tf[i] / bot_tf[i] - else: - trans_func[i] = top_tf[i] / (bot_tf[i] + wtr_use) - # trans_func[nfreq_min:nfreq_max] = \ - # top_tf[nfreq_min:nfreq_max] / \ - # (bot_tf[nfreq_min:nfreq_max] + wtr_use * - # (abs(bot_tf[nfreq_min:nfreq_max]) < wtr_use)) - - # Estimate phase and amplitude anomaly from transfer function - phi_w = np.zeros(nlen_f) - abs_w = np.zeros(nlen_f) - dtau_w = np.zeros(nlen_f) - dlna_w = np.zeros(nlen_f) - - phi_w[nfreq_min:nfreq_max] = np.arctan2( - trans_func[nfreq_min:nfreq_max].imag, - trans_func[nfreq_min:nfreq_max].real) - - abs_w[nfreq_min:nfreq_max] = np.abs(trans_func[nfreq_min:nfreq_max]) - - # cycle-skipping (check smoothness of phi, add cc measure, future - # implementation) - for iw in range(nfreq_min + 1, nfreq_max - 1): - smth = abs(phi_w[iw + 1] + phi_w[iw - 1] - 2.0 * phi_w[iw]) - smth1 = abs((phi_w[iw + 1] + 2*np.pi) + phi_w[iw - 1] - 2.0*phi_w[iw]) - smth2 = abs((phi_w[iw + 1] - 2*np.pi) + phi_w[iw - 1] - 2.0*phi_w[iw]) - - if smth1 < smth and smth1 < smth2 and \ - abs(phi_w[iw] - phi_w[iw + 1]) > phase_step: - logger.warning('2pi phase shift at {0} w={1} diff={2}'.format( - iw, wvec[iw], phi_w[iw] - phi_w[iw + 1])) - phi_w[iw + 1:nfreq_max] = phi_w[iw + 1:nfreq_max] + 2 * np.pi - - if smth2 < smth and smth2 < smth1 and \ - abs(phi_w[iw] - phi_w[iw + 1]) > phase_step: - logger.warning('-2pi phase shift at {0} w={1} diff={2}'.format( - iw, wvec[iw], phi_w[iw] - phi_w[iw + 1])) - phi_w[iw + 1:nfreq_max] = phi_w[iw + 1:nfreq_max] - 2 * np.pi - - # add the CC measurements to the transfer function - dtau_w[0] = cc_tshift - dtau_w[max(nfreq_min, 1): nfreq_max] =\ - - 1.0 / wvec[max(nfreq_min, 1): nfreq_max] * \ - phi_w[max(nfreq_min, 1): nfreq_max] + cc_tshift - - dlna_w[nfreq_min:nfreq_max] = np.log(abs_w[nfreq_min:nfreq_max]) + cc_dlna - - return phi_w, abs_w, dtau_w, dlna_w - - -def mt_error(d1, d2, deltat, tapers, wvec, df, nlen_f, waterlevel_mtm, - phase_step, nfreq_min, nfreq_max, cc_tshift, cc_dlna, phi_mtm, - abs_mtm, dtau_mtm, dlna_mtm): - - nlen_t = len(d1) - ntaper = len(tapers[0]) - - # Jacknife MT estimates - # initialization - phi_mul = np.zeros((nlen_f, ntaper)) - abs_mul = np.zeros((nlen_f, ntaper)) - dtau_mul = np.zeros((nlen_f, ntaper)) - dlna_mul = np.zeros((nlen_f, ntaper)) - ephi_ave = np.zeros(nlen_f) - eabs_ave = np.zeros(nlen_f) - edtau_ave = np.zeros(nlen_f) - edlna_ave = np.zeros(nlen_f) - err_phi = np.zeros(nlen_f) - err_abs = np.zeros(nlen_f) - err_dtau = np.zeros(nlen_f) - err_dlna = np.zeros(nlen_f) - - for itaper in range(0, ntaper): - # delete one taper - tapers_om = np.zeros((nlen_t, ntaper - 1)) - tapers_om[0:nlen_f, 0:ntaper - 1] = np.delete(tapers, itaper, 1) - - phi_om, abs_om, dtau_om, dlna_om =\ - mt_measure(d1, d2, deltat, tapers_om, - wvec, df, nlen_f, waterlevel_mtm, phase_step, - nfreq_min, nfreq_max, cc_tshift, cc_dlna) - - phi_mul[0:nlen_f, itaper] = phi_om[0:nlen_f] - abs_mul[0:nlen_f, itaper] = abs_om[0:nlen_f] - dtau_mul[0:nlen_f, itaper] = dtau_om[0:nlen_f] - dlna_mul[0:nlen_f, itaper] = dlna_om[0:nlen_f] - - # error estimation - ephi_ave[nfreq_min: nfreq_max] = ephi_ave[nfreq_min: nfreq_max] + \ - ntaper * phi_mtm[nfreq_min: nfreq_max] - (ntaper - 1) * \ - phi_mul[nfreq_min: nfreq_max, itaper] - eabs_ave[nfreq_min:nfreq_max] = eabs_ave[nfreq_min: nfreq_max] + \ - ntaper * abs_mtm[nfreq_min: nfreq_max] - (ntaper - 1) * \ - abs_mul[nfreq_min: nfreq_max, itaper] - edtau_ave[nfreq_min: nfreq_max] = edtau_ave[nfreq_min: nfreq_max] + \ - ntaper * dtau_mtm[nfreq_min: nfreq_max] - (ntaper - 1) * \ - dtau_mul[nfreq_min: nfreq_max, itaper] - edlna_ave[nfreq_min: nfreq_max] = edlna_ave[nfreq_min: nfreq_max] + \ - ntaper * dlna_mtm[nfreq_min: nfreq_max] - (ntaper - 1) * \ - dlna_mul[nfreq_min: nfreq_max, itaper] - - # take average - ephi_ave /= ntaper - eabs_ave /= ntaper - edtau_ave /= ntaper - edlna_ave /= ntaper - - # deviation - for itaper in range(0, ntaper): - err_phi[nfreq_min:nfreq_max] += \ - (phi_mul[nfreq_min: nfreq_max, itaper] - - ephi_ave[nfreq_min: nfreq_max])**2 - err_abs[nfreq_min:nfreq_max] += \ - (abs_mul[nfreq_min: nfreq_max, itaper] - - eabs_ave[nfreq_min: nfreq_max])**2 - err_dtau[nfreq_min:nfreq_max] += \ - (dtau_mul[nfreq_min: nfreq_max, itaper] - - edtau_ave[nfreq_min: nfreq_max])**2 - err_dlna[nfreq_min:nfreq_max] += \ - (dlna_mul[nfreq_min: nfreq_max, itaper] - - edlna_ave[nfreq_min: nfreq_max])**2 - - # standard deviation - err_phi[nfreq_min: nfreq_max] = np.sqrt( - err_phi[nfreq_min: nfreq_max] / (ntaper * (ntaper - 1))) - err_abs[nfreq_min: nfreq_max] = np.sqrt( - err_abs[nfreq_min: nfreq_max] / (ntaper * (ntaper - 1))) - err_dtau[nfreq_min: nfreq_max] = np.sqrt( - err_dtau[nfreq_min: nfreq_max] / (ntaper * (ntaper - 1))) - err_dlna[nfreq_min: nfreq_max] = np.sqrt( - err_dlna[nfreq_min: nfreq_max] / (ntaper * (ntaper - 1))) - - return err_phi, err_abs, err_dtau, err_dlna - - -def cc_adj(synt, cc_shift, cc_dlna, deltat, err_dt_cc, err_dlna_cc): - """ - cross correlation adjoint source and misfit - """ - - misfit_p = 0.0 - misfit_q = 0.0 - - dsdt = np.gradient(synt) / deltat - - nnorm = simps(y=dsdt*dsdt, dx=deltat) - dt_adj = cc_shift * deltat / err_dt_cc**2 / nnorm * dsdt - - mnorm = simps(y=synt*synt, dx=deltat) - am_adj = -1.0 * cc_dlna / err_dlna_cc**2 / mnorm * synt - - cc_tshift = cc_shift * deltat - misfit_p = 0.5 * (cc_tshift/err_dt_cc)**2 - misfit_q = 0.5 * (cc_dlna/err_dlna_cc)**2 - - return dt_adj, am_adj, misfit_p, misfit_q - - -def mt_adj(d1, d2, deltat, tapers, dtau_mtm, dlna_mtm, df, nlen_f, - use_cc_error, use_mt_error, nfreq_min, nfreq_max, err_dt_cc, - err_dlna_cc, err_dtau_mt, err_dlna_mt, wtr): - - nlen_t = len(d1) - ntaper = len(tapers[0]) - - misfit_p = 0.0 - misfit_q = 0.0 - - # Y. Ruan, 11/05/2015 - # frequency-domain taper based on adjusted frequency band and - # error estimation. It's not one of the filtering processes that - # needed to applied to adjoint source but an frequency domain - # weighting function for adjoint source and misfit function. - - wp_w = np.zeros(nlen_f) - wq_w = np.zeros(nlen_f) - - # iw = np.arange(nfreq_min, nfreq_max, 1) - - w_taper = np.zeros(nlen_f) - # w_taper[nfreq_min: nfreq_max] = 1.0 - - # Y. Ruan, 11/09/2015 - # Original higher order cosine taper used in measure_adj - # this cosine weighting function may taper off too much information - # will be replaced by a less aggressive taper - # ipwr_w = 10 - # w_taper[nfreq_min: nfreq_max] = 1.0 -\ - # np.cos(np.pi * (iw - nfreq_min) / (nfreq_max - nfreq_min)) ** ipwr_w - # for i in range(nfreq_min,nfreq_m): - # print(i, w_taper[i]) - win_taper_len = nfreq_max - nfreq_min - win_taper = np.ones(win_taper_len) - - window_taper(win_taper, taper_percentage=1.0, taper_type="cos_p10") - w_taper[nfreq_min: nfreq_max] = win_taper[0:win_taper_len] - - # normalization factor, factor 2 is needed for the integration from - # -inf to inf - ffac = 2.0 * df * np.sum(w_taper[nfreq_min: nfreq_max]) - if ffac <= 0.0: - logger.warning("frequency band too narrow:") - logger.warning("fmin=%f fmax=%f ffac=%f" % - (nfreq_min, nfreq_max, ffac)) - - wp_w = w_taper / ffac - wq_w = w_taper / ffac - - # cc error - if use_cc_error: - wp_w /= err_dt_cc**2 - wq_w /= err_dlna_cc**2 - - # mt error - if use_mt_error: - dtau_wtr = wtr * \ - np.sum(np.abs(dtau_mtm[nfreq_min: nfreq_max])) / \ - (nfreq_max - nfreq_min) - dlna_wtr = wtr * \ - np.sum(np.abs(dlna_mtm[nfreq_min: nfreq_max])) / \ - (nfreq_max - nfreq_min) - - err_dtau_mt[nfreq_min: nfreq_max] = \ - err_dtau_mt[nfreq_min: nfreq_max] + dtau_wtr * \ - (err_dtau_mt[nfreq_min: nfreq_max] < dtau_wtr) - err_dlna_mt[nfreq_min: nfreq_max] = \ - err_dlna_mt[nfreq_min: nfreq_max] + dlna_wtr * \ - (err_dlna_mt[nfreq_min: nfreq_max] < dlna_wtr) - - wp_w[nfreq_min: nfreq_max] = wp_w[nfreq_min: nfreq_max] / \ - ((err_dtau_mt[nfreq_min: nfreq_max]) ** 2) - wq_w[nfreq_min: nfreq_max] = wq_w[nfreq_min: nfreq_max] / \ - ((err_dlna_mt[nfreq_min: nfreq_max]) ** 2) - - # initialization - bottom_p = np.zeros(nlen_f, dtype=complex) - bottom_q = np.zeros(nlen_f, dtype=complex) - - d2_tw = np.zeros((nlen_f, ntaper), dtype=complex) - d2_tvw = np.zeros((nlen_f, ntaper), dtype=complex) - - # Multitaper measurements - for itaper in range(0, ntaper): - taper = np.zeros(nlen_f) - taper[0:nlen_t] = tapers[0:nlen_t, itaper] - - # multi-tapered measurements - d2_t = np.zeros(nlen_t) - d2_tv = np.zeros(nlen_t) - d2_t = d2 * taper[0:nlen_t] - d2_tv = np.gradient(d2_t, deltat) - - # apply FFT to tapered measurements - d2_tw[:, itaper] = np.fft.fft(d2_t, nlen_f)[:] * deltat - d2_tvw[:, itaper] = np.fft.fft(d2_tv, nlen_f)[:] * deltat - - # calculate bottom of adjoint term pj(w) qj(w) - bottom_p[:] = bottom_p[:] + \ - d2_tvw[:, itaper] * d2_tvw[:, itaper].conjugate() - bottom_q[:] = bottom_q[:] + \ - d2_tw[:, itaper] * d2_tw[:, itaper].conjugate() - - fp_t = np.zeros(nlen_f) - fq_t = np.zeros(nlen_f) - - for itaper in range(0, ntaper): - taper = np.zeros(nlen_f) - taper[0: nlen_t] = tapers[0:nlen_t, itaper] - - # calculate pj(w), qj(w) - p_w = np.zeros(nlen_f, dtype=complex) - q_w = np.zeros(nlen_f, dtype=complex) - - p_w[nfreq_min:nfreq_max] = d2_tvw[nfreq_min:nfreq_max, itaper] / \ - (bottom_p[nfreq_min:nfreq_max]) - q_w[nfreq_min:nfreq_max] = -d2_tw[nfreq_min:nfreq_max, itaper] / \ - (bottom_q[nfreq_min:nfreq_max]) - - # calculate weighted adjoint Pj(w), Qj(w) adding measurement dtau dlna - p_w *= dtau_mtm * wp_w - q_w *= dlna_mtm * wq_w - - # inverse FFT to weighted adjoint (take real part) - p_wt = np.fft.ifft(p_w, nlen_f).real * 2. / deltat - q_wt = np.fft.ifft(q_w, nlen_f).real * 2. / deltat - - # apply tapering to adjoint source - fp_t += p_wt * taper - fq_t += q_wt * taper - - # calculate misfit - dtau_mtm_weigh_sqr = dtau_mtm**2 * wp_w - dlna_mtm_weigh_sqr = dlna_mtm**2 * wq_w - - # Integrate with the composite Simpson's rule. - misfit_p = 0.5 * 2.0 * simps(y=dtau_mtm_weigh_sqr, dx=df) - misfit_q = 0.5 * 2.0 * simps(y=dlna_mtm_weigh_sqr, dx=df) - - return fp_t, fq_t, misfit_p, misfit_q - - -def calculate_adjoint_source(observed, synthetic, config, window, - adjoint_src, figure): # NOQA - # There is no need to perform any sanity checks on the passed trace - # object. At this point they will be guaranteed to have the same - # sampling rate, be sampled at the same points in time and a couple - # other things. - - # All adjoint sources will need some kind of windowing taper. - # Thus pyadjoint has a convenience function to assist with that. - # The next block tapers both observed and synthetic data. - - # frequencies points for FFT - nlen_f = 2**config.lnpt - - # constant for transfer function - waterlevel_mtm = config.transfunc_waterlevel - wtr = config.water_threshold - - # constant for cycle skip correction - phase_step = config.phase_step - - # for frequency limit calculation - ncycle_in_window = config.min_cycle_in_window - - # error estimation method - use_cc_error = config.use_cc_error - use_mt_error = config.use_mt_error - - # Frequency range for adjoint src - min_period = config.min_period - max_period = config.max_period - - # critiaria for rejecting mtm measurements - dt_fac = config.dt_fac - err_fac = config.err_fac - dt_max_scale = config.dt_max_scale - - # initialized the adjoint source - ret_val_p = {} - ret_val_q = {} - - nlen_data = len(synthetic.data) - deltat = synthetic.stats.delta - - fp = np.zeros(nlen_data) - fq = np.zeros(nlen_data) - - misfit_sum_p = 0.0 - misfit_sum_q = 0.0 - - # === - # Loop over time windows - # === - for wins in window: - - left_window_border = wins[0] - right_window_border = wins[1] - - # === - # pre-processing of the observed and sythetic - # to get windowed obsd and synt - # === - left_sample = int(np.floor(left_window_border / deltat)) - nlen = int(np.floor((right_window_border - left_window_border) / - deltat)) + 1 - right_sample = left_sample + nlen - - d = np.zeros(nlen) - s = np.zeros(nlen) - - d[0: nlen] = observed.data[left_sample: right_sample] - s[0: nlen] = synthetic.data[left_sample: right_sample] - - # Taper signals following the SAC taper command - window_taper(d, taper_percentage=config.taper_percentage, - taper_type=config.taper_type) - window_taper(s, taper_percentage=config.taper_percentage, - taper_type=config.taper_type) - - # cross-correlation - cc_shift = _xcorr_shift(d, s) - cc_tshift = cc_shift * deltat - cc_dlna = 0.5 * np.log(sum(d**2) / sum(s**2)) - - # uncertainty estimate based on cross-correlations - sigma_dt_cc = 1.0 - sigma_dlna_cc = 1.0 - - if use_cc_error: - sigma_dt_cc, sigma_dlna_cc = cc_error(d, s, deltat, cc_shift, - cc_dlna, config.dt_sigma_min, - config.dlna_sigma_min) - - logger.debug("cc_dt : %f +/- %f" % (cc_tshift, sigma_dt_cc)) - logger.debug("cc_dlna: %f +/- %f" % (cc_dlna, sigma_dlna_cc)) - - # re-window observed to align observed with synthetic for multitaper - # measurement: - left_sample_d = max(left_sample + cc_shift, 0) - right_sample_d = min(right_sample + cc_shift, nlen_data) - - nlen_d = right_sample_d - left_sample_d - - if nlen_d == nlen: - # Y. Ruan: No need to correct cc_dlna in multitaper measurements - d[0:nlen] = observed.data[left_sample_d:right_sample_d] - d *= np.exp(-cc_dlna) - window_taper(d, taper_percentage=config.taper_percentage, - taper_type=config.taper_type) - else: - raise Exception - - # === - # Make decision wihich method to use: c.c. or multi-taper - # always starts from multi-taper, if it doesn't work then - # switch to cross correlation misfit - # === - is_mtm = True - - # frequencies for FFT - freq = np.fft.fftfreq(n=nlen_f, d=observed.stats.delta) - df = freq[1] - freq[0] - wvec = freq * 2 * np.pi - # todo: check again see if dw is not used. - # dw = wvec[1] - wvec[0] - - # check window if okay for mtm measurements, and then find min/max - # frequency limit for calculations. - nfreq_min, nfreq_max, is_mtm = \ - frequency_limit(s, nlen, nlen_f, deltat, df, wtr, ncycle_in_window, - min_period, max_period, config.mt_nw) - - if is_mtm: - # Set the Rayleigh bin parameter (determin taper bandwithin - # frequency domain): nw (typical values are 2.5,3,3.5,4). - nw = config.mt_nw - ntaper = config.num_taper - - # generate discrete prolate slepian sequences - tapers = dpss_windows(nlen, nw, ntaper)[0].T - - # normalization - tapers = tapers * np.sqrt(nlen) - - # measure frequency-dependent phase and amplitude difference - phi_mtm = np.zeros(nlen_f) - abs_mtm = np.zeros(nlen_f) - - phi_mtm, abs_mtm, dtau_mtm, dlna_mtm =\ - mt_measure(d, s, deltat, tapers, wvec, df, nlen_f, - waterlevel_mtm, phase_step, nfreq_min, nfreq_max, - cc_tshift, cc_dlna) - - # multi-taper error estimation - sigma_phi_mt = np.zeros(nlen_f) - sigma_abs_mt = np.zeros(nlen_f) - sigma_dtau_mt = np.zeros(nlen_f) - sigma_dlna_mt = np.zeros(nlen_f) - - sigma_phi_mt, sigma_abs_mt, sigma_dtau_mt, sigma_dlna_mt =\ - mt_error(d, s, deltat, tapers, wvec, df, nlen_f, - waterlevel_mtm, phase_step, nfreq_min, nfreq_max, - cc_tshift, cc_dlna, phi_mtm, abs_mtm, dtau_mtm, - dlna_mtm) - - # check is_mtm again if the multitaper measurement results failed - # the selctuing criteria. change is_mtm if it's not okay - is_mtm = mt_measure_select(nfreq_min, nfreq_max, df, nlen, deltat, - dtau_mtm, dt_fac, sigma_dtau_mt, - err_fac, cc_tshift, dt_max_scale) - - # final decision which misfit will be used for adjoint source. - if is_mtm: - # calculate multi-taper adjoint source - fp_t, fq_t, misfit_p, misfit_q =\ - mt_adj(d, s, deltat, tapers, dtau_mtm, dlna_mtm, df, nlen_f, - use_cc_error, use_mt_error, nfreq_min, nfreq_max, - sigma_dt_cc, sigma_dlna_cc, sigma_dtau_mt, - sigma_dlna_mt, wtr) - - else: - # calculate c.c. adjoint source - fp_t, fq_t, misfit_p, misfit_q =\ - cc_adj(s, cc_shift, cc_dlna, deltat, sigma_dt_cc, - sigma_dlna_cc) - - # return to original location before windowing - # initialization - fp_wind = np.zeros(len(synthetic.data)) - fq_wind = np.zeros(len(synthetic.data)) - - fp_wind[left_sample: right_sample] = fp_t[0:nlen] - fq_wind[left_sample: right_sample] = fq_t[0:nlen] - - fp += fp_wind - fq += fq_wind - - misfit_sum_p += misfit_p - misfit_sum_q += misfit_q - - ret_val_p["misfit"] = misfit_sum_p - ret_val_q["misfit"] = misfit_sum_q - - if adjoint_src is True: - # Reverse in time and reverse the actual values. - ret_val_p["adjoint_source"] = fp[::-1] - ret_val_q["adjoint_source"] = fq[::-1] - - if config.measure_type == "dt": - if figure: - generic_adjoint_source_plot(observed, synthetic, - ret_val_p["adjoint_source"], - ret_val_p["misfit"], - window, VERBOSE_NAME) - - return ret_val_p - - if config.measure_type == "am": - if figure: - generic_adjoint_source_plot(observed, synthetic, - ret_val_q["adjoint_source"], - ret_val_q["misfit"], - window, VERBOSE_NAME) - - return ret_val_q diff --git a/src/pyadjoint/adjoint_source_types/waveform_misfit.py b/src/pyadjoint/adjoint_source_types/waveform_misfit.py deleted file mode 100644 index ddb4fb7..0000000 --- a/src/pyadjoint/adjoint_source_types/waveform_misfit.py +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env python -# -*- encoding: utf8 -*- -""" -Simple waveform misfit and adjoint source. - -This file will also serve as an explanation of how to add new adjoint -sources to Pyadjoint. - -:copyright: - Lion Krischer (krischer@geophysik.uni-muenchen.de), 2015 -:license: - BSD 3-Clause ("BSD New" or "BSD Simplified") -""" -from __future__ import absolute_import, division, print_function - -import numpy as np -from scipy.integrate import simps - -from ..utils import generic_adjoint_source_plot -from ..utils import window_taper - - -# This is the verbose and pretty name of the adjoint source defined in this -# function. -VERBOSE_NAME = "Waveform Misfit" - -# Long and detailed description of the adjoint source defined in this file. -# Don't spare any details. This will be rendered as restructured text in the -# documentation. Be careful to escape the string with an ``r`` prefix. -# Otherwise most backslashes will have a special meaning which messes with the -# TeX like formulas. -DESCRIPTION = r""" -This is the simplest of all misfits and is defined as the squared difference -between observed and synthetic data. The misfit :math:`\chi(\mathbf{m})` for a -given Earth model :math:`\mathbf{m}` and a single receiver and component is -given by - -.. math:: - - \chi (\mathbf{m}) = \frac{1}{2} \int_0^T \left| \mathbf{d}(t) - - \mathbf{s}(t, \mathbf{m}) \right| ^ 2 dt - -:math:`\mathbf{d}(t)` is the observed data and -:math:`\mathbf{s}(t, \mathbf{m})` the synthetic data. - -The adjoint source for the same receiver and component is given by - -.. math:: - - f^{\dagger}(t) = - \left[ \mathbf{d}(T - t) - - \mathbf{s}(T - t, \mathbf{m}) \right] - -For the sake of simplicity we omit the spatial Kronecker delta and define -the adjoint source as acting solely at the receiver's location. For more -details, please see [Tromp2005]_ and [Bozdag2011]_. - -This particular implementation here uses -`Simpson's rule `_ -to evaluate the definite integral. -""" - -# Optional: document any additional parameters this particular adjoint sources -# receives in addition to the ones passed to the central adjoint source -# calculation function. Make sure to indicate the default values. This is a -# bit redundant but the only way I could figure out to make it work with the -# rest of the architecture of pyadjoint. -ADDITIONAL_PARAMETERS = r""" -**taper_percentage** (:class:`float`) - Decimal percentage of taper at one end (ranging from ``0.0`` (0%) to - ``0.5`` (50%)). Defaults to ``0.15``. - -**taper_type** (:class:`str`) - The taper type, supports anything :method:`obspy.core.trace.Trace.taper` - can use. Defaults to ``"hann"``. -""" - - -# Each adjoint source file must contain a calculate_adjoint_source() -# function. It must take observed, synthetic, min_period, max_period, -# left_window_border, right_window_border, adjoint_src, and figure as -# parameters. Other optional keyword arguments are possible. -def calculate_adjoint_source(observed, synthetic, config, window, - adjoint_src, figure): # NOQA - - ret_val = {} - - nlen_data = len(synthetic.data) - deltat = synthetic.stats.delta - - adj = np.zeros(nlen_data) - - misfit_sum = 0.0 - - # loop over time windows - for wins in window: - left_window_border = wins[0] - right_window_border = wins[1] - - left_sample = int(np.floor(left_window_border / deltat)) + 1 - nlen = int(np.floor((right_window_border - left_window_border) / - deltat)) + 1 - right_sample = left_sample + nlen - - d = np.zeros(nlen) - s = np.zeros(nlen) - - d[0: nlen] = observed.data[left_sample: right_sample] - s[0: nlen] = synthetic.data[left_sample: right_sample] - - # All adjoint sources will need some kind of windowing taper - # to get rid of kinks at two ends - window_taper(d, taper_percentage=config.taper_percentage, - taper_type=config.taper_type) - window_taper(s, taper_percentage=config.taper_percentage, - taper_type=config.taper_type) - - diff = s - d - # Integrate with the composite Simpson's rule. - diff_w = diff * -1.0 - window_taper(diff_w, taper_percentage=config.taper_percentage, - taper_type=config.taper_type) - # for some reason the 0.5 (see 2012 measure_adj mannual, P11) is - # not in misfit definetion in measure_adj - # misfit_sum += 0.5 * simps(y=diff_w**2, dx=deltat) - misfit_sum += simps(y=diff_w**2, dx=deltat) - - adj[left_sample: right_sample] = diff[0:nlen] - - ret_val["misfit"] = misfit_sum - - if adjoint_src is True: - # Reverse in time - ret_val["adjoint_source"] = adj[::-1] - - if figure: - # return NotImplemented - generic_adjoint_source_plot(observed, synthetic, - ret_val["adjoint_source"], - ret_val["misfit"], - window, VERBOSE_NAME) - - return ret_val diff --git a/src/pyadjoint/config.py b/src/pyadjoint/config.py deleted file mode 100644 index b9711df..0000000 --- a/src/pyadjoint/config.py +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Configuration object for pyadjoint. - -:copyright: - Youyi Ruan (youyir@princeton.edu), 2016 - Lion Krischer (krischer@geophysik.uni-muenchen.de), 2016 -:license: - GNU General Public License, Version 3 - (http://www.gnu.org/copyleft/gpl.html) -""" -from __future__ import absolute_import, division, print_function - - -class Config(object): - def __init__(self, min_period, max_period, - lnpt=15, - transfunc_waterlevel=1.0E-10, - water_threshold=0.02, - ipower_costaper=10, - min_cycle_in_window=0.5, - taper_type='hann', - taper_percentage=0.3, - mt_nw=4.0, - num_taper=5, - dt_fac=2.0, - phase_step=1.5, - err_fac=2.5, - dt_max_scale=3.5, - measure_type='dt', - dt_sigma_min=1.0, - dlna_sigma_min=0.5, - use_cc_error=True, - use_mt_error=False): - """ - Central configuration object for Pyadjoint. - - - config = Config( - min_period, max_period, - lnpt = 15, - transfunc_waterlevel=1.0E-10, - water_threshold=0.02, - ipower_costaper=10, - min_cycle_in_window=3, - taper_type='hann', - taper_percentage=0.15, - mt_nw=4.0, - num_taper=5, - phase_step=1.5, - dt_fac=2.0, - err_fac=2.5, - dt_max_scale=3.5, - measure_type='dt', - dt_sigma_min=1.0, - dlna_sigma_min=0.5, - use_cc_error=False, - use_mt_error=False) - - :param min_period: Minimum period of the filtered input data in - seconds. - :type min_period: float - - :param max_period: Maximum period of the filtered input data in - seconds. - :type max_period: float - - :param lnpt: power index to determin the time lenght use in - FFT (2^lnpt) - :type lnpt: int - - :param transfunc_waterlevel: Water level on the transfer function - between data and synthetic. - :type transfunc_waterlevel: float - - :param ipower_costaper: order of cosine taper, higher the value, - steeper the shoulders. - :type ipower_costaper: int - - :param min_cycle_in_window: Minimum cycle of a wave in time window to - determin the maximum period can be reliably measured. - :type min_cycle_in_window: int - - :param taper_percentage: Percentage of a time window needs to be - tapered at two ends, to remove the non-zero values for adjoint - source and for fft. - :type taper_percentage: float - - :param taper_type: Taper type, supports - "hann", "cos", "cos_p10" so far - :type taper_type: str - - :param mt_nw: bin width of multitapers (nw*df is the the half - bandwidth of multitapers in frequency domain, - typical values are 2.5, 3., 3.5, 4.0) - :type mt_nw: float - - :param num_taper: number of eigen tapers (2*nw - 3 gives tapers - with eigen values larger than 0.96) - :type num_taper: int - - :param dt_fac - :type dt_fac: float - - :param err_fac - :type err_fac: float - - :param dt_max_scale - :type dt_max_scale: float - - :param phase_step: maximum step for cycle skip correction (?) - :type phase_step: float - - :param dt_sigma_min: minimum travel time error allowed - :type dt_sigma_min: float - - :param dlna_sigma_min: minimum amplitude error allowed - :type dlna_sigma_min: float - - :param measure_type: type of measurements: - dt(travel time), - am(dlnA), - wf(full waveform) - :param measure_type: string - - :param use_cc_error: use cross correlation errors for - :type use_cc_error: logic - - :param use_mt_error: use multi-taper error - :type use_mt_error: logic - """ - - self.min_period = min_period - self.max_period = max_period - - self.lnpt = lnpt - - self.transfunc_waterlevel = transfunc_waterlevel - self.water_threshold = water_threshold - - self.ipower_costaper = ipower_costaper - - self.min_cycle_in_window = min_cycle_in_window - - self.taper_type = taper_type - self.taper_percentage = taper_percentage - - self.mt_nw = mt_nw - self.num_taper = num_taper - self.phase_step = phase_step - - self.dt_fac = dt_fac - self.err_fac = err_fac - self.dt_max_scale = dt_max_scale - - self.dt_sigma_min = dt_sigma_min - self.dlna_sigma_min = dlna_sigma_min - - self.measure_type = measure_type - self.use_cc_error = use_cc_error - self.use_mt_error = use_mt_error diff --git a/src/pyadjoint/tests/__main__.py b/src/pyadjoint/tests/__main__.py deleted file mode 100644 index c5985e2..0000000 --- a/src/pyadjoint/tests/__main__.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -The only purpose of this file is to be able to run the pyadoint test suite with - -python -m pyadoint.tests - -:copyright: - Lion Krischer (krischer@geophysik.uni-muenchen.de), 2015 -:license: - BSD 3-Clause ("BSD New" or "BSD Simplified") -""" -from __future__ import absolute_import, division, print_function - -import inspect -import os -import pytest -import sys - - -if __name__ == "__main__": - PATH = os.path.dirname(os.path.dirname( - os.path.abspath(inspect.getfile(inspect.currentframe())))) - - sys.exit(pytest.main(PATH)) diff --git a/src/pyadjoint/tests/test_all_adjoint_sources.py b/src/pyadjoint/tests/test_all_adjoint_sources.py deleted file mode 100644 index 289b1db..0000000 --- a/src/pyadjoint/tests/test_all_adjoint_sources.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env pythonG -# -*- coding: utf-8 -*- -""" -Automated tests for all defined adjoint sources. Essentially just checks -that they all work and do something. - -:copyright: - Lion Krischer (krischer@geophysik.uni-muenchen.de), 2015 -:license: - BSD 3-Clause ("BSD New" or "BSD Simplified") -""" -from __future__ import absolute_import, division, print_function - -import numpy as np -import pytest - -import pyadjoint - - -@pytest.fixture(params=list(pyadjoint.AdjointSource._ad_srcs.keys())) -def adj_src(request): - """ - Fixture returning the name of all adjoint sources. - """ - return request.param - - -def test_normal_adjoint_source_calculation(adj_src): - """ - Make sure everything at least runs. Executed for every adjoint source type. - """ - config = pyadjoint.Config(min_period=30.0, max_period=75.0, - lnpt=15, - transfunc_waterlevel=1.0E-10, - water_threshold=0.02, - ipower_costaper=10, - min_cycle_in_window=3, - taper_percentage=0.3, - taper_type='hann', - mt_nw=4, - phase_step=1.5, - use_cc_error=False, - use_mt_error=False) - - obs, syn = pyadjoint.utils.get_example_data() - obs = obs.select(component="Z")[0] - syn = syn.select(component="Z")[0] - # start, end = pyadjoint.utils.EXAMPLE_DATA_PDIFF - - window = [[2076., 2418.0]] - - a_src = pyadjoint.calculate_adjoint_source( - adj_src_type=adj_src, observed=obs, - synthetic=syn, config=config, window=window, - adjoint_src=True, plot=False) - - # a_src = pyadjoint.calculate_adjoint_source( - # adj_src, obs, syn, 20, 100, start, end) - - assert a_src.adjoint_source.any() - assert a_src.misfit >= 0.0 - - assert isinstance(a_src.adjoint_source, np.ndarray) - - -def test_no_adjoint_src_calculation_is_honored(adj_src): - """ - If no adjoint source is requested, it should not be returned/calculated. - """ - config = pyadjoint.Config(min_period=30.0, max_period=75.0, - lnpt=15, - transfunc_waterlevel=1.0E-10, - water_threshold=0.02, - ipower_costaper=10, - min_cycle_in_window=3, - taper_percentage=0.3, - taper_type='hann', - mt_nw=4, - phase_step=1.5, - use_cc_error=False, - use_mt_error=False) - - obs, syn = pyadjoint.utils.get_example_data() - obs = obs.select(component="Z")[0] - syn = syn.select(component="Z")[0] - window = [[2076., 2418.0]] - - a_src = pyadjoint.calculate_adjoint_source( - adj_src_type=adj_src, observed=obs, - synthetic=syn, config=config, window=window, - adjoint_src=False, plot=False) - # start, end = pyadjoint.utils.EXAMPLE_DATA_PDIFF - # a_src = pyadjoint.calculate_adjoint_source( - # adj_src, obs, syn, 20, 100, start, end, adjoint_src=False) - - assert a_src.adjoint_source is None - # assert a_src.misfit >= 0.0 - - # But the misfit should nonetheless be identical as if the adjoint - # source would have been calculated. - - # assert a_src.misfit == pyadjoint.calculate_adjoint_source( - # adj_src_type=adj_src, observed=obs, - # synthetic=syn, config=config, window=window, - # adjoint_src=True, plot=False).misfit - - # pyadjoint.calculate_adjoint_source( - # adj_src, obs, syn, 20, 100, start, end, adjoint_src=True).misfit diff --git a/src/pyadjoint/tests/test_code_formatting.py b/src/pyadjoint/tests/test_code_formatting.py deleted file mode 100644 index aefee29..0000000 --- a/src/pyadjoint/tests/test_code_formatting.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env pythonG -# -*- coding: utf-8 -*- -""" -Tests all Python files of the project with flake8. This ensure PEP8 conformance -and some other sanity checks as well. - -:copyright: - Lion Krischer (krischer@geophysik.uni-muenchen.de), 2015 -:license: - BSD 3-Clause ("BSD New" or "BSD Simplified") -""" -import flake8 -import flake8.engine -import flake8.main -import inspect -import os - - -def test_flake8(): - test_dir = os.path.dirname(os.path.abspath(inspect.getfile( - inspect.currentframe()))) - pyadjoint_dir = os.path.dirname(test_dir) - - # Possibility to ignore some files and paths. - ignore_paths = [ - os.path.join(pyadjoint_dir, "doc"), - os.path.join(pyadjoint_dir, ".git")] - files = [] - for dirpath, _, filenames in os.walk(pyadjoint_dir): - ignore = False - for path in ignore_paths: - if dirpath.startswith(path): - ignore = True - break - if ignore: - continue - filenames = [_i for _i in filenames if - os.path.splitext(_i)[-1] == os.path.extsep + "py"] - if not filenames: - continue - for py_file in filenames: - full_path = os.path.join(dirpath, py_file) - files.append(full_path) - - # Get the style checker with the default style. - flake8_style = flake8.engine.get_style_guide( - parse_argv=False, config_file=flake8.main.DEFAULT_CONFIG) - - report = flake8_style.check_files(files) - - # Make sure at least 4 files are tested. - assert report.counters["files"] >= 4 - # And no errors occured. - assert report.get_count() == 0 diff --git a/src/pyadjoint/utils.py b/src/pyadjoint/utils.py deleted file mode 100644 index 72dbfe5..0000000 --- a/src/pyadjoint/utils.py +++ /dev/null @@ -1,262 +0,0 @@ -#!/usr/bin/env python -# -*- encoding: utf8 -*- -""" -Utility functions for Pyadjoint. - -:copyright: - Lion Krischer (krischer@geophysik.uni-muenchen.de), 2015 -:license: - BSD 3-Clause ("BSD New" or "BSD Simplified") -""" -from __future__ import absolute_import, division, print_function - -import inspect -import os - -import matplotlib.pyplot as plt -import matplotlib.patches as patches -import numpy as np -import obspy - -EXAMPLE_DATA_PDIFF = (800, 900) -EXAMPLE_DATA_SDIFF = (1500, 1600) - - -def taper_window(trace, left_border_in_seconds, right_border_in_seconds, - taper_percentage, taper_type, **kwargs): - """ - Helper function to taper a window within a data trace. - - This function modifies the passed trace object in-place. - - :param trace: The trace to be tapered. - :type trace: :class:`obspy.core.trace.Trace` - :param left_border_in_seconds: The left window border in seconds since - the first sample. - :type left_border_in_seconds: float - :param right_border_in_seconds: The right window border in seconds since - the first sample. - :type right_border_in_seconds: float - :param taper_percentage: Decimal percentage of taper at one end (ranging - from ``0.0`` (0%) to ``0.5`` (50%)). - :type taper_percentage: float - :param taper_type: The taper type, supports anything - :meth:`obspy.core.trace.Trace.taper` can use. - :type taper_type: str - - Any additional keyword arguments are passed to the - :meth:`obspy.core.trace.Trace.taper` method. - - - .. rubric:: Example - - >>> import obspy - >>> tr = obspy.read()[0] - >>> tr.plot() - - .. plot:: - - import obspy - tr = obspy.read()[0] - tr.plot() - - >>> from pyadjoint.utils import taper_window - >>> taper_window(tr, 4, 11, taper_percentage=0.10, taper_type="hann") - >>> tr.plot() - - .. plot:: - - import obspy - from pyadjoint.utils import taper_window - tr = obspy.read()[0] - taper_window(tr, 4, 11, taper_percentage=0.10, taper_type="hann") - tr.plot() - - """ - s, e = trace.stats.starttime, trace.stats.endtime - trace.trim(s + left_border_in_seconds, s + right_border_in_seconds) - trace.taper(max_percentage=taper_percentage, type=taper_type, **kwargs) - trace.trim(s, e, pad=True, fill_value=0.0) - # Enable method chaining. - return trace - - -def window_taper(signal, taper_percentage, taper_type): - """ - window taper function. - - :param signal: time series - :type signal: ndarray(float) - - :param taper_percentage: total percentage of taper in decimal - :type taper_percentage: float - - return : tapered input ndarray - - taper_type: - 1, cos - 2, cos_p10 - 3, hann - 4, hamming - - To do: - with options of more tapers - """ - taper_collection = ('cos', 'cos_p10', 'hann', "hamming") - - if taper_type not in taper_collection: - raise ValueError("Window taper not supported") - - if taper_percentage < 0 or taper_percentage > 1: - raise ValueError("Wrong taper percentage") - - npts = len(signal) - - if taper_percentage == 0.0 or taper_percentage == 1.0: - frac = int(npts*taper_percentage / 2.0) - else: - frac = int(npts*taper_percentage / 2.0 + 0.5) - - idx1 = frac - idx2 = npts - frac - - if taper_type == 'hann': - signal[:idx1] *=\ - (0.5 - 0.5 * np.cos(2.0 * np.pi * np.arange(0, frac) / - (2 * frac - 1))) - signal[idx2:] *=\ - (0.5 - 0.5 * np.cos(2.0 * np.pi * np.arange(frac, 2 * frac) / - (2 * frac - 1))) - - if taper_type == 'hamming': - signal[:idx1] *=\ - (0.54 - 0.46 * np.cos(2.0 * np.pi * np.arange(0, frac) / - (2 * frac - 1))) - signal[idx2:] *=\ - (0.54 - 0.46 * np.cos(2.0 * np.pi * np.arange(frac, 2 * frac) / - (2 * frac - 1))) - - if taper_type == 'cos': - power = 1. - signal[:idx1] *= np.cos(np.pi * np.arange(0, frac) / - (2 * frac - 1) - np.pi / 2.0) ** power - signal[idx2:] *= np.cos(np.pi * np.arange(frac, 2 * frac) / - (2 * frac - 1) - np.pi / 2.0) ** power - - if taper_type == 'cos_p10': - power = 10. - signal[:idx1] *= 1. - np.cos(np.pi * np.arange(0, frac) / - (2 * frac - 1)) ** power - signal[idx2:] *= 1. - np.cos(np.pi * np.arange(frac, 2 * frac) / - (2 * frac - 1)) ** power - return signal - - -def get_example_data(): - """ - Helper function returning example data for Pyadjoint. - - The returned data is fully preprocessed and ready to be used with Pyflex. - - :returns: Tuple of observed and synthetic streams - :rtype: tuple of :class:`obspy.core.stream.Stream` objects - - .. rubric:: Example - - >>> from pyadjoint.utils import get_example_data - >>> observed, synthetic = get_example_data() - >>> print(observed) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE - 3 Trace(s) in Stream: - SY.DBO.S3.MXR | 2014-11-15T02:31:50.259999Z - ... | 1.0 Hz, 3600 samples - SY.DBO.S3.MXT | 2014-11-15T02:31:50.259999Z - ... | 1.0 Hz, 3600 samples - SY.DBO.S3.MXZ | 2014-11-15T02:31:50.259999Z - ... | 1.0 Hz, 3600 samples - >>> print(synthetic) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE - 3 Trace(s) in Stream: - SY.DBO..LXR | 2014-11-15T02:31:50.259999Z - ... | 1.0 Hz, 3600 samples - SY.DBO..LXT | 2014-11-15T02:31:50.259999Z - ... | 1.0 Hz, 3600 samples - SY.DBO..LXZ | 2014-11-15T02:31:50.259999Z - ... | 1.0 Hz, 3600 samples - """ - path = os.path.join( - os.path.dirname(inspect.getfile(inspect.currentframe())), - "example_data") - observed = obspy.read(os.path.join(path, "observed_processed.mseed")) - observed.sort() - synthetic = obspy.read(os.path.join(path, "synthetic_processed.mseed")) - synthetic.sort() - - return observed, synthetic - - -def generic_adjoint_source_plot(observed, synthetic, adjoint_source, misfit, - window, adjoint_source_name): - """ - Generic plotting function for adjoint sources and data. - - Many types of adjoint sources can be represented in the same manner. - This is a convenience function that can be called by different - the implementations for different adjoint sources. - - :param observed: The observed data. - :type observed: :class:`obspy.core.trace.Trace` - :param synthetic: The synthetic data. - :type synthetic: :class:`obspy.core.trace.Trace` - :param adjoint_source: The adjoint source. - :type adjoint_source: `numpy.ndarray` - :param misfit: The associated misfit value. - :float misfit: misfit value - :param left_window_border: Left border of the window to be tapered in - seconds since the first sample in the data arrays. - :type left_window_border: float - :param right_window_border: Right border of the window to be tapered in - seconds since the first sample in the data arrays. - :type right_window_border: float - :param adjoint_source_name: The name of the adjoint source. - :type adjoint_source_name: str - """ - x_range = observed.stats.endtime - observed.stats.starttime - left_window_border = 60000. - right_window_border = 0. - - for wins in window: - left_window_border = min(left_window_border, wins[0]) - right_window_border = max(right_window_border, wins[1]) - - buf = (right_window_border - left_window_border) * 0.3 - left_window_border -= buf - right_window_border += buf - left_window_border = max(0, left_window_border) - right_window_border = min(x_range, right_window_border) - - plt.subplot(211) - plt.plot(observed.times(), observed.data, color="0.2", label="Observed", - lw=2) - plt.plot(synthetic.times(), synthetic.data, color="#bb474f", - label="Synthetic", lw=2) - for wins in window: - re = patches.Rectangle((wins[0], plt.ylim()[0]), - wins[1] - wins[0], - plt.ylim()[1] - plt.ylim()[0], - color="blue", alpha=0.1) - plt.gca().add_patch(re) - - plt.grid() - plt.legend(fancybox=True, framealpha=0.5) - plt.xlim(left_window_border, right_window_border) - ylim = max(map(abs, plt.ylim())) - plt.ylim(-ylim, ylim) - - plt.subplot(212) - plt.plot(observed.times(), adjoint_source[::-1], color="#2f8d5b", lw=2, - label="Adjoint Source") - plt.grid() - plt.legend(fancybox=True, framealpha=0.5) - # No time reversal for comparison with data - # plt.xlim(x_range - right_window_border, x_range - left_window_border) - # plt.xlabel("Time in seconds since first sample") - plt.xlim(left_window_border, right_window_border) - plt.xlabel("Time (seconds)") - ylim = max(map(abs, plt.ylim())) - plt.ylim(-ylim, ylim) - - plt.suptitle("%s Adjoint Source with a Misfit of %.3g" % ( - adjoint_source_name, misfit))