├── .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 |
33 | {% for text, link in menu_links %}
34 | - {{ text }}
35 | {% endfor %}
36 |
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 |
10 | -
11 | Ravasi, M., and I. Vasconcelos,
12 | 2020,
13 | PyLops—A linear-operator Python library for scalable algebra and optimization.
14 | SoftwareX, 11, 100361.
15 | doi: 10.1016/j.softx.2019.100361
16 |
17 |
18 |
19 |
20 |
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 |
--------------------------------------------------------------------------------