├── .coveragerc ├── .github └── workflows │ ├── build.yaml │ ├── codacy-coverage-reporter.yaml │ ├── deploy.yaml │ └── flake8.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── MANIFEST.in ├── MIGRATION_V1_V2.md ├── Makefile ├── README.md ├── azure-pipelines.yml ├── docs ├── Makefile └── source │ ├── _static │ ├── css │ │ └── custom.css │ ├── cupy_diagram.png │ ├── favicon.ico │ ├── numpy_cupy_bd_diagram.png │ ├── numpy_cupy_vs_diagram.png │ ├── pylops.png │ ├── pylops_b.png │ └── style.css │ ├── _templates │ ├── autosummary │ │ ├── base.rst │ │ ├── class.rst │ │ ├── exception.rst │ │ ├── function.rst │ │ └── module.rst │ └── layout.html │ ├── adding.rst │ ├── addingsolver.rst │ ├── api │ ├── index.rst │ └── others.rst │ ├── changelog.rst │ ├── citing.rst │ ├── conf.py │ ├── contributing.rst │ ├── credits.rst │ ├── extensions.rst │ ├── faq.rst │ ├── gpu.rst │ ├── index.rst │ ├── installation.rst │ ├── papers.rst │ └── roadmap.rst ├── environment-dev-arm.yml ├── environment-dev-gpu.yml ├── environment-dev.yml ├── environment.yml ├── examples ├── README.txt ├── plot_avo.py ├── plot_bayeslinearregr.py ├── plot_bilinear.py ├── plot_blending.py ├── plot_causalintegration.py ├── plot_cgls.py ├── plot_chirpradon.py ├── plot_conj.py ├── plot_convolve.py ├── plot_dct.py ├── plot_derivative.py ├── plot_describe.py ├── plot_diagonal.py ├── plot_dtcwt.py ├── plot_fft.py ├── plot_flip.py ├── plot_fourierradon.py ├── plot_identity.py ├── plot_imag.py ├── plot_ista.py ├── plot_l1l1.py ├── plot_linearregr.py ├── plot_matrixmult.py ├── plot_mdc.py ├── plot_multiproc.py ├── plot_nmo.py ├── plot_nonstatconvolve.py ├── plot_nonstatfilter.py ├── plot_pad.py ├── plot_patching.py ├── plot_phaseshift.py ├── plot_prestack.py ├── plot_radon.py ├── plot_real.py ├── plot_regr.py ├── plot_restriction.py ├── plot_roll.py ├── plot_seislet.py ├── plot_seismicevents.py ├── plot_shift.py ├── plot_sliding.py ├── plot_slopeest.py ├── plot_smoothing1d.py ├── plot_smoothing2d.py ├── plot_spread.py ├── plot_stacking.py ├── plot_sum.py ├── plot_symmetrize.py ├── plot_tapers.py ├── plot_transpose.py ├── plot_tvreg.py ├── plot_twoway.py ├── plot_wavelet.py ├── plot_wavest.py ├── plot_wavs.py └── plot_zero.py ├── pylops ├── __init__.py ├── _torchoperator.py ├── avo │ ├── __init__.py │ ├── avo.py │ ├── poststack.py │ └── prestack.py ├── basicoperators │ ├── __init__.py │ ├── _spread_numba.py │ ├── block.py │ ├── blockdiag.py │ ├── causalintegration.py │ ├── conj.py │ ├── diagonal.py │ ├── directionalderivative.py │ ├── firstderivative.py │ ├── flip.py │ ├── functionoperator.py │ ├── gradient.py │ ├── hstack.py │ ├── identity.py │ ├── imag.py │ ├── kronecker.py │ ├── laplacian.py │ ├── linearregression.py │ ├── matrixmult.py │ ├── memoizeoperator.py │ ├── pad.py │ ├── real.py │ ├── regression.py │ ├── restriction.py │ ├── roll.py │ ├── secondderivative.py │ ├── smoothing1d.py │ ├── smoothing2d.py │ ├── spread.py │ ├── sum.py │ ├── symmetrize.py │ ├── tocupy.py │ ├── transpose.py │ ├── vstack.py │ └── zero.py ├── config.py ├── jaxoperator.py ├── linearoperator.py ├── optimization │ ├── __init__.py │ ├── basesolver.py │ ├── basic.py │ ├── callback.py │ ├── cls_basic.py │ ├── cls_leastsquares.py │ ├── cls_sparsity.py │ ├── eigs.py │ ├── leastsquares.py │ └── sparsity.py ├── pytensoroperator.py ├── signalprocessing │ ├── __init__.py │ ├── _baseffts.py │ ├── _chirpradon2d.py │ ├── _chirpradon3d.py │ ├── _fourierradon2d_cuda.py │ ├── _fourierradon2d_numba.py │ ├── _fourierradon3d_cuda.py │ ├── _fourierradon3d_numba.py │ ├── _nonstatconvolve2d_cuda.py │ ├── _nonstatconvolve3d_cuda.py │ ├── _radon2d_numba.py │ ├── _radon3d_numba.py │ ├── bilinear.py │ ├── chirpradon2d.py │ ├── chirpradon3d.py │ ├── convolve1d.py │ ├── convolve2d.py │ ├── convolvend.py │ ├── dct.py │ ├── dtcwt.py │ ├── dwt.py │ ├── dwt2d.py │ ├── dwtnd.py │ ├── fft.py │ ├── fft2d.py │ ├── fftnd.py │ ├── fourierradon2d.py │ ├── fourierradon3d.py │ ├── fredholm1.py │ ├── interp.py │ ├── nonstatconvolve1d.py │ ├── nonstatconvolve2d.py │ ├── nonstatconvolve3d.py │ ├── patch2d.py │ ├── patch3d.py │ ├── radon2d.py │ ├── radon3d.py │ ├── seislet.py │ ├── shift.py │ ├── sliding1d.py │ ├── sliding2d.py │ └── sliding3d.py ├── torchoperator.py ├── utils │ ├── __init__.py │ ├── _internal.py │ ├── backend.py │ ├── decorators.py │ ├── deps.py │ ├── describe.py │ ├── dottest.py │ ├── estimators.py │ ├── metrics.py │ ├── multiproc.py │ ├── seismicevents.py │ ├── signalprocessing.py │ ├── tapers.py │ ├── typing.py │ ├── utils.py │ └── wavelets.py └── waveeqprocessing │ ├── __init__.py │ ├── _twoway.py │ ├── blending.py │ ├── kirchhoff.py │ ├── lsm.py │ ├── marchenko.py │ ├── mdd.py │ ├── oneway.py │ ├── seismicinterpolation.py │ ├── twoway.py │ └── wavedecomposition.py ├── pyproject.toml ├── pytests ├── test_avo.py ├── test_basicoperators.py ├── test_blending.py ├── test_causalintegration.py ├── test_chirpradon.py ├── test_combine.py ├── test_convolve.py ├── test_dct.py ├── test_derivative.py ├── test_describe.py ├── test_diagonal.py ├── test_directwave.py ├── test_dtcwt.py ├── test_dwts.py ├── test_eigs.py ├── test_estimators.py ├── test_ffts.py ├── test_fourierradon.py ├── test_fredholm.py ├── test_functionoperator.py ├── test_interpolation.py ├── test_jaxoperator.py ├── test_kirchhoff.py ├── test_kronecker.py ├── test_leastsquares.py ├── test_linearoperator.py ├── test_lsm.py ├── test_marchenko.py ├── test_memoizeoperator.py ├── test_metrics.py ├── test_nonstatconvolve.py ├── test_oneway.py ├── test_pad.py ├── test_patching.py ├── test_poststack.py ├── test_prestack.py ├── test_pytensoroperator.py ├── test_radon.py ├── test_restriction.py ├── test_seislet.py ├── test_seismicevents.py ├── test_seismicinterpolation.py ├── test_shift.py ├── test_signalutils.py ├── test_sliding.py ├── test_smoothing.py ├── test_solver.py ├── test_sparsity.py ├── test_tapers.py ├── test_torchoperator.py ├── test_transpose.py ├── test_twoway.py ├── test_utils.py ├── test_wavedecomposition.py ├── test_waveeqprocessing.py └── test_wavelets.py ├── requirements-dev-gpu.txt ├── requirements-dev.txt ├── requirements-doc.txt ├── requirements-torch.txt ├── requirements.txt ├── setup.cfg ├── testdata ├── avo │ └── poststack_model.npz ├── deblending │ └── mobil.npy ├── marchenko │ ├── direct3D.npz │ └── input.npz ├── optimization │ └── shepp_logan_phantom.npy ├── python.npy ├── python.png ├── sigmoid.npz ├── slope_estimate │ ├── concentric.png │ ├── concentric_angles.png │ ├── core_sample.png │ ├── core_sample_anisotropy.png │ └── core_sample_orientation.png └── updown │ └── input.npz └── tutorials ├── README.txt ├── bayesian.py ├── classsolvers.py ├── ctscan.py ├── deblending.py ├── deblurring.py ├── deghosting.py ├── dottest.py ├── ilsm.py ├── interpolation.py ├── jaxop.py ├── linearoperator.py ├── lsm.py ├── marchenko.py ├── mdd.py ├── poststack.py ├── prestack.py ├── radonfiltering.py ├── realcomplex.py ├── seismicinterpolation.py ├── solvers.py ├── torchop.py └── wavefielddecomposition.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = pylops 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: PyLops 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | platform: [ ubuntu-latest, macos-13 ] 10 | python-version: ["3.10", "3.11"] 11 | 12 | runs-on: ${{ matrix.platform }} 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Get history and tags for SCM versioning to work 16 | run: | 17 | git fetch --prune --unshallow 18 | git fetch --depth=1 origin +refs/tags/*:refs/tags/* 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip setuptools 26 | pip install flake8 pytest 27 | pip install -r requirements-dev.txt 28 | pip install -r requirements-torch.txt 29 | - name: Install pylops 30 | run: | 31 | python -m setuptools_scm 32 | pip install . 33 | - name: Test with pytest 34 | run: | 35 | pytest 36 | -------------------------------------------------------------------------------- /.github/workflows/codacy-coverage-reporter.yaml: -------------------------------------------------------------------------------- 1 | # This workflow uploads PyLops coverage analysis on Codacy 2 | # For more information see: https://github.com/codacy/codacy-coverage-reporter-action 3 | name: PyLops-coverage 4 | 5 | on: [push, pull_request_target] 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | platform: [ ubuntu-latest, ] 12 | python-version: ["3.11", ] 13 | 14 | runs-on: ${{ matrix.platform }} 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Get history and tags for SCM versioning to work 18 | run: | 19 | git fetch --prune --unshallow 20 | git fetch --depth=1 origin +refs/tags/*:refs/tags/* 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install flake8 pytest 29 | pip install -r requirements-dev.txt 30 | pip install -r requirements-torch.txt 31 | - name: Install pylops 32 | run: | 33 | pip install . 34 | pip install coverage 35 | - name: Code coverage with coverage 36 | run: | 37 | coverage run -m pytest 38 | coverage xml 39 | - name: Run codacy-coverage-reporter 40 | uses: codacy/codacy-coverage-reporter-action@v1 41 | with: 42 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 43 | coverage-reports: coverage.xml 44 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | # This workflow uploads PyLops on PyPI using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | name: PyLops-deploy 4 | 5 | on: 6 | release: 7 | types: [published] 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: '3.x' 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install build 22 | - name: Build package 23 | run: python -m build 24 | - name: Publish package 25 | uses: pypa/gh-action-pypi-publish@release/v1 26 | with: 27 | user: __token__ 28 | password: ${{ secrets.PYPI_API_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/flake8.yaml: -------------------------------------------------------------------------------- 1 | # This workflow runs Flake8 on the PR 2 | # For more information see: https://github.com/marketplace/actions/python-flake8-lint 3 | name: PyLops-flake8 4 | 5 | on: [push, pull_request] 6 | 7 | jobs: 8 | flake8-lint: 9 | runs-on: ubuntu-latest 10 | name: Lint 11 | steps: 12 | - name: Check out source repository 13 | uses: actions/checkout@v4 14 | - name: Set up Python environment 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: "3.8" 18 | - name: flake8 Lint 19 | uses: py-actions/flake8@v2 20 | with: 21 | ignore: "E203,E501,W503,E402" 22 | max-line-length: "88" 23 | path: "pylops" 24 | args: "--per-file-ignores=__init__.py:F401,F403,F405" 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS generated files # 2 | .*.swp 3 | *.py[cod] 4 | .DS_Store 5 | .DS_Store? 6 | ._* 7 | .Spotlight-V100 8 | .Trashes 9 | ehthumbs.db 10 | Thumbs.db 11 | .idea/* 12 | static/* 13 | db.sqlite3 14 | 15 | # Build # 16 | build 17 | dist 18 | pylops.egg-info/ 19 | .eggs/ 20 | __pycache__ 21 | 22 | # setuptools_scm generated # 23 | pylops/version.py 24 | 25 | # Development # 26 | .ipynb_checkpoints/ 27 | notebooks 28 | TODO 29 | .vscode/ 30 | 31 | # Documentation # 32 | docs/build 33 | docs/source/api/generated 34 | docs/source/gallery 35 | docs/source/tutorials 36 | 37 | # Pylint # 38 | pylint_plot.py 39 | pylintrc 40 | 41 | # Coverage reports 42 | COVERAGE 43 | .coverage 44 | coverage.xml 45 | htmlcov/ 46 | 47 | # Airspeed velocity benchmarks 48 | ASV 49 | .asv/ 50 | asv.conf.json 51 | benchmarks/ 52 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: "^docs/" 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.0.1 5 | hooks: 6 | - id: trailing-whitespace 7 | - id: end-of-file-fixer 8 | - id: check-yaml 9 | 10 | - repo: https://github.com/psf/black 11 | rev: 22.3.0 12 | hooks: 13 | - id: black 14 | args: # arguments to configure black 15 | - --line-length=88 16 | 17 | - repo: https://github.com/pycqa/isort 18 | rev: 5.12.0 19 | hooks: 20 | - id: isort 21 | name: isort (python) 22 | args: 23 | [ 24 | "--profile", 25 | "black", 26 | "--skip", 27 | "__init__.py", 28 | "--filter-files", 29 | "--line-length=88", 30 | ] 31 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-20.04 11 | tools: 12 | python: "3.9" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/source/conf.py 17 | 18 | # Declare the Python requirements required to build your docs 19 | python: 20 | install: 21 | - requirements: requirements-doc.txt 22 | - requirements: requirements-torch.txt 23 | - method: pip 24 | path: . 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | exclude .* 2 | exclude environment.yml requirements.txt Makefile 3 | exclude environment-dev.yml requirements-dev.txt azure-pipelines.yml readthedocs.yml 4 | recursive-exclude docs * 5 | recursive-exclude examples * 6 | recursive-exclude pytests * 7 | recursive-exclude testdata * 8 | recursive-exclude tutorials * 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PIP := $(shell command -v pip3 2> /dev/null || command which pip 2> /dev/null) 2 | PYTHON := $(shell command -v python3 2> /dev/null || command which python 2> /dev/null) 3 | 4 | .PHONY: install dev-install dev-install_gpu install_conda dev-install_conda dev-install_conda_arm tests doc docupdate servedoc lint typeannot coverage 5 | 6 | pipcheck: 7 | ifndef PIP 8 | $(error "Ensure pip or pip3 are in your PATH") 9 | endif 10 | @echo Using pip: $(PIP) 11 | 12 | pythoncheck: 13 | ifndef PYTHON 14 | $(error "Ensure python or python3 are in your PATH") 15 | endif 16 | @echo Using python: $(PYTHON) 17 | 18 | install: 19 | make pipcheck 20 | $(PIP) install -r requirements.txt && $(PIP) install . 21 | 22 | dev-install: 23 | make pipcheck 24 | $(PIP) install -r requirements-dev.txt &&\ 25 | $(PIP) install -r requirements-torch.txt && $(PIP) install -e . 26 | 27 | dev-install_gpu: 28 | make pipcheck 29 | $(PIP) install -r requirements-dev-gpu.txt &&\ 30 | $(PIP) install -e . 31 | 32 | install_conda: 33 | conda env create -f environment.yml && conda activate pylops && pip install . 34 | 35 | dev-install_conda: 36 | conda env create -f environment-dev.yml && conda activate pylops && pip install -e . 37 | 38 | dev-install_conda_arm: 39 | conda env create -f environment-dev-arm.yml && conda activate pylops && pip install -e . 40 | 41 | dev-install_conda_gpu: 42 | conda env create -f environment-dev-gpu.yml && conda activate pylops_gpu && pip install -e . 43 | 44 | tests: 45 | make pythoncheck 46 | pytest 47 | 48 | doc: 49 | cd docs && rm -rf source/api/generated && rm -rf source/gallery &&\ 50 | rm -rf source/tutorials && rm -rf build && make html && cd .. 51 | 52 | docupdate: 53 | cd docs && make html && cd .. 54 | 55 | servedoc: 56 | $(PYTHON) -m http.server --directory docs/build/html/ 57 | 58 | lint: 59 | flake8 docs/ examples/ pylops/ pytests/ tutorials/ 60 | 61 | typeannot: 62 | mypy pylops/ 63 | 64 | coverage: 65 | coverage run -m pytest && coverage xml && coverage html && $(PYTHON) -m http.server --directory htmlcov/ 66 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Azure pipeline for PyLops 2 | 3 | # Only build the master branch, tags, and PRs (on by default) to avoid building random 4 | # branches in the repository until a PR is opened. 5 | trigger: 6 | branches: 7 | include: 8 | - master 9 | - dev 10 | - refs/tags/* 11 | 12 | jobs: 13 | 14 | 15 | # Windows 16 | ######################################################################################## 17 | # - job: 18 | # displayName: 'Windows' 19 | # 20 | # pool: 21 | # vmImage: 'windows-2019' 22 | # 23 | # variables: 24 | # NUMBA_NUM_THREADS: 1 25 | # 26 | # steps: 27 | # - task: UsePythonVersion@0 28 | # inputs: 29 | # versionSpec: '3.9' 30 | # architecture: 'x64' 31 | # 32 | # - script: | 33 | # python -m pip install --upgrade pip setuptools wheel django 34 | # pip install -r requirements-dev.txt 35 | # pip install . 36 | # displayName: 'Install prerequisites and library' 37 | # 38 | # - script: | 39 | # python setup.py test 40 | # condition: succeededOrFailed() 41 | # displayName: 'Run tests' 42 | 43 | 44 | # Mac 45 | ######################################################################################## 46 | - job: 47 | displayName: 'Mac' 48 | 49 | pool: 50 | vmImage: 'macOS-latest' 51 | 52 | variables: 53 | NUMBA_NUM_THREADS: 1 54 | 55 | steps: 56 | - task: UsePythonVersion@0 57 | inputs: 58 | versionSpec: '3.11' 59 | architecture: 'x64' 60 | 61 | - script: | 62 | python -m pip install --upgrade pip setuptools wheel django 63 | pip install -r requirements-dev.txt 64 | pip install -r requirements-torch.txt 65 | pip install . 66 | displayName: 'Install prerequisites and library' 67 | 68 | - script: | 69 | pytest 70 | condition: succeededOrFailed() 71 | displayName: 'Run tests' 72 | 73 | 74 | # Linux 75 | ######################################################################################## 76 | - job: 77 | displayName: 'Linux' 78 | 79 | pool: 80 | vmImage: 'ubuntu-latest' 81 | 82 | variables: 83 | NUMBA_NUM_THREADS: 1 84 | 85 | steps: 86 | - task: UsePythonVersion@0 87 | inputs: 88 | versionSpec: '3.11' 89 | architecture: 'x64' 90 | 91 | - script: | 92 | python -m pip install --upgrade pip setuptools wheel django 93 | pip install -r requirements-dev.txt 94 | pip install -r requirements-torch.txt 95 | pip install . 96 | displayName: 'Install prerequisites and library' 97 | 98 | - script: | 99 | pytest 100 | condition: succeededOrFailed() 101 | displayName: 'Run tests' 102 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # Disable numba 5 | # export NUMBA_DISABLE_JIT=1 6 | 7 | # You can set these variables from the command line. 8 | SPHINXOPTS = 9 | SPHINXBUILD = sphinx-build 10 | SPHINXPROJ = Pylops 11 | SOURCEDIR = source 12 | BUILDDIR = build 13 | 14 | # Put it first so that "make" without argument is like "make help". 15 | help: 16 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 17 | 18 | .PHONY: help Makefile 19 | 20 | # Catch-all target: route all unknown targets to Sphinx using the new 21 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 22 | %: Makefile 23 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/source/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | @import 'theme.css'; 2 | 3 | .sphx-glr-multi-img { 4 | max-width: 100%; 5 | } -------------------------------------------------------------------------------- /docs/source/_static/cupy_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/docs/source/_static/cupy_diagram.png -------------------------------------------------------------------------------- /docs/source/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/docs/source/_static/favicon.ico -------------------------------------------------------------------------------- /docs/source/_static/numpy_cupy_bd_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/docs/source/_static/numpy_cupy_bd_diagram.png -------------------------------------------------------------------------------- /docs/source/_static/numpy_cupy_vs_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/docs/source/_static/numpy_cupy_vs_diagram.png -------------------------------------------------------------------------------- /docs/source/_static/pylops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/docs/source/_static/pylops.png -------------------------------------------------------------------------------- /docs/source/_static/pylops_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/docs/source/_static/pylops_b.png -------------------------------------------------------------------------------- /docs/source/_static/style.css: -------------------------------------------------------------------------------- 1 | /* To stick the footer to the bottom of the page */ 2 | html { 3 | } 4 | 5 | body { 6 | font-family: 'Open Sans', sans-serif; 7 | } 8 | 9 | h1, h2, h3, h4 { 10 | font-weight: 300; 11 | font-family: "Open Sans",sans-serif; 12 | } 13 | 14 | h1 { 15 | font-size: 200%; 16 | } 17 | 18 | .sidebar-title { 19 | margin-top: 10px; 20 | margin-bottom: 0px; 21 | } 22 | 23 | .banner { 24 | padding-bottom: 60px; 25 | text-align: center; 26 | } 27 | 28 | .banner img { 29 | margin-bottom: 40px; 30 | } 31 | 32 | .api-module { 33 | margin-bottom: 80px; 34 | } 35 | 36 | .youtube-embed { 37 | max-width: 600px; 38 | margin-bottom: 24px; 39 | } 40 | 41 | .video-container { 42 | position:relative; 43 | padding-bottom:56.25%; 44 | padding-top:30px; 45 | height:0; 46 | overflow:hidden; 47 | } 48 | 49 | .video-container iframe, .video-container object, .video-container embed { 50 | position:absolute; 51 | top:0; 52 | left:0; 53 | width:100%; 54 | height:100%; 55 | } 56 | 57 | .wy-nav-content { 58 | max-width: 1000px; 59 | } 60 | 61 | .wy-nav-top { 62 | background-color: #555555; 63 | } 64 | 65 | .wy-side-nav-search { 66 | background-color: #555555; 67 | } 68 | 69 | .wy-side-nav-search > a img.logo { 70 | width: 50%; 71 | } 72 | 73 | .wy-side-nav-search input[type="text"] { 74 | border-color: #555555; 75 | } 76 | 77 | /* Remove the padding from the Parameters table */ 78 | .rst-content table.field-list .field-name { 79 | padding-left: 0px; 80 | } 81 | 82 | /* Lign up the Parameters section with the descriptions */ 83 | .rst-content table.field-list td { 84 | padding-top: 8px; 85 | } 86 | 87 | .rst-content .highlight > pre { 88 | font-size: 14px; 89 | } 90 | 91 | .rst-content img { 92 | max-width: 100%; 93 | } 94 | 95 | .source-link { 96 | float: right; 97 | } 98 | 99 | .strike { 100 | text-decoration: line-through; 101 | } 102 | 103 | /* Don't let the edit and notebook download links disappear on mobile. */ 104 | @media screen and (max-width: 480px) { 105 | .wy-breadcrumbs li.source-link { 106 | float:none; 107 | display: block; 108 | margin-top: 20px; 109 | } 110 | } 111 | 112 | /* Sphinx-Gallery */ 113 | /****************************************************************************/ 114 | /* Don't let captions be italic */ 115 | .rst-content div.figure p.caption { 116 | font-style: normal; 117 | } -------------------------------------------------------------------------------- /docs/source/_templates/autosummary/base.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline }} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. auto{{ objtype }}:: {{ objname }} 6 | 7 | .. raw:: html 8 | 9 |
10 | -------------------------------------------------------------------------------- /docs/source/_templates/autosummary/class.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline }} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoclass:: {{ objname }} 6 | 7 | .. rubric:: Methods 8 | 9 | .. autosummary:: 10 | {% for item in methods %} 11 | ~{{ objname }}.{{ item }} 12 | {%- endfor %} 13 | 14 | {# .. rubric:: Attributes#} 15 | {##} 16 | {# .. autosummary::#} 17 | {# {% for item in attributes %}#} 18 | {# ~{{ objname }}.{{ item }}#} 19 | {# {%- endfor %}#} 20 | 21 | .. raw:: html 22 | 23 |
24 | 25 | 26 | .. include:: backreferences/{{ fullname }}.examples 27 | 28 | .. raw:: html 29 | 30 |
31 | 32 | 33 | {#{% for item in methods %} #} 34 | {#{% if item != '__init__' %} #} 35 | {#.. automethod:: {{ objname }}.{{ item }} #} 36 | {#{% endif %} #} 37 | {#{% endfor %} #} 38 | 39 | {#.. raw:: html #} 40 | {# #} 41 | {#
#} 42 | 43 | -------------------------------------------------------------------------------- /docs/source/_templates/autosummary/exception.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline }} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoexception:: {{ objname }} 6 | 7 | 8 | .. raw:: html 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /docs/source/_templates/autosummary/function.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline }} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autofunction:: {{ objname }} 6 | 7 | .. include:: backreferences/{{ fullname }}.examples 8 | 9 | .. raw:: html 10 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /docs/source/_templates/autosummary/module.rst: -------------------------------------------------------------------------------- 1 | .. raw:: html 2 | 3 |
4 | 5 | ``{{ fullname }}`` 6 | {% for i in range(fullname|length + 15) %}-{% endfor %} 7 | 8 | .. raw:: html 9 | 10 |
11 | 12 | .. automodule:: {{ fullname }} 13 | 14 | {% block classes %} 15 | {% if classes %} 16 | .. rubric:: Classes 17 | 18 | .. autosummary:: 19 | :toctree: ./ 20 | {% for item in classes %} 21 | {{ fullname }}.{{ item }} 22 | {% endfor %} 23 | {% endif %} 24 | {% endblock %} 25 | 26 | 27 | {% block functions %} 28 | {% if functions %} 29 | .. rubric:: Functions 30 | 31 | .. autosummary:: 32 | :toctree: ./ 33 | {% for item in functions %} 34 | {{ fullname }}.{{ item }} 35 | {% endfor %} 36 | {% endif %} 37 | {% endblock %} 38 | 39 | 40 | {% block exceptions %} 41 | {% if exceptions %} 42 | .. rubric:: Exceptions 43 | 44 | .. autosummary:: 45 | :toctree: ./ 46 | {% for item in exceptions %} 47 | {{ fullname }}.{{ item }} 48 | {% endfor %} 49 | {% endif %} 50 | {% endblock %} 51 | 52 | .. raw:: html 53 | 54 |
55 | -------------------------------------------------------------------------------- /docs/source/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {# Import the theme's layout. #} 2 | {% extends "!layout.html" %} 3 | 4 | {% block extrahead %} 5 | {# Include require.js so that we can use WorldWind #} 6 | 7 | {% endblock %} 8 | 9 | 10 | {% block htmltitle %} 11 | {% if title == '' or title == 'Home' %} 12 | {{ docstitle|e }} 13 | {% else %} 14 | {{ title|striptags|e }}{{ titlesuffix }} 15 | {% endif %} 16 | {% endblock %} 17 | 18 | 19 | {% block menu %} 20 | {{ super() }} 21 | 22 | {% if menu_links %} 23 |

24 | 25 | {% if menu_links_name %} 26 | {{ menu_links_name }} 27 | {% else %} 28 | External links 29 | {% endif %} 30 | 31 |

32 | 37 | {% endif %} 38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /docs/source/citing.rst: -------------------------------------------------------------------------------- 1 | .. _citing: 2 | 3 | How to cite 4 | =========== 5 | When using PyLops in scientific publications, please cite the following paper: 6 | 7 | .. raw:: html 8 | 9 | 21 | -------------------------------------------------------------------------------- /docs/source/credits.rst: -------------------------------------------------------------------------------- 1 | .. _credits: 2 | 3 | Contributors 4 | ============ 5 | 6 | * `Matteo Ravasi `_, mrava87 7 | * `Carlos da Costa `_, cako 8 | * `Dieter Werthmüller `_, prisae 9 | * `Tristan van Leeuwen `_, TristanvanLeeuwen 10 | * `Leonardo Uieda `_, leouieda 11 | * `Filippo Broggini `_, filippo82 12 | * `Tyler Hughes `_, twhughes 13 | * `Lyubov Skopintseva `_, lskopintseva 14 | * `Francesco Picetti `_, fpicetti 15 | * `Alan Richardson `_, ar4 16 | * `BurningKarl `_, BurningKarl 17 | * `Nick Luiken `_, NickLuiken 18 | * `Muhammad Izzatullah `_, izzatum 19 | * `Juan Daniel Romero `_, jdromerom 20 | * `Aniket Singh Rawat `_, dikwickley 21 | * `Rohan Babbar `_, rohanbabbar04 22 | * `Wei Zhang `_, ZhangWeiGeo 23 | * `Fedor Goncharov `_, fedor-goncharov 24 | * `Alex Rakowski `_, alex-rakowski 25 | * `David Sollberger `_, solldavid 26 | * `Gustavo Coelho `_, guaacoelho -------------------------------------------------------------------------------- /docs/source/extensions.rst: -------------------------------------------------------------------------------- 1 | .. _extensions: 2 | 3 | Extensions 4 | ========== 5 | 6 | PyLops brings to users the power of linear operators in a simple and easy 7 | to use programming interface. 8 | 9 | While very powerful on its own, this library is further extended to take 10 | advantage of more advanced computational resources, either in terms of 11 | **multiple-node clusters** or **GPUs**. Moreover, some independent 12 | libraries are created to use third party software that cannot be included as 13 | dependencies to our main library for licensing issues but may be useful 14 | for academic purposes. 15 | 16 | Spin-off projects that aim at extending the capabilities of PyLops are: 17 | 18 | * `PyLops-MPI `_: PyLops for distributed systems with many computing nodes using MPI 19 | * `PyProximal `_: Proximal solvers which integrate with PyLops Linear Operators. 20 | * `Curvelops `_: Python wrapper for the Curvelab 2D and 3D digital curvelet transforms. 21 | * `PyLops-GPU `_ : PyLops for GPU arrays (unmantained! the core features are now incorporated into PyLops). 22 | * `PyLops-Distributed `_ : PyLops for distributed systems with many computing nodes using Dask (unmantained!). 23 | -------------------------------------------------------------------------------- /docs/source/faq.rst: -------------------------------------------------------------------------------- 1 | .. _faq: 2 | 3 | Frequenty Asked Questions 4 | ========================= 5 | 6 | **1. Can I visualize my operator?** 7 | 8 | Yes, you can. Every operator has a method called ``todense`` that will return the dense matrix equivalent of 9 | the operator. Note, however, that in order to do so we need to allocate a ``numpy`` array of the size of your 10 | operator and apply the operator ``N`` times, where ``N`` is the number of columns of the operator. The allocation can 11 | be very heavy on your memory and the computation may take long time, so use it with care only for small toy 12 | examples to understand what your operator looks like. This method should however not be abused, as the reason of 13 | working with linear operators is indeed that you don't really need to access the explicit matrix representation 14 | of an operator. 15 | 16 | 17 | **2. Can I have an older version of** ``cupy`` **installed in my system (** ``cupy-cudaXX<10.6.0``)?** 18 | 19 | Yes. Nevertheless you need to tell PyLops that you don't want to use its ``cupy`` 20 | backend by setting the environment variable ``CUPY_PYLOPS=0``. 21 | Failing to do so will lead to an error when you import ``pylops`` because some of the ``cupyx`` 22 | routines that we use are not available in earlier version of ``cupy``. -------------------------------------------------------------------------------- /environment-dev-arm.yml: -------------------------------------------------------------------------------- 1 | name: pylops 2 | channels: 3 | - defaults 4 | - conda-forge 5 | - numba 6 | - pytorch 7 | dependencies: 8 | - python>=3.9.0 9 | - pip 10 | - numpy>=1.21.0 11 | - scipy>=1.11.0 12 | - pytorch>=1.2.0 13 | - cpuonly 14 | - jax 15 | - pyfftw 16 | - pywavelets 17 | - sympy 18 | - matplotlib 19 | - ipython 20 | - pytest 21 | - Sphinx 22 | - numpydoc 23 | - numba 24 | - pre-commit 25 | - autopep8 26 | - isort 27 | - black 28 | - pip: 29 | - devito 30 | - dtcwt 31 | - scikit-fmm 32 | - spgl1 33 | - pytest-runner 34 | - setuptools_scm 35 | - pydata-sphinx-theme 36 | - sphinx-gallery 37 | - nbsphinx 38 | - sphinxemoji 39 | - image 40 | - flake8 41 | - mypy 42 | -------------------------------------------------------------------------------- /environment-dev-gpu.yml: -------------------------------------------------------------------------------- 1 | name: pylops_gpu 2 | channels: 3 | - conda-forge 4 | - defaults 5 | - numba 6 | dependencies: 7 | - python>=3.11.0 8 | - pip 9 | - numpy>=1.21.0 10 | - scipy>=1.11.0 11 | - cupy 12 | - sympy 13 | - matplotlib 14 | - ipython 15 | - pytest 16 | - Sphinx 17 | - numpydoc 18 | - numba 19 | - icc_rt 20 | - pre-commit 21 | - autopep8 22 | - isort 23 | - black 24 | - pip: 25 | - torch 26 | - pytest-runner 27 | - setuptools_scm 28 | - pydata-sphinx-theme 29 | - sphinx-gallery 30 | - nbsphinx 31 | - sphinxemoji 32 | - image 33 | - flake8 34 | - mypy 35 | -------------------------------------------------------------------------------- /environment-dev.yml: -------------------------------------------------------------------------------- 1 | name: pylops 2 | channels: 3 | - defaults 4 | - conda-forge 5 | - numba 6 | - pytorch 7 | dependencies: 8 | - python>=3.9.0 9 | - pip 10 | - numpy>=1.21.0 11 | - scipy>=1.11.0 12 | - pytorch>=1.2.0 13 | - cpuonly 14 | - jax 15 | - pyfftw 16 | - pywavelets 17 | - sympy 18 | - matplotlib 19 | - ipython 20 | - pytest 21 | - Sphinx 22 | - numpydoc 23 | - numba 24 | - icc_rt 25 | - pre-commit 26 | - autopep8 27 | - isort 28 | - black 29 | - pip: 30 | - devito 31 | - dtcwt 32 | - scikit-fmm 33 | - spgl1 34 | - pytest-runner 35 | - setuptools_scm 36 | - pydata-sphinx-theme 37 | - sphinx-gallery 38 | - nbsphinx 39 | - sphinxemoji 40 | - image 41 | - flake8 42 | - mypy 43 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: pylops 2 | channels: 3 | - defaults 4 | dependencies: 5 | - python>=3.9.0 6 | - numpy>=1.21.0 7 | - scipy>=1.14.0 8 | -------------------------------------------------------------------------------- /examples/README.txt: -------------------------------------------------------------------------------- 1 | .. _general_examples: 2 | 3 | Gallery 4 | ------- 5 | 6 | Below is a gallery of examples which use PyLops operators and utilities. 7 | -------------------------------------------------------------------------------- /examples/plot_bilinear.py: -------------------------------------------------------------------------------- 1 | """ 2 | Bilinear Interpolation 3 | ====================== 4 | This example shows how to use the :py:class:`pylops.signalprocessing.Bilinar` 5 | operator to perform bilinear interpolation to a 2-dimensional input vector. 6 | """ 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | from scipy import misc 10 | 11 | import pylops 12 | 13 | plt.close("all") 14 | np.random.seed(0) 15 | 16 | ############################################################################### 17 | # First of all, we create a 2-dimensional input vector containing an image 18 | # from the ``scipy.misc`` family. 19 | x = misc.face()[::5, ::5, 0] 20 | nz, nx = x.shape 21 | 22 | ############################################################################### 23 | # We can now define a set of available samples in the 24 | # first and second direction of the array and apply bilinear interpolation. 25 | nsamples = 2000 26 | iava = np.vstack( 27 | (np.random.uniform(0, nz - 1, nsamples), np.random.uniform(0, nx - 1, nsamples)) 28 | ) 29 | 30 | Bop = pylops.signalprocessing.Bilinear(iava, (nz, nx)) 31 | y = Bop * x 32 | 33 | ############################################################################### 34 | # At this point we try to reconstruct the input signal imposing a smooth 35 | # solution by means of a regularization term that minimizes the Laplacian of 36 | # the solution. 37 | 38 | D2op = pylops.Laplacian((nz, nx), weights=(1, 1), dtype="float64") 39 | 40 | xadj = Bop.H * y 41 | xinv = pylops.optimization.leastsquares.normal_equations_inversion( 42 | Bop, y, [D2op], epsRs=[np.sqrt(0.1)], **dict(maxiter=100) 43 | )[0] 44 | xadj = xadj.reshape(nz, nx) 45 | xinv = xinv.reshape(nz, nx) 46 | 47 | fig, axs = plt.subplots(1, 3, figsize=(10, 4)) 48 | fig.suptitle("Bilinear interpolation", fontsize=14, fontweight="bold", y=0.95) 49 | axs[0].imshow(x, cmap="gray_r", vmin=0, vmax=250) 50 | axs[0].axis("tight") 51 | axs[0].set_title("Original") 52 | axs[1].imshow(xadj, cmap="gray_r", vmin=0, vmax=250) 53 | axs[1].axis("tight") 54 | axs[1].set_title("Sampled") 55 | axs[2].imshow(xinv, cmap="gray_r", vmin=0, vmax=250) 56 | axs[2].axis("tight") 57 | axs[2].set_title("2D Regularization") 58 | plt.tight_layout() 59 | plt.subplots_adjust(top=0.8) 60 | -------------------------------------------------------------------------------- /examples/plot_cgls.py: -------------------------------------------------------------------------------- 1 | r""" 2 | CGLS and LSQR Solvers 3 | ===================== 4 | 5 | This example shows how to use the :py:func:`pylops.optimization.leastsquares.cgls` 6 | and :py:func:`pylops.optimization.leastsquares.lsqr` PyLops solvers 7 | to minimize the following cost function: 8 | 9 | .. math:: 10 | J = \| \mathbf{y} - \mathbf{Ax} \|_2^2 + \epsilon \| \mathbf{x} \|_2^2 11 | 12 | Note that the LSQR solver behaves in the same way as the scipy's 13 | :py:func:`scipy.sparse.linalg.lsqr` solver. However, our solver is also able 14 | to operate on cupy arrays and perform computations on a GPU. 15 | 16 | """ 17 | 18 | import warnings 19 | 20 | import matplotlib.pyplot as plt 21 | import numpy as np 22 | 23 | import pylops 24 | 25 | plt.close("all") 26 | warnings.filterwarnings("ignore") 27 | 28 | ############################################################################### 29 | # Let's define a matrix :math:`\mathbf{A}` or size (``N`` and ``M``) and 30 | # fill the matrix with random numbers 31 | 32 | N, M = 20, 10 33 | A = np.random.normal(0, 1, (N, M)) 34 | Aop = pylops.MatrixMult(A, dtype="float64") 35 | 36 | x = np.ones(M) 37 | 38 | ############################################################################### 39 | # We can now use the cgls solver to invert this matrix 40 | 41 | y = Aop * x 42 | xest, istop, nit, r1norm, r2norm, cost_cgls = pylops.optimization.basic.cgls( 43 | Aop, y, x0=np.zeros_like(x), niter=10, tol=1e-10, show=True 44 | ) 45 | 46 | print(f"x= {x}") 47 | print(f"cgls solution xest= {xest}") 48 | 49 | ############################################################################### 50 | # And the lsqr solver to invert this matrix 51 | 52 | y = Aop * x 53 | ( 54 | xest, 55 | istop, 56 | itn, 57 | r1norm, 58 | r2norm, 59 | anorm, 60 | acond, 61 | arnorm, 62 | xnorm, 63 | var, 64 | cost_lsqr, 65 | ) = pylops.optimization.basic.lsqr(Aop, y, x0=np.zeros_like(x), niter=10, show=True) 66 | 67 | print(f"x= {x}") 68 | print(f"lsqr solution xest= {xest}") 69 | 70 | 71 | ############################################################################### 72 | # Finally we show that the L2 norm of the residual of the two solvers decays 73 | # in the same way, as LSQR is algebrically equivalent to CG on the normal 74 | # equations and CGLS 75 | 76 | plt.figure(figsize=(12, 3)) 77 | plt.plot(cost_cgls, "k", lw=2, label="CGLS") 78 | plt.plot(cost_lsqr, "--r", lw=2, label="LSQR") 79 | plt.title("Cost functions") 80 | plt.legend() 81 | plt.tight_layout() 82 | 83 | ############################################################################### 84 | # Note that while we used a dense matrix here, any other linear operator 85 | # can be fed to cgls and lsqr as is the case for any other PyLops solver. 86 | -------------------------------------------------------------------------------- /examples/plot_conj.py: -------------------------------------------------------------------------------- 1 | """ 2 | Conj 3 | ==== 4 | 5 | This example shows how to use the :py:class:`pylops.basicoperators.Conj` 6 | operator. 7 | This operator returns the complex conjugate in both forward and adjoint 8 | modes (it is self adjoint). 9 | """ 10 | import matplotlib.pyplot as plt 11 | import numpy as np 12 | 13 | import pylops 14 | 15 | plt.close("all") 16 | 17 | ############################################################################### 18 | # Let's define a Conj operator to get the complex conjugate 19 | # of the input. 20 | 21 | M = 5 22 | x = np.arange(M) + 1j * np.arange(M)[::-1] 23 | Rop = pylops.basicoperators.Conj(M, dtype="complex128") 24 | 25 | y = Rop * x 26 | xadj = Rop.H * y 27 | 28 | _, axs = plt.subplots(1, 3, figsize=(10, 4)) 29 | axs[0].plot(np.real(x), lw=2, label="Real") 30 | axs[0].plot(np.imag(x), lw=2, label="Imag") 31 | axs[0].legend() 32 | axs[0].set_title("Input") 33 | axs[1].plot(np.real(y), lw=2, label="Real") 34 | axs[1].plot(np.imag(y), lw=2, label="Imag") 35 | axs[1].legend() 36 | axs[1].set_title("Forward of Input") 37 | axs[2].plot(np.real(xadj), lw=2, label="Real") 38 | axs[2].plot(np.imag(xadj), lw=2, label="Imag") 39 | axs[2].legend() 40 | axs[2].set_title("Adjoint of Forward") 41 | plt.tight_layout() 42 | -------------------------------------------------------------------------------- /examples/plot_dct.py: -------------------------------------------------------------------------------- 1 | """ 2 | Discrete Cosine Transform 3 | ========================= 4 | This example shows how to use the :py:class:`pylops.signalprocessing.DCT` operator. 5 | This operator performs the Discrete Cosine Transform on a (single or multi-dimensional) 6 | input array. 7 | 8 | """ 9 | 10 | import matplotlib.pyplot as plt 11 | import numpy as np 12 | 13 | import pylops 14 | 15 | plt.close("all") 16 | 17 | 18 | ############################################################################### 19 | # Let's define a 1D array x of increasing numbers 20 | 21 | n = 21 22 | x = np.arange(n) + 1 23 | 24 | 25 | ############################################################################### 26 | # Next we create the DCT operator with the shape of our input array as 27 | # parameter, and we store the DCT coefficients in the array `y`. Finally, we 28 | # perform the inverse using the adjoint of the operator, and we obtain the 29 | # original input signal. 30 | DOp = pylops.signalprocessing.DCT(dims=x.shape) 31 | y = DOp @ x 32 | xadj = DOp.H @ y 33 | 34 | plt.figure(figsize=(8, 5)) 35 | plt.plot(x, "k", label="input array") 36 | plt.plot(y, "r", label="transformed array") 37 | plt.plot(xadj, "--b", label="transformed array") 38 | plt.title("1D Discrete Cosine Transform") 39 | plt.legend() 40 | plt.tight_layout() 41 | 42 | ################################################################################ 43 | # Next we apply the DCT to a sine wave 44 | 45 | cycles = 2 46 | resolution = 100 47 | 48 | length = np.pi * 2 * cycles 49 | s = np.sin(np.arange(0, length, length / resolution)) 50 | DOp = pylops.signalprocessing.DCT(dims=s.shape) 51 | y = DOp @ s 52 | 53 | plt.figure(figsize=(8, 5)) 54 | plt.plot(s, "k", label="sine wave") 55 | plt.plot(y, "r", label="dct of sine wave") 56 | plt.title("Discrete Cosine Transform of Sine wave") 57 | plt.legend() 58 | plt.tight_layout() 59 | 60 | ############################################################################### 61 | # The Discrete Cosine Transform is commonly used in lossy image compression 62 | # (i.e., JPEG encoding) due to its strong energy compaction nature. Here is an 63 | # example of DCT being used for image compression. 64 | # Note: This code is just an example and may not provide the best results 65 | # for all images. You may need to adjust the threshold value to get better 66 | # results. 67 | 68 | img = np.load("../testdata/python.npy")[::5, ::5, 0] 69 | DOp = pylops.signalprocessing.DCT(dims=img.shape) 70 | dct_img = DOp @ img 71 | 72 | # Set a threshold for the DCT coefficients to zero out 73 | threshold = np.percentile(np.abs(dct_img), 70) 74 | dct_img[np.abs(dct_img) < threshold] = 0 75 | 76 | # Inverse DCT to get back the image 77 | compressed_img = DOp.H @ dct_img 78 | 79 | # Plot original and compressed images 80 | fig, ax = plt.subplots(1, 2, figsize=(10, 5)) 81 | ax[0].imshow(img, cmap="gray") 82 | ax[0].set_title("Original Image") 83 | ax[1].imshow(compressed_img, cmap="gray") 84 | ax[1].set_title("Compressed Image") 85 | plt.tight_layout() 86 | -------------------------------------------------------------------------------- /examples/plot_describe.py: -------------------------------------------------------------------------------- 1 | r""" 2 | Describe 3 | ======== 4 | This example focuses on the usage of the :func:`pylops.utils.describe.describe` 5 | method, which allows expressing any PyLops operator into its equivalent 6 | mathematical representation. This is done with the aid of 7 | `sympy `_, a Python library for symbolic computing 8 | 9 | """ 10 | import matplotlib.pyplot as plt 11 | import numpy as np 12 | 13 | import pylops 14 | from pylops.utils.describe import describe 15 | 16 | plt.close("all") 17 | 18 | ############################################################################### 19 | # Let's start by defining 3 PyLops operators. Note that once an operator is 20 | # defined we can attach a name to the operator; by doing so, this name will 21 | # be used in the mathematical description of the operator. Alternatively, 22 | # the describe method will randomly choose a name for us. 23 | 24 | A = pylops.MatrixMult(np.ones((10, 5))) 25 | A.name = "A" 26 | B = pylops.Diagonal(np.ones(5)) 27 | B.name = "A" 28 | C = pylops.MatrixMult(np.ones((10, 5))) 29 | 30 | # Simple operator 31 | describe(A) 32 | 33 | # Transpose 34 | AT = A.T 35 | describe(AT) 36 | 37 | # Adjoint 38 | AH = A.H 39 | describe(AH) 40 | 41 | # Scaled 42 | A3 = 3 * A 43 | describe(A3) 44 | 45 | # Sum 46 | D = A + C 47 | describe(D) 48 | 49 | ############################################################################### 50 | # So far so good. Let's see what happens if we accidentally call two different 51 | # operators with the same name. You will see that PyLops catches that and 52 | # changes the name for us (and provides us with a nice warning!) 53 | 54 | D = A * B 55 | describe(D) 56 | 57 | ############################################################################### 58 | # We can move now to something more complicated using various composition 59 | # operators 60 | 61 | H = pylops.HStack((A * B, C * B)) 62 | describe(H) 63 | 64 | H = pylops.Block([[A * B, C], [A, A]]) 65 | describe(H) 66 | 67 | ############################################################################### 68 | # Finally, note that you can get the best out of the describe method if working 69 | # inside a Jupyter notebook. There, the mathematical expression will be 70 | # rendered using a LeTex format! See an example `notebook `_. 71 | -------------------------------------------------------------------------------- /examples/plot_flip.py: -------------------------------------------------------------------------------- 1 | r""" 2 | Flip along an axis 3 | ================== 4 | 5 | This example shows how to use the :py:class:`pylops.Flip` 6 | operator to simply flip an input signal along an axis. 7 | """ 8 | 9 | import matplotlib.pyplot as plt 10 | import numpy as np 11 | 12 | import pylops 13 | 14 | plt.close("all") 15 | 16 | ############################################################################### 17 | # Let's start with a 1D example. Define an input signal composed of 18 | # ``nt`` samples 19 | nt = 10 20 | x = np.arange(nt) 21 | 22 | ############################################################################### 23 | # We can now create our flip operator and apply it to the input 24 | # signal. We can also apply the adjoint to the flipped signal and we can 25 | # see how for this operator the adjoint is effectively equivalent to 26 | # the inverse. 27 | Fop = pylops.Flip(nt) 28 | y = Fop * x 29 | xadj = Fop.H * y 30 | 31 | plt.figure(figsize=(3, 5)) 32 | plt.plot(x, "k", lw=3, label=r"$x$") 33 | plt.plot(y, "r", lw=3, label=r"$y=Fx$") 34 | plt.plot(xadj, "--g", lw=3, label=r"$x_{adj} = F^H y$") 35 | plt.title("Flip in 1st direction", fontsize=14, fontweight="bold") 36 | plt.legend() 37 | plt.tight_layout() 38 | 39 | ############################################################################### 40 | # Let's now repeat the same exercise on a two dimensional signal. We will 41 | # first flip the model along the first axis and then along the second axis 42 | nt, nx = 10, 5 43 | x = np.outer(np.arange(nt), np.ones(nx)) 44 | Fop = pylops.Flip((nt, nx), axis=0) 45 | y = Fop * x 46 | xadj = Fop.H * y 47 | 48 | fig, axs = plt.subplots(1, 3, figsize=(7, 3)) 49 | fig.suptitle( 50 | "Flip in 1st direction for 2d data", fontsize=14, fontweight="bold", y=0.95 51 | ) 52 | axs[0].imshow(x, cmap="rainbow") 53 | axs[0].set_title(r"$x$") 54 | axs[0].axis("tight") 55 | axs[1].imshow(y, cmap="rainbow") 56 | axs[1].set_title(r"$y = F x$") 57 | axs[1].axis("tight") 58 | axs[2].imshow(xadj, cmap="rainbow") 59 | axs[2].set_title(r"$x_{adj} = F^H y$") 60 | axs[2].axis("tight") 61 | plt.tight_layout() 62 | plt.subplots_adjust(top=0.8) 63 | 64 | 65 | x = np.outer(np.ones(nt), np.arange(nx)) 66 | Fop = pylops.Flip(dims=(nt, nx), axis=1) 67 | y = Fop * x 68 | xadj = Fop.H * y 69 | 70 | # sphinx_gallery_thumbnail_number = 3 71 | fig, axs = plt.subplots(1, 3, figsize=(7, 3)) 72 | fig.suptitle( 73 | "Flip in 2nd direction for 2d data", fontsize=14, fontweight="bold", y=0.95 74 | ) 75 | axs[0].imshow(x, cmap="rainbow") 76 | axs[0].set_title(r"$x$") 77 | axs[0].axis("tight") 78 | axs[1].imshow(y, cmap="rainbow") 79 | axs[1].set_title(r"$y = F x$") 80 | axs[1].axis("tight") 81 | axs[2].imshow(xadj, cmap="rainbow") 82 | axs[2].set_title(r"$x_{adj} = F^H y$") 83 | axs[2].axis("tight") 84 | plt.tight_layout() 85 | plt.subplots_adjust(top=0.8) 86 | -------------------------------------------------------------------------------- /examples/plot_identity.py: -------------------------------------------------------------------------------- 1 | """ 2 | Identity 3 | ======== 4 | This example shows how to use the :py:class:`pylops.Identity` operator to transfer model 5 | into data and viceversa. 6 | """ 7 | import matplotlib.gridspec as pltgs 8 | import matplotlib.pyplot as plt 9 | import numpy as np 10 | 11 | import pylops 12 | 13 | plt.close("all") 14 | 15 | ############################################################################### 16 | # Let's define an identity operator :math:`\mathbf{Iop}` with same number of 17 | # elements for data and model (:math:`N=M`). 18 | N, M = 5, 5 19 | x = np.arange(M) 20 | Iop = pylops.Identity(M, dtype="int") 21 | 22 | y = Iop * x 23 | xadj = Iop.H * y 24 | 25 | gs = pltgs.GridSpec(1, 6) 26 | fig = plt.figure(figsize=(7, 4)) 27 | ax = plt.subplot(gs[0, 0:3]) 28 | im = ax.imshow(np.eye(N), cmap="rainbow") 29 | ax.set_title("A", size=20, fontweight="bold") 30 | ax.set_xticks(np.arange(N - 1) + 0.5) 31 | ax.set_yticks(np.arange(M - 1) + 0.5) 32 | ax.grid(linewidth=3, color="white") 33 | ax.xaxis.set_ticklabels([]) 34 | ax.yaxis.set_ticklabels([]) 35 | ax = plt.subplot(gs[0, 3]) 36 | ax.imshow(x[:, np.newaxis], cmap="rainbow") 37 | ax.set_title("x", size=20, fontweight="bold") 38 | ax.set_xticks([]) 39 | ax.set_yticks(np.arange(M - 1) + 0.5) 40 | ax.grid(linewidth=3, color="white") 41 | ax.xaxis.set_ticklabels([]) 42 | ax.yaxis.set_ticklabels([]) 43 | ax = plt.subplot(gs[0, 4]) 44 | ax.text( 45 | 0.35, 46 | 0.5, 47 | "=", 48 | horizontalalignment="center", 49 | verticalalignment="center", 50 | size=40, 51 | fontweight="bold", 52 | ) 53 | ax.axis("off") 54 | ax = plt.subplot(gs[0, 5]) 55 | ax.imshow(y[:, np.newaxis], cmap="rainbow") 56 | ax.set_title("y", size=20, fontweight="bold") 57 | ax.set_xticks([]) 58 | ax.set_yticks(np.arange(N - 1) + 0.5) 59 | ax.grid(linewidth=3, color="white") 60 | ax.xaxis.set_ticklabels([]) 61 | ax.yaxis.set_ticklabels([]) 62 | fig.colorbar(im, ax=ax, ticks=[0, 1], pad=0.3, shrink=0.7) 63 | plt.tight_layout() 64 | 65 | ############################################################################### 66 | # Similarly we can consider the case with data bigger than model 67 | N, M = 10, 5 68 | x = np.arange(M) 69 | Iop = pylops.Identity(N, M, dtype="int") 70 | 71 | y = Iop * x 72 | xadj = Iop.H * y 73 | 74 | print(f"x = {x} ") 75 | print(f"I*x = {y} ") 76 | print(f"I'*y = {xadj} ") 77 | 78 | ############################################################################### 79 | # and model bigger than data 80 | N, M = 5, 10 81 | x = np.arange(M) 82 | Iop = pylops.Identity(N, M, dtype="int") 83 | 84 | y = Iop * x 85 | xadj = Iop.H * y 86 | 87 | print(f"x = {x} ") 88 | print(f"I*x = {y} ") 89 | print(f"I'*y = {xadj} ") 90 | 91 | ############################################################################### 92 | # Note that this operator can be useful in many real-life applications when for example 93 | # we want to manipulate a subset of the model array and keep intact the rest of the array. 94 | # For example: 95 | # 96 | # .. math:: 97 | # \begin{bmatrix} 98 | # \mathbf{A} \quad \mathbf{I} 99 | # \end{bmatrix} 100 | # \begin{bmatrix} 101 | # \mathbf{x_1} \\ 102 | # \mathbf{x_2} 103 | # \end{bmatrix} = \mathbf{A} \mathbf{x_1} + \mathbf{x_2} 104 | # 105 | # Refer to the tutorial on *Optimization* for more details on this. 106 | -------------------------------------------------------------------------------- /examples/plot_imag.py: -------------------------------------------------------------------------------- 1 | """ 2 | Imag 3 | ==== 4 | 5 | This example shows how to use the :py:class:`pylops.basicoperators.Imag` 6 | operator. 7 | This operator returns the imaginary part of the data as a real value in 8 | forward mode, and the real part of the model as an imaginary value in 9 | adjoint mode (with zero real part). 10 | """ 11 | import matplotlib.pyplot as plt 12 | import numpy as np 13 | 14 | import pylops 15 | 16 | plt.close("all") 17 | 18 | ############################################################################### 19 | # Let's define a Imag operator :math:`\mathbf{\Im}` to extract the imaginary 20 | # component of the input. 21 | 22 | M = 5 23 | x = np.arange(M) + 1j * np.arange(M)[::-1] 24 | Rop = pylops.basicoperators.Imag(M, dtype="complex128") 25 | 26 | y = Rop * x 27 | xadj = Rop.H * y 28 | 29 | _, axs = plt.subplots(1, 3, figsize=(10, 4)) 30 | axs[0].plot(np.real(x), lw=2, label="Real") 31 | axs[0].plot(np.imag(x), lw=2, label="Imag") 32 | axs[0].legend() 33 | axs[0].set_title("Input") 34 | axs[1].plot(np.real(y), lw=2, label="Real") 35 | axs[1].plot(np.imag(y), lw=2, label="Imag") 36 | axs[1].legend() 37 | axs[1].set_title("Forward of Input") 38 | axs[2].plot(np.real(xadj), lw=2, label="Real") 39 | axs[2].plot(np.imag(xadj), lw=2, label="Imag") 40 | axs[2].legend() 41 | axs[2].set_title("Adjoint of Forward") 42 | plt.tight_layout() 43 | -------------------------------------------------------------------------------- /examples/plot_l1l1.py: -------------------------------------------------------------------------------- 1 | r""" 2 | L1-L1 IRLS 3 | ========== 4 | 5 | This example shows how to use the :py:class:`pylops.optimization.sparsity.irls` solver to 6 | solve problems in the form: 7 | 8 | .. math:: 9 | J = \left\| \mathbf{y}-\mathbf{Ax}\right\|_{1} + \epsilon \left\|\mathbf{x}\right\|_{1} 10 | 11 | This can be easily achieved by recasting the problem into this equivalent formulation: 12 | 13 | .. math:: 14 | J = \left\|\left[\begin{array}{c} 15 | \mathbf{A} \\ 16 | \epsilon \mathbf{I} 17 | \end{array}\right] \mathbf{x}-\left[\begin{array}{l} 18 | \mathbf{y} \\ 19 | \mathbf{0} 20 | \end{array}\right]\right\|_{1} 21 | 22 | and solving it using the classical version of the IRLS solver with L1 norm on the data term. In PyLops, 23 | the creation of the augmented system happens under the hood when users provide the following optional 24 | parameter (``kind="datamodel"``) to the solver. 25 | 26 | We will now consider a 1D deconvolution problem where the signal is contaminated with Laplace noise. 27 | We will compare the classical L2-L1 IRLS solver that works optimally under the condition of Gaussian 28 | noise with the above descrived L1-L1 IRLS solver that is best suited to the case of Laplace noise. 29 | """ 30 | import random 31 | 32 | import matplotlib.pyplot as plt 33 | import numpy as np 34 | 35 | import pylops 36 | 37 | plt.close("all") 38 | np.random.seed(10) 39 | random.seed(0) 40 | 41 | ############################################################################### 42 | # Let's start by creating a spiky input signal and convolving it with a Ricker 43 | # wavelet. 44 | dt = 0.004 45 | nt = 201 46 | t = np.arange(nt) * dt 47 | 48 | nspikes = 5 49 | x = np.zeros(nt) 50 | x[random.sample(range(0, nt - 1), nspikes)] = -1 + 2 * np.random.rand(nspikes) 51 | 52 | h, th, hcenter = pylops.utils.wavelets.ricker(t[:101], f0=20) 53 | Cop = pylops.signalprocessing.Convolve1D(nt, h=h, offset=hcenter) 54 | 55 | y = Cop @ x 56 | 57 | ############################################################################### 58 | # We add now a realization of Laplace-distributed noise to our signal and 59 | # perform a standard spiky deconvolution 60 | yn = y + np.random.laplace(loc=0.0, scale=0.05, size=y.shape) 61 | 62 | xl2l1 = pylops.optimization.sparsity.irls( 63 | Cop, 64 | yn, 65 | threshR=True, 66 | kind="model", 67 | nouter=100, 68 | epsR=1e-4, 69 | epsI=1.0, 70 | warm=True, 71 | **dict(iter_lim=100), 72 | )[0] 73 | 74 | xl1l1 = pylops.optimization.sparsity.irls( 75 | Cop, 76 | yn, 77 | threshR=True, 78 | kind="datamodel", 79 | nouter=100, 80 | epsR=1e-4, 81 | epsI=1.0, 82 | warm=True, 83 | **dict(iter_lim=100), 84 | )[0] 85 | 86 | fig, axs = plt.subplots(2, 1, sharex=True, figsize=(12, 5)) 87 | axs[0].plot(t, y, "k", lw=4, label="Clean") 88 | axs[0].plot(t, yn, "r", lw=2, label="Noisy") 89 | axs[0].legend() 90 | axs[0].set_title("Data") 91 | axs[1].plot(t, x, "k", lw=4, label="L2-L1") 92 | axs[1].plot( 93 | t, 94 | xl2l1, 95 | "r", 96 | lw=2, 97 | label=f"L2-L1 (NMSE={(np.linalg.norm(xl2l1 - x)/np.linalg.norm(x)):.2f})", 98 | ) 99 | axs[1].plot( 100 | t, 101 | xl1l1, 102 | "c", 103 | lw=2, 104 | label=f"L1-L1 (NMSE={(np.linalg.norm(xl1l1 - x)/np.linalg.norm(x)):.2f})", 105 | ) 106 | axs[1].legend() 107 | axs[1].set_xlabel("t") 108 | plt.tight_layout() 109 | -------------------------------------------------------------------------------- /examples/plot_multiproc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Operators with Multiprocessing 3 | ============================== 4 | This example shows how perform a scalability test for one of PyLops operators 5 | that uses ``multiprocessing`` to spawn multiple processes. Operators that 6 | support such feature are :class:`pylops.basicoperators.VStack`, 7 | :class:`pylops.basicoperators.HStack`, and 8 | :class:`pylops.basicoperators.BlockDiagonal`, and 9 | :class:`pylops.basicoperators.Block`. 10 | 11 | In this example we will consider the BlockDiagonal operator which contains 12 | :class:`pylops.basicoperators.MatrixMult` operators along its main diagonal. 13 | """ 14 | import matplotlib.pyplot as plt 15 | import numpy as np 16 | 17 | import pylops 18 | 19 | plt.close("all") 20 | 21 | ############################################################################### 22 | # Let's start by creating N MatrixMult operators and the BlockDiag operator 23 | N = 100 24 | Nops = 32 25 | Ops = [pylops.MatrixMult(np.random.normal(0.0, 1.0, (N, N))) for _ in range(Nops)] 26 | 27 | Op = pylops.BlockDiag(Ops, nproc=1) 28 | 29 | ############################################################################### 30 | # We can now perform a scalability test on the forward operation 31 | workers = [2, 3, 4] 32 | compute_times, speedup = pylops.utils.multiproc.scalability_test( 33 | Op, np.ones(Op.shape[1]), workers=workers, forward=True 34 | ) 35 | plt.figure(figsize=(12, 3)) 36 | plt.plot(workers, speedup, "ko-") 37 | plt.xlabel("# Workers") 38 | plt.ylabel("Speed Up") 39 | plt.title("Forward scalability test") 40 | plt.tight_layout() 41 | 42 | ############################################################################### 43 | # And likewise on the adjoint operation 44 | compute_times, speedup = pylops.utils.multiproc.scalability_test( 45 | Op, np.ones(Op.shape[0]), workers=workers, forward=False 46 | ) 47 | plt.figure(figsize=(12, 3)) 48 | plt.plot(workers, speedup, "ko-") 49 | plt.xlabel("# Workers") 50 | plt.ylabel("Speed Up") 51 | plt.title("Adjoint scalability test") 52 | plt.tight_layout() 53 | 54 | ############################################################################### 55 | # Note that we have not tested here the case with 1 worker. In this specific 56 | # case, since the computations are very small, the overhead of spawning processes 57 | # is actually dominating the time of computations and so computing the 58 | # forward and adjoint operations with a single worker is more efficient. We 59 | # hope that this example can serve as a basis to inspect the scalability of 60 | # multiprocessing-enabled operators and choose the best number of processes. 61 | -------------------------------------------------------------------------------- /examples/plot_pad.py: -------------------------------------------------------------------------------- 1 | """ 2 | Padding 3 | ======= 4 | This example shows how to use the :py:class:`pylops.Pad` operator to zero-pad a 5 | model 6 | """ 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | 10 | import pylops 11 | 12 | plt.close("all") 13 | 14 | ############################################################################### 15 | # Let's define a pad operator ``Pop`` for one dimensional data 16 | dims = 10 17 | pad = (2, 3) 18 | 19 | Pop = pylops.Pad(dims, pad) 20 | 21 | x = np.arange(dims) + 1.0 22 | y = Pop * x 23 | xadj = Pop.H * y 24 | 25 | print(f"x = {x}") 26 | print(f"P*x = {y}") 27 | print(f"P'*y = {xadj}") 28 | 29 | ############################################################################### 30 | # We move now to a multi-dimensional case. We pad the input model 31 | # with different extents along both dimensions 32 | dims = (5, 4) 33 | pad = ((1, 0), (3, 4)) 34 | 35 | Pop = pylops.Pad(dims, pad) 36 | 37 | x = (np.arange(np.prod(np.array(dims))) + 1.0).reshape(dims) 38 | y = Pop * x 39 | xadj = Pop.H * y 40 | 41 | fig, axs = plt.subplots(1, 3, figsize=(10, 4)) 42 | fig.suptitle("Pad for 2d data", fontsize=14, fontweight="bold", y=1.15) 43 | axs[0].imshow(x, cmap="rainbow", vmin=0, vmax=np.prod(np.array(dims)) + 1) 44 | axs[0].set_title(r"$x$") 45 | axs[0].axis("tight") 46 | axs[1].imshow(y, cmap="rainbow", vmin=0, vmax=np.prod(np.array(dims)) + 1) 47 | axs[1].set_title(r"$y = P x$") 48 | axs[1].axis("tight") 49 | axs[2].imshow(xadj, cmap="rainbow", vmin=0, vmax=np.prod(np.array(dims)) + 1) 50 | axs[2].set_title(r"$x_{adj} = P^{H} y$") 51 | axs[2].axis("tight") 52 | plt.tight_layout() 53 | -------------------------------------------------------------------------------- /examples/plot_real.py: -------------------------------------------------------------------------------- 1 | """ 2 | Real 3 | ==== 4 | 5 | This example shows how to use the :py:class:`pylops.basicoperators.Real` 6 | operator. 7 | This operator returns the real part of the data in forward and adjoint mode, 8 | but the forward output will be a real number, while the adjoint output will 9 | be a complex number with a zero-valued imaginary part. 10 | """ 11 | import matplotlib.pyplot as plt 12 | import numpy as np 13 | 14 | import pylops 15 | 16 | plt.close("all") 17 | 18 | ############################################################################### 19 | # Let's define a Real operator :math:`\mathbf{\Re}` to extract the real 20 | # component of the input. 21 | 22 | M = 5 23 | x = np.arange(M) + 1j * np.arange(M)[::-1] 24 | Rop = pylops.basicoperators.Real(M, dtype="complex128") 25 | 26 | y = Rop * x 27 | xadj = Rop.H * y 28 | 29 | _, axs = plt.subplots(1, 3, figsize=(10, 4)) 30 | axs[0].plot(np.real(x), lw=2, label="Real") 31 | axs[0].plot(np.imag(x), lw=2, label="Imag") 32 | axs[0].legend() 33 | axs[0].set_title("Input") 34 | axs[1].plot(np.real(y), lw=2, label="Real") 35 | axs[1].plot(np.imag(y), lw=2, label="Imag") 36 | axs[1].legend() 37 | axs[1].set_title("Forward of Input") 38 | axs[2].plot(np.real(xadj), lw=2, label="Real") 39 | axs[2].plot(np.imag(xadj), lw=2, label="Imag") 40 | axs[2].legend() 41 | axs[2].set_title("Adjoint of Forward") 42 | plt.tight_layout() 43 | -------------------------------------------------------------------------------- /examples/plot_roll.py: -------------------------------------------------------------------------------- 1 | """ 2 | Roll 3 | ==== 4 | This example shows how to use the :py:class:`pylops.Roll` operator. 5 | 6 | This operator simply shifts elements of multi-dimensional array along a 7 | specified direction a chosen number of samples. 8 | """ 9 | import matplotlib.pyplot as plt 10 | import numpy as np 11 | 12 | import pylops 13 | 14 | plt.close("all") 15 | 16 | ############################################################################### 17 | # Let's start with a 1d example. We make a signal, shift it by two samples 18 | # and then shift it back using its adjoint. We can immediately see how the 19 | # adjoint of this operator is equivalent to its inverse. 20 | nx = 10 21 | x = np.arange(nx) 22 | 23 | Rop = pylops.Roll(nx, shift=2) 24 | 25 | y = Rop * x 26 | xadj = Rop.H * y 27 | 28 | plt.figure() 29 | plt.plot(x, "k", lw=2, label="x") 30 | plt.plot(y, "b", lw=2, label="y") 31 | plt.plot(xadj, "--r", lw=2, label="xadj") 32 | plt.title("1D Roll") 33 | plt.legend() 34 | plt.tight_layout() 35 | 36 | ############################################################################### 37 | # We can now do the same with a 2d array. 38 | ny, nx = 10, 5 39 | x = np.arange(ny * nx).reshape(ny, nx) 40 | 41 | Rop = pylops.Roll(dims=(ny, nx), axis=1, shift=-2) 42 | 43 | y = Rop * x 44 | xadj = Rop.H * y 45 | 46 | fig, axs = plt.subplots(1, 3, figsize=(10, 4)) 47 | fig.suptitle("Roll for 2d data", fontsize=14, fontweight="bold", y=1.15) 48 | axs[0].imshow(x, cmap="rainbow", vmin=0, vmax=50) 49 | axs[0].set_title(r"$x$") 50 | axs[0].axis("tight") 51 | axs[1].imshow(y, cmap="rainbow", vmin=0, vmax=50) 52 | axs[1].set_title(r"$y = R x$") 53 | axs[1].axis("tight") 54 | axs[2].imshow(xadj, cmap="rainbow", vmin=0, vmax=50) 55 | axs[2].set_title(r"$x_{adj} = R^H y$") 56 | axs[2].axis("tight") 57 | plt.tight_layout() 58 | -------------------------------------------------------------------------------- /examples/plot_smoothing1d.py: -------------------------------------------------------------------------------- 1 | r""" 2 | 1D Smoothing 3 | ============ 4 | 5 | This example shows how to use the :py:class:`pylops.Smoothing1D` operator 6 | to smooth an input signal along a given axis. 7 | 8 | Derivative (or roughening) operators are generally used *regularization* 9 | in inverse problems. Smoothing has the opposite effect of roughening and 10 | it can be employed as *preconditioning* in inverse problems. 11 | 12 | A smoothing operator is a simple compact filter on lenght :math:`n_{smooth}` 13 | and each elements is equal to :math:`1/n_{smooth}`. 14 | """ 15 | 16 | import matplotlib.pyplot as plt 17 | import numpy as np 18 | 19 | import pylops 20 | 21 | plt.close("all") 22 | 23 | ############################################################################### 24 | # Define the input parameters: number of samples of input signal (``N``) and 25 | # lenght of the smoothing filter regression coefficients (:math:`n_{smooth}`). 26 | # In this first case the input signal is one at the center and zero elsewhere. 27 | N = 31 28 | nsmooth = 7 29 | x = np.zeros(N) 30 | x[int(N / 2)] = 1 31 | 32 | Sop = pylops.Smoothing1D(nsmooth=nsmooth, dims=[N], dtype="float32") 33 | 34 | y = Sop * x 35 | xadj = Sop.H * y 36 | 37 | fig, ax = plt.subplots(1, 1, figsize=(10, 3)) 38 | ax.plot(x, "k", lw=2, label=r"$x$") 39 | ax.plot(y, "r", lw=2, label=r"$y=Ax$") 40 | ax.set_title("Smoothing in 1st direction", fontsize=14, fontweight="bold") 41 | ax.legend() 42 | plt.tight_layout() 43 | 44 | ############################################################################### 45 | # Let's repeat the same exercise with a random signal as input. After applying smoothing, 46 | # we will also try to invert it. 47 | N = 120 48 | nsmooth = 13 49 | x = np.random.normal(0, 1, N) 50 | Sop = pylops.Smoothing1D(nsmooth=13, dims=(N), dtype="float32") 51 | 52 | y = Sop * x 53 | xest = Sop / y 54 | 55 | fig, ax = plt.subplots(1, 1, figsize=(10, 3)) 56 | ax.plot(x, "k", lw=2, label=r"$x$") 57 | ax.plot(y, "r", lw=2, label=r"$y=Ax$") 58 | ax.plot(xest, "--g", lw=2, label=r"$x_{ext}$") 59 | ax.set_title("Smoothing in 1st direction", fontsize=14, fontweight="bold") 60 | ax.legend() 61 | plt.tight_layout() 62 | 63 | ############################################################################### 64 | # Finally we show that the same operator can be applied to multi-dimensional 65 | # data along a chosen axis. 66 | A = np.zeros((11, 21)) 67 | A[5, 10] = 1 68 | 69 | Sop = pylops.Smoothing1D(nsmooth=5, dims=(11, 21), axis=0, dtype="float64") 70 | B = Sop * A 71 | 72 | fig, axs = plt.subplots(1, 2, figsize=(10, 3)) 73 | fig.suptitle( 74 | "Smoothing in 1st direction for 2d data", fontsize=14, fontweight="bold", y=0.95 75 | ) 76 | im = axs[0].imshow(A, interpolation="nearest", vmin=0, vmax=1) 77 | axs[0].axis("tight") 78 | axs[0].set_title("Model") 79 | plt.colorbar(im, ax=axs[0]) 80 | im = axs[1].imshow(B, interpolation="nearest", vmin=0, vmax=1) 81 | axs[1].axis("tight") 82 | axs[1].set_title("Data") 83 | plt.colorbar(im, ax=axs[1]) 84 | plt.tight_layout() 85 | plt.subplots_adjust(top=0.8) 86 | -------------------------------------------------------------------------------- /examples/plot_smoothing2d.py: -------------------------------------------------------------------------------- 1 | """ 2 | 2D Smoothing 3 | ============ 4 | 5 | This example shows how to use the :py:class:`pylops.Smoothing2D` operator 6 | to smooth a multi-dimensional input signal along two given axes. 7 | 8 | """ 9 | import matplotlib.pyplot as plt 10 | import numpy as np 11 | 12 | import pylops 13 | 14 | plt.close("all") 15 | 16 | ############################################################################### 17 | # Define the input parameters: number of samples of input signal (``N`` and ``M``) and 18 | # lenght of the smoothing filter regression coefficients 19 | # (:math:`n_{smooth,1}` and :math:`n_{smooth,2}`). In this first case the input 20 | # signal is one at the center and zero elsewhere. 21 | N, M = 11, 21 22 | nsmooth1, nsmooth2 = 5, 3 23 | A = np.zeros((N, M)) 24 | A[5, 10] = 1 25 | 26 | Sop = pylops.Smoothing2D(nsmooth=[nsmooth1, nsmooth2], dims=[N, M], dtype="float64") 27 | B = Sop * A 28 | 29 | ############################################################################### 30 | # After applying smoothing, we will also try to invert it. 31 | Aest = (Sop / B.ravel()).reshape(Sop.dims) 32 | 33 | fig, axs = plt.subplots(1, 3, figsize=(10, 3)) 34 | im = axs[0].imshow(A, interpolation="nearest", vmin=0, vmax=1) 35 | axs[0].axis("tight") 36 | axs[0].set_title("Model") 37 | plt.colorbar(im, ax=axs[0]) 38 | im = axs[1].imshow(B, interpolation="nearest", vmin=0, vmax=1) 39 | axs[1].axis("tight") 40 | axs[1].set_title("Data") 41 | plt.colorbar(im, ax=axs[1]) 42 | im = axs[2].imshow(Aest, interpolation="nearest", vmin=0, vmax=1) 43 | axs[2].axis("tight") 44 | axs[2].set_title("Estimated model") 45 | plt.colorbar(im, ax=axs[2]) 46 | plt.tight_layout() 47 | -------------------------------------------------------------------------------- /examples/plot_sum.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sum 3 | === 4 | This example shows how to use the :py:class:`pylops.Sum` operator to stack 5 | values along an axis of a multi-dimensional array 6 | """ 7 | import matplotlib.gridspec as pltgs 8 | import matplotlib.pyplot as plt 9 | import numpy as np 10 | 11 | import pylops 12 | 13 | plt.close("all") 14 | 15 | ############################################################################### 16 | # Let's start by defining a 2-dimensional data 17 | ny, nx = 5, 7 18 | x = (np.arange(ny * nx)).reshape(ny, nx) 19 | 20 | ############################################################################### 21 | # We can now create the operator and peform forward and adjoint 22 | Sop = pylops.Sum(dims=(ny, nx), axis=0) 23 | 24 | y = Sop * x 25 | xadj = Sop.H * y 26 | 27 | gs = pltgs.GridSpec(1, 7) 28 | fig = plt.figure(figsize=(7, 4)) 29 | ax = plt.subplot(gs[0, 0:3]) 30 | im = ax.imshow(x, cmap="rainbow", vmin=0, vmax=ny * nx) 31 | ax.set_title("x", size=20, fontweight="bold") 32 | ax.set_xticks(np.arange(nx - 1) + 0.5) 33 | ax.set_yticks(np.arange(ny - 1) + 0.5) 34 | ax.grid(linewidth=3, color="white") 35 | ax.xaxis.set_ticklabels([]) 36 | ax.yaxis.set_ticklabels([]) 37 | ax.axis("tight") 38 | ax = plt.subplot(gs[0, 3]) 39 | ax.imshow(y[:, np.newaxis], cmap="rainbow", vmin=0, vmax=ny * nx) 40 | ax.set_title("y", size=20, fontweight="bold") 41 | ax.set_xticks([]) 42 | ax.set_yticks(np.arange(nx - 1) + 0.5) 43 | ax.grid(linewidth=3, color="white") 44 | ax.xaxis.set_ticklabels([]) 45 | ax.yaxis.set_ticklabels([]) 46 | ax.axis("tight") 47 | ax = plt.subplot(gs[0, 4:]) 48 | ax.imshow(xadj, cmap="rainbow", vmin=0, vmax=ny * nx) 49 | ax.set_title("xadj", size=20, fontweight="bold") 50 | ax.set_xticks(np.arange(nx - 1) + 0.5) 51 | ax.set_yticks(np.arange(ny - 1) + 0.5) 52 | ax.grid(linewidth=3, color="white") 53 | ax.xaxis.set_ticklabels([]) 54 | ax.yaxis.set_ticklabels([]) 55 | ax.axis("tight") 56 | plt.tight_layout() 57 | 58 | ############################################################################### 59 | # Note that since the Sum operator creates and under-determined system of 60 | # equations (data has always lower dimensionality than the model), an exact 61 | # inverse is not possible for this operator. 62 | -------------------------------------------------------------------------------- /examples/plot_tapers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tapers 3 | ====== 4 | This example shows how to create some basic tapers in 1d, 2d, and 3d 5 | using the :py:mod:`pylops.utils.tapers` module. 6 | """ 7 | import matplotlib.pyplot as plt 8 | 9 | import pylops 10 | 11 | plt.close("all") 12 | 13 | ############################################ 14 | # Let's first define the time and space axes 15 | par = { 16 | "ox": -200, 17 | "dx": 2, 18 | "nx": 201, 19 | "oy": -100, 20 | "dy": 2, 21 | "ny": 101, 22 | "ot": 0, 23 | "dt": 0.004, 24 | "nt": 501, 25 | "ntapx": 21, 26 | "ntapy": 31, 27 | } 28 | 29 | ############################################ 30 | # We can now create tapers in 1d 31 | tap_han = pylops.utils.tapers.hanningtaper(par["nx"], par["ntapx"]) 32 | tap_cos = pylops.utils.tapers.cosinetaper(par["nx"], par["ntapx"], False) 33 | tap_cos2 = pylops.utils.tapers.cosinetaper(par["nx"], par["ntapx"], True) 34 | 35 | plt.figure(figsize=(7, 3)) 36 | plt.plot(tap_han, "r", label="hanning") 37 | plt.plot(tap_cos, "k", label="cosine") 38 | plt.plot(tap_cos2, "b", label="cosine square") 39 | plt.title("Tapers") 40 | plt.legend() 41 | plt.tight_layout() 42 | 43 | ############################################ 44 | # Similarly we can create 2d and 3d tapers with any of the tapers above 45 | tap2d = pylops.utils.tapers.taper2d(par["nt"], par["nx"], par["ntapx"]) 46 | 47 | plt.figure(figsize=(7, 3)) 48 | plt.plot(tap2d[:, par["nt"] // 2], "k", lw=2) 49 | plt.title("Taper") 50 | plt.tight_layout() 51 | 52 | tap3d = pylops.utils.tapers.taper3d( 53 | par["nt"], (par["ny"], par["nx"]), (par["ntapy"], par["ntapx"]) 54 | ) 55 | 56 | plt.figure(figsize=(7, 3)) 57 | plt.imshow(tap3d[:, :, par["nt"] // 2], "jet") 58 | plt.title("Taper in y-x slice") 59 | plt.xlabel("x") 60 | plt.ylabel("y") 61 | plt.tight_layout() 62 | -------------------------------------------------------------------------------- /examples/plot_transpose.py: -------------------------------------------------------------------------------- 1 | r""" 2 | Transpose 3 | ========= 4 | This example shows how to use the :py:class:`pylops.Transpose` 5 | operator. For arrays that are 2-dimensional in nature this operator 6 | simply transposes rows and columns. For multi-dimensional arrays, this 7 | operator can be used to permute dimensions 8 | """ 9 | import matplotlib.pyplot as plt 10 | import numpy as np 11 | 12 | import pylops 13 | 14 | plt.close("all") 15 | np.random.seed(0) 16 | 17 | ############################################################################### 18 | # Let's start by creating a 2-dimensional array 19 | dims = (20, 40) 20 | x = np.arange(800).reshape(dims) 21 | 22 | ############################################################################### 23 | # We use now the :py:class:`pylops.Transpose` operator to swap the two 24 | # dimensions. As you will see the adjoint of this operator brings the data 25 | # back to its original model, or in other words the adjoint operator is equal 26 | # in this case to the inverse operator. 27 | Top = pylops.Transpose(dims=dims, axes=(1, 0)) 28 | 29 | y = Top * x 30 | xadj = Top.H * y 31 | 32 | fig, axs = plt.subplots(1, 3, figsize=(10, 4)) 33 | fig.suptitle("Transpose for 2d data", fontsize=14, fontweight="bold", y=1.15) 34 | axs[0].imshow(x, cmap="rainbow", vmin=0, vmax=800) 35 | axs[0].set_title(r"$x$") 36 | axs[0].axis("tight") 37 | axs[1].imshow(y, cmap="rainbow", vmin=0, vmax=800) 38 | axs[1].set_title(r"$y = F x$") 39 | axs[1].axis("tight") 40 | axs[2].imshow(xadj, cmap="rainbow", vmin=0, vmax=800) 41 | axs[2].set_title(r"$x_{adj} = F^H y$") 42 | axs[2].axis("tight") 43 | plt.tight_layout() 44 | 45 | ############################################################################### 46 | # A similar approach can of course be taken two swap multiple axes of 47 | # multi-dimensional arrays for any number of dimensions. 48 | -------------------------------------------------------------------------------- /examples/plot_wavs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wavelets 3 | ======== 4 | This example shows how to use the different wavelets available PyLops. 5 | """ 6 | import matplotlib.pyplot as plt 7 | import numpy as np 8 | 9 | import pylops 10 | 11 | plt.close("all") 12 | 13 | ############################################################################### 14 | # Let's start with defining a time axis and creating the FFT operator 15 | dt = 0.004 16 | nt = 1001 17 | t = np.arange(nt) * dt 18 | 19 | Fop = pylops.signalprocessing.FFT(2 * nt - 1, sampling=dt, real=True) 20 | f = Fop.f 21 | 22 | ############################################################################### 23 | # We can now create the different wavelets and display them 24 | 25 | # Gaussian 26 | wg, twg, wgc = pylops.utils.wavelets.gaussian(t, std=2) 27 | 28 | # Gaussian 29 | wk, twk, wgk = pylops.utils.wavelets.klauder(t, f=[4, 30], taper=np.hanning) 30 | 31 | # Ormsby 32 | wo, two, woc = pylops.utils.wavelets.ormsby(t, f=[5, 9, 25, 30], taper=np.hanning) 33 | 34 | # Ricker 35 | wr, twr, wrc = pylops.utils.wavelets.ricker(t, f0=17) 36 | 37 | # Frequency domain 38 | wgf = Fop @ wg 39 | wkf = Fop @ wk 40 | wof = Fop @ wo 41 | wrf = Fop @ wr 42 | 43 | ############################################################################### 44 | fig, axs = plt.subplots(1, 2, figsize=(14, 6)) 45 | axs[0].plot(twg, wg, "k", lw=2, label="Gaussian") 46 | axs[0].plot(twk, wk, "r", lw=2, label="Klauder") 47 | axs[0].plot(two, wo, "b", lw=2, label="Ormsby") 48 | axs[0].plot(twr, wr, "y--", lw=2, label="Ricker") 49 | axs[0].set(xlim=(-0.4, 0.4), xlabel="Time [s]") 50 | axs[0].legend() 51 | axs[1].plot(f, np.abs(wgf) / np.abs(wgf).max(), "k", lw=2, label="Gaussian") 52 | axs[1].plot(f, np.abs(wkf) / np.abs(wkf).max(), "r", lw=2, label="Klauder") 53 | axs[1].plot(f, np.abs(wof) / np.abs(wof).max(), "b", lw=2, label="Ormsby") 54 | axs[1].plot(f, np.abs(wrf) / np.abs(wrf).max(), "y--", lw=2, label="Ricker") 55 | axs[1].set(xlim=(0, 50), xlabel="Frequency [Hz]") 56 | axs[1].legend() 57 | plt.tight_layout() 58 | -------------------------------------------------------------------------------- /examples/plot_zero.py: -------------------------------------------------------------------------------- 1 | """ 2 | Zero 3 | ==== 4 | 5 | This example shows how to use the :py:class:`pylops.basicoperators.Zero` operator. 6 | This operators simply zeroes the data in forward mode and the model in adjoint mode. 7 | """ 8 | import matplotlib.gridspec as pltgs 9 | import matplotlib.pyplot as plt 10 | import numpy as np 11 | 12 | import pylops 13 | 14 | plt.close("all") 15 | 16 | ############################################################################### 17 | # Let's define an zero operator :math:`\mathbf{0}` with same number of elements for data 18 | # :math:`N` and model :math:`M`. 19 | 20 | N, M = 5, 5 21 | x = np.arange(M) 22 | Zop = pylops.basicoperators.Zero(M, dtype="int") 23 | 24 | y = Zop * x 25 | xadj = Zop.H * y 26 | 27 | gs = pltgs.GridSpec(1, 6) 28 | fig = plt.figure(figsize=(7, 4)) 29 | ax = plt.subplot(gs[0, 0:3]) 30 | ax.imshow(np.zeros((N, N)), cmap="rainbow", vmin=-M, vmax=M) 31 | ax.set_title("A", size=20, fontweight="bold") 32 | ax.set_xticks(np.arange(N - 1) + 0.5) 33 | ax.set_yticks(np.arange(M - 1) + 0.5) 34 | ax.grid(linewidth=3, color="white") 35 | ax.xaxis.set_ticklabels([]) 36 | ax.yaxis.set_ticklabels([]) 37 | ax = plt.subplot(gs[0, 3]) 38 | im = ax.imshow(x[:, np.newaxis], cmap="rainbow", vmin=-M, vmax=M) 39 | ax.set_title("x", size=20, fontweight="bold") 40 | ax.set_xticks([]) 41 | ax.set_yticks(np.arange(M - 1) + 0.5) 42 | ax.grid(linewidth=3, color="white") 43 | ax.xaxis.set_ticklabels([]) 44 | ax.yaxis.set_ticklabels([]) 45 | ax = plt.subplot(gs[0, 4]) 46 | ax.text( 47 | 0.35, 48 | 0.5, 49 | "=", 50 | horizontalalignment="center", 51 | verticalalignment="center", 52 | size=40, 53 | fontweight="bold", 54 | ) 55 | ax.axis("off") 56 | ax = plt.subplot(gs[0, 5]) 57 | ax.imshow(y[:, np.newaxis], cmap="rainbow", vmin=-M, vmax=M) 58 | ax.set_title("y", size=20, fontweight="bold") 59 | ax.set_xticks([]) 60 | ax.set_yticks(np.arange(N - 1) + 0.5) 61 | ax.grid(linewidth=3, color="white") 62 | ax.xaxis.set_ticklabels([]) 63 | ax.yaxis.set_ticklabels([]) 64 | fig.colorbar(im, ax=ax, ticks=[0], pad=0.3, shrink=0.7) 65 | plt.tight_layout() 66 | 67 | ############################################################################### 68 | # Similarly we can consider the case with data bigger than model 69 | N, M = 10, 5 70 | x = np.arange(M) 71 | Zop = pylops.Zero(N, M, dtype="int") 72 | 73 | y = Zop * x 74 | xadj = Zop.H * y 75 | 76 | print(f"x = {x}") 77 | print(f"0*x = {y}") 78 | print(f"0'*y = {xadj}") 79 | 80 | ############################################################################### 81 | # and model bigger than data 82 | N, M = 5, 10 83 | x = np.arange(M) 84 | Zop = pylops.Zero(N, M, dtype="int") 85 | 86 | y = Zop * x 87 | xadj = Zop.H * y 88 | 89 | print(f"x = {x}") 90 | print(f"0*x = {y}") 91 | print(f"0'*y = {xadj}") 92 | 93 | ############################################################################### 94 | # Note that this operator can be useful in many real-life applications when for 95 | # example we want to manipulate a subset of the model array and keep intact the 96 | # rest of the array. For example: 97 | # 98 | # .. math:: 99 | # \begin{bmatrix} 100 | # \mathbf{A} \quad \mathbf{0} 101 | # \end{bmatrix} 102 | # \begin{bmatrix} 103 | # \mathbf{x_1} \\ 104 | # \mathbf{x_2} 105 | # \end{bmatrix} = \mathbf{A} \mathbf{x_1} 106 | # 107 | # Refer to the tutorial on *Optimization* for more details on this. 108 | -------------------------------------------------------------------------------- /pylops/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | PyLops 3 | ====== 4 | 5 | Linear operators and inverse problems are at the core of many of the most used 6 | algorithms in signal processing, image processing, and remote sensing. 7 | When dealing with small-scale problems, the Python numerical scientific 8 | libraries `numpy `_ 9 | and `scipy `_ allow to perform most 10 | of the underlying matrix operations (e.g., computation of matrix-vector 11 | products and manipulation of matrices) in a simple and expressive way. 12 | 13 | Many useful operators, however, do not lend themselves to an explicit matrix 14 | representation when used to solve large-scale problems. PyLops operators, 15 | on the other hand, still represent a matrix and can be treated in a similar 16 | way, but do not rely on the explicit creation of a dense (or sparse) matrix 17 | itself. Conversely, the forward and adjoint operators are represented by small 18 | pieces of codes that mimic the effect of the matrix on a vector or 19 | another matrix. 20 | 21 | Luckily, many iterative methods (e.g. cg, lsqr) do not need to know the 22 | individual entries of a matrix to solve a linear system. Such solvers only 23 | require the computation of forward and adjoint matrix-vector products as 24 | done for any of the PyLops operators. 25 | 26 | PyLops provides 27 | 1. A general construct for creating Linear Operators 28 | 2. An extensive set of commonly used linear operators 29 | 3. A set of least-squares and sparse solvers for linear operators. 30 | 31 | Available subpackages 32 | --------------------- 33 | basicoperators 34 | Basic Linear Operators 35 | signalprocessing 36 | Linear Operators for Signal Processing operations 37 | avo 38 | Linear Operators for Seismic Reservoir Characterization 39 | waveeqprocessing 40 | Linear Operators for Wave Equation oriented processing 41 | optimization 42 | Solvers 43 | utils 44 | Utility routines 45 | 46 | """ 47 | 48 | from .config import * 49 | from .linearoperator import * 50 | from .torchoperator import * 51 | from .pytensoroperator import * 52 | from .jaxoperator import * 53 | from .basicoperators import * 54 | from . import ( 55 | avo, 56 | basicoperators, 57 | optimization, 58 | signalprocessing, 59 | utils, 60 | waveeqprocessing, 61 | ) 62 | from .avo.poststack import * 63 | from .avo.prestack import * 64 | from .optimization.basic import * 65 | from .optimization.leastsquares import * 66 | from .optimization.sparsity import * 67 | from .utils.seismicevents import * 68 | from .utils.tapers import * 69 | from .utils.utils import * 70 | from .utils.wavelets import * 71 | 72 | try: 73 | from .version import version as __version__ 74 | except ImportError: 75 | # If it was not installed, then we don't know the version. We could throw a 76 | # warning here, but this case *should* be rare. pylops should be installed 77 | # properly! 78 | from datetime import datetime 79 | 80 | __version__ = "unknown-" + datetime.today().strftime("%Y%m%d") 81 | -------------------------------------------------------------------------------- /pylops/_torchoperator.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pylops.utils import deps 4 | 5 | if deps.torch_enabled: 6 | import torch 7 | from torch.utils.dlpack import from_dlpack, to_dlpack 8 | 9 | if deps.cupy_enabled: 10 | import cupy as cp 11 | 12 | 13 | class _TorchOperator(torch.autograd.Function): 14 | """Wrapper class for PyLops operators into Torch functions""" 15 | 16 | @staticmethod 17 | def forward(ctx, x, forw, adj, device, devicetorch): 18 | ctx.forw = forw 19 | ctx.adj = adj 20 | ctx.device = device 21 | ctx.devicetorch = devicetorch 22 | 23 | # check if data is moved to cpu and warn user 24 | if ctx.device == "cpu" and ctx.devicetorch != "cpu": 25 | logging.warning( 26 | "pylops operator will be applied on the cpu " 27 | "whilst the input torch vector is on " 28 | "%s, this may lead to poor performance" % ctx.devicetorch 29 | ) 30 | 31 | # prepare input 32 | if ctx.device == "cpu": 33 | # bring x to cpu and numpy 34 | x = x.cpu().detach().numpy() 35 | else: 36 | # pass x to cupy using DLPack 37 | x = cp.fromDlpack(to_dlpack(x)) 38 | 39 | # apply forward operator 40 | y = ctx.forw(x) 41 | 42 | # prepare output 43 | if ctx.device == "cpu": 44 | # move y to torch and device 45 | y = torch.from_numpy(y).to(ctx.devicetorch) 46 | else: 47 | # move y to torch and device 48 | y = from_dlpack(y.toDlpack()) 49 | return y 50 | 51 | @staticmethod 52 | def backward(ctx, y): 53 | # prepare input 54 | if ctx.device == "cpu": 55 | y = y.cpu().detach().numpy() 56 | else: 57 | # pass x to cupy using DLPack 58 | y = cp.fromDlpack(to_dlpack(y)) 59 | 60 | # apply adjoint operator 61 | x = ctx.adj(y) 62 | 63 | # prepare output 64 | if ctx.device == "cpu": 65 | x = torch.from_numpy(x).to(ctx.devicetorch) 66 | else: 67 | x = from_dlpack(x.toDlpack()) 68 | return x, None, None, None, None, None 69 | -------------------------------------------------------------------------------- /pylops/avo/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | AVO Operators 3 | ============= 4 | 5 | The subpackage avo provides linear operators and applications aimed at 6 | solving various inverse problems in the area of Seismic Reservoir 7 | Characterization. 8 | 9 | A list of available operators present in pylops.avo: 10 | 11 | AVOLinearModelling AVO modelling. 12 | PoststackLinearModelling Post-stack seismic modelling. 13 | PrestackLinearModelling Pre-stack seismic modelling. 14 | PrestackWaveletModelling Pre-stack modelling operator for wavelet. 15 | 16 | and a list of applications: 17 | 18 | PoststackInversion Post-stack seismic inversion. 19 | PrestackInversion Pre-stack seismic inversion. 20 | 21 | """ 22 | 23 | from .poststack import * 24 | from .prestack import * 25 | 26 | __all__ = [ 27 | "AVOLinearModelling", 28 | "PoststackLinearModelling", 29 | "PrestackWaveletModelling", 30 | "PrestackLinearModelling", 31 | "PoststackInversion", 32 | "PrestackInversion", 33 | ] 34 | -------------------------------------------------------------------------------- /pylops/basicoperators/conj.py: -------------------------------------------------------------------------------- 1 | __all__ = ["Conj"] 2 | 3 | 4 | from typing import Union 5 | 6 | import numpy as np 7 | 8 | from pylops import LinearOperator 9 | from pylops.utils._internal import _value_or_sized_to_tuple 10 | from pylops.utils.typing import DTypeLike, InputDimsLike, NDArray 11 | 12 | 13 | class Conj(LinearOperator): 14 | r"""Complex conjugate operator. 15 | 16 | Return the complex conjugate of the input. It is self-adjoint. 17 | 18 | Parameters 19 | ---------- 20 | dims : :obj:`int` or :obj:`tuple` 21 | Number of samples for each dimension 22 | dtype : :obj:`str`, optional 23 | Type of elements in input array. 24 | name : :obj:`str`, optional 25 | .. versionadded:: 2.0.0 26 | 27 | Name of operator (to be used by :func:`pylops.utils.describe.describe`) 28 | 29 | Attributes 30 | ---------- 31 | shape : :obj:`tuple` 32 | Operator shape 33 | explicit : :obj:`bool` 34 | Operator contains a matrix that can be solved explicitly (``True``) or 35 | not (``False``) 36 | 37 | Notes 38 | ----- 39 | In forward mode: 40 | 41 | .. math:: 42 | 43 | y_{i} = \Re\{x_{i}\} - i\Im\{x_{i}\} \quad \forall i=0,\ldots,N-1 44 | 45 | In adjoint mode: 46 | 47 | .. math:: 48 | 49 | x_{i} = \Re\{y_{i}\} - i\Im\{y_{i}\} \quad \forall i=0,\ldots,N-1 50 | 51 | """ 52 | 53 | def __init__( 54 | self, 55 | dims: Union[int, InputDimsLike], 56 | dtype: DTypeLike = "complex128", 57 | name: str = "C", 58 | ) -> None: 59 | dims = _value_or_sized_to_tuple(dims) 60 | super().__init__( 61 | dtype=np.dtype(dtype), dims=dims, dimsd=dims, clinear=False, name=name 62 | ) 63 | 64 | def _matvec(self, x: NDArray) -> NDArray: 65 | return x.conj() 66 | 67 | def _rmatvec(self, x: NDArray) -> NDArray: 68 | return x.conj() 69 | -------------------------------------------------------------------------------- /pylops/basicoperators/flip.py: -------------------------------------------------------------------------------- 1 | __all__ = ["Flip"] 2 | 3 | from typing import Union 4 | 5 | import numpy as np 6 | 7 | from pylops import LinearOperator 8 | from pylops.utils._internal import _value_or_sized_to_tuple 9 | from pylops.utils.decorators import reshaped 10 | from pylops.utils.typing import DTypeLike, InputDimsLike, NDArray 11 | 12 | 13 | class Flip(LinearOperator): 14 | r"""Flip along an axis. 15 | 16 | Flip a multi-dimensional array along ``axis``. 17 | 18 | Parameters 19 | ---------- 20 | dims : :obj:`list` or :obj:`int` 21 | Number of samples for each dimension 22 | axis : :obj:`int`, optional 23 | .. versionadded:: 2.0.0 24 | 25 | Axis along which model is flipped. 26 | dtype : :obj:`str`, optional 27 | Type of elements in input array. 28 | name : :obj:`str`, optional 29 | .. versionadded:: 2.0.0 30 | 31 | Name of operator (to be used by :func:`pylops.utils.describe.describe`) 32 | 33 | Attributes 34 | ---------- 35 | shape : :obj:`tuple` 36 | Operator shape 37 | explicit : :obj:`bool` 38 | Operator contains a matrix that can be solved explicitly 39 | (``True``) or not (``False``) 40 | 41 | Notes 42 | ----- 43 | The Flip operator flips the input model (and data) along any chosen 44 | direction. For simplicity, given a one dimensional array, 45 | in forward mode this is equivalent to: 46 | 47 | .. math:: 48 | y[i] = x[N-1-i] \quad \forall i=0,1,2,\ldots,N-1 49 | 50 | where :math:`N` is the dimension of the input model along ``axis``. As this operator is 51 | self-adjoint, :math:`x` and :math:`y` in the equation above are simply 52 | swapped in adjoint mode. 53 | 54 | """ 55 | 56 | def __init__( 57 | self, 58 | dims: Union[int, InputDimsLike], 59 | axis: int = -1, 60 | dtype: DTypeLike = "float64", 61 | name: str = "F", 62 | ) -> None: 63 | dims = _value_or_sized_to_tuple(dims) 64 | super().__init__(dtype=np.dtype(dtype), dims=dims, dimsd=dims, name=name) 65 | self.axis = axis 66 | 67 | @reshaped(swapaxis=True) 68 | def _matvec(self, x: NDArray) -> NDArray: 69 | y = np.flip(x, axis=-1) 70 | return y 71 | 72 | def _rmatvec(self, x: NDArray) -> NDArray: 73 | return self._matvec(x) 74 | -------------------------------------------------------------------------------- /pylops/basicoperators/gradient.py: -------------------------------------------------------------------------------- 1 | __all__ = ["Gradient"] 2 | 3 | from typing import Union 4 | 5 | from pylops import LinearOperator 6 | from pylops.basicoperators import FirstDerivative, VStack 7 | from pylops.utils._internal import _value_or_sized_to_tuple 8 | from pylops.utils.typing import DTypeLike, InputDimsLike, NDArray 9 | 10 | 11 | class Gradient(LinearOperator): 12 | r"""Gradient. 13 | 14 | Apply gradient operator to a multi-dimensional array. 15 | 16 | .. note:: At least 2 dimensions are required, use 17 | :py:func:`pylops.FirstDerivative` for 1d arrays. 18 | 19 | Parameters 20 | ---------- 21 | dims : :obj:`tuple` 22 | Number of samples for each dimension. 23 | sampling : :obj:`tuple`, optional 24 | Sampling steps for each direction. 25 | edge : :obj:`bool`, optional 26 | Use reduced order derivative at edges (``True``) or 27 | ignore them (``False``). 28 | kind : :obj:`str`, optional 29 | Derivative kind (``forward``, ``centered``, or ``backward``). 30 | dtype : :obj:`str`, optional 31 | Type of elements in input array. 32 | 33 | Notes 34 | ----- 35 | The Gradient operator applies a first-order derivative to each dimension of 36 | a multi-dimensional array in forward mode. 37 | 38 | For simplicity, given a three dimensional array, the Gradient in forward 39 | mode using a centered stencil can be expressed as: 40 | 41 | .. math:: 42 | \mathbf{g}_{i, j, k} = 43 | (f_{i+1, j, k} - f_{i-1, j, k}) / d_1 \mathbf{i_1} + 44 | (f_{i, j+1, k} - f_{i, j-1, k}) / d_2 \mathbf{i_2} + 45 | (f_{i, j, k+1} - f_{i, j, k-1}) / d_3 \mathbf{i_3} 46 | 47 | which is discretized as follows: 48 | 49 | .. math:: 50 | \mathbf{g} = 51 | \begin{bmatrix} 52 | \mathbf{df_1} \\ 53 | \mathbf{df_2} \\ 54 | \mathbf{df_3} 55 | \end{bmatrix} 56 | 57 | In adjoint mode, the adjoints of the first derivatives along different 58 | axes are instead summed together. 59 | 60 | """ 61 | 62 | def __init__(self, 63 | dims: Union[int, InputDimsLike], 64 | sampling: int = 1, 65 | edge: bool = False, 66 | kind: str = "centered", 67 | dtype: DTypeLike = "float64", name: str = 'G'): 68 | dims = _value_or_sized_to_tuple(dims) 69 | ndims = len(dims) 70 | sampling = _value_or_sized_to_tuple(sampling, repeat=ndims) 71 | self.sampling = sampling 72 | self.edge = edge 73 | self.kind = kind 74 | Op = VStack([FirstDerivative( 75 | dims=dims, 76 | axis=iax, 77 | sampling=sampling[iax], 78 | edge=edge, 79 | kind=kind, 80 | dtype=dtype, 81 | ) 82 | for iax in range(ndims) 83 | ]) 84 | super().__init__(Op=Op, dims=dims, dimsd=(ndims, *dims), name=name) 85 | 86 | def _matvec(self, x: NDArray) -> NDArray: 87 | return super()._matvec(x) 88 | 89 | def _rmatvec(self, x: NDArray) -> NDArray: 90 | return super()._rmatvec(x) 91 | -------------------------------------------------------------------------------- /pylops/basicoperators/imag.py: -------------------------------------------------------------------------------- 1 | __all__ = ["Imag"] 2 | 3 | from typing import Union 4 | 5 | import numpy as np 6 | 7 | from pylops import LinearOperator 8 | from pylops.utils._internal import _value_or_sized_to_tuple 9 | from pylops.utils.typing import DTypeLike, InputDimsLike, NDArray 10 | 11 | 12 | class Imag(LinearOperator): 13 | r"""Imag operator. 14 | 15 | Return the imaginary component of the input as a real value. 16 | The adjoint returns a complex number with zero real component and 17 | the imaginary component set to the real component of the input. 18 | 19 | Parameters 20 | ---------- 21 | dims : :obj:`int` or :obj:`tuple` 22 | Number of samples for each dimension 23 | dtype : :obj:`str`, optional 24 | Type of elements in input array. 25 | name : :obj:`str`, optional 26 | .. versionadded:: 2.0.0 27 | 28 | Name of operator (to be used by :func:`pylops.utils.describe.describe`) 29 | 30 | Attributes 31 | ---------- 32 | shape : :obj:`tuple` 33 | Operator shape 34 | explicit : :obj:`bool` 35 | Operator contains a matrix that can be solved explicitly (``True``) or 36 | not (``False``) 37 | 38 | Notes 39 | ----- 40 | In forward mode: 41 | 42 | .. math:: 43 | 44 | y_{i} = \Im\{x_{i}\} \quad \forall i=0,\ldots,N-1 45 | 46 | In adjoint mode: 47 | 48 | .. math:: 49 | 50 | x_{i} = 0 + i\Re\{y_{i}\} \quad \forall i=0,\ldots,N-1 51 | 52 | """ 53 | 54 | def __init__( 55 | self, 56 | dims: Union[int, InputDimsLike], 57 | dtype: DTypeLike = "complex128", 58 | name: str = "I", 59 | ) -> None: 60 | dims = _value_or_sized_to_tuple(dims) 61 | super().__init__( 62 | dtype=np.dtype(dtype), dims=dims, dimsd=dims, clinear=False, name=name 63 | ) 64 | self.rdtype = np.real(np.ones(1, self.dtype)).dtype 65 | 66 | def _matvec(self, x: NDArray) -> NDArray: 67 | return x.imag.astype(self.rdtype) 68 | 69 | def _rmatvec(self, x: NDArray) -> NDArray: 70 | return (0 + 1j * x.real).astype(self.dtype) 71 | -------------------------------------------------------------------------------- /pylops/basicoperators/kronecker.py: -------------------------------------------------------------------------------- 1 | __all__ = ["Kronecker"] 2 | 3 | import numpy as np 4 | 5 | from pylops import LinearOperator 6 | from pylops.utils.typing import DTypeLike, NDArray 7 | 8 | 9 | class Kronecker(LinearOperator): 10 | r"""Kronecker operator. 11 | 12 | Perform Kronecker product of two operators. Note that the combined operator 13 | is never created explicitly, rather the product of this operator with the 14 | model vector is performed in forward mode, or the product of the adjoint of 15 | this operator and the data vector in adjoint mode. 16 | 17 | Parameters 18 | ---------- 19 | Op1 : :obj:`pylops.LinearOperator` 20 | First operator 21 | Op2 : :obj:`pylops.LinearOperator` 22 | Second operator 23 | dtype : :obj:`str`, optional 24 | Type of elements in input array. 25 | name : :obj:`str`, optional 26 | .. versionadded:: 2.0.0 27 | 28 | Name of operator (to be used by :func:`pylops.utils.describe.describe`) 29 | 30 | Attributes 31 | ---------- 32 | shape : :obj:`tuple` 33 | Operator shape 34 | explicit : :obj:`bool` 35 | Operator contains a matrix that can be solved 36 | explicitly (``True``) or not (``False``) 37 | 38 | Notes 39 | ----- 40 | The Kronecker product (denoted with :math:`\otimes`) is an operation 41 | on two operators :math:`\mathbf{Op}_1` and :math:`\mathbf{Op}_2` of 42 | sizes :math:`\lbrack n_1 \times m_1 \rbrack` and 43 | :math:`\lbrack n_2 \times m_2 \rbrack` respectively, resulting in a 44 | block matrix of size :math:`\lbrack n_1 n_2 \times m_1 m_2 \rbrack`. 45 | 46 | .. math:: 47 | 48 | \mathbf{Op}_1 \otimes \mathbf{Op}_2 = \begin{bmatrix} 49 | \text{Op}_1^{1,1} \mathbf{Op}_2 & \ldots & \text{Op}_1^{1,m_1} \mathbf{Op}_2 \\ 50 | \vdots & \ddots & \vdots \\ 51 | \text{Op}_1^{n_1,1} \mathbf{Op}_2 & \ldots & \text{Op}_1^{n_1,m_1} \mathbf{Op}_2 52 | \end{bmatrix} 53 | 54 | The application of the resulting matrix to a vector :math:`\mathbf{x}` of 55 | size :math:`\lbrack m_1 m_2 \times 1 \rbrack` is equivalent to the 56 | application of the second operator :math:`\mathbf{Op}_2` to the rows of 57 | a matrix of size :math:`\lbrack m_2 \times m_1 \rbrack` obtained by 58 | reshaping the input vector :math:`\mathbf{x}`, followed by the application 59 | of the first operator to the transposed matrix produced by the first 60 | operator. In adjoint mode the same procedure is followed but the adjoint of 61 | each operator is used. 62 | 63 | """ 64 | 65 | def __init__( 66 | self, 67 | Op1: LinearOperator, 68 | Op2: LinearOperator, 69 | dtype: DTypeLike = "float64", 70 | name: str = "K", 71 | ) -> None: 72 | self.Op1 = Op1 73 | self.Op2 = Op2 74 | self.Op1H = self.Op1.H 75 | self.Op2H = self.Op2.H 76 | shape = ( 77 | self.Op1.shape[0] * self.Op2.shape[0], 78 | self.Op1.shape[1] * self.Op2.shape[1], 79 | ) 80 | super().__init__(dtype=np.dtype(dtype), shape=shape, name=name) 81 | 82 | def _matvec(self, x: NDArray) -> NDArray: 83 | x = x.reshape(self.Op1.shape[1], self.Op2.shape[1]) 84 | y = self.Op2.matmat(x.T).T 85 | y = self.Op1.matmat(y).ravel() 86 | return y 87 | 88 | def _rmatvec(self, x: NDArray) -> NDArray: 89 | x = x.reshape(self.Op1.shape[0], self.Op2.shape[0]) 90 | y = self.Op2H.matmat(x.T).T 91 | y = self.Op1H.matmat(y).ravel() 92 | return y 93 | -------------------------------------------------------------------------------- /pylops/basicoperators/linearregression.py: -------------------------------------------------------------------------------- 1 | __all__ = ["LinearRegression"] 2 | 3 | import logging 4 | 5 | import numpy.typing as npt 6 | 7 | from pylops.basicoperators import Regression 8 | from pylops.utils.typing import DTypeLike 9 | 10 | logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.WARNING) 11 | 12 | 13 | class LinearRegression(Regression): 14 | r"""Linear regression. 15 | 16 | Creates an operator that applies linear regression to a set of points. 17 | Values along the :math:`t`-axis must be provided while initializing the operator. 18 | Intercept and gradient form the model vector to be provided in forward 19 | mode, while the values of the regression line curve shall be provided 20 | in adjoint mode. 21 | 22 | Parameters 23 | ---------- 24 | taxis : :obj:`numpy.ndarray` 25 | Elements along the :math:`t`-axis. 26 | dtype : :obj:`str`, optional 27 | Type of elements in input array. 28 | 29 | Attributes 30 | ---------- 31 | shape : :obj:`tuple` 32 | Operator shape 33 | explicit : :obj:`bool` 34 | Operator contains a matrix that can be solved explicitly 35 | (``True``) or not (``False``) 36 | 37 | Raises 38 | ------ 39 | TypeError 40 | If ``taxis`` is not :obj:`numpy.ndarray`. 41 | 42 | See Also 43 | -------- 44 | Regression: Polynomial regression 45 | 46 | Notes 47 | ----- 48 | The LinearRegression operator solves the following problem: 49 | 50 | .. math:: 51 | y_i = x_0 + x_1 t_i \qquad \forall i=0,1,\ldots,N-1 52 | 53 | We can express this problem in a matrix form 54 | 55 | .. math:: 56 | \mathbf{y}= \mathbf{A} \mathbf{x} 57 | 58 | where 59 | 60 | .. math:: 61 | \mathbf{y}= [y_0, y_1,\ldots,y_{N-1}]^T, \qquad \mathbf{x}= [x_0, x_1]^T 62 | 63 | and 64 | 65 | .. math:: 66 | \mathbf{A} 67 | = \begin{bmatrix} 68 | 1 & t_{0} \\ 69 | 1 & t_{1} \\ 70 | \vdots & \vdots \\ 71 | 1 & t_{N-1} 72 | \end{bmatrix} 73 | 74 | Note that this is a particular case of the :py:class:`pylops.Regression` 75 | operator and it is in fact just a lazy call of that operator with 76 | ``order=1``. 77 | """ 78 | 79 | def __init__(self, taxis: npt.ArrayLike, dtype: DTypeLike = "float64", name: str = 'L'): 80 | super().__init__(taxis=taxis, order=1, dtype=dtype, name=name) 81 | -------------------------------------------------------------------------------- /pylops/basicoperators/memoizeoperator.py: -------------------------------------------------------------------------------- 1 | __all__ = ["MemoizeOperator"] 2 | 3 | from typing import List, Tuple 4 | 5 | import numpy as np 6 | 7 | from pylops import LinearOperator 8 | from pylops.utils.typing import NDArray 9 | 10 | 11 | class MemoizeOperator(LinearOperator): 12 | r"""Memoize Operator. 13 | 14 | This operator can be used to wrap any PyLops operator and add a memoize 15 | functionality 16 | and stores the last ``max_neval`` model/data 17 | vector pairs 18 | 19 | Parameters 20 | ---------- 21 | Op : :obj:`pylops.LinearOperator` 22 | PyLops linear operator 23 | max_neval : :obj:`int`, optional 24 | Maximum number of previous evaluations stored, 25 | use ``np.inf`` for infinite memory 26 | 27 | Attributes 28 | ---------- 29 | shape : :obj:`tuple` 30 | Operator shape :math:`[n \times m]` 31 | explicit : :obj:`bool` 32 | Operator contains a matrix that can be solved explicitly 33 | (``True``) or not (``False``) 34 | 35 | """ 36 | 37 | def __init__( 38 | self, 39 | Op: LinearOperator, 40 | max_neval: int = 10, 41 | ) -> None: 42 | super().__init__(Op=Op) 43 | 44 | self.max_neval = max_neval 45 | self.store: List[Tuple[NDArray, NDArray]] = [] # Store a list of (x, y) 46 | self.neval = 0 # Number of evaluations of the operator 47 | 48 | def _matvec(self, x: NDArray) -> NDArray: 49 | for xstored, ystored in self.store: 50 | if np.allclose(xstored, x): 51 | return ystored 52 | if len(self.store) + 1 > self.max_neval: 53 | del self.store[0] # Delete oldest 54 | y = self.Op._matvec(x) 55 | self.neval += 1 56 | self.store.append((x.copy(), y.copy())) 57 | return y 58 | 59 | def _rmatvec(self, y: NDArray) -> NDArray: 60 | for xstored, ystored in self.store: 61 | if np.allclose(ystored, y): 62 | return xstored 63 | if len(self.store) + 1 > self.max_neval: 64 | del self.store[0] # Delete oldest 65 | x = self.Op._rmatvec(y) 66 | self.neval += 1 67 | self.store.append((x.copy(), y.copy())) 68 | return x 69 | -------------------------------------------------------------------------------- /pylops/basicoperators/real.py: -------------------------------------------------------------------------------- 1 | __all__ = ["Real"] 2 | from typing import Union 3 | 4 | import numpy as np 5 | 6 | from pylops import LinearOperator 7 | from pylops.utils._internal import _value_or_sized_to_tuple 8 | from pylops.utils.typing import DTypeLike, InputDimsLike, NDArray 9 | 10 | 11 | class Real(LinearOperator): 12 | r"""Real operator. 13 | 14 | Return the real component of the input. The adjoint returns a complex 15 | number with the same real component as the input and zero imaginary 16 | component. 17 | 18 | Parameters 19 | ---------- 20 | dims : :obj:`int` or :obj:`tuple` 21 | Number of samples for each dimension 22 | dtype : :obj:`str`, optional 23 | Type of elements in input array. 24 | name : :obj:`str`, optional 25 | .. versionadded:: 2.0.0 26 | 27 | Name of operator (to be used by :func:`pylops.utils.describe.describe`) 28 | 29 | Attributes 30 | ---------- 31 | shape : :obj:`tuple` 32 | Operator shape 33 | explicit : :obj:`bool` 34 | Operator contains a matrix that can be solved explicitly (``True``) or 35 | not (``False``) 36 | 37 | Notes 38 | ----- 39 | In forward mode: 40 | 41 | .. math:: 42 | 43 | y_{i} = \Re\{x_{i}\} \quad \forall i=0,\ldots,N-1 44 | 45 | In adjoint mode: 46 | 47 | .. math:: 48 | 49 | x_{i} = \Re\{y_{i}\} + 0i \quad \forall i=0,\ldots,N-1 50 | 51 | """ 52 | 53 | def __init__( 54 | self, 55 | dims: Union[int, InputDimsLike], 56 | dtype: DTypeLike = "complex128", 57 | name: str = "R", 58 | ) -> None: 59 | dims = _value_or_sized_to_tuple(dims) 60 | super().__init__( 61 | dtype=np.dtype(dtype), dims=dims, dimsd=dims, clinear=False, name=name 62 | ) 63 | self.rdtype = np.real(np.ones(1, self.dtype)).dtype 64 | 65 | def _matvec(self, x: NDArray) -> NDArray: 66 | return x.real.astype(self.rdtype) 67 | 68 | def _rmatvec(self, x: NDArray) -> NDArray: 69 | return (x.real + 0j).astype(self.dtype) 70 | -------------------------------------------------------------------------------- /pylops/basicoperators/roll.py: -------------------------------------------------------------------------------- 1 | __all__ = ["Roll"] 2 | 3 | from typing import Union 4 | 5 | import numpy as np 6 | 7 | from pylops import LinearOperator 8 | from pylops.utils._internal import _value_or_sized_to_tuple 9 | from pylops.utils.backend import get_array_module 10 | from pylops.utils.decorators import reshaped 11 | from pylops.utils.typing import DTypeLike, InputDimsLike, NDArray 12 | 13 | 14 | class Roll(LinearOperator): 15 | r"""Roll along an axis. 16 | 17 | Roll a multi-dimensional array along ``axis`` for 18 | a chosen number of samples (``shift``). 19 | 20 | Parameters 21 | ---------- 22 | dims : :obj:`list` or :obj:`int` 23 | Number of samples for each dimension 24 | axis : :obj:`int`, optional 25 | .. versionadded:: 2.0.0 26 | 27 | Axis along which model is rolled. 28 | shift : :obj:`int`, optional 29 | Number of samples by which elements are shifted 30 | dtype : :obj:`str`, optional 31 | Type of elements in input array. 32 | name : :obj:`str`, optional 33 | .. versionadded:: 2.0.0 34 | 35 | Name of operator (to be used by :func:`pylops.utils.describe.describe`) 36 | 37 | Attributes 38 | ---------- 39 | shape : :obj:`tuple` 40 | Operator shape 41 | explicit : :obj:`bool` 42 | Operator contains a matrix that can be solved explicitly 43 | (``True``) or not (``False``) 44 | 45 | Notes 46 | ----- 47 | The Roll operator is a thin wrapper around :func:`numpy.roll` and shifts 48 | elements in a multi-dimensional array along a specified direction for a 49 | chosen number of samples. 50 | 51 | """ 52 | 53 | def __init__( 54 | self, 55 | dims: Union[int, InputDimsLike], 56 | axis: int = -1, 57 | shift: int = 1, 58 | dtype: DTypeLike = "float64", 59 | name: str = "R", 60 | ) -> None: 61 | dims = _value_or_sized_to_tuple(dims) 62 | super().__init__(dtype=np.dtype(dtype), dims=dims, dimsd=dims, name=name) 63 | self.axis = axis 64 | self.shift = shift 65 | 66 | @reshaped(swapaxis=True) 67 | def _matvec(self, x: NDArray) -> NDArray: 68 | ncp = get_array_module(x) 69 | return ncp.roll(x, shift=self.shift, axis=-1) 70 | 71 | @reshaped(swapaxis=True) 72 | def _rmatvec(self, x: NDArray) -> NDArray: 73 | ncp = get_array_module(x) 74 | return ncp.roll(x, shift=-self.shift, axis=-1) 75 | -------------------------------------------------------------------------------- /pylops/basicoperators/smoothing1d.py: -------------------------------------------------------------------------------- 1 | __all__ = ["Smoothing1D"] 2 | 3 | from typing import Union 4 | 5 | import numpy as np 6 | 7 | from pylops.signalprocessing import Convolve1D 8 | from pylops.utils.typing import DTypeLike, InputDimsLike 9 | 10 | 11 | class Smoothing1D(Convolve1D): 12 | r"""1D Smoothing. 13 | 14 | Apply smoothing to model (and data) to a multi-dimensional array 15 | along ``axis``. 16 | 17 | Parameters 18 | ---------- 19 | nsmooth : :obj:`int` 20 | Length of smoothing operator (must be odd) 21 | dims : :obj:`tuple` or :obj:`int` 22 | Number of samples for each dimension 23 | axis : :obj:`int`, optional 24 | .. versionadded:: 2.0.0 25 | 26 | Axis along which model (and data) are smoothed. 27 | dtype : :obj:`str`, optional 28 | Type of elements in input array. 29 | 30 | Attributes 31 | ---------- 32 | shape : :obj:`tuple` 33 | Operator shape 34 | explicit : :obj:`bool` 35 | Operator contains a matrix that can be solved explicitly (``True``) or 36 | not (``False``) 37 | 38 | Notes 39 | ----- 40 | The Smoothing1D operator is a special type of convolutional operator that 41 | convolves the input model (or data) with a constant filter of size 42 | :math:`n_\text{smooth}`: 43 | 44 | .. math:: 45 | \mathbf{f} = [ 1/n_\text{smooth}, 1/n_\text{smooth}, ..., 1/n_\text{smooth} ] 46 | 47 | When applied to the first direction: 48 | 49 | .. math:: 50 | y[i,j,k] = 1/n_\text{smooth} \sum_{l=-(n_\text{smooth}-1)/2}^{(n_\text{smooth}-1)/2} 51 | x[l,j,k] 52 | 53 | Similarly when applied to the second direction: 54 | 55 | .. math:: 56 | y[i,j,k] = 1/n_\text{smooth} \sum_{l=-(n_\text{smooth}-1)/2}^{(n_\text{smooth}-1)/2} 57 | x[i,l,k] 58 | 59 | and the third direction: 60 | 61 | .. math:: 62 | y[i,j,k] = 1/n_\text{smooth} \sum_{l=-(n_\text{smooth}-1)/2}^{(n_\text{smooth}-1)/2} 63 | x[i,j,l] 64 | 65 | Note that since the filter is symmetrical, the *Smoothing1D* operator is 66 | self-adjoint. 67 | 68 | """ 69 | 70 | def __init__(self, nsmooth: int, dims: Union[int, InputDimsLike], axis: int = -1, 71 | dtype: DTypeLike = "float64", name: str = 'S'): 72 | if nsmooth % 2 == 0: 73 | nsmooth += 1 74 | h = np.ones(nsmooth) / float(nsmooth) 75 | offset = (nsmooth - 1) // 2 76 | super().__init__(dims=dims, h=h, axis=axis, offset=offset, dtype=dtype, name=name) 77 | -------------------------------------------------------------------------------- /pylops/basicoperators/smoothing2d.py: -------------------------------------------------------------------------------- 1 | __all__ = ["Smoothing2D"] 2 | 3 | from typing import Union 4 | 5 | import numpy as np 6 | 7 | from pylops.signalprocessing import Convolve2D 8 | from pylops.utils.typing import DTypeLike, InputDimsLike 9 | 10 | 11 | class Smoothing2D(Convolve2D): 12 | r"""2D Smoothing. 13 | 14 | Apply smoothing to model (and data) along two ``axes`` of a 15 | multi-dimensional array. 16 | 17 | Parameters 18 | ---------- 19 | nsmooth : :obj:`tuple` or :obj:`list` 20 | Length of smoothing operator in 1st and 2nd dimensions (must be odd) 21 | dims : :obj:`tuple` 22 | Number of samples for each dimension 23 | axes : :obj:`int`, optional 24 | .. versionadded:: 2.0.0 25 | 26 | Axes along which model (and data) are smoothed. 27 | dtype : :obj:`str`, optional 28 | Type of elements in input array. 29 | 30 | Attributes 31 | ---------- 32 | shape : :obj:`tuple` 33 | Operator shape 34 | explicit : :obj:`bool` 35 | Operator contains a matrix that can be solved explicitly (``True``) or 36 | not (``False``) 37 | 38 | See Also 39 | -------- 40 | pylops.signalprocessing.Convolve2D : 2D convolution 41 | 42 | Notes 43 | ----- 44 | The 2D Smoothing operator is a special type of convolutional operator that 45 | convolves the input model (or data) with a constant 2d filter of size 46 | :math:`n_{\text{smooth}, 1} \times n_{\text{smooth}, 2}`: 47 | 48 | Its application to a two dimensional input signal is: 49 | 50 | .. math:: 51 | y[i,j] = 1/(n_{\text{smooth}, 1}\cdot n_{\text{smooth}, 2}) 52 | \sum_{l=-(n_{\text{smooth},1}-1)/2}^{(n_{\text{smooth},1}-1)/2} 53 | \sum_{m=-(n_{\text{smooth},2}-1)/2}^{(n_{\text{smooth},2}-1)/2} x[l,m] 54 | 55 | Note that since the filter is symmetrical, the *Smoothing2D* operator is 56 | self-adjoint. 57 | 58 | """ 59 | 60 | def __init__(self, nsmooth: InputDimsLike, 61 | dims: Union[int, InputDimsLike], 62 | axes: InputDimsLike = (-2, -1), 63 | dtype: DTypeLike = "float64", name: str = 'S'): 64 | nsmooth = list(nsmooth) 65 | if nsmooth[0] % 2 == 0: 66 | nsmooth[0] += 1 67 | if nsmooth[1] % 2 == 0: 68 | nsmooth[1] += 1 69 | h = np.ones((nsmooth[0], nsmooth[1])) / float(nsmooth[0] * nsmooth[1]) 70 | offset = [(nsmooth[0] - 1) // 2, (nsmooth[1] - 1) // 2] 71 | super().__init__(dims=dims, h=h, offset=offset, axes=axes, dtype=dtype, name=name) 72 | -------------------------------------------------------------------------------- /pylops/basicoperators/tocupy.py: -------------------------------------------------------------------------------- 1 | __all__ = ["ToCupy"] 2 | 3 | from typing import Union 4 | 5 | import numpy as np 6 | 7 | from pylops import LinearOperator 8 | from pylops.utils._internal import _value_or_sized_to_tuple 9 | from pylops.utils.backend import to_cupy, to_numpy 10 | from pylops.utils.typing import DTypeLike, InputDimsLike, NDArray 11 | 12 | 13 | class ToCupy(LinearOperator): 14 | r"""Convert to CuPy. 15 | 16 | Convert an input NumPy array to CuPy in forward mode, 17 | and convert back to NumPy in adjoint mode. 18 | 19 | Parameters 20 | ---------- 21 | dims : :obj:`list` or :obj:`int` 22 | Number of samples for each dimension 23 | dtype : :obj:`str`, optional 24 | Type of elements in input array. 25 | name : :obj:`str`, optional 26 | Name of operator (to be used by :func:`pylops.utils.describe.describe`) 27 | 28 | Attributes 29 | ---------- 30 | shape : :obj:`tuple` 31 | Operator shape 32 | explicit : :obj:`bool` 33 | Operator contains a matrix that can be solved explicitly 34 | (``True``) or not (``False``) 35 | 36 | Notes 37 | ----- 38 | The ToCupy operator is a special operator that does not perform 39 | any transformation on the input arrays other than converting 40 | them from NumPy to CuPy. This operator can be used when one 41 | is interested to create a chain of operators where only one 42 | (or some of them) act on CuPy arrays, whilst other operate 43 | on NumPy arrays. 44 | 45 | """ 46 | 47 | def __init__( 48 | self, 49 | dims: Union[int, InputDimsLike], 50 | dtype: DTypeLike = "float64", 51 | name: str = "C", 52 | ) -> None: 53 | dims = _value_or_sized_to_tuple(dims) 54 | super().__init__(dtype=np.dtype(dtype), dims=dims, dimsd=dims, name=name) 55 | 56 | def _matvec(self, x: NDArray) -> NDArray: 57 | return to_cupy(x) 58 | 59 | def _rmatvec(self, x: NDArray) -> NDArray: 60 | return to_numpy(x) 61 | -------------------------------------------------------------------------------- /pylops/basicoperators/transpose.py: -------------------------------------------------------------------------------- 1 | __all__ = ["Transpose"] 2 | 3 | import numpy as np 4 | 5 | from pylops import LinearOperator 6 | from pylops.utils._internal import _value_or_sized_to_tuple 7 | from pylops.utils.backend import get_normalize_axis_index 8 | from pylops.utils.decorators import reshaped 9 | from pylops.utils.typing import DTypeLike, InputDimsLike, NDArray 10 | 11 | 12 | class Transpose(LinearOperator): 13 | r"""Transpose operator. 14 | 15 | Transpose axes of a multi-dimensional array. This operator works with 16 | flattened input model (or data), which are however multi-dimensional in 17 | nature and will be reshaped and treated as such in both forward and adjoint 18 | modes. 19 | 20 | Parameters 21 | ---------- 22 | dims : :obj:`tuple`, optional 23 | Number of samples for each dimension 24 | axes : :obj:`tuple`, optional 25 | Direction along which transposition is applied 26 | dtype : :obj:`str`, optional 27 | Type of elements in input array 28 | name : :obj:`str`, optional 29 | .. versionadded:: 2.0.0 30 | 31 | Name of operator (to be used by :func:`pylops.utils.describe.describe`) 32 | 33 | Attributes 34 | ---------- 35 | shape : :obj:`tuple` 36 | Operator shape 37 | explicit : :obj:`bool` 38 | Operator contains a matrix that can be solved explicitly 39 | (``True``) or not (``False``) 40 | 41 | Raises 42 | ------ 43 | ValueError 44 | If ``axes`` contains repeated dimensions (or a dimension is missing) 45 | 46 | Notes 47 | ----- 48 | The Transpose operator reshapes the input model into a multi-dimensional 49 | array of size ``dims`` and transposes (or swaps) its axes as defined 50 | in ``axes``. 51 | 52 | Similarly, in adjoint mode the data is reshaped into a multi-dimensional 53 | array whose size is a permuted version of ``dims`` defined by ``axes``. 54 | The array is then rearragned into the original model dimensions ``dims``. 55 | 56 | """ 57 | 58 | def __init__( 59 | self, 60 | dims: InputDimsLike, 61 | axes: InputDimsLike, 62 | dtype: DTypeLike = "float64", 63 | name: str = "T", 64 | ) -> None: 65 | dims = _value_or_sized_to_tuple(dims) 66 | ndims = len(dims) 67 | self.axes = [get_normalize_axis_index()(ax, ndims) for ax in axes] 68 | 69 | # find out if all axes are present only once in axes 70 | if len(np.unique(self.axes)) != ndims: 71 | raise ValueError("axes must contain each direction once") 72 | 73 | # find out how axes should be transposed in adjoint mode 74 | axesd = np.empty(ndims, dtype=int) 75 | axesd[self.axes] = np.arange(ndims, dtype=int) 76 | 77 | dimsd = np.empty(ndims, dtype=int) 78 | dimsd[axesd] = dims 79 | self.axesd = list(axesd) 80 | 81 | super().__init__(dtype=np.dtype(dtype), dims=dims, dimsd=dimsd, name=name) 82 | 83 | @reshaped 84 | def _matvec(self, x: NDArray) -> NDArray: 85 | return x.transpose(self.axes) 86 | 87 | @reshaped 88 | def _rmatvec(self, x: NDArray) -> NDArray: 89 | return x.transpose(self.axesd) 90 | -------------------------------------------------------------------------------- /pylops/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration 3 | ============= 4 | 5 | The configuration module controls module-level behavior in PyLops. 6 | 7 | You can either set behavior globally with getter/setter: 8 | 9 | get_ndarray_multiplication Check the status of ndarray multiplication (True/False). 10 | set_ndarray_multiplication Enable/disable ndarray multiplication. 11 | 12 | or use context managers (with blocks): 13 | 14 | enabled_ndarray_multiplication Enable ndarray multiplication within context. 15 | disabled_ndarray_multiplication Disable ndarray multiplication within context. 16 | 17 | """ 18 | from contextlib import contextmanager 19 | from dataclasses import dataclass 20 | from typing import Generator 21 | 22 | __all__ = [ 23 | "get_ndarray_multiplication", 24 | "set_ndarray_multiplication", 25 | "enabled_ndarray_multiplication", 26 | "disabled_ndarray_multiplication", 27 | ] 28 | 29 | 30 | @dataclass 31 | class Config: 32 | ndarray_multiplication: bool = True 33 | 34 | 35 | _config = Config() 36 | 37 | 38 | def get_ndarray_multiplication() -> bool: 39 | return _config.ndarray_multiplication 40 | 41 | 42 | def set_ndarray_multiplication(val: bool) -> None: 43 | _config.ndarray_multiplication = val 44 | 45 | 46 | @contextmanager 47 | def enabled_ndarray_multiplication() -> Generator: 48 | enabled = get_ndarray_multiplication() 49 | set_ndarray_multiplication(True) 50 | try: 51 | yield enabled 52 | finally: 53 | set_ndarray_multiplication(enabled) 54 | 55 | 56 | @contextmanager 57 | def disabled_ndarray_multiplication() -> Generator: 58 | enabled = get_ndarray_multiplication() 59 | set_ndarray_multiplication(False) 60 | try: 61 | yield enabled 62 | finally: 63 | set_ndarray_multiplication(enabled) 64 | -------------------------------------------------------------------------------- /pylops/jaxoperator.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "JaxOperator", 3 | ] 4 | 5 | from typing import Any, NewType 6 | 7 | from pylops import LinearOperator 8 | from pylops.utils import deps 9 | 10 | if deps.jax_enabled: 11 | import jax 12 | 13 | jaxarrayin_type = jax.typing.ArrayLike 14 | jaxarrayout_type = jax.Array 15 | else: 16 | jax_message = ( 17 | "JAX package not installed. In order to be able to use" 18 | 'the jaxoperator module run "pip install jax" or' 19 | '"conda install -c conda-forge jax".' 20 | ) 21 | jaxarrayin_type = Any 22 | jaxarrayout_type = Any 23 | 24 | JaxTypeIn = NewType("JaxTypeIn", jaxarrayin_type) 25 | JaxTypeOut = NewType("JaxTypeOut", jaxarrayout_type) 26 | 27 | 28 | class JaxOperator(LinearOperator): 29 | """Enable JAX backend for PyLops operator. 30 | 31 | This class can be used to wrap a pylops operator to enable the JAX 32 | backend. Doing so, users can run all of the methods of a pylops 33 | operator with JAX arrays. Moreover, the forward and adjoint 34 | are internally just-in-time compiled, and other JAX functionalities 35 | such as automatic differentiation and automatic vectorization 36 | are enabled. 37 | 38 | Parameters 39 | ---------- 40 | Op : :obj:`pylops.LinearOperator` 41 | PyLops operator 42 | 43 | """ 44 | 45 | def __init__(self, Op: LinearOperator) -> None: 46 | if not deps.jax_enabled: 47 | raise NotImplementedError(jax_message) 48 | super().__init__( 49 | dtype=Op.dtype, 50 | dims=Op.dims, 51 | dimsd=Op.dimsd, 52 | clinear=Op.clinear, 53 | explicit=False, 54 | forceflat=Op.forceflat, 55 | name=Op.name, 56 | ) 57 | self._matvec = jax.jit(Op._matvec) 58 | self._rmatvec = jax.jit(Op._rmatvec) 59 | 60 | def __call__(self, x, *args, **kwargs): 61 | return self._matvec(x) 62 | 63 | def _rmatvecad(self, x: JaxTypeIn, y: JaxTypeIn) -> JaxTypeOut: 64 | _, f_vjp = jax.vjp(self._matvec, x) 65 | xadj = jax.jit(f_vjp)(y)[0] 66 | return xadj 67 | 68 | def rmatvecad(self, x: JaxTypeIn, y: JaxTypeIn) -> JaxTypeOut: 69 | """Vector-Jacobian product 70 | 71 | JIT-compiled Vector-Jacobian product 72 | 73 | Parameters 74 | ---------- 75 | x : :obj:`jax.Array` 76 | Input array for forward 77 | y : :obj:`jax.Array` 78 | Input array for adjoint 79 | 80 | Returns 81 | ------- 82 | xadj : :obj:`jax.typing.ArrayLike` 83 | Output array 84 | 85 | """ 86 | M, N = self.shape 87 | 88 | if x.shape != (M,) and x.shape != (M, 1): 89 | raise ValueError( 90 | f"Dimension mismatch. Got {x.shape}, but expected ({M},) or ({M}, 1)." 91 | ) 92 | 93 | y = self._rmatvecad(x, y) 94 | 95 | if x.ndim == 1: 96 | y = y.reshape(N) 97 | elif x.ndim == 2: 98 | y = y.reshape(N, 1) 99 | else: 100 | raise ValueError( 101 | f"Invalid shape returned by user-defined rmatvecad(). " 102 | f"Expected 2-d ndarray or matrix, not {x.ndim}-d ndarray" 103 | ) 104 | return y 105 | -------------------------------------------------------------------------------- /pylops/optimization/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Optimization 3 | ============ 4 | 5 | The subpackage optimization provides an extensive set of solvers to be 6 | used with PyLops linear operators. 7 | 8 | A list of least-squares solvers in pylops.optimization.solver: 9 | 10 | cg Conjugate gradient. 11 | cgls Conjugate gradient least-squares. 12 | lsqr LSQR. 13 | 14 | and wrappers for regularized or preconditioned inversion in pylops.optimization.leastsquares: 15 | 16 | normal_equations_inversion Inversion of normal equations. 17 | regularized_inversion Regularized inversion. 18 | preconditioned_inversion Preconditioned inversion. 19 | 20 | and sparsity-promoting solvers in pylops.optimization.sparsity: 21 | 22 | irls Iteratively reweighted least squares. 23 | omp Orthogonal Matching Pursuit (OMP). 24 | ista Iterative Soft Thresholding Algorithm. 25 | fista Fast Iterative Soft Thresholding Algorithm. 26 | spgl1 Spectral Projected-Gradient for L1 norm. 27 | splitbregman Split Bregman for mixed L2-L1 norms. 28 | 29 | Note that these solvers are thin wrappers over class-based solvers (new in v2), which can be accessed from 30 | submodules with equivalent name and suffix c. 31 | 32 | """ 33 | -------------------------------------------------------------------------------- /pylops/optimization/eigs.py: -------------------------------------------------------------------------------- 1 | __all__ = ["power_iteration"] 2 | 3 | from typing import Tuple 4 | 5 | import numpy as np 6 | 7 | from pylops import LinearOperator 8 | from pylops.utils.backend import get_module 9 | from pylops.utils.typing import NDArray 10 | 11 | 12 | def power_iteration( 13 | Op: LinearOperator, 14 | niter: int = 10, 15 | tol: float = 1e-5, 16 | dtype: str = "float32", 17 | backend: str = "numpy", 18 | ) -> Tuple[float, NDArray, int]: 19 | """Power iteration algorithm. 20 | 21 | Power iteration algorithm, used to compute the largest eigenvector and 22 | corresponding eigenvalue. Note that for complex numbers, the eigenvalue 23 | with largest module is found. 24 | 25 | This implementation closely follow that of 26 | https://en.wikipedia.org/wiki/Power_iteration. 27 | 28 | Parameters 29 | ---------- 30 | Op : :obj:`pylops.LinearOperator` 31 | Square operator 32 | niter : :obj:`int`, optional 33 | Number of iterations 34 | tol : :obj:`float`, optional 35 | Update tolerance 36 | dtype : :obj:`str`, optional 37 | Type of elements in input array. 38 | backend : :obj:`str`, optional 39 | Backend to use (`numpy` or `cupy`) 40 | 41 | Returns 42 | ------- 43 | maxeig : :obj:`float` 44 | Largest eigenvalue 45 | b_k : :obj:`np.ndarray` or :obj:`cp.ndarray` 46 | Largest eigenvector 47 | iiter : :obj:`int` 48 | Effective number of iterations 49 | 50 | """ 51 | ncp = get_module(backend) 52 | 53 | # Identify if operator is complex 54 | if np.issubdtype(dtype, np.complexfloating): 55 | cmpx = 1j 56 | else: 57 | cmpx = 0 58 | 59 | # Choose a random vector to decrease the chance that vector 60 | # is orthogonal to the eigenvector 61 | b_k = ncp.random.rand(Op.shape[1]).astype(dtype) + cmpx * ncp.random.rand( 62 | Op.shape[1] 63 | ).astype(dtype) 64 | b_k = b_k / ncp.linalg.norm(b_k) 65 | 66 | niter = 10 if niter is None else niter 67 | maxeig_old = 0.0 68 | for iiter in range(niter): 69 | # compute largest eigenvector 70 | b1_k = Op.matvec(b_k) 71 | 72 | # compute largest eigevalue 73 | maxeig = ncp.vdot(b_k, b1_k) 74 | 75 | # renormalize the vector 76 | b_k = b1_k / ncp.linalg.norm(b1_k) 77 | 78 | if ncp.abs(maxeig - maxeig_old) < tol * maxeig_old: 79 | break 80 | maxeig_old = maxeig 81 | 82 | return maxeig, b_k, iiter + 1 83 | -------------------------------------------------------------------------------- /pylops/pytensoroperator.py: -------------------------------------------------------------------------------- 1 | import pylops 2 | from pylops.utils import deps 3 | 4 | pytensor_message = deps.pytensor_import("the pytensor module") 5 | 6 | if pytensor_message is not None: 7 | 8 | class PyTensorOperator: 9 | """PyTensor Op which applies a PyLops Linear Operator, including gradient support. 10 | 11 | This class "converts" a PyLops `LinearOperator` class into a PyTensor `Op`. 12 | This applies the `LinearOperator` in "forward-mode" in `self.perform`, and applies 13 | its adjoint when computing the vector-Jacobian product (`self.grad`), as that is 14 | the analytically correct gradient for linear operators. This class should pass 15 | `pytensor.gradient.verify_grad`. 16 | 17 | Parameters 18 | ---------- 19 | LOp : pylops.LinearOperator 20 | """ 21 | 22 | def __init__(self, LOp: pylops.LinearOperator) -> None: 23 | if not deps.pytensor_enabled: 24 | raise NotImplementedError(pytensor_message) 25 | 26 | else: 27 | import pytensor.tensor as pt 28 | from pytensor.graph.basic import Apply 29 | from pytensor.graph.op import Op 30 | 31 | class _PyTensorOperatorNoGrad(Op): 32 | """PyTensor Op which applies a PyLops Linear Operator, excluding gradient support. 33 | 34 | This class "converts" a PyLops `LinearOperator` class into a PyTensor `Op`. 35 | This applies the `LinearOperator` in "forward-mode" in `self.perform`. 36 | 37 | Parameters 38 | ---------- 39 | LOp : pylops.LinearOperator 40 | """ 41 | 42 | __props__ = ("dims", "dimsd", "shape") 43 | 44 | def __init__(self, LOp: pylops.LinearOperator) -> None: 45 | self._LOp = LOp 46 | self.dims = self._LOp.dims 47 | self.dimsd = self._LOp.dimsd 48 | self.shape = self._LOp.shape 49 | super().__init__() 50 | 51 | def make_node(self, x) -> Apply: 52 | x = pt.as_tensor_variable(x) 53 | inputs = [x] 54 | outputs = [pt.tensor(dtype=x.type.dtype, shape=self._LOp.dimsd)] 55 | return Apply(self, inputs, outputs) 56 | 57 | def perform( 58 | self, node: Apply, inputs: list, output_storage: list[list[None]] 59 | ) -> None: 60 | (x,) = inputs 61 | (yt,) = output_storage 62 | yt[0] = self._LOp @ x 63 | 64 | class PyTensorOperator(_PyTensorOperatorNoGrad): 65 | """PyTensor Op which applies a PyLops Linear Operator, including gradient support. 66 | 67 | This class "converts" a PyLops `LinearOperator` class into a PyTensor `Op`. 68 | This applies the `LinearOperator` in "forward-mode" in `self.perform`, and applies 69 | its adjoint when computing the vector-Jacobian product (`self.grad`), as that is 70 | the analytically correct gradient for linear operators. This class should pass 71 | `pytensor.gradient.verify_grad`. 72 | 73 | Parameters 74 | ---------- 75 | LOp : pylops.LinearOperator 76 | """ 77 | 78 | def __init__(self, LOp: pylops.LinearOperator) -> None: 79 | super().__init__(LOp) 80 | self._gradient_op = _PyTensorOperatorNoGrad(self._LOp.H) 81 | 82 | def grad( 83 | self, inputs: list[pt.TensorVariable], output_grads: list[pt.TensorVariable] 84 | ): 85 | return [self._gradient_op(output_grads[0])] 86 | -------------------------------------------------------------------------------- /pylops/signalprocessing/_chirpradon2d.py: -------------------------------------------------------------------------------- 1 | from pylops.utils.backend import get_array_module 2 | from pylops.utils.typing import NDArray 3 | 4 | 5 | def _chirp_radon_2d( 6 | data: NDArray, dt: float, dx: float, pmax: float, mode: str = "f" 7 | ) -> NDArray: 8 | r"""2D Chirp Radon transform 9 | 10 | Applies 2D Radon transform using Fast Fourier Transform and Chirp 11 | functions. (mode='f': forward, 'a': adjoint, and 'i': inverse). See 12 | Chirp2DRadon operator docstring for more details. 13 | 14 | Parameters 15 | ---------- 16 | data : :obj:`np.ndarray` 17 | 2D input data of size :math:`[n_x \times n_t]` 18 | dt : :obj:`float` 19 | Time sampling :math:`dt` 20 | dx : :obj:`float` 21 | Spatial sampling in :math:`x` direction :math:`dx` 22 | pmax : :obj:`float` 23 | Maximum slope defined as :math:`\tan` of maximum stacking angle 24 | :math:`x` direction :math:`p_{max} = \tan(\alpha_x_{max})`. 25 | If one operates in terms of minimum velocity :math:`c_0`, then 26 | :math:`p_x_{max}=c_0 dy/dt`. 27 | mode : :obj:`str`, optional 28 | Mode of operation, 'f': forward, 'a': adjoint, and 'i': inverse 29 | 30 | Returns 31 | ------- 32 | g : :obj:`np.ndarray` 33 | 2D output of size :math:`[\times n_{x} \times n_t]` 34 | 35 | """ 36 | ncp = get_array_module(data) 37 | 38 | # define sign for mode 39 | sign = -1.0 if mode == "f" else 1.0 40 | 41 | # data size 42 | (nx, nt) = data.shape 43 | 44 | # find dtype of input 45 | dtype = ncp.real(data).dtype 46 | cdtype = (ncp.ones(1, dtype=dtype) + 1j * ncp.ones(1, dtype=dtype)).dtype 47 | 48 | # frequency axis 49 | omega = (ncp.fft.fftfreq(nt, 1 / nt) / (nt * dt)).reshape((1, nt)).astype(dtype) 50 | 51 | # slowness sampling 52 | dp = 2 * dt * pmax / dx / nx 53 | 54 | # spatial axis 55 | x = (ncp.fft.fftfreq(2 * nx, 1 / (2 * nx)) ** 2).reshape((2 * nx, 1)).astype(dtype) 56 | 57 | # K coefficients 58 | K0 = ncp.exp(sign * ncp.pi * 1j * dp * dx * omega * x).reshape((2 * nx, nt)) 59 | 60 | # K conj coefficients 61 | K = ncp.conj(ncp.fft.fftshift(K0, axes=(0,)))[nx // 2 : 3 * nx // 2, :] 62 | 63 | # perform transform 64 | h = ncp.zeros((2 * nx, nt)).astype(cdtype) 65 | h[0:nx, :] = ncp.fft.fftn(data, axes=(1,)) * K 66 | g = ncp.fft.ifftn( 67 | ncp.fft.fftn(h, axes=(0,)) * ncp.fft.fftn(K0, axes=(0,)), axes=(0,) 68 | ) 69 | if mode == "i": 70 | g = ncp.fft.ifftn(g[0:nx, :] * K * abs(omega), axes=(1,)).real * dp * dx 71 | else: 72 | g = ncp.fft.ifftn(g[0:nx, :] * K, axes=(1,)).real 73 | return g 74 | -------------------------------------------------------------------------------- /pylops/signalprocessing/_fourierradon2d_cuda.py: -------------------------------------------------------------------------------- 1 | from math import pi 2 | 3 | import cupy as cp 4 | from numba import cuda 5 | 6 | TWO_PI_MINUS = cp.float32(-2.0 * pi) 7 | TWO_PI_PLUS = cp.float32(2.0 * pi) 8 | IMG = cp.complex64(1j) 9 | 10 | 11 | @cuda.jit 12 | def _radon_inner_2d_kernel(x, y, f, px, h, flim0, flim1, npx, nh): 13 | """Cuda kernels for FourierRadon2D operator 14 | 15 | Cuda implementation of the on-the-fly kernel creation and application for the 16 | FourierRadon2D operator. See :class:`pylops.signalprocessing.FourierRadon2D` 17 | for details about input parameters. 18 | 19 | """ 20 | ih, ifr = cuda.grid(2) 21 | if ih < nh and ifr >= flim0 and ifr <= flim1: 22 | for ipx in range(npx): 23 | # slow computation of exp(1j * x) 24 | # y[ih, ifr] += x[ipx, ifr] * exp(TWO_PI_MINUS * f[ifr] * px[ipx] * h[ih]) 25 | # fast computation of exp(1j * x) - see https://stackoverflow.com/questions/9860711/cucomplex-h-and-exp/9863048#9863048 26 | s, c = cuda.libdevice.sincosf(TWO_PI_MINUS * f[ifr] * px[ipx] * h[ih]) 27 | y[ih, ifr] += x[ipx, ifr] * (c + IMG * s) 28 | 29 | 30 | @cuda.jit 31 | def _aradon_inner_2d_kernel(x, y, f, px, h, flim0, flim1, npx, nh): 32 | """Cuda kernels for FourierRadon2D operator 33 | 34 | Cuda implementation of the on-the-fly kernel creation and application for the 35 | FourierRadon2D operator. See :class:`pylops.signalprocessing.FourierRadon2D` 36 | for details about input parameters. 37 | 38 | """ 39 | ipx, ifr = cuda.grid(2) 40 | if ipx < npx and ifr >= flim0 and ifr <= flim1: 41 | for ih in range(nh): 42 | # slow computation of exp(1j * x) 43 | # x[ipx, ifr] += y[ih, ifr] * exp(TWO_PI_I_PLUS * f[ifr] * px[ipx] * h[ih]) 44 | # fast computation of exp(1j * x) - see https://stackoverflow.com/questions/9860711/cucomplex-h-and-exp/9863048#9863048 45 | s, c = cuda.libdevice.sincosf(TWO_PI_PLUS * f[ifr] * px[ipx] * h[ih]) 46 | x[ipx, ifr] += y[ih, ifr] * (c + IMG * s) 47 | 48 | 49 | def _radon_inner_2d_cuda( 50 | x, 51 | y, 52 | f, 53 | px, 54 | h, 55 | flim0, 56 | flim1, 57 | npx, 58 | nh, 59 | num_blocks=(32, 32), 60 | num_threads_per_blocks=(32, 32), 61 | ): 62 | """Caller for FourierRadon2D operator 63 | 64 | Caller for cuda implementation of matvec kernel for FourierRadon2D operator. 65 | See :class:`pylops.signalprocessing.FourierRadon2D` for details about 66 | input parameters. 67 | 68 | """ 69 | _radon_inner_2d_kernel[num_blocks, num_threads_per_blocks]( 70 | x, y, f, px, h, flim0, flim1, npx, nh 71 | ) 72 | return y 73 | 74 | 75 | def _aradon_inner_2d_cuda( 76 | x, 77 | y, 78 | f, 79 | px, 80 | h, 81 | flim0, 82 | flim1, 83 | npx, 84 | nh, 85 | num_blocks=(32, 32), 86 | num_threads_per_blocks=(32, 32), 87 | ): 88 | """Caller for FourierRadon2D operator 89 | 90 | Caller for cuda implementation of rmatvec kernel for FourierRadon2D operator. 91 | See :class:`pylops.signalprocessing.FourierRadon2D` for details about 92 | input parameters. 93 | 94 | """ 95 | _aradon_inner_2d_kernel[num_blocks, num_threads_per_blocks]( 96 | x, y, f, px, h, flim0, flim1, npx, nh 97 | ) 98 | return x 99 | -------------------------------------------------------------------------------- /pylops/signalprocessing/_fourierradon2d_numba.py: -------------------------------------------------------------------------------- 1 | import os 2 | from cmath import exp 3 | from math import pi 4 | 5 | from numba import jit, prange 6 | 7 | # detect whether to use parallel or not 8 | numba_threads = int(os.getenv("NUMBA_NUM_THREADS", "1")) 9 | parallel = True if numba_threads != 1 else False 10 | 11 | 12 | @jit(nopython=True, parallel=parallel, nogil=True, cache=True, fastmath=True) 13 | def _radon_inner_2d(X, Y, f, px, h, flim0, flim1, npx, nh): 14 | for ih in prange(nh): 15 | for ifr in range(flim0, flim1): 16 | for ipx in range(npx): 17 | Y[ih, ifr] += X[ipx, ifr] * exp(-1j * 2 * pi * f[ifr] * px[ipx] * h[ih]) 18 | 19 | 20 | @jit(nopython=True, parallel=parallel, nogil=True, cache=True, fastmath=True) 21 | def _aradon_inner_2d(X, Y, f, px, h, flim0, flim1, npx, nh): 22 | for ipx in prange(npx): 23 | for ifr in range(flim0, flim1): 24 | for ih in range(nh): 25 | X[ipx, ifr] += Y[ih, ifr] * exp(1j * 2 * pi * f[ifr] * px[ipx] * h[ih]) 26 | -------------------------------------------------------------------------------- /pylops/signalprocessing/_fourierradon3d_numba.py: -------------------------------------------------------------------------------- 1 | import os 2 | from cmath import exp 3 | from math import pi 4 | 5 | from numba import jit, prange 6 | 7 | # detect whether to use parallel or not 8 | numba_threads = int(os.getenv("NUMBA_NUM_THREADS", "1")) 9 | parallel = True if numba_threads != 1 else False 10 | 11 | 12 | @jit(nopython=True, parallel=parallel, nogil=True, cache=True, fastmath=True) 13 | def _radon_inner_3d(X, Y, f, py, px, hy, hx, flim0, flim1, npy, npx, nhy, nhx): 14 | for ihy in prange(nhy): 15 | for ihx in prange(nhx): 16 | for ifr in range(flim0, flim1): 17 | for ipy in range(npy): 18 | for ipx in range(npx): 19 | Y[ihy, ihx, ifr] += X[ipy, ipx, ifr] * exp( 20 | -1j 21 | * 2 22 | * pi 23 | * f[ifr] 24 | * (py[ipy] * hy[ihy] + px[ipx] * hx[ihx]) 25 | ) 26 | 27 | 28 | @jit(nopython=True, parallel=parallel, nogil=True, cache=True, fastmath=True) 29 | def _aradon_inner_3d(X, Y, f, py, px, hy, hx, flim0, flim1, npy, npx, nhy, nhx): 30 | for ipy in prange(npy): 31 | for ipx in range(npx): 32 | for ifr in range(flim0, flim1): 33 | for ihy in range(nhy): 34 | for ihx in range(nhx): 35 | X[ipy, ipx, ifr] += Y[ihy, ihx, ifr] * exp( 36 | 1j 37 | * 2 38 | * pi 39 | * f[ifr] 40 | * (py[ipy] * hy[ihy] + px[ipx] * hx[ihx]) 41 | ) 42 | -------------------------------------------------------------------------------- /pylops/signalprocessing/_radon2d_numba.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | from numba import jit 5 | 6 | # detect whether to use parallel or not 7 | numba_threads = int(os.getenv("NUMBA_NUM_THREADS", "1")) 8 | parallel = True if numba_threads != 1 else False 9 | 10 | 11 | @jit(nopython=True) 12 | def _linear_numba(x, t, px): 13 | return t + px * x 14 | 15 | 16 | @jit(nopython=True) 17 | def _parabolic_numba(x, t, px): 18 | return t + px * x**2 19 | 20 | 21 | @jit(nopython=True) 22 | def _hyperbolic_numba(x, t, px): 23 | return np.sqrt(t**2 + (x / px) ** 2) 24 | 25 | 26 | @jit(nopython=True, nogil=True) 27 | def _indices_2d_numba(f, x, px, t, nt, interp=True): 28 | """Compute time and space indices of parametric line in ``f`` function 29 | using numba. Refer to ``_indices_2d`` for full documentation. 30 | 31 | """ 32 | tdecscan = f(x, t, px) 33 | if not interp: 34 | xscan = (tdecscan >= 0) & (tdecscan < nt) 35 | else: 36 | xscan = (tdecscan >= 0) & (tdecscan < nt - 1) 37 | tscanfs = tdecscan[xscan] 38 | tscan = np.zeros(len(tscanfs)) 39 | dtscan = np.zeros(len(tscanfs)) 40 | for it, tscanf in enumerate(tscanfs): 41 | tscan[it] = int(tscanf) 42 | if interp: 43 | dtscan[it] = tscanf - tscan[it] 44 | return xscan, tscan, dtscan 45 | 46 | 47 | @jit(nopython=True, parallel=parallel, nogil=True) 48 | def _indices_2d_onthefly_numba(f, x, px, ip, t, nt, interp=True): 49 | """Wrapper around _indices_2d to allow on-the-fly computation of 50 | parametric curves using numba 51 | """ 52 | tscan = np.full(len(x), np.nan, dtype=np.float32) 53 | if interp: 54 | dtscan = np.full(len(x), np.nan) 55 | else: 56 | dtscan = None 57 | xscan, tscan1, dtscan1 = _indices_2d_numba(f, x, px[ip], t, nt, interp=interp) 58 | tscan[xscan] = tscan1 59 | if interp: 60 | dtscan[xscan] = dtscan1 61 | return xscan, tscan, dtscan 62 | 63 | 64 | @jit(nopython=True, parallel=parallel, nogil=True) 65 | def _create_table_numba(f, x, pxaxis, nt, npx, nx, interp): 66 | """Create look up table using numba""" 67 | table = np.full((npx, nt, nx), np.nan, dtype=np.float32) 68 | dtable = np.full((npx, nt, nx), np.nan) 69 | for ipx in range(npx): 70 | px = pxaxis[ipx] 71 | for it in range(nt): 72 | xscans, tscan, dtscan = _indices_2d_numba(f, x, px, it, nt, interp=interp) 73 | itscan = 0 74 | for ixscan, xscan in enumerate(xscans): 75 | if xscan: 76 | table[ipx, it, ixscan] = tscan[itscan] 77 | if interp: 78 | dtable[ipx, it, ixscan] = dtscan[itscan] 79 | itscan += 1 80 | return table, dtable 81 | -------------------------------------------------------------------------------- /pylops/signalprocessing/_radon3d_numba.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | from numba import jit 5 | 6 | # detect whether to use parallel or not 7 | numba_threads = int(os.getenv("NUMBA_NUM_THREADS", "1")) 8 | parallel = True if numba_threads != 1 else False 9 | 10 | 11 | @jit(nopython=True) 12 | def _linear_numba(y, x, t, py, px): 13 | return t + px * x + py * y 14 | 15 | 16 | @jit(nopython=True) 17 | def _parabolic_numba(y, x, t, py, px): 18 | return t + px * x**2 + py * y**2 19 | 20 | 21 | @jit(nopython=True) 22 | def _hyperbolic_numba(y, x, t, py, px): 23 | return np.sqrt(t**2 + (x / px) ** 2 + (y / py) ** 2) 24 | 25 | 26 | @jit(nopython=True, parallel=parallel, nogil=True) 27 | def _indices_3d_numba(f, y, x, py, px, t, nt, interp=True): 28 | """Compute time and space indices of parametric line in ``f`` function 29 | using numba. Refer to ``_indices_3d`` for full documentation. 30 | """ 31 | tdecscan = f(y, x, t, py, px) 32 | if not interp: 33 | sscan = (tdecscan >= 0) & (tdecscan < nt) 34 | else: 35 | sscan = (tdecscan >= 0) & (tdecscan < nt - 1) 36 | tscanfs = tdecscan[sscan] 37 | tscan = np.zeros(len(tscanfs)) 38 | dtscan = np.zeros(len(tscanfs)) 39 | for it, tscanf in enumerate(tscanfs): 40 | tscan[it] = int(tscanf) 41 | if interp: 42 | dtscan[it] = tscanf - tscan[it] 43 | return sscan, tscan, dtscan 44 | 45 | 46 | @jit(nopython=True, parallel=parallel, nogil=True) 47 | def _indices_3d_onthefly_numba(f, y, x, py, px, ip, t, nt, interp=True): 48 | """Wrapper around _indices_3d to allow on-the-fly computation of 49 | parametric curves using numba 50 | """ 51 | tscan = np.full(len(y), np.nan, dtype=np.float32) 52 | if interp: 53 | dtscan = np.full(len(y), np.nan) 54 | else: 55 | dtscan = None 56 | sscan, tscan1, dtscan1 = _indices_3d_numba( 57 | f, y, x, py[ip], px[ip], t, nt, interp=interp 58 | ) 59 | tscan[sscan] = tscan1 60 | if interp: 61 | dtscan[sscan] = dtscan1 62 | return sscan, tscan, dtscan 63 | 64 | 65 | @jit(nopython=True, parallel=parallel, nogil=True) 66 | def _create_table_numba(f, y, x, pyaxis, pxaxis, nt, npy, npx, ny, nx, interp): 67 | """Create look up table using numba""" 68 | table = np.full((npx * npy, nt, ny * nx), np.nan, dtype=np.float32) 69 | dtable = np.full((npx * npy, nt, ny * nx), np.nan) 70 | for ip in range(len(pyaxis)): 71 | py = pyaxis[ip] 72 | px = pxaxis[ip] 73 | for it in range(nt): 74 | sscans, tscan, dtscan = _indices_3d_numba( 75 | f, y, x, py, px, it, nt, interp=interp 76 | ) 77 | itscan = 0 78 | for isscan, sscan in enumerate(sscans): 79 | if sscan: 80 | table[ip, it, isscan] = tscan[itscan] 81 | if interp: 82 | dtable[ip, it, isscan] = dtscan[itscan] 83 | itscan += 1 84 | return table, dtable 85 | -------------------------------------------------------------------------------- /pylops/signalprocessing/chirpradon2d.py: -------------------------------------------------------------------------------- 1 | __all__ = ["ChirpRadon2D"] 2 | 3 | import logging 4 | 5 | import numpy as np 6 | 7 | from pylops import LinearOperator 8 | from pylops.utils.decorators import reshaped 9 | from pylops.utils.typing import DTypeLike, NDArray 10 | 11 | from ._chirpradon2d import _chirp_radon_2d 12 | 13 | logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.WARNING) 14 | 15 | 16 | class ChirpRadon2D(LinearOperator): 17 | r"""2D Chirp Radon transform 18 | 19 | Apply Radon forward (and adjoint) transform using Fast 20 | Fourier Transform and Chirp functions to a 2-dimensional array of size 21 | :math:`[n_x \times n_t]` (both in forward and adjoint mode). 22 | 23 | Note that forward and adjoint are swapped compared to the time-space 24 | implementation in :class:`pylops.signalprocessing.Radon2D` and a direct 25 | `inverse` method is also available for this implementation. 26 | 27 | Parameters 28 | ---------- 29 | taxis : :obj:`np.ndarray` 30 | Time axis 31 | haxis : :obj:`np.ndarray` 32 | Spatial axis 33 | pmax : :obj:`np.ndarray` 34 | Maximum slope defined as :math:`\tan` of maximum stacking angle in 35 | :math:`x` direction :math:`p_\text{max} = \tan(\alpha_{x, \text{max}})`. 36 | If one operates in terms of minimum velocity :math:`c_0`, set 37 | :math:`p_{x, \text{max}}=c_0 \,\mathrm{d}y/\mathrm{d}t`. 38 | dtype : :obj:`str`, optional 39 | Type of elements in input array. 40 | name : :obj:`str`, optional 41 | .. versionadded:: 2.0.0 42 | 43 | Name of operator (to be used by :func:`pylops.utils.describe.describe`) 44 | 45 | Attributes 46 | ---------- 47 | shape : :obj:`tuple` 48 | Operator shape 49 | explicit : :obj:`bool` 50 | Operator contains a matrix that can be solved explicitly (``True``) or 51 | not (``False``) 52 | 53 | Notes 54 | ----- 55 | Refer to [1]_ for the theoretical and implementation details. 56 | 57 | .. [1] Andersson, F and Robertsson J. "Fast :math:`\tau-p` transforms by 58 | chirp modulation", Geophysics, vol 84, NO.1, pp. A13-A17, 2019. 59 | 60 | """ 61 | 62 | def __init__( 63 | self, 64 | taxis: NDArray, 65 | haxis: NDArray, 66 | pmax: float, 67 | dtype: DTypeLike = "float64", 68 | name: str = "C", 69 | ) -> None: 70 | dims = len(haxis), len(taxis) 71 | super().__init__(dtype=np.dtype(dtype), dims=dims, dimsd=dims, name=name) 72 | 73 | self.nh, self.nt = self.dims 74 | self.dt = taxis[1] - taxis[0] 75 | self.dh = haxis[1] - haxis[0] 76 | self.pmax = pmax 77 | 78 | @reshaped 79 | def _matvec(self, x: NDArray) -> NDArray: 80 | return _chirp_radon_2d(x, self.dt, self.dh, self.pmax, mode="f") 81 | 82 | @reshaped 83 | def _rmatvec(self, x: NDArray) -> NDArray: 84 | return _chirp_radon_2d(x, self.dt, self.dh, self.pmax, mode="a") 85 | 86 | def inverse(self, x: NDArray) -> NDArray: 87 | x = x.reshape(self.dimsd) 88 | y = _chirp_radon_2d(x, self.dt, self.dh, self.pmax, mode="i") 89 | return y.ravel() 90 | -------------------------------------------------------------------------------- /pylops/signalprocessing/convolve2d.py: -------------------------------------------------------------------------------- 1 | __all__ = ["Convolve2D"] 2 | 3 | from typing import Union 4 | 5 | from pylops.signalprocessing import ConvolveND 6 | from pylops.utils.typing import DTypeLike, InputDimsLike, NDArray 7 | 8 | 9 | class Convolve2D(ConvolveND): 10 | r"""2D convolution operator. 11 | 12 | Apply two-dimensional convolution with a compact filter to model 13 | (and data) along a pair of ``axes`` of a two or 14 | three-dimensional array. 15 | 16 | Parameters 17 | ---------- 18 | dims : :obj:`list` or :obj:`int` 19 | Number of samples for each dimension 20 | h : :obj:`numpy.ndarray` 21 | 2d compact filter to be convolved to input signal 22 | offset : :obj:`tuple`, optional 23 | Indices of the center of the compact filter 24 | axes : :obj:`int`, optional 25 | .. versionadded:: 2.0.0 26 | 27 | Axes along which convolution is applied 28 | method : :obj:`str`, optional 29 | Method used to calculate the convolution (``direct`` or ``fft``). 30 | dtype : :obj:`str`, optional 31 | Type of elements in input array. 32 | name : :obj:`str`, optional 33 | .. versionadded:: 2.0.0 34 | 35 | Name of operator (to be used by :func:`pylops.utils.describe.describe`) 36 | 37 | Notes 38 | ----- 39 | The Convolve2D operator applies two-dimensional convolution 40 | between the input signal :math:`d(t,x)` and a compact filter kernel 41 | :math:`h(t,x)` in forward model: 42 | 43 | .. math:: 44 | y(t,x) = \iint\limits_{-\infty}^{\infty} 45 | h(t-\tau,x-\chi) d(\tau,\chi) \,\mathrm{d}\tau \,\mathrm{d}\chi 46 | 47 | This operation can be discretized as follows 48 | 49 | .. math:: 50 | y[i,n] = \sum_{j=-\infty}^{\infty} \sum_{m=-\infty}^{\infty} h[i-j,n-m] d[j,m] 51 | 52 | 53 | as well as performed in the frequency domain. 54 | 55 | .. math:: 56 | Y(f, k_x) = \mathscr{F} (h(t,x)) * \mathscr{F} (d(t,x)) 57 | 58 | Convolve2D operator uses :py:func:`scipy.signal.convolve2d` 59 | that automatically chooses the best domain for the operation 60 | to be carried out. 61 | 62 | As the adjoint of convolution is correlation, Convolve2D operator 63 | applies correlation in the adjoint mode. 64 | 65 | In time domain: 66 | 67 | .. math:: 68 | y(t,x) = \iint\limits_{-\infty}^{\infty} 69 | h(t+\tau,x+\chi) d(\tau,\chi) \,\mathrm{d}\tau \,\mathrm{d}\chi 70 | 71 | or in frequency domain: 72 | 73 | .. math:: 74 | y(t, x) = \mathscr{F}^{-1} (H(f, k_x)^* * X(f, k_x)) 75 | 76 | """ 77 | def __init__(self, dims: Union[int, InputDimsLike], 78 | h: NDArray, 79 | offset: InputDimsLike = (0, 0), 80 | axes: InputDimsLike = (-2, -1), 81 | method: str = "fft", 82 | dtype: DTypeLike = "float64", 83 | name: str = "C", ): 84 | if h.ndim != 2: 85 | raise ValueError("h must be 2-dimensional") 86 | super().__init__(dims=dims, h=h, offset=offset, axes=axes, method=method, dtype=dtype, name=name) 87 | -------------------------------------------------------------------------------- /pylops/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # isort: skip_file 2 | from .backend import * 3 | from .deps import * 4 | from .dottest import * 5 | from .estimators import * 6 | from .metrics import * 7 | from .multiproc import * 8 | from .utils import * 9 | from .typing import * 10 | -------------------------------------------------------------------------------- /pylops/utils/_internal.py: -------------------------------------------------------------------------------- 1 | from typing import Sized, Tuple 2 | 3 | import numpy as np 4 | import numpy.typing as npt 5 | 6 | from pylops.utils.typing import DTypeLike, NDArray 7 | 8 | 9 | def _value_or_sized_to_array(value_or_sized, repeat: int = 1) -> NDArray: 10 | """Convert an object which is either single value or a list-like to an array. 11 | 12 | Parameters 13 | ---------- 14 | value_or_sized : `obj`:`int` or `obj`:`float` or `obj`:`list` or `obj`:`tuple` 15 | Single value or list-like. 16 | repeat : `obj`:`int` 17 | Size of resulting array if value is passed. If list is passed, it is ignored. 18 | 19 | Returns 20 | ------- 21 | out : `obj`:`numpy.array` 22 | When the input is a single value, returned an array with `repeat` samples 23 | containing that value. When the input is a list-like object, converts it to an 24 | array. 25 | 26 | """ 27 | return ( 28 | np.asarray(value_or_sized) 29 | if isinstance(value_or_sized, Sized) 30 | else np.array([value_or_sized] * repeat) 31 | ) 32 | 33 | 34 | def _value_or_sized_to_tuple(value_or_sized, repeat: int = 1) -> Tuple: 35 | """Convert an object which is either single value or a list-like to a tuple. 36 | 37 | Parameters 38 | ---------- 39 | value_or_sized : `obj`:`int` or `obj`:`float` or `obj`:`list` or `obj`:`tuple` 40 | Single value or list-like. 41 | repeat : `obj`:`int` 42 | Size of resulting array if value is passed. If list is passed, it is ignored. 43 | 44 | Returns 45 | ------- 46 | out : `obj`:`tuple` 47 | When the input is a single value, returned an array with `repeat` samples 48 | containing that value. When the input is a list-like object, converts it to a 49 | tuple. 50 | 51 | """ 52 | return tuple(_value_or_sized_to_array(value_or_sized, repeat=repeat)) 53 | 54 | 55 | def _raise_on_wrong_dtype(arr: npt.ArrayLike, dtype: DTypeLike, name: str) -> None: 56 | """Raises an error if dtype of `arr` is not a subdtype of `dtype`. 57 | 58 | Parameters 59 | ---------- 60 | arr : `obj`:`numpy.array` 61 | Array whose type will be checked 62 | dtype : `obj`:`numpy.dtype` 63 | Type which must be a supertype of `arr.dtype`. 64 | name : `obj`:`str` 65 | Name of parameter to issue error. 66 | 67 | Raises 68 | ------ 69 | TypeError 70 | When `arr.dtype` is not a subdtype of `dtype`. 71 | 72 | """ 73 | if not np.issubdtype(arr.dtype, dtype): 74 | raise TypeError( 75 | ( 76 | f"Wrong input type for `{name}`. " 77 | f'Must be "{dtype}", but received to "{arr.dtype}".' 78 | ) 79 | ) 80 | -------------------------------------------------------------------------------- /pylops/utils/metrics.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "mae", 3 | "mse", 4 | "snr", 5 | "psnr", 6 | ] 7 | 8 | from typing import Optional 9 | 10 | import numpy as np 11 | import numpy.typing as npt 12 | 13 | 14 | def mae(xref: npt.ArrayLike, xcmp: npt.ArrayLike) -> float: 15 | """Mean Absolute Error (MAE) 16 | 17 | Compute Mean Absolute Error between two vectors 18 | 19 | Parameters 20 | ---------- 21 | xref : :obj:`numpy.ndarray` 22 | Reference vector 23 | xcmp : :obj:`numpy.ndarray` 24 | Comparison vector 25 | 26 | Returns 27 | ------- 28 | mae : :obj:`float` 29 | Mean Absolute Error 30 | 31 | """ 32 | mae = np.mean(np.abs(xref - xcmp)) 33 | return mae 34 | 35 | 36 | def mse(xref: npt.ArrayLike, xcmp: npt.ArrayLike) -> float: 37 | """Mean Square Error (MSE) 38 | 39 | Compute Mean Square Error between two vectors 40 | 41 | Parameters 42 | ---------- 43 | xref : :obj:`numpy.ndarray` 44 | Reference vector 45 | xcmp : :obj:`numpy.ndarray` 46 | Comparison vector 47 | 48 | Returns 49 | ------- 50 | mse : :obj:`float` 51 | Mean Square Error 52 | 53 | """ 54 | mse = np.mean(np.abs(xref - xcmp) ** 2) 55 | return mse 56 | 57 | 58 | def snr(xref: npt.ArrayLike, xcmp: npt.ArrayLike) -> float: 59 | """Signal to Noise Ratio (SNR) 60 | 61 | Compute Signal to Noise Ratio between two vectors 62 | 63 | Parameters 64 | ---------- 65 | xref : :obj:`numpy.ndarray` 66 | Reference vector 67 | xcmp : :obj:`numpy.ndarray` 68 | Comparison vector 69 | 70 | Returns 71 | ------- 72 | snr : :obj:`float` 73 | Signal to Noise Ratio of ``xcmp`` with respect to ``xref`` 74 | 75 | """ 76 | xrefv = np.mean(np.abs(xref) ** 2) 77 | snr = 10.0 * np.log10(xrefv / mse(xref, xcmp)) 78 | return snr 79 | 80 | 81 | def psnr( 82 | xref: npt.ArrayLike, 83 | xcmp: npt.ArrayLike, 84 | xmax: Optional[float] = None, 85 | xmin: Optional[float] = 0.0, 86 | ) -> float: 87 | """Peak Signal to Noise Ratio (PSNR) 88 | 89 | Compute Peak Signal to Noise Ratio between two vectors 90 | 91 | Parameters 92 | ---------- 93 | xref : :obj:`numpy.ndarray` 94 | Reference vector 95 | xcmp : :obj:`numpy.ndarray` 96 | Comparison vector 97 | xmax : :obj:`float`, optional 98 | Maximum value to use. If ``None``, the actual maximum of 99 | the reference vector is used 100 | xmin : :obj:`float`, optional 101 | Minimum value to use. If ``None``, the actual minimum of 102 | the reference vector is used (``0`` is default for 103 | backward compatibility) 104 | 105 | Returns 106 | ------- 107 | psnr : :obj:`float` 108 | Peak Signal to Noise Ratio of ``xcmp`` with respect to ``xref`` 109 | 110 | """ 111 | if xmax is None: 112 | xmax = xref.max() 113 | if xmin is None: 114 | xmin = xref.min() 115 | xrange = xmax - xmin 116 | psnr = 10.0 * np.log10(xrange**2 / mse(xref, xcmp)) 117 | return psnr 118 | -------------------------------------------------------------------------------- /pylops/utils/multiproc.py: -------------------------------------------------------------------------------- 1 | __all__ = ["scalability_test"] 2 | 3 | import time 4 | from typing import List, Optional, Tuple 5 | 6 | import numpy.typing as npt 7 | 8 | 9 | def scalability_test( 10 | Op, 11 | x: npt.ArrayLike, 12 | workers: Optional[List[int]] = None, 13 | forward: bool = True, 14 | ) -> Tuple[List[float], List[float]]: 15 | r"""Scalability test. 16 | 17 | Small auxiliary routine to test the performance of operators using 18 | ``multiprocessing``. This helps identifying the maximum number of workers 19 | beyond which no performance gain is observed. 20 | 21 | Parameters 22 | ---------- 23 | Op : :obj:`pylops.LinearOperator` 24 | Operator to test. It must allow for multiprocessing. 25 | x : :obj:`numpy.ndarray`, optional 26 | Input vector. 27 | workers : :obj:`list`, optional 28 | Number of workers to test out. Defaults to `[1, 2, 4]`. 29 | forward : :obj:`bool`, optional 30 | Apply forward (``True``) or adjoint (``False``) 31 | 32 | Returns 33 | ------- 34 | compute_times : :obj:`list` 35 | Compute times as function of workers 36 | speedup : :obj:`list` 37 | Speedup as function of workers 38 | 39 | """ 40 | if workers is None: 41 | workers = [1, 2, 4] 42 | compute_times = [] 43 | speedup = [] 44 | for nworkers in workers: 45 | print(f"Working with {nworkers} workers...") 46 | # update number of workers in operator 47 | Op.nproc = nworkers 48 | # run forward/adjoint computation 49 | starttime = time.time() 50 | if forward: 51 | _ = Op.matvec(x) 52 | else: 53 | _ = Op.rmatvec(x) 54 | elapsedtime = time.time() - starttime 55 | compute_times.append(elapsedtime) 56 | speedup.append(compute_times[0] / elapsedtime) 57 | Op.pool.close() 58 | return compute_times, speedup 59 | -------------------------------------------------------------------------------- /pylops/utils/typing.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "IntNDArray", 3 | "NDArray", 4 | "InputDimsLike", 5 | "SamplingLike", 6 | "ShapeLike", 7 | "DTypeLike", 8 | "TensorTypeLike", 9 | ] 10 | 11 | from typing import Sequence, Tuple, Union 12 | 13 | import numpy as np 14 | import numpy.typing as npt 15 | 16 | from pylops.utils.deps import torch_enabled 17 | 18 | if torch_enabled: 19 | import torch 20 | 21 | IntNDArray = npt.NDArray[np.int_] 22 | NDArray = npt.NDArray 23 | 24 | InputDimsLike = Union[Sequence[int], IntNDArray] 25 | SamplingLike = Union[Sequence[float], NDArray] 26 | ShapeLike = Tuple[int, ...] 27 | DTypeLike = npt.DTypeLike 28 | 29 | if torch_enabled: 30 | TensorTypeLike = torch.Tensor 31 | else: 32 | TensorTypeLike = None 33 | -------------------------------------------------------------------------------- /pylops/utils/utils.py: -------------------------------------------------------------------------------- 1 | __all__ = ["Report"] 2 | 3 | # scooby is a soft dependency for pylops 4 | from typing import Optional 5 | 6 | try: 7 | from scooby import Report as ScoobyReport 8 | except ImportError: 9 | 10 | class ScoobyReport: 11 | def __init__(self, additional, core, optional, ncol, text_width, sort): 12 | print( 13 | "\nNOTE: `pylops.Report` requires `scooby`. Install it via" 14 | "\n `pip install scooby` or " 15 | "`conda install -c conda-forge scooby`.\n" 16 | ) 17 | 18 | 19 | class Report(ScoobyReport): 20 | r"""Print date, time, and version information. 21 | 22 | Use ``scooby`` to print date, time, and package version information in any 23 | environment (Jupyter notebook, IPython console, Python console, QT 24 | console), either as html-table (notebook) or as plain text (anywhere). 25 | 26 | Always shown are the OS, number of CPU(s), ``numpy``, ``scipy``, 27 | ``pylops``, ``sys.version``, and time/date. 28 | 29 | Additionally shown are, if they can be imported, ``IPython``, ``numba``, 30 | and ``matplotlib``. It also shows MKL information, if available. 31 | 32 | All modules provided in ``add_pckg`` are also shown. 33 | 34 | .. note:: 35 | 36 | The package ``scooby`` has to be installed in order to use ``Report``: 37 | ``pip install scooby`` or ``conda install -c conda-forge scooby``. 38 | 39 | 40 | Parameters 41 | ---------- 42 | add_pckg : packages, optional 43 | Package or list of packages to add to output information (must be 44 | imported beforehand). 45 | 46 | ncol : int, optional 47 | Number of package-columns in html table (no effect in text-version); 48 | Defaults to 3. 49 | 50 | text_width : int, optional 51 | The text width for non-HTML display modes 52 | 53 | sort : bool, optional 54 | Sort the packages when the report is shown 55 | 56 | 57 | Examples 58 | -------- 59 | >>> import pytest 60 | >>> import dateutil 61 | >>> from pylops import Report 62 | >>> Report() # Default values 63 | >>> Report(pytest) # Provide additional package 64 | >>> Report([pytest, dateutil], ncol=5) # Set nr of columns 65 | 66 | """ 67 | 68 | def __init__( 69 | self, 70 | add_pckg: Optional[list] = None, 71 | ncol: int = 3, 72 | text_width: int = 80, 73 | sort: bool = False, 74 | ) -> None: 75 | """Initiate a scooby.Report instance.""" 76 | 77 | # Mandatory packages. 78 | core = ["numpy", "scipy", "pylops"] 79 | 80 | # Optional packages. 81 | optional = ["IPython", "matplotlib", "numba"] 82 | 83 | super().__init__( 84 | additional=add_pckg, 85 | core=core, 86 | optional=optional, 87 | ncol=ncol, 88 | text_width=text_width, 89 | sort=sort, 90 | ) 91 | -------------------------------------------------------------------------------- /pylops/waveeqprocessing/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wave Equation processing 3 | ======================== 4 | 5 | The subpackage waveeqprocessing provides linear operators and applications 6 | aimed at solving various inverse problems in the area of Seismic Wave 7 | Equation Processing. 8 | 9 | A list of operators present in pylops.waveeqprocessing: 10 | 11 | PressureToVelocity Pressure to Vertical velocity conversion. 12 | UpDownComposition2D 2D Up-down wavefield composition. 13 | UpDownComposition3D 3D Up-down wavefield composition. 14 | MDC Multi-dimensional convolution. 15 | PhaseShift Phase shift operator. 16 | BlendingContinuous Continuous Blending operator. 17 | BlendingGroup Group Blending operator. 18 | BlendingHalf Half Blending operator. 19 | Kirchhoff Kirchoff demigration operator. 20 | AcousticWave2D Two-way wave equation demigration operator. 21 | 22 | and a list of applications: 23 | 24 | SeismicInterpolation Seismic interpolation (or regularization). 25 | Deghosting Single-component wavefield decomposition. 26 | WavefieldDecomposition Multi-component wavefield decomposition. 27 | MDD Multi-dimensional deconvolution. 28 | Marchenko Marchenko redatuming. 29 | LSM Least-squares Migration (LSM). 30 | 31 | """ 32 | 33 | from .blending import * 34 | from .kirchhoff import * 35 | from .lsm import * 36 | from .marchenko import * 37 | from .mdd import * 38 | from .oneway import * 39 | from .seismicinterpolation import * 40 | from .twoway import * 41 | from .wavedecomposition import * 42 | 43 | __all__ = [ 44 | "MDC", 45 | "MDD", 46 | "Marchenko", 47 | "SeismicInterpolation", 48 | "PressureToVelocity", 49 | "UpDownComposition2D", 50 | "UpDownComposition3D", 51 | "WavefieldDecomposition", 52 | "PhaseShift", 53 | "Deghosting", 54 | "BlendingContinuous", 55 | "BlendingGroup", 56 | "BlendingHalf", 57 | "Kirchhoff", 58 | "AcousticWave2D", 59 | "LSM", 60 | ] 61 | -------------------------------------------------------------------------------- /pylops/waveeqprocessing/_twoway.py: -------------------------------------------------------------------------------- 1 | from examples.seismic.utils import PointSource 2 | 3 | 4 | class _CustomSource(PointSource): 5 | """Custom source 6 | 7 | This class creates a Devito symbolic object that encapsulates a set of 8 | sources with a user defined source signal wavelet ``wav`` 9 | 10 | Parameters 11 | ---------- 12 | name : :obj:`str` 13 | Name for the resulting symbol. 14 | grid : :obj:`devito.types.grid.Grid` 15 | The computational domain. 16 | time_range : :obj:`examples.seismic.source.TimeAxis` 17 | TimeAxis(start, step, num) object. 18 | wav : :obj:`numpy.ndarray` 19 | Wavelet of size 20 | 21 | """ 22 | 23 | __rkwargs__ = PointSource.__rkwargs__ + ["wav"] 24 | 25 | @classmethod 26 | def __args_setup__(cls, *args, **kwargs): 27 | kwargs.setdefault("npoint", 1) 28 | 29 | return super().__args_setup__(*args, **kwargs) 30 | 31 | def __init_finalize__(self, *args, **kwargs): 32 | super().__init_finalize__(*args, **kwargs) 33 | 34 | self.wav = kwargs.get("wav") 35 | 36 | if not self.alias: 37 | for p in range(kwargs["npoint"]): 38 | self.data[:, p] = self.wavelet 39 | 40 | @property 41 | def wavelet(self): 42 | """Return user-provided wavelet""" 43 | return self.wav 44 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools >= 65", 4 | "setuptools_scm[toml]", 5 | "wheel", 6 | ] 7 | build-backend = "setuptools.build_meta" 8 | 9 | [project] 10 | name = "pylops" 11 | description = "Python library implementing linear operators to allow solving large-scale optimization problems" 12 | readme = "README.md" 13 | authors = [ 14 | {name = "Matteo Ravasi", email = "matteoravasi@gmail.com"}, 15 | ] 16 | license = {file = "LICENSE.md"} 17 | keywords = ["algebra", "inverse problems", "large-scale optimization"] 18 | classifiers = [ 19 | "Development Status :: 5 - Production/Stable", 20 | "Intended Audience :: Developers", 21 | "Intended Audience :: Science/Research", 22 | "Intended Audience :: Education", 23 | "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", 24 | "Natural Language :: English", 25 | "Operating System :: OS Independent", 26 | "Programming Language :: Python :: 3 :: Only", 27 | "Programming Language :: Python :: 3.8", 28 | "Programming Language :: Python :: 3.9", 29 | "Programming Language :: Python :: 3.10", 30 | "Topic :: Scientific/Engineering :: Mathematics", 31 | ] 32 | dependencies = [ 33 | "numpy >= 1.21.0", 34 | "scipy >= 1.11.0", 35 | ] 36 | dynamic = ["version"] 37 | 38 | [project.optional-dependencies] 39 | advanced = [ 40 | "llvmlite", 41 | "numba", 42 | "pyfftw", 43 | "PyWavelets", 44 | "scikit-fmm", 45 | "spgl1", 46 | "dtcwt", 47 | ] 48 | 49 | [tool.setuptools.packages.find] 50 | exclude = ["pytests"] 51 | 52 | [tool.setuptools_scm] 53 | version_file = "pylops/version.py" 54 | -------------------------------------------------------------------------------- /pytests/test_blending.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): 4 | import cupy as np 5 | 6 | backend = "cupy" 7 | else: 8 | import numpy as np 9 | 10 | backend = "numpy" 11 | import numpy as npp 12 | import pytest 13 | 14 | from pylops.utils import dottest 15 | from pylops.waveeqprocessing import BlendingContinuous, BlendingGroup, BlendingHalf 16 | 17 | par = {"nt": 101, "ns": 50, "nr": 20, "dtype": "float64"} 18 | 19 | d = np.random.normal(0, 1, (par["ns"], par["nr"], par["nt"])) 20 | dt = 0.004 21 | 22 | 23 | @pytest.mark.parametrize("par", [(par)]) 24 | def test_Blending_continuous(par): 25 | """Dot-test for continuous Blending operator""" 26 | npp.random.seed(0) 27 | # ignition times 28 | overlap = 0.5 29 | ignition_times = 2.0 * npp.random.rand(par["ns"]) - 1.0 30 | ignition_times += ( 31 | npp.arange(0, overlap * par["nt"] * par["ns"], overlap * par["nt"]) * dt 32 | ) 33 | ignition_times[0] = 0.0 34 | 35 | Bop = BlendingContinuous( 36 | par["nt"], 37 | par["nr"], 38 | par["ns"], 39 | dt, 40 | ignition_times, 41 | dtype=par["dtype"], 42 | ) 43 | assert dottest( 44 | Bop, 45 | Bop.nttot * par["nr"], 46 | par["nt"] * par["ns"] * par["nr"], 47 | backend=backend, 48 | ) 49 | 50 | 51 | @pytest.mark.parametrize("par", [(par)]) 52 | def test_Blending_group(par): 53 | """Dot-test for group Blending operator""" 54 | npp.random.seed(0) 55 | group_size = 2 56 | n_groups = par["ns"] // group_size 57 | ignition_times = 0.8 * npp.random.rand(par["ns"]) 58 | 59 | Bop = BlendingGroup( 60 | par["nt"], 61 | par["nr"], 62 | par["ns"], 63 | dt, 64 | ignition_times.reshape(group_size, n_groups), 65 | n_groups=n_groups, 66 | group_size=group_size, 67 | dtype=par["dtype"], 68 | ) 69 | assert dottest( 70 | Bop, 71 | par["nt"] * n_groups * par["nr"], 72 | par["nt"] * par["ns"] * par["nr"], 73 | backend=backend, 74 | ) 75 | 76 | 77 | @pytest.mark.parametrize("par", [(par)]) 78 | def test_Blending_half(par): 79 | """Dot-test for half Blending operator""" 80 | npp.random.seed(0) 81 | group_size = 2 82 | n_groups = par["ns"] // group_size 83 | ignition_times = 0.8 * npp.random.rand(par["ns"]) 84 | 85 | Bop = BlendingHalf( 86 | par["nt"], 87 | par["nr"], 88 | par["ns"], 89 | dt, 90 | ignition_times.reshape(group_size, n_groups), 91 | n_groups=n_groups, 92 | group_size=group_size, 93 | dtype=par["dtype"], 94 | ) 95 | assert dottest( 96 | Bop, 97 | par["nt"] * n_groups * par["nr"], 98 | par["nt"] * par["ns"] * par["nr"], 99 | backend=backend, 100 | ) 101 | -------------------------------------------------------------------------------- /pytests/test_dct.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | from pylops.signalprocessing import DCT 7 | from pylops.utils import dottest 8 | 9 | par1 = {"ny": 11, "nx": 11, "imag": 0, "dtype": "float64"} 10 | par2 = {"ny": 11, "nx": 21, "imag": 0, "dtype": "float64"} 11 | par3 = {"ny": 21, "nx": 21, "imag": 0, "dtype": "float64"} 12 | 13 | 14 | @pytest.mark.skipif( 15 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 16 | ) 17 | @pytest.mark.parametrize("par", [(par1), (par3)]) 18 | def test_DCT1D(par): 19 | """Dot test for Discrete Cosine Transform Operator 1D""" 20 | 21 | t = np.arange(par["ny"]) + 1 22 | 23 | for type in [1, 2, 3, 4]: 24 | Dct = DCT(dims=(par["ny"],), type=type, dtype=par["dtype"]) 25 | assert dottest(Dct, par["ny"], par["ny"], rtol=1e-6, complexflag=0, verb=True) 26 | 27 | y = Dct.H * (Dct * t) 28 | np.testing.assert_allclose(t, y) 29 | 30 | 31 | @pytest.mark.skipif( 32 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 33 | ) 34 | @pytest.mark.parametrize("par", [(par1), (par2), (par3)]) 35 | def test_DCT2D(par): 36 | """Dot test for Discrete Cosine Transform Operator 2D""" 37 | 38 | t = np.outer(np.arange(par["ny"]) + 1, np.arange(par["nx"]) + 1) 39 | 40 | for type in [1, 2, 3, 4]: 41 | for axes in [0, 1]: 42 | Dct = DCT(dims=t.shape, type=type, axes=axes, dtype=par["dtype"]) 43 | assert dottest( 44 | Dct, 45 | par["nx"] * par["ny"], 46 | par["nx"] * par["ny"], 47 | rtol=1e-6, 48 | complexflag=0, 49 | verb=True, 50 | ) 51 | 52 | y = Dct.H * (Dct * t) 53 | np.testing.assert_allclose(t, y) 54 | 55 | 56 | @pytest.mark.skipif( 57 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 58 | ) 59 | @pytest.mark.parametrize("par", [(par1), (par2), (par3)]) 60 | def test_DCT3D(par): 61 | """Dot test for Discrete Cosine Transform Operator 3D""" 62 | 63 | t = np.random.rand(par["nx"], par["nx"], par["nx"]) 64 | 65 | for type in [1, 2, 3, 4]: 66 | for axes in [0, 1, 2]: 67 | Dct = DCT(dims=t.shape, type=type, axes=axes, dtype=par["dtype"]) 68 | assert dottest( 69 | Dct, 70 | par["nx"] * par["nx"] * par["nx"], 71 | par["nx"] * par["nx"] * par["nx"], 72 | rtol=1e-6, 73 | complexflag=0, 74 | verb=True, 75 | ) 76 | 77 | y = Dct.H * (Dct * t) 78 | np.testing.assert_allclose(t, y) 79 | 80 | 81 | @pytest.mark.skipif( 82 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 83 | ) 84 | @pytest.mark.parametrize("par", [(par1), (par3)]) 85 | def test_DCT_workers(par): 86 | """Dot test for Discrete Cosine Transform Operator with workers""" 87 | t = np.arange(par["ny"]) + 1 88 | 89 | Dct = DCT(dims=(par["ny"],), type=1, dtype=par["dtype"], workers=2) 90 | assert dottest(Dct, par["ny"], par["ny"], rtol=1e-6, complexflag=0, verb=True) 91 | 92 | y = Dct.H * (Dct * t) 93 | np.testing.assert_allclose(t, y) 94 | -------------------------------------------------------------------------------- /pytests/test_describe.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): 4 | import cupy as np 5 | else: 6 | import numpy as np 7 | 8 | from pylops.basicoperators import BlockDiag, Diagonal, HStack, MatrixMult 9 | from pylops.utils.describe import describe 10 | 11 | 12 | def test_describe(): 13 | """Testing the describe method. As it is is difficult to verify that the 14 | output is correct, at this point we merely test that no error arises when 15 | applying this method to a variety of operators 16 | """ 17 | A = MatrixMult(np.ones((10, 5))) 18 | A.name = "A" 19 | B = Diagonal(np.ones(5)) 20 | B.name = "A" 21 | C = MatrixMult(np.ones((10, 5))) 22 | C.name = "C" 23 | 24 | AT = A.T 25 | AH = A.H 26 | A3 = 3 * A 27 | D = A + C 28 | E = D * B 29 | F = (A + C) * B + A 30 | G = HStack((A * B, C * B)) 31 | H = BlockDiag((F, G)) 32 | 33 | describe(A) 34 | describe(AT) 35 | describe(AH) 36 | describe(A3) 37 | describe(D) 38 | describe(E) 39 | describe(F) 40 | describe(G) 41 | describe(H) 42 | -------------------------------------------------------------------------------- /pytests/test_directwave.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | import pytest 5 | from numpy.testing import assert_array_almost_equal 6 | from scipy.signal import convolve 7 | 8 | from pylops.waveeqprocessing.marchenko import directwave 9 | 10 | # Test data 11 | inputfile2d = "testdata/marchenko/input.npz" 12 | inputfile3d = "testdata/marchenko/direct3D.npz" 13 | 14 | # Parameters 15 | vel = 2400.0 # velocity 16 | 17 | 18 | @pytest.mark.skipif( 19 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 20 | ) 21 | def test_direct2D(): 22 | """Check consistency of analytical 2D Green's function with FD modelling""" 23 | inputdata = np.load(inputfile2d) 24 | 25 | # Receivers 26 | r = inputdata["r"] 27 | nr = r.shape[1] 28 | 29 | # Virtual points 30 | vs = inputdata["vs"] 31 | 32 | # Time axis 33 | t = inputdata["t"] 34 | dt, nt = t[1] - t[0], len(t) 35 | 36 | # FD GF 37 | G0FD = inputdata["G0sub"] 38 | wav = inputdata["wav"] 39 | wav_c = np.argmax(wav) 40 | 41 | G0FD = np.apply_along_axis(convolve, 0, G0FD, wav, mode="full") 42 | G0FD = G0FD[wav_c:][:nt] 43 | 44 | # Analytic GF 45 | trav = np.sqrt((vs[0] - r[0]) ** 2 + (vs[1] - r[1]) ** 2) / vel 46 | G0ana = directwave(wav, trav, nt, dt, nfft=nt, derivative=False) 47 | 48 | # Differentiate to get same response as in FD modelling 49 | G0ana = np.diff(G0ana, axis=0) 50 | G0ana = np.vstack([G0ana, np.zeros(nr)]) 51 | 52 | assert_array_almost_equal( 53 | G0FD / np.max(np.abs(G0FD)), G0ana / np.max(np.abs(G0ana)), decimal=1 54 | ) 55 | 56 | 57 | @pytest.mark.skipif( 58 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 59 | ) 60 | def test_direct3D(): 61 | """Check consistency of analytical 3D Green's function with FD modelling""" 62 | inputdata = np.load(inputfile3d) 63 | 64 | # Receivers 65 | r = inputdata["r"] 66 | nr = r.shape[0] 67 | 68 | # Virtual points 69 | vs = inputdata["vs"] 70 | 71 | # Time axis 72 | t = inputdata["t"] 73 | dt, nt = t[1] - t[0], len(t) 74 | 75 | # FD GF 76 | G0FD = inputdata["G0"][:, :nr] 77 | wav = inputdata["wav"] 78 | wav_c = np.argmax(wav) 79 | 80 | G0FD = np.apply_along_axis(convolve, 0, G0FD, wav, mode="full") 81 | G0FD = G0FD[wav_c:][:nt] 82 | 83 | # Analytic GF 84 | dist = np.sqrt( 85 | (vs[0] - r[:, 0]) ** 2 + (vs[1] - r[:, 1]) ** 2 + (vs[2] - r[:, 2]) ** 2 86 | ) 87 | trav = dist / vel 88 | G0ana = directwave( 89 | wav, trav, nt, dt, nfft=nt, dist=dist, kind="3d", derivative=False 90 | ) 91 | 92 | # Differentiate to get same response as in FD modelling 93 | G0ana = np.diff(G0ana, axis=0) 94 | G0ana = np.vstack([G0ana, np.zeros(nr)]) 95 | 96 | assert_array_almost_equal( 97 | G0FD / np.max(np.abs(G0FD)), G0ana / np.max(np.abs(G0ana)), decimal=1 98 | ) 99 | -------------------------------------------------------------------------------- /pytests/test_dtcwt.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | from pylops.signalprocessing import DTCWT 7 | 8 | # currently test only if numpy<2.0.0 is installed... 9 | np_version = np.__version__.split(".") 10 | 11 | par1 = {"ny": 10, "nx": 10, "dtype": "float64"} 12 | par2 = {"ny": 50, "nx": 50, "dtype": "float64"} 13 | 14 | 15 | def sequential_array(shape): 16 | num_elements = np.prod(shape) 17 | seq_array = np.arange(1, num_elements + 1) 18 | result = seq_array.reshape(shape) 19 | return result 20 | 21 | 22 | @pytest.mark.skipif( 23 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 24 | ) 25 | @pytest.mark.parametrize("par", [(par1), (par2)]) 26 | def test_dtcwt1D_input1D(par): 27 | """Test for DTCWT with 1D input""" 28 | if int(np_version[0]) >= 2: 29 | return 30 | 31 | t = sequential_array((par["ny"],)) 32 | 33 | for level in range(1, 10): 34 | Dtcwt = DTCWT(dims=t.shape, level=level, dtype=par["dtype"]) 35 | x = Dtcwt @ t 36 | y = Dtcwt.H @ x 37 | 38 | np.testing.assert_allclose(t, y) 39 | 40 | 41 | @pytest.mark.skipif( 42 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 43 | ) 44 | @pytest.mark.parametrize("par", [(par1), (par2)]) 45 | def test_dtcwt1D_input2D(par): 46 | """Test for DTCWT with 2D input (forward-inverse pair)""" 47 | if int(np_version[0]) >= 2: 48 | return 49 | 50 | t = sequential_array( 51 | ( 52 | par["ny"], 53 | par["ny"], 54 | ) 55 | ) 56 | 57 | for level in range(1, 10): 58 | Dtcwt = DTCWT(dims=t.shape, level=level, dtype=par["dtype"]) 59 | x = Dtcwt @ t 60 | y = Dtcwt.H @ x 61 | 62 | np.testing.assert_allclose(t, y) 63 | 64 | 65 | @pytest.mark.skipif( 66 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 67 | ) 68 | @pytest.mark.parametrize("par", [(par1), (par2)]) 69 | def test_dtcwt1D_input3D(par): 70 | """Test for DTCWT with 3D input (forward-inverse pair)""" 71 | if int(np_version[0]) >= 2: 72 | return 73 | 74 | t = sequential_array((par["ny"], par["ny"], par["ny"])) 75 | 76 | for level in range(1, 10): 77 | Dtcwt = DTCWT(dims=t.shape, level=level, dtype=par["dtype"]) 78 | x = Dtcwt @ t 79 | y = Dtcwt.H @ x 80 | 81 | np.testing.assert_allclose(t, y) 82 | 83 | 84 | @pytest.mark.skipif( 85 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 86 | ) 87 | @pytest.mark.parametrize("par", [(par1), (par2)]) 88 | def test_dtcwt1D_birot(par): 89 | """Test for DTCWT birot (forward-inverse pair)""" 90 | if int(np_version[0]) >= 2: 91 | return 92 | 93 | birots = ["antonini", "legall", "near_sym_a", "near_sym_b"] 94 | 95 | t = sequential_array( 96 | ( 97 | par["ny"], 98 | par["ny"], 99 | ) 100 | ) 101 | 102 | for _b in birots: 103 | print(f"birot {_b}") 104 | Dtcwt = DTCWT(dims=t.shape, biort=_b, dtype=par["dtype"]) 105 | x = Dtcwt @ t 106 | y = Dtcwt.H @ x 107 | 108 | np.testing.assert_allclose(t, y) 109 | -------------------------------------------------------------------------------- /pytests/test_eigs.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): 4 | import cupy as np 5 | 6 | backend = "cupy" 7 | else: 8 | import numpy as np 9 | 10 | backend = "numpy" 11 | import numpy as npp 12 | import pytest 13 | 14 | from pylops.basicoperators import MatrixMult 15 | from pylops.optimization.eigs import power_iteration 16 | from pylops.utils.backend import to_numpy 17 | 18 | par1 = {"n": 21, "imag": 0, "dtype": "float32"} # square, real 19 | par2 = {"n": 21, "imag": 1j, "dtype": "complex64"} # square, complex 20 | 21 | 22 | @pytest.mark.parametrize("par", [(par1), (par2)]) 23 | def test_power_iteration(par): 24 | """Max eigenvalue computation with power iteration method vs. scipy methods""" 25 | np.random.seed(10) 26 | 27 | A = np.random.randn(par["n"], par["n"]) + par["imag"] * np.random.randn( 28 | par["n"], par["n"] 29 | ) 30 | A1 = np.conj(A.T) @ A 31 | 32 | # non-symmetric 33 | Aop = MatrixMult(A) 34 | eig = power_iteration(Aop, niter=200, tol=0, backend=backend)[0] 35 | eig_np = npp.max(npp.abs(npp.linalg.eig(to_numpy(A))[0])) 36 | 37 | assert np.abs(np.abs(eig) - eig_np) < 1e-3 38 | 39 | # symmetric 40 | A1op = MatrixMult(A1) 41 | eig = power_iteration(A1op, niter=200, tol=0, backend=backend)[0] 42 | eig_np = npp.max(npp.abs(npp.linalg.eig(to_numpy(A1))[0])) 43 | 44 | assert np.abs(np.abs(eig) - eig_np) < 1e-3 45 | -------------------------------------------------------------------------------- /pytests/test_fredholm.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): 4 | import cupy as np 5 | from cupy.testing import assert_array_almost_equal 6 | 7 | backend = "cupy" 8 | else: 9 | import numpy as np 10 | from numpy.testing import assert_array_almost_equal 11 | 12 | backend = "numpy" 13 | import itertools 14 | 15 | import pytest 16 | 17 | from pylops.optimization.basic import lsqr 18 | from pylops.signalprocessing import Fredholm1 19 | from pylops.utils import dottest 20 | 21 | par1 = { 22 | "nsl": 3, 23 | "ny": 6, 24 | "nx": 4, 25 | "nz": 5, 26 | "usematmul": True, 27 | "saveGt": True, 28 | "imag": 0, 29 | "dtype": "float32", 30 | } # real, saved Gt 31 | par2 = { 32 | "nsl": 3, 33 | "ny": 6, 34 | "nx": 4, 35 | "nz": 5, 36 | "usematmul": True, 37 | "saveGt": False, 38 | "imag": 0, 39 | "dtype": "float32", 40 | } # real, unsaved Gt 41 | par3 = { 42 | "nsl": 3, 43 | "ny": 6, 44 | "nx": 4, 45 | "nz": 5, 46 | "usematmul": False, 47 | "saveGt": True, 48 | "imag": 1j, 49 | "dtype": "complex64", 50 | } # complex, saved Gt 51 | par4 = { 52 | "nsl": 3, 53 | "ny": 6, 54 | "nx": 4, 55 | "nz": 5, 56 | "saveGt": False, 57 | "usematmul": False, 58 | "saveGt": False, 59 | "imag": 1j, 60 | "dtype": "complex64", 61 | } # complex, unsaved Gt 62 | par5 = { 63 | "nsl": 3, 64 | "ny": 6, 65 | "nx": 4, 66 | "nz": 1, 67 | "usematmul": True, 68 | "saveGt": True, 69 | "imag": 0, 70 | "dtype": "float32", 71 | } # real, saved Gt, nz=1 72 | par6 = { 73 | "nsl": 3, 74 | "ny": 6, 75 | "nx": 4, 76 | "nz": 1, 77 | "usematmul": True, 78 | "saveGt": False, 79 | "imag": 0, 80 | "dtype": "float32", 81 | } # real, unsaved Gt, nz=1 82 | 83 | 84 | @pytest.mark.parametrize("par", [(par1), (par2), (par3), (par4), (par5), (par6)]) 85 | def test_Fredholm1(par): 86 | """Dot-test and inversion for Fredholm1 operator""" 87 | np.random.seed(10) 88 | 89 | _F = np.arange(par["nsl"] * par["nx"] * par["ny"]).reshape( 90 | par["nsl"], par["nx"], par["ny"] 91 | ) 92 | F = _F - par["imag"] * _F 93 | 94 | x = np.ones((par["nsl"], par["ny"], par["nz"])) + par["imag"] * np.ones( 95 | (par["nsl"], par["ny"], par["nz"]) 96 | ) 97 | 98 | Fop = Fredholm1( 99 | F, 100 | nz=par["nz"], 101 | saveGt=par["saveGt"], 102 | usematmul=par["usematmul"], 103 | dtype=par["dtype"], 104 | ) 105 | assert dottest( 106 | Fop, 107 | par["nsl"] * par["nx"] * par["nz"], 108 | par["nsl"] * par["ny"] * par["nz"], 109 | complexflag=0 if par["imag"] == 0 else 3, 110 | backend=backend, 111 | ) 112 | xlsqr = lsqr( 113 | Fop, 114 | Fop * x.ravel(), 115 | x0=np.zeros_like(x), 116 | damp=1e-20, 117 | niter=30, 118 | atol=1e-8, 119 | btol=1e-8, 120 | show=0, 121 | )[0] 122 | xlsqr = xlsqr.reshape(par["nsl"], par["ny"], par["nz"]) 123 | assert_array_almost_equal(x, xlsqr, decimal=3) 124 | -------------------------------------------------------------------------------- /pytests/test_jaxoperator.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | import pytest 5 | from numpy.testing import assert_array_almost_equal, assert_array_equal 6 | 7 | from pylops import JaxOperator, MatrixMult 8 | from pylops.utils import deps 9 | 10 | jax_message = deps.jax_import("the jax module") 11 | 12 | if jax_message is None: 13 | import jax 14 | import jax.numpy as jnp 15 | 16 | 17 | par1 = {"ny": 11, "nx": 11, "dtype": np.float32} # square 18 | par2 = {"ny": 21, "nx": 11, "dtype": np.float32} # overdetermined 19 | 20 | np.random.seed(0) 21 | 22 | 23 | @pytest.mark.skipif( 24 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 25 | ) 26 | @pytest.mark.parametrize("par", [(par1)]) 27 | def test_JaxOperator(par): 28 | """Apply forward and adjoint and compare with native pylops.""" 29 | M = np.random.normal(0.0, 1.0, (par["ny"], par["nx"])).astype(par["dtype"]) 30 | Mop = MatrixMult(jnp.array(M), dtype=par["dtype"]) 31 | Jop = JaxOperator(Mop) 32 | 33 | x = np.random.normal(0.0, 1.0, par["nx"]).astype(par["dtype"]) 34 | xjnp = jnp.array(x) 35 | 36 | # pylops operator 37 | y = Mop * x 38 | xadj = Mop.H * y 39 | 40 | # jax operator 41 | yjnp = Jop * xjnp 42 | xadjnp = Jop.rmatvecad(xjnp, yjnp) 43 | 44 | assert_array_equal(y, np.array(yjnp)) 45 | assert_array_equal(xadj, np.array(xadjnp)) 46 | 47 | 48 | @pytest.mark.skipif( 49 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 50 | ) 51 | @pytest.mark.parametrize("par", [(par1)]) 52 | def test_TorchOperator_batch(par): 53 | """Apply forward for input with multiple samples 54 | (= batch) and flattened arrays""" 55 | 56 | M = np.random.normal(0.0, 1.0, (par["ny"], par["nx"])).astype(par["dtype"]) 57 | Mop = MatrixMult(jnp.array(M), dtype=par["dtype"]) 58 | Jop = JaxOperator(Mop) 59 | auto_batch_matvec = jax.vmap(Jop._matvec) 60 | 61 | x = np.random.normal(0.0, 1.0, (4, par["nx"])).astype(par["dtype"]) 62 | xjnp = jnp.array(x) 63 | 64 | y = Mop.matmat(x.T).T 65 | yjnp = auto_batch_matvec(xjnp) 66 | 67 | assert_array_almost_equal(y, np.array(yjnp), decimal=5) 68 | -------------------------------------------------------------------------------- /pytests/test_kronecker.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): 4 | import cupy as np 5 | from cupy.testing import assert_array_almost_equal, assert_array_equal 6 | 7 | backend = "cupy" 8 | else: 9 | import numpy as np 10 | from numpy.testing import assert_array_almost_equal, assert_array_equal 11 | 12 | backend = "numpy" 13 | import pytest 14 | 15 | from pylops.basicoperators import FirstDerivative, Identity, Kronecker, MatrixMult 16 | from pylops.optimization.basic import lsqr 17 | from pylops.utils import dottest 18 | 19 | par1 = {"ny": 11, "nx": 11, "imag": 0, "dtype": "float64"} # square real 20 | par2 = {"ny": 21, "nx": 11, "imag": 0, "dtype": "float64"} # overdetermined real 21 | par1j = {"ny": 11, "nx": 11, "imag": 1j, "dtype": "complex128"} # square imag 22 | par2j = {"ny": 21, "nx": 11, "imag": 1j, "dtype": "complex128"} # overdetermined imag 23 | 24 | 25 | @pytest.mark.parametrize("par", [(par1), (par2), (par1j), (par2j)]) 26 | def test_Kroneker(par): 27 | """Dot-test, inversion and comparison with np.kron for Kronecker operator""" 28 | np.random.seed(10) 29 | G1 = np.random.normal(0, 10, (par["ny"], par["nx"])).astype(par["dtype"]) 30 | G2 = np.random.normal(0, 10, (par["ny"], par["nx"])).astype(par["dtype"]) 31 | x = np.ones(par["nx"] ** 2) + par["imag"] * np.ones(par["nx"] ** 2) 32 | 33 | Kop = Kronecker( 34 | MatrixMult(G1, dtype=par["dtype"]), 35 | MatrixMult(G2, dtype=par["dtype"]), 36 | dtype=par["dtype"], 37 | ) 38 | assert dottest( 39 | Kop, 40 | par["ny"] ** 2, 41 | par["nx"] ** 2, 42 | complexflag=0 if par["imag"] == 0 else 3, 43 | backend=backend, 44 | ) 45 | 46 | if backend == "numpy": # cupy is not accurate enough for square systems 47 | xlsqr = lsqr( 48 | Kop, 49 | Kop * x, 50 | x0=np.zeros_like(x), 51 | damp=1e-20, 52 | niter=300, 53 | atol=0, 54 | btol=0, 55 | conlim=np.inf, 56 | show=0, 57 | )[0] 58 | assert_array_almost_equal(x, xlsqr, decimal=2) 59 | 60 | # Comparison with numpy 61 | assert_array_almost_equal(np.kron(G1, G2), Kop * np.eye(par["nx"] ** 2), decimal=3) 62 | 63 | 64 | @pytest.mark.parametrize("par", [(par1), (par2)]) 65 | def test_Kroneker_Derivative(par): 66 | """Use Kronecker operator to apply the Derivative operator over one axis 67 | and compare with FirstDerivative(... axis=axis) 68 | """ 69 | Dop = FirstDerivative(par["ny"], sampling=1, edge=True, dtype="float32") 70 | D2op = FirstDerivative( 71 | (par["ny"], par["nx"]), axis=0, sampling=1, edge=True, dtype="float32" 72 | ) 73 | 74 | Kop = Kronecker(Dop, Identity(par["nx"], dtype=par["dtype"]), dtype=par["dtype"]) 75 | 76 | x = np.zeros((par["ny"], par["nx"])) + par["imag"] * np.zeros( 77 | (par["ny"], par["nx"]) 78 | ) 79 | x[par["ny"] // 2, par["nx"] // 2] = 1 80 | 81 | y = D2op * x.ravel() 82 | yk = Kop * x.ravel() 83 | assert_array_equal(y, yk) 84 | -------------------------------------------------------------------------------- /pytests/test_lsm.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | import pytest 5 | from numpy.testing import assert_array_almost_equal 6 | 7 | from pylops.utils.wavelets import ricker 8 | from pylops.waveeqprocessing.lsm import LSM 9 | 10 | PAR = { 11 | "ny": 10, 12 | "nx": 12, 13 | "nz": 20, 14 | "nt": 50, 15 | "dy": 3, 16 | "dx": 1, 17 | "dz": 2, 18 | "dt": 0.004, 19 | "nsy": 4, 20 | "nry": 8, 21 | "nsx": 6, 22 | "nrx": 4, 23 | } 24 | 25 | # Check if skfmm is available and by-pass tests using it otherwise. This is 26 | # currently required for Travis as since we moved to Python3.8 it has 27 | # stopped working 28 | try: 29 | import skfmm # noqa: F401 30 | 31 | skfmm_enabled = True 32 | except ImportError: 33 | skfmm_enabled = False 34 | 35 | v0 = 500 36 | y = np.arange(PAR["ny"]) * PAR["dy"] 37 | x = np.arange(PAR["nx"]) * PAR["dx"] 38 | z = np.arange(PAR["nz"]) * PAR["dz"] 39 | t = np.arange(PAR["nt"]) * PAR["dt"] 40 | 41 | sy = np.linspace(y.min(), y.max(), PAR["nsy"]) 42 | sx = np.linspace(x.min(), x.max(), PAR["nsx"]) 43 | syy, sxx = np.meshgrid(sy, sx, indexing="ij") 44 | s2d = np.vstack((sx, 2 * np.ones(PAR["nsx"]))) 45 | s3d = np.vstack((syy.ravel(), sxx.ravel(), 2 * np.ones(PAR["nsx"] * PAR["nsy"]))) 46 | 47 | ry = np.linspace(y.min(), y.max(), PAR["nry"]) 48 | rx = np.linspace(x.min(), x.max(), PAR["nrx"]) 49 | ryy, rxx = np.meshgrid(ry, rx, indexing="ij") 50 | r2d = np.vstack((rx, 2 * np.ones(PAR["nrx"]))) 51 | r3d = np.vstack((ryy.ravel(), rxx.ravel(), 2 * np.ones(PAR["nrx"] * PAR["nry"]))) 52 | 53 | wav, _, wavc = ricker(t[:21], f0=40) 54 | 55 | par1 = {"mode": "analytic", "dynamic": False} 56 | par2 = {"mode": "eikonal", "dynamic": False} 57 | par1d = {"mode": "analytic", "dynamic": True} 58 | par2d = {"mode": "eikonal", "dynamic": True} 59 | 60 | 61 | @pytest.mark.skipif( 62 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 63 | ) 64 | def test_unknown_mode(): 65 | """Check error is raised if unknown mode is passed""" 66 | with pytest.raises(NotImplementedError): 67 | _ = LSM(z, x, t, s2d, r2d, 0, np.ones(3), 1, mode="foo") 68 | 69 | 70 | @pytest.mark.skipif( 71 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 72 | ) 73 | @pytest.mark.parametrize("par", [(par1), (par2), (par1d), (par2d)]) 74 | def test_lsm2d(par): 75 | """Dot-test and inverse for LSM operator""" 76 | if skfmm_enabled or par["mode"] != "eikonal": 77 | vel = v0 * np.ones((PAR["nx"], PAR["nz"])) 78 | refl = np.zeros((PAR["nx"], PAR["nz"])) 79 | refl[:, PAR["nz"] // 2] = 1 80 | refl[:, 3 * PAR["nz"] // 4] = 1 81 | 82 | lsm = LSM( 83 | z, 84 | x, 85 | t, 86 | s2d, 87 | r2d, 88 | vel if par["mode"] == "eikonal" else v0, 89 | wav, 90 | wavc, 91 | mode=par["mode"], 92 | dynamic=par["dynamic"], 93 | dottest=True, 94 | ) 95 | 96 | d = lsm.Demop * refl.ravel() 97 | d = d.reshape(PAR["nsx"], PAR["nrx"], PAR["nt"]) 98 | 99 | minv = lsm.solve(d.ravel(), **dict(iter_lim=100, show=True)) 100 | minv = minv.reshape(PAR["nx"], PAR["nz"]) 101 | 102 | dinv = lsm.Demop * minv.ravel() 103 | dinv = dinv.reshape(PAR["nsx"], PAR["nrx"], PAR["nt"]) 104 | 105 | assert_array_almost_equal(d / d.max(), dinv / d.max(), decimal=2) 106 | assert_array_almost_equal(refl / refl.max(), minv / refl.max(), decimal=1) 107 | -------------------------------------------------------------------------------- /pytests/test_memoizeoperator.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): 4 | import cupy as np 5 | from cupy.testing import assert_array_almost_equal 6 | 7 | backend = "cupy" 8 | else: 9 | import numpy as np 10 | from numpy.testing import assert_array_almost_equal 11 | 12 | backend = "numpy" 13 | import itertools 14 | 15 | import pytest 16 | 17 | from pylops import MemoizeOperator 18 | from pylops.basicoperators import MatrixMult, VStack 19 | 20 | par1 = {"ny": 11, "nx": 11, "imag": 0, "dtype": "float32"} # square real 21 | par1j = {"ny": 11, "nx": 11, "imag": 1j, "dtype": "complex64"} # square imag 22 | 23 | 24 | @pytest.mark.parametrize("par", [(par1), (par1j)]) 25 | def test_memoize_evals(par): 26 | """Check nevals counter when same model/data vectors are inputted 27 | to the operator 28 | """ 29 | np.random.seed(0) 30 | A = np.random.normal(0, 10, (par["ny"], par["nx"])).astype("float32") + par[ 31 | "imag" 32 | ] * np.random.normal(0, 10, (par["ny"], par["nx"])).astype("float32") 33 | Aop = MatrixMult(A, dtype=par["dtype"]) 34 | Amemop = MemoizeOperator(Aop, max_neval=2) 35 | 36 | # 1st evaluation 37 | Amemop * np.ones(par["nx"]) 38 | assert Amemop.neval == 1 39 | # repeat 1st evaluation multiple times 40 | for _ in range(2): 41 | Amemop * np.ones(par["nx"]) 42 | assert Amemop.neval == 1 43 | # 2nd evaluation 44 | Amemop * np.full(par["nx"], 2) # same 45 | assert Amemop.neval == 2 46 | # 3rd evaluation (np.ones goes out of store) 47 | Amemop * np.full(par["nx"], 3) # same 48 | assert Amemop.neval == 3 49 | # 4th evaluation 50 | Amemop * np.ones(par["nx"]) 51 | assert Amemop.neval == 4 52 | 53 | 54 | @pytest.mark.parametrize( 55 | "par", 56 | [ 57 | (par1j), 58 | ], 59 | ) 60 | def test_memoize_evals_2(par): 61 | """Inversion of problem with real model and complex data, using two 62 | equivalent approaches: 1. complex operator enforcing the output of adjoint 63 | to be real, 2. joint system of equations for real and complex parts 64 | """ 65 | np.random.seed(0) 66 | rdtype = np.real(np.ones(1, dtype=par["dtype"])).dtype 67 | A = np.random.normal(0, 10, (par["ny"], par["nx"])).astype(rdtype) + par[ 68 | "imag" 69 | ] * np.random.normal(0, 10, (par["ny"], par["nx"])).astype(rdtype) 70 | Aop = MatrixMult(A, dtype=par["dtype"]) 71 | x = np.ones(par["nx"], dtype=rdtype) 72 | y = Aop * x 73 | 74 | # Approach 1 75 | Aop1 = Aop.toreal(forw=False, adj=True) 76 | xinv1 = Aop1.div(y) 77 | assert_array_almost_equal(x, xinv1) 78 | 79 | # Approach 2 80 | Amop = MemoizeOperator(Aop, max_neval=10) 81 | Aop2 = VStack([Amop.toreal(), Amop.toimag()]) 82 | y2 = np.concatenate([np.real(y), np.imag(y)]) 83 | xinv2 = Aop2.div(y2) 84 | assert_array_almost_equal(x, xinv2) 85 | -------------------------------------------------------------------------------- /pytests/test_metrics.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): 4 | import cupy as np 5 | 6 | backend = "cupy" 7 | else: 8 | import numpy as np 9 | 10 | backend = "numpy" 11 | import itertools 12 | 13 | import pytest 14 | 15 | from pylops.utils.metrics import mae, mse, psnr, snr 16 | 17 | par1 = {"nx": 11, "dtype": "float64"} # float64 18 | par2 = {"nx": 11, "dtype": "float32"} # float32 19 | 20 | 21 | @pytest.mark.parametrize("par", [(par1), (par2)]) 22 | def test_mae(par): 23 | """Check MAE with same vector and vector of zeros""" 24 | xref = np.ones(par["nx"]) 25 | xcmp = np.zeros(par["nx"]) 26 | 27 | maesame = mae(xref, xref) 28 | maecmp = mae(xref, xcmp) 29 | assert maesame == 0.0 30 | assert maecmp == 1.0 31 | 32 | 33 | @pytest.mark.parametrize("par", [(par1), (par2)]) 34 | def test_mse(par): 35 | """Check MSE with same vector and vector of zeros""" 36 | xref = np.ones(par["nx"]) 37 | xcmp = np.zeros(par["nx"]) 38 | 39 | msesame = mse(xref, xref) 40 | msecmp = mse(xref, xcmp) 41 | assert msesame == 0.0 42 | assert msecmp == 1.0 43 | 44 | 45 | @pytest.mark.parametrize("par", [(par1), (par2)]) 46 | def test_snr(par): 47 | """Check SNR with same vector and vector of zeros""" 48 | xref = np.random.normal(0, 1, par["nx"]) 49 | xcmp = np.zeros(par["nx"]) 50 | 51 | snrsame = snr(xref, xref) 52 | snrcmp = snr(xref, xcmp) 53 | assert snrsame == np.inf 54 | assert snrcmp == 0.0 55 | 56 | 57 | @pytest.mark.parametrize("par", [(par1), (par2)]) 58 | def test_psnr(par): 59 | """Check PSNR with same vector and vector of zeros""" 60 | xref = np.ones(par["nx"]) 61 | xcmp = np.zeros(par["nx"]) 62 | 63 | psnrsame = psnr(xref, xref, xmax=1.0, xmin=0.0) 64 | psnrcmp = psnr(xref, xcmp, xmax=1.0, xmin=0.0) 65 | assert psnrsame == np.inf 66 | assert psnrcmp == 0.0 67 | -------------------------------------------------------------------------------- /pytests/test_pad.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): 4 | import cupy as np 5 | from cupy.testing import assert_array_equal 6 | 7 | backend = "cupy" 8 | else: 9 | import numpy as np 10 | from numpy.testing import assert_array_equal 11 | 12 | backend = "numpy" 13 | import pytest 14 | 15 | from pylops.basicoperators import Pad 16 | from pylops.utils import dottest 17 | 18 | par1 = {"ny": 11, "nx": 11, "pad": ((0, 2), (4, 5)), "dtype": "float64"} # square 19 | par2 = {"ny": 21, "nx": 11, "pad": ((3, 1), (0, 3)), "dtype": "float64"} # rectangular 20 | 21 | np.random.seed(10) 22 | 23 | 24 | @pytest.mark.parametrize("par", [(par1)]) 25 | def test_Pad_1d_negative(par): 26 | """Check error is raised when pad has negative number""" 27 | with pytest.raises(ValueError): 28 | _ = Pad(dims=par["ny"], pad=(-10, 0)) 29 | 30 | 31 | @pytest.mark.parametrize("par", [(par1)]) 32 | def test_Pad_2d_negative(par): 33 | """Check error is raised when pad has negative number for 2d""" 34 | with pytest.raises(ValueError): 35 | _ = Pad(dims=(par["ny"], par["nx"]), pad=((-10, 0), (3, -5))) 36 | 37 | 38 | @pytest.mark.parametrize("par", [(par1)]) 39 | def test_Pad1d(par): 40 | """Dot-test and adjoint for Pad operator on 1d signal""" 41 | Pop = Pad(dims=par["ny"], pad=par["pad"][0], dtype=par["dtype"]) 42 | assert dottest(Pop, Pop.shape[0], Pop.shape[1], backend=backend) 43 | 44 | x = np.arange(par["ny"], dtype=par["dtype"]) + 1.0 45 | y = Pop * x 46 | xinv = Pop.H * y 47 | assert_array_equal(x, xinv) 48 | 49 | 50 | @pytest.mark.parametrize("par", [(par1), (par2)]) 51 | def test_Pad2d(par): 52 | """Dot-test and adjoint for Pad operator on 2d signal""" 53 | Pop = Pad(dims=(par["ny"], par["nx"]), pad=par["pad"], dtype=par["dtype"]) 54 | assert dottest(Pop, Pop.shape[0], Pop.shape[1], backend=backend) 55 | 56 | x = (np.arange(par["ny"] * par["nx"], dtype=par["dtype"]) + 1.0).reshape( 57 | par["ny"], par["nx"] 58 | ) 59 | y = Pop * x.ravel() 60 | xadj = Pop.H * y 61 | assert_array_equal(x.ravel(), xadj) 62 | -------------------------------------------------------------------------------- /pytests/test_pytensoroperator.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | import pytest 5 | from numpy.testing import assert_array_equal 6 | 7 | from pylops import MatrixMult, PyTensorOperator 8 | from pylops.utils import deps 9 | 10 | pytensor_message = deps.pytensor_import("the pytensor module") 11 | 12 | if pytensor_message is None: 13 | import pytensor 14 | 15 | 16 | par1 = {"ny": 11, "nx": 11, "dtype": np.float32} # square 17 | par2 = {"ny": 21, "nx": 11, "dtype": np.float32} # overdetermined 18 | 19 | np.random.seed(0) 20 | rng = np.random.default_rng() 21 | 22 | 23 | @pytest.mark.skipif( 24 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 25 | ) 26 | @pytest.mark.parametrize("par", [(par1)]) 27 | def test_PyTensorOperator(par): 28 | """Verify output and gradient of PyTensor function obtained from a LinearOperator.""" 29 | Dop = MatrixMult(np.random.normal(0.0, 1.0, (par["ny"], par["nx"]))) 30 | pytensor_op = PyTensorOperator(Dop) 31 | 32 | # Check gradient 33 | inp = np.random.randn(*pytensor_op.dims) 34 | pytensor.gradient.verify_grad(pytensor_op, (inp,), rng=rng) 35 | 36 | # Check value 37 | x = pytensor.tensor.dvector() 38 | f = pytensor.function([x], pytensor_op(x)) 39 | out = f(inp) 40 | assert_array_equal(out, Dop @ inp) 41 | 42 | 43 | @pytest.mark.skipif( 44 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 45 | ) 46 | @pytest.mark.parametrize("par", [(par1)]) 47 | def test_PyTensorOperator_nd(par): 48 | """Verify output and gradient of PyTensor function obtained from a LinearOperator 49 | using an ND-array.""" 50 | otherdims = rng.choice(range(1, 3), size=rng.choice(range(2, 8))) 51 | Dop = MatrixMult( 52 | np.random.normal(0.0, 1.0, (par["ny"], par["nx"])), otherdims=otherdims 53 | ) 54 | pytensor_op = PyTensorOperator(Dop) 55 | 56 | # Check gradient 57 | inp = np.random.randn(*pytensor_op.dims) 58 | pytensor.gradient.verify_grad(pytensor_op, (inp,), rng=rng) 59 | 60 | # Check value 61 | tensor = pytensor.tensor.TensorType(dtype="float64", shape=(None,) * inp.ndim) 62 | x = tensor() 63 | f = pytensor.function([x], pytensor_op(x)) 64 | out = f(inp) 65 | assert_array_equal(out, Dop @ inp) 66 | -------------------------------------------------------------------------------- /pytests/test_tapers.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | import pytest 5 | from numpy.testing import assert_array_equal 6 | 7 | from pylops.utils.tapers import taper2d, taper3d 8 | 9 | par1 = { 10 | "nt": 21, 11 | "nspat": (11, 13), 12 | "ntap": (3, 5), 13 | "tapertype": "hanning", 14 | } # hanning, odd samples and taper 15 | par2 = { 16 | "nt": 20, 17 | "nspat": (12, 16), 18 | "ntap": (4, 6), 19 | "tapertype": "hanning", 20 | } # hanning, even samples and taper 21 | par3 = { 22 | "nt": 21, 23 | "nspat": (11, 13), 24 | "ntap": (3, 5), 25 | "tapertype": "cosine", 26 | } # cosine, odd samples and taper 27 | par4 = { 28 | "nt": 20, 29 | "nspat": (12, 16), 30 | "ntap": (4, 6), 31 | "tapertype": "cosine", 32 | } # cosine, even samples and taper 33 | par5 = { 34 | "nt": 21, 35 | "nspat": (11, 13), 36 | "ntap": (3, 5), 37 | "tapertype": "cosinesquare", 38 | } # cosinesquare, odd samples and taper 39 | par6 = { 40 | "nt": 20, 41 | "nspat": (12, 16), 42 | "ntap": (4, 6), 43 | "tapertype": "cosinesquare", 44 | } # cosinesquare, even samples and taper 45 | par7 = { 46 | "nt": 21, 47 | "nspat": (11, 13), 48 | "ntap": (3, 5), 49 | "tapertype": "cosinesqrt", 50 | } # cosinesqrt, odd samples and taper 51 | par8 = { 52 | "nt": 20, 53 | "nspat": (12, 16), 54 | "ntap": (4, 6), 55 | "tapertype": "cosinesqrt", 56 | } # cosinesqrt, even samples and taper 57 | 58 | 59 | @pytest.mark.skipif( 60 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 61 | ) 62 | @pytest.mark.parametrize( 63 | "par", [(par1), (par2), (par3), (par4), (par5), (par6), (par7), (par8)] 64 | ) 65 | def test_taper2d(par): 66 | """Create taper wavelet and check size and values""" 67 | tap = taper2d(par["nt"], par["nspat"][0], par["ntap"][0], par["tapertype"]) 68 | 69 | assert tap.shape == (par["nspat"][0], par["nt"]) 70 | assert_array_equal(tap[0], np.zeros(par["nt"])) 71 | assert_array_equal(tap[-1], np.zeros(par["nt"])) 72 | assert_array_equal(tap[par["ntap"][0] + 1], np.ones(par["nt"])) 73 | assert_array_equal(tap[par["nspat"][0] // 2], np.ones(par["nt"])) 74 | 75 | 76 | @pytest.mark.skipif( 77 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 78 | ) 79 | @pytest.mark.parametrize( 80 | "par", [(par1), (par2), (par3), (par4), (par5), (par6), (par7), (par8)] 81 | ) 82 | def test_taper3d(par): 83 | """Create taper wavelet and check size and values""" 84 | tap = taper3d(par["nt"], par["nspat"], par["ntap"], par["tapertype"]) 85 | 86 | assert tap.shape == (par["nspat"][0], par["nspat"][1], par["nt"]) 87 | assert_array_equal(tap[0][0], np.zeros(par["nt"])) 88 | assert_array_equal(tap[-1][-1], np.zeros(par["nt"])) 89 | assert_array_equal(tap[par["ntap"][0], par["ntap"][1]], np.ones(par["nt"])) 90 | assert_array_equal( 91 | tap[par["nspat"][0] // 2, par["nspat"][1] // 2], np.ones(par["nt"]) 92 | ) 93 | -------------------------------------------------------------------------------- /pytests/test_transpose.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | if int(os.environ.get("TEST_CUPY_PYLOPS", 0)): 4 | import cupy as np 5 | from cupy.testing import assert_array_equal 6 | 7 | backend = "cupy" 8 | else: 9 | import numpy as np 10 | from numpy.testing import assert_array_equal 11 | 12 | backend = "numpy" 13 | import numpy as npp 14 | import pytest 15 | 16 | from pylops.basicoperators import Transpose 17 | from pylops.utils import dottest 18 | 19 | par1 = {"ny": 21, "nx": 11, "nt": 20, "imag": 0, "dtype": "float64"} # real 20 | par2 = {"ny": 21, "nx": 11, "nt": 20, "imag": 1j, "dtype": "complex128"} # complex 21 | 22 | np.random.seed(10) 23 | 24 | 25 | @pytest.mark.parametrize("par", [(par1), (par2)]) 26 | def test_Transpose_2dsignal(par): 27 | """Dot-test and adjoint for Transpose operator for 2d signals""" 28 | dims = (par["ny"], par["nx"]) 29 | x = np.arange(par["ny"] * par["nx"]).reshape(dims) + par["imag"] * np.arange( 30 | par["ny"] * par["nx"] 31 | ).reshape(dims) 32 | 33 | Top = Transpose(dims=dims, axes=(1, 0), dtype=par["dtype"]) 34 | assert dottest( 35 | Top, 36 | npp.prod(dims), 37 | npp.prod(dims), 38 | complexflag=0 if par["imag"] == 0 else 3, 39 | backend=backend, 40 | ) 41 | y = Top * x.ravel() 42 | xadj = Top.H * y 43 | 44 | y = y.reshape(Top.dimsd) 45 | xadj = xadj.reshape(Top.dims) 46 | 47 | assert_array_equal(x, xadj) 48 | assert_array_equal(y, x.T) 49 | 50 | 51 | @pytest.mark.parametrize("par", [(par1), (par2)]) 52 | def test_Transpose_3dsignal(par): 53 | """Dot-test and adjoint for Transpose operator for 3d signals""" 54 | dims = (par["ny"], par["nx"], par["nt"]) 55 | x = np.arange(par["ny"] * par["nx"] * par["nt"]).reshape(dims) + par[ 56 | "imag" 57 | ] * np.arange(par["ny"] * par["nx"] * par["nt"]).reshape(dims) 58 | 59 | Top = Transpose(dims=dims, axes=(2, 1, 0)) 60 | assert dottest( 61 | Top, 62 | npp.prod(dims), 63 | npp.prod(dims), 64 | complexflag=0 if par["imag"] == 0 else 3, 65 | backend=backend, 66 | ) 67 | 68 | y = Top * x.ravel() 69 | xadj = Top.H * y 70 | 71 | y = y.reshape(Top.dimsd) 72 | xadj = xadj.reshape(Top.dims) 73 | 74 | assert_array_equal(x, xadj) 75 | -------------------------------------------------------------------------------- /pytests/test_twoway.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | from pylops.utils import deps, dottest 7 | from pylops.waveeqprocessing.twoway import AcousticWave2D 8 | 9 | devito_message = deps.devito_import("the twoway module") 10 | 11 | if devito_message is None: 12 | import devito 13 | 14 | devito.configuration["log-level"] = "ERROR" 15 | 16 | 17 | par = { 18 | "ny": 10, 19 | "nx": 12, 20 | "nz": 20, 21 | "tn": 500, 22 | "dy": 3, 23 | "dx": 1, 24 | "dz": 2, 25 | "nr": 8, 26 | "ns": 2, 27 | } 28 | 29 | v0 = 2 30 | y = np.arange(par["ny"]) * par["dy"] 31 | x = np.arange(par["nx"]) * par["dx"] 32 | z = np.arange(par["nz"]) * par["dz"] 33 | 34 | sx = np.linspace(x.min(), x.max(), par["ns"]) 35 | rx = np.linspace(x.min(), x.max(), par["nr"]) 36 | 37 | 38 | @pytest.mark.skipif( 39 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 40 | ) 41 | def test_acwave2d(): 42 | """Dot-test for AcousticWave2D operator""" 43 | Dop = AcousticWave2D( 44 | (par["nx"], par["nz"]), 45 | (0, 0), 46 | (par["dx"], par["dz"]), 47 | np.ones((par["nx"], par["nz"])) * 2e3, 48 | sx, 49 | 5, 50 | rx, 51 | 5, 52 | 0.0, 53 | par["tn"], 54 | "Ricker", 55 | space_order=4, 56 | nbl=30, 57 | f0=15, 58 | dtype="float32", 59 | ) 60 | 61 | assert dottest( 62 | Dop, par["ns"] * par["nr"] * Dop.geometry.nt, par["nz"] * par["nx"], atol=1e-1 63 | ) 64 | -------------------------------------------------------------------------------- /pytests/test_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from pylops import utils 6 | 7 | try: 8 | import scooby 9 | except ImportError: 10 | scooby = False 11 | 12 | 13 | @pytest.mark.skipif( 14 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 15 | ) 16 | def test_report(capsys): 17 | out, _ = capsys.readouterr() # Empty capsys 18 | 19 | # Reporting is done by the external package scooby. 20 | # We just ensure the shown packages do not change (core and optional). 21 | if scooby: 22 | out1 = utils.Report() 23 | out2 = scooby.Report( 24 | core=["numpy", "scipy", "pylops"], 25 | optional=["IPython", "matplotlib", "numba"], 26 | ) 27 | 28 | # Ensure they're the same; exclude time to avoid errors. 29 | assert out1.__repr__()[115:] == out2.__repr__()[115:] 30 | 31 | else: # soft dependency 32 | _ = utils.Report() 33 | out, _ = capsys.readouterr() # Empty capsys 34 | assert "NOTE: `pylops.Report` requires `scooby`. Install it via" in out 35 | -------------------------------------------------------------------------------- /pytests/test_wavelets.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | from pylops.utils.wavelets import gaussian, klauder, ormsby, ricker 7 | 8 | par1 = {"nt": 21, "dt": 0.004} # odd samples 9 | par2 = {"nt": 20, "dt": 0.004} # even samples 10 | 11 | 12 | @pytest.mark.skipif( 13 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 14 | ) 15 | @pytest.mark.parametrize("par", [(par1), (par2)]) 16 | def test_gaussian(par): 17 | """Create gaussian wavelet and check size and central value""" 18 | t = np.arange(par["nt"]) * par["dt"] 19 | wav, twav, wcenter = gaussian(t, std=10) 20 | 21 | assert twav.size == (par["nt"] - 1 if par["nt"] % 2 == 0 else par["nt"]) * 2 - 1 22 | assert wav.shape[0] == (par["nt"] - 1 if par["nt"] % 2 == 0 else par["nt"]) * 2 - 1 23 | assert wav[wcenter] == 1 24 | 25 | 26 | @pytest.mark.skipif( 27 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 28 | ) 29 | @pytest.mark.parametrize("par", [(par1), (par2)]) 30 | def test_klauder(par): 31 | """Create klauder wavelet and check size and central value""" 32 | t = np.arange(par["nt"]) * par["dt"] 33 | wav, twav, wcenter = klauder(t, f=(10, 20)) 34 | 35 | assert twav.size == (par["nt"] - 1 if par["nt"] % 2 == 0 else par["nt"]) * 2 - 1 36 | assert wav.shape[0] == (par["nt"] - 1 if par["nt"] % 2 == 0 else par["nt"]) * 2 - 1 37 | assert wav[wcenter] == 1 38 | 39 | 40 | @pytest.mark.skipif( 41 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 42 | ) 43 | @pytest.mark.parametrize("par", [(par1), (par2)]) 44 | def test_ormsby(par): 45 | """Create ormsby wavelet and check size and central value""" 46 | t = np.arange(par["nt"]) * par["dt"] 47 | wav, twav, wcenter = ormsby(t, f=(5, 10, 25, 30)) 48 | 49 | assert twav.size == (par["nt"] - 1 if par["nt"] % 2 == 0 else par["nt"]) * 2 - 1 50 | assert wav.shape[0] == (par["nt"] - 1 if par["nt"] % 2 == 0 else par["nt"]) * 2 - 1 51 | assert wav[wcenter] == 1 52 | 53 | 54 | @pytest.mark.skipif( 55 | int(os.environ.get("TEST_CUPY_PYLOPS", 0)) == 1, reason="Not CuPy enabled" 56 | ) 57 | @pytest.mark.parametrize("par", [(par1), (par2)]) 58 | def test_ricker(par): 59 | """Create ricker wavelet and check size and central value""" 60 | t = np.arange(par["nt"]) * par["dt"] 61 | wav, twav, wcenter = ricker(t, f0=20) 62 | 63 | assert twav.size == (par["nt"] - 1 if par["nt"] % 2 == 0 else par["nt"]) * 2 - 1 64 | assert wav.shape[0] == (par["nt"] - 1 if par["nt"] % 2 == 0 else par["nt"]) * 2 - 1 65 | assert wav[wcenter] == 1 66 | -------------------------------------------------------------------------------- /requirements-dev-gpu.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.21.0 2 | scipy>=1.11.0 3 | cupy-cuda12x 4 | torch 5 | numba 6 | sympy 7 | matplotlib 8 | ipython 9 | pytest 10 | pytest-runner 11 | setuptools_scm 12 | docutils<0.18 13 | Sphinx 14 | pydata-sphinx-theme 15 | sphinx-gallery 16 | sphinxemoji 17 | numpydoc 18 | nbsphinx 19 | image 20 | pre-commit 21 | autopep8 22 | isort 23 | black 24 | flake8 25 | mypy 26 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.21.0 2 | scipy>=1.11.0 3 | jax 4 | numba 5 | pyfftw 6 | PyWavelets 7 | spgl1 8 | scikit-fmm 9 | sympy 10 | devito 11 | dtcwt 12 | matplotlib 13 | ipython 14 | pytest 15 | pytest-runner 16 | setuptools_scm 17 | docutils<0.18 18 | Sphinx 19 | pydata-sphinx-theme 20 | sphinx-gallery 21 | sphinxemoji 22 | numpydoc 23 | nbsphinx 24 | image 25 | pre-commit 26 | autopep8 27 | isort 28 | black 29 | flake8 30 | mypy 31 | pytensor 32 | -------------------------------------------------------------------------------- /requirements-doc.txt: -------------------------------------------------------------------------------- 1 | # Currently we force rdt to use numpy<2.0.0 to build the documentation 2 | # since the dtcwt is not yet compatible with numpy=2.0.0. For the 3 | # same reason, we force devito==4.8.7 as later versions of devito 4 | # require numpy>=2.0.0 5 | numpy>=1.21.0,<2.0.0 6 | scipy>=1.11.0,<1.13 7 | jax 8 | --extra-index-url https://download.pytorch.org/whl/cpu 9 | torch>=1.2.0 10 | numba 11 | pyfftw 12 | PyWavelets 13 | spgl1 14 | scikit-fmm 15 | sympy 16 | devito==4.8.6 17 | dtcwt 18 | matplotlib 19 | ipython 20 | pytest 21 | pytest-runner 22 | setuptools_scm 23 | docutils<0.18 24 | Sphinx 25 | pydata-sphinx-theme 26 | sphinx-gallery 27 | sphinxemoji 28 | numpydoc 29 | nbsphinx 30 | image 31 | pre-commit 32 | autopep8 33 | isort 34 | black 35 | flake8 36 | mypy 37 | pytensor 38 | pymc 39 | -------------------------------------------------------------------------------- /requirements-torch.txt: -------------------------------------------------------------------------------- 1 | --index-url https://download.pytorch.org/whl/cpu 2 | torch>=1.2.0,<2.5 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | . 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [tool:pytest] 5 | addopts = --verbose 6 | python_files = pytests/*.py 7 | 8 | [flake8] 9 | ignore = E203, E501, W503, E402 10 | per-file-ignores = 11 | __init__.py: F401, F403, F405 12 | max-line-length = 88 13 | 14 | # mypy global options 15 | [mypy] 16 | plugins = numpy.typing.mypy_plugin 17 | ignore_errors = False 18 | allow_redefinition = True 19 | 20 | # mypy per-module options 21 | [mypy-IPython.*] 22 | ignore_missing_imports = True 23 | 24 | [mypy-scipy.*] 25 | ignore_missing_imports = True 26 | 27 | [mypy-numba.*] 28 | ignore_missing_imports = True 29 | 30 | [mypy-pyfftw.*] 31 | ignore_missing_imports = True 32 | 33 | [mypy-pywt.*] 34 | ignore_missing_imports = True 35 | 36 | [mypy-spgl1.*] 37 | ignore_missing_imports = True 38 | 39 | [mypy-skfmm.*] 40 | ignore_missing_imports = True 41 | 42 | [mypy-devito.*] 43 | ignore_missing_imports = True 44 | 45 | [mypy-examples.*] # devito-examples 46 | ignore_missing_imports = True 47 | 48 | [mypy-cupy.*] 49 | ignore_missing_imports = True 50 | -------------------------------------------------------------------------------- /testdata/avo/poststack_model.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/testdata/avo/poststack_model.npz -------------------------------------------------------------------------------- /testdata/deblending/mobil.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/testdata/deblending/mobil.npy -------------------------------------------------------------------------------- /testdata/marchenko/direct3D.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/testdata/marchenko/direct3D.npz -------------------------------------------------------------------------------- /testdata/marchenko/input.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/testdata/marchenko/input.npz -------------------------------------------------------------------------------- /testdata/optimization/shepp_logan_phantom.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/testdata/optimization/shepp_logan_phantom.npy -------------------------------------------------------------------------------- /testdata/python.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/testdata/python.npy -------------------------------------------------------------------------------- /testdata/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/testdata/python.png -------------------------------------------------------------------------------- /testdata/sigmoid.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/testdata/sigmoid.npz -------------------------------------------------------------------------------- /testdata/slope_estimate/concentric.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/testdata/slope_estimate/concentric.png -------------------------------------------------------------------------------- /testdata/slope_estimate/concentric_angles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/testdata/slope_estimate/concentric_angles.png -------------------------------------------------------------------------------- /testdata/slope_estimate/core_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/testdata/slope_estimate/core_sample.png -------------------------------------------------------------------------------- /testdata/slope_estimate/core_sample_anisotropy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/testdata/slope_estimate/core_sample_anisotropy.png -------------------------------------------------------------------------------- /testdata/slope_estimate/core_sample_orientation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/testdata/slope_estimate/core_sample_orientation.png -------------------------------------------------------------------------------- /testdata/updown/input.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PyLops/pylops/f94d55062c5475badb124cec1b03f12612834f7e/testdata/updown/input.npz -------------------------------------------------------------------------------- /tutorials/README.txt: -------------------------------------------------------------------------------- 1 | .. _tutorials: 2 | 3 | Tutorials 4 | --------- 5 | -------------------------------------------------------------------------------- /tutorials/realcomplex.py: -------------------------------------------------------------------------------- 1 | r""" 2 | 17. Real/Complex Inversion 3 | ========================== 4 | In this tutorial we will discuss two equivalent approaches to the solution 5 | of inverse problems with real-valued model vector and complex-valued data vector. 6 | In other words, we consider a modelling operator 7 | :math:`\mathbf{A}:\mathbb{F}^m \to \mathbb{C}^n` (which could be the case 8 | for example for the real FFT). 9 | 10 | Mathematically speaking, this problem can be solved equivalently by inverting 11 | the complex-valued problem: 12 | 13 | .. math:: 14 | \mathbf{y} = \mathbf{A} \mathbf{x} 15 | 16 | or the real-valued augmented system 17 | 18 | .. math:: 19 | \DeclareMathOperator{\Real}{Re} 20 | \DeclareMathOperator{\Imag}{Im} 21 | \begin{bmatrix} 22 | \Real(\mathbf{y}) \\ 23 | \Imag(\mathbf{y}) 24 | \end{bmatrix} = 25 | \begin{bmatrix} 26 | \Real(\mathbf{A}) \\ 27 | \Imag(\mathbf{A}) 28 | \end{bmatrix} \mathbf{x} 29 | 30 | Whilst we already know how to solve the first problem, let's see how we can 31 | solve the second one by taking advantage of the ``real`` method of the 32 | :class:`pylops.LinearOperator` object. We will also wrap our linear operator 33 | into a :class:`pylops.MemoizeOperator` which remembers the last N model and 34 | data vectors and by-passes the computation of the forward and/or adjoint pass 35 | whenever the same pair reappears. This is very useful in our case when we 36 | want to compute the real and the imag components of 37 | 38 | """ 39 | import matplotlib.pyplot as plt 40 | import numpy as np 41 | 42 | import pylops 43 | 44 | plt.close("all") 45 | np.random.seed(0) 46 | 47 | ############################################################################### 48 | # To start we create the forward problem 49 | 50 | n = 5 51 | x = np.arange(n) + 1.0 52 | 53 | # make A 54 | Ar = np.random.normal(0, 1, (n, n)) 55 | Ai = np.random.normal(0, 1, (n, n)) 56 | A = Ar + 1j * Ai 57 | Aop = pylops.MatrixMult(A, dtype=np.complex128) 58 | y = Aop @ x 59 | 60 | ############################################################################### 61 | # Let's check we can solve this problem using the first formulation 62 | A1op = Aop.toreal(forw=False, adj=True) 63 | xinv = A1op.div(y) 64 | 65 | print(f"xinv={xinv}\n") 66 | 67 | ############################################################################### 68 | # Let's now see how we formulate the second problem 69 | Amop = pylops.MemoizeOperator(Aop, max_neval=10) 70 | Arop = Amop.toreal() 71 | Aiop = Amop.toimag() 72 | 73 | A1op = pylops.VStack([Arop, Aiop]) 74 | y1 = np.concatenate([np.real(y), np.imag(y)]) 75 | xinv1 = np.real(A1op.div(y1)) 76 | 77 | print(f"xinv1={xinv1}\n") 78 | --------------------------------------------------------------------------------