Overview¶
PyLops is an open-source Python library focused on providing a backend-agnostic, idiomatic, matrix-free library of linear operators and related computations. It is inspired by the iconic MATLAB Spot – A Linear-Operator Toolbox project.
Linear operators and inverse problems are at the core of many of the most used algorithms in signal processing, image processing, and remote sensing. For small-scale problems, matrices can be explicitly computed and manipulated with Python numerical scientific libraries such as NumPy and SciPy.
Large-scale problems often feature matrices that are prohibitive in size—but whose operations can be described by simple functions. PyLops operators exploit this to represent a linear operator not as array of numbers, but by functions which describe matrix-vector products in forward and adjoint modes. Moreover, many iterative methods (e.g. cg, lsqr) are designed to not rely on the elements of the matrix, only these matrix-vector products. PyLops offers such solvers for many different types of problems, in particular least-squares and sparsity-promoting inversions.
Get started by installing PyLops and following our quick tour.
Terminology¶
A common terminology is used within the entire documentation of PyLops. Every linear operator and its application to a model will be referred to as forward model (or operation)
while its application to a data is referred to as adjoint model (or operation)
where \(\mathbf{x}\) is called model and \(\mathbf{y}\) is called data. The operator \(\mathbf{A}:\mathbb{F}^m \to \mathbb{F}^n\) effectively maps a vector of size \(m\) in the model space to a vector of size \(n\) in the data space, conversely the adjoint operator \(\mathbf{A}^H:\mathbb{F}^n \to \mathbb{F}^m\) maps a vector of size \(n\) in the data space to a vector of size \(m\) in the model space. As linear operators mimics the effect a matrix on a vector we can also loosely refer to \(m\) as the number of columns and \(n\) as the number of rows of the operator.
Ultimately, solving an inverse problems accounts to removing the effect of \(\mathbf{A}\) from the data \(\mathbf{y}\) to retrieve the model \(\mathbf{x}\).
For a more detailed description of the concepts of linear operators, adjoints and inverse problems in general, you can head over to one of Jon Claerbout’s books such as Basic Earth Imaging.
Implementation¶
PyLops is build on top of the scipy class scipy.sparse.linalg.LinearOperator
.
This class allows in fact for the creation of objects (or interfaces) for matrix-vector and matrix-matrix products that can ultimately be used to solve any inverse problem of the form \(\mathbf{y}=\mathbf{A}\mathbf{x}\).
As explained in the scipy LinearOperator
official documentation, to construct a scipy.sparse.linalg.LinearOperator
, a user is required to pass appropriate callables
to the constructor of this class, or subclass it. More specifically one of the methods _matvec
and _matmat
must be implemented for
the forward operator and one of the methods _rmatvec
or _adjoint
may be implemented to apply the Hermitian adjoint.
The attributes/properties shape
(pair of integers) and dtype
(may be None) must also be provided during __init__
of this class.
Any linear operator developed within the PyLops library follows this philosophy. As explained more in details in Implementing new operators section,
a linear operator is created by subclassing the scipy.sparse.linalg.LinearOperator
class and _matvec
and _rmatvec
are implemented.
History¶
PyLops was initially written by Equinor It is a flexible and scalable python library for large-scale optimization with linear operators that can be tailored to our needs, and as contribution to the free software community. Since June 2021, PyLops is a NUMFOCUS Affiliated Project.
Installation¶
Dependencies¶
The PyLops project strives to create a library that is easy to install in any environment and has a very limited number of dependencies. Required dependencies are limited to:
We highly encourage using the Anaconda Python distribution or its standalone package manager Conda. Especially for Intel processors, this ensures a higher performance with no configuration. If you are interested in getting the best code performance, read carefully Advanced installation. For learning, however, the standard installation is often good enough.
Some operators have additional, optional “engines” to improve their performance. These often rely on third-party libraries which are added to the list of our optional dependencies. Optional dependencies therefore refer to those dependencies that are not strictly needed nor installed directly as part of a standard installation. For details more details, see Optional dependencies.
Step-by-step installation for users¶
Conda (recommended)¶
If using conda
, install our conda-forge
distribution via:
>> conda install --channel conda-forge pylops
Using the conda-forge
distribution is recommended as all the dependencies (both required
and optional) will be automatically installed for you.
Pip¶
If you are using pip
, and simply type the following command in your terminal
to install the PyPI distribution:
>> pip install pylops
Note that when installing via pip
, only required dependencies are installed.
Docker¶
If you want to try PyLops but do not have Python in your local machine, you can use our Docker image instead.
After installing Docker in your computer, type the following command in your terminal (note that this will take some time the first time you type it as you will download and install the Docker image):
>> docker run -it -v /path/to/local/folder:/home/jupyter/notebook -p 8888:8888 mrava87/pylops:notebook
This will give you an address that you can put in your browser and will open a Jupyter notebook environment with PyLops
and other basic Python libraries installed. Here, /path/to/local/folder
is the absolute path of a local folder
on your computer where you will create a notebook (or containing notebooks that you want to continue working on). Note that
anything you do to the notebook(s) will be saved in your local folder.
A larger image with conda
a distribution is also available:
>> docker run -it -v /path/to/local/folder:/home/jupyter/notebook -p 8888:8888 mrava87/pylops:conda_notebook
Step-by-step installation for developers¶
Fork PyLops¶
Fork the PyLops repository and clone it by executing the following in your terminal:
>> git clone https://github.com/YOUR-USERNAME/pylops.git
We recommend installing dependencies into a separate environment. For that end, we provide a Makefile with useful commands for setting up the environment.
Install dependencies¶
Conda (recommended)¶
For a conda
environment, run
>> make dev-install_conda
This will create and activate an environment called pylops
, with all required and optional dependencies.
Pip¶
If you prefer a pip
installation, we provide the following command
>> make dev-install
Note that, differently from the conda
command, the above will not create a virtual environment.
Make sure you create and activate your environment previously.
Run tests¶
To ensure that everything has been setup correctly, run tests:
>> make tests
Make sure no tests fail, this guarantees that the installation has been successful.
Add remote (optional)¶
To keep up-to-date on the latest changes while you are developing, you may optionally add the PyLops repository as a remote. Run the following command to add the PyLops repo as a remote named upstream:
>> git remote add upstream https://github.com/PyLops/pylops
From then on, you can pull changes (for example, in the dev branch) with:
>> git pull upstream dev
Install pre-commit hooks¶
To ensure consistency in the coding style of our developers we rely on
pre-commit to perform a series of checks when you are
ready to commit and push some changes. This is accomplished by means of git hooks
that have been configured in the .pre-commit-config.yaml
file.
In order to setup such hooks in your local repository, run:
>> pre-commit install
Once this is set up, when committing changes, pre-commit
will reject and “fix” your code by running the proper hooks.
At this point, the user must check the changes and then stage them before trying to commit again.
Final steps¶
PyLops does not enforce the use of a linter as a pre-commit hook, but we do highly encourage using one before submitting a Pull Request.
A properly configured linter (flake8
) can be run with:
>> make lint
In addition, it is highly encouraged to build the docs prior to submitting a Pull Request. Apart from ensuring that docstrings are properly formatted, they can aid in catching bugs during development. Build (or update) the docs with:
>> make doc
or
>> make docupdate
Advanced installation¶
In this section we discuss some important details regarding code performance when using PyLops.
To get the most out of PyLops operators in terms of speed you will need to follow these guidelines as much as possible or ensure that the Python libraries used by PyLops are efficiently installed in your system.
BLAS¶
PyLops relies on the NumPy and SciPy, and being able to link these to the most performant BLAS library will ensure optimal performance of PyLops when using only required dependencies.
We strongly encourage using the Anaconda Python distribution as
NumPy and SciPy will, when available, be automatically linked to Intel MKL, the most performant library for basic linear algebra
operations to date (see Markus Beuckelmann’s benchmarks).
The PyPI version installed with pip
, however, will default to OpenBLAS.
For more information, see NumPy’s section on BLAS.
To check which BLAS NumPy and SciPy were compiled against, run the following commands in a Python interpreter:
import numpy as np
import scipy as sp
print(np.__config__.show())
print(sp.__config__.show())
Intel also provides NumPy and SciPy replacement packages in PyPI intel-numpy
and intel-scipy
, respectively, which link to Intel MKL.
These are an option for an environment without conda
that needs Intel MKL without requiring manual compilation.
Warning
intel-numpy
and intel-scipy
not only link against Intel MKL, but also substitute NumPy and
SciPy FFTs for Intel MKL FFT. MKL FFT is not supported
and may break PyLops.
Multithreading¶
It is important to ensure that your environment variable which sets threads is correctly assigned to the maximum number of cores you would like to use in your code. Multiprocessing parallelism in NumPy and SciPy can be controlled in different ways depending on where it comes from.
Environment variable |
Library |
---|---|
OMP_NUM_THREADS |
|
NUMEXPR_NUM_THREADS |
|
OPENBLAS_NUM_THREADS |
|
MKL_NUM_THREADS |
|
VECLIB_MAXIMUM_THREADS |
For example, try setting one processor to be used with (if using OpenBlas)
>> export OMP_NUM_THREADS=1
>> export NUMEXPR_NUM_THREADS=1
>> export OPENBLAS_NUM_THREADS=1
and run the following code in Python:
import os
import numpy as np
from timeit import timeit
size = 1024
A = np.random.random((size, size)),
B = np.random.random((size, size))
print("Time with %s threads: %f s" \
%(os.environ.get("OMP_NUM_THREADS"),
timeit(lambda: np.dot(A, B), number=4)))
Subsequently set the environment variables to 2
or any higher number of threads available
in your hardware (multi-threaded), and run the same code.
By looking at both the load on your processors (e.g., using top
), and at the
Python print statement you should see a speed-up in the second case.
Alternatively, you could set the OMP_NUM_THREADS
variable directly
inside your script using os.environ["OMP_NUM_THREADS"]="2"
, but ensure that
this is done before loading NumPy.
Note
Always remember to set OMP_NUM_THREADS
and other relevant variables
in your environment when using PyLops
Optional dependencies¶
To avoid increasing the number of required dependencies, which may lead to conflicts with other libraries that you have in your system, we have decided to build some of the additional features of PyLops in such a way that if an optional dependency is not present in your Python environment, a safe fallback to one of the required dependencies will be enforced.
When available in your system, we recommend using the Conda package manager and install all the required and optional dependencies of PyLops at once using the command:
>> conda install --channel conda-forge pylops
in this case all dependencies will be installed from their Conda distributions.
Alternatively, from version 1.4.0
optional dependencies can also be installed as
part of the pip installation via:
>> pip install pylops[advanced]
Dependencies are however installed from their PyPI wheels. An exception is however represented by CuPy. This library is not installed automatically. Users interested to accelerate their computations with the aid of GPUs should install it prior to installing PyLops as described in Optional Dependencies for GPU.
Note
If you are a developer, all the optional dependencies below (except GPU) can
be installed automatically by cloning the repository and installing
PyLops via make dev-install_conda
(conda
) or make dev-install
(pip
).
In alphabetic order:
Devito¶
Devito is library used to solve PDEs via
the finite-difference method. It is used in PyLops to compute wavefields
pylops.waveeqprocessing.AcousticWave2D
Install it via pip
with
>> pip install devito
FFTW¶
Three different “engines” are provided by the pylops.signalprocessing.FFT
operator:
engine="numpy"
(default), engine="scipy"
and engine="fftw"
.
The first two engines are part of the required PyLops dependencies.
The latter implements the well-known FFTW
via the Python wrapper pyfftw.FFTW
. While this optimized FFT tends to
outperform the other two in many cases, it is not included by default.
To use this library, install it manually either via conda
:
>> conda install --channel conda-forge pyfftw
or via pip:
>> pip install pyfftw
Note
FFTW is only available for pylops.signalprocessing.FFT
,
not pylops.signalprocessing.FFT2D
or pylops.signalprocessing.FFTND
.
Warning
Intel MKL FFT is not supported.
Numba¶
Although we always strive to write code for forward and adjoint operators that takes advantage of the perks of NumPy and SciPy (e.g., broadcasting, ufunc), in some case we may end up using for loops that may lead to poor performance. In those cases we may decide to implement alternative (optional) back-ends in Numba, a Just-In-Time compiler that translates a subset of Python and NumPy code into fast machine code.
A user can simply switch from the native,
always available implementation to the Numba implementation by simply providing the following
additional input parameter to the operator engine="numba"
. This is for example the case in the
pylops.signalprocessing.Radon2D
.
If interested to use Numba backend from conda
, you will need to manually install it:
>> conda install numba
It is also advised to install the additional package icc_rt to use optimised transcendental functions as compiler intrinsics.
>> conda install --channel numba icc_rt
Through pip
the equivalent would be:
>> pip install numba
>> pip install icc_rt
However, it is important to note that icc_rt
will only be identified by Numba if
LD_LIBRARY_PATH
is properly set.
If you are using a virtual environment, you can ensure this with:
>> export LD_LIBRARY_PATH=/path/to/venv/lib/:$LD_LIBRARY_PATH
To ensure that icc_rt
is being recognized, run
>> numba -s | grep SVML
__SVML Information__
SVML State, config.USING_SVML : True
SVML Library Loaded : True
llvmlite Using SVML Patched LLVM : True
SVML Operational : True
Numba also offers threading parallelism through a variety of Threading Layers.
You may need to set the environment variable NUMBA_NUM_THREADS
define how many threads to use out of the available ones (numba -s | grep "CPU Count"
).
It can also be checked dynamically with numba.config.NUMBA_DEFAULT_NUM_THREADS
.
PyWavelets¶
PyWavelets is used to implement the wavelet operators.
Install it via conda
with:
>> conda install pywavelets
or via pip
with
>> pip install PyWavelets
scikit-fmm¶
scikit-fmm is a library which implements the
fast marching method. It is used in PyLops to compute traveltime tables in the
initialization of pylops.waveeqprocessing.Kirchhoff
when choosing mode="eikonal"
. As this may not be of interest for many users, this library has not been added
to the mandatory requirements of PyLops. With conda
, install it via
>> conda install --channel conda-forge scikit-fmm
or with pip
via
>> pip install scikit-fmm
SPGL1¶
SPGL1 is used to solve sparsity-promoting
basis pursuit, basis pursuit denoise, and Lasso problems
in pylops.optimization.sparsity.SPGL1
solver.
Install it via pip
with:
>> pip install spgl1
Sympy¶
This library is used to implement the describe
method, which transforms
PyLops operators into their mathematical expression.
Install it via conda
with:
>> conda install sympy
or via pip
with
>> pip install sympy
Torch¶
Torch used to allow seamless integration between PyLops and PyTorch operators.
Install it via conda
with:
>> conda install -c pytorch pytorch
or via pip
with
>> pip install torch
Optional Dependencies for GPU¶
PyLops will automatically check if the libraries below are installed and, in that case, use them any time the input vector passed to an operator is of compatible type. Users can, however, disable this option. For more details of GPU-accelerated PyLops read GPU Support.
CuPy¶
CuPy is a library used as a drop-in replacement to NumPy for GPU-accelerated computations. Since many different versions of CuPy exist (based on the CUDA drivers of the GPU), users must install CuPy prior to installing PyLops. To do so, follow their installation instructions.
cuSignal¶
cuSignal is a library is used as a drop-in replacement to SciPy Signal for GPU-accelerated computations. Similar to CuPy, users must install cuSignal prior to installing PyLops. To do so, follow their installation instructions.
GPU Support¶
Overview¶
From v1.12.0
, PyLops supports computations on GPUs powered by
CuPy (cupy-cudaXX>=8.1.0
) and cuSignal (cusignal>=0.16.0
).
They must be installed before PyLops is installed.
Note
Set environment variables CUPY_PYLOPS=0
and/or CUSIGNAL_PYLOPS=0
to force PyLops to ignore
cupy
and cusignal
backends.
This can be also used if a previous version of cupy
or cusignal
is installed in your system, otherwise you will get an error when importing PyLops.
Apart from a few exceptions, all operators and solvers in PyLops can
seamlessly work with numpy
arrays on CPU as well as with cupy
arrays
on GPU. Users do simply need to consistently create operators and
provide data vectors to the solvers, e.g., when using
pylops.MatrixMult
the input matrix must be a
cupy
array if the data provided to a solver is also cupy
array.
Warning
Some pylops.LinearOperator
methods are currently on GPU:
pylops.LinearOperator.eigs
pylops.LinearOperator.cond
pylops.LinearOperator.tosparse
pylops.LinearOperator.estimate_spectral_norm
Warning
Some operators are currently not available on GPU:
Warning
Some solvers are currently not available on GPU:
pylops.optimization.sparsity.SPGL1
Example¶
Finally, let’s briefly look at an example. First we write a code snippet using
numpy
arrays which PyLops will run on your CPU:
ny, nx = 400, 400
G = np.random.normal(0, 1, (ny, nx)).astype(np.float32)
x = np.ones(nx, dtype=np.float32)
Gop = MatrixMult(G, dtype='float32')
y = Gop * x
xest = Gop / y
Now we write a code snippet using cupy
arrays which PyLops will run on
your GPU:
ny, nx = 400, 400
G = cp.random.normal(0, 1, (ny, nx)).astype(np.float32)
x = cp.ones(nx, dtype=np.float32)
Gop = MatrixMult(G, dtype='float32')
y = Gop * x
xest = Gop / y
The code is almost unchanged apart from the fact that we now use cupy
arrays,
PyLops will figure this out!
Note
The CuPy backend is in active development, with many examples not yet in the docs. You can find many other examples from the PyLops Notebooks repository.
Extensions¶
PyLops brings to users the power of linear operators in a simple and easy to use programming interface.
While very powerful on its own, this library is further extended to take advantage of more advanced computational resources, either in terms of multiple-node clusters or GPUs. Moreover, some independent libraries are created to use third party software that cannot be included as dependencies to our main library for licensing issues but may be useful for academic purposes.
Spin-off projects that aim at extending the capabilities of PyLops are:
PyLops-GPU : PyLops for GPU arrays (incorporated into PyLops).
PyLops-Distributed: PyLops for distributed systems with many computing nodes.
PyProximal: Proximal solvers which integrate with PyLops Linear Operators.
Curvelops: Python wrapper for the Curvelab 2D and 3D digital curvelet transforms.
Tutorials¶
Note
Click here to download the full example code
01. The LinearOpeator¶
This first tutorial is aimed at easing the use of the PyLops library for both new users and developers.
Since PyLops heavily relies on the use of the
scipy.sparse.linalg.LinearOperator
class of SciPy, we will start
by looking at how to initialize a linear operator as well as
different ways to apply the forward and adjoint operations. Finally we will
investigate various special methods, also called magic methods
(i.e., methods with the double underscores at the beginning and the end) that
have been implemented for such a class and will allow summing, subtractring,
chaining, etc. multiple operators in very easy and expressive way.
Let’s start by defining a simple operator that applies element-wise
multiplication of the model with a vector d
in forward mode and
element-wise multiplication of the data with the same vector d
in
adjoint mode. This operator is present in PyLops under the
name of pylops.Diagonal
and
its implementation is discussed in more details in the Implementing new operators
page.
import timeit
import matplotlib.pyplot as plt
import numpy as np
import pylops
n = 10
d = np.arange(n) + 1.0
x = np.ones(n)
Dop = pylops.Diagonal(d)
First of all we apply the operator in the forward mode. This can be done in four different ways:
_matvec
: directly applies the method implemented for forward modematvec
: performs some checks before and after applying_matvec
*
: operator used to map the special method__matmul__
which checks whether the inputx
is a vector or matrix and applies_matvec
or_matmul
accordingly.@
: operator used to map the special method__mul__
which performs like the*
opetator
We will time these 4 different executions and see how using _matvec
(or matvec
) will result in the faster computation. It is thus advised to
use *
(or @
) in examples when expressivity has priority but prefer
_matvec
(or matvec
) for efficient implementations.
# setup command
cmd_setup = """\
import numpy as np
import pylops
n = 10
d = np.arange(n) + 1.
x = np.ones(n)
Dop = pylops.Diagonal(d)
DopH = Dop.H
"""
# _matvec
cmd1 = "Dop._matvec(x)"
# matvec
cmd2 = "Dop.matvec(x)"
# @
cmd3 = "Dop@x"
# *
cmd4 = "Dop*x"
# timing
t1 = 1.0e3 * np.array(timeit.repeat(cmd1, setup=cmd_setup, number=500, repeat=5))
t2 = 1.0e3 * np.array(timeit.repeat(cmd2, setup=cmd_setup, number=500, repeat=5))
t3 = 1.0e3 * np.array(timeit.repeat(cmd3, setup=cmd_setup, number=500, repeat=5))
t4 = 1.0e3 * np.array(timeit.repeat(cmd4, setup=cmd_setup, number=500, repeat=5))
plt.figure(figsize=(7, 2))
plt.plot(t1, "k", label=" _matvec")
plt.plot(t2, "r", label="matvec")
plt.plot(t3, "g", label="@")
plt.plot(t4, "b", label="*")
plt.axis("tight")
plt.legend()
plt.tight_layout()

Similarly we now consider the adjoint mode. This can be done in three different ways:
_rmatvec
: directly applies the method implemented for adjoint modermatvec
: performs some checks before and after applying_rmatvec
.H*
: first applies the adjoint.H
which creates a new scipy.sparse.linalg._CustomLinearOperator` where_matvec
and_rmatvec
are swapped and then applies the new_matvec
.
Once again, after timing these 3 different executions we can see
see how using _rmatvec
(or rmatvec
) will result in the faster
computation while .H*
is very unefficient and slow. Note that if the
adjoint has to be applied multiple times it is at least advised to create
the adjoint operator by applying .H
only once upfront.
Not surprisingly, the linear solvers in scipy as well as in PyLops
actually use matvec
and rmatvec
when dealing with linear operators.
# _rmatvec
cmd1 = "Dop._rmatvec(x)"
# rmatvec
cmd2 = "Dop.rmatvec(x)"
# .H* (pre-computed H)
cmd3 = "DopH*x"
# .H*
cmd4 = "Dop.H*x"
# timing
t1 = 1.0e3 * np.array(timeit.repeat(cmd1, setup=cmd_setup, number=500, repeat=5))
t2 = 1.0e3 * np.array(timeit.repeat(cmd2, setup=cmd_setup, number=500, repeat=5))
t3 = 1.0e3 * np.array(timeit.repeat(cmd3, setup=cmd_setup, number=500, repeat=5))
t4 = 1.0e3 * np.array(timeit.repeat(cmd4, setup=cmd_setup, number=500, repeat=5))
plt.figure(figsize=(7, 2))
plt.plot(t1, "k", label=" _rmatvec")
plt.plot(t2, "r", label="rmatvec")
plt.plot(t3, "g", label=".H* (pre-computed H)")
plt.plot(t4, "b", label=".H*")
plt.axis("tight")
plt.legend()
plt.tight_layout()

Just to reiterate once again, it is advised to call matvec
and rmatvec
unless PyLops linear operators are used for
teaching purposes.
We now go through some other methods and special methods that
are implemented in scipy.sparse.linalg.LinearOperator
(and
pylops.LinearOperator
):
Op1+Op2
: maps the special method__add__
and performs summation between two operators and returns apylops.LinearOperator
-Op
: maps the special method__neg__
and performs negation of an operators and returns apylops.LinearOperator
Op1-Op2
: maps the special method__sub__
and performs summation between two operators and returns apylops.LinearOperator
Op1**N
: maps the special method__pow__
and performs exponentiation of an operator and returns apylops.LinearOperator
Op/y
(andOp.div(y)
): maps the special method__truediv__
and performs inversion of an operatorOp.eigs()
: estimates the eigenvalues of the operatorOp.cond()
: estimates the condition number of the operatorOp.conj()
: create complex conjugate operator
Dop = pylops.Diagonal(d)
# +
print(Dop + Dop)
# -
print(-Dop)
print(Dop - 0.5 * Dop)
# **
print(Dop**3)
# * and /
y = Dop * x
print(Dop / y)
# eigs
print(Dop.eigs(neigs=3))
# cond
print(Dop.cond())
# conj
print(Dop.conj())
<10x10 LinearOperator with dtype=float64>
<10x10 LinearOperator with dtype=float64>
<10x10 LinearOperator with dtype=float64>
<10x10 LinearOperator with dtype=float64>
[1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
[10.+0.j 9.+0.j 8.+0.j]
(10.00000000000003+0j)
<10x10 _ConjLinearOperator with dtype=float64>
To understand the effect of conj
we need to look into a problem with an
operator in the complex domain. Let’s create again our
pylops.Diagonal
operator but this time we populate it with
complex numbers. We will see that the action of the operator and its complex
conjugate is different even if the model is real.
n = 5
d = 1j * (np.arange(n) + 1.0)
x = np.ones(n)
Dop = pylops.Diagonal(d)
print(f"y = Dx = {Dop * x}")
print(f"y = conj(D)x = {Dop.conj() * x}")
y = Dx = [0.+1.j 0.+2.j 0.+3.j 0.+4.j 0.+5.j]
y = conj(D)x = [0.-1.j 0.-2.j 0.-3.j 0.-4.j 0.-5.j]
At this point, the concept of linear operator may sound abstract.
The convinience method pylops.LinearOperator.todense
can be used to
create the equivalent dense matrix of any operator. In this case for example
we expect to see a diagonal matrix with d
values along the main diagonal
D = Dop.todense()
plt.figure(figsize=(5, 5))
plt.imshow(np.abs(D))
plt.title("Dense representation of Diagonal operator")
plt.axis("tight")
plt.colorbar()
plt.tight_layout()

At this point it is worth reiterating that if two linear operators are
combined by means of the algebraical operations shown above, the resulting
operator is still a pylops.LinearOperator
operator. This means
that we can still apply any of the methods implemented in the original
scipy class definition like *
, as well as those in our class
definition like /
Dop1 = Dop - Dop.conj()
y = Dop1 * x
print(f"x = (Dop - conj(Dop))/y = {Dop1 / y}")
D1 = Dop1.todense()
plt.figure(figsize=(5, 5))
plt.imshow(np.abs(D1))
plt.title(r"Dense representation of $|D - D^*|$")
plt.axis("tight")
plt.colorbar()
plt.tight_layout()

x = (Dop - conj(Dop))/y = [1.+0.j 1.+0.j 1.+0.j 1.+0.j 1.+0.j]
Finally, another important feature of PyLops linear operators is that we can always keep track of how many times the forward and adjoint passes have been applied (and reset when needed). This is particularly useful when running a third party solver to see how many evaluations of our operator are performed inside the solver.
Dop = pylops.Diagonal(d)
y = Dop.matvec(x)
y = Dop.matvec(x)
y = Dop.rmatvec(y)
print(f"Forward evaluations: {Dop.matvec_count}")
print(f"Adjoint evaluations: {Dop.rmatvec_count}")
# Reset
Dop.reset_count()
print(f"Forward evaluations: {Dop.matvec_count}")
print(f"Adjoint evaluations: {Dop.rmatvec_count}")
Forward evaluations: 2
Adjoint evaluations: 1
Forward evaluations: 0
Adjoint evaluations: 0
This first tutorial is completed. You have seen the basic operations that
can be performed using scipy.sparse.linalg.LinearOperator
and
our overload of such a class pylops.LinearOperator
and you
should be able to get started combining various PyLops operators and
solving your own inverse problems.
Total running time of the script: ( 0 minutes 1.049 seconds)
Note
Click here to download the full example code
02. The Dot-Test¶
One of the most important aspect of writing a Linear operator is to be able
to verify that the code implemented in forward mode and the code implemented
in adjoint mode are effectively adjoint to each other. If this is the case,
your Linear operator will successfully pass the so-called dot-test.
Refer to the Notes section of pylops.utils.dottest
)
for a more detailed description.
In this example, I will show you how to use the dot-test for a variety of operator when model and data are either real or complex numbers.
import matplotlib.gridspec as pltgs
import matplotlib.pyplot as plt
# pylint: disable=C0103
import numpy as np
import pylops
from pylops.utils import dottest
plt.close("all")
Let’s start with something very simple. We will make a pylops.MatrixMult
operator and verify that its implementation passes the dot-test.
For this time, we will do this step-by-step, replicating what happens in the
pylops.utils.dottest
routine.
N, M = 5, 3
Mat = np.arange(N * M).reshape(N, M)
Op = pylops.MatrixMult(Mat)
v = np.random.randn(N)
u = np.random.randn(M)
# Op * u
y = Op.matvec(u)
# Op'* v
x = Op.rmatvec(v)
yy = np.dot(y, v) # (Op * u)' * v
xx = np.dot(u, x) # u' * (Op' * v)
print(f"Dot-test {np.abs((yy - xx) / ((yy + xx + 1e-15) / 2)):.2e}")
Dot-test 1.37e-16
And here is a visual intepretation of what a dot-test is
gs = pltgs.GridSpec(1, 9)
fig = plt.figure(figsize=(7, 3))
ax = plt.subplot(gs[0, 0:2])
ax.imshow(Op.A, cmap="rainbow")
ax.set_title(r"$(Op*$", size=20, fontweight="bold")
ax.set_xticks(np.arange(M - 1) + 0.5)
ax.set_yticks(np.arange(N - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax.axis("tight")
ax = plt.subplot(gs[0, 2])
ax.imshow(u[:, np.newaxis], cmap="rainbow")
ax.set_title(r"$u)^T$", size=20, fontweight="bold")
ax.set_xticks([])
ax.set_yticks(np.arange(M - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax.axis("tight")
ax = plt.subplot(gs[0, 3])
ax.imshow(v[:, np.newaxis], cmap="rainbow")
ax.set_title(r"$v$", size=20, fontweight="bold")
ax.set_xticks([])
ax.set_yticks(np.arange(N - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax = plt.subplot(gs[0, 4])
ax.text(
0.35,
0.5,
"=",
horizontalalignment="center",
verticalalignment="center",
size=40,
fontweight="bold",
)
ax.axis("off")
ax = plt.subplot(gs[0, 5])
ax.imshow(u[:, np.newaxis].T, cmap="rainbow")
ax.set_title(r"$u^T$", size=20, fontweight="bold")
ax.set_xticks(np.arange(M - 1) + 0.5)
ax.set_yticks([])
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax = plt.subplot(gs[0, 6:8])
ax.imshow(Op.A.T, cmap="rainbow")
ax.set_title(r"$(Op^T*$", size=20, fontweight="bold")
ax.set_xticks(np.arange(N - 1) + 0.5)
ax.set_yticks(np.arange(M - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax.axis("tight")
ax = plt.subplot(gs[0, 8])
ax.imshow(v[:, np.newaxis], cmap="rainbow")
ax.set_title(r"$v)$", size=20, fontweight="bold")
ax.set_xticks([])
ax.set_yticks(np.arange(N - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
plt.tight_layout()

From now on, we can simply use the pylops.utils.dottest
implementation
of the dot-test and pass the operator we would like to validate,
its size in the model and data spaces and optionally the tolerance we will be
accepting for the dot-test to be considered succesfull. Finally we need to
specify if our data or/and model vectors contain complex numbers using the
complexflag
parameter. While the dot-test will return True
when
succesfull and False
otherwise, we can also ask to print its outcome putting the
verb
parameters to True
.
N = 10
d = np.arange(N)
Dop = pylops.Diagonal(d)
_ = dottest(Dop, N, N, rtol=1e-6, complexflag=0, verb=True)
Dot test passed, v^H(Opu)=-6.91965436475742 - u^H(Op^Hv)=-6.91965436475742
We move now to a more complicated operator, the pylops.signalprocessing.FFT
operator. We use once again the pylops.utils.dottest
to verify its implementation
and since we are dealing with a transform that can be applied to both real and complex
array, we try different combinations using the complexflag
input.
dt = 0.005
nt = 100
nfft = 2**10
FFTop = pylops.signalprocessing.FFT(
dims=(nt,), nfft=nfft, sampling=dt, dtype=np.complex128
)
dottest(FFTop, nfft, nt, complexflag=2, verb=True)
_ = dottest(FFTop, nfft, nt, complexflag=3, verb=True)
Dot test passed, v^H(Opu)=(-17.13910350422714+3.8288339959112507j) - u^H(Op^Hv)=(-17.139103504227144+3.8288339959112503j)
Dot test passed, v^H(Opu)=(11.42080013111478-3.688151329246325j) - u^H(Op^Hv)=(11.420800131114781-3.6881513292463266j)
Total running time of the script: ( 0 minutes 0.266 seconds)
Note
Click here to download the full example code
03. Solvers¶
This tutorial will guide you through the pylops.optimization
module and show how to use various solvers that are included in the
PyLops library.
The main idea here is to provide the user of PyLops with very high-level functionalities to quickly and easily set up and solve complex systems of linear equations as well as include regularization and/or preconditioning terms (all of those constructed by means of PyLops linear operators).
To make this tutorial more interesting, we will present a real life problem and show how the choice of the solver and regularization/preconditioning terms is vital in many circumstances to successfully retrieve an estimate of the model. The problem that we are going to consider is generally referred to as the data reconstruction problem and aims at reconstructing a regularly sampled signal of size \(M\) from \(N\) randomly selected samples:
where the restriction operator \(\mathbf{R}\) that selects the \(M\)
elements from \(\mathbf{x}\) at random locations is implemented using
pylops.Restriction
, and
with \(M \gg N\).
import matplotlib.pyplot as plt
# pylint: disable=C0103
import numpy as np
import pylops
plt.close("all")
np.random.seed(10)
Let’s first create the data in the frequency domain. The data is composed by the superposition of 3 sinusoids with different frequencies.
# Signal creation in frequency domain
ifreqs = [41, 25, 66]
amps = [1.0, 1.0, 1.0]
N = 200
nfft = 2**11
dt = 0.004
t = np.arange(N) * dt
f = np.fft.rfftfreq(nfft, dt)
FFTop = 10 * pylops.signalprocessing.FFT(N, nfft=nfft, real=True)
X = np.zeros(nfft // 2 + 1, dtype="complex128")
X[ifreqs] = amps
x = FFTop.H * X
fig, axs = plt.subplots(2, 1, figsize=(12, 8))
axs[0].plot(f, np.abs(X), "k", lw=2)
axs[0].set_xlim(0, 30)
axs[0].set_title("Data(frequency domain)")
axs[1].plot(t, x, "k", lw=2)
axs[1].set_title("Data(time domain)")
axs[1].axis("tight")
plt.tight_layout()

We now define the locations at which the signal will be sampled.
# subsampling locations
perc_subsampling = 0.2
Nsub = int(np.round(N * perc_subsampling))
iava = np.sort(np.random.permutation(np.arange(N))[:Nsub])
# Create restriction operator
Rop = pylops.Restriction(N, iava, dtype="float64")
y = Rop * x
ymask = Rop.mask(x)
# Visualize data
fig = plt.figure(figsize=(12, 4))
plt.plot(t, x, "k", lw=3)
plt.plot(t, x, ".k", ms=20, label="all samples")
plt.plot(t, ymask, ".g", ms=15, label="available samples")
plt.legend()
plt.title("Data restriction")
plt.tight_layout()

To start let’s consider the simplest ‘solver’, i.e., least-square inversion without regularization. We aim here to minimize the following cost function:
\[J = \|\mathbf{y} - \mathbf{R} \mathbf{x}\|_2^2\]
Depending on the choice of the operator \(\mathbf{R}\), such problem can
be solved using explicit matrix solvers as well as iterative solvers. In
this case we will be using the latter approach
(more specifically the scipy implementation of the LSQR solver -
i.e., scipy.sparse.linalg.lsqr
) as we do not want to explicitly
create and invert a matrix. In most cases this will be the only viable
approach as most of the large-scale optimization problems that we are
interested to solve using PyLops do not lend naturally to the creation and
inversion of explicit matrices.
This first solver can be very easily implemented using the
/
for PyLops operators, which will automatically call the
scipy.sparse.linalg.lsqr
with some default parameters.
xinv = Rop / y
We can also use pylops.optimization.leastsquares.regularized_inversion
(without regularization term for now) and customize our solvers using
kwargs
.
xinv = pylops.optimization.leastsquares.regularized_inversion(
Rop, y, [], **dict(damp=0, iter_lim=10, show=True)
)[0]
RegularizedInversion
-----------------------------------------------------------------
The Operator Op has 40 rows and 200 cols
Regs=[]
epsRs=[]
-----------------------------------------------------------------
LSQR Least-squares solution of Ax = b
The matrix A has 40 rows and 200 columns
damp = 0.00000000000000e+00 calc_var = 0
atol = 1.00e-06 conlim = 1.00e+08
btol = 1.00e-06 iter_lim = 10
Itn x[0] r1norm r2norm Compatible LS Norm A Cond A
0 0.00000e+00 2.658e+00 2.658e+00 1.0e+00 3.8e-01
1 0.00000e+00 0.000e+00 0.000e+00 0.0e+00 0.0e+00 0.0e+00 0.0e+00
LSQR finished
Ax - b is small enough, given atol, btol
istop = 1 r1norm = 0.0e+00 anorm = 0.0e+00 arnorm = 0.0e+00
itn = 1 r2norm = 0.0e+00 acond = 0.0e+00 xnorm = 2.7e+00
Finally we can select a different starting guess from the null vector
xinv_fromx0 = pylops.optimization.leastsquares.regularized_inversion(
Rop, y, [], x0=np.ones(N), **dict(damp=0, iter_lim=10, show=True)
)[0]
RegularizedInversion
-----------------------------------------------------------------
The Operator Op has 40 rows and 200 cols
Regs=[]
epsRs=[]
-----------------------------------------------------------------
LSQR Least-squares solution of Ax = b
The matrix A has 40 rows and 200 columns
damp = 0.00000000000000e+00 calc_var = 0
atol = 1.00e-06 conlim = 1.00e+08
btol = 1.00e-06 iter_lim = 10
Itn x[0] r1norm r2norm Compatible LS Norm A Cond A
0 0.00000e+00 6.737e+00 6.737e+00 1.0e+00 1.5e-01
1 0.00000e+00 0.000e+00 0.000e+00 0.0e+00 0.0e+00 0.0e+00 0.0e+00
LSQR finished
Ax - b is small enough, given atol, btol
istop = 1 r1norm = 0.0e+00 anorm = 0.0e+00 arnorm = 0.0e+00
itn = 1 r2norm = 0.0e+00 acond = 0.0e+00 xnorm = 6.7e+00
The cost function above can be also expanded in terms of its normal equations
\[\mathbf{x}_{ne}= (\mathbf{R}^T \mathbf{R})^{-1} \mathbf{R}^T \mathbf{y}\]
The method pylops.optimization.leastsquares.normal_equations_inversion
implements such system of equations explicitly and solves them using an
iterative scheme suitable for square matrices (i.e., \(M=N\)).
While this approach may seem not very useful, we will soon see how regularization terms could be easily added to the normal equations using this method.
xne = pylops.optimization.leastsquares.normal_equations_inversion(Rop, y, [])[0]
Let’s now visualize the different inversion results
fig = plt.figure(figsize=(12, 4))
plt.plot(t, x, "k", lw=2, label="original")
plt.plot(t, xinv, "b", ms=10, label="inversion")
plt.plot(t, xinv_fromx0, "--r", ms=10, label="inversion from x0")
plt.plot(t, xne, "--g", ms=10, label="normal equations")
plt.legend()
plt.title("Data reconstruction without regularization")
plt.tight_layout()

Regularization¶
You may have noticed that none of the inversion has been successfull in recovering the original signal. This is a clear indication that the problem we are trying to solve is highly ill-posed and requires some prior knowledge from the user.
We will now see how to add prior information to the inverse process in the form of regularization (or preconditioning). This can be done in two different ways
regularization via
pylops.optimization.leastsquares.normal_equations_inversion
orpylops.optimization.leastsquares.regularized_inversion
)preconditioning via
pylops.optimization.leastsquares.preconditioned_inversion
Let’s start by regularizing the normal equations using a second derivative operator
\[\mathbf{x} = (\mathbf{R^TR}+\epsilon_\nabla^2\nabla^T\nabla)^{-1} \mathbf{R^Ty}\]
# Create regularization operator
D2op = pylops.SecondDerivative(N, dtype="float64")
# Regularized inversion
epsR = np.sqrt(0.1)
epsI = np.sqrt(1e-4)
xne = pylops.optimization.leastsquares.normal_equations_inversion(
Rop, y, [D2op], epsI=epsI, epsRs=[epsR], **dict(maxiter=50)
)[0]
Note that in case we have access to a fast implementation for the chain of
forward and adjoint for the regularization operator
(i.e., \(\nabla^T\nabla\)), we can modify our call to
pylops.optimization.leastsquares.normal_equations_inversion
as
follows:
ND2op = pylops.MatrixMult((D2op.H * D2op).tosparse()) # mimic fast D^T D
xne1 = pylops.optimization.leastsquares.normal_equations_inversion(
Rop, y, [], NRegs=[ND2op], epsI=epsI, epsNRs=[epsR], **dict(maxiter=50)
)[0]
We can do the same while using
pylops.optimization.leastsquares.regularized_inversion
which solves the following augmented problem
\[\begin{split}\begin{bmatrix} \mathbf{R} \\ \epsilon_\nabla \nabla \end{bmatrix} \mathbf{x} = \begin{bmatrix} \mathbf{y} \\ 0 \end{bmatrix}\end{split}\]
xreg = pylops.optimization.leastsquares.regularized_inversion(
Rop,
y,
[D2op],
epsRs=[np.sqrt(0.1)],
**dict(damp=np.sqrt(1e-4), iter_lim=50, show=0)
)[0]
We can also write a preconditioned problem, whose cost function is
\[J= \|\mathbf{y} - \mathbf{R} \mathbf{P} \mathbf{p}\|_2^2\]
where \(\mathbf{P}\) is the precondioned operator, \(\mathbf{p}\) is
the projected model in the preconditioned space, and
\(\mathbf{x}=\mathbf{P}\mathbf{p}\) is the model in the original model
space we want to solve for. Note that a preconditioned problem converges
much faster to its solution than its corresponding regularized problem.
This can be done using the routine
pylops.optimization.leastsquares.preconditioned_inversion
.
# Create regularization operator
Sop = pylops.Smoothing1D(nsmooth=11, dims=[N], dtype="float64")
# Invert for interpolated signal
xprec = pylops.optimization.leastsquares.preconditioned_inversion(
Rop, y, Sop, **dict(damp=np.sqrt(1e-9), iter_lim=20, show=0)
)[0]
Let’s finally visualize these solutions
# sphinx_gallery_thumbnail_number=4
fig = plt.figure(figsize=(12, 4))
plt.plot(t[iava], y, ".k", ms=20, label="available samples")
plt.plot(t, x, "k", lw=3, label="original")
plt.plot(t, xne, "b", lw=3, label="normal equations")
plt.plot(t, xne1, "--c", lw=3, label="normal equations (with direct D^T D)")
plt.plot(t, xreg, "-.r", lw=3, label="regularized")
plt.plot(t, xprec, "--g", lw=3, label="preconditioned equations")
plt.legend()
plt.title("Data reconstruction with regularization")
subax = fig.add_axes([0.7, 0.2, 0.15, 0.6])
subax.plot(t[iava], y, ".k", ms=20)
subax.plot(t, x, "k", lw=3)
subax.plot(t, xne, "b", lw=3)
subax.plot(t, xne1, "--c", lw=3)
subax.plot(t, xreg, "-.r", lw=3)
subax.plot(t, xprec, "--g", lw=3)
subax.set_xlim(0.05, 0.3)
plt.tight_layout()

Much better estimates! We have seen here how regularization and/or preconditioning can be vital to succesfully solve some ill-posed inverse problems.
We have however so far only considered solvers that can include additional norm-2 regularization terms. A very active area of research is that of sparsity-promoting solvers (also sometimes referred to as compressive sensing): the regularization term added to the cost function to minimize has norm-p (\(p \le 1\)) and the problem is generally recasted by considering the model to be sparse in some domain. We can follow this philosophy as our signal to invert was actually created as superposition of 3 sinusoids (i.e., three spikes in the Fourier domain). Our new cost function is:
\[J_1 = \|\mathbf{y} - \mathbf{R} \mathbf{F} \mathbf{p}\|_2^2 + \epsilon \|\mathbf{p}\|_1\]
where \(\mathbf{F}\) is the FFT operator. We will thus use the
pylops.optimization.sparsity.ista
and
pylops.optimization.sparsity.fista
solvers to estimate our input
signal.
pista, niteri, costi = pylops.optimization.sparsity.ista(
Rop * FFTop.H,
y,
niter=1000,
eps=0.1,
tol=1e-7,
)
xista = FFTop.H * pista
pfista, niterf, costf = pylops.optimization.sparsity.fista(
Rop * FFTop.H,
y,
niter=1000,
eps=0.1,
tol=1e-7,
)
xfista = FFTop.H * pfista
fig, axs = plt.subplots(2, 1, figsize=(12, 8))
fig.suptitle("Data reconstruction with sparsity", fontsize=14, fontweight="bold", y=0.9)
axs[0].plot(f, np.abs(X), "k", lw=3)
axs[0].plot(f, np.abs(pista), "--r", lw=3)
axs[0].plot(f, np.abs(pfista), "--g", lw=3)
axs[0].set_xlim(0, 30)
axs[0].set_title("Frequency domain")
axs[1].plot(t[iava], y, ".k", ms=20, label="available samples")
axs[1].plot(t, x, "k", lw=3, label="original")
axs[1].plot(t, xista, "--r", lw=3, label="ISTA")
axs[1].plot(t, xfista, "--g", lw=3, label="FISTA")
axs[1].set_title("Time domain")
axs[1].axis("tight")
axs[1].legend()
plt.tight_layout()
plt.subplots_adjust(top=0.8)
fig, ax = plt.subplots(1, 1, figsize=(12, 3))
ax.semilogy(costi, "r", lw=2, label="ISTA")
ax.semilogy(costf, "g", lw=2, label="FISTA")
ax.set_title("Cost functions", size=15, fontweight="bold")
ax.set_xlabel("Iteration")
ax.legend()
ax.grid(True)
plt.tight_layout()
As you can see, changing parametrization of the model and imposing sparsity in the Fourier domain has given an extra improvement to our ability of recovering the underlying densely sampled input signal. Moreover, FISTA converges much faster than ISTA as expected and should be preferred when using sparse solvers.
Finally we consider a slightly different cost function (note that in this case we try to solve a constrained problem):
\[J_1 = \|\mathbf{p}\|_1 \quad \text{subject to} \quad \|\mathbf{y} - \mathbf{R} \mathbf{F} \mathbf{p}\|\]
A very popular solver to solve such kind of cost function is called spgl1
and can be accessed via pylops.optimization.sparsity.spgl1
.
xspgl1, pspgl1, info = pylops.optimization.sparsity.spgl1(
Rop, y, SOp=FFTop, tau=3, iter_lim=200
)
fig, axs = plt.subplots(2, 1, figsize=(12, 8))
fig.suptitle("Data reconstruction with SPGL1", fontsize=14, fontweight="bold", y=0.9)
axs[0].plot(f, np.abs(X), "k", lw=3)
axs[0].plot(f, np.abs(pspgl1), "--m", lw=3)
axs[0].set_xlim(0, 30)
axs[0].set_title("Frequency domain")
axs[1].plot(t[iava], y, ".k", ms=20, label="available samples")
axs[1].plot(t, x, "k", lw=3, label="original")
axs[1].plot(t, xspgl1, "--m", lw=3, label="SPGL1")
axs[1].set_title("Time domain")
axs[1].axis("tight")
axs[1].legend()
plt.tight_layout()
plt.subplots_adjust(top=0.8)
fig, ax = plt.subplots(1, 1, figsize=(12, 3))
ax.semilogy(info["rnorm2"], "k", lw=2, label="ISTA")
ax.set_title("Cost functions", size=15, fontweight="bold")
ax.set_xlabel("Iteration")
ax.legend()
ax.grid(True)
plt.tight_layout()
Total running time of the script: ( 0 minutes 3.996 seconds)
Note
Click here to download the full example code
03. Solvers (Advanced)¶
This is a follow up tutorial to the 03. Solvers tutorial. The same example will be considered, however we will showcase how to use the class-based version of our solvers (introduced in PyLops v2).
First of all, when shall you use class-based solvers over function-based ones? The answer is simple, every time you feel you would have like to have more flexibility when using one PyLops function-based solvers.
In fact, a function-based solver in PyLops v2 is nothing more than a thin wrapper over its class-based equivalent, which generally performs the following steps:
solver initialization
setup
run
(by calling multiple timesstep
)finalize
The nice thing about class-based solvers is that i) a user can manually orchestrate these steps and do anything
in between them; ii) a user can create a class-based pylops.optimization.callback.Callbacks
object and
define a set of callbacks that will be run pre and post setup, step and run. One example of how such callbacks can
be handy to track evolving variables in the solver can be found in Linear Regression.
In the following we will leverage the very same mechanism to keep track of a number of metrics using the predefined
pylops.optimization.callback.MetricsCallback
callback. Finally we show how to create a customized callback
that can track the percentage change of the solution and residual. This is of course just an example, we expect
users will find different use cases based on the problem at hand.
import matplotlib.pyplot as plt
# pylint: disable=C0103
import numpy as np
import pylops
plt.close("all")
np.random.seed(10)
Let’s first create the data in the frequency domain. The data is composed by the superposition of 3 sinusoids with different frequencies.
# Signal creation in frequency domain
ifreqs = [41, 25, 66]
amps = [1.0, 1.0, 1.0]
N = 200
nfft = 2**11
dt = 0.004
t = np.arange(N) * dt
f = np.fft.rfftfreq(nfft, dt)
FFTop = 10 * pylops.signalprocessing.FFT(N, nfft=nfft, real=True)
X = np.zeros(nfft // 2 + 1, dtype="complex128")
X[ifreqs] = amps
x = FFTop.H * X
We now define the locations at which the signal will be sampled.
# subsampling locations
perc_subsampling = 0.2
Nsub = int(np.round(N * perc_subsampling))
iava = np.sort(np.random.permutation(np.arange(N))[:Nsub])
# Create restriction operator
Rop = pylops.Restriction(N, iava, dtype="float64")
y = Rop * x
ymask = Rop.mask(x)
Let’s now solve the interpolation problem using the
pylops.optimization.sparsity.ISTA
class-based solver.
cb = pylops.optimization.callback.MetricsCallback(x, FFTop.H)
istasolve = pylops.optimization.sparsity.ISTA(
Rop * FFTop.H,
callbacks=[
cb,
],
)
pista, niteri, costi = istasolve.solve(y, niter=1000, eps=0.1, tol=1e-7)
xista = FFTop.H * pista
fig, axs = plt.subplots(1, 4, figsize=(16, 3))
for i, metric in enumerate(["mae", "mse", "snr", "psnr"]):
axs[i].plot(cb.metrics[metric], "k", lw=2)
axs[i].set_title(metric)
plt.tight_layout()

Finally, we show how we can also define customized callbacks. What we are really interested in here is to store the first residual norm once the setup of the solver is over, and repeat the same after each step (using the previous estimate to compute the percentage change). And, we do the same for the solution norm.
class CallbackISTA(pylops.optimization.callback.Callbacks):
def __init__(self):
self.res_perc = []
self.x_perc = []
def on_setup_end(self, solver, x):
self.x = x
if x is not None:
self.rec = solver.Op @ x - solver.y
else:
self.rec = None
def on_step_end(self, solver, x):
self.xold = self.x
self.x = x
self.recold = self.rec
self.rec = solver.Op @ x - solver.y
if self.xold is not None:
self.x_perc.append(
100 * np.linalg.norm(self.x - self.xold) / np.linalg.norm(self.xold)
)
self.res_perc.append(
100
* np.linalg.norm(self.rec - self.recold)
/ np.linalg.norm(self.recold)
)
def on_run_end(self, solver, x):
# remove first percentage
self.x_perc = np.array(self.x_perc[1:])
self.res_perc = np.array(self.res_perc[1:])
cb = CallbackISTA()
istasolve = pylops.optimization.sparsity.ISTA(
Rop * FFTop.H,
callbacks=[
cb,
],
)
pista, niteri, costi = istasolve.solve(y, niter=1000, eps=0.1, tol=1e-7)
xista = FFTop.H * pista
cbf = CallbackISTA()
fistasolve = pylops.optimization.sparsity.FISTA(
Rop * FFTop.H,
callbacks=[
cbf,
],
)
pfista, niterf, costf = fistasolve.solve(y, niter=1000, eps=0.1, tol=1e-7)
xfista = FFTop.H * pfista
fig, axs = plt.subplots(2, 1, figsize=(12, 8))
fig.suptitle("Data reconstruction with sparsity", fontsize=14, fontweight="bold", y=0.9)
axs[0].plot(f, np.abs(X), "k", lw=3)
axs[0].plot(f, np.abs(pista), "--r", lw=3)
axs[0].plot(f, np.abs(pfista), "--g", lw=3)
axs[0].set_xlim(0, 30)
axs[0].set_title("Frequency domain")
axs[1].plot(t[iava], y, ".k", ms=20, label="available samples")
axs[1].plot(t, x, "k", lw=3, label="original")
axs[1].plot(t, xista, "--r", lw=3, label="ISTA")
axs[1].plot(t, xfista, "--g", lw=3, label="FISTA")
axs[1].set_title("Time domain")
axs[1].axis("tight")
axs[1].legend()
plt.tight_layout()
plt.subplots_adjust(top=0.8)
fig, axs = plt.subplots(2, 1, figsize=(12, 8))
fig.suptitle("Norms history", fontsize=14, fontweight="bold", y=0.9)
axs[0].semilogy(cb.res_perc, "r", lw=3)
axs[0].semilogy(cbf.res_perc, "g", lw=3)
axs[0].set_title("Residual percentage change")
axs[1].semilogy(cb.x_perc, "r", lw=3, label="ISTA")
axs[1].semilogy(cbf.x_perc, "g", lw=3, label="FISTA")
axs[1].set_title("Solution percentage change")
axs[1].legend()
plt.tight_layout()
plt.subplots_adjust(top=0.8)
Total running time of the script: ( 0 minutes 3.739 seconds)
Note
Click here to download the full example code
04. Bayesian Inversion¶
This tutorial focuses on Bayesian inversion, a special type of inverse problem that aims at incorporating prior information in terms of model and data probabilities in the inversion process.
In this case we will be dealing with the same problem that we discussed in 03. Solvers, but instead of defining ad-hoc regularization or preconditioning terms we parametrize and model our input signal in the frequency domain in a probabilistic fashion: the central frequency, amplitude and phase of the three sinusoids have gaussian distributions as follows:
where \(f_i \sim N(f_{0,i}, \sigma_{f,i})\), \(a_i \sim N(a_{0,i}, \sigma_{a,i})\), and \(\phi_i \sim N(\phi_{0,i}, \sigma_{\phi,i})\).
Based on the above definition, we construct some prior models in the frequency domain, convert each of them to the time domain and use such an ensemble to estimate the prior mean \(\mu_\mathbf{x}\) and model covariance \(\mathbf{C_x}\).
We then create our data by sampling the true signal at certain locations and solve the resconstruction problem within a Bayesian framework. Since we are assuming gaussianity in our priors, the equation to obtain the posterion mean can be derived analytically:
import matplotlib.pyplot as plt
# sphinx_gallery_thumbnail_number = 2
import numpy as np
from scipy.sparse.linalg import lsqr
import pylops
plt.close("all")
np.random.seed(10)
Let’s start by creating our true model and prior realizations
def prior_realization(f0, a0, phi0, sigmaf, sigmaa, sigmaphi, dt, nt, nfft):
"""Create realization from prior mean and std for amplitude, frequency and
phase
"""
f = np.fft.rfftfreq(nfft, dt)
df = f[1] - f[0]
ifreqs = [int(np.random.normal(f, sigma) / df) for f, sigma in zip(f0, sigmaf)]
amps = [np.random.normal(a, sigma) for a, sigma in zip(a0, sigmaa)]
phis = [np.random.normal(phi, sigma) for phi, sigma in zip(phi0, sigmaphi)]
# input signal in frequency domain
X = np.zeros(nfft // 2 + 1, dtype="complex128")
X[ifreqs] = (
np.array(amps).squeeze() * np.exp(1j * np.deg2rad(np.array(phis))).squeeze()
)
# input signal in time domain
FFTop = pylops.signalprocessing.FFT(nt, nfft=nfft, real=True)
x = FFTop.H * X
return x
# Priors
nreals = 100
f0 = [5, 3, 8]
sigmaf = [0.5, 1.0, 0.6]
a0 = [1.0, 1.0, 1.0]
sigmaa = [0.1, 0.5, 0.6]
phi0 = [-90.0, 0.0, 0.0]
sigmaphi = [0.1, 0.2, 0.4]
sigmad = 1e-2
# Prior models
nt = 200
nfft = 2**11
dt = 0.004
t = np.arange(nt) * dt
xs = np.array(
[
prior_realization(f0, a0, phi0, sigmaf, sigmaa, sigmaphi, dt, nt, nfft)
for _ in range(nreals)
]
)
# True model (taken as one possible realization)
x = prior_realization(f0, a0, phi0, [0, 0, 0], [0, 0, 0], [0, 0, 0], dt, nt, nfft)
We have now a set of prior models in time domain. We can easily use sample statistics to estimate the prior mean and covariance. For the covariance, we perform a second step where we average values around the main diagonal for each row and find a smooth, compact filter that we use to define a convolution linear operator that mimics the action of the covariance matrix on a vector
x0 = np.average(xs, axis=0)
Cm = ((xs - x0).T @ (xs - x0)) / nreals
N = 30 # lenght of decorrelation
diags = np.array([Cm[i, i - N : i + N + 1] for i in range(N, nt - N)])
diag_ave = np.average(diags, axis=0)
# add a taper at the end to avoid edge effects
diag_ave *= np.hamming(2 * N + 1)
fig, ax = plt.subplots(1, 1, figsize=(12, 4))
ax.plot(t, xs.T, "r", lw=1)
ax.plot(t, x0, "g", lw=4)
ax.plot(t, x, "k", lw=4)
ax.set_title("Prior realizations and mean")
ax.set_xlim(0, 0.8)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
im = ax1.imshow(
Cm, interpolation="nearest", cmap="seismic", extent=(t[0], t[-1], t[-1], t[0])
)
ax1.set_title(r"$\mathbf{C}_m^{prior}$")
ax1.axis("tight")
ax2.plot(np.arange(-N, N + 1) * dt, diags.T, "--r", lw=1)
ax2.plot(np.arange(-N, N + 1) * dt, diag_ave, "k", lw=4)
ax2.set_title("Averaged covariance 'filter'")
plt.tight_layout()
Let’s define now the sampling operator as well as create our covariance matrices in terms of linear operators. This may not be strictly necessary here but shows how even Bayesian-type of inversion can very easily scale to large model and data spaces.
# Sampling operator
perc_subsampling = 0.2
ntsub = int(np.round(nt * perc_subsampling))
iava = np.sort(np.random.permutation(np.arange(nt))[:ntsub])
iava[-1] = nt - 1 # assume we have the last sample to avoid instability
Rop = pylops.Restriction(nt, iava, dtype="float64")
# Covariance operators
Cm_op = pylops.signalprocessing.Convolve1D(nt, diag_ave, offset=N)
Cd_op = sigmad**2 * pylops.Identity(ntsub)
We model now our data and add noise that respects our prior definition
n = np.random.normal(0, sigmad, nt)
y = Rop * x
yn = Rop * (x + n)
ymask = Rop.mask(x)
ynmask = Rop.mask(x + n)
First we apply the Bayesian inversion equation
xbayes = x0 + Cm_op * Rop.H * (
lsqr(Rop * Cm_op * Rop.H + Cd_op, yn - Rop * x0, iter_lim=400)[0]
)
# Visualize
fig, ax = plt.subplots(1, 1, figsize=(12, 5))
ax.plot(t, x, "k", lw=6, label="true")
ax.plot(t, ymask, ".k", ms=25, label="available samples")
ax.plot(t, ynmask, ".r", ms=25, label="available noisy samples")
ax.plot(t, xbayes, "r", lw=3, label="bayesian inverse")
ax.legend()
ax.set_title("Signal")
ax.set_xlim(0, 0.8)
plt.tight_layout()

So far we have been able to estimate our posterion mean. What about its uncertainties (i.e., posterion covariance)?
In real-life applications it is very difficult (if not impossible) to directly compute the posterior covariance matrix. It is much more useful to create a set of models that sample the posterion probability. We can do that by solving our problem several times using different prior realizations as starting guesses:
xpost = [
x0
+ Cm_op
* Rop.H
* (lsqr(Rop * Cm_op * Rop.H + Cd_op, yn - Rop * x0, iter_lim=400)[0])
for x0 in xs[:30]
]
xpost = np.array(xpost)
x0post = np.average(xpost, axis=0)
Cm_post = ((xpost - x0post).T @ (xpost - x0post)) / nreals
# Visualize
fig, ax = plt.subplots(1, 1, figsize=(12, 5))
ax.plot(t, x, "k", lw=6, label="true")
ax.plot(t, xpost.T, "--r", lw=1)
ax.plot(t, x0post, "r", lw=3, label="bayesian inverse")
ax.plot(t, ymask, ".k", ms=25, label="available samples")
ax.plot(t, ynmask, ".r", ms=25, label="available noisy samples")
ax.legend()
ax.set_title("Signal")
ax.set_xlim(0, 0.8)
fig, ax = plt.subplots(1, 1, figsize=(5, 4))
im = ax.imshow(
Cm_post, interpolation="nearest", cmap="seismic", extent=(t[0], t[-1], t[-1], t[0])
)
ax.set_title(r"$\mathbf{C}_m^{posterior}$")
ax.axis("tight")
plt.tight_layout()
Note that here we have been able to compute a sample posterior covariance from its estimated samples. By displaying it we can see how both the overall variances and the correlation between different parameters have become narrower compared to their prior counterparts.
Total running time of the script: ( 0 minutes 1.659 seconds)
Note
Click here to download the full example code
05. Image deblurring¶
Deblurring is the process of removing blurring effects from images, caused for example by defocus aberration or motion blur.
In forward mode, such blurring effect is typically modelled as a 2-dimensional convolution between the so-called point spread function and a target sharp input image, where the sharp input image (which has to be recovered) is unknown and the point-spread function can be either known or unknown.
In this tutorial, an example of 2d blurring and deblurring will be shown using
the pylops.signalprocessing.Convolve2D
operator assuming knowledge
of the point-spread function.
import matplotlib.pyplot as plt
import numpy as np
import pylops
Let’s start by importing a 2d image and defining the blurring operator
im = np.load("../testdata/python.npy")[::5, ::5, 0]
Nz, Nx = im.shape
# Blurring guassian operator
nh = [15, 25]
hz = np.exp(-0.1 * np.linspace(-(nh[0] // 2), nh[0] // 2, nh[0]) ** 2)
hx = np.exp(-0.03 * np.linspace(-(nh[1] // 2), nh[1] // 2, nh[1]) ** 2)
hz /= np.trapz(hz) # normalize the integral to 1
hx /= np.trapz(hx) # normalize the integral to 1
h = hz[:, np.newaxis] * hx[np.newaxis, :]
fig, ax = plt.subplots(1, 1, figsize=(5, 3))
him = ax.imshow(h)
ax.set_title("Blurring operator")
fig.colorbar(him, ax=ax)
ax.axis("tight")
Cop = pylops.signalprocessing.Convolve2D(
(Nz, Nx), h=h, offset=(nh[0] // 2, nh[1] // 2), dtype="float32"
)

We first apply the blurring operator to the sharp image. We then try to recover the sharp input image by inverting the convolution operator from the blurred image. Note that when we perform inversion without any regularization, the deblurred image will show some ringing due to the instabilities of the inverse process. Using a L1 solver with a DWT preconditioner or TV regularization allows to recover sharper contrasts.
imblur = Cop * im
imdeblur = pylops.optimization.leastsquares.normal_equations_inversion(
Cop, imblur.ravel(), None, maxiter=50 # solvers need 1D arrays
)[0]
imdeblur = imdeblur.reshape(Cop.dims)
Wop = pylops.signalprocessing.DWT2D((Nz, Nx), wavelet="haar", level=3)
Dop = [
pylops.FirstDerivative((Nz, Nx), axis=0, edge=False),
pylops.FirstDerivative((Nz, Nx), axis=1, edge=False),
]
DWop = Dop + [Wop]
imdeblurfista = pylops.optimization.sparsity.fista(
Cop * Wop.H, imblur.ravel(), eps=1e-1, niter=100
)[0]
imdeblurfista = imdeblurfista.reshape((Cop * Wop.H).dims)
imdeblurfista = Wop.H * imdeblurfista
imdeblurtv = pylops.optimization.sparsity.splitbregman(
Cop,
imblur.ravel(),
Dop,
niter_outer=10,
niter_inner=5,
mu=1.5,
epsRL1s=[2e0, 2e0],
tol=1e-4,
tau=1.0,
show=False,
**dict(iter_lim=5, damp=1e-4)
)[0]
imdeblurtv = imdeblurtv.reshape(Cop.dims)
imdeblurtv1 = pylops.optimization.sparsity.splitbregman(
Cop,
imblur.ravel(),
DWop,
niter_outer=10,
niter_inner=5,
mu=1.5,
epsRL1s=[1e0, 1e0, 1e0],
tol=1e-4,
tau=1.0,
show=False,
**dict(iter_lim=5, damp=1e-4)
)[0]
imdeblurtv1 = imdeblurtv1.reshape(Cop.dims)
Finally we visualize the original, blurred, and recovered images.
# sphinx_gallery_thumbnail_number = 2
fig = plt.figure(figsize=(12, 6))
fig.suptitle("Deblurring", fontsize=14, fontweight="bold", y=0.95)
ax1 = plt.subplot2grid((2, 5), (0, 0))
ax2 = plt.subplot2grid((2, 5), (0, 1))
ax3 = plt.subplot2grid((2, 5), (0, 2))
ax4 = plt.subplot2grid((2, 5), (1, 0))
ax5 = plt.subplot2grid((2, 5), (1, 1))
ax6 = plt.subplot2grid((2, 5), (1, 2))
ax7 = plt.subplot2grid((2, 5), (0, 3), colspan=2)
ax8 = plt.subplot2grid((2, 5), (1, 3), colspan=2)
ax1.imshow(im, cmap="viridis", vmin=0, vmax=250)
ax1.axis("tight")
ax1.set_title("Original")
ax2.imshow(imblur, cmap="viridis", vmin=0, vmax=250)
ax2.axis("tight")
ax2.set_title("Blurred")
ax3.imshow(imdeblur, cmap="viridis", vmin=0, vmax=250)
ax3.axis("tight")
ax3.set_title("Deblurred")
ax4.imshow(imdeblurfista, cmap="viridis", vmin=0, vmax=250)
ax4.axis("tight")
ax4.set_title("FISTA deblurred")
ax5.imshow(imdeblurtv, cmap="viridis", vmin=0, vmax=250)
ax5.axis("tight")
ax5.set_title("TV deblurred")
ax6.imshow(imdeblurtv1, cmap="viridis", vmin=0, vmax=250)
ax6.axis("tight")
ax6.set_title("TV+Haar deblurred")
ax7.plot(im[Nz // 2], "k")
ax7.plot(imblur[Nz // 2], "--r")
ax7.plot(imdeblur[Nz // 2], "--b")
ax7.plot(imdeblurfista[Nz // 2], "--g")
ax7.plot(imdeblurtv[Nz // 2], "--m")
ax7.plot(imdeblurtv1[Nz // 2], "--y")
ax7.axis("tight")
ax7.set_title("Horizontal section")
ax8.plot(im[:, Nx // 2], "k", label="Original")
ax8.plot(imblur[:, Nx // 2], "--r", label="Blurred")
ax8.plot(imdeblur[:, Nx // 2], "--b", label="Deblurred")
ax8.plot(imdeblurfista[:, Nx // 2], "--g", label="FISTA deblurred")
ax8.plot(imdeblurtv[:, Nx // 2], "--m", label="TV deblurred")
ax8.plot(imdeblurtv1[:, Nx // 2], "--y", label="TV+Haar deblurred")
ax8.axis("tight")
ax8.set_title("Vertical section")
ax8.legend(loc=5, fontsize="small")
plt.tight_layout()
plt.subplots_adjust(top=0.8)

Total running time of the script: ( 0 minutes 6.555 seconds)
Note
Click here to download the full example code
06. 2D Interpolation¶
In the mathematical field of numerical analysis, interpolation is the problem of constructing new data points within the range of a discrete set of known data points. In signal and image processing, the data may be recorded at irregular locations and it is often required to regularize the data into a regular grid.
In this tutorial, an example of 2d interpolation of an image is carried out using a combination
of PyLops operators (pylops.Restriction
and
pylops.Laplacian
) and the pylops.optimization
module.
Mathematically speaking, if we want to interpolate a signal using the theory of inverse problems, we can define the following forward problem:
where the restriction operator \(\mathbf{R}\) selects \(M\) elements from the regularly sampled signal \(\mathbf{x}\) at random locations. The input and output signals are:
with \(M \gg N\).
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
np.random.seed(0)
To start we import a 2d image and define our restriction operator to irregularly and randomly sample the image for 30% of the entire grid
im = np.load("../testdata/python.npy")[:, :, 0]
Nz, Nx = im.shape
N = Nz * Nx
# Subsample signal
perc_subsampling = 0.2
Nsub2d = int(np.round(N * perc_subsampling))
iava = np.sort(np.random.permutation(np.arange(N))[:Nsub2d])
# Create operators and data
Rop = pylops.Restriction(N, iava, dtype="float64")
D2op = pylops.Laplacian((Nz, Nx), weights=(1, 1), dtype="float64")
x = im.ravel()
y = Rop * x
y1 = Rop.mask(x)
We will now use two different routines from our optimization toolbox to estimate our original image in the regular grid.
xcg_reg_lop = pylops.optimization.leastsquares.normal_equations_inversion(
Rop, y, [D2op], epsRs=[np.sqrt(0.1)], **dict(maxiter=200)
)[0]
# Invert for interpolated signal, lsqrt
xlsqr_reg_lop = pylops.optimization.leastsquares.regularized_inversion(
Rop,
y,
[D2op],
epsRs=[np.sqrt(0.1)],
**dict(damp=0, iter_lim=200, show=0),
)[0]
# Reshape estimated images
im_sampled = y1.reshape((Nz, Nx))
im_rec_lap_cg = xcg_reg_lop.reshape((Nz, Nx))
im_rec_lap_lsqr = xlsqr_reg_lop.reshape((Nz, Nx))
Finally we visualize the original image, the reconstructed images and their error
fig, axs = plt.subplots(1, 4, figsize=(12, 4))
fig.suptitle("Data reconstruction - normal eqs", fontsize=14, fontweight="bold", y=0.95)
axs[0].imshow(im, cmap="viridis", vmin=0, vmax=250)
axs[0].axis("tight")
axs[0].set_title("Original")
axs[1].imshow(im_sampled.data, cmap="viridis", vmin=0, vmax=250)
axs[1].axis("tight")
axs[1].set_title("Sampled")
axs[2].imshow(im_rec_lap_cg, cmap="viridis", vmin=0, vmax=250)
axs[2].axis("tight")
axs[2].set_title("2D Regularization")
axs[3].imshow(im - im_rec_lap_cg, cmap="gray", vmin=-80, vmax=80)
axs[3].axis("tight")
axs[3].set_title("2D Regularization Error")
plt.tight_layout()
plt.subplots_adjust(top=0.8)
fig, axs = plt.subplots(1, 4, figsize=(12, 4))
fig.suptitle(
"Data reconstruction - regularized eqs", fontsize=14, fontweight="bold", y=0.95
)
axs[0].imshow(im, cmap="viridis", vmin=0, vmax=250)
axs[0].axis("tight")
axs[0].set_title("Original")
axs[1].imshow(im_sampled.data, cmap="viridis", vmin=0, vmax=250)
axs[1].axis("tight")
axs[1].set_title("Sampled")
axs[2].imshow(im_rec_lap_lsqr, cmap="viridis", vmin=0, vmax=250)
axs[2].axis("tight")
axs[2].set_title("2D Regularization")
axs[3].imshow(im - im_rec_lap_lsqr, cmap="gray", vmin=-80, vmax=80)
axs[3].axis("tight")
axs[3].set_title("2D Regularization Error")
plt.tight_layout()
plt.subplots_adjust(top=0.8)
Total running time of the script: ( 0 minutes 17.005 seconds)
Note
Click here to download the full example code
07. Post-stack inversion¶
Estimating subsurface properties from band-limited seismic data represents an important task for geophysical subsurface characterization.
In this tutorial, the pylops.avo.poststack.PoststackLinearModelling
operator is used for modelling of both 1d and 2d synthetic post-stack seismic
data from a profile or 2d model of the subsurface acoustic impedence.
where \(\text{AI}(t)\) is the acoustic impedance profile and \(w(t)\) is the time domain seismic wavelet. In compact form:
where \(\mathbf{W}\) is a convolution operator, \(\mathbf{D}\) is a
first derivative operator, and \(\mathbf{ai}\) is the input model.
Subsequently the acoustic impedance model is estimated via the
pylops.avo.poststack.PoststackInversion
module. A two-steps
inversion strategy is finally presented to deal with the case of noisy data.
import matplotlib.pyplot as plt
# sphinx_gallery_thumbnail_number = 4
import numpy as np
from scipy.signal import filtfilt
import pylops
from pylops.utils.wavelets import ricker
plt.close("all")
np.random.seed(10)
Let’s start with a 1d example. A synthetic profile of acoustic impedance
is created and data is modelled using both the dense and linear operator
version of pylops.avo.poststack.PoststackLinearModelling
operator.
# model
nt0 = 301
dt0 = 0.004
t0 = np.arange(nt0) * dt0
vp = 1200 + np.arange(nt0) + filtfilt(np.ones(5) / 5.0, 1, np.random.normal(0, 80, nt0))
rho = 1000 + vp + filtfilt(np.ones(5) / 5.0, 1, np.random.normal(0, 30, nt0))
vp[131:] += 500
rho[131:] += 100
m = np.log(vp * rho)
# smooth model
nsmooth = 100
mback = filtfilt(np.ones(nsmooth) / float(nsmooth), 1, m)
# wavelet
ntwav = 41
wav, twav, wavc = ricker(t0[: ntwav // 2 + 1], 20)
# dense operator
PPop_dense = pylops.avo.poststack.PoststackLinearModelling(
wav / 2, nt0=nt0, explicit=True
)
# lop operator
PPop = pylops.avo.poststack.PoststackLinearModelling(wav / 2, nt0=nt0)
# data
d_dense = PPop_dense * m.ravel()
d = PPop * m
# add noise
dn_dense = d_dense + np.random.normal(0, 2e-2, d_dense.shape)
We can now estimate the acoustic profile from band-limited data using either the dense operator or linear operator.
# solve dense
minv_dense = pylops.avo.poststack.PoststackInversion(
d, wav / 2, m0=mback, explicit=True, simultaneous=False
)[0]
# solve lop
minv = pylops.avo.poststack.PoststackInversion(
d_dense,
wav / 2,
m0=mback,
explicit=False,
simultaneous=False,
**dict(iter_lim=2000)
)[0]
# solve noisy
mn = pylops.avo.poststack.PoststackInversion(
dn_dense, wav / 2, m0=mback, explicit=True, epsR=1e0, **dict(damp=1e-1)
)[0]
fig, axs = plt.subplots(1, 2, figsize=(6, 7), sharey=True)
axs[0].plot(d_dense, t0, "k", lw=4, label="Dense")
axs[0].plot(d, t0, "--r", lw=2, label="Lop")
axs[0].plot(dn_dense, t0, "-.g", lw=2, label="Noisy")
axs[0].set_title("Data")
axs[0].invert_yaxis()
axs[0].axis("tight")
axs[0].legend(loc=1)
axs[1].plot(m, t0, "k", lw=4, label="True")
axs[1].plot(mback, t0, "--b", lw=4, label="Back")
axs[1].plot(minv_dense, t0, "--m", lw=2, label="Inv Dense")
axs[1].plot(minv, t0, "--r", lw=2, label="Inv Lop")
axs[1].plot(mn, t0, "--g", lw=2, label="Inv Noisy")
axs[1].set_title("Model")
axs[1].axis("tight")
axs[1].legend(loc=1)
plt.tight_layout()

We see how inverting a dense matrix is in this case faster than solving for the linear operator (a good estimate of the model is in fact obtained only after 2000 iterations of lsqr). Nevertheless, having a linear operator is useful when we deal with larger dimensions (2d or 3d) and we want to couple our modelling operator with different types of spatial regularizations or preconditioning.
Before we move onto a 2d example, let’s consider the case of non-stationary wavelet and see how we can easily use the same routines in this case
# wavelet
ntwav = 41
f0s = np.flip(np.arange(nt0) * 0.05 + 3)
wavs = np.array([ricker(t0[:ntwav], f0)[0] for f0 in f0s])
wavc = np.argmax(wavs[0])
plt.figure(figsize=(5, 4))
plt.imshow(wavs.T, cmap="gray", extent=(t0[0], t0[-1], t0[ntwav], -t0[ntwav]))
plt.xlabel("t")
plt.title("Wavelets")
plt.axis("tight")
# operator
PPop = pylops.avo.poststack.PoststackLinearModelling(wavs / 2, nt0=nt0, explicit=True)
# data
d = PPop * m
# solve
minv = pylops.avo.poststack.PoststackInversion(
d, wavs / 2, m0=mback, explicit=True, **dict(cond=1e-10)
)[0]
fig, axs = plt.subplots(1, 2, figsize=(6, 7), sharey=True)
axs[0].plot(d, t0, "k", lw=4)
axs[0].set_title("Data")
axs[0].invert_yaxis()
axs[0].axis("tight")
axs[1].plot(m, t0, "k", lw=4, label="True")
axs[1].plot(mback, t0, "--b", lw=4, label="Back")
axs[1].plot(minv, t0, "--r", lw=2, label="Inv")
axs[1].set_title("Model")
axs[1].axis("tight")
axs[1].legend(loc=1)
plt.tight_layout()
We move now to a 2d example. First of all the model is loaded and data generated.
# model
inputfile = "../testdata/avo/poststack_model.npz"
model = np.load(inputfile)
m = np.log(model["model"][:, ::3])
x, z = model["x"][::3] / 1000.0, model["z"] / 1000.0
nx, nz = len(x), len(z)
# smooth model
nsmoothz, nsmoothx = 60, 50
mback = filtfilt(np.ones(nsmoothz) / float(nsmoothz), 1, m, axis=0)
mback = filtfilt(np.ones(nsmoothx) / float(nsmoothx), 1, mback, axis=1)
# dense operator
PPop_dense = pylops.avo.poststack.PoststackLinearModelling(
wav / 2, nt0=nz, spatdims=nx, explicit=True
)
# lop operator
PPop = pylops.avo.poststack.PoststackLinearModelling(wav / 2, nt0=nz, spatdims=nx)
# data
d = (PPop_dense * m.ravel()).reshape(nz, nx)
n = np.random.normal(0, 1e-1, d.shape)
dn = d + n
Finally we perform 4 different inversions:
trace-by-trace inversion with explicit solver and dense operator with noise-free data
trace-by-trace inversion with explicit solver and dense operator with noisy data
multi-trace regularized inversion with iterative solver and linear operator using the result of trace-by-trace inversion as starting guess
\[J = ||\Delta \mathbf{d} - \mathbf{W} \Delta \mathbf{ai}||_2 + \epsilon_\nabla ^2 ||\nabla \mathbf{ai}||_2\]where \(\Delta \mathbf{d}=\mathbf{d}-\mathbf{W}\mathbf{AI_0}\) is the residual data
multi-trace blocky inversion with iterative solver and linear operator
# dense inversion with noise-free data
minv_dense = pylops.avo.poststack.PoststackInversion(
d, wav / 2, m0=mback, explicit=True, simultaneous=False
)[0]
# dense inversion with noisy data
minv_dense_noisy = pylops.avo.poststack.PoststackInversion(
dn, wav / 2, m0=mback, explicit=True, epsI=4e-2, simultaneous=False
)[0]
# spatially regularized lop inversion with noisy data
minv_lop_reg = pylops.avo.poststack.PoststackInversion(
dn,
wav / 2,
m0=minv_dense_noisy,
explicit=False,
epsR=5e1,
**dict(damp=np.sqrt(1e-4), iter_lim=80)
)[0]
# blockiness promoting inversion with noisy data
minv_lop_blocky = pylops.avo.poststack.PoststackInversion(
dn,
wav / 2,
m0=mback,
explicit=False,
epsR=[0.4],
epsRL1=[0.1],
**dict(mu=0.1, niter_outer=5, niter_inner=10, iter_lim=5, damp=1e-3)
)[0]
fig, axs = plt.subplots(2, 4, figsize=(15, 9))
axs[0][0].imshow(d, cmap="gray", extent=(x[0], x[-1], z[-1], z[0]), vmin=-0.4, vmax=0.4)
axs[0][0].set_title("Data")
axs[0][0].axis("tight")
axs[0][1].imshow(
dn, cmap="gray", extent=(x[0], x[-1], z[-1], z[0]), vmin=-0.4, vmax=0.4
)
axs[0][1].set_title("Noisy Data")
axs[0][1].axis("tight")
axs[0][2].imshow(
m,
cmap="gist_rainbow",
extent=(x[0], x[-1], z[-1], z[0]),
vmin=m.min(),
vmax=m.max(),
)
axs[0][2].set_title("Model")
axs[0][2].axis("tight")
axs[0][3].imshow(
mback,
cmap="gist_rainbow",
extent=(x[0], x[-1], z[-1], z[0]),
vmin=m.min(),
vmax=m.max(),
)
axs[0][3].set_title("Smooth Model")
axs[0][3].axis("tight")
axs[1][0].imshow(
minv_dense,
cmap="gist_rainbow",
extent=(x[0], x[-1], z[-1], z[0]),
vmin=m.min(),
vmax=m.max(),
)
axs[1][0].set_title("Noise-free Inversion")
axs[1][0].axis("tight")
axs[1][1].imshow(
minv_dense_noisy,
cmap="gist_rainbow",
extent=(x[0], x[-1], z[-1], z[0]),
vmin=m.min(),
vmax=m.max(),
)
axs[1][1].set_title("Trace-by-trace Noisy Inversion")
axs[1][1].axis("tight")
axs[1][2].imshow(
minv_lop_reg,
cmap="gist_rainbow",
extent=(x[0], x[-1], z[-1], z[0]),
vmin=m.min(),
vmax=m.max(),
)
axs[1][2].set_title("Regularized Noisy Inversion - lop ")
axs[1][2].axis("tight")
axs[1][3].imshow(
minv_lop_blocky,
cmap="gist_rainbow",
extent=(x[0], x[-1], z[-1], z[0]),
vmin=m.min(),
vmax=m.max(),
)
axs[1][3].set_title("Blocky Noisy Inversion - lop ")
axs[1][3].axis("tight")
fig, ax = plt.subplots(1, 1, figsize=(3, 7))
ax.plot(m[:, nx // 2], z, "k", lw=4, label="True")
ax.plot(mback[:, nx // 2], z, "--r", lw=4, label="Back")
ax.plot(minv_dense[:, nx // 2], z, "--b", lw=2, label="Inv Dense")
ax.plot(minv_dense_noisy[:, nx // 2], z, "--m", lw=2, label="Inv Dense noisy")
ax.plot(minv_lop_reg[:, nx // 2], z, "--g", lw=2, label="Inv Lop regularized")
ax.plot(minv_lop_blocky[:, nx // 2], z, "--y", lw=2, label="Inv Lop blocky")
ax.set_title("Model")
ax.invert_yaxis()
ax.axis("tight")
ax.legend()
plt.tight_layout()
That’s almost it. If you wonder how this can be applied to real data, head over to the following notebook where the open-source segyio library is used alongside pylops to create an end-to-end open-source seismic inversion workflow with SEG-Y input data.
Total running time of the script: ( 0 minutes 12.010 seconds)
Note
Click here to download the full example code
08. Pre-stack (AVO) inversion¶
Pre-stack inversion represents one step beyond post-stack inversion in that not only the profile of acoustic impedance can be inferred from seismic data, rather a set of elastic parameters is estimated from pre-stack data (i.e., angle gathers) using the information contained in the so-called AVO (amplitude versus offset) response. Such elastic parameters represent vital information for more sophisticated geophysical subsurface characterization than it would be possible to achieve working with post-stack seismic data.
In this tutorial, the pylops.avo.prestack.PrestackLinearModelling
operator is used for modelling of both 1d and 2d synthetic pre-stack seismic
data using 1d profiles or 2d models of different subsurface elastic parameters
(P-wave velocity, S-wave velocity, and density) as input.
where \(\mathbf{m}(t)=[V_P(t), V_S(t), \rho(t)]\) is a vector containing three elastic parameters at time \(t\), \(G_i(t, \theta)\) are the coefficients of the AVO parametrization used to model pre-stack data and \(w(t)\) is the time domain seismic wavelet. In compact form:
where \(\mathbf{W}\) is a convolution operator, \(\mathbf{G}\) is
the AVO modelling operator, \(\mathbf{D}\) is a block-diagonal
derivative operator, and \(\mathbf{m}\) is the input model.
Subsequently the elastic parameters are estimated via the
pylops.avo.prestack.PrestackInversion
module.
Once again, a two-steps inversion strategy can also be used to deal
with the case of noisy data.
import matplotlib.pyplot as plt
import numpy as np
from scipy.signal import filtfilt
import pylops
from pylops.utils.wavelets import ricker
plt.close("all")
np.random.seed(0)
Let’s start with a 1d example. A synthetic profile of acoustic impedance
is created and data is modelled using both the dense and linear operator
version of pylops.avo.prestack.PrestackLinearModelling
operator
# sphinx_gallery_thumbnail_number = 5
# model
nt0 = 301
dt0 = 0.004
t0 = np.arange(nt0) * dt0
vp = 1200 + np.arange(nt0) + filtfilt(np.ones(5) / 5.0, 1, np.random.normal(0, 80, nt0))
vs = 600 + vp / 2 + filtfilt(np.ones(5) / 5.0, 1, np.random.normal(0, 20, nt0))
rho = 1000 + vp + filtfilt(np.ones(5) / 5.0, 1, np.random.normal(0, 30, nt0))
vp[131:] += 500
vs[131:] += 200
rho[131:] += 100
vsvp = 0.5
m = np.stack((np.log(vp), np.log(vs), np.log(rho)), axis=1)
# background model
nsmooth = 50
mback = filtfilt(np.ones(nsmooth) / float(nsmooth), 1, m, axis=0)
# angles
ntheta = 21
thetamin, thetamax = 0, 40
theta = np.linspace(thetamin, thetamax, ntheta)
# wavelet
ntwav = 41
wav = ricker(t0[: ntwav // 2 + 1], 15)[0]
# lop
PPop = pylops.avo.prestack.PrestackLinearModelling(
wav, theta, vsvp=vsvp, nt0=nt0, linearization="akirich"
)
# dense
PPop_dense = pylops.avo.prestack.PrestackLinearModelling(
wav, theta, vsvp=vsvp, nt0=nt0, linearization="akirich", explicit=True
)
# data lop
dPP = PPop * m.ravel()
dPP = dPP.reshape(nt0, ntheta)
# data dense
dPP_dense = PPop_dense * m.T.ravel()
dPP_dense = dPP_dense.reshape(ntheta, nt0).T
# noisy data
dPPn_dense = dPP_dense + np.random.normal(0, 1e-2, dPP_dense.shape)
We can now invert our data and retrieve elastic profiles for both noise-free
and noisy data using pylops.avo.prestack.PrestackInversion
.
# dense
minv_dense, dPP_dense_res = pylops.avo.prestack.PrestackInversion(
dPP_dense,
theta,
wav,
m0=mback,
linearization="akirich",
explicit=True,
returnres=True,
**dict(cond=1e-10)
)
# lop
minv, dPP_res = pylops.avo.prestack.PrestackInversion(
dPP,
theta,
wav,
m0=mback,
linearization="akirich",
explicit=False,
returnres=True,
**dict(damp=1e-10, iter_lim=2000)
)
# dense noisy
minv_dense_noise, dPPn_dense_res = pylops.avo.prestack.PrestackInversion(
dPPn_dense,
theta,
wav,
m0=mback,
linearization="akirich",
explicit=True,
returnres=True,
**dict(cond=1e-1)
)
# lop noisy (with vertical smoothing)
minv_noise, dPPn_res = pylops.avo.prestack.PrestackInversion(
dPPn_dense,
theta,
wav,
m0=mback,
linearization="akirich",
explicit=False,
epsR=5e-1,
returnres=True,
**dict(damp=1e-1, iter_lim=100)
)
The data, inverted models and residuals are now displayed.
# data and model
fig, (axd, axdn, axvp, axvs, axrho) = plt.subplots(1, 5, figsize=(8, 5), sharey=True)
axd.imshow(
dPP_dense,
cmap="gray",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=-np.abs(dPP_dense).max(),
vmax=np.abs(dPP_dense).max(),
)
axd.set_title("Data")
axd.axis("tight")
axdn.imshow(
dPPn_dense,
cmap="gray",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=-np.abs(dPP_dense).max(),
vmax=np.abs(dPP_dense).max(),
)
axdn.set_title("Noisy Data")
axdn.axis("tight")
axvp.plot(vp, t0, "k", lw=4, label="True")
axvp.plot(np.exp(mback[:, 0]), t0, "--r", lw=4, label="Back")
axvp.plot(np.exp(minv_dense[:, 0]), t0, "--b", lw=2, label="Inv Dense")
axvp.plot(np.exp(minv[:, 0]), t0, "--m", lw=2, label="Inv Lop")
axvp.plot(np.exp(minv_dense_noise[:, 0]), t0, "--c", lw=2, label="Noisy Dense")
axvp.plot(np.exp(minv_noise[:, 0]), t0, "--g", lw=2, label="Noisy Lop")
axvp.set_title(r"$V_P$")
axvs.plot(vs, t0, "k", lw=4, label="True")
axvs.plot(np.exp(mback[:, 1]), t0, "--r", lw=4, label="Back")
axvs.plot(np.exp(minv_dense[:, 1]), t0, "--b", lw=2, label="Inv Dense")
axvs.plot(np.exp(minv[:, 1]), t0, "--m", lw=2, label="Inv Lop")
axvs.plot(np.exp(minv_dense_noise[:, 1]), t0, "--c", lw=2, label="Noisy Dense")
axvs.plot(np.exp(minv_noise[:, 1]), t0, "--g", lw=2, label="Noisy Lop")
axvs.set_title(r"$V_S$")
axrho.plot(rho, t0, "k", lw=4, label="True")
axrho.plot(np.exp(mback[:, 2]), t0, "--r", lw=4, label="Back")
axrho.plot(np.exp(minv_dense[:, 2]), t0, "--b", lw=2, label="Inv Dense")
axrho.plot(np.exp(minv[:, 2]), t0, "--m", lw=2, label="Inv Lop")
axrho.plot(np.exp(minv_dense_noise[:, 2]), t0, "--c", lw=2, label="Noisy Dense")
axrho.plot(np.exp(minv_noise[:, 2]), t0, "--g", lw=2, label="Noisy Lop")
axrho.set_title(r"$\rho$")
axrho.legend(loc="center left", bbox_to_anchor=(1, 0.5))
axd.axis("tight")
plt.tight_layout()
# residuals
fig, axs = plt.subplots(1, 4, figsize=(8, 5), sharey=True)
fig.suptitle("Residuals", fontsize=14, fontweight="bold", y=0.95)
im = axs[0].imshow(
dPP_dense_res,
cmap="gray",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=-0.1,
vmax=0.1,
)
axs[0].set_title("Dense")
axs[0].set_xlabel(r"$\theta$")
axs[0].set_ylabel("t[s]")
axs[0].axis("tight")
axs[1].imshow(
dPP_res,
cmap="gray",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=-0.1,
vmax=0.1,
)
axs[1].set_title("Lop")
axs[1].set_xlabel(r"$\theta$")
axs[1].axis("tight")
axs[2].imshow(
dPPn_dense_res,
cmap="gray",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=-0.1,
vmax=0.1,
)
axs[2].set_title("Noisy Dense")
axs[2].set_xlabel(r"$\theta$")
axs[2].axis("tight")
axs[3].imshow(
dPPn_res,
cmap="gray",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=-0.1,
vmax=0.1,
)
axs[3].set_title("Noisy Lop")
axs[3].set_xlabel(r"$\theta$")
axs[3].axis("tight")
plt.tight_layout()
plt.subplots_adjust(top=0.85)
Finally before moving to the 2d example, we consider the case when both PP and PS data are available. A joint PP-PS inversion can be easily solved as follows.
PSop = pylops.avo.prestack.PrestackLinearModelling(
2 * wav, theta, vsvp=vsvp, nt0=nt0, linearization="ps"
)
PPPSop = pylops.VStack((PPop, PSop))
# data
dPPPS = PPPSop * m.ravel()
dPPPS = dPPPS.reshape(2, nt0, ntheta)
dPPPSn = dPPPS + np.random.normal(0, 1e-2, dPPPS.shape)
# Invert
minvPPSP, dPPPS_res = pylops.avo.prestack.PrestackInversion(
dPPPS,
theta,
[wav, 2 * wav],
m0=mback,
linearization=["fatti", "ps"],
epsR=5e-1,
returnres=True,
**dict(damp=1e-1, iter_lim=100)
)
# Data and model
fig, (axd, axdn, axvp, axvs, axrho) = plt.subplots(1, 5, figsize=(8, 5), sharey=True)
axd.imshow(
dPPPSn[0],
cmap="gray",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=-np.abs(dPPPSn[0]).max(),
vmax=np.abs(dPPPSn[0]).max(),
)
axd.set_title("PP Data")
axd.axis("tight")
axdn.imshow(
dPPPSn[1],
cmap="gray",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=-np.abs(dPPPSn[1]).max(),
vmax=np.abs(dPPPSn[1]).max(),
)
axdn.set_title("PS Data")
axdn.axis("tight")
axvp.plot(vp, t0, "k", lw=4, label="True")
axvp.plot(np.exp(mback[:, 0]), t0, "--r", lw=4, label="Back")
axvp.plot(np.exp(minv_noise[:, 0]), t0, "--g", lw=2, label="PP")
axvp.plot(np.exp(minvPPSP[:, 0]), t0, "--b", lw=2, label="PP+PS")
axvp.set_title(r"$V_P$")
axvs.plot(vs, t0, "k", lw=4, label="True")
axvs.plot(np.exp(mback[:, 1]), t0, "--r", lw=4, label="Back")
axvs.plot(np.exp(minv_noise[:, 1]), t0, "--g", lw=2, label="PP")
axvs.plot(np.exp(minvPPSP[:, 1]), t0, "--b", lw=2, label="PP+PS")
axvs.set_title(r"$V_S$")
axrho.plot(rho, t0, "k", lw=4, label="True")
axrho.plot(np.exp(mback[:, 2]), t0, "--r", lw=4, label="Back")
axrho.plot(np.exp(minv_noise[:, 2]), t0, "--g", lw=2, label="PP")
axrho.plot(np.exp(minvPPSP[:, 2]), t0, "--b", lw=2, label="PP+PS")
axrho.set_title(r"$\rho$")
axrho.legend(loc="center left", bbox_to_anchor=(1, 0.5))
axd.axis("tight")
plt.tight_layout()

We move now to a 2d example. First of all the model is loaded and data generated.
# model
inputfile = "../testdata/avo/poststack_model.npz"
model = np.load(inputfile)
x, z = model["x"][::6] / 1000.0, model["z"][:300] / 1000.0
nx, nz = len(x), len(z)
m = 1000 * model["model"][:300, ::6]
mvp = m.copy()
mvs = m / 2
mrho = m / 3 + 300
m = np.log(np.stack((mvp, mvs, mrho), axis=1))
# smooth model
nsmoothz, nsmoothx = 30, 25
mback = filtfilt(np.ones(nsmoothz) / float(nsmoothz), 1, m, axis=0)
mback = filtfilt(np.ones(nsmoothx) / float(nsmoothx), 1, mback, axis=2)
# dense operator
PPop_dense = pylops.avo.prestack.PrestackLinearModelling(
wav,
theta,
vsvp=vsvp,
nt0=nz,
spatdims=(nx,),
linearization="akirich",
explicit=True,
)
# lop operator
PPop = pylops.avo.prestack.PrestackLinearModelling(
wav, theta, vsvp=vsvp, nt0=nz, spatdims=(nx,), linearization="akirich"
)
# data
dPP = PPop_dense * m.swapaxes(0, 1).ravel()
dPP = dPP.reshape(ntheta, nz, nx).swapaxes(0, 1)
dPPn = dPP + np.random.normal(0, 5e-2, dPP.shape)
Finally we perform the same 4 different inversions as in the post-stack tutorial (see 07. Post-stack inversion for more details).
# dense inversion with noise-free data
minv_dense = pylops.avo.prestack.PrestackInversion(
dPP, theta, wav, m0=mback, explicit=True, simultaneous=False
)
# dense inversion with noisy data
minv_dense_noisy = pylops.avo.prestack.PrestackInversion(
dPPn, theta, wav, m0=mback, explicit=True, epsI=4e-2, simultaneous=False
)
# spatially regularized lop inversion with noisy data
minv_lop_reg = pylops.avo.prestack.PrestackInversion(
dPPn,
theta,
wav,
m0=minv_dense_noisy,
explicit=False,
epsR=1e1,
**dict(damp=np.sqrt(1e-4), iter_lim=20)
)
# blockiness promoting inversion with noisy data
minv_blocky = pylops.avo.prestack.PrestackInversion(
dPPn,
theta,
wav,
m0=mback,
explicit=False,
epsR=0.4,
epsRL1=0.1,
**dict(mu=0.1, niter_outer=3, niter_inner=3, iter_lim=5, damp=1e-3)
)
Let’s now visualize the inverted elastic parameters for the different scenarios
def plotmodel(
axs,
m,
x,
z,
vmin,
vmax,
params=("VP", "VS", "Rho"),
cmap="gist_rainbow",
title=None,
):
"""Quick visualization of model"""
for ip, param in enumerate(params):
axs[ip].imshow(
m[:, ip], extent=(x[0], x[-1], z[-1], z[0]), vmin=vmin, vmax=vmax, cmap=cmap
)
axs[ip].set_title("%s - %s" % (param, title))
axs[ip].axis("tight")
plt.setp(axs[1].get_yticklabels(), visible=False)
plt.setp(axs[2].get_yticklabels(), visible=False)
# data
fig = plt.figure(figsize=(8, 9))
ax1 = plt.subplot2grid((2, 3), (0, 0), colspan=3)
ax2 = plt.subplot2grid((2, 3), (1, 0))
ax3 = plt.subplot2grid((2, 3), (1, 1), sharey=ax2)
ax4 = plt.subplot2grid((2, 3), (1, 2), sharey=ax2)
ax1.imshow(
dPP[:, 0], cmap="gray", extent=(x[0], x[-1], z[-1], z[0]), vmin=-0.4, vmax=0.4
)
ax1.vlines(
[x[nx // 5], x[nx // 2], x[4 * nx // 5]],
ymin=z[0],
ymax=z[-1],
colors="w",
linestyles="--",
)
ax1.set_xlabel("x [km]")
ax1.set_ylabel("z [km]")
ax1.set_title(r"Stack ($\theta$=0)")
ax1.axis("tight")
ax2.imshow(
dPP[:, :, nx // 5],
cmap="gray",
extent=(theta[0], theta[-1], z[-1], z[0]),
vmin=-0.4,
vmax=0.4,
)
ax2.set_xlabel(r"$\theta$")
ax2.set_ylabel("z [km]")
ax2.set_title(r"Gather (x=%.2f)" % x[nx // 5])
ax2.axis("tight")
ax3.imshow(
dPP[:, :, nx // 2],
cmap="gray",
extent=(theta[0], theta[-1], z[-1], z[0]),
vmin=-0.4,
vmax=0.4,
)
ax3.set_xlabel(r"$\theta$")
ax3.set_title(r"Gather (x=%.2f)" % x[nx // 2])
ax3.axis("tight")
ax4.imshow(
dPP[:, :, 4 * nx // 5],
cmap="gray",
extent=(theta[0], theta[-1], z[-1], z[0]),
vmin=-0.4,
vmax=0.4,
)
ax4.set_xlabel(r"$\theta$")
ax4.set_title(r"Gather (x=%.2f)" % x[4 * nx // 5])
ax4.axis("tight")
plt.setp(ax3.get_yticklabels(), visible=False)
plt.setp(ax4.get_yticklabels(), visible=False)
# noisy data
fig = plt.figure(figsize=(8, 9))
ax1 = plt.subplot2grid((2, 3), (0, 0), colspan=3)
ax2 = plt.subplot2grid((2, 3), (1, 0))
ax3 = plt.subplot2grid((2, 3), (1, 1), sharey=ax2)
ax4 = plt.subplot2grid((2, 3), (1, 2), sharey=ax2)
ax1.imshow(
dPPn[:, 0], cmap="gray", extent=(x[0], x[-1], z[-1], z[0]), vmin=-0.4, vmax=0.4
)
ax1.vlines(
[x[nx // 5], x[nx // 2], x[4 * nx // 5]],
ymin=z[0],
ymax=z[-1],
colors="w",
linestyles="--",
)
ax1.set_xlabel("x [km]")
ax1.set_ylabel("z [km]")
ax1.set_title(r"Noisy Stack ($\theta$=0)")
ax1.axis("tight")
ax2.imshow(
dPPn[:, :, nx // 5],
cmap="gray",
extent=(theta[0], theta[-1], z[-1], z[0]),
vmin=-0.4,
vmax=0.4,
)
ax2.set_xlabel(r"$\theta$")
ax2.set_ylabel("z [km]")
ax2.set_title(r"Gather (x=%.2f)" % x[nx // 5])
ax2.axis("tight")
ax3.imshow(
dPPn[:, :, nx // 2],
cmap="gray",
extent=(theta[0], theta[-1], z[-1], z[0]),
vmin=-0.4,
vmax=0.4,
)
ax3.set_title(r"Gather (x=%.2f)" % x[nx // 2])
ax3.set_xlabel(r"$\theta$")
ax3.axis("tight")
ax4.imshow(
dPPn[:, :, 4 * nx // 5],
cmap="gray",
extent=(theta[0], theta[-1], z[-1], z[0]),
vmin=-0.4,
vmax=0.4,
)
ax4.set_xlabel(r"$\theta$")
ax4.set_title(r"Gather (x=%.2f)" % x[4 * nx // 5])
ax4.axis("tight")
plt.setp(ax3.get_yticklabels(), visible=False)
plt.setp(ax4.get_yticklabels(), visible=False)
# inverted models
fig, axs = plt.subplots(6, 3, figsize=(8, 19))
fig.suptitle("Model", fontsize=12, fontweight="bold", y=0.95)
plotmodel(axs[0], m, x, z, m.min(), m.max(), title="True")
plotmodel(axs[1], mback, x, z, m.min(), m.max(), title="Back")
plotmodel(axs[2], minv_dense, x, z, m.min(), m.max(), title="Dense")
plotmodel(axs[3], minv_dense_noisy, x, z, m.min(), m.max(), title="Dense noisy")
plotmodel(axs[4], minv_lop_reg, x, z, m.min(), m.max(), title="Lop regularized")
plotmodel(axs[5], minv_blocky, x, z, m.min(), m.max(), title="Lop blocky")
plt.tight_layout()
plt.subplots_adjust(top=0.92)
fig, axs = plt.subplots(1, 3, figsize=(8, 7))
for ip, param in enumerate(["VP", "VS", "Rho"]):
axs[ip].plot(m[:, ip, nx // 2], z, "k", lw=4, label="True")
axs[ip].plot(mback[:, ip, nx // 2], z, "--r", lw=4, label="Back")
axs[ip].plot(minv_dense[:, ip, nx // 2], z, "--b", lw=2, label="Inv Dense")
axs[ip].plot(
minv_dense_noisy[:, ip, nx // 2], z, "--m", lw=2, label="Inv Dense noisy"
)
axs[ip].plot(
minv_lop_reg[:, ip, nx // 2], z, "--g", lw=2, label="Inv Lop regularized"
)
axs[ip].plot(minv_blocky[:, ip, nx // 2], z, "--y", lw=2, label="Inv Lop blocky")
axs[ip].set_title(param)
axs[ip].invert_yaxis()
axs[2].legend(loc=8, fontsize="small")
plt.tight_layout()
While the background model m0
has been provided in all the examples so
far, it is worth showing that the module
pylops.avo.prestack.PrestackInversion
can also produce so-called
relative elastic parameters (i.e., variations from an average medium
property) when the background model m0
is not available.
dminv = pylops.avo.prestack.PrestackInversion(
dPP, theta, wav, m0=None, explicit=True, simultaneous=False
)
fig, axs = plt.subplots(1, 3, figsize=(8, 3))
plotmodel(axs, dminv, x, z, -dminv.max(), dminv.max(), cmap="seismic", title="relative")
fig, axs = plt.subplots(1, 3, figsize=(8, 7))
for ip, param in enumerate(["VP", "VS", "Rho"]):
axs[ip].plot(dminv[:, ip, nx // 2], z, "k", lw=2)
axs[ip].set_title(param)
axs[ip].invert_yaxis()
Total running time of the script: ( 0 minutes 31.440 seconds)
Note
Click here to download the full example code
09. Multi-Dimensional Deconvolution¶
This example shows how to set-up and run the
pylops.waveeqprocessing.MDD
inversion using synthetic data.
import warnings
import matplotlib.pyplot as plt
import numpy as np
import pylops
from pylops.utils.seismicevents import hyperbolic2d, makeaxis
from pylops.utils.tapers import taper3d
from pylops.utils.wavelets import ricker
warnings.filterwarnings("ignore")
plt.close("all")
# sphinx_gallery_thumbnail_number = 5
Let’s start by creating a set of hyperbolic events to be used as our MDC kernel
# Input parameters
par = {
"ox": -150,
"dx": 10,
"nx": 31,
"oy": -250,
"dy": 10,
"ny": 51,
"ot": 0,
"dt": 0.004,
"nt": 300,
"f0": 20,
"nfmax": 200,
}
t0_m = [0.2]
vrms_m = [700.0]
amp_m = [1.0]
t0_G = [0.2, 0.5, 0.7]
vrms_G = [800.0, 1200.0, 1500.0]
amp_G = [1.0, 0.6, 0.5]
# Taper
tap = taper3d(par["nt"], [par["ny"], par["nx"]], (5, 5), tapertype="hanning")
# Create axis
t, t2, x, y = makeaxis(par)
# Create wavelet
wav = ricker(t[:41], f0=par["f0"])[0]
# Generate model
m, mwav = hyperbolic2d(x, t, t0_m, vrms_m, amp_m, wav)
# Generate operator
G, Gwav = np.zeros((par["ny"], par["nx"], par["nt"])), np.zeros(
(par["ny"], par["nx"], par["nt"])
)
for iy, y0 in enumerate(y):
G[iy], Gwav[iy] = hyperbolic2d(x - y0, t, t0_G, vrms_G, amp_G, wav)
G, Gwav = G * tap, Gwav * tap
# Add negative part to data and model
m = np.concatenate((np.zeros((par["nx"], par["nt"] - 1)), m), axis=-1)
mwav = np.concatenate((np.zeros((par["nx"], par["nt"] - 1)), mwav), axis=-1)
Gwav2 = np.concatenate((np.zeros((par["ny"], par["nx"], par["nt"] - 1)), Gwav), axis=-1)
# Define MDC linear operator
Gwav_fft = np.fft.rfft(Gwav2, 2 * par["nt"] - 1, axis=-1)
Gwav_fft = (Gwav_fft[..., : par["nfmax"]]).transpose(2, 0, 1)
MDCop = pylops.waveeqprocessing.MDC(
Gwav_fft,
nt=2 * par["nt"] - 1,
nv=1,
dt=0.004,
dr=1.0,
)
# Create data
d = MDCop * m.T.ravel()
d = d.reshape(2 * par["nt"] - 1, par["ny"]).T
Let’s display what we have so far: operator, input model, and data
fig, axs = plt.subplots(1, 2, figsize=(8, 6))
axs[0].imshow(
Gwav2[int(par["ny"] / 2)].T,
aspect="auto",
interpolation="nearest",
cmap="gray",
vmin=-np.abs(Gwav2.max()),
vmax=np.abs(Gwav2.max()),
extent=(x.min(), x.max(), t2.max(), t2.min()),
)
axs[0].set_title("G - inline view", fontsize=15)
axs[0].set_xlabel(r"$x_R$")
axs[1].set_ylabel(r"$t$")
axs[1].imshow(
Gwav2[:, int(par["nx"] / 2)].T,
aspect="auto",
interpolation="nearest",
cmap="gray",
vmin=-np.abs(Gwav2.max()),
vmax=np.abs(Gwav2.max()),
extent=(y.min(), y.max(), t2.max(), t2.min()),
)
axs[1].set_title("G - inline view", fontsize=15)
axs[1].set_xlabel(r"$x_S$")
axs[1].set_ylabel(r"$t$")
fig.tight_layout()
fig, axs = plt.subplots(1, 2, figsize=(8, 6))
axs[0].imshow(
mwav.T,
aspect="auto",
interpolation="nearest",
cmap="gray",
vmin=-np.abs(mwav.max()),
vmax=np.abs(mwav.max()),
extent=(x.min(), x.max(), t2.max(), t2.min()),
)
axs[0].set_title(r"$m$", fontsize=15)
axs[0].set_xlabel(r"$x_R$")
axs[1].set_ylabel(r"$t$")
axs[1].imshow(
d.T,
aspect="auto",
interpolation="nearest",
cmap="gray",
vmin=-np.abs(d.max()),
vmax=np.abs(d.max()),
extent=(x.min(), x.max(), t2.max(), t2.min()),
)
axs[1].set_title(r"$d$", fontsize=15)
axs[1].set_xlabel(r"$x_S$")
axs[1].set_ylabel(r"$t$")
fig.tight_layout()
We are now ready to feed our operator to
pylops.waveeqprocessing.MDD
and invert back for our input model
minv, madj, psfinv, psfadj = pylops.waveeqprocessing.MDD(
Gwav,
d[:, par["nt"] - 1 :],
dt=par["dt"],
dr=par["dx"],
nfmax=par["nfmax"],
wav=wav,
twosided=True,
add_negative=True,
adjoint=True,
psf=True,
dottest=False,
**dict(damp=1e-4, iter_lim=20, show=0)
)
fig = plt.figure(figsize=(8, 6))
ax1 = plt.subplot2grid((1, 5), (0, 0), colspan=2)
ax2 = plt.subplot2grid((1, 5), (0, 2), colspan=2)
ax3 = plt.subplot2grid((1, 5), (0, 4))
ax1.imshow(
madj.T,
aspect="auto",
interpolation="nearest",
cmap="gray",
vmin=-np.abs(madj.max()),
vmax=np.abs(madj.max()),
extent=(x.min(), x.max(), t2.max(), t2.min()),
)
ax1.set_title("Adjoint m", fontsize=15)
ax1.set_xlabel(r"$x_V$")
axs[1].set_ylabel(r"$t$")
ax2.imshow(
minv.T,
aspect="auto",
interpolation="nearest",
cmap="gray",
vmin=-np.abs(minv.max()),
vmax=np.abs(minv.max()),
extent=(x.min(), x.max(), t2.max(), t2.min()),
)
ax2.set_title("Inverted m", fontsize=15)
ax2.set_xlabel(r"$x_V$")
axs[1].set_ylabel(r"$t$")
ax3.plot(
madj[int(par["nx"] / 2)] / np.abs(madj[int(par["nx"] / 2)]).max(), t2, "r", lw=5
)
ax3.plot(
minv[int(par["nx"] / 2)] / np.abs(minv[int(par["nx"] / 2)]).max(), t2, "k", lw=3
)
ax3.set_ylim([t2[-1], t2[0]])
fig.tight_layout()
fig, axs = plt.subplots(1, 2, figsize=(8, 6))
axs[0].imshow(
psfinv[int(par["nx"] / 2)].T,
aspect="auto",
interpolation="nearest",
vmin=-np.abs(psfinv.max()),
vmax=np.abs(psfinv.max()),
cmap="gray",
extent=(x.min(), x.max(), t2.max(), t2.min()),
)
axs[0].set_title("Inverted psf - inline view", fontsize=15)
axs[0].set_xlabel(r"$x_V$")
axs[1].set_ylabel(r"$t$")
axs[1].imshow(
psfinv[:, int(par["nx"] / 2)].T,
aspect="auto",
interpolation="nearest",
vmin=-np.abs(psfinv.max()),
vmax=np.abs(psfinv.max()),
cmap="gray",
extent=(y.min(), y.max(), t2.max(), t2.min()),
)
axs[1].set_title("Inverted psf - xline view", fontsize=15)
axs[1].set_xlabel(r"$x_V$")
axs[1].set_ylabel(r"$t$")
fig.tight_layout()
We repeat the same procedure but this time we will add a preconditioning
by means of causality_precond
parameter, which enforces the inverted
model to be zero in the negative part of the time axis (as expected by
theory). This preconditioning will have the effect of speeding up the
convergence of the iterative solver and thus reduce the computation time
of the deconvolution
minvprec = pylops.waveeqprocessing.MDD(
Gwav,
d[:, par["nt"] - 1 :],
dt=par["dt"],
dr=par["dx"],
nfmax=par["nfmax"],
wav=wav,
twosided=True,
add_negative=True,
adjoint=False,
psf=False,
causality_precond=True,
dottest=False,
**dict(damp=1e-4, iter_lim=50, show=0)
)
fig = plt.figure(figsize=(8, 6))
ax1 = plt.subplot2grid((1, 5), (0, 0), colspan=2)
ax2 = plt.subplot2grid((1, 5), (0, 2), colspan=2)
ax3 = plt.subplot2grid((1, 5), (0, 4))
ax1.imshow(
madj.T,
aspect="auto",
interpolation="nearest",
cmap="gray",
vmin=-np.abs(madj.max()),
vmax=np.abs(madj.max()),
extent=(x.min(), x.max(), t2.max(), t2.min()),
)
ax1.set_title("Adjoint m", fontsize=15)
ax1.set_xlabel(r"$x_V$")
axs[1].set_ylabel(r"$t$")
ax2.imshow(
minvprec.T,
aspect="auto",
interpolation="nearest",
cmap="gray",
vmin=-np.abs(minvprec.max()),
vmax=np.abs(minvprec.max()),
extent=(x.min(), x.max(), t2.max(), t2.min()),
)
ax2.set_title("Inverted m", fontsize=15)
ax2.set_xlabel(r"$x_V$")
axs[1].set_ylabel(r"$t$")
ax3.plot(
madj[int(par["nx"] / 2)] / np.abs(madj[int(par["nx"] / 2)]).max(), t2, "r", lw=5
)
ax3.plot(
minvprec[int(par["nx"] / 2)] / np.abs(minv[int(par["nx"] / 2)]).max(), t2, "k", lw=3
)
ax3.set_ylim([t2[-1], t2[0]])
fig.tight_layout()

Total running time of the script: ( 0 minutes 11.100 seconds)
Note
Click here to download the full example code
10. Marchenko redatuming by inversion¶
This example shows how to set-up and run the
pylops.waveeqprocessing.Marchenko
inversion using synthetic data.
# sphinx_gallery_thumbnail_number = 5
# pylint: disable=C0103
import warnings
import matplotlib.pyplot as plt
import numpy as np
from scipy.signal import convolve
from pylops.waveeqprocessing import Marchenko
warnings.filterwarnings("ignore")
plt.close("all")
Let’s start by defining some input parameters and loading the test data
# Input parameters
inputfile = "../testdata/marchenko/input.npz"
vel = 2400.0 # velocity
toff = 0.045 # direct arrival time shift
nsmooth = 10 # time window smoothing
nfmax = 1000 # max frequency for MDC (#samples)
niter = 10 # iterations
inputdata = np.load(inputfile)
# Receivers
r = inputdata["r"]
nr = r.shape[1]
dr = r[0, 1] - r[0, 0]
# Sources
s = inputdata["s"]
ns = s.shape[1]
ds = s[0, 1] - s[0, 0]
# Virtual points
vs = inputdata["vs"]
# Density model
rho = inputdata["rho"]
z, x = inputdata["z"], inputdata["x"]
# Reflection data (R[s, r, t]) and subsurface fields
R = inputdata["R"][:, :, :-100]
R = np.swapaxes(R, 0, 1) # just because of how the data was saved
Gsub = inputdata["Gsub"][:-100]
G0sub = inputdata["G0sub"][:-100]
wav = inputdata["wav"]
wav_c = np.argmax(wav)
t = inputdata["t"][:-100]
ot, dt, nt = t[0], t[1] - t[0], len(t)
Gsub = np.apply_along_axis(convolve, 0, Gsub, wav, mode="full")
Gsub = Gsub[wav_c:][:nt]
G0sub = np.apply_along_axis(convolve, 0, G0sub, wav, mode="full")
G0sub = G0sub[wav_c:][:nt]
plt.figure(figsize=(10, 5))
plt.imshow(rho, cmap="gray", extent=(x[0], x[-1], z[-1], z[0]))
plt.scatter(s[0, 5::10], s[1, 5::10], marker="*", s=150, c="r", edgecolors="k")
plt.scatter(r[0, ::10], r[1, ::10], marker="v", s=150, c="b", edgecolors="k")
plt.scatter(vs[0], vs[1], marker=".", s=250, c="m", edgecolors="k")
plt.axis("tight")
plt.xlabel("x [m]")
plt.ylabel("y [m]")
plt.title("Model and Geometry")
plt.xlim(x[0], x[-1])
fig, axs = plt.subplots(1, 3, sharey=True, figsize=(12, 7))
axs[0].imshow(
R[0].T, cmap="gray", vmin=-1e-2, vmax=1e-2, extent=(r[0, 0], r[0, -1], t[-1], t[0])
)
axs[0].set_title("R shot=0")
axs[0].set_xlabel(r"$x_R$")
axs[0].set_ylabel(r"$t$")
axs[0].axis("tight")
axs[0].set_ylim(1.5, 0)
axs[1].imshow(
R[ns // 2].T,
cmap="gray",
vmin=-1e-2,
vmax=1e-2,
extent=(r[0, 0], r[0, -1], t[-1], t[0]),
)
axs[1].set_title("R shot=%d" % (ns // 2))
axs[1].set_xlabel(r"$x_R$")
axs[1].set_ylabel(r"$t$")
axs[1].axis("tight")
axs[1].set_ylim(1.5, 0)
axs[2].imshow(
R[-1].T, cmap="gray", vmin=-1e-2, vmax=1e-2, extent=(r[0, 0], r[0, -1], t[-1], t[0])
)
axs[2].set_title("R shot=%d" % ns)
axs[2].set_xlabel(r"$x_R$")
axs[2].axis("tight")
axs[2].set_ylim(1.5, 0)
fig.tight_layout()
fig, axs = plt.subplots(1, 2, sharey=True, figsize=(8, 6))
axs[0].imshow(
Gsub, cmap="gray", vmin=-1e6, vmax=1e6, extent=(r[0, 0], r[0, -1], t[-1], t[0])
)
axs[0].set_title("G")
axs[0].set_xlabel(r"$x_R$")
axs[0].set_ylabel(r"$t$")
axs[0].axis("tight")
axs[0].set_ylim(1.5, 0)
axs[1].imshow(
G0sub, cmap="gray", vmin=-1e6, vmax=1e6, extent=(r[0, 0], r[0, -1], t[-1], t[0])
)
axs[1].set_title("G0")
axs[1].set_xlabel(r"$x_R$")
axs[1].set_ylabel(r"$t$")
axs[1].axis("tight")
axs[1].set_ylim(1.5, 0)
fig.tight_layout()
Let’s now create an object of the
pylops.waveeqprocessing.Marchenko
class and apply redatuming
for a single subsurface point vs
.
# direct arrival window
trav = np.sqrt((vs[0] - r[0]) ** 2 + (vs[1] - r[1]) ** 2) / vel
MarchenkoWM = Marchenko(
R, dt=dt, dr=dr, nfmax=nfmax, wav=wav, toff=toff, nsmooth=nsmooth
)
(
f1_inv_minus,
f1_inv_plus,
p0_minus,
g_inv_minus,
g_inv_plus,
) = MarchenkoWM.apply_onepoint(
trav,
G0=G0sub.T,
rtm=True,
greens=True,
dottest=True,
**dict(iter_lim=niter, show=True)
)
g_inv_tot = g_inv_minus + g_inv_plus
Dot test passed, v^H(Opu)=405.16509639614344 - u^H(Op^Hv)=405.1650963961446
Dot test passed, v^H(Opu)=172.06507561051328 - u^H(Op^Hv)=172.06507561051316
LSQR Least-squares solution of Ax = b
The matrix A has 282598 rows and 282598 columns
damp = 0.00000000000000e+00 calc_var = 0
atol = 1.00e-06 conlim = 1.00e+08
btol = 1.00e-06 iter_lim = 10
Itn x[0] r1norm r2norm Compatible LS Norm A Cond A
0 0.00000e+00 3.134e+07 3.134e+07 1.0e+00 3.3e-08
1 0.00000e+00 1.374e+07 1.374e+07 4.4e-01 9.3e-01 1.1e+00 1.0e+00
2 0.00000e+00 7.770e+06 7.770e+06 2.5e-01 3.9e-01 1.8e+00 2.2e+00
3 0.00000e+00 5.750e+06 5.750e+06 1.8e-01 3.3e-01 2.1e+00 3.4e+00
4 0.00000e+00 3.930e+06 3.930e+06 1.3e-01 3.4e-01 2.5e+00 5.1e+00
5 0.00000e+00 3.042e+06 3.042e+06 9.7e-02 2.6e-01 2.9e+00 6.8e+00
6 0.00000e+00 2.423e+06 2.423e+06 7.7e-02 2.2e-01 3.3e+00 8.6e+00
7 0.00000e+00 1.675e+06 1.675e+06 5.3e-02 2.5e-01 3.6e+00 1.1e+01
8 0.00000e+00 1.248e+06 1.248e+06 4.0e-02 2.0e-01 3.9e+00 1.3e+01
9 0.00000e+00 1.004e+06 1.004e+06 3.2e-02 1.5e-01 4.2e+00 1.4e+01
10 0.00000e+00 7.762e+05 7.762e+05 2.5e-02 1.8e-01 4.4e+00 1.6e+01
LSQR finished
The iteration limit has been reached
istop = 7 r1norm = 7.8e+05 anorm = 4.4e+00 arnorm = 6.1e+05
itn = 10 r2norm = 7.8e+05 acond = 1.6e+01 xnorm = 3.6e+07
We can now compare the result of Marchenko redatuming via LSQR with standard redatuming
fig, axs = plt.subplots(1, 3, sharey=True, figsize=(12, 7))
axs[0].imshow(
p0_minus.T,
cmap="gray",
vmin=-5e5,
vmax=5e5,
extent=(r[0, 0], r[0, -1], t[-1], -t[-1]),
)
axs[0].set_title(r"$p_0^-$")
axs[0].set_xlabel(r"$x_R$")
axs[0].set_ylabel(r"$t$")
axs[0].axis("tight")
axs[0].set_ylim(1.2, 0)
axs[1].imshow(
g_inv_minus.T,
cmap="gray",
vmin=-5e5,
vmax=5e5,
extent=(r[0, 0], r[0, -1], t[-1], -t[-1]),
)
axs[1].set_title(r"$g^-$")
axs[1].set_xlabel(r"$x_R$")
axs[1].set_ylabel(r"$t$")
axs[1].axis("tight")
axs[1].set_ylim(1.2, 0)
axs[2].imshow(
g_inv_plus.T,
cmap="gray",
vmin=-5e5,
vmax=5e5,
extent=(r[0, 0], r[0, -1], t[-1], -t[-1]),
)
axs[2].set_title(r"$g^+$")
axs[2].set_xlabel(r"$x_R$")
axs[2].set_ylabel(r"$t$")
axs[2].axis("tight")
axs[2].set_ylim(1.2, 0)
fig.tight_layout()
fig = plt.figure(figsize=(12, 7))
ax1 = plt.subplot2grid((1, 5), (0, 0), colspan=2)
ax2 = plt.subplot2grid((1, 5), (0, 2), colspan=2)
ax3 = plt.subplot2grid((1, 5), (0, 4))
ax1.imshow(
Gsub, cmap="gray", vmin=-5e5, vmax=5e5, extent=(r[0, 0], r[0, -1], t[-1], t[0])
)
ax1.set_title(r"$G_{true}$")
axs[0].set_xlabel(r"$x_R$")
axs[0].set_ylabel(r"$t$")
ax1.axis("tight")
ax1.set_ylim(1.2, 0)
ax2.imshow(
g_inv_tot.T,
cmap="gray",
vmin=-5e5,
vmax=5e5,
extent=(r[0, 0], r[0, -1], t[-1], -t[-1]),
)
ax2.set_title(r"$G_{est}$")
axs[1].set_xlabel(r"$x_R$")
axs[1].set_ylabel(r"$t$")
ax2.axis("tight")
ax2.set_ylim(1.2, 0)
ax3.plot(Gsub[:, nr // 2] / Gsub.max(), t, "r", lw=5)
ax3.plot(g_inv_tot[nr // 2, nt - 1 :] / g_inv_tot.max(), t, "k", lw=3)
ax3.set_ylim(1.2, 0)
fig.tight_layout()
Note that Marchenko redatuming can also be applied simultaneously
to multiple subsurface points. Use
pylops.waveeqprocessing.Marchenko.apply_multiplepoints
instead of
pylops.waveeqprocessing.Marchenko.apply_onepoint
.
Total running time of the script: ( 0 minutes 7.702 seconds)
Note
Click here to download the full example code
11. Radon filtering¶
In this example we will be taking advantage of the
pylops.signalprocessing.Radon2D
operator to perform filtering of
unwanted events from a seismic data. For those of you not familiar with seismic
data, let’s imagine that we have a data composed of a certain number of flat
events and a parabolic event , we are after a transform that allows us to
separate such an event from the others and filter it out.
Those of you with a geophysics background may immediately realize this
is the case of seismic angle (or offset) gathers after migration and those
events with parabolic moveout are generally residual multiples that we would
like to suppress prior to performing further analysis of our data.
The Radon transform is actually a very good transform to perform such a separation. We can thus devise a simple workflow that takes our data as input, applies a Radon transform, filters some of the events out and goes back to the original domain.
import matplotlib.pyplot as plt
import numpy as np
import pylops
from pylops.utils.wavelets import ricker
plt.close("all")
np.random.seed(0)
Let’s first create a data composed on 3 linear events and a parabolic event.
par = {"ox": 0, "dx": 2, "nx": 121, "ot": 0, "dt": 0.004, "nt": 100, "f0": 30}
# linear events
v = 1500 # m/s
t0 = [0.1, 0.2, 0.3] # s
theta = [0, 0, 0]
amp = [1.0, -2, 0.5]
# parabolic event
tp0 = [0.13] # s
px = [0] # s/m
pxx = [5e-7] # s²/m²
ampp = [0.7]
# create axis
taxis, taxis2, xaxis, yaxis = pylops.utils.seismicevents.makeaxis(par)
# create wavelet
wav = ricker(taxis[:41], f0=par["f0"])[0]
# generate model
y = (
pylops.utils.seismicevents.linear2d(xaxis, taxis, v, t0, theta, amp, wav)[1]
+ pylops.utils.seismicevents.parabolic2d(xaxis, taxis, tp0, px, pxx, ampp, wav)[1]
)
We can now create the pylops.signalprocessing.Radon2D
operator.
We also apply its adjoint to the data to obtain a representation of those
3 linear events overlapping to a parabolic event in the Radon domain.
Similarly, we feed the operator to a sparse solver like
pylops.optimization.sparsity.FISTA
to obtain a sparse
represention of the data in the Radon domain. At this point we try to filter
out the unwanted event. We can see how this is much easier for the sparse
transform as each event has a much more compact representation in the Radon
domain than for the adjoint transform.
# radon operator
npx = 61
pxmax = 5e-4 # s/m
px = np.linspace(-pxmax, pxmax, npx)
Rop = pylops.signalprocessing.Radon2D(
taxis, xaxis, px, kind="linear", interp="nearest", centeredh=False, dtype="float64"
)
# adjoint Radon transform
xadj = Rop.H * y
# sparse Radon transform
xinv, niter, cost = pylops.optimization.sparsity.fista(
Rop, y.ravel(), niter=15, eps=1e1
)
xinv = xinv.reshape(Rop.dims)
# filtering
xfilt = np.zeros_like(xadj)
xfilt[npx // 2 - 3 : npx // 2 + 4] = xadj[npx // 2 - 3 : npx // 2 + 4]
yfilt = Rop * xfilt
# filtering on sparse transform
xinvfilt = np.zeros_like(xinv)
xinvfilt[npx // 2 - 3 : npx // 2 + 4] = xinv[npx // 2 - 3 : npx // 2 + 4]
yinvfilt = Rop * xinvfilt
Finally we visualize our results.
pclip = 0.7
fig, axs = plt.subplots(1, 5, sharey=True, figsize=(12, 5))
axs[0].imshow(
y.T,
cmap="gray",
vmin=-pclip * np.abs(y).max(),
vmax=pclip * np.abs(y).max(),
extent=(xaxis[0], xaxis[-1], taxis[-1], taxis[0]),
)
axs[0].set(xlabel="$x$ [m]", ylabel="$t$ [s]", title="Data")
axs[0].axis("tight")
axs[1].imshow(
xadj.T,
cmap="gray",
vmin=-pclip * np.abs(xadj).max(),
vmax=pclip * np.abs(xadj).max(),
extent=(px[0], px[-1], taxis[-1], taxis[0]),
)
axs[1].axvline(px[npx // 2 - 3], color="r", linestyle="--")
axs[1].axvline(px[npx // 2 + 3], color="r", linestyle="--")
axs[1].set(xlabel="$p$ [s/m]", title="Radon")
axs[1].axis("tight")
axs[2].imshow(
yfilt.T,
cmap="gray",
vmin=-pclip * np.abs(yfilt).max(),
vmax=pclip * np.abs(yfilt).max(),
extent=(xaxis[0], xaxis[-1], taxis[-1], taxis[0]),
)
axs[2].set(xlabel="$x$ [m]", title="Filtered data")
axs[2].axis("tight")
axs[3].imshow(
xinv.T,
cmap="gray",
vmin=-pclip * np.abs(xinv).max(),
vmax=pclip * np.abs(xinv).max(),
extent=(px[0], px[-1], taxis[-1], taxis[0]),
)
axs[3].axvline(px[npx // 2 - 3], color="r", linestyle="--")
axs[3].axvline(px[npx // 2 + 3], color="r", linestyle="--")
axs[3].set(xlabel="$p$ [s/m]", title="Sparse Radon")
axs[3].axis("tight")
axs[4].imshow(
yinvfilt.T,
cmap="gray",
vmin=-pclip * np.abs(y).max(),
vmax=pclip * np.abs(y).max(),
extent=(xaxis[0], xaxis[-1], taxis[-1], taxis[0]),
)
axs[4].set(xlabel="$x$ [m]", title="Sparse filtered data")
axs[4].axis("tight")
plt.tight_layout()

As expected, the Radon domain is a suitable domain for this type of filtering and the sparse transform improves the ability to filter out parabolic events with small curvature.
On the other hand, it is important to note that we have not been able to correctly preserve the amplitudes of each event. This is because the sparse Radon transform can only identify a sparsest response that explain the data within a certain threshold. For this reason a more suitable approach for preserving amplitudes could be to apply a parabolic Raodn transform with the aim of reconstructing only the unwanted event and apply an adaptive subtraction between the input data and the reconstructed unwanted event.
Total running time of the script: ( 0 minutes 26.034 seconds)
Note
Click here to download the full example code
12. Seismic regularization¶
The problem of seismic data regularization (or interpolation) is a very simple one to write, yet ill-posed and very hard to solve.
The forward modelling operator is a simple pylops.Restriction
operator which is applied along the spatial direction(s).
Here \(\mathbf{y} = [\mathbf{y}_{R_1}^T, \mathbf{y}_{R_2}^T,\ldots, \mathbf{y}_{R_N^T}]^T\) where each vector \(\mathbf{y}_{R_i}\) contains all time samples recorded in the seismic data at the specific receiver \(R_i\). Similarly, \(\mathbf{x} = [\mathbf{x}_{r_1}^T, \mathbf{x}_{r_2}^T,\ldots, \mathbf{x}_{r_M}^T]\), contains all traces at the regularly and finely sampled receiver locations \(r_i\).
By inverting such an equation we can create a regularized data with densely and regularly spatial direction(s).
import matplotlib.pyplot as plt
import numpy as np
from scipy.signal import convolve
import pylops
from pylops.utils.seismicevents import linear2d, makeaxis
from pylops.utils.wavelets import ricker
np.random.seed(0)
plt.close("all")
Let’s start by creating a very simple 2d data composed of 3 linear events input parameters
par = {"ox": 0, "dx": 2, "nx": 70, "ot": 0, "dt": 0.004, "nt": 80, "f0": 20}
v = 1500
t0_m = [0.1, 0.2, 0.28]
theta_m = [0, 30, -80]
phi_m = [0]
amp_m = [1.0, -2, 0.5]
# axis
taxis, t2, xaxis, y = makeaxis(par)
# wavelet
wav = ricker(taxis[:41], f0=par["f0"])[0]
# model
_, x = linear2d(xaxis, taxis, v, t0_m, theta_m, amp_m, wav)
We can now define the spatial locations along which the data has been sampled. In this specific example we will assume that we have access only to 40% of the ‘original’ locations.
perc_subsampling = 0.6
nxsub = int(np.round(par["nx"] * perc_subsampling))
iava = np.sort(np.random.permutation(np.arange(par["nx"]))[:nxsub])
# restriction operator
Rop = pylops.Restriction((par["nx"], par["nt"]), iava, axis=0, dtype="float64")
# data
y = Rop * x.ravel()
y = y.reshape(nxsub, par["nt"])
# mask
ymask = Rop.mask(x.ravel())
# inverse
xinv = Rop / y.ravel()
xinv = xinv.reshape(par["nx"], par["nt"])
fig, axs = plt.subplots(1, 2, sharey=True, figsize=(5, 4))
axs[0].imshow(
x.T, cmap="gray", vmin=-2, vmax=2, extent=(xaxis[0], xaxis[-1], taxis[-1], taxis[0])
)
axs[0].set_title("Model")
axs[0].axis("tight")
axs[1].imshow(
ymask.T,
cmap="gray",
vmin=-2,
vmax=2,
extent=(xaxis[0], xaxis[-1], taxis[-1], taxis[0]),
)
axs[1].set_title("Masked model")
axs[1].axis("tight")
plt.tight_layout()

As we can see, inverting the restriction operator is not possible without adding any prior information into the inverse problem. In the following we will consider two possible routes:
regularized inversion with second derivative along the spatial axis
\[J = \|\mathbf{y} - \mathbf{R} \mathbf{x}\|_2 + \epsilon_\nabla ^2 \|\nabla \mathbf{x}\|_2\]sparsity-promoting inversion with
pylops.FFT2
operator used as sparsyfing transform\[J = \|\mathbf{y} - \mathbf{R} \mathbf{F}^H \mathbf{x}\|_2 + \epsilon \|\mathbf{F}^H \mathbf{x}\|_1\]
# smooth inversion
D2op = pylops.SecondDerivative((par["nx"], par["nt"]), axis=0, dtype="float64")
xsmooth, _, _ = pylops.waveeqprocessing.SeismicInterpolation(
y,
par["nx"],
iava,
kind="spatial",
**dict(epsRs=[np.sqrt(0.1)], damp=np.sqrt(1e-4), iter_lim=50, show=0)
)
# sparse inversion with FFT2
nfft = 2**8
FFTop = pylops.signalprocessing.FFT2D(
dims=[par["nx"], par["nt"]], nffts=[nfft, nfft], sampling=[par["dx"], par["dt"]]
)
X = FFTop * x.ravel()
X = np.reshape(X, (nfft, nfft))
xl1, Xl1, cost = pylops.waveeqprocessing.SeismicInterpolation(
y,
par["nx"],
iava,
kind="fk",
nffts=(nfft, nfft),
sampling=(par["dx"], par["dt"]),
**dict(niter=50, eps=1e-1)
)
fig, axs = plt.subplots(1, 4, sharey=True, figsize=(13, 4))
axs[0].imshow(
x.T, cmap="gray", vmin=-2, vmax=2, extent=(xaxis[0], xaxis[-1], taxis[-1], taxis[0])
)
axs[0].set_title("Model")
axs[0].axis("tight")
axs[1].imshow(
ymask.T,
cmap="gray",
vmin=-2,
vmax=2,
extent=(xaxis[0], xaxis[-1], taxis[-1], taxis[0]),
)
axs[1].set_title("Masked model")
axs[1].axis("tight")
axs[2].imshow(
xsmooth.T,
cmap="gray",
vmin=-2,
vmax=2,
extent=(xaxis[0], xaxis[-1], taxis[-1], taxis[0]),
)
axs[2].set_title("Smoothed model")
axs[2].axis("tight")
axs[3].imshow(
xl1.T,
cmap="gray",
vmin=-2,
vmax=2,
extent=(xaxis[0], xaxis[-1], taxis[-1], taxis[0]),
)
axs[3].set_title("L1 model")
axs[3].axis("tight")
fig, axs = plt.subplots(1, 3, figsize=(10, 2))
axs[0].imshow(
np.fft.fftshift(np.abs(X[:, : nfft // 2 - 1]), axes=0).T,
extent=(
np.fft.fftshift(FFTop.f1)[0],
np.fft.fftshift(FFTop.f1)[-1],
FFTop.f2[nfft // 2 - 1],
FFTop.f2[0],
),
)
axs[0].set_title("Model in f-k domain")
axs[0].axis("tight")
axs[0].set_xlim(-0.1, 0.1)
axs[0].set_ylim(50, 0)
axs[1].imshow(
np.fft.fftshift(np.abs(Xl1[:, : nfft // 2 - 1]), axes=0).T,
extent=(
np.fft.fftshift(FFTop.f1)[0],
np.fft.fftshift(FFTop.f1)[-1],
FFTop.f2[nfft // 2 - 1],
FFTop.f2[0],
),
)
axs[1].set_title("Reconstructed model in f-k domain")
axs[1].axis("tight")
axs[1].set_xlim(-0.1, 0.1)
axs[1].set_ylim(50, 0)
axs[2].plot(cost, "k", lw=3)
axs[2].set_title("FISTA convergence")
plt.tight_layout()
We see how adding prior information to the inversion can help improving the
estimate of the regularized seismic data. Nevertheless, in both cases the
reconstructed data is not perfect. A better sparsyfing transform could in
fact be chosen here to be the linear
pylops.signalprocessing.Radon2D
transform in spite of the
pylops.FFT2
transform.
npx = 40
pxmax = 1e-3
px = np.linspace(-pxmax, pxmax, npx)
Radop = pylops.signalprocessing.Radon2D(taxis, xaxis, px, engine="numba")
RRop = Rop * Radop
# adjoint
Xadj_fromx = Radop.H * x.ravel()
Xadj_fromx = Xadj_fromx.reshape(npx, par["nt"])
Xadj = RRop.H * y.ravel()
Xadj = Xadj.reshape(npx, par["nt"])
# L1 inverse
xl1, Xl1, cost = pylops.waveeqprocessing.SeismicInterpolation(
y,
par["nx"],
iava,
kind="radon-linear",
spataxis=xaxis,
taxis=taxis,
paxis=px,
centeredh=True,
**dict(niter=50, eps=1e-1)
)
fig, axs = plt.subplots(2, 3, sharey=True, figsize=(12, 7))
axs[0][0].imshow(
x.T, cmap="gray", vmin=-2, vmax=2, extent=(xaxis[0], xaxis[-1], taxis[-1], taxis[0])
)
axs[0][0].set_title("Data", fontsize=12)
axs[0][0].axis("tight")
axs[0][1].imshow(
ymask.T,
cmap="gray",
vmin=-2,
vmax=2,
extent=(xaxis[0], xaxis[-1], taxis[-1], taxis[0]),
)
axs[0][1].set_title("Masked data", fontsize=12)
axs[0][1].axis("tight")
axs[0][2].imshow(
xl1.T,
cmap="gray",
vmin=-2,
vmax=2,
extent=(xaxis[0], xaxis[-1], taxis[-1], taxis[0]),
)
axs[0][2].set_title("Reconstructed data", fontsize=12)
axs[0][2].axis("tight")
axs[1][0].imshow(
Xadj_fromx.T,
cmap="gray",
vmin=-70,
vmax=70,
extent=(px[0], px[-1], taxis[-1], taxis[0]),
)
axs[1][0].set_title("Adj. Radon on data", fontsize=12)
axs[1][0].axis("tight")
axs[1][1].imshow(
Xadj.T, cmap="gray", vmin=-50, vmax=50, extent=(px[0], px[-1], taxis[-1], taxis[0])
)
axs[1][1].set_title("Adj. Radon on subsampled data", fontsize=12)
axs[1][1].axis("tight")
axs[1][2].imshow(
Xl1.T, cmap="gray", vmin=-0.2, vmax=0.2, extent=(px[0], px[-1], taxis[-1], taxis[0])
)
axs[1][2].set_title("Inverse Radon on subsampled data", fontsize=12)
axs[1][2].axis("tight")
plt.tight_layout()

Finally, let’s take now a more realistic dataset. We will use once again the
linear pylops.signalprocessing.Radon2D
transform but we will
take advantnge of the pylops.signalprocessing.Sliding2D
operator
to perform such a transform locally instead of globally to the entire
dataset.
inputfile = "../testdata/marchenko/input.npz"
inputdata = np.load(inputfile)
x = inputdata["R"][50, :, ::2]
x = x / np.abs(x).max()
taxis, xaxis = inputdata["t"][::2], inputdata["r"][0]
par = {}
par["nx"], par["nt"] = x.shape
par["dx"] = inputdata["r"][0, 1] - inputdata["r"][0, 0]
par["dt"] = inputdata["t"][1] - inputdata["t"][0]
# add wavelet
wav = inputdata["wav"][::2]
wav_c = np.argmax(wav)
x = np.apply_along_axis(convolve, 1, x, wav, mode="full")
x = x[:, wav_c:][:, : par["nt"]]
# gain
gain = np.tile((taxis**2)[:, np.newaxis], (1, par["nx"])).T
x = x * gain
# subsampling locations
perc_subsampling = 0.5
Nsub = int(np.round(par["nx"] * perc_subsampling))
iava = np.sort(np.random.permutation(np.arange(par["nx"]))[:Nsub])
# restriction operator
Rop = pylops.Restriction((par["nx"], par["nt"]), iava, axis=0, dtype="float64")
y = Rop * x.ravel()
xadj = Rop.H * y.ravel()
y = y.reshape(Nsub, par["nt"])
xadj = xadj.reshape(par["nx"], par["nt"])
# apply mask
ymask = Rop.mask(x.ravel())
# sliding windows with radon transform
dx = par["dx"]
nwins = 4
nwin = 27
nover = 3
npx = 31
pxmax = 5e-4
px = np.linspace(-pxmax, pxmax, npx)
dimsd = x.shape
dims = (nwins * npx, dimsd[1])
Op = pylops.signalprocessing.Radon2D(
taxis,
np.linspace(-par["dx"] * nwin // 2, par["dx"] * nwin // 2, nwin),
px,
centeredh=True,
kind="linear",
engine="numba",
)
Slidop = pylops.signalprocessing.Sliding2D(
Op, dims, dimsd, nwin, nover, tapertype="cosine"
)
# adjoint
RSop = Rop * Slidop
Xadj_fromx = Slidop.H * x.ravel()
Xadj_fromx = Xadj_fromx.reshape(npx * nwins, par["nt"])
Xadj = RSop.H * y.ravel()
Xadj = Xadj.reshape(npx * nwins, par["nt"])
# inverse
xl1, Xl1, _ = pylops.waveeqprocessing.SeismicInterpolation(
y,
par["nx"],
iava,
kind="sliding",
spataxis=xaxis,
taxis=taxis,
paxis=px,
nwins=nwins,
nwin=nwin,
nover=nover,
**dict(niter=50, eps=1e-2)
)
fig, axs = plt.subplots(2, 3, sharey=True, figsize=(12, 14))
axs[0][0].imshow(
x.T,
cmap="gray",
vmin=-0.1,
vmax=0.1,
extent=(xaxis[0], xaxis[-1], taxis[-1], taxis[0]),
)
axs[0][0].set_title("Data")
axs[0][0].axis("tight")
axs[0][1].imshow(
ymask.T,
cmap="gray",
vmin=-0.1,
vmax=0.1,
extent=(xaxis[0], xaxis[-1], taxis[-1], taxis[0]),
)
axs[0][1].set_title("Masked data")
axs[0][1].axis("tight")
axs[0][2].imshow(
xl1.T,
cmap="gray",
vmin=-0.1,
vmax=0.1,
extent=(xaxis[0], xaxis[-1], taxis[-1], taxis[0]),
)
axs[0][2].set_title("Reconstructed data")
axs[0][2].axis("tight")
axs[1][0].imshow(
Xadj_fromx.T,
cmap="gray",
vmin=-1,
vmax=1,
extent=(px[0], px[-1], taxis[-1], taxis[0]),
)
axs[1][0].set_title("Adjoint Radon on data")
axs[1][0].axis("tight")
axs[1][1].imshow(
Xadj.T,
cmap="gray",
vmin=-0.6,
vmax=0.6,
extent=(px[0], px[-1], taxis[-1], taxis[0]),
)
axs[1][1].set_title("Adjoint Radon on subsampled data")
axs[1][1].axis("tight")
axs[1][2].imshow(
Xl1.T,
cmap="gray",
vmin=-0.03,
vmax=0.03,
extent=(px[0], px[-1], taxis[-1], taxis[0]),
)
axs[1][2].set_title("Inverse Radon on subsampled data")
axs[1][2].axis("tight")
plt.tight_layout()

As expected the linear pylops.signalprocessing.Radon2D
is
able to locally explain events in the input data and leads to a satisfactory
recovery. Note that increasing the number of iterations and sliding windows
can further refine the result, especially the accuracy of weak events, as
shown in this companion
notebook.
Total running time of the script: ( 0 minutes 8.310 seconds)
Note
Click here to download the full example code
13. Deghosting¶
Single-component seismic data can be decomposed
in their up- and down-going constituents in a model driven fashion.
This task can be achieved by defining an f-k propagator (or ghost model) and
solving an inverse problem as described in
pylops.waveeqprocessing.Deghosting
.
import matplotlib.pyplot as plt
# sphinx_gallery_thumbnail_number = 3
import numpy as np
from scipy.sparse.linalg import lsqr
import pylops
np.random.seed(0)
plt.close("all")
Let’s start by loading the input dataset and geometry
inputfile = "../testdata/updown/input.npz"
inputdata = np.load(inputfile)
vel_sep = 2400.0 # velocity at separation level
clip = 1e-1 # plotting clip
# Receivers
r = inputdata["r"]
nr = r.shape[1]
dr = r[0, 1] - r[0, 0]
# Sources
s = inputdata["s"]
# Model
rho = inputdata["rho"]
# Axes
t = inputdata["t"]
nt, dt = len(t), t[1] - t[0]
x, z = inputdata["x"], inputdata["z"]
dx, dz = x[1] - x[0], z[1] - z[0]
# Data
p = inputdata["p"].T
p /= p.max()
fig = plt.figure(figsize=(9, 4))
ax1 = plt.subplot2grid((1, 5), (0, 0), colspan=4)
ax2 = plt.subplot2grid((1, 5), (0, 4))
ax1.imshow(rho, cmap="gray", extent=(x[0], x[-1], z[-1], z[0]))
ax1.scatter(r[0, ::5], r[1, ::5], marker="v", s=150, c="b", edgecolors="k")
ax1.scatter(s[0], s[1], marker="*", s=250, c="r", edgecolors="k")
ax1.axis("tight")
ax1.set_xlabel("x [m]")
ax1.set_ylabel("y [m]")
ax1.set_title("Model and Geometry")
ax1.set_xlim(x[0], x[-1])
ax1.set_ylim(z[-1], z[0])
ax2.plot(rho[:, len(x) // 2], z, "k", lw=2)
ax2.set_ylim(z[-1], z[0])
ax2.set_yticks([])
plt.tight_layout()

To be able to deghost the input dataset, we need to remove its direct arrival. In this example we will create a mask based on the analytical traveltime of the direct arrival.
direct = np.sqrt(np.sum((s[:, np.newaxis] - r) ** 2, axis=0)) / vel_sep
# Window
off = 0.035
direct_off = direct + off
win = np.zeros((nt, nr))
iwin = np.round(direct_off / dt).astype(int)
for i in range(nr):
win[iwin[i] :, i] = 1
fig, axs = plt.subplots(1, 2, sharey=True, figsize=(8, 7))
axs[0].imshow(
p.T,
cmap="gray",
vmin=-clip * np.abs(p).max(),
vmax=clip * np.abs(p).max(),
extent=(r[0, 0], r[0, -1], t[-1], t[0]),
)
axs[0].plot(r[0], direct_off, "r", lw=2)
axs[0].set_title(r"$P$")
axs[0].axis("tight")
axs[1].imshow(
win * p.T,
cmap="gray",
vmin=-clip * np.abs(p).max(),
vmax=clip * np.abs(p).max(),
extent=(r[0, 0], r[0, -1], t[-1], t[0]),
)
axs[1].set_title(r"Windowed $P$")
axs[1].axis("tight")
axs[1].set_ylim(1, 0)
plt.tight_layout()

We can now perform deghosting
pup, pdown = pylops.waveeqprocessing.Deghosting(
p.T,
nt,
nr,
dt,
dr,
vel_sep,
r[1, 0] + dz,
win=win,
npad=5,
ntaper=11,
solver=lsqr,
dottest=False,
dtype="complex128",
**dict(damp=1e-10, iter_lim=60)
)
fig, axs = plt.subplots(1, 3, sharey=True, figsize=(12, 7))
axs[0].imshow(
p.T,
cmap="gray",
vmin=-clip * np.abs(p).max(),
vmax=clip * np.abs(p).max(),
extent=(r[0, 0], r[0, -1], t[-1], t[0]),
)
axs[0].set_title(r"$P$")
axs[0].axis("tight")
axs[1].imshow(
pup,
cmap="gray",
vmin=-clip * np.abs(p).max(),
vmax=clip * np.abs(p).max(),
extent=(r[0, 0], r[0, -1], t[-1], t[0]),
)
axs[1].set_title(r"$P^-$")
axs[1].axis("tight")
axs[2].imshow(
pdown,
cmap="gray",
vmin=-clip * np.abs(p).max(),
vmax=clip * np.abs(p).max(),
extent=(r[0, 0], r[0, -1], t[-1], t[0]),
)
axs[2].set_title(r"$P^+$")
axs[2].axis("tight")
axs[2].set_ylim(1, 0)
plt.figure(figsize=(14, 3))
plt.plot(t, p[nr // 2], "k", lw=2, label=r"$p$")
plt.plot(t, pup[:, nr // 2], "r", lw=2, label=r"$p^-$")
plt.xlim(0, t[200])
plt.ylim(-0.2, 0.2)
plt.legend()
plt.tight_layout()
plt.figure(figsize=(14, 3))
plt.plot(t, pdown[:, nr // 2], "b", lw=2, label=r"$p^+$")
plt.plot(t, pup[:, nr // 2], "r", lw=2, label=r"$p^-$")
plt.xlim(0, t[200])
plt.ylim(-0.2, 0.2)
plt.legend()
plt.tight_layout()
To see more examples head over to the following notebook: notebook1.
Total running time of the script: ( 0 minutes 6.874 seconds)
Note
Click here to download the full example code
14. Seismic wavefield decomposition¶
Multi-component seismic data can be decomposed
in their up- and down-going constituents in a purely data driven fashion.
This task can be accurately achieved by linearly combining the input pressure
and particle velocity data in the frequency-wavenumber described in details in
pylops.waveeqprocessing.UpDownComposition2D
and
pylops.waveeqprocessing.WavefieldDecomposition
.
In this tutorial we will consider a simple synthetic data composed of six events (three up-going and three down-going). We will first combine them to create pressure and particle velocity data and then show how we can retrieve their directional constituents both by directly combining the input data as well as by setting an inverse problem. The latter approach results vital in case of spatial aliasing, as applying simple scaled summation in the frequency-wavenumber would result in sub-optimal decomposition due to the superposition of different frequency-wavenumber pairs at some (aliased) locations.
import matplotlib.pyplot as plt
import numpy as np
from scipy.signal import filtfilt
import pylops
from pylops.utils.seismicevents import hyperbolic2d, makeaxis
from pylops.utils.wavelets import ricker
np.random.seed(0)
plt.close("all")
Let’s first the input up- and down-going wavefields
par = {"ox": -220, "dx": 5, "nx": 89, "ot": 0, "dt": 0.004, "nt": 200, "f0": 40}
t0_plus = np.array([0.2, 0.5, 0.7])
t0_minus = t0_plus + 0.04
vrms = np.array([1400.0, 1500.0, 2000.0])
amp = np.array([1.0, -0.6, 0.5])
vel_sep = 1000.0 # velocity at separation level
rho_sep = 1000.0 # density at separation level
# Create axis
t, t2, x, y = makeaxis(par)
# Create wavelet
wav = ricker(t[:41], f0=par["f0"])[0]
# Create data
_, p_minus = hyperbolic2d(x, t, t0_minus, vrms, amp, wav)
_, p_plus = hyperbolic2d(x, t, t0_plus, vrms, amp, wav)
We can now combine them to create pressure and particle velocity data
critical = 1.1
ntaper = 51
nfft = 2**10
# 2d fft operator
FFTop = pylops.signalprocessing.FFT2D(
dims=[par["nx"], par["nt"]], nffts=[nfft, nfft], sampling=[par["dx"], par["dt"]]
)
# obliquity factor
[Kx, F] = np.meshgrid(FFTop.f1, FFTop.f2, indexing="ij")
k = F / vel_sep
Kz = np.sqrt((k**2 - Kx**2).astype(np.complex128))
Kz[np.isnan(Kz)] = 0
OBL = rho_sep * (np.abs(F) / Kz)
OBL[Kz == 0] = 0
mask = np.abs(Kx) < critical * np.abs(F) / vel_sep
OBL *= mask
OBL = filtfilt(np.ones(ntaper) / float(ntaper), 1, OBL, axis=0)
OBL = filtfilt(np.ones(ntaper) / float(ntaper), 1, OBL, axis=1)
# composition operator
UPop = pylops.waveeqprocessing.UpDownComposition2D(
par["nt"],
par["nx"],
par["dt"],
par["dx"],
rho_sep,
vel_sep,
nffts=(nfft, nfft),
critical=critical * 100.0,
ntaper=ntaper,
dtype="complex128",
)
# wavefield modelling
d = UPop * np.concatenate((p_plus.ravel(), p_minus.ravel())).ravel()
d = np.real(d.reshape(2 * par["nx"], par["nt"]))
p, vz = d[: par["nx"]], d[par["nx"] :]
# obliquity scaled vz
VZ = FFTop * vz.ravel()
VZ = VZ.reshape(nfft, nfft)
VZ_obl = OBL * VZ
vz_obl = FFTop.H * VZ_obl.ravel()
vz_obl = np.real(vz_obl.reshape(par["nx"], par["nt"]))
fig, axs = plt.subplots(1, 4, figsize=(10, 5))
axs[0].imshow(
p.T,
aspect="auto",
vmin=-1,
vmax=1,
interpolation="nearest",
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[0].set_title(r"$p$", fontsize=15)
axs[0].set_xlabel("x")
axs[0].set_ylabel("t")
axs[1].imshow(
vz_obl.T,
aspect="auto",
vmin=-1,
vmax=1,
interpolation="nearest",
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[1].set_title(r"$v_z^{obl}$", fontsize=15)
axs[1].set_xlabel("x")
axs[1].set_ylabel("t")
axs[2].imshow(
p_plus.T,
aspect="auto",
vmin=-1,
vmax=1,
interpolation="nearest",
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[2].set_title(r"$p^+$", fontsize=15)
axs[2].set_xlabel("x")
axs[2].set_ylabel("t")
axs[3].imshow(
p_minus.T,
aspect="auto",
interpolation="nearest",
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
vmin=-1,
vmax=1,
)
axs[3].set_title(r"$p^-$", fontsize=15)
axs[3].set_xlabel("x")
axs[3].set_ylabel("t")
plt.tight_layout()

Wavefield separation is first performed using the analytical expression for combining pressure and particle velocity data in the wavenumber-frequency domain
pup_sep, pdown_sep = pylops.waveeqprocessing.WavefieldDecomposition(
p,
vz,
par["nt"],
par["nx"],
par["dt"],
par["dx"],
rho_sep,
vel_sep,
nffts=(nfft, nfft),
kind="analytical",
critical=critical * 100,
ntaper=ntaper,
dtype="complex128",
)
fig = plt.figure(figsize=(12, 5))
axs0 = plt.subplot2grid((2, 5), (0, 0), rowspan=2)
axs1 = plt.subplot2grid((2, 5), (0, 1), rowspan=2)
axs2 = plt.subplot2grid((2, 5), (0, 2), colspan=3)
axs3 = plt.subplot2grid((2, 5), (1, 2), colspan=3)
axs0.imshow(
pup_sep.T, cmap="gray", vmin=-1, vmax=1, extent=(x.min(), x.max(), t.max(), t.min())
)
axs0.set_title(r"$p^-$ analytical")
axs0.axis("tight")
axs1.imshow(
pdown_sep.T,
cmap="gray",
vmin=-1,
vmax=1,
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs1.set_title(r"$p^+$ analytical")
axs1.axis("tight")
axs2.plot(t, p[par["nx"] // 2], "r", lw=2, label=r"$p$")
axs2.plot(t, vz_obl[par["nx"] // 2], "--b", lw=2, label=r"$v_z^{obl}$")
axs2.set_ylim(-1, 1)
axs2.set_title("Data at x=%.2f" % x[par["nx"] // 2])
axs2.set_xlabel("t [s]")
axs2.legend()
axs3.plot(t, pup_sep[par["nx"] // 2], "r", lw=2, label=r"$p^-$ ana")
axs3.plot(t, pdown_sep[par["nx"] // 2], "--b", lw=2, label=r"$p^+$ ana")
axs3.set_title("Separated wavefields at x=%.2f" % x[par["nx"] // 2])
axs3.set_xlabel("t [s]")
axs3.set_ylim(-1, 1)
axs3.legend()
plt.tight_layout()

We repeat the same exercise but this time we invert the composition operator
pylops.waveeqprocessing.UpDownComposition2D
pup_inv, pdown_inv = pylops.waveeqprocessing.WavefieldDecomposition(
p,
vz,
par["nt"],
par["nx"],
par["dt"],
par["dx"],
rho_sep,
vel_sep,
nffts=(nfft, nfft),
kind="inverse",
critical=critical * 100,
ntaper=ntaper,
scaling=1.0 / vz.max(),
dtype="complex128",
**dict(damp=1e-10, iter_lim=20)
)
fig = plt.figure(figsize=(12, 5))
axs0 = plt.subplot2grid((2, 5), (0, 0), rowspan=2)
axs1 = plt.subplot2grid((2, 5), (0, 1), rowspan=2)
axs2 = plt.subplot2grid((2, 5), (0, 2), colspan=3)
axs3 = plt.subplot2grid((2, 5), (1, 2), colspan=3)
axs0.imshow(
pup_inv.T, cmap="gray", vmin=-1, vmax=1, extent=(x.min(), x.max(), t.max(), t.min())
)
axs0.set_title(r"$p^-$ inverse")
axs0.axis("tight")
axs1.imshow(
pdown_inv.T,
cmap="gray",
vmin=-1,
vmax=1,
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs1.set_title(r"$p^+$ inverse")
axs1.axis("tight")
axs2.plot(t, p[par["nx"] // 2], "r", lw=2, label=r"$p$")
axs2.plot(t, vz_obl[par["nx"] // 2], "--b", lw=2, label=r"$v_z^{obl}$")
axs2.set_ylim(-1, 1)
axs2.set_title("Data at x=%.2f" % x[par["nx"] // 2])
axs2.set_xlabel("t [s]")
axs2.legend()
axs3.plot(t, pup_inv[par["nx"] // 2], "r", lw=2, label=r"$p^-$ inv")
axs3.plot(t, pdown_inv[par["nx"] // 2], "--b", lw=2, label=r"$p^+$ inv")
axs3.set_title("Separated wavefields at x=%.2f" % x[par["nx"] // 2])
axs3.set_xlabel("t [s]")
axs3.set_ylim(-1, 1)
axs3.legend()
plt.tight_layout()

The up- and down-going constituents have been succesfully separated in both
cases. Finally, we use the
pylops.waveeqprocessing.UpDownComposition2D
operator to reconstruct
the particle velocity wavefield from its up- and down-going pressure
constituents
PtoVop = pylops.waveeqprocessing.PressureToVelocity(
par["nt"],
par["nx"],
par["dt"],
par["dx"],
rho_sep,
vel_sep,
nffts=(nfft, nfft),
critical=critical * 100.0,
ntaper=ntaper,
topressure=False,
)
vdown_rec = (PtoVop * pdown_inv.ravel()).reshape(par["nx"], par["nt"])
vup_rec = (PtoVop * pup_inv.ravel()).reshape(par["nx"], par["nt"])
vz_rec = np.real(vdown_rec - vup_rec)
fig, axs = plt.subplots(1, 3, figsize=(13, 6))
axs[0].imshow(
vz.T,
cmap="gray",
vmin=-1e-6,
vmax=1e-6,
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[0].set_title(r"$vz$")
axs[0].axis("tight")
axs[1].imshow(
vz_rec.T, cmap="gray", vmin=-1e-6, vmax=1e-6, extent=(x.min(), x.max(), t[-1], t[0])
)
axs[1].set_title(r"$vz rec$")
axs[1].axis("tight")
axs[2].imshow(
vz.T - vz_rec.T,
cmap="gray",
vmin=-1e-6,
vmax=1e-6,
extent=(x.min(), x.max(), t[-1], t[0]),
)
axs[2].set_title(r"$error$")
axs[2].axis("tight")
plt.tight_layout()

To see more examples, including applying wavefield separation and regularization simultaneously, as well as 3D examples, head over to the following notebooks: notebook1 and notebook2
Total running time of the script: ( 0 minutes 15.497 seconds)
Note
Click here to download the full example code
15. Least-squares migration¶
Seismic migration is the process by which seismic data are manipulated to create an image of the subsurface reflectivity.
While traditionally solved as the adjont of the demigration operator, it is becoming more and more common to solve the underlying inverse problem in the quest for more accurate and detailed subsurface images.
Indipendently of the choice of the modelling operator (i.e., ray-based or full wavefield-based), the demigration/migration process can be expressed as a linear operator of such a kind:
where \(m(\mathbf{x})\) is the reflectivity at every location in the subsurface, \(G(\mathbf{x}, \mathbf{x_s}, t)\) and \(G(\mathbf{x_r}, \mathbf{x}, t)\) are the Green’s functions from source-to-subsurface-to-receiver and finally \(w(t)\) is the wavelet. Ultimately, while the Green’s functions can be computed in many different ways, solving this system of equations for the reflectivity model is what we generally refer to as Least-squares migration (LSM).
In this tutorial we will consider the most simple scenario where we use an
eikonal solver to compute the Green’s functions and show how we can use the
pylops.waveeqprocessing.LSM
operator to perform LSM.
import matplotlib.pyplot as plt
import numpy as np
from scipy.sparse.linalg import lsqr
import pylops
plt.close("all")
np.random.seed(0)
To start we create a simple model with 2 interfaces
# Velocity Model
nx, nz = 81, 60
dx, dz = 4, 4
x, z = np.arange(nx) * dx, np.arange(nz) * dz
v0 = 1000 # initial velocity
kv = 0.0 # gradient
vel = np.outer(np.ones(nx), v0 + kv * z)
# Reflectivity Model
refl = np.zeros((nx, nz))
refl[:, 30] = -1
refl[:, 50] = 0.5
# Receivers
nr = 11
rx = np.linspace(10 * dx, (nx - 10) * dx, nr)
rz = 20 * np.ones(nr)
recs = np.vstack((rx, rz))
dr = recs[0, 1] - recs[0, 0]
# Sources
ns = 10
sx = np.linspace(dx * 10, (nx - 10) * dx, ns)
sz = 10 * np.ones(ns)
sources = np.vstack((sx, sz))
ds = sources[0, 1] - sources[0, 0]
plt.figure(figsize=(10, 5))
im = plt.imshow(vel.T, cmap="summer", extent=(x[0], x[-1], z[-1], z[0]))
plt.scatter(recs[0], recs[1], marker="v", s=150, c="b", edgecolors="k")
plt.scatter(sources[0], sources[1], marker="*", s=150, c="r", edgecolors="k")
cb = plt.colorbar(im)
cb.set_label("[m/s]")
plt.axis("tight")
plt.xlabel("x [m]"), plt.ylabel("y [m]")
plt.title("Velocity")
plt.xlim(x[0], x[-1])
plt.tight_layout()
plt.figure(figsize=(10, 5))
im = plt.imshow(refl.T, cmap="gray", extent=(x[0], x[-1], z[-1], z[0]))
plt.scatter(recs[0], recs[1], marker="v", s=150, c="b", edgecolors="k")
plt.scatter(sources[0], sources[1], marker="*", s=150, c="r", edgecolors="k")
plt.colorbar(im)
plt.axis("tight")
plt.xlabel("x [m]"), plt.ylabel("y [m]")
plt.title("Reflectivity")
plt.xlim(x[0], x[-1])
plt.tight_layout()
We can now create our LSM object and invert for the reflectivity using two
different solvers: scipy.sparse.linalg.lsqr
(LS solution) and
pylops.optimization.sparsity.fista
(LS solution with sparse model).
nt = 651
dt = 0.004
t = np.arange(nt) * dt
wav, wavt, wavc = pylops.utils.wavelets.ricker(t[:41], f0=20)
lsm = pylops.waveeqprocessing.LSM(
z,
x,
t,
sources,
recs,
v0,
wav,
wavc,
mode="analytic",
engine="numba",
)
d = lsm.Demop * refl
madj = lsm.Demop.H * d
minv = lsm.solve(d.ravel(), solver=lsqr, **dict(iter_lim=100))
minv = minv.reshape(nx, nz)
minv_sparse = lsm.solve(
d.ravel(), solver=pylops.optimization.sparsity.fista, **dict(eps=1e2, niter=100)
)
minv_sparse = minv_sparse.reshape(nx, nz)
# demigration
d = d.reshape(ns, nr, nt)
dadj = lsm.Demop * madj # (ns * nr, nt)
dadj = dadj.reshape(ns, nr, nt)
dinv = lsm.Demop * minv
dinv = dinv.reshape(ns, nr, nt)
dinv_sparse = lsm.Demop * minv_sparse
dinv_sparse = dinv_sparse.reshape(ns, nr, nt)
# sphinx_gallery_thumbnail_number = 2
fig, axs = plt.subplots(2, 2, figsize=(10, 8))
axs[0][0].imshow(refl.T, cmap="gray", vmin=-1, vmax=1)
axs[0][0].axis("tight")
axs[0][0].set_title(r"$m$")
axs[0][1].imshow(madj.T, cmap="gray", vmin=-madj.max(), vmax=madj.max())
axs[0][1].set_title(r"$m_{adj}$")
axs[0][1].axis("tight")
axs[1][0].imshow(minv.T, cmap="gray", vmin=-1, vmax=1)
axs[1][0].axis("tight")
axs[1][0].set_title(r"$m_{inv}$")
axs[1][1].imshow(minv_sparse.T, cmap="gray", vmin=-1, vmax=1)
axs[1][1].axis("tight")
axs[1][1].set_title(r"$m_{FISTA}$")
plt.tight_layout()
fig, axs = plt.subplots(1, 4, figsize=(10, 4))
axs[0].imshow(d[0, :, :300].T, cmap="gray", vmin=-d.max(), vmax=d.max())
axs[0].set_title(r"$d$")
axs[0].axis("tight")
axs[1].imshow(dadj[0, :, :300].T, cmap="gray", vmin=-dadj.max(), vmax=dadj.max())
axs[1].set_title(r"$d_{adj}$")
axs[1].axis("tight")
axs[2].imshow(dinv[0, :, :300].T, cmap="gray", vmin=-d.max(), vmax=d.max())
axs[2].set_title(r"$d_{inv}$")
axs[2].axis("tight")
axs[3].imshow(dinv_sparse[0, :, :300].T, cmap="gray", vmin=-d.max(), vmax=d.max())
axs[3].set_title(r"$d_{fista}$")
axs[3].axis("tight")
plt.tight_layout()
fig, axs = plt.subplots(1, 4, figsize=(10, 4))
axs[0].imshow(d[ns // 2, :, :300].T, cmap="gray", vmin=-d.max(), vmax=d.max())
axs[0].set_title(r"$d$")
axs[0].axis("tight")
axs[1].imshow(dadj[ns // 2, :, :300].T, cmap="gray", vmin=-dadj.max(), vmax=dadj.max())
axs[1].set_title(r"$d_{adj}$")
axs[1].axis("tight")
axs[2].imshow(dinv[ns // 2, :, :300].T, cmap="gray", vmin=-d.max(), vmax=d.max())
axs[2].set_title(r"$d_{inv}$")
axs[2].axis("tight")
axs[3].imshow(dinv_sparse[ns // 2, :, :300].T, cmap="gray", vmin=-d.max(), vmax=d.max())
axs[3].set_title(r"$d_{fista}$")
axs[3].axis("tight")
plt.tight_layout()
This was just a short teaser, for a more advanced set of examples of 2D and 3D traveltime-based LSM head over to this notebook.
Total running time of the script: ( 0 minutes 7.017 seconds)
Note
Click here to download the full example code
16. CT Scan Imaging¶
This tutorial considers a very well-known inverse problem from the field of medical imaging.
We will be using the pylops.signalprocessing.Radon2D
operator
to model a sinogram, which is a graphic representation of the raw data
obtained from a CT scan. The sinogram is further inverted using both a L2
solver and a TV-regularized solver like Split-Bregman.
import matplotlib.pyplot as plt
# sphinx_gallery_thumbnail_number = 2
import numpy as np
from numba import jit
import pylops
plt.close("all")
np.random.seed(10)
Let’s start by loading the Shepp-Logan phantom model. We can then construct
the sinogram by providing a custom-made function to the
pylops.signalprocessing.Radon2D
that samples parametric curves of
such a type:
where \(\theta\) is the angle between the x-axis (\(x\)) and the perpendicular to the summation line and \(r\) is the distance from the origin of the summation line.
@jit(nopython=True)
def radoncurve(x, r, theta):
return (
(r - ny // 2) / (np.sin(theta) + 1e-15)
+ np.tan(np.pi / 2.0 - theta) * x
+ ny // 2
)
x = np.load("../testdata/optimization/shepp_logan_phantom.npy").T
x = x / x.max()
nx, ny = x.shape
ntheta = 151
theta = np.linspace(0.0, np.pi, ntheta, endpoint=False)
RLop = pylops.signalprocessing.Radon2D(
np.arange(ny),
np.arange(nx),
theta,
kind=radoncurve,
centeredh=True,
interp=False,
engine="numba",
dtype="float64",
)
y = RLop.H * x
We can now first perform the adjoint, which in the medical imaging literature is also referred to as back-projection.
This is the first step of a common reconstruction technique, named filtered back-projection, which simply applies a correction filter in the frequency domain to the adjoint model.
xrec = RLop * y
fig, axs = plt.subplots(1, 3, figsize=(10, 4))
axs[0].imshow(x.T, vmin=0, vmax=1, cmap="gray")
axs[0].set_title("Model")
axs[0].axis("tight")
axs[1].imshow(y.T, cmap="gray")
axs[1].set_title("Data")
axs[1].axis("tight")
axs[2].imshow(xrec.T, cmap="gray")
axs[2].set_title("Adjoint model")
axs[2].axis("tight")
fig.tight_layout()

Finally we take advantage of our different solvers and try to invert the modelling operator both in a least-squares sense and using TV-reg.
Dop = [
pylops.FirstDerivative(
(nx, ny), axis=0, edge=True, kind="backward", dtype=np.float64
),
pylops.FirstDerivative(
(nx, ny), axis=1, edge=True, kind="backward", dtype=np.float64
),
]
D2op = pylops.Laplacian(dims=(nx, ny), edge=True, dtype=np.float64)
# L2
xinv_sm = pylops.optimization.leastsquares.regularized_inversion(
RLop.H, y.ravel(), [D2op], epsRs=[1e1], **dict(iter_lim=20)
)[0]
xinv_sm = np.real(xinv_sm.reshape(nx, ny))
# TV
mu = 1.5
lamda = [1.0, 1.0]
niter = 3
niterinner = 4
xinv = pylops.optimization.sparsity.splitbregman(
RLop.H,
y.ravel(),
Dop,
niter_outer=niter,
niter_inner=niterinner,
mu=mu,
epsRL1s=lamda,
tol=1e-4,
tau=1.0,
show=False,
**dict(iter_lim=20, damp=1e-2)
)[0]
xinv = np.real(xinv.reshape(nx, ny))
fig, axs = plt.subplots(1, 3, figsize=(10, 4))
axs[0].imshow(x.T, vmin=0, vmax=1, cmap="gray")
axs[0].set_title("Model")
axs[0].axis("tight")
axs[1].imshow(xinv_sm.T, vmin=0, vmax=1, cmap="gray")
axs[1].set_title("L2 Inversion")
axs[1].axis("tight")
axs[2].imshow(xinv.T, vmin=0, vmax=1, cmap="gray")
axs[2].set_title("TV-Reg Inversion")
axs[2].axis("tight")
fig.tight_layout()

Total running time of the script: ( 0 minutes 12.445 seconds)
Note
Click here to download the full example code
17. Real/Complex Inversion¶
In this tutorial we will discuss two equivalent approaches to the solution of inverse problems with real-valued model vector and complex-valued data vector. In other words, we consider a modelling operator \(\mathbf{A}:\mathbb{F}^m \to \mathbb{C}^n\) (which could be the case for example for the real FFT).
Mathematically speaking, this problem can be solved equivalently by inverting the complex-valued problem:
or the real-valued augmented system
Whilst we already know how to solve the first problem, let’s see how we can
solve the second one by taking advantage of the real
method of the
pylops.LinearOperator
object. We will also wrap our linear operator
into a pylops.MemoizeOperator
which remembers the last N model and
data vectors and by-passes the computation of the forward and/or adjoint pass
whenever the same pair reappears. This is very useful in our case when we
want to compute the real and the imag components of
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
np.random.seed(0)
To start we create the forward problem
n = 5
x = np.arange(n) + 1.0
# make A
Ar = np.random.normal(0, 1, (n, n))
Ai = np.random.normal(0, 1, (n, n))
A = Ar + 1j * Ai
Aop = pylops.MatrixMult(A, dtype=np.complex128)
y = Aop @ x
Let’s check we can solve this problem using the first formulation
A1op = Aop.toreal(forw=False, adj=True)
xinv = A1op.div(y)
print(f"xinv={xinv}\n")
xinv=[1.+1.83186799e-15j 2.-8.88178420e-16j 3.-8.88178420e-16j
4.-2.22044605e-16j 5.+6.66133815e-16j]
Let’s now see how we formulate the second problem
Amop = pylops.MemoizeOperator(Aop, max_neval=10)
Arop = Amop.toreal()
Aiop = Amop.toimag()
A1op = pylops.VStack([Arop, Aiop])
y1 = np.concatenate([np.real(y), np.imag(y)])
xinv1 = np.real(A1op.div(y1))
print(f"xinv1={xinv1}\n")
xinv1=[1. 2. 3. 4. 5.]
Total running time of the script: ( 0 minutes 0.010 seconds)
Note
Click here to download the full example code
18. Deblending¶
The cocktail party problem arises when sounds from different sources mix before reaching our ears (or any recording device), requiring the brain (or any hardware in the recording device) to estimate individual sources from the received mixture. In seismic acquisition, an analog problem is present when multiple sources are fired simultaneously. This family of acquisition methods is usually referred to as simultaneous shooting and the problem of separating the blendend shot gathers into their individual components is called deblending. Whilst various firing strategies can be adopted, in this example we consider the continuos blending problem where a single source is fired sequentially at an interval shorter than the amount of time required for waves to travel into the Earth and come back.
Simply stated the forward problem can be written as:
Here \(\mathbf{d} = [\mathbf{d}_1^T, \mathbf{d}_2^T,\ldots, \mathbf{d}_N^T]^T\) is a stack of \(N\) individual shot gathers, \(\boldsymbol\Phi=[\boldsymbol\Phi_1, \boldsymbol\Phi_2,\ldots, \boldsymbol\Phi_N]\) is the blending operator, \(\mathbf{d}^b\) is the so-called supergather than contains all shots superimposed to each other.
In order to successfully invert this severely underdetermined problem, two key ingredients must be introduced:
the firing time of each source (i.e., shifts of the blending operator) must be chosen to be dithered around a nominal regular, periodic firing interval. In our case, we consider shots of duration \(T=4s\), regular firing time of \(T_s=2s\) and a dithering code as follows \(\Delta t = U(-1,1)\);
prior information about the data to reconstruct, either in the form of regularization or preconditioning must be introduced. In our case we will use a patch-FK transform as preconditioner and solve the problem imposing sparsity in the transformed domain.
In other words, we aim to solve the following problem:
for which we will use the pylops.optimization.sparsity.fista
solver.
import matplotlib.pyplot as plt
import numpy as np
from scipy.sparse.linalg import lobpcg as sp_lobpcg
import pylops
np.random.seed(10)
plt.close("all")
Let’s start by defining a blending operator
def Blending(nt, ns, dt, overlap, times, dtype="float64"):
"""Blending operator"""
pad = int(overlap * nt)
OpShiftPad = []
for i in range(ns):
PadOp = pylops.Pad(nt, (pad * i, pad * (ns - 1 - i)), dtype=dtype)
ShiftOp = pylops.signalprocessing.Shift(
pad * (ns - 1) + nt, times[i], axis=0, sampling=dt, real=False, dtype=dtype
)
OpShiftPad.append(ShiftOp * PadOp)
return pylops.HStack(OpShiftPad)
We can now load and display a small portion of the MobilAVO dataset composed of 60 shots and a single receiver. This data is unblended.
data = np.load("../testdata/deblending/mobil.npy")
ns, nt = data.shape
dt = 0.004
t = np.arange(nt) * dt
fig, ax = plt.subplots(1, 1, figsize=(12, 8))
ax.imshow(
data.T,
cmap="gray",
vmin=-50,
vmax=50,
extent=(0, ns, t[-1], 0),
interpolation="none",
)
ax.set_title("CRG")
ax.set_xlabel("#Src")
ax.set_ylabel("t [s]")
ax.axis("tight")
plt.tight_layout()

We are now ready to define the blending operator, blend our data, and apply the adjoint of the blending operator to it. This is usually referred as pseudo-deblending: as we will see brings back each source to its own nominal firing time, but since sources partially overlap in time, it will also generate some burst like noise in the data. Deblending can hopefully fix this.
overlap = 0.5
pad = int(overlap * nt)
ignition_times = 2.0 * np.random.rand(ns) - 1.0
Bop = Blending(nt, ns, dt, overlap, ignition_times, dtype="complex128")
data_blended = Bop * data.ravel()
data_pseudo = Bop.H * data_blended.ravel()
data_pseudo = data_pseudo.reshape(ns, nt)
fig, ax = plt.subplots(1, 1, figsize=(12, 8))
ax.imshow(
data_pseudo.T.real,
cmap="gray",
vmin=-50,
vmax=50,
extent=(0, ns, t[-1], 0),
interpolation="none",
)
ax.set_title("Pseudo-deblended CRG")
ax.set_xlabel("#Src")
ax.set_ylabel("t [s]")
ax.axis("tight")
plt.tight_layout()

We are finally ready to solve our deblending inverse problem
# Patched FK
dimsd = data.shape
nwin = (20, 80)
nover = (10, 40)
nop = (128, 128)
nop1 = (128, 65)
nwins = (5, 24)
dims = (nwins[0] * nop1[0], nwins[1] * nop1[1])
Fop = pylops.signalprocessing.FFT2D(nwin, nffts=nop, real=True)
Sop = pylops.signalprocessing.Patch2D(
Fop.H, dims, dimsd, nwin, nover, nop1, tapertype="hanning"
)
# Overall operator
Op = Bop * Sop
# Compute max eigenvalue (we do this explicitly to be able to run this fast)
Op1 = pylops.LinearOperator(Op.H * Op, explicit=False)
X = np.random.rand(Op1.shape[0], 1).astype(Op1.dtype)
maxeig = sp_lobpcg(Op1, X=X, maxiter=5, tol=1e-10)[0][0]
alpha = 1.0 / maxeig
# Deblend
niter = 60
decay = (np.exp(-0.05 * np.arange(niter)) + 0.2) / 1.2
with pylops.disabled_ndarray_multiplication():
p_inv = pylops.optimization.sparsity.fista(
Op,
data_blended.ravel(),
niter=niter,
eps=5e0,
alpha=alpha,
decay=decay,
show=True,
)[0]
data_inv = Sop * p_inv
data_inv = data_inv.reshape(ns, nt)
fig, axs = plt.subplots(1, 4, sharey=False, figsize=(12, 8))
axs[0].imshow(
data.T.real,
cmap="gray",
extent=(0, ns, t[-1], 0),
vmin=-50,
vmax=50,
interpolation="none",
)
axs[0].set_title("CRG")
axs[0].set_xlabel("#Src")
axs[0].set_ylabel("t [s]")
axs[0].axis("tight")
axs[1].imshow(
data_pseudo.T.real,
cmap="gray",
extent=(0, ns, t[-1], 0),
vmin=-50,
vmax=50,
interpolation="none",
)
axs[1].set_title("Pseudo-deblended CRG")
axs[1].set_xlabel("#Src")
axs[1].axis("tight")
axs[2].imshow(
data_inv.T.real,
cmap="gray",
extent=(0, ns, t[-1], 0),
vmin=-50,
vmax=50,
interpolation="none",
)
axs[2].set_xlabel("#Src")
axs[2].set_title("Deblended CRG")
axs[2].axis("tight")
axs[3].imshow(
data.T.real - data_inv.T.real,
cmap="gray",
extent=(0, ns, t[-1], 0),
vmin=-50,
vmax=50,
interpolation="none",
)
axs[3].set_xlabel("#Src")
axs[3].set_title("Blending error")
axs[3].axis("tight")
plt.tight_layout()

FISTA (soft thresholding)
--------------------------------------------------------------------------------
The Operator Op has 30500 rows and 998400 cols
eps = 5.000000e+00 tol = 1.000000e-10 niter = 60
alpha = 3.261681e-01 thresh = 8.154204e-01
--------------------------------------------------------------------------------
Itn x[0] r2norm r12norm xupdate
1 0.00e+00+0.00e+00j 3.463e+06 5.081e+06 1.251e+03
2 0.00e+00+0.00e+00j 1.955e+06 4.282e+06 6.844e+02
3 0.00e+00+0.00e+00j 1.134e+06 3.898e+06 5.635e+02
4 0.00e+00+0.00e+00j 7.080e+05 3.681e+06 4.576e+02
5 0.00e+00+0.00e+00j 4.873e+05 3.510e+06 3.831e+02
6 0.00e+00+0.00e+00j 3.688e+05 3.348e+06 3.357e+02
7 0.00e+00+0.00e+00j 3.014e+05 3.192e+06 3.048e+02
8 0.00e+00+0.00e+00j 2.600e+05 3.048e+06 2.826e+02
9 0.00e+00+0.00e+00j 2.316e+05 2.923e+06 2.642e+02
10 0.00e+00+0.00e+00j 2.105e+05 2.816e+06 2.479e+02
11 0.00e+00+0.00e+00j 1.934e+05 2.727e+06 2.327e+02
21 0.00e+00+0.00e+00j 1.028e+05 2.435e+06 1.202e+02
31 -0.00e+00+0.00e+00j 6.359e+04 2.450e+06 8.024e+01
41 -0.00e+00+0.00e+00j 4.281e+04 2.490e+06 6.262e+01
51 -0.00e+00+0.00e+00j 3.179e+04 2.520e+06 5.211e+01
52 -0.00e+00+0.00e+00j 3.101e+04 2.523e+06 5.126e+01
53 -0.00e+00+0.00e+00j 3.027e+04 2.525e+06 5.040e+01
54 -0.00e+00+0.00e+00j 2.957e+04 2.527e+06 4.960e+01
55 -0.00e+00+0.00e+00j 2.890e+04 2.529e+06 4.879e+01
56 -0.00e+00+0.00e+00j 2.827e+04 2.531e+06 4.804e+01
57 -0.00e+00+0.00e+00j 2.768e+04 2.533e+06 4.733e+01
58 -0.00e+00+0.00e+00j 2.712e+04 2.535e+06 4.663e+01
59 -0.00e+00+0.00e+00j 2.660e+04 2.536e+06 4.598e+01
60 -0.00e+00+0.00e+00j 2.610e+04 2.538e+06 4.531e+01
Iterations = 60 Total time (s) = 44.24
--------------------------------------------------------------------------------
Finally, let’s look a bit more at what really happened under the hood. We display a number of patches and their associated FK spectrum
Sop1 = pylops.signalprocessing.Patch2D(
Fop.H, dims, dimsd, nwin, nover, nop1, tapertype=None
)
# Original
p = Sop1.H * data.ravel()
preshape = p.reshape(nwins[0], nwins[1], nop1[0], nop1[1])
ix = 16
fig, axs = plt.subplots(2, 4, figsize=(12, 5))
fig.suptitle("Data patches")
for i in range(4):
axs[0][i].imshow(np.fft.fftshift(np.abs(preshape[i, ix]).T, axes=1))
axs[0][i].axis("tight")
axs[1][i].imshow(
np.real((Fop.H * preshape[i, ix].ravel()).reshape(nwin)).T,
cmap="gray",
vmin=-30,
vmax=30,
interpolation="none",
)
axs[1][i].axis("tight")
plt.tight_layout()
# Pseudo-deblended
p_pseudo = Sop1.H * data_pseudo.ravel()
p_pseudoreshape = p_pseudo.reshape(nwins[0], nwins[1], nop1[0], nop1[1])
ix = 16
fig, axs = plt.subplots(2, 4, figsize=(12, 5))
fig.suptitle("Pseudo-deblended patches")
for i in range(4):
axs[0][i].imshow(np.fft.fftshift(np.abs(p_pseudoreshape[i, ix]).T, axes=1))
axs[0][i].axis("tight")
axs[1][i].imshow(
np.real((Fop.H * p_pseudoreshape[i, ix].ravel()).reshape(nwin)).T,
cmap="gray",
vmin=-30,
vmax=30,
interpolation="none",
)
axs[1][i].axis("tight")
plt.tight_layout()
# Deblended
p_inv = Sop1.H * data_inv.ravel()
p_invreshape = p_inv.reshape(nwins[0], nwins[1], nop1[0], nop1[1])
ix = 16
fig, axs = plt.subplots(2, 4, figsize=(12, 5))
fig.suptitle("Deblended patches")
for i in range(4):
axs[0][i].imshow(np.fft.fftshift(np.abs(p_invreshape[i, ix]).T, axes=1))
axs[0][i].axis("tight")
axs[1][i].imshow(
np.real((Fop.H * p_invreshape[i, ix].ravel()).reshape(nwin)).T,
cmap="gray",
vmin=-30,
vmax=30,
interpolation="none",
)
axs[1][i].axis("tight")
plt.tight_layout()
Total running time of the script: ( 0 minutes 51.398 seconds)
Note
Click here to download the full example code
19. Automatic Differentiation¶
This tutorial focuses on the use of pylops.TorchOperator
to allow performing
Automatic Differentiation (AD) on chains of operators which can be:
native PyTorch mathematical operations (e.g.,
torch.log
,torch.sin
,torch.tan
,torch.pow
, …)neural network operators in
torch.nn
PyLops linear operators
This opens up many opportunities, such as easily including linear regularization terms to nonlinear cost functions or using linear preconditioners with nonlinear modelling operators.
import matplotlib.pyplot as plt
import numpy as np
import torch
import torch.nn as nn
from torch.autograd import gradcheck
import pylops
plt.close("all")
np.random.seed(10)
torch.manual_seed(10)
<torch._C.Generator object at 0x7f686f92f450>
In this example we consider a simple multidimensional functional:
and we use AD to compute the gradient with respect to the input vector evaluated at \(\mathbf{x}=\mathbf{x}_0\) : \(\mathbf{g} = d\mathbf{y} / d\mathbf{x} |_{\mathbf{x}=\mathbf{x}_0}\).
Let’s start by defining the Jacobian:
\[\begin{split}\textbf{J} = \begin{bmatrix} dy_1 / dx_1 & ... & dy_1 / dx_M \\ ... & ... & ... \\ dy_N / dx_1 & ... & dy_N / dx_M \end{bmatrix} = \begin{bmatrix} a_{11} cos(x_1) & ... & a_{1M} cos(x_M) \\ ... & ... & ... \\ a_{N1} cos(x_1) & ... & a_{NM} cos(x_M) \end{bmatrix} = \textbf{A} cos(\mathbf{x})\end{split}\]
Since both input and output are multidimensional,
PyTorch backward
actually computes the product between the transposed
Jacobian and a vector \(\mathbf{v}\):
\(\mathbf{g}=\mathbf{J^T} \mathbf{v}\).
To validate the correctness of the AD result, we can in this simple case
also compute the Jacobian analytically and apply it to the same vector
\(\mathbf{v}\) that we have provided to PyTorch backward
.
nx, ny = 10, 6
x0 = torch.arange(nx, dtype=torch.double, requires_grad=True)
# Forward
A = np.random.normal(0.0, 1.0, (ny, nx))
At = torch.from_numpy(A)
Aop = pylops.TorchOperator(pylops.MatrixMult(A))
y = Aop.apply(torch.sin(x0))
# AD
v = torch.ones(ny, dtype=torch.double)
y.backward(v, retain_graph=True)
adgrad = x0.grad
# Analytical
J = At * torch.cos(x0)
anagrad = torch.matmul(J.T, v)
print("Input: ", x0)
print("AD gradient: ", adgrad)
print("Analytical gradient: ", anagrad)
Input: tensor([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.], dtype=torch.float64,
requires_grad=True)
AD gradient: tensor([ 0.1539, -0.2356, 1.4944, -3.1160, -2.1699, 0.4492, 1.6168, 1.9101,
-0.4259, 1.7154], dtype=torch.float64)
Analytical gradient: tensor([ 0.1539, -0.2356, 1.4944, -3.1160, -2.1699, 0.4492, 1.6168, 1.9101,
-0.4259, 1.7154], dtype=torch.float64, grad_fn=<MvBackward>)
Similarly we can use the torch.autograd.gradcheck
directly from
PyTorch. Note that doubles must be used for this to succeed with very small
eps and atol
input = (
torch.arange(nx, dtype=torch.double, requires_grad=True),
Aop.matvec,
Aop.rmatvec,
Aop.device,
"cpu",
)
test = gradcheck(Aop.Top, input, eps=1e-6, atol=1e-4)
print(test)
True
Note that while matrix-vector multiplication could have been performed using
the native PyTorch operator torch.matmul
, in this case we have shown
that we are also able to use a PyLops operator wrapped in
pylops.TorchOperator
. As already mentioned, this gives us the
ability to use much more complex linear operators provided by PyLops within
a chain of mixed linear and nonlinear AD-enabled operators.
To conclude, let’s see how we can chain a torch convolutional network
with PyLops pylops.Smoothing2D
operator. First of all, we consider
a single training sample.
class Network(nn.Module):
def __init__(self, input_channels):
super(Network, self).__init__()
self.conv1 = nn.Conv2d(
input_channels, input_channels // 2, kernel_size=3, padding=1
)
self.conv2 = nn.Conv2d(
input_channels // 2, input_channels // 4, kernel_size=3, padding=1
)
self.activation = nn.LeakyReLU(0.2)
self.maxpool = nn.MaxPool2d(kernel_size=2, stride=2)
def forward(self, x):
x = self.conv1(x)
x = self.activation(x)
x = self.conv2(x)
x = self.activation(x)
return x
net = Network(4)
Cop = pylops.TorchOperator(pylops.Smoothing2D((5, 5), dims=(32, 32)))
# Forward
x = torch.randn(1, 4, 32, 32).requires_grad_()
y = Cop.apply(net(x).view(-1)).reshape(32, 32)
# Backward
loss = y.sum()
loss.backward()
fig, axs = plt.subplots(1, 2, figsize=(12, 3))
axs[0].imshow(y.detach().numpy())
axs[0].set_title("Forward")
axs[0].axis("tight")
axs[1].imshow(x.grad.reshape(4 * 32, 32).T)
axs[1].set_title("Gradient")
axs[1].axis("tight")
plt.tight_layout()

And finally we do the same with a batch of 3 training samples.
net = Network(4)
Cop = pylops.TorchOperator(pylops.Smoothing2D((5, 5), dims=(32, 32)), batch=True)
# Forward
x = torch.randn(3, 4, 32, 32).requires_grad_()
y = Cop.apply(net(x).reshape(3, 32 * 32)).reshape(3, 32, 32)
# Backward
loss = y.sum()
loss.backward()
fig, axs = plt.subplots(1, 2, figsize=(12, 3))
axs[0].imshow(y[0].detach().numpy())
axs[0].set_title("Forward")
axs[0].axis("tight")
axs[1].imshow(x.grad[0].reshape(4 * 32, 32).T)
axs[1].set_title("Gradient")
axs[1].axis("tight")
plt.tight_layout()

Total running time of the script: ( 0 minutes 0.531 seconds)
Gallery¶
Below is a gallery of examples which use PyLops operators and utilities.
Note
Click here to download the full example code
1D Smoothing¶
This example shows how to use the pylops.Smoothing1D
operator
to smooth an input signal along a given axis.
Derivative (or roughening) operators are generally used regularization in inverse problems. Smoothing has the opposite effect of roughening and it can be employed as preconditioning in inverse problems.
A smoothing operator is a simple compact filter on lenght \(n_{smooth}\) and each elements is equal to \(1/n_{smooth}\).
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Define the input parameters: number of samples of input signal (N
) and
lenght of the smoothing filter regression coefficients (\(n_{smooth}\)).
In this first case the input signal is one at the center and zero elsewhere.
N = 31
nsmooth = 7
x = np.zeros(N)
x[int(N / 2)] = 1
Sop = pylops.Smoothing1D(nsmooth=nsmooth, dims=[N], dtype="float32")
y = Sop * x
xadj = Sop.H * y
fig, ax = plt.subplots(1, 1, figsize=(10, 3))
ax.plot(x, "k", lw=2, label=r"$x$")
ax.plot(y, "r", lw=2, label=r"$y=Ax$")
ax.set_title("Smoothing in 1st direction", fontsize=14, fontweight="bold")
ax.legend()
plt.tight_layout()

Let’s repeat the same exercise with a random signal as input. After applying smoothing, we will also try to invert it.
N = 120
nsmooth = 13
x = np.random.normal(0, 1, N)
Sop = pylops.Smoothing1D(nsmooth=13, dims=(N), dtype="float32")
y = Sop * x
xest = Sop / y
fig, ax = plt.subplots(1, 1, figsize=(10, 3))
ax.plot(x, "k", lw=2, label=r"$x$")
ax.plot(y, "r", lw=2, label=r"$y=Ax$")
ax.plot(xest, "--g", lw=2, label=r"$x_{ext}$")
ax.set_title("Smoothing in 1st direction", fontsize=14, fontweight="bold")
ax.legend()
plt.tight_layout()

Finally we show that the same operator can be applied to multi-dimensional data along a chosen axis.
A = np.zeros((11, 21))
A[5, 10] = 1
Sop = pylops.Smoothing1D(nsmooth=5, dims=(11, 21), axis=0, dtype="float64")
B = Sop * A
fig, axs = plt.subplots(1, 2, figsize=(10, 3))
fig.suptitle(
"Smoothing in 1st direction for 2d data", fontsize=14, fontweight="bold", y=0.95
)
im = axs[0].imshow(A, interpolation="nearest", vmin=0, vmax=1)
axs[0].axis("tight")
axs[0].set_title("Model")
plt.colorbar(im, ax=axs[0])
im = axs[1].imshow(B, interpolation="nearest", vmin=0, vmax=1)
axs[1].axis("tight")
axs[1].set_title("Data")
plt.colorbar(im, ax=axs[1])
plt.tight_layout()
plt.subplots_adjust(top=0.8)

Total running time of the script: ( 0 minutes 0.954 seconds)
Note
Click here to download the full example code
1D, 2D and 3D Sliding¶
This example shows how to use the
pylops.signalprocessing.Sliding1D
,
pylops.signalprocessing.Sliding2D
and pylops.signalprocessing.Sliding3D
operators
to perform repeated transforms over small strides of a 1-, 2- or 3-dimensional
array.
For the 1-d case, the transform that we apply in this example is the
pylops.signalprocessing.FFT
.
For the 2- and 3-d cases, the transform that we apply in this example is the
pylops.signalprocessing.Radon2D
(and pylops.signalprocessing.Radon3D
) but this operator has been
design to allow a variety of transforms as long as they operate with signals
that are 2 or 3-dimensional in nature, respectively.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s start by creating a 1-dimensional array of size \(n_t\) and create a sliding operator to compute its transformed representation.
nwins = 4
nwin = 26
nover = 3
nop = 64
dimd = nwin * nwins - 3 * nover
t = np.arange(dimd) * 0.004
data = np.sin(2 * np.pi * 20 * t)
Op = pylops.signalprocessing.FFT(nwin, nfft=nop, real=True)
nwins, dim, mwin_inends, dwin_inends = pylops.signalprocessing.sliding1d_design(
dimd, nwin, nover, (nop + 2) // 2
)
Slid = pylops.signalprocessing.Sliding1D(
Op.H,
dim,
dimd,
nwin,
nover,
tapertype=None,
)
x = Slid.H * data
We now create a similar operator but we also add a taper to the overlapping
parts of the patches and use it to reconstruct the original signal.
This is done by simply using the adjoint of the
pylops.signalprocessing.Sliding1D
operator. Note that for non-
orthogonal operators, this must be replaced by an inverse.
Slid = pylops.signalprocessing.Sliding1D(
Op.H, dim, dimd, nwin, nover, tapertype="cosine"
)
reconstructed_data = Slid * x
fig, axs = plt.subplots(1, 2, figsize=(15, 3))
axs[0].plot(t, data, "k", label="Data")
axs[0].plot(t, reconstructed_data.real, "--r", label="Rec Data")
axs[0].legend()
axs[1].set(xlabel=r"$t$ [s]", title="Original domain")
for i in range(nwins):
axs[1].plot(Op.f, np.abs(x[i, :]), label=f"Window {i+1}/{nwins}")
axs[1].set(xlabel=r"$f$ [Hz]", title="Transformed domain")
axs[1].legend()
plt.tight_layout()

We now create a 2-dimensional array of size \(n_x \times n_t\) composed of 3 parabolic events
par = {"ox": -140, "dx": 2, "nx": 140, "ot": 0, "dt": 0.004, "nt": 200, "f0": 20}
v = 1500
t0 = [0.2, 0.4, 0.5]
px = [0, 0, 0]
pxx = [1e-5, 5e-6, 1e-20]
amp = [1.0, -2, 0.5]
# Create axis
t, t2, x, y = pylops.utils.seismicevents.makeaxis(par)
# Create wavelet
wav = pylops.utils.wavelets.ricker(t[:41], f0=par["f0"])[0]
# Generate model
_, data = pylops.utils.seismicevents.parabolic2d(x, t, t0, px, pxx, amp, wav)
We want to divide this 2-dimensional data into small overlapping
patches in the spatial direction and apply the adjoint of the
pylops.signalprocessing.Radon2D
operator to each patch. This is
done by simply using the adjoint of the
pylops.signalprocessing.Sliding2D
operator
winsize = 36
overlap = 10
npx = 61
px = np.linspace(-5e-3, 5e-3, npx)
dimsd = data.shape
# Sliding window transform without taper
Op = pylops.signalprocessing.Radon2D(
t,
np.linspace(-par["dx"] * winsize // 2, par["dx"] * winsize // 2, winsize),
px,
centeredh=True,
kind="linear",
engine="numba",
)
nwins, dims, mwin_inends, dwin_inends = pylops.signalprocessing.sliding2d_design(
dimsd, winsize, overlap, (npx, par["nt"])
)
Slid = pylops.signalprocessing.Sliding2D(
Op, dims, dimsd, winsize, overlap, tapertype=None
)
radon = Slid.H * data
We now create a similar operator but we also add a taper to the overlapping parts of the patches.
Slid = pylops.signalprocessing.Sliding2D(
Op, dims, dimsd, winsize, overlap, tapertype="cosine"
)
reconstructed_data = Slid * radon
# Reshape for plotting
radon = radon.reshape(dims)
reconstructed_data = reconstructed_data.reshape(dimsd)
We will see that our reconstructed signal presents some small artifacts. This is because we have not inverted our operator but simply applied the adjoint to estimate the representation of the input data in the Radon domain. We can do better if we use the inverse instead.
radoninv = pylops.LinearOperator(Slid, explicit=False).div(data.ravel(), niter=10)
reconstructed_datainv = Slid * radoninv.ravel()
radoninv = radoninv.reshape(dims)
reconstructed_datainv = reconstructed_datainv.reshape(dimsd)
Let’s finally visualize all the intermediate results as well as our final
data reconstruction after inverting the
pylops.signalprocessing.Sliding2D
operator.
fig, axs = plt.subplots(2, 3, sharey=True, figsize=(12, 10))
im = axs[0][0].imshow(data.T, cmap="gray")
axs[0][0].set_title("Original data")
plt.colorbar(im, ax=axs[0][0])
axs[0][0].axis("tight")
im = axs[0][1].imshow(radon.T, cmap="gray")
axs[0][1].set_title("Adjoint Radon")
plt.colorbar(im, ax=axs[0][1])
axs[0][1].axis("tight")
im = axs[0][2].imshow(reconstructed_data.T, cmap="gray")
axs[0][2].set_title("Reconstruction from adjoint")
plt.colorbar(im, ax=axs[0][2])
axs[0][2].axis("tight")
axs[1][0].axis("off")
im = axs[1][1].imshow(radoninv.T, cmap="gray")
axs[1][1].set_title("Inverse Radon")
plt.colorbar(im, ax=axs[1][1])
axs[1][1].axis("tight")
im = axs[1][2].imshow(reconstructed_datainv.T, cmap="gray")
axs[1][2].set_title("Reconstruction from inverse")
plt.colorbar(im, ax=axs[1][2])
axs[1][2].axis("tight")
for i in range(0, 114, 24):
axs[0][0].axvline(i, color="w", lw=1, ls="--")
axs[0][0].axvline(i + winsize, color="k", lw=1, ls="--")
axs[0][0].text(
i + winsize // 2,
par["nt"] - 10,
"w" + str(i // 24),
ha="center",
va="center",
weight="bold",
color="w",
)
for i in range(0, 305, 61):
axs[0][1].axvline(i, color="w", lw=1, ls="--")
axs[0][1].text(
i + npx // 2,
par["nt"] - 10,
"w" + str(i // 61),
ha="center",
va="center",
weight="bold",
color="w",
)
axs[1][1].axvline(i, color="w", lw=1, ls="--")
axs[1][1].text(
i + npx // 2,
par["nt"] - 10,
"w" + str(i // 61),
ha="center",
va="center",
weight="bold",
color="w",
)

We notice two things, i)provided small enough patches and a transform that can explain data locally, we have been able reconstruct our original data almost to perfection. ii) inverse is betten than adjoint as expected as the adjoin does not only introduce small artifacts but also does not respect the original amplitudes of the data.
An appropriate transform alongside with a sliding window approach will result a very good approach for interpolation (or regularization) or irregularly sampled seismic data.
Finally we do the same for a 3-dimensional array of size \(n_y \times n_x \times n_t\) composed of 3 hyperbolic events
par = {
"oy": -15,
"dy": 2,
"ny": 14,
"ox": -18,
"dx": 2,
"nx": 18,
"ot": 0,
"dt": 0.004,
"nt": 50,
"f0": 30,
}
vrms = [200, 200]
t0 = [0.05, 0.1]
amp = [1.0, -2]
# Create axis
t, t2, x, y = pylops.utils.seismicevents.makeaxis(par)
# Create wavelet
wav = pylops.utils.wavelets.ricker(t[:41], f0=par["f0"])[0]
# Generate model
_, data = pylops.utils.seismicevents.hyperbolic3d(x, y, t, t0, vrms, vrms, amp, wav)
# Sliding window plan
winsize = (5, 6)
overlap = (2, 3)
npx = 21
px = np.linspace(-5e-3, 5e-3, npx)
dimsd = data.shape
# Sliding window transform without taper
Op = pylops.signalprocessing.Radon3D(
t,
np.linspace(-par["dy"] * winsize[0] // 2, par["dy"] * winsize[0] // 2, winsize[0]),
np.linspace(-par["dx"] * winsize[1] // 2, par["dx"] * winsize[1] // 2, winsize[1]),
px,
px,
centeredh=True,
kind="linear",
engine="numba",
)
nwins, dims, mwin_inends, dwin_inends = pylops.signalprocessing.sliding3d_design(
dimsd, winsize, overlap, (npx, npx, par["nt"])
)
Slid = pylops.signalprocessing.Sliding3D(
Op, dims, dimsd, winsize, overlap, (npx, npx), tapertype=None
)
radon = Slid.H * data
Slid = pylops.signalprocessing.Sliding3D(
Op, dims, dimsd, winsize, overlap, (npx, npx), tapertype="cosine"
)
reconstructed_data = Slid * radon
radoninv = pylops.LinearOperator(Slid, explicit=False).div(data.ravel(), niter=10)
radoninv = radoninv.reshape(Slid.dims)
reconstructed_datainv = Slid * radoninv
fig, axs = plt.subplots(2, 3, sharey=True, figsize=(12, 7))
im = axs[0][0].imshow(data[par["ny"] // 2].T, cmap="gray", vmin=-2, vmax=2)
axs[0][0].set_title("Original data")
plt.colorbar(im, ax=axs[0][0])
axs[0][0].axis("tight")
im = axs[0][1].imshow(
radon[nwins[0] // 2, :, :, npx // 2].reshape(nwins[1] * npx, par["nt"]).T,
cmap="gray",
vmin=-25,
vmax=25,
)
axs[0][1].set_title("Adjoint Radon")
plt.colorbar(im, ax=axs[0][1])
axs[0][1].axis("tight")
im = axs[0][2].imshow(
reconstructed_data[par["ny"] // 2].T, cmap="gray", vmin=-1000, vmax=1000
)
axs[0][2].set_title("Reconstruction from adjoint")
plt.colorbar(im, ax=axs[0][2])
axs[0][2].axis("tight")
axs[1][0].axis("off")
im = axs[1][1].imshow(
radoninv[nwins[0] // 2, :, :, npx // 2].reshape(nwins[1] * npx, par["nt"]).T,
cmap="gray",
vmin=-0.025,
vmax=0.025,
)
axs[1][1].set_title("Inverse Radon")
plt.colorbar(im, ax=axs[1][1])
axs[1][1].axis("tight")
im = axs[1][2].imshow(
reconstructed_datainv[par["ny"] // 2].T, cmap="gray", vmin=-2, vmax=2
)
axs[1][2].set_title("Reconstruction from inverse")
plt.colorbar(im, ax=axs[1][2])
axs[1][2].axis("tight")
fig, axs = plt.subplots(2, 3, figsize=(12, 7))
im = axs[0][0].imshow(data[:, :, 25], cmap="gray", vmin=-2, vmax=2)
axs[0][0].set_title("Original data")
plt.colorbar(im, ax=axs[0][0])
axs[0][0].axis("tight")
im = axs[0][1].imshow(
radon[nwins[0] // 2, :, :, :, 25].reshape(nwins[1] * npx, npx).T,
cmap="gray",
vmin=-25,
vmax=25,
)
axs[0][1].set_title("Adjoint Radon")
plt.colorbar(im, ax=axs[0][1])
axs[0][1].axis("tight")
im = axs[0][2].imshow(reconstructed_data[:, :, 25], cmap="gray", vmin=-1000, vmax=1000)
axs[0][2].set_title("Reconstruction from adjoint")
plt.colorbar(im, ax=axs[0][2])
axs[0][2].axis("tight")
axs[1][0].axis("off")
im = axs[1][1].imshow(
radoninv[nwins[0] // 2, :, :, :, 25].reshape(nwins[1] * npx, npx).T,
cmap="gray",
vmin=-0.025,
vmax=0.025,
)
axs[1][1].set_title("Inverse Radon")
plt.colorbar(im, ax=axs[1][1])
axs[1][1].axis("tight")
im = axs[1][2].imshow(reconstructed_datainv[:, :, 25], cmap="gray", vmin=-2, vmax=2)
axs[1][2].set_title("Reconstruction from inverse")
plt.colorbar(im, ax=axs[1][2])
axs[1][2].axis("tight")
plt.tight_layout()
Total running time of the script: ( 0 minutes 9.120 seconds)
Note
Click here to download the full example code
2D Smoothing¶
This example shows how to use the pylops.Smoothing2D
operator
to smooth a multi-dimensional input signal along two given axes.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Define the input parameters: number of samples of input signal (N
and M
) and
lenght of the smoothing filter regression coefficients
(\(n_{smooth,1}\) and \(n_{smooth,2}\)). In this first case the input
signal is one at the center and zero elsewhere.
N, M = 11, 21
nsmooth1, nsmooth2 = 5, 3
A = np.zeros((N, M))
A[5, 10] = 1
Sop = pylops.Smoothing2D(nsmooth=[nsmooth1, nsmooth2], dims=[N, M], dtype="float64")
B = Sop * A
After applying smoothing, we will also try to invert it.
Aest = (Sop / B.ravel()).reshape(Sop.dims)
fig, axs = plt.subplots(1, 3, figsize=(10, 3))
im = axs[0].imshow(A, interpolation="nearest", vmin=0, vmax=1)
axs[0].axis("tight")
axs[0].set_title("Model")
plt.colorbar(im, ax=axs[0])
im = axs[1].imshow(B, interpolation="nearest", vmin=0, vmax=1)
axs[1].axis("tight")
axs[1].set_title("Data")
plt.colorbar(im, ax=axs[1])
im = axs[2].imshow(Aest, interpolation="nearest", vmin=0, vmax=1)
axs[2].axis("tight")
axs[2].set_title("Estimated model")
plt.colorbar(im, ax=axs[2])
plt.tight_layout()

Total running time of the script: ( 0 minutes 0.459 seconds)
Note
Click here to download the full example code
AVO modelling¶
This example shows how to create pre-stack angle gathers using
the pylops.avo.avo.AVOLinearModelling
operator.
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.axes_grid1.axes_divider import make_axes_locatable
from scipy.signal import filtfilt
import pylops
from pylops.utils.wavelets import ricker
plt.close("all")
np.random.seed(0)
Let’s start by creating the input elastic property profiles
nt0 = 501
dt0 = 0.004
ntheta = 21
t0 = np.arange(nt0) * dt0
thetamin, thetamax = 0.0, 40.0
theta = np.linspace(thetamin, thetamax, ntheta)
# Elastic property profiles
vp = (
2000
+ 5 * np.arange(nt0)
+ 2 * filtfilt(np.ones(5) / 5.0, 1, np.random.normal(0, 160, nt0))
)
vs = 600 + vp / 2 + 3 * filtfilt(np.ones(5) / 5.0, 1, np.random.normal(0, 100, nt0))
rho = 1000 + vp + filtfilt(np.ones(5) / 5.0, 1, np.random.normal(0, 120, nt0))
vp[201:] += 1500
vs[201:] += 500
rho[201:] += 100
# Wavelet
ntwav = 41
wavoff = 10
wav, twav, wavc = ricker(t0[: ntwav // 2 + 1], 20)
wav_phase = np.hstack((wav[wavoff:], np.zeros(wavoff)))
# vs/vp profile
vsvp = 0.5
vsvp_z = vs / vp
# Model
m = np.stack((np.log(vp), np.log(vs), np.log(rho)), axis=1)
fig, axs = plt.subplots(1, 3, figsize=(9, 7), sharey=True)
axs[0].plot(vp, t0, "k", lw=3)
axs[0].set(xlabel="[m/s]", ylabel=r"$t$ [s]", ylim=[t0[0], t0[-1]], title="Vp")
axs[0].grid()
axs[1].plot(vp / vs, t0, "k", lw=3)
axs[1].set(title="Vp/Vs")
axs[1].grid()
axs[2].plot(rho, t0, "k", lw=3)
axs[2].set(xlabel="[kg/m³]", title="Rho")
axs[2].invert_yaxis()
axs[2].grid()

We create now the operators to model the AVO responses for a set of elastic profiles
# constant vsvp
PPop_const = pylops.avo.avo.AVOLinearModelling(
theta, vsvp=vsvp, nt0=nt0, linearization="akirich", dtype=np.float64
)
# depth-variant vsvp
PPop_variant = pylops.avo.avo.AVOLinearModelling(
theta, vsvp=vsvp_z, linearization="akirich", dtype=np.float64
)
We can then apply those operators to the elastic model and create some synthetic reflection responses
dPP_const = PPop_const * m
dPP_variant = PPop_variant * m
To visualize these responses, we will plot their anomaly - how much they deveiate from their mean
mean_dPP_const = dPP_const.mean()
dPP_const -= mean_dPP_const
mean_dPP_variant = dPP_variant.mean()
dPP_variant -= mean_dPP_variant
fig, axs = plt.subplots(1, 2, figsize=(10, 5), sharey=True)
im = axs[0].imshow(
dPP_const,
cmap="RdBu_r",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=-dPP_const.max(),
vmax=dPP_const.max(),
)
cax = make_axes_locatable(axs[0]).append_axes("right", size="5%", pad="2%")
cb = fig.colorbar(im, cax=cax)
cb.set_label(f"Deviation from mean = {mean_dPP_const:.2f}")
axs[0].set(xlabel=r"$\theta$ [°]", ylabel=r"$t$ [s]", title="Data with constant VP/VS")
axs[0].axis("tight")
im = axs[1].imshow(
dPP_variant,
cmap="RdBu_r",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=-dPP_variant.max(),
vmax=dPP_variant.max(),
)
cax = make_axes_locatable(axs[1]).append_axes("right", size="5%", pad="2%")
cb = fig.colorbar(im, cax=cax)
cb.set_label(f"Deviation from mean = {mean_dPP_variant:.2f}")
axs[1].set(xlabel=r"$\theta$ [°]", title="Data with variable VP/VS")
axs[1].axis("tight")
plt.tight_layout()

Finally we can also model the PS response by simply changing the
linearization
choice as follows
PSop = pylops.avo.avo.AVOLinearModelling(
theta, vsvp=vsvp, nt0=nt0, linearization="ps", dtype=np.float64
)
We can then apply those operators to the elastic model and create some synthetic reflection responses
dPS = PSop * m
mean_dPS = dPS.mean()
dPS -= mean_dPS
fig, axs = plt.subplots(1, 2, figsize=(10, 5), sharey=True)
im = axs[0].imshow(
dPP_const,
cmap="RdBu_r",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=-dPP_const.max(),
vmax=dPP_const.max(),
)
cax = make_axes_locatable(axs[0]).append_axes("right", size="5%", pad="2%")
cb = fig.colorbar(im, cax=cax)
cb.set_label(f"Deviation from mean = {mean_dPP_const:.2f}")
axs[0].set(xlabel=r"$\theta$ [°]", ylabel=r"$t$ [s]", title="PP Data")
axs[0].axis("tight")
im = axs[1].imshow(
dPS,
cmap="RdBu_r",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=-dPS.max(),
vmax=dPS.max(),
)
cax = make_axes_locatable(axs[1]).append_axes("right", size="5%", pad="2%")
cb = fig.colorbar(im, cax=cax)
cb.set_label(f"Deviation from mean = {mean_dPS:.2f}")
axs[1].set(xlabel=r"$\theta$ [°]", title="PS Data")
axs[1].axis("tight")
plt.tight_layout()

Total running time of the script: ( 0 minutes 1.095 seconds)
Note
Click here to download the full example code
Bilinear Interpolation¶
This example shows how to use the pylops.signalprocessing.Bilinar
operator to perform bilinear interpolation to a 2-dimensional input vector.
import matplotlib.pyplot as plt
import numpy as np
from scipy import misc
import pylops
plt.close("all")
np.random.seed(0)
First of all, we create a 2-dimensional input vector containing an image
from the scipy.misc
family.
x = misc.face()[::5, ::5, 0]
nz, nx = x.shape
We can now define a set of available samples in the first and second direction of the array and apply bilinear interpolation.
nsamples = 2000
iava = np.vstack(
(np.random.uniform(0, nz - 1, nsamples), np.random.uniform(0, nx - 1, nsamples))
)
Bop = pylops.signalprocessing.Bilinear(iava, (nz, nx))
y = Bop * x
At this point we try to reconstruct the input signal imposing a smooth solution by means of a regularization term that minimizes the Laplacian of the solution.
D2op = pylops.Laplacian((nz, nx), weights=(1, 1), dtype="float64")
xadj = Bop.H * y
xinv = pylops.optimization.leastsquares.normal_equations_inversion(
Bop, y, [D2op], epsRs=[np.sqrt(0.1)], **dict(maxiter=100)
)[0]
xadj = xadj.reshape(nz, nx)
xinv = xinv.reshape(nz, nx)
fig, axs = plt.subplots(1, 3, figsize=(10, 4))
fig.suptitle("Bilinear interpolation", fontsize=14, fontweight="bold", y=0.95)
axs[0].imshow(x, cmap="gray_r", vmin=0, vmax=250)
axs[0].axis("tight")
axs[0].set_title("Original")
axs[1].imshow(xadj, cmap="gray_r", vmin=0, vmax=250)
axs[1].axis("tight")
axs[1].set_title("Sampled")
axs[2].imshow(xinv, cmap="gray_r", vmin=0, vmax=250)
axs[2].axis("tight")
axs[2].set_title("2D Regularization")
plt.tight_layout()
plt.subplots_adjust(top=0.8)

Total running time of the script: ( 0 minutes 1.372 seconds)
Note
Click here to download the full example code
CGLS and LSQR Solvers¶
This example shows how to use the pylops.optimization.leastsquares.cgls
and pylops.optimization.leastsquares.lsqr
PyLops solvers
to minimize the following cost function:
Note that the LSQR solver behaves in the same way as the scipy’s
scipy.sparse.linalg.lsqr
solver. However, our solver is also able
to operate on cupy arrays and perform computations on a GPU.
import warnings
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
warnings.filterwarnings("ignore")
Let’s define a matrix \(\mathbf{A}\) or size (N
and M
) and
fill the matrix with random numbers
N, M = 20, 10
A = np.random.normal(0, 1, (N, M))
Aop = pylops.MatrixMult(A, dtype="float64")
x = np.ones(M)
We can now use the cgls solver to invert this matrix
y = Aop * x
xest, istop, nit, r1norm, r2norm, cost_cgls = pylops.optimization.basic.cgls(
Aop, y, x0=np.zeros_like(x), niter=10, tol=1e-10, show=True
)
print(f"x= {x}")
print(f"cgls solution xest= {xest}")
CGLS
-----------------------------------------------------------------
The Operator Op has 20 rows and 10 cols
damp = 0.000000e+00 tol = 1.000000e-10 niter = 10
-----------------------------------------------------------------
Itn x[0] r1norm r2norm
1 9.1362e-01 3.5210e+00 3.5210e+00
2 1.1328e+00 1.9174e+00 1.9174e+00
3 1.1030e+00 7.9210e-01 7.9210e-01
4 1.0366e+00 3.9919e-01 3.9919e-01
5 1.0086e+00 1.4627e-01 1.4627e-01
6 1.0069e+00 8.0987e-02 8.0987e-02
7 9.9981e-01 3.8979e-02 3.8979e-02
8 9.9936e-01 1.9302e-02 1.9302e-02
9 1.0006e+00 3.0820e-03 3.0820e-03
10 1.0000e+00 3.6146e-15 3.6146e-15
Iterations = 10 Total time (s) = 0.00
-----------------------------------------------------------------
x= [1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
cgls solution xest= [1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
And the lsqr solver to invert this matrix
y = Aop * x
(
xest,
istop,
itn,
r1norm,
r2norm,
anorm,
acond,
arnorm,
xnorm,
var,
cost_lsqr,
) = pylops.optimization.basic.lsqr(Aop, y, x0=np.zeros_like(x), niter=10, show=True)
print(f"x= {x}")
print(f"lsqr solution xest= {xest}")
LSQR
------------------------------------------------------------------------------------------
The Operator Op has 20 rows and 10 cols
damp = 0.00000000000000e+00 calc_var = 1
atol = 1.00e-08 conlim = 1.00e+08
btol = 1.00e-08 niter = 10
------------------------------------------------------------------------------------------
Itn x[0] r1norm r2norm Compatible LS Norm A Cond A
0 0.0000e+00 1.650e+01 1.650e+01 1.0e+00 3.4e-01
1 9.1362e-01 3.521e+00 3.521e+00 2.1e-01 1.4e-01 5.7e+00 1.0e+00
2 1.1328e+00 1.917e+00 1.917e+00 1.2e-01 8.8e-02 7.3e+00 2.1e+00
3 1.1030e+00 7.921e-01 7.921e-01 4.8e-02 3.9e-02 9.0e+00 3.5e+00
4 1.0366e+00 3.992e-01 3.992e-01 2.4e-02 1.3e-02 1.1e+01 4.7e+00
5 1.0086e+00 1.463e-01 1.463e-01 8.9e-03 5.1e-03 1.1e+01 6.2e+00
6 1.0069e+00 8.099e-02 8.099e-02 4.9e-03 3.3e-03 1.2e+01 7.4e+00
7 9.9981e-01 3.898e-02 3.898e-02 2.4e-03 1.5e-03 1.3e+01 8.8e+00
8 9.9936e-01 1.930e-02 1.930e-02 1.2e-03 7.2e-04 1.4e+01 1.0e+01
9 1.0006e+00 3.082e-03 3.082e-03 1.9e-04 8.3e-05 1.4e+01 1.2e+01
10 1.0000e+00 4.480e-15 4.480e-15 2.7e-16 3.1e-16 1.4e+01 1.3e+01
LSQR finished, Op x - b is small enough, given atol, btol
istop = 1 r1norm = 4.5e-15 anorm = 1.4e+01 arnorm = 2.8e-14
itn = 10 r2norm = 4.5e-15 acond = 1.3e+01 xnorm = 3.2e+00
Total time (s) = 0.00
------------------------------------------------------------------------------------------
x= [1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
lsqr solution xest= [1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]
Finally we show that the L2 norm of the residual of the two solvers decays in the same way, as LSQR is algebrically equivalent to CG on the normal equations and CGLS
plt.figure(figsize=(12, 3))
plt.plot(cost_cgls, "k", lw=2, label="CGLS")
plt.plot(cost_lsqr, "--r", lw=2, label="LSQR")
plt.title("Cost functions")
plt.legend()
plt.tight_layout()

Note that while we used a dense matrix here, any other linear operator can be fed to cgls and lsqr as is the case for any other PyLops solver.
Total running time of the script: ( 0 minutes 0.185 seconds)
Note
Click here to download the full example code
Causal Integration¶
This example shows how to use the pylops.CausalIntegration
operator to integrate an input signal (in forward mode) and to apply a smooth,
regularized derivative (in inverse mode). This is a very interesting
by-product of this operator which may result very useful when the data
to which you want to apply a numerical derivative is noisy.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s start with a 1D example. Define the input parameters: number of samples
of input signal (nt
), sampling step (dt
) as well as the input
signal which will be equal to \(x(t)=\sin(t)\):
nt = 81
dt = 0.3
t = np.arange(nt) * dt
x = np.sin(t)
We can now create our causal integration operator and apply it to the input
signal. We can also compute the analytical integral
\(y(t)=\int \sin(t)\,\mathrm{d}t=-\cos(t)\) and compare the results. We can also
invert the integration operator and by remembering that this is equivalent
to a first order derivative, we will compare our inverted model with the
result obtained by simply applying the pylops.FirstDerivative
forward operator to the same data.
Note that, as explained in details in pylops.CausalIntegration
,
integration has no unique solution, as any constant \(c\) can be added
to the integrated signal \(y\), for example if \(x(t)=t^2\) the
\(y(t) = \int t^2 \,\mathrm{d}t = \frac{t^3}{3} + c\). We thus subtract first
sample from the analytical integral to obtain the same result as the
numerical one.
Cop = pylops.CausalIntegration(nt, sampling=dt, kind="half")
yana = -np.cos(t) + np.cos(t[0])
y = Cop * x
xinv = Cop / y
# Numerical derivative
Dop = pylops.FirstDerivative(nt, sampling=dt)
xder = Dop * y
# Visualize data and inversion
fig, axs = plt.subplots(1, 2, figsize=(18, 5))
axs[0].plot(t, yana, "r", lw=5, label="analytic integration")
axs[0].plot(t, y, "--g", lw=3, label="numerical integration")
axs[0].legend()
axs[0].set_title("Causal integration")
axs[1].plot(t, x, "k", lw=8, label="original")
axs[1].plot(t[1:-1], xder[1:-1], "r", lw=5, label="numerical")
axs[1].plot(t, xinv, "--g", lw=3, label="inverted")
axs[1].legend()
axs[1].set_title("Inverse causal integration = Derivative")
plt.tight_layout()

As expected we obtain the same result. Let’s see what happens if we now add some random noise to our data.
# Add noise
yn = y + np.random.normal(0, 4e-1, y.shape)
# Numerical derivative
Dop = pylops.FirstDerivative(nt, sampling=dt)
xder = Dop * yn
# Regularized derivative
Rop = pylops.SecondDerivative(nt)
xreg = pylops.optimization.leastsquares.regularized_inversion(
Cop, yn, [Rop], epsRs=[1e0], **dict(iter_lim=100, atol=1e-5)
)[0]
# Preconditioned derivative
Sop = pylops.Smoothing1D(41, nt)
xp = pylops.optimization.leastsquares.preconditioned_inversion(
Cop, yn, Sop, **dict(iter_lim=10, atol=1e-3)
)[0]
# Visualize data and inversion
fig, axs = plt.subplots(1, 2, figsize=(18, 5))
axs[0].plot(t, y, "k", lw=3, label="data")
axs[0].plot(t, yn, "--g", lw=3, label="noisy data")
axs[0].legend()
axs[0].set_title("Causal integration")
axs[1].plot(t, x, "k", lw=8, label="original")
axs[1].plot(t[1:-1], xder[1:-1], "r", lw=3, label="numerical derivative")
axs[1].plot(t, xreg, "g", lw=3, label="regularized")
axs[1].plot(t, xp, "m", lw=3, label="preconditioned")
axs[1].legend()
axs[1].set_title("Inverse causal integration")
plt.tight_layout()

We can see here the great advantage of framing our numerical derivative as an inverse problem, and more specifically as the inverse of the causal integration operator.
Let’s conclude with a 2d example where again the integration/derivative will be performed along the first axis
nt, nx = 41, 11
dt = 0.3
ot = 0
t = np.arange(nt) * dt + ot
x = np.outer(np.sin(t), np.ones(nx))
Cop = pylops.CausalIntegration(dims=(nt, nx), sampling=dt, axis=0, kind="half")
y = Cop * x
yn = y + np.random.normal(0, 4e-1, y.shape)
# Numerical derivative
Dop = pylops.FirstDerivative(dims=(nt, nx), axis=0, sampling=dt)
xder = Dop * yn
# Regularized derivative
Rop = pylops.Laplacian(dims=(nt, nx))
xreg = pylops.optimization.leastsquares.regularized_inversion(
Cop, yn.ravel(), [Rop], epsRs=[1e0], **dict(iter_lim=100, atol=1e-5)
)[0]
xreg = xreg.reshape(nt, nx)
# Preconditioned derivative
Sop = pylops.Smoothing2D((11, 21), dims=(nt, nx))
xp = pylops.optimization.leastsquares.preconditioned_inversion(
Cop, yn.ravel(), Sop, **dict(iter_lim=10, atol=1e-2)
)[0]
xp = xp.reshape(nt, nx)
# Visualize data and inversion
vmax = 2 * np.max(np.abs(x))
fig, axs = plt.subplots(2, 3, figsize=(18, 12))
axs[0][0].imshow(x, cmap="seismic", vmin=-vmax, vmax=vmax)
axs[0][0].set_title("Model")
axs[0][0].axis("tight")
axs[0][1].imshow(y, cmap="seismic", vmin=-vmax, vmax=vmax)
axs[0][1].set_title("Data")
axs[0][1].axis("tight")
axs[0][2].imshow(yn, cmap="seismic", vmin=-vmax, vmax=vmax)
axs[0][2].set_title("Noisy data")
axs[0][2].axis("tight")
axs[1][0].imshow(xder, cmap="seismic", vmin=-vmax, vmax=vmax)
axs[1][0].set_title("Numerical derivative")
axs[1][0].axis("tight")
axs[1][1].imshow(xreg, cmap="seismic", vmin=-vmax, vmax=vmax)
axs[1][1].set_title("Regularized")
axs[1][1].axis("tight")
axs[1][2].imshow(xp, cmap="seismic", vmin=-vmax, vmax=vmax)
axs[1][2].set_title("Preconditioned")
axs[1][2].axis("tight")
plt.tight_layout()
# Visualize data and inversion at a chosen xlocation
fig, axs = plt.subplots(1, 2, figsize=(18, 5))
axs[0].plot(t, y[:, nx // 2], "k", lw=3, label="data")
axs[0].plot(t, yn[:, nx // 2], "--g", lw=3, label="noisy data")
axs[0].legend()
axs[0].set_title("Causal integration")
axs[1].plot(t, x[:, nx // 2], "k", lw=8, label="original")
axs[1].plot(t, xder[:, nx // 2], "r", lw=3, label="numerical derivative")
axs[1].plot(t, xreg[:, nx // 2], "g", lw=3, label="regularized")
axs[1].plot(t, xp[:, nx // 2], "m", lw=3, label="preconditioned")
axs[1].legend()
axs[1].set_title("Inverse causal integration")
plt.tight_layout()
Total running time of the script: ( 0 minutes 1.737 seconds)
Note
Click here to download the full example code
Chirp Radon Transform¶
This example shows how to use the pylops.signalprocessing.ChirpRadon2D
and pylops.signalprocessing.ChirpRadon3D
operators to apply the
linear Radon Transform to 2-dimensional or 3-dimensional signals, respectively.
When working with the linear Radon transform, this is a faster implementation
compared to in pylops.signalprocessing.Radon2D
and
pylops.signalprocessing.Radon3D
and should be preferred.
This method provides also an analytical inverse.
Note that the forward and adjoint definitions in these two pairs of operators are swapped.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s start by creating a empty 2d matrix of size \(n_x \times n_t\) with a single linear event.
par = {
"ot": 0,
"dt": 0.004,
"nt": 51,
"ox": -250,
"dx": 10,
"nx": 51,
"oy": -250,
"dy": 10,
"ny": 51,
"f0": 40,
}
theta = [0.0]
t0 = [0.1]
amp = [1.0]
# Create axes
t, t2, x, y = pylops.utils.seismicevents.makeaxis(par)
dt, dx, dy = par["dt"], par["dx"], par["dy"]
# Create wavelet
wav, _, wav_c = pylops.utils.wavelets.ricker(t[:41], f0=par["f0"])
# Generate data
_, d = pylops.utils.seismicevents.linear2d(x, t, 1500.0, t0, theta, amp, wav)
We can now define our operators and apply the forward, adjoint and inverse steps.
npx, pxmax = par["nx"], 5e-4
px = np.linspace(-pxmax, pxmax, npx)
R2Op = pylops.signalprocessing.ChirpRadon2D(t, x, pxmax * dx / dt, dtype="float64")
dL_chirp = R2Op * d
dadj_chirp = R2Op.H * dL_chirp
dinv_chirp = R2Op.inverse(dL_chirp).reshape(R2Op.dimsd)
fig, axs = plt.subplots(1, 4, figsize=(12, 4), sharey=True)
axs[0].imshow(d.T, vmin=-1, vmax=1, cmap="bwr_r", extent=(x[0], x[-1], t[-1], t[0]))
axs[0].set(xlabel=r"$x$ [m]", ylabel=r"$t$ [s]", title="Input model")
axs[0].axis("tight")
axs[1].imshow(
dL_chirp.T,
cmap="bwr_r",
vmin=-dL_chirp.max(),
vmax=dL_chirp.max(),
extent=(1e3 * px[0], 1e3 * px[-1], t[-1], t[0]),
)
axs[1].set(xlabel=r"$p$ [s/km]", title="Radon Chirp")
axs[1].axis("tight")
axs[2].imshow(
dadj_chirp.T,
cmap="bwr_r",
vmin=-dadj_chirp.max(),
vmax=dadj_chirp.max(),
extent=(x[0], x[-1], t[-1], t[0]),
)
axs[2].set(xlabel=r"$x$ [m]", title="Adj Radon Chirp")
axs[2].axis("tight")
axs[3].imshow(
dinv_chirp.T,
cmap="bwr_r",
vmin=-d.max(),
vmax=d.max(),
extent=(x[0], x[-1], t[-1], t[0]),
)
axs[3].set(xlabel=r"$x$ [m]", title="Inv Radon Chirp")
axs[3].axis("tight")
plt.tight_layout()

Finally we repeat the same exercise with 3d data.
par = {
"ot": 0,
"dt": 0.004,
"nt": 51,
"ox": -400,
"dx": 10,
"nx": 81,
"oy": -600,
"dy": 10,
"ny": 61,
"f0": 20,
}
theta = [10]
phi = [0]
t0 = [0.1]
amp = [1.0]
# Create axes
t, t2, x, y = pylops.utils.seismicevents.makeaxis(par)
dt, dx, dy = par["dt"], par["dx"], par["dy"]
# Generate data
_, d = pylops.utils.seismicevents.linear3d(x, y, t, 1500.0, t0, theta, phi, amp, wav)
npy, pymax = par["ny"], 3e-4
npx, pxmax = par["nx"], 5e-4
py = np.linspace(-pymax, pymax, npy)
px = np.linspace(-pxmax, pxmax, npx)
R3Op = pylops.signalprocessing.ChirpRadon3D(
t, y, x, (pymax * dy / dt, pxmax * dx / dt), dtype="float64"
)
dL_chirp = R3Op * d
dadj_chirp = R3Op.H * dL_chirp
dinv_chirp = R3Op.inverse(dL_chirp).reshape(R3Op.dimsd)
fig, axs = plt.subplots(1, 4, figsize=(12, 4), sharey=True)
axs[0].imshow(
d[par["ny"] // 2].T,
vmin=-1,
vmax=1,
cmap="bwr_r",
extent=(x[0], x[-1], t[-1], t[0]),
)
axs[0].set(xlabel=r"$x$ [m]", ylabel=r"$t$ [s]", title="Input model")
axs[0].axis("tight")
axs[1].imshow(
dL_chirp[par["ny"] // 2].T,
cmap="bwr_r",
vmin=-dL_chirp.max(),
vmax=dL_chirp.max(),
extent=(1e3 * px[0], 1e3 * px[-1], t[-1], t[0]),
)
axs[1].set(xlabel=r"$p_x$ [s/km]", title="Radon Chirp")
axs[1].axis("tight")
axs[2].imshow(
dadj_chirp[par["ny"] // 2].T,
cmap="bwr_r",
vmin=-dadj_chirp.max(),
vmax=dadj_chirp.max(),
extent=(x[0], x[-1], t[-1], t[0]),
)
axs[2].set(xlabel=r"$x$ [m]", title="Adj Radon Chirp")
axs[2].axis("tight")
axs[3].imshow(
dinv_chirp[par["ny"] // 2].T,
cmap="bwr_r",
vmin=-d.max(),
vmax=d.max(),
extent=(x[0], x[-1], t[-1], t[0]),
)
axs[3].set(xlabel=r"$x$ [m]", title="Inv Radon Chirp")
axs[3].axis("tight")
plt.tight_layout()
fig, axs = plt.subplots(1, 4, figsize=(12, 4), sharey=True)
axs[0].imshow(
d[:, par["nx"] // 2].T,
vmin=-1,
vmax=1,
cmap="bwr_r",
extent=(x[0], x[-1], t[-1], t[0]),
)
axs[0].set(xlabel=r"$y$ [m]", ylabel=r"$t$ [s]", title="Input model")
axs[0].axis("tight")
axs[1].imshow(
dL_chirp[:, 2 * par["nx"] // 3].T,
cmap="bwr_r",
vmin=-dL_chirp.max(),
vmax=dL_chirp.max(),
extent=(1e3 * py[0], 1e3 * py[-1], t[-1], t[0]),
)
axs[1].set(xlabel=r"$p_y$ [s/km]", title="Radon Chirp")
axs[1].axis("tight")
axs[2].imshow(
dadj_chirp[:, par["nx"] // 2].T,
cmap="bwr_r",
vmin=-dadj_chirp.max(),
vmax=dadj_chirp.max(),
extent=(x[0], x[-1], t[-1], t[0]),
)
axs[2].set(xlabel=r"$y$ [m]", title="Adj Radon Chirp")
axs[2].axis("tight")
axs[3].imshow(
dinv_chirp[:, par["nx"] // 2].T,
cmap="bwr_r",
vmin=-d.max(),
vmax=d.max(),
extent=(x[0], x[-1], t[-1], t[0]),
)
axs[3].set(xlabel=r"$y$ [m]", title="Inv Radon Chirp")
axs[3].axis("tight")
plt.tight_layout()
Total running time of the script: ( 0 minutes 1.630 seconds)
Note
Click here to download the full example code
Conj¶
This example shows how to use the pylops.basicoperators.Conj
operator.
This operator returns the complex conjugate in both forward and adjoint
modes (it is self adjoint).
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s define a Conj operator to get the complex conjugate of the input.
M = 5
x = np.arange(M) + 1j * np.arange(M)[::-1]
Rop = pylops.basicoperators.Conj(M, dtype="complex128")
y = Rop * x
xadj = Rop.H * y
_, axs = plt.subplots(1, 3, figsize=(10, 4))
axs[0].plot(np.real(x), lw=2, label="Real")
axs[0].plot(np.imag(x), lw=2, label="Imag")
axs[0].legend()
axs[0].set_title("Input")
axs[1].plot(np.real(y), lw=2, label="Real")
axs[1].plot(np.imag(y), lw=2, label="Imag")
axs[1].legend()
axs[1].set_title("Forward of Input")
axs[2].plot(np.real(xadj), lw=2, label="Real")
axs[2].plot(np.imag(xadj), lw=2, label="Imag")
axs[2].legend()
axs[2].set_title("Adjoint of Forward")
plt.tight_layout()

Total running time of the script: ( 0 minutes 0.364 seconds)
Note
Click here to download the full example code
Convolution¶
This example shows how to use the pylops.signalprocessing.Convolve1D
,
pylops.signalprocessing.Convolve2D
and
pylops.signalprocessing.ConvolveND
operators to perform convolution
between two signals.
Such operators can be used in the forward model of several common application in signal processing that require filtering of an input signal for the instrument response. Similarly, removing the effect of the instrument response from signal is equivalent to solving linear system of equations based on Convolve1D, Convolve2D or ConvolveND operators. This problem is generally referred to as Deconvolution.
A very practical example of deconvolution can be found in the geophysical processing of seismic data where the effect of the source response (i.e., airgun or vibroseis) should be removed from the recorded signal to be able to better interpret the response of the subsurface. Similar examples can be found in telecommunication and speech analysis.
import matplotlib.pyplot as plt
import numpy as np
from scipy.sparse.linalg import lsqr
import pylops
from pylops.utils.wavelets import ricker
plt.close("all")
We will start by creating a zero signal of lenght \(nt\) and we will
place a unitary spike at its center. We also create our filter to be
applied by means of pylops.signalprocessing.Convolve1D
operator.
Following the seismic example mentioned above, the filter is a
Ricker wavelet
with dominant frequency \(f_0 = 30 Hz\).
nt = 1001
dt = 0.004
t = np.arange(nt) * dt
x = np.zeros(nt)
x[int(nt / 2)] = 1
h, th, hcenter = ricker(t[:101], f0=30)
Cop = pylops.signalprocessing.Convolve1D(nt, h=h, offset=hcenter, dtype="float32")
y = Cop * x
xinv = Cop / y
fig, ax = plt.subplots(1, 1, figsize=(10, 3))
ax.plot(t, x, "k", lw=2, label=r"$x$")
ax.plot(t, y, "r", lw=2, label=r"$y=Ax$")
ax.plot(t, xinv, "--g", lw=2, label=r"$x_{ext}$")
ax.set_title("Convolve 1d data", fontsize=14, fontweight="bold")
ax.legend()
ax.set_xlim(1.9, 2.1)
plt.tight_layout()

We show now that also a filter with mixed phase (i.e., not centered
around zero) can be applied and inverted for using the
pylops.signalprocessing.Convolve1D
operator.
Cop = pylops.signalprocessing.Convolve1D(nt, h=h, offset=hcenter - 3, dtype="float32")
y = Cop * x
y1 = Cop.H * x
xinv = Cop / y
fig, ax = plt.subplots(1, 1, figsize=(10, 3))
ax.plot(t, x, "k", lw=2, label=r"$x$")
ax.plot(t, y, "r", lw=2, label=r"$y=Ax$")
ax.plot(t, y1, "b", lw=2, label=r"$y=A^Hx$")
ax.plot(t, xinv, "--g", lw=2, label=r"$x_{ext}$")
ax.set_title(
"Convolve 1d data with non-zero phase filter", fontsize=14, fontweight="bold"
)
ax.set_xlim(1.9, 2.1)
ax.legend()
plt.tight_layout()

We repeat a similar exercise but using two dimensional signals and
filters taking advantage of the
pylops.signalprocessing.Convolve2D
operator.
nt = 51
nx = 81
dt = 0.004
t = np.arange(nt) * dt
x = np.zeros((nt, nx))
x[int(nt / 2), int(nx / 2)] = 1
nh = [11, 5]
h = np.ones((nh[0], nh[1]))
Cop = pylops.signalprocessing.Convolve2D(
(nt, nx),
h=h,
offset=(int(nh[0]) / 2, int(nh[1]) / 2),
dtype="float32",
)
y = Cop * x
xinv = (Cop / y.ravel()).reshape(Cop.dims)
fig, axs = plt.subplots(1, 3, figsize=(10, 3))
fig.suptitle("Convolve 2d data", fontsize=14, fontweight="bold", y=0.95)
axs[0].imshow(x, cmap="gray", vmin=-1, vmax=1)
axs[1].imshow(y, cmap="gray", vmin=-1, vmax=1)
axs[2].imshow(xinv, cmap="gray", vmin=-1, vmax=1)
axs[0].set_title("x")
axs[0].axis("tight")
axs[1].set_title("y")
axs[1].axis("tight")
axs[2].set_title("xlsqr")
axs[2].axis("tight")
plt.tight_layout()
plt.subplots_adjust(top=0.8)
fig, ax = plt.subplots(1, 2, figsize=(10, 3))
fig.suptitle("Convolve in 2d data - traces", fontsize=14, fontweight="bold", y=0.95)
ax[0].plot(x[int(nt / 2), :], "k", lw=2, label=r"$x$")
ax[0].plot(y[int(nt / 2), :], "r", lw=2, label=r"$y=Ax$")
ax[0].plot(xinv[int(nt / 2), :], "--g", lw=2, label=r"$x_{ext}$")
ax[1].plot(x[:, int(nx / 2)], "k", lw=2, label=r"$x$")
ax[1].plot(y[:, int(nx / 2)], "r", lw=2, label=r"$y=Ax$")
ax[1].plot(xinv[:, int(nx / 2)], "--g", lw=2, label=r"$x_{ext}$")
ax[0].legend()
ax[0].set_xlim(30, 50)
ax[1].legend()
ax[1].set_xlim(10, 40)
plt.tight_layout()
plt.subplots_adjust(top=0.8)
Finally we do the same using three dimensional signals and
filters taking advantage of the
pylops.signalprocessing.ConvolveND
operator.
ny, nx, nz = 13, 10, 7
x = np.zeros((ny, nx, nz))
x[ny // 3, nx // 2, nz // 4] = 1
h = np.ones((3, 5, 3))
offset = [1, 2, 1]
Cop = pylops.signalprocessing.ConvolveND(
dims=(ny, nx, nz), h=h, offset=offset, axes=(0, 1, 2), dtype="float32"
)
y = Cop * x
xlsqr = lsqr(Cop, y.ravel(), damp=0, iter_lim=300, show=0)[0]
xlsqr = xlsqr.reshape(Cop.dims)
fig, axs = plt.subplots(3, 3, figsize=(10, 12))
fig.suptitle("Convolve 3d data", y=0.95, fontsize=14, fontweight="bold")
axs[0][0].imshow(x[ny // 3], cmap="gray", vmin=-1, vmax=1)
axs[0][1].imshow(y[ny // 3], cmap="gray", vmin=-1, vmax=1)
axs[0][2].imshow(xlsqr[ny // 3], cmap="gray", vmin=-1, vmax=1)
axs[0][0].set_title("x")
axs[0][0].axis("tight")
axs[0][1].set_title("y")
axs[0][1].axis("tight")
axs[0][2].set_title("xlsqr")
axs[0][2].axis("tight")
axs[1][0].imshow(x[:, nx // 2], cmap="gray", vmin=-1, vmax=1)
axs[1][1].imshow(y[:, nx // 2], cmap="gray", vmin=-1, vmax=1)
axs[1][2].imshow(xlsqr[:, nx // 2], cmap="gray", vmin=-1, vmax=1)
axs[1][0].axis("tight")
axs[1][1].axis("tight")
axs[1][2].axis("tight")
axs[2][0].imshow(x[..., nz // 4], cmap="gray", vmin=-1, vmax=1)
axs[2][1].imshow(y[..., nz // 4], cmap="gray", vmin=-1, vmax=1)
axs[2][2].imshow(xlsqr[..., nz // 4], cmap="gray", vmin=-1, vmax=1)
axs[2][0].axis("tight")
axs[2][1].axis("tight")
axs[2][2].axis("tight")
plt.tight_layout()

Total running time of the script: ( 0 minutes 2.024 seconds)
Note
Click here to download the full example code
Derivatives¶
This example shows how to use the suite of derivative operators, namely
pylops.FirstDerivative
, pylops.SecondDerivative
,
pylops.Laplacian
and pylops.Gradient
,
pylops.FirstDirectionalDerivative
and
pylops.SecondDirectionalDerivative
.
The derivative operators are very useful when the model to be inverted for is expect to be smooth in one or more directions. As shown in the Optimization tutorial, these operators will be used as part of the regularization term to obtain a smooth solution.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
np.random.seed(0)
Let’s start by looking at a simple first-order centered derivative and how could implement it naively by creating a dense matrix. Note that we will not apply the derivative where the stencil is partially outside of the range of the input signal (i.e., at the edge of the signal)
nx = 10
D = np.diag(0.5 * np.ones(nx - 1), k=1) - np.diag(0.5 * np.ones(nx - 1), -1)
D[0] = D[-1] = 0
fig, ax = plt.subplots(1, 1, figsize=(6, 4))
im = plt.imshow(D, cmap="rainbow", vmin=-0.5, vmax=0.5)
ax.set_title("First derivative", size=14, fontweight="bold")
ax.set_xticks(np.arange(nx - 1) + 0.5)
ax.set_yticks(np.arange(nx - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
fig.colorbar(im, ax=ax, ticks=[-0.5, 0.5], shrink=0.7)

<matplotlib.colorbar.Colorbar object at 0x7f6840c540d0>
We now create a signal filled with zero and a single one at its center and apply the derivative matrix by means of a dot product
x = np.zeros(nx)
x[int(nx / 2)] = 1
y_dir = np.dot(D, x)
xadj_dir = np.dot(D.T, y_dir)
Let’s now do the same using the pylops.FirstDerivative
operator
and compare its outputs after applying the forward and adjoint operators
to those from the dense matrix.
D1op = pylops.FirstDerivative(nx, dtype="float32")
y_lop = D1op * x
xadj_lop = D1op.H * y_lop
fig, axs = plt.subplots(3, 1, figsize=(13, 8), sharex=True)
axs[0].stem(np.arange(nx), x, linefmt="k", markerfmt="ko")
axs[0].set_title("Input", size=20, fontweight="bold")
axs[1].stem(np.arange(nx), y_dir, linefmt="k", markerfmt="ko", label="direct")
axs[1].stem(np.arange(nx), y_lop, linefmt="--r", markerfmt="ro", label="lop")
axs[1].set_title("Forward", size=20, fontweight="bold")
axs[1].legend()
axs[2].stem(np.arange(nx), xadj_dir, linefmt="k", markerfmt="ko", label="direct")
axs[2].stem(np.arange(nx), xadj_lop, linefmt="--r", markerfmt="ro", label="lop")
axs[2].set_title("Adjoint", size=20, fontweight="bold")
axs[2].legend()
plt.tight_layout()

As expected we obtain the same result, with the only difference that in the second case we did not need to explicitly create a matrix, saving memory and computational time.
Let’s move onto applying the same first derivative to a 2d array in the first direction
nx, ny = 11, 21
A = np.zeros((nx, ny))
A[nx // 2, ny // 2] = 1.0
D1op = pylops.FirstDerivative((nx, ny), axis=0, dtype="float64")
B = D1op * A
fig, axs = plt.subplots(1, 2, figsize=(10, 3), sharey=True)
fig.suptitle(
"First Derivative in 1st direction", fontsize=12, fontweight="bold", y=0.95
)
im = axs[0].imshow(A, interpolation="nearest", cmap="rainbow")
axs[0].axis("tight")
axs[0].set_title("x")
plt.colorbar(im, ax=axs[0])
im = axs[1].imshow(B, interpolation="nearest", cmap="rainbow")
axs[1].axis("tight")
axs[1].set_title("y")
plt.colorbar(im, ax=axs[1])
plt.tight_layout()
plt.subplots_adjust(top=0.8)

We can now do the same for the second derivative
A = np.zeros((nx, ny))
A[nx // 2, ny // 2] = 1.0
D2op = pylops.SecondDerivative(dims=(nx, ny), axis=0, dtype="float64")
B = D2op * A
fig, axs = plt.subplots(1, 2, figsize=(10, 3), sharey=True)
fig.suptitle(
"Second Derivative in 1st direction", fontsize=12, fontweight="bold", y=0.95
)
im = axs[0].imshow(A, interpolation="nearest", cmap="rainbow")
axs[0].axis("tight")
axs[0].set_title("x")
plt.colorbar(im, ax=axs[0])
im = axs[1].imshow(B, interpolation="nearest", cmap="rainbow")
axs[1].axis("tight")
axs[1].set_title("y")
plt.colorbar(im, ax=axs[1])
plt.tight_layout()
plt.subplots_adjust(top=0.8)

We can also apply the second derivative to the second direction of
our data (axis=1
)
D2op = pylops.SecondDerivative(dims=(nx, ny), axis=1, dtype="float64")
B = D2op * A
fig, axs = plt.subplots(1, 2, figsize=(10, 3), sharey=True)
fig.suptitle(
"Second Derivative in 2nd direction", fontsize=12, fontweight="bold", y=0.95
)
im = axs[0].imshow(A, interpolation="nearest", cmap="rainbow")
axs[0].axis("tight")
axs[0].set_title("x")
plt.colorbar(im, ax=axs[0])
im = axs[1].imshow(B, interpolation="nearest", cmap="rainbow")
axs[1].axis("tight")
axs[1].set_title("y")
plt.colorbar(im, ax=axs[1])
plt.tight_layout()
plt.subplots_adjust(top=0.8)

We use the symmetrical Laplacian operator as well as a asymmetrical version of it (by adding more weight to the derivative along one direction)
# symmetrical
L2symop = pylops.Laplacian(dims=(nx, ny), weights=(1, 1), dtype="float64")
# asymmetrical
L2asymop = pylops.Laplacian(dims=(nx, ny), weights=(3, 1), dtype="float64")
Bsym = L2symop * A
Basym = L2asymop * A
fig, axs = plt.subplots(1, 3, figsize=(10, 3), sharey=True)
fig.suptitle("Laplacian", fontsize=12, fontweight="bold", y=0.95)
im = axs[0].imshow(A, interpolation="nearest", cmap="rainbow")
axs[0].axis("tight")
axs[0].set_title("x")
plt.colorbar(im, ax=axs[0])
im = axs[1].imshow(Bsym, interpolation="nearest", cmap="rainbow")
axs[1].axis("tight")
axs[1].set_title("y sym")
plt.colorbar(im, ax=axs[1])
im = axs[2].imshow(Basym, interpolation="nearest", cmap="rainbow")
axs[2].axis("tight")
axs[2].set_title("y asym")
plt.colorbar(im, ax=axs[2])
plt.tight_layout()
plt.subplots_adjust(top=0.8)

We consider now the gradient operator. Given a 2-dimensional array, this operator applies first-order derivatives on both dimensions and concatenates them.
Gop = pylops.Gradient(dims=(nx, ny), dtype="float64")
B = Gop * A
C = Gop.H * B
fig, axs = plt.subplots(2, 2, figsize=(10, 6), sharex=True, sharey=True)
fig.suptitle("Gradient", fontsize=12, fontweight="bold", y=0.95)
im = axs[0, 0].imshow(A, interpolation="nearest", cmap="rainbow")
axs[0, 0].axis("tight")
axs[0, 0].set_title("x")
plt.colorbar(im, ax=axs[0, 0])
im = axs[0, 1].imshow(B[0, ...], interpolation="nearest", cmap="rainbow")
axs[0, 1].axis("tight")
axs[0, 1].set_title("y - 1st direction")
plt.colorbar(im, ax=axs[0, 1])
im = axs[1, 1].imshow(B[1, ...], interpolation="nearest", cmap="rainbow")
axs[1, 1].axis("tight")
axs[1, 1].set_title("y - 2nd direction")
plt.colorbar(im, ax=axs[1, 1])
im = axs[1, 0].imshow(C, interpolation="nearest", cmap="rainbow")
axs[1, 0].axis("tight")
axs[1, 0].set_title("xadj")
plt.colorbar(im, ax=axs[1, 0])
plt.tight_layout()

Finally we use the Gradient operator to compute directional derivatives. We create a model which has some layering in the horizontal and vertical directions and show how the direction derivatives differs from standard derivatives
nx, nz = 60, 40
horlayers = np.cumsum(np.random.uniform(2, 10, 20).astype(int))
horlayers = horlayers[horlayers < nz // 2]
nhorlayers = len(horlayers)
vertlayers = np.cumsum(np.random.uniform(2, 20, 10).astype(int))
vertlayers = vertlayers[vertlayers < nx]
nvertlayers = len(vertlayers)
A = 1500 * np.ones((nz, nx))
for top, base in zip(horlayers[:-1], horlayers[1:]):
A[top:base] = np.random.normal(2000, 200)
for top, base in zip(vertlayers[:-1], vertlayers[1:]):
A[horlayers[-1] :, top:base] = np.random.normal(2000, 200)
v = np.zeros((2, nz, nx))
v[0, : horlayers[-1]] = 1
v[1, horlayers[-1] :] = 1
Ddop = pylops.FirstDirectionalDerivative((nz, nx), v=v, sampling=(nz, nx))
D2dop = pylops.SecondDirectionalDerivative((nz, nx), v=v, sampling=(nz, nx))
dirder = Ddop * A
dir2der = D2dop * A
jump = 4
fig, axs = plt.subplots(3, 1, figsize=(4, 9), sharex=True)
im = axs[0].imshow(A, cmap="gist_rainbow", extent=(0, nx // jump, nz // jump, 0))
q = axs[0].quiver(
np.arange(nx // jump) + 0.5,
np.arange(nz // jump) + 0.5,
np.flipud(v[1, ::jump, ::jump]),
np.flipud(v[0, ::jump, ::jump]),
color="w",
linewidths=20,
)
axs[0].set_title("x")
axs[0].axis("tight")
axs[1].imshow(dirder, cmap="gray", extent=(0, nx // jump, nz // jump, 0))
axs[1].set_title("y = D * x")
axs[1].axis("tight")
axs[2].imshow(dir2der, cmap="gray", extent=(0, nx // jump, nz // jump, 0))
axs[2].set_title("y = D2 * x")
axs[2].axis("tight")
plt.tight_layout()

Total running time of the script: ( 0 minutes 2.968 seconds)
Note
Click here to download the full example code
Describe¶
This example focuses on the usage of the pylops.utils.describe.describe
method, which allows expressing any PyLops operator into its equivalent
mathematical representation. This is done with the aid of
sympy, a Python library for symbolic computing
import matplotlib.pyplot as plt
import numpy as np
import pylops
from pylops.utils.describe import describe
plt.close("all")
Let’s start by defining 3 PyLops operators. Note that once an operator is defined we can attach a name to the operator; by doing so, this name will be used in the mathematical description of the operator. Alternatively, the describe method will randomly choose a name for us.
A = pylops.MatrixMult(np.ones((10, 5)))
A.name = "A"
B = pylops.Diagonal(np.ones(5))
B.name = "A"
C = pylops.MatrixMult(np.ones((10, 5)))
# Simple operator
describe(A)
# Transpose
AT = A.T
describe(AT)
# Adjoint
AH = A.H
describe(AH)
# Scaled
A3 = 3 * A
describe(A3)
# Sum
D = A + C
describe(D)
A
where: {'A': 'MatrixMult'}
A.T
where: {'A': 'MatrixMult'}
Adjoint(A)
where: {'A': 'MatrixMult'}
3*A
where: {'A': 'MatrixMult'}
A + M
where: {'A': 'MatrixMult', 'M': 'MatrixMult'}
So far so good. Let’s see what happens if we accidentally call two different operators with the same name. You will see that PyLops catches that and changes the name for us (and provides us with a nice warning!)
D = A * B
describe(D)
A*K
where: {'A': 'MatrixMult', 'K': 'Diagonal'}
We can move now to something more complicated using various composition operators
H = pylops.HStack((A * B, C * B))
describe(H)
H = pylops.Block([[A * B, C], [A, A]])
describe(H)
Matrix([[A*K, M*K]])
where: {'A': 'MatrixMult', 'K': 'Diagonal', 'M': 'MatrixMult'}
Matrix([
[Matrix([[A*K, M]])],
[ Matrix([[A, A]])]])
where: {'A': 'MatrixMult', 'K': 'Diagonal', 'M': 'MatrixMult'}
Finally, note that you can get the best out of the describe method if working inside a Jupyter notebook. There, the mathematical expression will be rendered using a LeTex format! See an example notebook.
Total running time of the script: ( 0 minutes 0.323 seconds)
Note
Click here to download the full example code
Diagonal¶
This example shows how to use the pylops.Diagonal
operator
to perform Element-wise multiplication between the input vector and a vector \(\mathbf{d}\).
In other words, the operator acts as a diagonal operator \(\mathbf{D}\) whose elements along the diagonal are the elements of the vector \(\mathbf{d}\).
import matplotlib.gridspec as pltgs
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s define a diagonal operator \(\mathbf{d}\) with increasing numbers from
0
to N
and a unitary model \(\mathbf{x}\).
N = 10
d = np.arange(N)
x = np.ones(N)
Dop = pylops.Diagonal(d)
y = Dop * x
y1 = Dop.H * x
gs = pltgs.GridSpec(1, 6)
fig = plt.figure(figsize=(7, 4))
ax = plt.subplot(gs[0, 0:3])
im = ax.imshow(Dop.matrix(), cmap="rainbow", vmin=0, vmax=N)
ax.set_title("A", size=20, fontweight="bold")
ax.set_xticks(np.arange(N - 1) + 0.5)
ax.set_yticks(np.arange(N - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax.axis("tight")
ax = plt.subplot(gs[0, 3])
ax.imshow(x[:, np.newaxis], cmap="rainbow", vmin=0, vmax=N)
ax.set_title("x", size=20, fontweight="bold")
ax.set_xticks([])
ax.set_yticks(np.arange(N - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax = plt.subplot(gs[0, 4])
ax.text(
0.35,
0.5,
"=",
horizontalalignment="center",
verticalalignment="center",
size=40,
fontweight="bold",
)
ax.axis("off")
ax = plt.subplot(gs[0, 5])
ax.imshow(y[:, np.newaxis], cmap="rainbow", vmin=0, vmax=N)
ax.set_title("y", size=20, fontweight="bold")
ax.set_xticks([])
ax.set_yticks(np.arange(N - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
fig.colorbar(im, ax=ax, ticks=[0, N], pad=0.3, shrink=0.7)
plt.tight_layout()

Similarly we can consider the input model as composed of two or more dimensions. In this case the diagonal operator can be still applied to each element or broadcasted along a specific direction. Let’s start with the simplest case where each element is multipled by a different value
nx, ny = 3, 5
x = np.ones((nx, ny))
print(f"x =\n{x}")
d = np.arange(nx * ny).reshape(nx, ny)
Dop = pylops.Diagonal(d)
y = Dop * x.ravel()
y1 = Dop.H * x.ravel()
print(f"y = D*x =\n{y.reshape(nx, ny)}")
print(f"xadj = D'*x =\n{y1.reshape(nx, ny)}")
x =
[[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]]
y = D*x =
[[ 0. 1. 2. 3. 4.]
[ 5. 6. 7. 8. 9.]
[10. 11. 12. 13. 14.]]
xadj = D'*x =
[[ 0. 1. 2. 3. 4.]
[ 5. 6. 7. 8. 9.]
[10. 11. 12. 13. 14.]]
And we now broadcast
nx, ny = 3, 5
x = np.ones((nx, ny))
print(f"x =\n{x}")
# 1st dim
d = np.arange(nx)
Dop = pylops.Diagonal(d, dims=(nx, ny), axis=0)
y = Dop * x.ravel()
y1 = Dop.H * x.ravel()
print(f"1st dim: y = D*x =\n{y.reshape(nx, ny)}")
print(f"1st dim: xadj = D'*x =\n{y1.reshape(nx, ny)}")
# 2nd dim
d = np.arange(ny)
Dop = pylops.Diagonal(d, dims=(nx, ny), axis=1)
y = Dop * x.ravel()
y1 = Dop.H * x.ravel()
print(f"2nd dim: y = D*x =\n{y.reshape(nx, ny)}")
print(f"2nd dim: xadj = D'*x =\n{y1.reshape(nx, ny)}")
x =
[[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]
[1. 1. 1. 1. 1.]]
1st dim: y = D*x =
[[0. 0. 0. 0. 0.]
[1. 1. 1. 1. 1.]
[2. 2. 2. 2. 2.]]
1st dim: xadj = D'*x =
[[0. 0. 0. 0. 0.]
[1. 1. 1. 1. 1.]
[2. 2. 2. 2. 2.]]
2nd dim: y = D*x =
[[0. 1. 2. 3. 4.]
[0. 1. 2. 3. 4.]
[0. 1. 2. 3. 4.]]
2nd dim: xadj = D'*x =
[[0. 1. 2. 3. 4.]
[0. 1. 2. 3. 4.]
[0. 1. 2. 3. 4.]]
Total running time of the script: ( 0 minutes 0.261 seconds)
Note
Click here to download the full example code
Flip along an axis¶
This example shows how to use the pylops.Flip
operator to simply flip an input signal along an axis.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s start with a 1D example. Define an input signal composed of
nt
samples
nt = 10
x = np.arange(nt)
We can now create our flip operator and apply it to the input signal. We can also apply the adjoint to the flipped signal and we can see how for this operator the adjoint is effectively equivalent to the inverse.
Fop = pylops.Flip(nt)
y = Fop * x
xadj = Fop.H * y
plt.figure(figsize=(3, 5))
plt.plot(x, "k", lw=3, label=r"$x$")
plt.plot(y, "r", lw=3, label=r"$y=Fx$")
plt.plot(xadj, "--g", lw=3, label=r"$x_{adj} = F^H y$")
plt.title("Flip in 1st direction", fontsize=14, fontweight="bold")
plt.legend()
plt.tight_layout()

Let’s now repeat the same exercise on a two dimensional signal. We will first flip the model along the first axis and then along the second axis
nt, nx = 10, 5
x = np.outer(np.arange(nt), np.ones(nx))
Fop = pylops.Flip((nt, nx), axis=0)
y = Fop * x
xadj = Fop.H * y
fig, axs = plt.subplots(1, 3, figsize=(7, 3))
fig.suptitle(
"Flip in 1st direction for 2d data", fontsize=14, fontweight="bold", y=0.95
)
axs[0].imshow(x, cmap="rainbow")
axs[0].set_title(r"$x$")
axs[0].axis("tight")
axs[1].imshow(y, cmap="rainbow")
axs[1].set_title(r"$y = F x$")
axs[1].axis("tight")
axs[2].imshow(xadj, cmap="rainbow")
axs[2].set_title(r"$x_{adj} = F^H y$")
axs[2].axis("tight")
plt.tight_layout()
plt.subplots_adjust(top=0.8)
x = np.outer(np.ones(nt), np.arange(nx))
Fop = pylops.Flip(dims=(nt, nx), axis=1)
y = Fop * x
xadj = Fop.H * y
# sphinx_gallery_thumbnail_number = 3
fig, axs = plt.subplots(1, 3, figsize=(7, 3))
fig.suptitle(
"Flip in 2nd direction for 2d data", fontsize=14, fontweight="bold", y=0.95
)
axs[0].imshow(x, cmap="rainbow")
axs[0].set_title(r"$x$")
axs[0].axis("tight")
axs[1].imshow(y, cmap="rainbow")
axs[1].set_title(r"$y = F x$")
axs[1].axis("tight")
axs[2].imshow(xadj, cmap="rainbow")
axs[2].set_title(r"$x_{adj} = F^H y$")
axs[2].axis("tight")
plt.tight_layout()
plt.subplots_adjust(top=0.8)
Total running time of the script: ( 0 minutes 0.729 seconds)
Note
Click here to download the full example code
Fourier Transform¶
This example shows how to use the pylops.signalprocessing.FFT
,
pylops.signalprocessing.FFT2D
and pylops.signalprocessing.FFTND
operators to apply the Fourier
Transform to the model and the inverse Fourier Transform to the data.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s start by applying the one dimensional FFT to a one dimensional sinusoidal signal \(d(t)=sin(2 \pi f_0t)\) using a time axis of lenght \(nt\) and sampling \(dt\)
dt = 0.005
nt = 100
t = np.arange(nt) * dt
f0 = 10
nfft = 2**10
d = np.sin(2 * np.pi * f0 * t)
FFTop = pylops.signalprocessing.FFT(dims=nt, nfft=nfft, sampling=dt, engine="numpy")
D = FFTop * d
# Adjoint = inverse for FFT
dinv = FFTop.H * D
dinv = FFTop / D
fig, axs = plt.subplots(1, 2, figsize=(10, 4))
axs[0].plot(t, d, "k", lw=2, label="True")
axs[0].plot(t, dinv.real, "--r", lw=2, label="Inverted")
axs[0].legend()
axs[0].set_title("Signal")
axs[1].plot(FFTop.f[: int(FFTop.nfft / 2)], np.abs(D[: int(FFTop.nfft / 2)]), "k", lw=2)
axs[1].set_title("Fourier Transform")
axs[1].set_xlim([0, 3 * f0])
plt.tight_layout()

In this example we used numpy as our engine for the fft
and ifft
.
PyLops implements a second engine (engine='fftw'
) which uses the
well-known FFTW via the python wrapper
pyfftw.FFTW
. This optimized fft tends to outperform the one from
numpy in many cases but it is not inserted in the mandatory requirements of
PyLops. If interested to use FFTW
backend, read the fft routines
section at Advanced installation.
FFTop = pylops.signalprocessing.FFT(dims=nt, nfft=nfft, sampling=dt, engine="fftw")
D = FFTop * d
# Adjoint = inverse for FFT
dinv = FFTop.H * D
dinv = FFTop / D
fig, axs = plt.subplots(1, 2, figsize=(10, 4))
axs[0].plot(t, d, "k", lw=2, label="True")
axs[0].plot(t, dinv.real, "--r", lw=2, label="Inverted")
axs[0].legend()
axs[0].set_title("Signal")
axs[1].plot(FFTop.f[: int(FFTop.nfft / 2)], np.abs(D[: int(FFTop.nfft / 2)]), "k", lw=2)
axs[1].set_title("Fourier Transform with fftw")
axs[1].set_xlim([0, 3 * f0])
plt.tight_layout()

We can also apply the one dimensional FFT to to a two-dimensional signal (along one of the first axis)
dt = 0.005
nt, nx = 100, 20
t = np.arange(nt) * dt
f0 = 10
nfft = 2**10
d = np.outer(np.sin(2 * np.pi * f0 * t), np.arange(nx) + 1)
FFTop = pylops.signalprocessing.FFT(dims=(nt, nx), axis=0, nfft=nfft, sampling=dt)
D = FFTop * d.ravel()
# Adjoint = inverse for FFT
dinv = FFTop.H * D
dinv = FFTop / D
dinv = np.real(dinv).reshape(nt, nx)
fig, axs = plt.subplots(2, 2, figsize=(10, 6))
axs[0][0].imshow(d, vmin=-20, vmax=20, cmap="bwr")
axs[0][0].set_title("Signal")
axs[0][0].axis("tight")
axs[0][1].imshow(np.abs(D.reshape(nfft, nx)[:200, :]), cmap="bwr")
axs[0][1].set_title("Fourier Transform")
axs[0][1].axis("tight")
axs[1][0].imshow(dinv, vmin=-20, vmax=20, cmap="bwr")
axs[1][0].set_title("Inverted")
axs[1][0].axis("tight")
axs[1][1].imshow(d - dinv, vmin=-20, vmax=20, cmap="bwr")
axs[1][1].set_title("Error")
axs[1][1].axis("tight")
fig.tight_layout()

We can also apply the two dimensional FFT to to a two-dimensional signal
dt, dx = 0.005, 5
nt, nx = 100, 201
t = np.arange(nt) * dt
x = np.arange(nx) * dx
f0 = 10
nfft = 2**10
d = np.outer(np.sin(2 * np.pi * f0 * t), np.arange(nx) + 1)
FFTop = pylops.signalprocessing.FFT2D(
dims=(nt, nx), nffts=(nfft, nfft), sampling=(dt, dx)
)
D = FFTop * d.ravel()
dinv = FFTop.H * D
dinv = FFTop / D
dinv = np.real(dinv).reshape(nt, nx)
fig, axs = plt.subplots(2, 2, figsize=(10, 6))
axs[0][0].imshow(d, vmin=-100, vmax=100, cmap="bwr")
axs[0][0].set_title("Signal")
axs[0][0].axis("tight")
axs[0][1].imshow(
np.abs(np.fft.fftshift(D.reshape(nfft, nfft), axes=1)[:200, :]), cmap="bwr"
)
axs[0][1].set_title("Fourier Transform")
axs[0][1].axis("tight")
axs[1][0].imshow(dinv, vmin=-100, vmax=100, cmap="bwr")
axs[1][0].set_title("Inverted")
axs[1][0].axis("tight")
axs[1][1].imshow(d - dinv, vmin=-100, vmax=100, cmap="bwr")
axs[1][1].set_title("Error")
axs[1][1].axis("tight")
fig.tight_layout()

Finally can apply the three dimensional FFT to to a three-dimensional signal
dt, dx, dy = 0.005, 5, 3
nt, nx, ny = 30, 21, 11
t = np.arange(nt) * dt
x = np.arange(nx) * dx
y = np.arange(nx) * dy
f0 = 10
nfft = 2**6
nfftk = 2**5
d = np.outer(np.sin(2 * np.pi * f0 * t), np.arange(nx) + 1)
d = np.tile(d[:, :, np.newaxis], [1, 1, ny])
FFTop = pylops.signalprocessing.FFTND(
dims=(nt, nx, ny), nffts=(nfft, nfftk, nfftk), sampling=(dt, dx, dy)
)
D = FFTop * d.ravel()
dinv = FFTop.H * D
dinv = FFTop / D
dinv = np.real(dinv).reshape(nt, nx, ny)
fig, axs = plt.subplots(2, 2, figsize=(10, 6))
axs[0][0].imshow(d[:, :, ny // 2], vmin=-20, vmax=20, cmap="bwr")
axs[0][0].set_title("Signal")
axs[0][0].axis("tight")
axs[0][1].imshow(
np.abs(np.fft.fftshift(D.reshape(nfft, nfftk, nfftk), axes=1)[:20, :, nfftk // 2]),
cmap="bwr",
)
axs[0][1].set_title("Fourier Transform")
axs[0][1].axis("tight")
axs[1][0].imshow(dinv[:, :, ny // 2], vmin=-20, vmax=20, cmap="bwr")
axs[1][0].set_title("Inverted")
axs[1][0].axis("tight")
axs[1][1].imshow(d[:, :, ny // 2] - dinv[:, :, ny // 2], vmin=-20, vmax=20, cmap="bwr")
axs[1][1].set_title("Error")
axs[1][1].axis("tight")
fig.tight_layout()

Total running time of the script: ( 0 minutes 1.977 seconds)
Note
Click here to download the full example code
Identity¶
This example shows how to use the pylops.Identity
operator to transfer model
into data and viceversa.
import matplotlib.gridspec as pltgs
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s define an identity operator \(\mathbf{Iop}\) with same number of elements for data and model (\(N=M\)).
N, M = 5, 5
x = np.arange(M)
Iop = pylops.Identity(M, dtype="int")
y = Iop * x
xadj = Iop.H * y
gs = pltgs.GridSpec(1, 6)
fig = plt.figure(figsize=(7, 4))
ax = plt.subplot(gs[0, 0:3])
im = ax.imshow(np.eye(N), cmap="rainbow")
ax.set_title("A", size=20, fontweight="bold")
ax.set_xticks(np.arange(N - 1) + 0.5)
ax.set_yticks(np.arange(M - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax = plt.subplot(gs[0, 3])
ax.imshow(x[:, np.newaxis], cmap="rainbow")
ax.set_title("x", size=20, fontweight="bold")
ax.set_xticks([])
ax.set_yticks(np.arange(M - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax = plt.subplot(gs[0, 4])
ax.text(
0.35,
0.5,
"=",
horizontalalignment="center",
verticalalignment="center",
size=40,
fontweight="bold",
)
ax.axis("off")
ax = plt.subplot(gs[0, 5])
ax.imshow(y[:, np.newaxis], cmap="rainbow")
ax.set_title("y", size=20, fontweight="bold")
ax.set_xticks([])
ax.set_yticks(np.arange(N - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
fig.colorbar(im, ax=ax, ticks=[0, 1], pad=0.3, shrink=0.7)
plt.tight_layout()

Similarly we can consider the case with data bigger than model
N, M = 10, 5
x = np.arange(M)
Iop = pylops.Identity(N, M, dtype="int")
y = Iop * x
xadj = Iop.H * y
print(f"x = {x} ")
print(f"I*x = {y} ")
print(f"I'*y = {xadj} ")
x = [0 1 2 3 4]
I*x = [0 1 2 3 4 0 0 0 0 0]
I'*y = [0 1 2 3 4]
and model bigger than data
N, M = 5, 10
x = np.arange(M)
Iop = pylops.Identity(N, M, dtype="int")
y = Iop * x
xadj = Iop.H * y
print(f"x = {x} ")
print(f"I*x = {y} ")
print(f"I'*y = {xadj} ")
x = [0 1 2 3 4 5 6 7 8 9]
I*x = [0 1 2 3 4]
I'*y = [0 1 2 3 4 0 0 0 0 0]
Note that this operator can be useful in many real-life applications when for example we want to manipulate a subset of the model array and keep intact the rest of the array. For example:
\[\begin{split}\begin{bmatrix} \mathbf{A} \quad \mathbf{I} \end{bmatrix} \begin{bmatrix} \mathbf{x_1} \\ \mathbf{x_2} \end{bmatrix} = \mathbf{A} \mathbf{x_1} + \mathbf{x_2}\end{split}\]
Refer to the tutorial on Optimization for more details on this.
Total running time of the script: ( 0 minutes 0.207 seconds)
Note
Click here to download the full example code
Imag¶
This example shows how to use the pylops.basicoperators.Imag
operator.
This operator returns the imaginary part of the data as a real value in
forward mode, and the real part of the model as an imaginary value in
adjoint mode (with zero real part).
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s define a Imag operator \(\mathbf{\Im}\) to extract the imaginary component of the input.
M = 5
x = np.arange(M) + 1j * np.arange(M)[::-1]
Rop = pylops.basicoperators.Imag(M, dtype="complex128")
y = Rop * x
xadj = Rop.H * y
_, axs = plt.subplots(1, 3, figsize=(10, 4))
axs[0].plot(np.real(x), lw=2, label="Real")
axs[0].plot(np.imag(x), lw=2, label="Imag")
axs[0].legend()
axs[0].set_title("Input")
axs[1].plot(np.real(y), lw=2, label="Real")
axs[1].plot(np.imag(y), lw=2, label="Imag")
axs[1].legend()
axs[1].set_title("Forward of Input")
axs[2].plot(np.real(xadj), lw=2, label="Real")
axs[2].plot(np.imag(xadj), lw=2, label="Imag")
axs[2].legend()
axs[2].set_title("Adjoint of Forward")
plt.tight_layout()

Total running time of the script: ( 0 minutes 0.356 seconds)
Note
Click here to download the full example code
Linear Regression¶
This example shows how to use the pylops.LinearRegression
operator
to perform Linear regression analysis.
In short, linear regression is the problem of finding the best fitting coefficients, namely intercept \(\mathbf{x_0}\) and gradient \(\mathbf{x_1}\), for this equation:
\[y_i = x_0 + x_1 t_i \qquad \forall i=0,1,\ldots,N-1\]
As we can express this problem in a matrix form:
\[\mathbf{y}= \mathbf{A} \mathbf{x}\]
our solution can be obtained by solving the following optimization problem:
\[J= \|\mathbf{y} - \mathbf{A} \mathbf{x}\|_2\]
See documentation of pylops.LinearRegression
for more detailed
definition of the forward problem.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
np.random.seed(10)
Define the input parameters: number of samples along the t-axis (N
),
linear regression coefficients (x
), and standard deviation of noise
to be added to data (sigma
).
N = 30
x = np.array([1.0, 2.0])
sigma = 1
Let’s create the time axis and initialize the
pylops.LinearRegression
operator
t = np.arange(N, dtype="float64")
LRop = pylops.LinearRegression(t, dtype="float64")
We can then apply the operator in forward mode to compute our data points
along the x-axis (y
). We will also generate some random gaussian noise
and create a noisy version of the data (yn
).
y = LRop * x
yn = y + np.random.normal(0, sigma, N)
We are now ready to solve our problem. As we are using an operator from the
pylops.LinearOperator
family, we can simply use /
,
which in this case will solve the system by means of an iterative solver
(i.e., scipy.sparse.linalg.lsqr
).
xest = LRop / y
xnest = LRop / yn
Let’s plot the best fitting line for the case of noise free and noisy data
plt.figure(figsize=(5, 7))
plt.plot(
np.array([t.min(), t.max()]),
np.array([t.min(), t.max()]) * x[1] + x[0],
"k",
lw=4,
label=rf"true: $x_0$ = {x[0]:.2f}, $x_1$ = {x[1]:.2f}",
)
plt.plot(
np.array([t.min(), t.max()]),
np.array([t.min(), t.max()]) * xest[1] + xest[0],
"--r",
lw=4,
label=rf"est noise-free: $x_0$ = {xest[0]:.2f}, $x_1$ = {xest[1]:.2f}",
)
plt.plot(
np.array([t.min(), t.max()]),
np.array([t.min(), t.max()]) * xnest[1] + xnest[0],
"--g",
lw=4,
label=rf"est noisy: $x_0$ = {xnest[0]:.2f}, $x_1$ = {xnest[1]:.2f}",
)
plt.scatter(t, y, c="r", s=70)
plt.scatter(t, yn, c="g", s=70)
plt.legend()
plt.tight_layout()

Once that we have estimated the best fitting coefficients \(\mathbf{x}\) we can now use them to compute the y values for a different set of values along the t-axis.
t1 = np.linspace(-N, N, 2 * N, dtype="float64")
y1 = LRop.apply(t1, xest)
plt.figure(figsize=(5, 7))
plt.plot(t, y, "k", label="Original axis")
plt.plot(t1, y1, "r", label="New axis")
plt.scatter(t, y, c="k", s=70)
plt.scatter(t1, y1, c="r", s=40)
plt.legend()
plt.tight_layout()

We consider now the case where some of the observations have large errors.
Such elements are generally referred to as outliers and can affect the
quality of the least-squares solution if not treated with care. In this
example we will see how using a L1 solver such as
pylops.optimization.sparsity.IRLS
can drammatically improve the
quality of the estimation of intercept and gradient.
class CallbackIRLS(pylops.optimization.callback.Callbacks):
def __init__(self, n):
self.n = n
self.xirls_hist = []
self.rw_hist = []
def on_step_end(self, solver, x):
print(solver.iiter)
if solver.iiter > 1:
self.xirls_hist.append(x)
self.rw_hist.append(solver.rw)
else:
self.rw_hist.append(np.ones(self.n))
# Add outliers
yn[1] += 40
yn[N - 2] -= 20
# IRLS
nouter = 20
epsR = 1e-2
epsI = 0
tolIRLS = 1e-2
xnest = LRop / yn
cb = CallbackIRLS(N)
irlssolve = pylops.optimization.sparsity.IRLS(
LRop,
[
cb,
],
)
xirls, nouter = irlssolve.solve(
yn, nouter=nouter, threshR=False, epsR=epsR, epsI=epsI, tolIRLS=tolIRLS
)
xirls_hist, rw_hist = np.array(cb.xirls_hist), cb.rw_hist
print(f"IRLS converged at {nouter} iterations...")
plt.figure(figsize=(5, 7))
plt.plot(
np.array([t.min(), t.max()]),
np.array([t.min(), t.max()]) * x[1] + x[0],
"k",
lw=4,
label=rf"true: $x_0$ = {x[0]:.2f}, $x_1$ = {x[1]:.2f}",
)
plt.plot(
np.array([t.min(), t.max()]),
np.array([t.min(), t.max()]) * xnest[1] + xnest[0],
"--r",
lw=4,
label=rf"L2: $x_0$ = {xnest[0]:.2f}, $x_1$ = {xnest[1]:.2f}",
)
plt.plot(
np.array([t.min(), t.max()]),
np.array([t.min(), t.max()]) * xirls[1] + xirls[0],
"--g",
lw=4,
label=rf"L1 - IRSL: $x_0$ = {xirls[0]:.2f}, $x_1$ = {xirls[1]:.2f}",
)
plt.scatter(t, y, c="r", s=70)
plt.scatter(t, yn, c="g", s=70)
plt.legend()
plt.tight_layout()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
IRLS converged at 16 iterations...
Let’s finally take a look at the convergence of IRLS. First we visualize the evolution of intercept and gradient
fig, axs = plt.subplots(2, 1, figsize=(8, 10))
fig.suptitle("IRLS evolution", fontsize=14, fontweight="bold", y=0.95)
axs[0].plot(xirls_hist[:, 0], xirls_hist[:, 1], ".-k", lw=2, ms=20)
axs[0].scatter(x[0], x[1], c="r", s=70)
axs[0].set_title("Intercept and gradient")
axs[0].grid()
for iiter in range(nouter):
axs[1].semilogy(
rw_hist[iiter],
color=(iiter / nouter, iiter / nouter, iiter / nouter),
label="iter%d" % iiter,
)
axs[1].set_title("Weights")
axs[1].legend(loc=5, fontsize="small")
plt.tight_layout()
plt.subplots_adjust(top=0.8)

Total running time of the script: ( 0 minutes 1.131 seconds)
Note
Click here to download the full example code
MP, OMP, ISTA and FISTA¶
This example shows how to use the pylops.optimization.sparsity.omp
,
pylops.optimization.sparsity.irls
,
pylops.optimization.sparsity.ista
, and
pylops.optimization.sparsity.fista
solvers.
These solvers can be used when the model to retrieve is supposed to have a sparse representation in a certain domain. MP and OMP use a L0 norm and mathematically translates to solving the following constrained problem:
while IRLS, ISTA and FISTA solve an uncostrained problem with a L1 regularization term:
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
np.random.seed(0)
Let’s start with a simple example, where we create a dense mixing matrix and a sparse signal and we use OMP and ISTA to recover such a signal. Note that the mixing matrix leads to an underdetermined system of equations (\(N < M\)) so being able to add some extra prior information regarding the sparsity of our desired model is essential to be able to invert such a system.
N, M = 15, 20
A = np.random.randn(N, M)
A = A / np.linalg.norm(A, axis=0)
Aop = pylops.MatrixMult(A)
x = np.random.rand(M)
x[x < 0.9] = 0
y = Aop * x
# MP/OMP
eps = 1e-2
maxit = 500
x_mp = pylops.optimization.sparsity.omp(
Aop, y, niter_outer=maxit, niter_inner=0, sigma=1e-4
)[0]
x_omp = pylops.optimization.sparsity.omp(Aop, y, niter_outer=maxit, sigma=1e-4)[0]
# IRLS
x_irls = pylops.optimization.sparsity.irls(
Aop, y, nouter=50, epsI=1e-5, kind="model", **dict(iter_lim=10)
)[0]
# ISTA
x_ista = pylops.optimization.sparsity.ista(
Aop,
y,
niter=maxit,
eps=eps,
tol=1e-3,
)[0]
fig, ax = plt.subplots(1, 1, figsize=(8, 3))
m, s, b = ax.stem(x, linefmt="k", basefmt="k", markerfmt="ko", label="True")
plt.setp(m, markersize=15)
m, s, b = ax.stem(x_mp, linefmt="--c", basefmt="--c", markerfmt="co", label="MP")
plt.setp(m, markersize=10)
m, s, b = ax.stem(x_omp, linefmt="--g", basefmt="--g", markerfmt="go", label="OMP")
plt.setp(m, markersize=7)
m, s, b = ax.stem(x_irls, linefmt="--m", basefmt="--m", markerfmt="mo", label="IRLS")
plt.setp(m, markersize=7)
m, s, b = ax.stem(x_ista, linefmt="--r", basefmt="--r", markerfmt="ro", label="ISTA")
plt.setp(m, markersize=3)
ax.set_title("Model", size=15, fontweight="bold")
ax.legend()
plt.tight_layout()

We now consider a more interesting problem problem, wavelet deconvolution
from a signal that we assume being composed by a train of spikes convolved
with a certain wavelet. We will see how solving such a problem with a
least-squares solver such as
pylops.optimization.leastsquares.regularized_inversion
does not
produce the expected results (especially in the presence of noisy data),
conversely using the pylops.optimization.sparsity.ista
and
pylops.optimization.sparsity.fista
solvers allows us
to succesfully retrieve the input signal even in the presence of noise.
pylops.optimization.sparsity.fista
shows faster convergence which
is particularly useful for this problem.
nt = 61
dt = 0.004
t = np.arange(nt) * dt
x = np.zeros(nt)
x[10] = -0.4
x[int(nt / 2)] = 1
x[nt - 20] = 0.5
h, th, hcenter = pylops.utils.wavelets.ricker(t[:101], f0=20)
Cop = pylops.signalprocessing.Convolve1D(nt, h=h, offset=hcenter, dtype="float32")
y = Cop * x
yn = y + np.random.normal(0, 0.1, y.shape)
# noise free
xls = Cop / y
xomp, nitero, costo = pylops.optimization.sparsity.omp(
Cop, y, niter_outer=200, sigma=1e-8
)
xista, niteri, costi = pylops.optimization.sparsity.ista(
Cop,
y,
niter=400,
eps=5e-1,
tol=1e-8,
)
fig, ax = plt.subplots(1, 1, figsize=(8, 3))
ax.plot(t, x, "k", lw=8, label=r"$x$")
ax.plot(t, y, "r", lw=4, label=r"$y=Ax$")
ax.plot(t, xls, "--g", lw=4, label=r"$x_{LS}$")
ax.plot(t, xomp, "--b", lw=4, label=r"$x_{OMP} (niter=%d)$" % nitero)
ax.plot(t, xista, "--m", lw=4, label=r"$x_{ISTA} (niter=%d)$" % niteri)
ax.set_title("Noise-free deconvolution", fontsize=14, fontweight="bold")
ax.legend()
plt.tight_layout()
# noisy
xls = pylops.optimization.leastsquares.regularized_inversion(
Cop, yn, [], **dict(damp=1e-1, atol=1e-3, iter_lim=100, show=0)
)[0]
xista, niteri, costi = pylops.optimization.sparsity.ista(
Cop,
yn,
niter=100,
eps=5e-1,
tol=1e-5,
)
xfista, niterf, costf = pylops.optimization.sparsity.fista(
Cop,
yn,
niter=100,
eps=5e-1,
tol=1e-5,
)
fig, ax = plt.subplots(1, 1, figsize=(8, 3))
ax.plot(t, x, "k", lw=8, label=r"$x$")
ax.plot(t, y, "r", lw=4, label=r"$y=Ax$")
ax.plot(t, yn, "--b", lw=4, label=r"$y_n$")
ax.plot(t, xls, "--g", lw=4, label=r"$x_{LS}$")
ax.plot(t, xista, "--m", lw=4, label=r"$x_{ISTA} (niter=%d)$" % niteri)
ax.plot(t, xfista, "--y", lw=4, label=r"$x_{FISTA} (niter=%d)$" % niterf)
ax.set_title("Noisy deconvolution", fontsize=14, fontweight="bold")
ax.legend()
plt.tight_layout()
fig, ax = plt.subplots(1, 1, figsize=(8, 3))
ax.semilogy(costi, "m", lw=2, label=r"$x_{ISTA} (niter=%d)$" % niteri)
ax.semilogy(costf, "y", lw=2, label=r"$x_{FISTA} (niter=%d)$" % niterf)
ax.set_title("Cost function", size=15, fontweight="bold")
ax.set_xlabel("Iteration")
ax.legend()
ax.grid(True, which="both")
plt.tight_layout()
Total running time of the script: ( 0 minutes 1.817 seconds)
Note
Click here to download the full example code
Matrix Multiplication¶
This example shows how to use the pylops.MatrixMult
operator
to perform Matrix inversion of the following linear system.
You will see that since this operator is a simple overloading to a
numpy.ndarray
object, the solution of the linear system
can be obtained via both direct inversion (i.e., by means explicit
solver such as scipy.linalg.solve
or scipy.linalg.lstsq
)
and iterative solver (i.e., from scipy.sparse.linalg.lsqr
).
Note that in case of rectangular \(\mathbf{A}\), an exact inverse does not exist and a least-square solution is computed instead.
import warnings
import matplotlib.gridspec as pltgs
import matplotlib.pyplot as plt
import numpy as np
from scipy.sparse import rand
from scipy.sparse.linalg import lsqr
import pylops
plt.close("all")
warnings.filterwarnings("ignore")
# sphinx_gallery_thumbnail_number = 2
Let’s define the sizes of the matrix \(\mathbf{A}\) (N
and M
) and
fill the matrix with random numbers
N, M = 20, 20
A = np.random.normal(0, 1, (N, M))
Aop = pylops.MatrixMult(A, dtype="float64")
x = np.ones(M)
We can now apply the forward operator to create the data vector \(\mathbf{y}\)
and use /
to solve the system by means of an explicit solver.
y = Aop * x
xest = Aop / y
Let’s visually plot the system of equations we just solved.
gs = pltgs.GridSpec(1, 6)
fig = plt.figure(figsize=(7, 3))
ax = plt.subplot(gs[0, 0])
ax.imshow(y[:, np.newaxis], cmap="rainbow")
ax.set_title("y", size=20, fontweight="bold")
ax.set_xticks([])
ax.set_yticks(np.arange(N - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax = plt.subplot(gs[0, 1])
ax.text(
0.35,
0.5,
"=",
horizontalalignment="center",
verticalalignment="center",
size=40,
fontweight="bold",
)
ax.axis("off")
ax = plt.subplot(gs[0, 2:5])
ax.imshow(Aop.A, cmap="rainbow")
ax.set_title("A", size=20, fontweight="bold")
ax.set_xticks(np.arange(N - 1) + 0.5)
ax.set_yticks(np.arange(M - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax = plt.subplot(gs[0, 5])
ax.imshow(x[:, np.newaxis], cmap="rainbow")
ax.set_title("x", size=20, fontweight="bold")
ax.set_xticks([])
ax.set_yticks(np.arange(M - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
plt.tight_layout()
gs = pltgs.GridSpec(1, 6)
fig = plt.figure(figsize=(7, 3))
ax = plt.subplot(gs[0, 0])
ax.imshow(x[:, np.newaxis], cmap="rainbow")
ax.set_title("xest", size=20, fontweight="bold")
ax.set_xticks([])
ax.set_yticks(np.arange(M - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax = plt.subplot(gs[0, 1])
ax.text(
0.35,
0.5,
"=",
horizontalalignment="center",
verticalalignment="center",
size=40,
fontweight="bold",
)
ax.axis("off")
ax = plt.subplot(gs[0, 2:5])
ax.imshow(Aop.inv(), cmap="rainbow")
ax.set_title(r"A$^{-1}$", size=20, fontweight="bold")
ax.set_xticks(np.arange(N - 1) + 0.5)
ax.set_yticks(np.arange(M - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax = plt.subplot(gs[0, 5])
ax.imshow(y[:, np.newaxis], cmap="rainbow")
ax.set_title("y", size=20, fontweight="bold")
ax.set_xticks([])
ax.set_yticks(np.arange(N - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
plt.tight_layout()
Let’s also plot the matrix eigenvalues
plt.figure(figsize=(8, 3))
plt.plot(Aop.eigs(), "k", lw=2)
plt.title("Eigenvalues", size=16, fontweight="bold")
plt.xlabel("#eigenvalue")
plt.xlabel("intensity")
plt.tight_layout()

We can also repeat the same exercise for a non-square matrix
N, M = 200, 50
A = np.random.normal(0, 1, (N, M))
x = np.ones(M)
Aop = pylops.MatrixMult(A, dtype="float64")
y = Aop * x
yn = y + np.random.normal(0, 0.3, N)
xest = Aop / y
xnest = Aop / yn
plt.figure(figsize=(8, 3))
plt.plot(x, "k", lw=2, label="True")
plt.plot(xest, "--r", lw=2, label="Noise-free")
plt.plot(xnest, "--g", lw=2, label="Noisy")
plt.title("Matrix inversion", size=16, fontweight="bold")
plt.legend()
plt.tight_layout()

And we can also use a sparse matrix from the scipy.sparse
family of sparse matrices.
N, M = 5, 5
A = rand(N, M, density=0.75)
x = np.ones(M)
Aop = pylops.MatrixMult(A, dtype="float64")
y = Aop * x
xest = Aop / y
print(f"A= {Aop.A.todense()}")
print(f"A^-1= {Aop.inv().todense()}")
print(f"eigs= {Aop.eigs()}")
print(f"x= {x}")
print(f"y= {y}")
print(f"lsqr solution xest= {xest}")
A= [[0.09004394 0.07465359 0. 0.87688481 0.47238513]
[0.79938802 0.85640696 0.71661781 0.85258904 0. ]
[0. 0.14734131 0.56655652 0.16975416 0. ]
[0.29282169 0.46547813 0.90723735 0.62796526 0. ]
[0.50711649 0.9563702 0.77100806 0.01124646 0. ]]
A^-1= [[ 0. 13.07319194 29.81875979 -25.7426719 -3.77140228]
[ 0. -10.01880875 -25.46927387 20.41578795 4.00446478]
[ 0. 3.89143177 12.16350133 -8.55004722 -1.19720943]
[ 0. -4.29168649 -12.5984235 10.81561744 0.51995029]
[ 2.11691676 7.05799755 21.72748426 -18.39641109 -0.87913918]]
eigs= [ 1.91944139+0.j 0.50019806+0.j -0.17658613-0.24554532j]
x= [1. 1. 1. 1. 1.]
y= [1.51396747 3.22500183 0.88365199 2.29350244 2.24574121]
lsqr solution xest= [1. 1. 1. 1. 1.]
Finally, in several circumstances the input model \(\mathbf{x}\) may be more naturally arranged as a matrix or a multi-dimensional array and it may be desirable to apply the same matrix to every columns of the model. This can be mathematically expressed as:
\[\begin{split}\mathbf{y} = \begin{bmatrix} \mathbf{A} \quad \mathbf{0} \quad \mathbf{0} \\ \mathbf{0} \quad \mathbf{A} \quad \mathbf{0} \\ \mathbf{0} \quad \mathbf{0} \quad \mathbf{A} \end{bmatrix} \begin{bmatrix} \mathbf{x_1} \\ \mathbf{x_2} \\ \mathbf{x_3} \end{bmatrix}\end{split}\]
and apply it using the same implementation of the
pylops.MatrixMult
operator by simply telling the operator how we
want the model to be organized through the otherdims
input parameter.
A = np.array([[1.0, 2.0], [4.0, 5.0]])
x = np.array([[1.0, 1.0], [2.0, 2.0], [3.0, 3.0]])
Aop = pylops.MatrixMult(A, otherdims=(3,), dtype="float64")
y = Aop * x.ravel()
xest, istop, itn, r1norm, r2norm = lsqr(Aop, y, damp=1e-10, iter_lim=10, show=0)[0:5]
xest = xest.reshape(3, 2)
print(f"A= {A}")
print(f"x= {x}")
print(f"y={y}")
print(f"lsqr solution xest= {xest}")
A= [[1. 2.]
[4. 5.]]
x= [[1. 1.]
[2. 2.]
[3. 3.]]
y=[ 5. 7. 8. 14. 19. 23.]
lsqr solution xest= [[1. 1.]
[2. 2.]
[3. 3.]]
Total running time of the script: ( 0 minutes 1.044 seconds)
Note
Click here to download the full example code
Multi-Dimensional Convolution¶
This example shows how to use the pylops.waveeqprocessing.MDC
operator
to convolve a 3D kernel with an input seismic data. The resulting data is
a blurred version of the input data and the problem of removing such blurring
is reffered to as Multi-dimensional Deconvolution (MDD) and its implementation
is discussed in more details in the MDD tutorial.
import matplotlib.pyplot as plt
import numpy as np
import pylops
from pylops.utils.seismicevents import hyperbolic2d, makeaxis
from pylops.utils.tapers import taper3d
from pylops.utils.wavelets import ricker
plt.close("all")
Let’s start by creating a set of hyperbolic events to be used as our MDC kernel
# Input parameters
par = {
"ox": -300,
"dx": 10,
"nx": 61,
"oy": -500,
"dy": 10,
"ny": 101,
"ot": 0,
"dt": 0.004,
"nt": 400,
"f0": 20,
"nfmax": 200,
}
t0_m = 0.2
vrms_m = 1100.0
amp_m = 1.0
t0_G = (0.2, 0.5, 0.7)
vrms_G = (1200.0, 1500.0, 2000.0)
amp_G = (1.0, 0.6, 0.5)
# Taper
tap = taper3d(par["nt"], (par["ny"], par["nx"]), (5, 5), tapertype="hanning")
# Create axis
t, t2, x, y = makeaxis(par)
# Create wavelet
wav = ricker(t[:41], f0=par["f0"])[0]
# Generate model
m, mwav = hyperbolic2d(x, t, t0_m, vrms_m, amp_m, wav)
# Generate operator
G, Gwav = np.zeros((par["ny"], par["nx"], par["nt"])), np.zeros(
(par["ny"], par["nx"], par["nt"])
)
for iy, y0 in enumerate(y):
G[iy], Gwav[iy] = hyperbolic2d(x - y0, t, t0_G, vrms_G, amp_G, wav)
G, Gwav = G * tap, Gwav * tap
# Add negative part to data and model
m = np.concatenate((np.zeros((par["nx"], par["nt"] - 1)), m), axis=-1)
mwav = np.concatenate((np.zeros((par["nx"], par["nt"] - 1)), mwav), axis=-1)
Gwav2 = np.concatenate((np.zeros((par["ny"], par["nx"], par["nt"] - 1)), Gwav), axis=-1)
# Define MDC linear operator
Gwav_fft = np.fft.rfft(Gwav2, 2 * par["nt"] - 1, axis=-1)
Gwav_fft = Gwav_fft[..., : par["nfmax"]]
# Move frequency/time to first axis
m, mwav = m.T, mwav.T
Gwav_fft = Gwav_fft.transpose(2, 0, 1)
# Create operator
MDCop = pylops.waveeqprocessing.MDC(
Gwav_fft,
nt=2 * par["nt"] - 1,
nv=1,
dt=0.004,
dr=1.0,
)
# Create data
d = MDCop * m.ravel()
d = d.reshape(2 * par["nt"] - 1, par["ny"])
# Apply adjoint operator to data
madj = MDCop.H * d.ravel()
madj = madj.reshape(2 * par["nt"] - 1, par["nx"])
Finally let’s display the operator, input model, data and adjoint model
fig, axs = plt.subplots(1, 2, figsize=(9, 6))
axs[0].imshow(
Gwav2[int(par["ny"] / 2)].T,
aspect="auto",
interpolation="nearest",
cmap="gray",
vmin=-Gwav2.max(),
vmax=Gwav2.max(),
extent=(x.min(), x.max(), t2.max(), t2.min()),
)
axs[0].set_title("G - inline view", fontsize=15)
axs[0].set_xlabel("r")
axs[1].set_ylabel("t")
axs[1].imshow(
Gwav2[:, int(par["nx"] / 2)].T,
aspect="auto",
interpolation="nearest",
cmap="gray",
vmin=-Gwav2.max(),
vmax=Gwav2.max(),
extent=(y.min(), y.max(), t2.max(), t2.min()),
)
axs[1].set_title("G - inline view", fontsize=15)
axs[1].set_xlabel("s")
axs[1].set_ylabel("t")
fig.tight_layout()
fig, axs = plt.subplots(1, 3, figsize=(9, 6))
axs[0].imshow(
mwav,
aspect="auto",
interpolation="nearest",
cmap="gray",
vmin=-mwav.max(),
vmax=mwav.max(),
extent=(x.min(), x.max(), t2.max(), t2.min()),
)
axs[0].set_title(r"$m$", fontsize=15)
axs[0].set_xlabel("r")
axs[0].set_ylabel("t")
axs[1].imshow(
d,
aspect="auto",
interpolation="nearest",
cmap="gray",
vmin=-d.max(),
vmax=d.max(),
extent=(x.min(), x.max(), t2.max(), t2.min()),
)
axs[1].set_title(r"$d$", fontsize=15)
axs[1].set_xlabel("s")
axs[1].set_ylabel("t")
axs[2].imshow(
madj,
aspect="auto",
interpolation="nearest",
cmap="gray",
vmin=-madj.max(),
vmax=madj.max(),
extent=(x.min(), x.max(), t2.max(), t2.min()),
)
axs[2].set_title(r"$m_{adj}$", fontsize=15)
axs[2].set_xlabel("s")
axs[2].set_ylabel("t")
fig.tight_layout()
Total running time of the script: ( 0 minutes 1.161 seconds)
Note
Click here to download the full example code
Normal Moveout (NMO) Correction¶
This example shows how to create your own operator for performing
normal moveout (NMO) correction to a seismic record.
We will perform classic NMO using an operator created from scratch,
as well as using the pylops.Spread
operator.
from math import floor
from time import time
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.axes_grid1 import ImageGrid, make_axes_locatable
from numba import jit, prange
from scipy.interpolate import griddata
from scipy.ndimage import gaussian_filter
from pylops import LinearOperator, Spread
from pylops.utils import dottest
from pylops.utils.decorators import reshaped
from pylops.utils.seismicevents import hyperbolic2d, makeaxis
from pylops.utils.wavelets import ricker
def create_colorbar(im, ax):
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.1)
cb = fig.colorbar(im, cax=cax, orientation="vertical")
return cax, cb
Given a common-shot or common-midpoint (CMP) record, the objective of NMO correction is to “flatten” events, that is, align events at later offsets to that of the zero offset. NMO has long been a staple of seismic data processing, used even today for initial velocity analysis and QC purposes. In addition, it can be the domain of choice for many useful processing steps, such as angle muting.
To get started, let us create a 2D seismic dataset containing some hyperbolic events representing reflections from flat reflectors. Events are created with a true RMS velocity, which we will be using as if we picked them from, for example, a semblance panel.
par = dict(ox=0, dx=40, nx=80, ot=0, dt=0.004, nt=520)
t, _, x, _ = makeaxis(par)
t0s_true = np.array([0.5, 1.22, 1.65])
vrms_true = np.array([2000.0, 2400.0, 2500.0])
amps = np.array([1, 0.2, 0.5])
freq = 10 # Hz
wav, *_ = ricker(t[:41], f0=freq)
_, data = hyperbolic2d(x, t, t0s_true, vrms_true, amp=amps, wav=wav)
# NMO correction plot
pclip = 0.5
dmax = np.max(np.abs(data))
opts = dict(
cmap="gray_r",
extent=[x[0], x[-1], t[-1], t[0]],
aspect="auto",
vmin=-pclip * dmax,
vmax=pclip * dmax,
)
# Offset-dependent traveltime of the first hyperbolic event
t_nmo_ev1 = np.sqrt(t0s_true[0] ** 2 + (x / vrms_true[0]) ** 2)
fig, ax = plt.subplots(figsize=(4, 5))
vmax = np.max(np.abs(data))
im = ax.imshow(data.T, **opts)
ax.plot(x, t_nmo_ev1, "C1--", label="Hyperbolic moveout")
ax.plot(x, t0s_true[0] + x * 0, "C1", label="NMO-corrected")
idx = 3 * par["nx"] // 4
ax.annotate(
"",
xy=(x[idx], t0s_true[0]),
xycoords="data",
xytext=(x[idx], t_nmo_ev1[idx]),
textcoords="data",
fontsize=7,
arrowprops=dict(edgecolor="w", arrowstyle="->", shrinkA=10),
)
ax.set(title="Data", xlabel="Offset [m]", ylabel="Time [s]")
cax, _ = create_colorbar(im, ax)
cax.set_ylabel("Amplitude")
ax.legend()
fig.tight_layout()

NMO correction consists of applying an offset- and time-dependent shift to each sample of the trace in such a way that all events corresponding to the same reflection will be located at the same time intercept after correction.
An arbitrary hyperbolic event at position \((t, h)\) is linked to its zero-offset traveltime \(t_0\) by the following equation
Our strategy in applying the correction is to loop over our time axis (which we will associate to \(t_0\)) and respective RMS velocities and, for each offset, move the sample at \(t(x)\) to location \(t_0(x) \equiv t_0\). In the figure above, we are considering a single \(t_0 = 0.5\mathrm{s}\) which would have values along the dotted curve (i.e., \(t(x)\)) moved to \(t_0\) for every offset.
Notice that we need NMO velocities for each sample of our time axis.
In this example, we actually only have 3 samples, when we need nt
samples.
In practice, we would have many more samples, but probably not one for each
nt
. To resolve this issue, we will interpolate these 3 samples to all samples
of our time axis (or, more accurately, their slownesses to preserve traveltimes).
def interpolate_vrms(t0_picks, vrms_picks, taxis, smooth=None):
assert len(t0_picks) == len(vrms_picks)
# Sampled points in time axis
points = np.zeros((len(t0_picks) + 2,))
points[0] = taxis[0]
points[-1] = taxis[-1]
points[1:-1] = t0_picks
# Sampled values of slowness (in s/km)
values = np.zeros((len(vrms_picks) + 2,))
values[0] = 1000.0 / vrms_picks[0] # Use first slowness before t0_picks[0]
values[-1] = 1000.0 / vrms_picks[-1] # Use the last slowness after t0_picks[-1]
values[1:-1] = 1000.0 / vrms_picks
slowness = griddata(points, values, taxis, method="linear")
if smooth is not None:
slowness = gaussian_filter(slowness, sigma=smooth)
return 1000.0 / slowness
vel_t = interpolate_vrms(t0s_true, vrms_true, t, smooth=11)
# Plot interpolated RMS velocities which will be used for NMO
fig, ax = plt.subplots(figsize=(4, 5))
ax.plot(vel_t, t, "k", lw=3, label="Interpolated", zorder=-1)
ax.plot(vrms_true, t0s_true, "C1o", markersize=10, label="Picks")
ax.invert_yaxis()
ax.set(xlabel="RMS Velocity [m/s]", ylabel="Time [s]", ylim=[t[-1], t[0]])
ax.legend()
fig.tight_layout()

NMO from scratch¶
We are very close to building our NMO correction, we just need to take care of
one final issue. When moving the sample from \(t(x)\) to \(t_0\), we
know that, by definition, \(t_0\) is always on our time axis grid. In contrast,
\(t(x)\) may not fall exactly on a multiple of dt
(our time axis
sampling). Suppose its nearest sample smaller than itself (floor) is i
.
Instead of moving only sample i, we will be moving samples both samples
i
and i+1
with an appropriate weight to account for how far
\(t(x)\) is from i*dt
and (i+1)*dt
.
@jit(nopython=True, fastmath=True, nogil=True, parallel=True)
def nmo_forward(data, taxis, haxis, vels_rms):
dt = taxis[1] - taxis[0]
ot = taxis[0]
nt = len(taxis)
nh = len(haxis)
dnmo = np.zeros_like(data)
# Parallel outer loop on slow axis
for ih in prange(nh):
h = haxis[ih]
for it0, (t0, vrms) in enumerate(zip(taxis, vels_rms)):
# Compute NMO traveltime
tx = np.sqrt(t0**2 + (h / vrms) ** 2)
it_frac = (tx - ot) / dt # Fractional index
it_floor = floor(it_frac)
it_ceil = it_floor + 1
w = it_frac - it_floor
if 0 <= it_floor and it_ceil < nt: # it_floor and it_ceil must be valid
# Linear interpolation
dnmo[ih, it0] += (1 - w) * data[ih, it_floor] + w * data[ih, it_ceil]
return dnmo
dnmo = nmo_forward(data, t, x, vel_t) # Compile and run
# Time execution
start = time()
nmo_forward(data, t, x, vel_t)
end = time()
print(f"Ran in {1e6*(end-start):.0f} μs")
Ran in 325 μs
# Plot Data and NMO-corrected data
fig = plt.figure(figsize=(6.5, 5))
grid = ImageGrid(
fig,
111,
nrows_ncols=(1, 2),
axes_pad=0.15,
cbar_location="right",
cbar_mode="single",
cbar_size="7%",
cbar_pad=0.15,
aspect=False,
share_all=True,
)
im = grid[0].imshow(data.T, **opts)
grid[0].set(title="Data", xlabel="Offset [m]", ylabel="Time [s]")
grid[0].cax.colorbar(im)
grid[0].cax.set_ylabel("Amplitude")
grid[1].imshow(dnmo.T, **opts)
grid[1].set(title="NMO-corrected Data", xlabel="Offset [m]")
plt.show()

Now that we know how to compute the forward, we’ll compute the adjoint pass.
With these two functions, we can create a LinearOperator
and ensure that
it passes the dot-test.
@jit(nopython=True, fastmath=True, nogil=True, parallel=True)
def nmo_adjoint(dnmo, taxis, haxis, vels_rms):
dt = taxis[1] - taxis[0]
ot = taxis[0]
nt = len(taxis)
nh = len(haxis)
data = np.zeros_like(dnmo)
# Parallel outer loop on slow axis; use range if Numba is not installed
for ih in prange(nh):
h = haxis[ih]
for it0, (t0, vrms) in enumerate(zip(taxis, vels_rms)):
# Compute NMO traveltime
tx = np.sqrt(t0**2 + (h / vrms) ** 2)
it_frac = (tx - ot) / dt # Fractional index
it_floor = floor(it_frac)
it_ceil = it_floor + 1
w = it_frac - it_floor
if 0 <= it_floor and it_ceil < nt:
# Linear interpolation
# In the adjoint, we must spread the same it0 to both it_floor and
# it_ceil, since in the forward pass, both of these samples were
# pushed onto it0
data[ih, it_floor] += (1 - w) * dnmo[ih, it0]
data[ih, it_ceil] += w * dnmo[ih, it0]
return data
Finally, we can create our linear operator. To exemplify the
class-based interface we will subclass pylops.LinearOperator
and
implement the required methods: _matvec
which will compute the forward and
_rmatvec
which will compute the adjoint. Note the use of the reshaped
decorator which allows us to pass x
directly into our auxiliary function
without having to do x.reshape(self.dims)
and to output without having to
call ravel()
.
class NMO(LinearOperator):
def __init__(self, taxis, haxis, vels_rms, dtype=None):
self.taxis = taxis
self.haxis = haxis
self.vels_rms = vels_rms
dims = (len(haxis), len(taxis))
if dtype is None:
dtype = np.result_type(taxis.dtype, haxis.dtype, vels_rms.dtype)
super().__init__(dims=dims, dimsd=dims, dtype=dtype)
@reshaped
def _matvec(self, x):
return nmo_forward(x, self.taxis, self.haxis, self.vels_rms)
@reshaped
def _rmatvec(self, y):
return nmo_adjoint(y, self.taxis, self.haxis, self.vels_rms)
With our new NMO
linear operator, we can instantiate it with our current
example and ensure that it passes the dot test which proves that our forward
and adjoint transforms truly are adjoints of each other.
NMOOp = NMO(t, x, vel_t)
dottest(NMOOp, rtol=1e-4, verb=True)
Dot test passed, v^H(Opu)=-172.4466288315369 - u^H(Op^Hv)=-172.44662883153708
True
NMO using pylops.Spread
¶
We learned how to implement an NMO correction and its adjoint from scratch. The adjoint has an interesting pattern, where energy taken from one domain is “spread” along a previously-defined parametric curve (the NMO hyperbola in this case). This pattern is very common in many algorithms, including Radon transform, Kirchhoff migration (also known as Total Focusing Method in ultrasonics) and many others.
For these classes of operators, PyLops offers a pylops.Spread
constructor, which we will leverage to implement a version of the NMO correction.
The pylops.Spread
operator will take a value in the “input” domain,
and spread it along a parametric curve, defined in the “output” domain.
In our case, the spreading operation is the adjoint of the NMO, so our
“input” domain is the NMO domain, and the “output” domain is the original
data domain.
In order to use pylops.Spread
, we need to define the
parametric curves. This can be done through the use of a table with shape
\((n_{x_i}, n_{t}, n_{x_o})\), where \(n_{x_i}\) and \(n_{t}\)
represent the 2d dimensions of the “input” domain (NMO domain) and \(n_{x_o}\)
and \(n_{t}\) the 2d dimensions of the “output” domain. In our NMO case,
\(n_{x_i} = n_{x_o} = n_h\) represents the number of offsets.
Following the documentation of pylops.Spread
, the table will be
used in the following manner:
d_out[ix_o, table[ix_i, it, ix_o]] += d_in[ix_i, it]
In our case, ix_o = ix_i = ih
, and comparing with our NMO adjoint, it
refers to \(t_0\) while table[ix, it, ix]
should then provide the
appropriate index for \(t(x)\). In our implementation we will also be
constructing a second table containing the weights to be used for linear
interpolation.
def create_tables(taxis, haxis, vels_rms):
dt = taxis[1] - taxis[0]
ot = taxis[0]
nt = len(taxis)
nh = len(haxis)
# NaN values will be not be spread.
# Using np.zeros has the same result but much slower.
table = np.full((nh, nt, nh), fill_value=np.nan)
dtable = np.full((nh, nt, nh), fill_value=np.nan)
for ih, h in enumerate(haxis):
for it0, (t0, vrms) in enumerate(zip(taxis, vels_rms)):
# Compute NMO traveltime
tx = np.sqrt(t0**2 + (h / vrms) ** 2)
it_frac = (tx - ot) / dt
it_floor = floor(it_frac)
w = it_frac - it_floor
# Both it_floor and it_floor + 1 must be valid indices for taxis
# when using two tables (interpolation).
if 0 <= it_floor and it_floor + 1 < nt:
table[ih, it0, ih] = it_floor
dtable[ih, it0, ih] = w
return table, dtable
nmo_table, nmo_dtable = create_tables(t, x, vel_t)
SpreadNMO = Spread(
dims=data.shape, # "Input" shape: NMO-ed data shape
dimsd=data.shape, # "Output" shape: original data shape
table=nmo_table, # Table of time indices
dtable=nmo_dtable, # Table of weights for linear interpolation
engine="numba", # numba or numpy
).H # To perform NMO *correction*, we need the adjoint
dottest(SpreadNMO, rtol=1e-4)
True
We see it passes the dot test, but are the results right? Let’s find out.
dnmo_spr = SpreadNMO @ data
start = time()
SpreadNMO @ data
end = time()
print(f"Ran in {1e6*(end-start):.0f} μs")
Ran in 14426 μs
Note that since v2.0, we do not need to pass a flattened array. Consequently,
the output will not be flattened, but will have SpreadNMO.dimsd
as shape.
# Plot Data and NMO-corrected data
fig = plt.figure(figsize=(6.5, 5))
grid = ImageGrid(
fig,
111,
nrows_ncols=(1, 2),
axes_pad=0.15,
cbar_location="right",
cbar_mode="single",
cbar_size="7%",
cbar_pad=0.15,
aspect=False,
share_all=True,
)
im = grid[0].imshow(data.T, **opts)
grid[0].set(title="Data", xlabel="Offset [m]", ylabel="Time [s]")
grid[0].cax.colorbar(im)
grid[0].cax.set_ylabel("Amplitude")
grid[1].imshow(dnmo_spr.T, **opts)
grid[1].set(title="NMO correction using Spread", xlabel="Offset [m]")
plt.show()

Not as blazing fast as out original implementation, but pretty good (try the
“numpy” backend for comparison!). In fact, using the Spread
operator for
NMO will always have a speed disadvantage. While iterating over the table, it must
loop over the offsets twice: one for the “input” offsets and one for the “output”
offsets. We know they are the same for NMO, but since Spread
is a generic
operator, it does not know that. So right off the bat we can expect an 80x
slowdown (nh = 80). We diminished this cost to about 30x by setting values where
ix_i != ix_o
to NaN, but nothing beats the custom implementation. Despite this,
we can still produce the same result to numerical accuracy:
np.allclose(dnmo, dnmo_spr)
True
Total running time of the script: ( 0 minutes 3.968 seconds)
Note
Click here to download the full example code
Operators concatenation¶
This example shows how to use ‘stacking’ operators such as
pylops.VStack
, pylops.HStack
,
pylops.Block
, pylops.BlockDiag
,
and pylops.Kronecker
.
These operators allow for different combinations of multiple linear operators in a single operator. Such functionalities are used within PyLops as the basis for the creation of complex operators as well as in the definition of various types of optimization problems with regularization or preconditioning.
Some of this operators naturally lend to embarassingly parallel computations. Within PyLops we leverage the multiprocessing module to run multiple processes at the same time evaluating a subset of the operators involved in one of the stacking operations.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s start by defining two second derivatives pylops.SecondDerivative
that we will be using in this example.
D2hop = pylops.SecondDerivative(dims=(11, 21), axis=1, dtype="float32")
D2vop = pylops.SecondDerivative(dims=(11, 21), axis=0, dtype="float32")
Chaining of operators represents the simplest concatenation that
can be performed between two or more linear operators.
This can be easily achieved using the *
symbol
\[\mathbf{D_{cat}}= \mathbf{D_v} \mathbf{D_h}\]
Nv, Nh = 11, 21
X = np.zeros((Nv, Nh))
X[int(Nv / 2), int(Nh / 2)] = 1
D2op = D2vop * D2hop
Y = D2op * X
fig, axs = plt.subplots(1, 2, figsize=(10, 3))
fig.suptitle("Chain", fontsize=14, fontweight="bold", y=0.95)
im = axs[0].imshow(X, interpolation="nearest")
axs[0].axis("tight")
axs[0].set_title(r"$x$")
plt.colorbar(im, ax=axs[0])
im = axs[1].imshow(Y, interpolation="nearest")
axs[1].axis("tight")
axs[1].set_title(r"$y=(D_x+D_y) x$")
plt.colorbar(im, ax=axs[1])
plt.tight_layout()
plt.subplots_adjust(top=0.8)

We now want to vertically stack three operators
\[\begin{split}\mathbf{D_{Vstack}} = \begin{bmatrix} \mathbf{D_v} \\ \mathbf{D_h} \end{bmatrix}, \qquad \mathbf{y} = \begin{bmatrix} \mathbf{D_v}\mathbf{x} \\ \mathbf{D_h}\mathbf{x} \end{bmatrix}\end{split}\]
Nv, Nh = 11, 21
X = np.zeros((Nv, Nh))
X[int(Nv / 2), int(Nh / 2)] = 1
Dstack = pylops.VStack([D2vop, D2hop])
Y = np.reshape(Dstack * X.ravel(), (Nv * 2, Nh))
fig, axs = plt.subplots(1, 2, figsize=(10, 3))
fig.suptitle("Vertical stacking", fontsize=14, fontweight="bold", y=0.95)
im = axs[0].imshow(X, interpolation="nearest")
axs[0].axis("tight")
axs[0].set_title(r"$x$")
plt.colorbar(im, ax=axs[0])
im = axs[1].imshow(Y, interpolation="nearest")
axs[1].axis("tight")
axs[1].set_title(r"$y$")
plt.colorbar(im, ax=axs[1])
plt.tight_layout()
plt.subplots_adjust(top=0.8)

Similarly we can now horizontally stack three operators
\[\mathbf{D_{Hstack}} = \begin{bmatrix} \mathbf{D_v} & 0.5*\mathbf{D_v} & -1*\mathbf{D_h} \end{bmatrix}, \qquad \mathbf{y} = \mathbf{D_v}\mathbf{x}_1 + 0.5*\mathbf{D_v}\mathbf{x}_2 - \mathbf{D_h}\mathbf{x}_3\]
Nv, Nh = 11, 21
X = np.zeros((Nv * 3, Nh))
X[int(Nv / 2), int(Nh / 2)] = 1
X[int(Nv / 2) + Nv, int(Nh / 2)] = 1
X[int(Nv / 2) + 2 * Nv, int(Nh / 2)] = 1
Hstackop = pylops.HStack([D2vop, 0.5 * D2vop, -1 * D2hop])
Y = np.reshape(Hstackop * X.ravel(), (Nv, Nh))
fig, axs = plt.subplots(1, 2, figsize=(10, 3))
fig.suptitle("Horizontal stacking", fontsize=14, fontweight="bold", y=0.95)
im = axs[0].imshow(X, interpolation="nearest")
axs[0].axis("tight")
axs[0].set_title(r"$x$")
plt.colorbar(im, ax=axs[0])
im = axs[1].imshow(Y, interpolation="nearest")
axs[1].axis("tight")
axs[1].set_title(r"$y$")
plt.colorbar(im, ax=axs[1])
plt.tight_layout()
plt.subplots_adjust(top=0.8)

We can even stack them both horizontally and vertically such that we create a block operator
\[\begin{split}\mathbf{D_{Block}} = \begin{bmatrix} \mathbf{D_v} & 0.5*\mathbf{D_v} & -1*\mathbf{D_h} \\ \mathbf{D_h} & 2*\mathbf{D_h} & \mathbf{D_v} \\ \end{bmatrix}, \qquad \mathbf{y} = \begin{bmatrix} \mathbf{D_v} \mathbf{x_1} + 0.5*\mathbf{D_v} \mathbf{x_2} - \mathbf{D_h} \mathbf{x_3} \\ \mathbf{D_h} \mathbf{x_1} + 2*\mathbf{D_h} \mathbf{x_2} + \mathbf{D_v} \mathbf{x_3} \end{bmatrix}\end{split}\]
Bop = pylops.Block([[D2vop, 0.5 * D2vop, -1 * D2hop], [D2hop, 2 * D2hop, D2vop]])
Y = np.reshape(Bop * X.ravel(), (2 * Nv, Nh))
fig, axs = plt.subplots(1, 2, figsize=(10, 3))
fig.suptitle("Block", fontsize=14, fontweight="bold", y=0.95)
im = axs[0].imshow(X, interpolation="nearest")
axs[0].axis("tight")
axs[0].set_title(r"$x$")
plt.colorbar(im, ax=axs[0])
im = axs[1].imshow(Y, interpolation="nearest")
axs[1].axis("tight")
axs[1].set_title(r"$y$")
plt.colorbar(im, ax=axs[1])
plt.tight_layout()
plt.subplots_adjust(top=0.8)

Finally we can use the block-diagonal operator to apply three operators to three different subset of the model and data
\[\begin{split}\mathbf{D_{BDiag}} = \begin{bmatrix} \mathbf{D_v} & \mathbf{0} & \mathbf{0} \\ \mathbf{0} & 0.5*\mathbf{D_v} & \mathbf{0} \\ \mathbf{0} & \mathbf{0} & -\mathbf{D_h} \end{bmatrix}, \qquad \mathbf{y} = \begin{bmatrix} \mathbf{D_v} \mathbf{x_1} \\ 0.5*\mathbf{D_v} \mathbf{x_2} \\ -\mathbf{D_h} \mathbf{x_3} \end{bmatrix}\end{split}\]
BD = pylops.BlockDiag([D2vop, 0.5 * D2vop, -1 * D2hop])
Y = np.reshape(BD * X.ravel(), (3 * Nv, Nh))
fig, axs = plt.subplots(1, 2, figsize=(10, 3))
fig.suptitle("Block-diagonal", fontsize=14, fontweight="bold", y=0.95)
im = axs[0].imshow(X, interpolation="nearest")
axs[0].axis("tight")
axs[0].set_title(r"$x$")
plt.colorbar(im, ax=axs[0])
im = axs[1].imshow(Y, interpolation="nearest")
axs[1].axis("tight")
axs[1].set_title(r"$y$")
plt.colorbar(im, ax=axs[1])
plt.tight_layout()
plt.subplots_adjust(top=0.8)

If we consider now the case of having a large number of operators inside a
blockdiagonal structure, it may be convenient to span multiple processes
handling subset of operators at the same time. This can be easily achieved
by simply defining the number of processes we want to use via nproc
.
X = np.zeros((Nv * 10, Nh))
for iv in range(10):
X[int(Nv / 2) + iv * Nv, int(Nh / 2)] = 1
BD = pylops.BlockDiag([D2vop] * 10, nproc=2)
print("BD Operator multiprocessing pool", BD.pool)
Y = np.reshape(BD * X.ravel(), (10 * Nv, Nh))
BD.pool.close()
fig, axs = plt.subplots(1, 2, figsize=(10, 3))
fig.suptitle("Block-diagonal", fontsize=14, fontweight="bold", y=0.95)
im = axs[0].imshow(X, interpolation="nearest")
axs[0].axis("tight")
axs[0].set_title(r"$x$")
plt.colorbar(im, ax=axs[0])
im = axs[1].imshow(Y, interpolation="nearest")
axs[1].axis("tight")
axs[1].set_title(r"$y$")
plt.colorbar(im, ax=axs[1])
plt.tight_layout()
plt.subplots_adjust(top=0.8)

BD Operator multiprocessing pool <multiprocessing.pool.Pool state=RUN pool_size=2>
Finally we use the Kronecker operator and replicate this example on wiki.
\[\begin{split}\begin{bmatrix} 1 & 2 \\ 3 & 4 \\ \end{bmatrix} \otimes \begin{bmatrix} 0 & 5 \\ 6 & 7 \\ \end{bmatrix} = \begin{bmatrix} 0 & 5 & 0 & 10 \\ 6 & 7 & 12 & 14 \\ 0 & 15 & 0 & 20 \\ 18 & 21 & 24 & 28 \\ \end{bmatrix}\end{split}\]
A = np.array([[1, 2], [3, 4]])
B = np.array([[0, 5], [6, 7]])
AB = np.kron(A, B)
n1, m1 = A.shape
n2, m2 = B.shape
Aop = pylops.MatrixMult(A)
Bop = pylops.MatrixMult(B)
ABop = pylops.Kronecker(Aop, Bop)
x = np.ones(m1 * m2)
y = AB.dot(x)
yop = ABop * x
xinv = ABop / yop
print(f"AB = \n {AB}")
print(f"x = {x}")
print(f"y = {y}")
print(f"yop = {yop}")
print(f"xinv = {xinv}")
AB =
[[ 0 5 0 10]
[ 6 7 12 14]
[ 0 15 0 20]
[18 21 24 28]]
x = [1. 1. 1. 1.]
y = [15. 39. 35. 91.]
yop = [15. 39. 35. 91.]
xinv = [1. 1. 1. 1.]
We can also use pylops.Kronecker
to do something more
interesting. Any operator can in fact be applied on a single direction of a
multi-dimensional input array if combined with an pylops.Identity
operator via Kronecker product. We apply here the
pylops.FirstDerivative
to the second dimension of the model.
Note that for those operators whose implementation allows their application
to a single axis via the axis
parameter, using the Kronecker product
would lead to slower performance. Nevertheless, the Kronecker product allows
any other operator to be applied to a single dimension.
Nv, Nh = 11, 21
Iop = pylops.Identity(Nv, dtype="float32")
D2hop = pylops.FirstDerivative(Nh, dtype="float32")
X = np.zeros((Nv, Nh))
X[Nv // 2, Nh // 2] = 1
D2hop = pylops.Kronecker(Iop, D2hop)
Y = D2hop * X.ravel()
Y = Y.reshape(Nv, Nh)
fig, axs = plt.subplots(1, 2, figsize=(10, 3))
fig.suptitle("Kronecker", fontsize=14, fontweight="bold", y=0.95)
im = axs[0].imshow(X, interpolation="nearest")
axs[0].axis("tight")
axs[0].set_title(r"$x$")
plt.colorbar(im, ax=axs[0])
im = axs[1].imshow(Y, interpolation="nearest")
axs[1].axis("tight")
axs[1].set_title(r"$y$")
plt.colorbar(im, ax=axs[1])
plt.tight_layout()
plt.subplots_adjust(top=0.8)

Total running time of the script: ( 0 minutes 2.387 seconds)
Note
Click here to download the full example code
Operators with Multiprocessing¶
This example shows how perform a scalability test for one of PyLops operators
that uses multiprocessing
to spawn multiple processes. Operators that
support such feature are pylops.basicoperators.VStack
,
pylops.basicoperators.HStack
, and
pylops.basicoperators.BlockDiagonal
, and
pylops.basicoperators.Block
.
In this example we will consider the BlockDiagonal operator which contains
pylops.basicoperators.MatrixMult
operators along its main diagonal.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s start by creating N MatrixMult operators and the BlockDiag operator
N = 100
Nops = 32
Ops = [pylops.MatrixMult(np.random.normal(0.0, 1.0, (N, N))) for _ in range(Nops)]
Op = pylops.BlockDiag(Ops, nproc=1)
We can now perform a scalability test on the forward operation
workers = [2, 3, 4]
compute_times, speedup = pylops.utils.multiproc.scalability_test(
Op, np.ones(Op.shape[1]), workers=workers, forward=True
)
plt.figure(figsize=(12, 3))
plt.plot(workers, speedup, "ko-")
plt.xlabel("# Workers")
plt.ylabel("Speed Up")
plt.title("Forward scalability test")
plt.tight_layout()

Working with 2 workers...
Working with 3 workers...
Working with 4 workers...
And likewise on the adjoint operation
compute_times, speedup = pylops.utils.multiproc.scalability_test(
Op, np.ones(Op.shape[0]), workers=workers, forward=False
)
plt.figure(figsize=(12, 3))
plt.plot(workers, speedup, "ko-")
plt.xlabel("# Workers")
plt.ylabel("Speed Up")
plt.title("Adjoint scalability test")
plt.tight_layout()

Working with 2 workers...
Working with 3 workers...
Working with 4 workers...
Note that we have not tested here the case with 1 worker. In this specific case, since the computations are very small, the overhead of spawning processes is actually dominating the time of computations and so computing the forward and adjoint operations with a single worker is more efficient. We hope that this example can serve as a basis to inspect the scalability of multiprocessing-enabled operators and choose the best number of processes.
Total running time of the script: ( 0 minutes 0.873 seconds)
Note
Click here to download the full example code
Padding¶
This example shows how to use the pylops.Pad
operator to zero-pad a
model
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s define a pad operator Pop
for one dimensional data
dims = 10
pad = (2, 3)
Pop = pylops.Pad(dims, pad)
x = np.arange(dims) + 1.0
y = Pop * x
xadj = Pop.H * y
print(f"x = {x}")
print(f"P*x = {y}")
print(f"P'*y = {xadj}")
x = [ 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.]
P*x = [ 0. 0. 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 0. 0. 0.]
P'*y = [ 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.]
We move now to a multi-dimensional case. We pad the input model with different extents along both dimensions
dims = (5, 4)
pad = ((1, 0), (3, 4))
Pop = pylops.Pad(dims, pad)
x = (np.arange(np.prod(np.array(dims))) + 1.0).reshape(dims)
y = Pop * x
xadj = Pop.H * y
fig, axs = plt.subplots(1, 3, figsize=(10, 4))
fig.suptitle("Pad for 2d data", fontsize=14, fontweight="bold", y=1.15)
axs[0].imshow(x, cmap="rainbow", vmin=0, vmax=np.prod(np.array(dims)) + 1)
axs[0].set_title(r"$x$")
axs[0].axis("tight")
axs[1].imshow(y, cmap="rainbow", vmin=0, vmax=np.prod(np.array(dims)) + 1)
axs[1].set_title(r"$y = P x$")
axs[1].axis("tight")
axs[2].imshow(xadj, cmap="rainbow", vmin=0, vmax=np.prod(np.array(dims)) + 1)
axs[2].set_title(r"$x_{adj} = P^{H} y$")
axs[2].axis("tight")
plt.tight_layout()

Total running time of the script: ( 0 minutes 0.309 seconds)
Note
Click here to download the full example code
Patching¶
This example shows how to use the pylops.signalprocessing.Patch2D
and pylops.signalprocessing.Patch3D
operators to perform repeated
transforms over small patches of a 2-dimensional or 3-dimensional
array. The transforms that we apply in this example are the
pylops.signalprocessing.FFT2D
and
pylops.signalprocessing.FFT3D
but this operator has been
designed to allow a variety of transforms as long as they operate with signals
that are 2- or 3-dimensional in nature, respectively.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s start by creating an 2-dimensional array of size \(n_x \times n_t\) composed of 3 parabolic events
par = {"ox": -140, "dx": 2, "nx": 140, "ot": 0, "dt": 0.004, "nt": 200, "f0": 20}
v = 1500
t0 = [0.2, 0.4, 0.5]
px = [0, 0, 0]
pxx = [1e-5, 5e-6, 1e-20]
amp = [1.0, -2, 0.5]
# Create axis
t, t2, x, y = pylops.utils.seismicevents.makeaxis(par)
# Create wavelet
wav = pylops.utils.wavelets.ricker(t[:41], f0=par["f0"])[0]
# Generate model
_, data = pylops.utils.seismicevents.parabolic2d(x, t, t0, px, pxx, amp, wav)
We want to divide this 2-dimensional data into small overlapping
patches in the spatial direction and apply the adjoint of the
pylops.signalprocessing.FFT2D
operator to each patch. This is
done by simply using the adjoint of the
pylops.signalprocessing.Patch2D
operator. Note that for non-
orthogonal operators, this must be replaced by an inverse.
nwin = (20, 34) # window size in data domain
nop = (
128,
128 // 2 + 1,
) # window size in model domain; we use real FFT, second axis is half
nover = (10, 4) # overlap between windows
dimsd = data.shape
# Sliding window transform without taper
Op = pylops.signalprocessing.FFT2D(nwin, nffts=(128, 128), real=True)
nwins, dims, mwin_inends, dwin_inends = pylops.signalprocessing.patch2d_design(
dimsd, nwin, nover, (128, 65)
)
Patch = pylops.signalprocessing.Patch2D(
Op.H, dims, dimsd, nwin, nover, nop, tapertype=None
)
fftdata = Patch.H * data
We now create a similar operator but we also add a taper to the overlapping parts of the patches. We then apply the forward to restore the original signal.
Patch = pylops.signalprocessing.Patch2D(
Op.H, dims, dimsd, nwin, nover, nop, tapertype="hanning"
)
reconstructed_data = Patch * fftdata
Finally we re-arrange the transformed patches so that we can also display them
fftdatareshaped = np.zeros((nop[0] * nwins[0], nop[1] * nwins[1]), dtype=fftdata.dtype)
iwin = 1
for ix in range(nwins[0]):
for it in range(nwins[1]):
fftdatareshaped[
ix * nop[0] : (ix + 1) * nop[0], it * nop[1] : (it + 1) * nop[1]
] = np.fft.fftshift(fftdata[ix, it])
iwin += 1
Let’s finally visualize all the intermediate results as well as our final
data reconstruction after inverting the
pylops.signalprocessing.Sliding2D
operator.
fig, axs = plt.subplots(1, 3, figsize=(12, 5))
im = axs[0].imshow(data.T, cmap="gray")
axs[0].set_title("Original data")
plt.colorbar(im, ax=axs[0])
axs[0].axis("tight")
im = axs[1].imshow(reconstructed_data.real.T, cmap="gray")
axs[1].set_title("Reconstruction from adjoint")
plt.colorbar(im, ax=axs[1])
axs[1].axis("tight")
axs[2].imshow(np.abs(fftdatareshaped).T, cmap="jet")
axs[2].set_title("FFT data")
axs[2].axis("tight")
plt.tight_layout()

We repeat now the same exercise in 3d
par = {
"oy": -60,
"dy": 2,
"ny": 60,
"ox": -50,
"dx": 2,
"nx": 50,
"ot": 0,
"dt": 0.004,
"nt": 100,
"f0": 20,
}
v = 1500
t0 = [0.05, 0.2, 0.3]
vrms = [500, 700, 1700]
amp = [1.0, -2, 0.5]
# Create axis
t, t2, x, y = pylops.utils.seismicevents.makeaxis(par)
# Create wavelet
wav = pylops.utils.wavelets.ricker(t[:41], f0=par["f0"])[0]
# Generate model
_, data = pylops.utils.seismicevents.hyperbolic3d(x, y, t, t0, vrms, vrms, amp, wav)
fig, axs = plt.subplots(1, 3, figsize=(12, 5))
fig.suptitle("Original data", fontsize=12, fontweight="bold", y=0.95)
axs[0].imshow(
data[par["ny"] // 2].T,
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[0].set_xlabel(r"$x(m)$")
axs[0].set_ylabel(r"$t(s)$")
axs[1].imshow(
data[:, par["nx"] // 2].T,
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(y.min(), y.max(), t.max(), t.min()),
)
axs[1].set_xlabel(r"$y(m)$")
axs[1].set_ylabel(r"$t(s)$")
axs[2].imshow(
data[:, :, par["nt"] // 2],
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(x.min(), x.max(), y.max(), x.min()),
)
axs[2].set_xlabel(r"$x(m)$")
axs[2].set_ylabel(r"$y(m)$")
plt.tight_layout()

Let’s create now the pylops.signalprocessing.Patch3D
operator
applying the adjoint of the pylops.signalprocessing.FFT3D
operator to each patch.
nwin = (20, 20, 34) # window size in data domain
nop = (
128,
128,
128 // 2 + 1,
) # window size in model domain; we use real FFT, third axis is half
nover = (10, 10, 4) # overlap between windows
dimsd = data.shape
# Sliding window transform without taper
Op = pylops.signalprocessing.FFTND(nwin, nffts=(128, 128, 128), real=True)
nwins, dims, mwin_inends, dwin_inends = pylops.signalprocessing.patch3d_design(
dimsd, nwin, nover, (128, 128, 65)
)
Patch = pylops.signalprocessing.Patch3D(
Op.H, dims, dimsd, nwin, nover, nop, tapertype=None
)
fftdata = Patch.H * data
Patch = pylops.signalprocessing.Patch3D(
Op.H, dims, dimsd, nwin, nover, nop, tapertype="hanning"
)
reconstructed_data = np.real(Patch * fftdata)
fig, axs = plt.subplots(1, 3, figsize=(12, 5))
fig.suptitle("Reconstructed data", fontsize=12, fontweight="bold", y=0.95)
axs[0].imshow(
reconstructed_data[par["ny"] // 2].T,
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[0].set_xlabel(r"$x(m)$")
axs[0].set_ylabel(r"$t(s)$")
axs[1].imshow(
reconstructed_data[:, par["nx"] // 2].T,
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(y.min(), y.max(), t.max(), t.min()),
)
axs[1].set_xlabel(r"$y(m)$")
axs[1].set_ylabel(r"$t(s)$")
axs[2].imshow(
reconstructed_data[:, :, par["nt"] // 2],
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(x.min(), x.max(), y.max(), x.min()),
)
axs[2].set_xlabel(r"$x(m)$")
axs[2].set_ylabel(r"$y(m)$")
plt.tight_layout()

Total running time of the script: ( 0 minutes 7.681 seconds)
Note
Click here to download the full example code
PhaseShift operator¶
This example shows how to use the pylops.waveeqprocessing.PhaseShift
operator to perform frequency-wavenumber shift of an input multi-dimensional
signal. Such a procedure is applied in a variety of disciplines including
geophysics, medical imaging and non-destructive testing.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s first create a synthetic dataset composed of a number of hyperbolas
par = {
"ox": -300,
"dx": 20,
"nx": 31,
"oy": -200,
"dy": 20,
"ny": 21,
"ot": 0,
"dt": 0.004,
"nt": 201,
"f0": 20,
"nfmax": 210,
}
# Create axis
t, t2, x, y = pylops.utils.seismicevents.makeaxis(par)
# Create wavelet
wav = pylops.utils.wavelets.ricker(np.arange(41) * par["dt"], f0=par["f0"])[0]
vrms = [900, 1300, 1800]
t0 = [0.2, 0.3, 0.6]
amp = [1.0, 0.6, -2.0]
_, m = pylops.utils.seismicevents.hyperbolic2d(x, t, t0, vrms, amp, wav)
We can now apply a taper at the edges and also pad the input to avoid artifacts during the phase shift
pad = 11
taper = pylops.utils.tapers.taper2d(par["nt"], par["nx"], 5)
mpad = np.pad(m * taper, ((pad, pad), (0, 0)), mode="constant")
We perform now forward propagation in a constant velocity \(v=2000\) for a depth of \(z_{prop} = 100 m\). We should expect the hyperbolas to move forward in time and become flatter.
vel = 1500.0
zprop = 100
freq = np.fft.rfftfreq(par["nt"], par["dt"])
kx = np.fft.fftshift(np.fft.fftfreq(par["nx"] + 2 * pad, par["dx"]))
Pop = pylops.waveeqprocessing.PhaseShift(vel, zprop, par["nt"], freq, kx)
mdown = Pop * mpad.T.ravel()
We now take the forward propagated wavefield and apply backward propagation, which is in this case simply the adjoint of our operator. We should expect the hyperbolas to move backward in time and show the same traveltime as the original dataset. Of course, as we are only performing the adjoint operation we should expect some small differences between this wavefield and the input dataset.
mup = Pop.H * mdown.ravel()
mdown = np.real(mdown.reshape(par["nt"], par["nx"] + 2 * pad)[:, pad:-pad])
mup = np.real(mup.reshape(par["nt"], par["nx"] + 2 * pad)[:, pad:-pad])
fig, axs = plt.subplots(1, 3, figsize=(10, 6), sharey=True)
fig.suptitle("2D Phase shift", fontsize=12, fontweight="bold")
axs[0].imshow(
m.T,
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[0].set_xlabel(r"$x(m)$")
axs[0].set_ylabel(r"$t(s)$")
axs[0].set_title("Original data")
axs[1].imshow(
mdown,
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[1].set_xlabel(r"$x(m)$")
axs[1].set_title("Forward propagation")
axs[2].imshow(
mup,
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[2].set_xlabel(r"$x(m)$")
axs[2].set_title("Backward propagation")
plt.tight_layout()

Finally we perform the same for a 3-dimensional signal
_, m = pylops.utils.seismicevents.hyperbolic3d(x, y, t, t0, vrms, vrms, amp, wav)
pad = 11
taper = pylops.utils.tapers.taper3d(par["nt"], (par["ny"], par["nx"]), (3, 3))
mpad = np.pad(m * taper, ((pad, pad), (pad, pad), (0, 0)), mode="constant")
kx = np.fft.fftshift(np.fft.fftfreq(par["nx"] + 2 * pad, par["dx"]))
ky = np.fft.fftshift(np.fft.fftfreq(par["ny"] + 2 * pad, par["dy"]))
Pop = pylops.waveeqprocessing.PhaseShift(vel, zprop, par["nt"], freq, kx, ky)
mdown = Pop * mpad.transpose(2, 1, 0).ravel()
mup = Pop.H * mdown.ravel()
mdown = np.real(
mdown.reshape(par["nt"], par["nx"] + 2 * pad, par["ny"] + 2 * pad)[
:, pad:-pad, pad:-pad
]
)
mup = np.real(
mup.reshape(par["nt"], par["nx"] + 2 * pad, par["ny"] + 2 * pad)[
:, pad:-pad, pad:-pad
]
)
fig, axs = plt.subplots(2, 3, figsize=(10, 12), sharey=True)
fig.suptitle("3D Phase shift", fontsize=12, fontweight="bold")
axs[0][0].imshow(
m[:, par["nx"] // 2].T,
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[0][0].set_xlabel(r"$y(m)$")
axs[0][0].set_ylabel(r"$t(s)$")
axs[0][0].set_title("Original data")
axs[0][1].imshow(
mdown[:, par["nx"] // 2],
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[0][1].set_xlabel(r"$y(m)$")
axs[0][1].set_title("Forward propagation")
axs[0][2].imshow(
mup[:, par["nx"] // 2],
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[0][2].set_xlabel(r"$y(m)$")
axs[0][2].set_title("Backward propagation")
axs[1][0].imshow(
m[par["ny"] // 2].T,
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[1][0].set_xlabel(r"$x(m)$")
axs[1][0].set_ylabel(r"$t(s)$")
axs[1][0].set_title("Original data")
axs[1][1].imshow(
mdown[:, :, par["ny"] // 2],
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[1][1].set_xlabel(r"$x(m)$")
axs[1][1].set_title("Forward propagation")
axs[1][2].imshow(
mup[:, :, par["ny"] // 2],
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[1][2].set_xlabel(r"$x(m)$")
axs[1][2].set_title("Backward propagation")
plt.tight_layout()

Total running time of the script: ( 0 minutes 1.117 seconds)
Note
Click here to download the full example code
Polynomial Regression¶
This example shows how to use the pylops.Regression
operator
to perform Polynomial regression analysis.
In short, polynomial regression is the problem of finding the best fitting coefficients for the following equation:
\[y_i = \sum_{n=0}^\text{order} x_n t_i^n \qquad \forall i=0,1,\ldots,N-1\]
As we can express this problem in a matrix form:
\[\mathbf{y}= \mathbf{A} \mathbf{x}\]
our solution can be obtained by solving the following optimization problem:
\[J= ||\mathbf{y} - \mathbf{A} \mathbf{x}||_2\]
See documentation of pylops.Regression
for more detailed
definition of the forward problem.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
np.random.seed(10)
Define the input parameters: number of samples along the t-axis (N
),
order (order
), regression coefficients (x
), and standard deviation
of noise to be added to data (sigma
).
N = 30
order = 3
x = np.array([1.0, 0.05, 0.0, -0.01])
sigma = 1
Let’s create the time axis and initialize the
pylops.Regression
operator
t = np.arange(N, dtype="float64") - N // 2
PRop = pylops.Regression(t, order=order, dtype="float64")
We can then apply the operator in forward mode to compute our data points
along the x-axis (y
). We will also generate some random gaussian noise
and create a noisy version of the data (yn
).
y = PRop * x
yn = y + np.random.normal(0, sigma, N)
We are now ready to solve our problem. As we are using an operator from the
pylops.LinearOperator
family, we can simply use /
,
which in this case will solve the system by means of an iterative solver
(i.e., scipy.sparse.linalg.lsqr
).
xest = PRop / y
xnest = PRop / yn
Let’s plot the best fitting curve for the case of noise free and noisy data
plt.figure(figsize=(5, 7))
plt.plot(
t,
PRop * x,
"k",
lw=4,
label=r"true: $x_0$ = %.2f, $x_1$ = %.2f, "
r"$x_2$ = %.2f, $x_3$ = %.2f" % (x[0], x[1], x[2], x[3]),
)
plt.plot(
t,
PRop * xest,
"--r",
lw=4,
label="est noise-free: $x_0$ = %.2f, $x_1$ = %.2f, "
r"$x_2$ = %.2f, $x_3$ = %.2f" % (xest[0], xest[1], xest[2], xest[3]),
)
plt.plot(
t,
PRop * xnest,
"--g",
lw=4,
label="est noisy: $x_0$ = %.2f, $x_1$ = %.2f, "
r"$x_2$ = %.2f, $x_3$ = %.2f" % (xnest[0], xnest[1], xnest[2], xnest[3]),
)
plt.scatter(t, y, c="r", s=70)
plt.scatter(t, yn, c="g", s=70)
plt.legend(fontsize="x-small")
plt.tight_layout()

We consider now the case where some of the observations have large errors.
Such elements are generally referred to as outliers and can affect the
quality of the least-squares solution if not treated with care. In this
example we will see how using a L1 solver such as
pylops.optimization.sparsity.IRLS
can drammatically improve the
quality of the estimation of intercept and gradient.
# Add outliers
yn[1] += 40
yn[N - 2] -= 20
# IRLS
nouter = 20
epsR = 1e-2
epsI = 0
tolIRLS = 1e-2
xnest = PRop / yn
xirls, nouter = pylops.optimization.sparsity.irls(
PRop,
yn,
nouter=nouter,
threshR=False,
epsR=epsR,
epsI=epsI,
tolIRLS=tolIRLS,
)
print(f"IRLS converged at {nouter} iterations...")
plt.figure(figsize=(5, 7))
plt.plot(
t,
PRop * x,
"k",
lw=4,
label=r"true: $x_0$ = %.2f, $x_1$ = %.2f, "
r"$x_2$ = %.2f, $x_3$ = %.2f" % (x[0], x[1], x[2], x[3]),
)
plt.plot(
t,
PRop * xnest,
"--r",
lw=4,
label=r"L2: $x_0$ = %.2f, $x_1$ = %.2f, "
r"$x_2$ = %.2f, $x_3$ = %.2f" % (xnest[0], xnest[1], xnest[2], xnest[3]),
)
plt.plot(
t,
PRop * xirls,
"--g",
lw=4,
label=r"IRLS: $x_0$ = %.2f, $x_1$ = %.2f, "
r"$x_2$ = %.2f, $x_3$ = %.2f" % (xirls[0], xirls[1], xirls[2], xirls[3]),
)
plt.scatter(t, y, c="r", s=70)
plt.scatter(t, yn, c="g", s=70)
plt.legend(fontsize="x-small")
plt.tight_layout()

IRLS converged at 6 iterations...
Total running time of the script: ( 0 minutes 0.473 seconds)
Note
Click here to download the full example code
Pre-stack modelling¶
This example shows how to create pre-stack angle gathers using
the pylops.avo.prestack.PrestackLinearModelling
operator.
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.axes_grid1.axes_divider import make_axes_locatable
from scipy.signal import filtfilt
import pylops
from pylops.utils.wavelets import ricker
plt.close("all")
np.random.seed(0)
Let’s start by creating the input elastic property profiles and wavelet
nt0 = 501
dt0 = 0.004
ntheta = 21
t0 = np.arange(nt0) * dt0
thetamin, thetamax = 0, 40
theta = np.linspace(thetamin, thetamax, ntheta)
# Elastic property profiles
vp = (
2000
+ 5 * np.arange(nt0)
+ 2 * filtfilt(np.ones(5) / 5.0, 1, np.random.normal(0, 160, nt0))
)
vs = 600 + vp / 2 + 3 * filtfilt(np.ones(5) / 5.0, 1, np.random.normal(0, 100, nt0))
rho = 1000 + vp + filtfilt(np.ones(5) / 5.0, 1, np.random.normal(0, 120, nt0))
vp[201:] += 1500
vs[201:] += 500
rho[201:] += 100
# Wavelet
ntwav = 81
wav, twav, wavc = ricker(t0[: ntwav // 2 + 1], 5)
# vs/vp profile
vsvp = 0.5
vsvp_z = vs / vp
# Model
m = np.stack((np.log(vp), np.log(vs), np.log(rho)), axis=1)
fig, axs = plt.subplots(1, 3, figsize=(9, 7), sharey=True)
axs[0].plot(vp, t0, "k", lw=3)
axs[0].set(xlabel="[m/s]", ylabel=r"$t$ [s]", ylim=[t0[0], t0[-1]], title="Vp")
axs[0].grid()
axs[1].plot(vp / vs, t0, "k", lw=3)
axs[1].set(title="Vp/Vs")
axs[1].grid()
axs[2].plot(rho, t0, "k", lw=3)
axs[2].set(xlabel="[kg/m³]", title="Rho")
axs[2].invert_yaxis()
axs[2].grid()

We create now the operators to model a synthetic pre-stack seismic gather
with a zero-phase using both a constant and a depth-variant vsvp
profile
# constant vsvp
PPop_const = pylops.avo.prestack.PrestackLinearModelling(
wav, theta, vsvp=vsvp, nt0=nt0, linearization="akirich"
)
# depth-variant vsvp
PPop_variant = pylops.avo.prestack.PrestackLinearModelling(
wav, theta, vsvp=vsvp_z, linearization="akirich"
)
Let’s apply those operators to the elastic model and create some synthetic data
dPP_const = PPop_const * m
dPP_variant = PPop_variant * m
Finally we visualize the two datasets
# sphinx_gallery_thumbnail_number = 2
fig = plt.figure(figsize=(6, 7))
ax1 = plt.subplot2grid((3, 2), (0, 0), rowspan=2)
ax2 = plt.subplot2grid((3, 2), (0, 1), rowspan=2, sharey=ax1)
ax3 = plt.subplot2grid((3, 2), (2, 0), sharex=ax1)
ax4 = plt.subplot2grid((3, 2), (2, 1), sharex=ax2)
im = ax1.imshow(
dPP_const,
cmap="bwr",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=-0.2,
vmax=0.2,
)
cax = make_axes_locatable(ax1).append_axes("bottom", size="5%", pad="3%")
cb = fig.colorbar(im, cax=cax, orientation="horizontal")
cb.ax.xaxis.set_ticks_position("bottom")
ax1.set(ylabel=r"$t$ [s]")
ax1.set_title(r"Data with constant $VP/VS$", fontsize=10)
ax1.tick_params(labelbottom=False)
ax1.axhline(t0[nt0 // 4], color="k", linestyle="--")
ax1.axhline(t0[nt0 // 2], color="k", linestyle="--")
ax1.axis("tight")
im = ax2.imshow(
dPP_variant,
cmap="bwr",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=-0.2,
vmax=0.2,
)
cax = make_axes_locatable(ax2).append_axes("bottom", size="5%", pad="3%")
cb = fig.colorbar(im, cax=cax, orientation="horizontal")
cb.ax.xaxis.set_ticks_position("bottom")
ax2.set_title(r"Data with depth-variant $VP/VS$", fontsize=10)
ax2.tick_params(labelbottom=False, labelleft=False)
ax2.axhline(t0[nt0 // 4], color="k", linestyle="--")
ax2.axhline(t0[nt0 // 2], color="k", linestyle="--")
ax2.axis("tight")
ax3.plot(theta, dPP_const[nt0 // 4], "k", lw=2)
ax3.plot(theta, dPP_variant[nt0 // 4], "--r", lw=2)
ax3.set(xlabel=r"$\theta$ [°]")
ax3.set_title("AVO curve at t=%.2f s" % t0[nt0 // 4], fontsize=10)
ax4.plot(theta, dPP_const[nt0 // 2], "k", lw=2, label=r"constant $VP/VS$")
ax4.plot(theta, dPP_variant[nt0 // 2], "--r", lw=2, label=r"variable $VP/VS$")
ax4.set(xlabel=r"$\theta$ [°]")
ax4.set_title("AVO curve at t=%.2f s" % t0[nt0 // 2], fontsize=10)
ax4.legend()
plt.tight_layout()

Total running time of the script: ( 0 minutes 0.761 seconds)
Note
Click here to download the full example code
Radon Transform¶
This example shows how to use the pylops.signalprocessing.Radon2D
and pylops.signalprocessing.Radon3D
operators to apply the Radon
Transform to 2-dimensional or 3-dimensional signals, respectively.
In our implementation both linear, parabolic and hyperbolic parametrization
can be chosen.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s start by creating an empty 2d matrix of size \(n_{p_x} \times n_t\) and add a single spike in it. We will see that applying the forward Radon operator will result in a single event (linear, parabolic or hyperbolic) in the resulting data vector.
nt, nh = 41, 51
npx, pxmax = 41, 1e-2
dt, dh = 0.005, 1
t = np.arange(nt) * dt
h = np.arange(nh) * dh
px = np.linspace(0, pxmax, npx)
x = np.zeros((npx, nt))
x[4, nt // 2] = 1
We can now define our operators for different parametric curves and apply them to the input model vector. We also apply the adjoint to the resulting data vector.
RLop = pylops.signalprocessing.Radon2D(
t, h, px, centeredh=True, kind="linear", interp=False, engine="numpy"
)
RPop = pylops.signalprocessing.Radon2D(
t, h, px, centeredh=True, kind="parabolic", interp=False, engine="numpy"
)
RHop = pylops.signalprocessing.Radon2D(
t, h, px, centeredh=True, kind="hyperbolic", interp=False, engine="numpy"
)
# forward
yL = RLop * x
yP = RPop * x
yH = RHop * x
# adjoint
xadjL = RLop.H * yL
xadjP = RPop.H * yP
xadjH = RHop.H * yH
Let’s now visualize the input model in the Radon domain, the data, and the adjoint model the different parametric curves.
fig, axs = plt.subplots(2, 4, figsize=(10, 6), sharey=True)
axs[0, 0].axis("off")
axs[1, 0].imshow(
x.T,
vmin=-1,
vmax=1,
cmap="seismic_r",
extent=(1e3 * px[0], 1e3 * px[-1], t[-1], t[0]),
)
axs[1, 0].set(xlabel=r"$p$ [s/km]", ylabel=r"$t$ [s]", title="Input model")
axs[1, 0].axis("tight")
axs[0, 1].imshow(
yL.T, vmin=-1, vmax=1, cmap="seismic_r", extent=(h[0], h[-1], t[-1], t[0])
)
axs[0, 1].tick_params(labelleft=True)
axs[0, 1].set(xlabel=r"$x$ [m]", ylabel=r"$t$ [s]", title="Linear data")
axs[0, 1].axis("tight")
axs[0, 2].imshow(
yP.T, vmin=-1, vmax=1, cmap="seismic_r", extent=(h[0], h[-1], t[-1], t[0])
)
axs[0, 2].set(xlabel=r"$x$ [m]", title="Parabolic data")
axs[0, 2].axis("tight")
axs[0, 3].imshow(
yH.T, vmin=-1, vmax=1, cmap="seismic_r", extent=(h[0], h[-1], t[-1], t[0])
)
axs[0, 3].set(xlabel=r"$x$ [m]", title="Hyperbolic data")
axs[0, 3].axis("tight")
axs[1, 1].imshow(
xadjL.T,
vmin=-20,
vmax=20,
cmap="seismic_r",
extent=(1e3 * px[0], 1e3 * px[-1], t[-1], t[0]),
)
axs[1, 1].set(xlabel=r"$p$ [s/km]", title="Linear adjoint")
axs[1, 1].axis("tight")
axs[1, 2].imshow(
xadjP.T,
vmin=-20,
vmax=20,
cmap="seismic_r",
extent=(1e3 * px[0], 1e3 * px[-1], t[-1], t[0]),
)
axs[1, 2].set(xlabel=r"$p$ [s/km]", title="Parabolic adjoint")
axs[1, 2].axis("tight")
axs[1, 3].imshow(
xadjH.T,
vmin=-20,
vmax=20,
cmap="seismic_r",
extent=(1e3 * px[0], 1e3 * px[-1], t[-1], t[0]),
)
axs[1, 3].set(xlabel=r"$p$ [s/km]", title="Hyperbolic adjoint")
axs[1, 3].axis("tight")
fig.tight_layout()

As we can see in the bottom figures, the adjoint Radon transform is far from being close to the inverse Radon transform, i.e. \(\mathbf{R^H}\mathbf{R} \neq \mathbf{I}\) (compared to the case of FFT where the adjoint and inverse are equivalent, i.e. \(\mathbf{F^H}\mathbf{F} = \mathbf{I}\)). In fact when we apply the adjoint Radon Transform we obtain a model that is a smoothed version of the original model polluted by smearing and artifacts. In tutorial 11. Radon filtering we will exploit a sparsity-promiting Radon transform to perform filtering of unwanted signals from an input data.
Finally we repeat the same exercise with 3d data.
nt, ny, nx = 21, 21, 11
npy, pymax = 13, 5e-3
npx, pxmax = 11, 5e-3
dt, dy, dx = 0.005, 1, 1
t = np.arange(nt) * dt
hy = np.arange(ny) * dy
hx = np.arange(nx) * dx
py = np.linspace(0, pymax, npy)
px = np.linspace(0, pxmax, npx)
x = np.zeros((npy, npx, nt))
x[npy // 2, npx // 2 - 2, nt // 2] = 1
RLop = pylops.signalprocessing.Radon3D(
t, hy, hx, py, px, centeredh=True, kind="linear", interp=False, engine="numpy"
)
RPop = pylops.signalprocessing.Radon3D(
t, hy, hx, py, px, centeredh=True, kind="parabolic", interp=False, engine="numpy"
)
RHop = pylops.signalprocessing.Radon3D(
t, hy, hx, py, px, centeredh=True, kind="hyperbolic", interp=False, engine="numpy"
)
# forward
yL = RLop * x.reshape(npy * npx, nt)
yP = RPop * x.reshape(npy * npx, nt)
yH = RHop * x.reshape(npy * npx, nt)
# adjoint
xadjL = RLop.H * yL
xadjP = RPop.H * yP
xadjH = RHop.H * yH
# reshape
yL = yL.reshape(ny, nx, nt)
yP = yP.reshape(ny, nx, nt)
yH = yH.reshape(ny, nx, nt)
xadjL = xadjL.reshape(npy, npx, nt)
xadjP = xadjP.reshape(npy, npx, nt)
xadjH = xadjH.reshape(npy, npx, nt)
# plotting
fig, axs = plt.subplots(2, 4, figsize=(10, 6), sharey=True)
axs[1, 0].imshow(
x[npy // 2].T,
vmin=-1,
vmax=1,
cmap="seismic_r",
extent=(1e3 * px[0], 1e3 * px[-1], t[-1], t[0]),
)
axs[1, 0].set(xlabel=r"$p_x$ [s/km]", ylabel=r"$t$ [s]", title="Input model")
axs[1, 0].axis("tight")
axs[0, 1].imshow(
yL[ny // 2].T,
vmin=-1,
vmax=1,
cmap="seismic_r",
extent=(hx[0], hx[-1], t[-1], t[0]),
)
axs[0, 1].tick_params(labelleft=True)
axs[0, 1].set(xlabel=r"$x$ [m]", ylabel=r"$t$ [s]", title="Linear data")
axs[0, 1].axis("tight")
axs[0, 2].imshow(
yP[ny // 2].T,
vmin=-1,
vmax=1,
cmap="seismic_r",
extent=(hx[0], hx[-1], t[-1], t[0]),
)
axs[0, 2].set(xlabel=r"$x$ [m]", title="Parabolic data")
axs[0, 2].axis("tight")
axs[0, 3].imshow(
yH[ny // 2].T,
vmin=-1,
vmax=1,
cmap="seismic_r",
extent=(hx[0], hx[-1], t[-1], t[0]),
)
axs[0, 3].set(xlabel=r"$x$ [m]", title="Hyperbolic data")
axs[0, 3].axis("tight")
axs[1, 1].imshow(
xadjL[npy // 2].T,
vmin=-100,
vmax=100,
cmap="seismic_r",
extent=(1e3 * px[0], 1e3 * px[-1], t[-1], t[0]),
)
axs[0, 0].axis("off")
axs[1, 1].set(xlabel=r"$p_x$ [s/km]", title="Linear adjoint")
axs[1, 1].axis("tight")
axs[1, 2].imshow(
xadjP[npy // 2].T,
vmin=-100,
vmax=100,
cmap="seismic_r",
extent=(1e3 * px[0], 1e3 * px[-1], t[-1], t[0]),
)
axs[1, 2].set(xlabel=r"$p_x$ [s/km]", title="Parabolic adjoint")
axs[1, 2].axis("tight")
axs[1, 3].imshow(
xadjH[npy // 2].T,
vmin=-100,
vmax=100,
cmap="seismic_r",
extent=(1e3 * px[0], 1e3 * px[-1], t[-1], t[0]),
)
axs[1, 3].set(xlabel=r"$p_x$ [s/km]", title="Hyperbolic adjoint")
axs[1, 3].axis("tight")
fig.tight_layout()
fig, axs = plt.subplots(2, 4, figsize=(10, 6), sharey=True)
axs[1, 0].imshow(
x[:, npx // 2 - 2].T,
vmin=-1,
vmax=1,
cmap="seismic_r",
extent=(1e3 * py[0], 1e3 * py[-1], t[-1], t[0]),
)
axs[1, 0].set(xlabel=r"$p_y$ [s/km]", ylabel=r"$t$ [s]", title="Input model")
axs[1, 0].axis("tight")
axs[0, 1].imshow(
yL[:, nx // 2].T,
vmin=-1,
vmax=1,
cmap="seismic_r",
extent=(hy[0], hy[-1], t[-1], t[0]),
)
axs[0, 1].tick_params(labelleft=True)
axs[0, 1].set(xlabel=r"$y$ [m]", ylabel=r"$t$ [s]", title="Linear data")
axs[0, 1].axis("tight")
axs[0, 2].imshow(
yP[:, nx // 2].T,
vmin=-1,
vmax=1,
cmap="seismic_r",
extent=(hy[0], hy[-1], t[-1], t[0]),
)
axs[0, 2].set(xlabel=r"$y$ [m]", title="Parabolic data")
axs[0, 2].axis("tight")
axs[0, 3].imshow(
yH[:, nx // 2].T,
vmin=-1,
vmax=1,
cmap="seismic_r",
extent=(hy[0], hy[-1], t[-1], t[0]),
)
axs[0, 3].set(xlabel=r"$y$ [m]", title="Hyperbolic data")
axs[0, 3].axis("tight")
axs[1, 1].imshow(
xadjL[:, npx // 2 - 5].T,
vmin=-100,
vmax=100,
cmap="seismic_r",
extent=(1e3 * py[0], 1e3 * py[-1], t[-1], t[0]),
)
axs[0, 0].axis("off")
axs[1, 1].set(xlabel=r"$p_y$ [s/km]", title="Linear adjoint")
axs[1, 1].axis("tight")
axs[1, 2].imshow(
xadjP[:, npx // 2 - 2].T,
vmin=-100,
vmax=100,
cmap="seismic_r",
extent=(1e3 * py[0], 1e3 * py[-1], t[-1], t[0]),
)
axs[1, 2].set(xlabel=r"$p_y$ [s/km]", title="Parabolic adjoint")
axs[1, 2].axis("tight")
axs[1, 3].imshow(
xadjH[:, npx // 2 - 2].T,
vmin=-100,
vmax=100,
cmap="seismic_r",
extent=(1e3 * py[0], 1e3 * py[-1], t[-1], t[0]),
)
axs[1, 3].set(xlabel=r"$p_y$ [s/km]", title="Hyperbolic adjoint")
axs[1, 3].axis("tight")
fig.tight_layout()
Total running time of the script: ( 0 minutes 2.203 seconds)
Note
Click here to download the full example code
Real¶
This example shows how to use the pylops.basicoperators.Real
operator.
This operator returns the real part of the data in forward and adjoint mode,
but the forward output will be a real number, while the adjoint output will
be a complex number with a zero-valued imaginary part.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s define a Real operator \(\mathbf{\Re}\) to extract the real component of the input.
M = 5
x = np.arange(M) + 1j * np.arange(M)[::-1]
Rop = pylops.basicoperators.Real(M, dtype="complex128")
y = Rop * x
xadj = Rop.H * y
_, axs = plt.subplots(1, 3, figsize=(10, 4))
axs[0].plot(np.real(x), lw=2, label="Real")
axs[0].plot(np.imag(x), lw=2, label="Imag")
axs[0].legend()
axs[0].set_title("Input")
axs[1].plot(np.real(y), lw=2, label="Real")
axs[1].plot(np.imag(y), lw=2, label="Imag")
axs[1].legend()
axs[1].set_title("Forward of Input")
axs[2].plot(np.real(xadj), lw=2, label="Real")
axs[2].plot(np.imag(xadj), lw=2, label="Imag")
axs[2].legend()
axs[2].set_title("Adjoint of Forward")
plt.tight_layout()

Total running time of the script: ( 0 minutes 0.358 seconds)
Note
Click here to download the full example code
Restriction and Interpolation¶
This example shows how to use the pylops.Restriction
operator
to sample a certain input vector at desired locations iava
. Moreover,
we go one step further and use the pylops.signalprocessing.Interp
operator to show how we can also sample values at locations that are not
exactly on the grid of the input vector.
As explained in the 03. Solvers tutorial, such operators can be used as forward model in an inverse problem aimed at interpolate irregularly sampled 1d or 2d signals onto a regular grid.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
np.random.seed(10)
Let’s create a signal of size nt
and sampling dt
that is composed
of three sinusoids at frequencies freqs
.
nt = 200
dt = 0.004
freqs = [5.0, 3.0, 8.0]
t = np.arange(nt) * dt
x = np.zeros(nt)
for freq in freqs:
x = x + np.sin(2 * np.pi * freq * t)
First of all, we subsample the signal at random locations and we retain 40% of the initial samples.
perc_subsampling = 0.4
ntsub = int(np.round(nt * perc_subsampling))
isample = np.arange(nt)
iava = np.sort(np.random.permutation(np.arange(nt))[:ntsub])
We then create the restriction and interpolation operators and display the original signal as well as the subsampled signal.
Rop = pylops.Restriction(nt, iava, dtype="float64")
NNop, iavann = pylops.signalprocessing.Interp(
nt, iava + 0.4, kind="nearest", dtype="float64"
)
LIop, iavali = pylops.signalprocessing.Interp(
nt, iava + 0.4, kind="linear", dtype="float64"
)
SIop, iavasi = pylops.signalprocessing.Interp(
nt, iava + 0.4, kind="sinc", dtype="float64"
)
y = Rop * x
ynn = NNop * x
yli = LIop * x
ysi = SIop * x
ymask = Rop.mask(x)
# Visualize data
fig = plt.figure(figsize=(15, 5))
plt.plot(isample, x, ".-k", lw=3, ms=10, label="all samples")
plt.plot(isample, ymask, ".g", ms=35, label="available samples")
plt.plot(iavann, ynn, ".r", ms=25, label="NN interp samples")
plt.plot(iavali, yli, ".m", ms=20, label="Linear interp samples")
plt.plot(iavasi, ysi, ".y", ms=15, label="Sinc interp samples")
plt.legend(loc="right")
plt.title("Data restriction")
subax = fig.add_axes([0.2, 0.2, 0.15, 0.6])
subax.plot(isample, x, ".-k", lw=3, ms=10)
subax.plot(isample, ymask, ".g", ms=35)
subax.plot(iavann, ynn, ".r", ms=25)
subax.plot(iavali, yli, ".m", ms=20)
subax.plot(iavasi, ysi, ".y", ms=15)
subax.set_xlim([120, 127])
subax.set_ylim([-0.5, 0.5])
plt.tight_layout()

Finally we show how the pylops.Restriction
is not limited to
one dimensional signals but can be applied to sample locations of a specific
axis of a multi-dimensional array.
subsampling locations
nx, nt = 100, 50
x = np.random.normal(0, 1, (nx, nt))
perc_subsampling = 0.4
nxsub = int(np.round(nx * perc_subsampling))
iava = np.sort(np.random.permutation(np.arange(nx))[:nxsub])
Rop = pylops.Restriction((nx, nt), iava, axis=0, dtype="float64")
y = Rop * x
ymask = Rop.mask(x)
fig, axs = plt.subplots(1, 3, figsize=(10, 5), sharey=True)
axs[0].imshow(x.T, cmap="gray")
axs[0].set_title("Model")
axs[0].axis("tight")
axs[1].imshow(y.T, cmap="gray")
axs[1].set_title("Data")
axs[1].axis("tight")
axs[2].imshow(ymask.T, cmap="gray")
axs[2].set_title("Masked model")
axs[2].axis("tight")
plt.tight_layout()

Total running time of the script: ( 0 minutes 0.581 seconds)
Note
Click here to download the full example code
Roll¶
This example shows how to use the pylops.Roll
operator.
This operator simply shifts elements of multi-dimensional array along a specified direction a chosen number of samples.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s start with a 1d example. We make a signal, shift it by two samples and then shift it back using its adjoint. We can immediately see how the adjoint of this operator is equivalent to its inverse.
nx = 10
x = np.arange(nx)
Rop = pylops.Roll(nx, shift=2)
y = Rop * x
xadj = Rop.H * y
plt.figure()
plt.plot(x, "k", lw=2, label="x")
plt.plot(y, "b", lw=2, label="y")
plt.plot(xadj, "--r", lw=2, label="xadj")
plt.title("1D Roll")
plt.legend()
plt.tight_layout()

We can now do the same with a 2d array.
ny, nx = 10, 5
x = np.arange(ny * nx).reshape(ny, nx)
Rop = pylops.Roll(dims=(ny, nx), axis=1, shift=-2)
y = Rop * x
xadj = Rop.H * y
fig, axs = plt.subplots(1, 3, figsize=(10, 4))
fig.suptitle("Roll for 2d data", fontsize=14, fontweight="bold", y=1.15)
axs[0].imshow(x, cmap="rainbow", vmin=0, vmax=50)
axs[0].set_title(r"$x$")
axs[0].axis("tight")
axs[1].imshow(y, cmap="rainbow", vmin=0, vmax=50)
axs[1].set_title(r"$y = R x$")
axs[1].axis("tight")
axs[2].imshow(xadj, cmap="rainbow", vmin=0, vmax=50)
axs[2].set_title(r"$x_{adj} = R^H y$")
axs[2].axis("tight")
plt.tight_layout()

Total running time of the script: ( 0 minutes 0.471 seconds)
Note
Click here to download the full example code
Seislet transform¶
This example shows how to use the pylops.signalprocessing.Seislet
operator. This operator the forward, adjoint and inverse Seislet transform
that is a modification of the well-know Wavelet transform where local slopes
are used in the prediction and update steps to further improve the prediction
of a trace from its previous (or subsequent) one and reduce the amount of
information passed to the subsequent scale. While this transform was initially
developed in the context of processing and compression of seismic data, it is
also suitable to any other oscillatory dataset such as GPR or Acoustic
recordings.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import MaxNLocator
from mpl_toolkits.axes_grid1 import make_axes_locatable
import pylops
plt.close("all")
In this example we use the same benchmark
dataset
that was used in the original paper describing the Seislet transform. First,
local slopes are estimated using
pylops.utils.signalprocessing.slope_estimate
.
inputfile = "../testdata/sigmoid.npz"
d = np.load(inputfile)
d = d["sigmoid"]
nx, nt = d.shape
dx, dt = 0.008, 0.004
x, t = np.arange(nx) * dx, np.arange(nt) * dt
# slope estimation
slope, _ = pylops.utils.signalprocessing.slope_estimate(d.T, dt, dx, smooth=2.5)
slope = -slope.T # t-axis points down, reshape
# clip slopes above 80°
pmax = np.arctan(80 * np.pi / 180)
slope[slope > pmax] = pmax
slope[slope < -pmax] = -pmax
clip = 0.5 * np.max(np.abs(d))
clip_s = min(pmax, np.max(np.abs(slope)))
opts = dict(aspect=2, extent=(x[0], x[-1], t[-1], t[0]))
fig, axs = plt.subplots(1, 2, figsize=(14, 7), sharey=True, sharex=True)
axs[0].imshow(d.T, cmap="gray", vmin=-clip, vmax=clip, **opts)
axs[0].set(xlabel="Position [km]", ylabel="Time [s]", title="Data")
im = axs[1].imshow(slope.T, cmap="RdBu_r", vmin=-clip_s, vmax=clip_s, **opts)
axs[1].set(xlabel="Position [km]", title="Slopes")
fig.tight_layout()
pos = axs[1].get_position()
cbpos = [
pos.x0 + 0.1 * pos.width,
pos.y0 + 0.9 * pos.height,
0.8 * pos.width,
0.05 * pos.height,
]
cax = fig.add_axes(cbpos)
cb = fig.colorbar(im, cax=cax, orientation="horizontal")
cb.set_label("[s/km]")

Next the Seislet transform is computed.
Sop = pylops.signalprocessing.Seislet(slope, sampling=(dx, dt))
seis = Sop * d
nlevels_max = int(np.log2(nx))
levels_size = np.flip(np.array([2**i for i in range(nlevels_max)]))
levels_cum = np.cumsum(levels_size)
fig, ax = plt.subplots(figsize=(14, 6))
im = ax.imshow(
seis.T,
cmap="gray",
vmin=-clip,
vmax=clip,
aspect="auto",
interpolation="none",
extent=(1, seis.shape[0], t[-1], t[0]),
)
ax.xaxis.set_major_locator(MaxNLocator(nbins=20, integer=True))
for level in levels_cum:
ax.axvline(level + 0.5, color="w")
ax.set(xlabel="Scale", ylabel="Time [s]", title="Seislet transform")
cax = make_axes_locatable(ax).append_axes("right", size="2%", pad=0.1)
cb = fig.colorbar(im, cax=cax, orientation="vertical")
cb.formatter.set_powerlimits((0, 0))
fig.tight_layout()

We may also stretch the finer scales to be the width of the image
fig, axs = plt.subplots(2, nlevels_max // 2, figsize=(14, 7), sharex=True, sharey=True)
for i, ax in enumerate(axs.ravel()[:-1]):
curdata = seis[levels_cum[i] : levels_cum[i + 1], :].T
vmax = np.max(np.abs(curdata))
ax.imshow(curdata, vmin=-vmax, vmax=vmax, cmap="gray", interpolation="none", **opts)
ax.set(title=f"Scale {i+1}")
if i + 1 > nlevels_max // 2:
ax.set(xlabel="Position [km]")
curdata = seis[levels_cum[-1] :, :].T
vmax = np.max(np.abs(curdata))
axs[-1, -1].imshow(
curdata, vmin=-vmax, vmax=vmax, cmap="gray", interpolation="none", **opts
)
axs[0, 0].set(ylabel="Time [s]")
axs[1, 0].set(ylabel="Time [s]")
axs[-1, -1].set(xlabel="Position [km]", title=f"Scale {nlevels_max}")
fig.tight_layout()

As a comparison we also compute the Seislet transform fixing slopes to zero. This way we turn the Seislet tranform into a basic 1D Wavelet transform performed over the spatial axis.
Wop = pylops.signalprocessing.Seislet(np.zeros_like(slope), sampling=(dx, dt))
dwt = Wop * d
fig, ax = plt.subplots(figsize=(14, 6))
im = ax.imshow(
dwt.T,
cmap="gray",
vmin=-clip,
vmax=clip,
aspect="auto",
interpolation="none",
extent=(1, dwt.shape[0], t[-1], t[0]),
)
ax.xaxis.set_major_locator(MaxNLocator(nbins=20, integer=True))
for level in levels_cum:
ax.axvline(level + 0.5, color="w")
ax.set(xlabel="Scale", ylabel="Time [s]", title="Wavelet transform")
cax = make_axes_locatable(ax).append_axes("right", size="2%", pad=0.1)
cb = fig.colorbar(im, cax=cax, orientation="vertical")
cb.formatter.set_powerlimits((0, 0))
fig.tight_layout()

Again, we may decompress the finer scales
fig, axs = plt.subplots(2, nlevels_max // 2, figsize=(14, 7), sharex=True, sharey=True)
for i, ax in enumerate(axs.ravel()[:-1]):
curdata = dwt[levels_cum[i] : levels_cum[i + 1], :].T
vmax = np.max(np.abs(curdata))
ax.imshow(curdata, vmin=-vmax, vmax=vmax, cmap="gray", interpolation="none", **opts)
ax.set(title=f"Scale {i+1}")
if i + 1 > nlevels_max // 2:
ax.set(xlabel="Position [km]")
curdata = dwt[levels_cum[-1] :, :].T
vmax = np.max(np.abs(curdata))
axs[-1, -1].imshow(
curdata, vmin=-vmax, vmax=vmax, cmap="gray", interpolation="none", **opts
)
axs[0, 0].set(ylabel="Time [s]")
axs[1, 0].set(ylabel="Time [s]")
axs[-1, -1].set(xlabel="Position [km]", title=f"Scale {nlevels_max}")
fig.tight_layout()

Finally we evaluate the compression capabilities of the Seislet transform compared to the 1D Wavelet transform. We zero-out all but the strongest 25% of the components. We perform the inverse transforms and assess the compression error.
perc = 0.25
seis_strong_idx = np.argsort(-np.abs(seis.ravel()))
dwt_strong_idx = np.argsort(-np.abs(dwt.ravel()))
seis_strong = np.abs(seis.ravel())[seis_strong_idx]
dwt_strong = np.abs(dwt.ravel())[dwt_strong_idx]
fig, ax = plt.subplots()
ax.plot(range(1, len(seis_strong) + 1), seis_strong / seis_strong[0], label="Seislet")
ax.plot(
range(1, len(dwt_strong) + 1), dwt_strong / dwt_strong[0], "--", label="Wavelet"
)
ax.set(xlabel="n", ylabel="Coefficient strength [%]", title="Transform Coefficients")
ax.axvline(np.rint(len(seis_strong) * perc), color="k", label=f"{100*perc:.0f}%")
ax.legend()
fig.tight_layout()

seis1 = np.zeros_like(seis.ravel())
seis_strong_idx = seis_strong_idx[: int(np.rint(len(seis_strong) * perc))]
seis1[seis_strong_idx] = seis.ravel()[seis_strong_idx]
d_seis = Sop.inverse(seis1).reshape(Sop.dims)
dwt1 = np.zeros_like(dwt.ravel())
dwt_strong_idx = dwt_strong_idx[: int(np.rint(len(dwt_strong) * perc))]
dwt1[dwt_strong_idx] = dwt.ravel()[dwt_strong_idx]
d_dwt = Wop.inverse(dwt1).reshape(Wop.dims)
opts.update(dict(cmap="gray", vmin=-clip, vmax=clip))
fig, axs = plt.subplots(2, 3, figsize=(14, 7), sharex=True, sharey=True)
axs[0, 0].imshow(d.T, **opts)
axs[0, 0].set(title="Data")
axs[0, 1].imshow(d_seis.T, **opts)
axs[0, 1].set(title=f"Rec. from Seislet ({100*perc:.0f}% of coeffs.)")
axs[0, 2].imshow((d - d_seis).T, **opts)
axs[0, 2].set(title="Error from Seislet Rec.")
axs[1, 0].imshow(d.T, **opts)
axs[1, 0].set(ylabel="Time [s]", title="Data [Repeat]")
axs[1, 1].imshow(d_dwt.T, **opts)
axs[1, 1].set(title=f"Rec. from Wavelet ({100*perc:.0f}% of coeffs.)")
axs[1, 2].imshow((d - d_dwt).T, **opts)
axs[1, 2].set(title="Error from Wavelet Rec.")
for i in range(3):
axs[1, i].set(xlabel="Position [km]")
plt.tight_layout()
![Data, Rec. from Seislet (25% of coeffs.), Error from Seislet Rec., Data [Repeat], Rec. from Wavelet (25% of coeffs.), Error from Wavelet Rec.](_images/sphx_glr_plot_seislet_007.png)
To conclude it is worth noting that the Seislet transform, differently to the Wavelet transform, is not orthogonal: in other words, its adjoint and inverse are not equivalent. While we have used the forward and inverse transformations, when used as linear operator in composition with other operators, the Seislet transform requires the adjoint be defined and that it also passes the dot-test pair that is. As shown below, this is the case when using the implementation in the PyLops package.
pylops.utils.dottest(Sop, verb=True)
Dot test passed, v^H(Opu)=-98.52334183020213 - u^H(Op^Hv)=-98.52334183020218
True
Total running time of the script: ( 0 minutes 12.160 seconds)
Note
Click here to download the full example code
Shift¶
This example shows how to use the pylops.signalprocessing.Shift
operator to apply fractional delay to an input signal. Whilst this operator
acts on 1D signals it can also be applied on any multi-dimensional signal on
a specific direction of it.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s start with a 1D example. Define the input parameters: number of samples
of input signal (nt
), sampling step (dt
) as well as the input
signal which will be equal to a ricker wavelet:
nt = 127
dt = 0.004
t = np.arange(nt) * dt
ntwav = 41
wav = pylops.utils.wavelets.ricker(t[:ntwav], f0=20)[0]
wav = np.pad(wav, [0, nt - len(wav)])
WAV = np.fft.rfft(wav, n=nt)
We can shift this wavelet by \(5.5\mathrm{dt}\):
shift = 5.5 * dt
Op = pylops.signalprocessing.Shift(nt, shift, sampling=dt, real=True, dtype=np.float64)
wavshift = Op * wav
wavshiftback = Op.H * wavshift
plt.figure(figsize=(10, 3))
plt.plot(t, wav, "k", lw=2, label="Original")
plt.plot(t, wavshift, "r", lw=2, label="Shifted")
plt.plot(t, wavshiftback, "--b", lw=2, label="Adjoint")
plt.axvline(t[ntwav - 1], color="k")
plt.axvline(t[ntwav - 1] + shift, color="r")
plt.xlim(0, 0.3)
plt.legend()
plt.title("1D Shift")
plt.tight_layout()

We can repeat the same exercise for a 2D signal and perform the shift along the first and second dimensions.
shift = 10.5 * dt
# 1st axis
wav2d = np.outer(wav, np.ones(10))
Op = pylops.signalprocessing.Shift(
(nt, 10), shift, axis=0, sampling=dt, real=True, dtype=np.float64
)
wav2dshift = Op * wav2d
wav2dshiftback = Op.H * wav2dshift
fig, axs = plt.subplots(1, 3, figsize=(10, 3))
axs[0].imshow(wav2d, cmap="gray")
axs[0].axis("tight")
axs[0].set_title("Original")
axs[1].imshow(wav2dshift, cmap="gray")
axs[1].set_title("Shifted")
axs[1].axis("tight")
axs[2].imshow(wav2dshiftback, cmap="gray")
axs[2].set_title("Adjoint")
axs[2].axis("tight")
fig.tight_layout()
# 2nd axis
wav2d = np.outer(wav, np.ones(10)).T
Op = pylops.signalprocessing.Shift(
(10, nt), shift, axis=1, sampling=dt, real=True, dtype=np.float64
)
wav2dshift = Op * wav2d
wav2dshiftback = Op.H * wav2dshift
fig, axs = plt.subplots(1, 3, figsize=(10, 3))
axs[0].imshow(wav2d, cmap="gray")
axs[0].axis("tight")
axs[0].set_title("Original")
axs[1].imshow(wav2dshift, cmap="gray")
axs[1].set_title("Shifted")
axs[1].axis("tight")
axs[2].imshow(wav2dshiftback, cmap="gray")
axs[2].set_title("Adjoint")
axs[2].axis("tight")
fig.tight_layout()
Finally we consider a more generic case where we apply a trace varying shift
shift = dt * np.arange(10)
wav2d = np.outer(wav, np.ones(10))
Op = pylops.signalprocessing.Shift(
(nt, 10), shift, axis=0, sampling=dt, real=True, dtype=np.float64
)
wav2dshift = Op * wav2d
wav2dshiftback = Op.H * wav2dshift
fig, axs = plt.subplots(1, 3, figsize=(10, 3))
axs[0].imshow(wav2d, cmap="gray")
axs[0].axis("tight")
axs[0].set_title("Original")
axs[1].imshow(wav2dshift, cmap="gray")
axs[1].set_title("Shifted")
axs[1].axis("tight")
axs[2].imshow(wav2dshiftback, cmap="gray")
axs[2].set_title("Adjoint")
axs[2].axis("tight")
fig.tight_layout()

Total running time of the script: ( 0 minutes 1.116 seconds)
Note
Click here to download the full example code
Slope estimation via Structure Tensor algorithm¶
This example shows how to estimate local slopes or local dips of a two-dimensional
array using pylops.utils.signalprocessing.slope_estimate
and
pylops.utils.signalprocessing.dip_estimate
.
Knowing the local slopes of an image (or a seismic data) can be useful for
a variety of tasks in image (or geophysical) processing such as denoising,
smoothing, or interpolation. When slopes are used with the
pylops.signalprocessing.Seislet
operator, the input dataset can be
compressed and the sparse nature of the Seislet transform can also be used to
precondition sparsity-promoting inverse problems.
We will show examples of a variety of different settings, including a comparison with the original implementation in [1].
- 1
van Vliet, L. J., Verbeek, P. W., “Estimators for orientation and anisotropy in digitized images”, Journal ASCI Imaging Workshop. 1995.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.image import imread
from matplotlib.ticker import FuncFormatter, MultipleLocator
from mpl_toolkits.axes_grid1 import make_axes_locatable
import pylops
from pylops.signalprocessing.seislet import _predict_trace
from pylops.utils.signalprocessing import dip_estimate, slope_estimate
plt.close("all")
np.random.seed(10)
Python logo¶
To start we import a 2d image and estimate the local dips of the image.
im = np.load("../testdata/python.npy")[..., 0]
im = im / 255.0 - 0.5
angles, anisotropy = dip_estimate(im, smooth=7)
angles = -np.rad2deg(angles)
fig, axs = plt.subplots(1, 3, figsize=(12, 4), sharex=True, sharey=True)
iax = axs[0].imshow(im, cmap="viridis", origin="lower")
axs[0].set_title("Data")
cax = make_axes_locatable(axs[0]).append_axes("right", size="5%", pad=0.05)
cax.axis("off")
iax = axs[1].imshow(angles, cmap="twilight_shifted", origin="lower", vmin=-90, vmax=90)
axs[1].set_title("Angle of incline")
cax = make_axes_locatable(axs[1]).append_axes("right", size="5%", pad=0.05)
cb = fig.colorbar(
iax,
ticks=MultipleLocator(30),
format=FuncFormatter(lambda x, pos: "{:.0f}°".format(x)),
cax=cax,
orientation="vertical",
)
iax = axs[2].imshow(anisotropy, cmap="Reds", origin="lower", vmin=0, vmax=1)
axs[2].set_title("Anisotropy")
cax = make_axes_locatable(axs[2]).append_axes("right", size="5%", pad=0.05)
cb = fig.colorbar(iax, cax=cax, orientation="vertical")
fig.tight_layout()

Seismic data¶
We can now repeat the same using some seismic data. We will first define a single trace and a slope field, apply such slope field to the trace recursively to create the other traces of the data and finally try to recover the underlying slope field from the data alone.
# Reflectivity model
nx, nt = 2**7, 121
dx, dt = 0.01, 0.004
x, t = np.arange(nx) * dx, np.arange(nt) * dt
nspike = nt // 8
refl = np.zeros(nt)
it = np.sort(np.random.permutation(range(10, nt - 20))[:nspike])
refl[it] = np.random.normal(0.0, 1.0, nspike)
# Wavelet
ntwav = 41
f0 = 30
twav = np.arange(ntwav) * dt
wav, *_ = pylops.utils.wavelets.ricker(twav, f0)
# Input trace
trace = np.convolve(refl, wav, mode="same")
# Slopes
theta = np.deg2rad(np.linspace(0, 30, nx))
slope = np.outer(np.ones(nt), np.tan(theta) * dt / dx)
# Model data
d = np.zeros((nt, nx))
tr = trace.copy()
for ix in range(nx):
tr = _predict_trace(tr, t, dt, dx, slope[:, ix])
d[:, ix] = tr
# Estimate slopes
slope_est, _ = slope_estimate(d, dt, dx, smooth=10)
slope_est *= -1
fig, axs = plt.subplots(2, 2, figsize=(6, 6), sharex=True, sharey=True)
opts = dict(aspect="auto", extent=(x[0], x[-1], t[-1], t[0]))
iax = axs[0, 0].imshow(d, cmap="gray", vmin=-1, vmax=1, **opts)
axs[0, 0].set(title="Data", ylabel="Time [s]")
cax = make_axes_locatable(axs[0, 0]).append_axes("right", size="5%", pad=0.05)
fig.colorbar(iax, cax=cax, orientation="vertical")
opts.update(dict(cmap="cividis", vmin=np.min(slope), vmax=np.max(slope)))
iax = axs[0, 1].imshow(slope, **opts)
axs[0, 1].set(title="True Slope")
cax = make_axes_locatable(axs[0, 1]).append_axes("right", size="5%", pad=0.05)
fig.colorbar(iax, cax=cax, orientation="vertical")
cax.set_ylabel("[s/km]")
iax = axs[1, 0].imshow(np.abs(slope - slope_est), **opts)
axs[1, 0].set(
title="Estimate absolute error", ylabel="Time [s]", xlabel="Position [km]"
)
cax = make_axes_locatable(axs[1, 0]).append_axes("right", size="5%", pad=0.05)
fig.colorbar(iax, cax=cax, orientation="vertical")
cax.set_ylabel("[s/km]")
iax = axs[1, 1].imshow(slope_est, **opts)
axs[1, 1].set(title="Estimated Slope", xlabel="Position [km]")
cax = make_axes_locatable(axs[1, 1]).append_axes("right", size="5%", pad=0.05)
fig.colorbar(iax, cax=cax, orientation="vertical")
cax.set_ylabel("[s/km]")
fig.tight_layout()

Concentric circles¶
The original paper by van Vliet and Verbeek [1] has an example with concentric circles. We recover their original images and compare our implementation with theirs.
def rgb2gray(rgb):
return np.dot(rgb[..., :3], [0.2989, 0.5870, 0.1140])
circles_input = rgb2gray(imread("../testdata/slope_estimate/concentric.png"))
circles_angles = rgb2gray(imread("../testdata/slope_estimate/concentric_angles.png"))
angles, anisos_sm0 = dip_estimate(circles_input, smooth=0)
angles_sm0 = np.rad2deg(angles)
angles, anisos_sm4 = dip_estimate(circles_input, smooth=4)
angles_sm4 = np.rad2deg(angles)
fig, axs = plt.subplots(2, 3, figsize=(6, 4), sharex=True, sharey=True)
axs[0, 0].imshow(circles_input, cmap="gray", aspect="equal")
axs[0, 0].set(title="Original Image")
cax = make_axes_locatable(axs[0, 0]).append_axes("right", size="5%", pad=0.05)
cax.axis("off")
axs[1, 0].imshow(-circles_angles, cmap="twilight_shifted")
axs[1, 0].set(title="Original Angles")
cax = make_axes_locatable(axs[1, 0]).append_axes("right", size="5%", pad=0.05)
cax.axis("off")
im = axs[0, 1].imshow(angles_sm0, cmap="twilight_shifted", vmin=-90, vmax=90)
cax = make_axes_locatable(axs[0, 1]).append_axes("right", size="5%", pad=0.05)
cb = fig.colorbar(
im,
ticks=MultipleLocator(30),
format=FuncFormatter(lambda x, pos: "{:.0f}°".format(x)),
cax=cax,
orientation="vertical",
)
axs[0, 1].set(title="Angles (smooth=0)")
im = axs[1, 1].imshow(angles_sm4, cmap="twilight_shifted", vmin=-90, vmax=90)
cax = make_axes_locatable(axs[1, 1]).append_axes("right", size="5%", pad=0.05)
cb = fig.colorbar(
im,
ticks=MultipleLocator(30),
format=FuncFormatter(lambda x, pos: "{:.0f}°".format(x)),
cax=cax,
orientation="vertical",
)
axs[1, 1].set(title="Angles (smooth=4)")
im = axs[0, 2].imshow(anisos_sm0, cmap="Reds", vmin=0, vmax=1)
cax = make_axes_locatable(axs[0, 2]).append_axes("right", size="5%", pad=0.05)
cb = fig.colorbar(im, cax=cax, orientation="vertical")
axs[0, 2].set(title="Anisotropy (smooth=0)")
im = axs[1, 2].imshow(anisos_sm4, cmap="Reds", vmin=0, vmax=1)
cax = make_axes_locatable(axs[1, 2]).append_axes("right", size="5%", pad=0.05)
cb = fig.colorbar(im, cax=cax, orientation="vertical")
axs[1, 2].set(title="Anisotropy (smooth=4)")
for ax in axs.ravel():
ax.axis("off")
fig.tight_layout()

Core samples¶
The original paper by van Vliet and Verbeek [1] also has an example with images of core samples. Since the original paper does not have a scale with which to plot the angles, we have chosen ours it to match their image as closely as possible.
core_input = rgb2gray(imread("../testdata/slope_estimate/core_sample.png"))
core_angles = rgb2gray(imread("../testdata/slope_estimate/core_sample_orientation.png"))
core_aniso = rgb2gray(imread("../testdata/slope_estimate/core_sample_anisotropy.png"))
angles, anisos_sm4 = dip_estimate(core_input, smooth=4)
angles_sm4 = np.rad2deg(angles)
angles, anisos_sm8 = dip_estimate(core_input, smooth=8)
angles_sm8 = np.rad2deg(angles)
fig, axs = plt.subplots(1, 6, figsize=(10, 6))
axs[0].imshow(core_input, cmap="gray_r", aspect="equal")
axs[0].set(title="Original\nImage")
cax = make_axes_locatable(axs[0]).append_axes("right", size="20%", pad=0.05)
cax.axis("off")
axs[1].imshow(-core_angles, cmap="YlGnBu_r")
axs[1].set(title="Original\nAngles")
cax = make_axes_locatable(axs[1]).append_axes("right", size="20%", pad=0.05)
cax.axis("off")
im = axs[2].imshow(angles_sm8, cmap="YlGnBu_r", vmin=-49, vmax=-11)
cax = make_axes_locatable(axs[2]).append_axes("right", size="20%", pad=0.05)
cb = fig.colorbar(
im,
ticks=MultipleLocator(30),
format=FuncFormatter(lambda x, pos: "{:.0f}°".format(x)),
cax=cax,
orientation="vertical",
)
axs[2].set(title="Angles\n(smooth=8)")
im = axs[3].imshow(angles_sm4, cmap="YlGnBu_r", vmin=-49, vmax=-11)
cax = make_axes_locatable(axs[3]).append_axes("right", size="20%", pad=0.05)
cb = fig.colorbar(
im,
ticks=MultipleLocator(30),
format=FuncFormatter(lambda x, pos: "{:.0f}°".format(x)),
cax=cax,
orientation="vertical",
)
axs[3].set(title="Angles\n(smooth=4)")
im = axs[4].imshow(anisos_sm8, cmap="Reds", vmin=0, vmax=1)
cax = make_axes_locatable(axs[4]).append_axes("right", size="20%", pad=0.05)
cb = fig.colorbar(im, cax=cax, orientation="vertical")
axs[4].set(title="Anisotropy\n(smooth=8)")
im = axs[5].imshow(anisos_sm4, cmap="Reds", vmin=0, vmax=1)
cax = make_axes_locatable(axs[5]).append_axes("right", size="20%", pad=0.05)
cb = fig.colorbar(im, cax=cax, orientation="vertical")
axs[5].set(title="Anisotropy\n(smooth=4)")
for ax in axs.ravel():
ax.axis("off")
fig.tight_layout()

Final considerations¶
As you can see the Structure Tensor algorithm is a very fast, general purpose algorithm that can be used to estimate local slopes to input datasets of very different natures.
Total running time of the script: ( 0 minutes 2.322 seconds)
Note
Click here to download the full example code
Spread How-to¶
This example focuses on the pylops.basicoperators.Spread
operator,
which is a highly versatile operator in PyLops to perform spreading/stacking
operations in a vectorized manner (or efficiently via Numba-jitted for
loops).
The pylops.basicoperators.Spread
is powerful in its generality, but
it may not be obvious for at first how to structure your code to leverage it properly.
While it is highly recommended for advanced users to inspect the
pylops.signalprocessing.Radon2D
and
pylops.signalprocessing.Radon3D
operators since
they are built using the pylops.basicoperators.Spread
class,
here we provide a simple example on how to get started.
In this example we will recreate a simplified version of the famous linear Radon operator, which stacks data along straight lines with a given intercept and slope.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s first define the time and space axes as well as some auxiliary input parameters that we will use to create a Ricker wavelet
par = {
"ox": -200,
"dx": 2,
"nx": 201,
"ot": 0,
"dt": 0.004,
"nt": 501,
"f0": 20,
"nfmax": 210,
}
# Create axis
t, _, x, _ = pylops.utils.seismicevents.makeaxis(par)
# Create centered Ricker wavelet
t_wav = np.arange(41) * par["dt"]
wav, _, _ = pylops.utils.wavelets.ricker(t_wav, f0=par["f0"])
We will create a 2d data with a number of crossing linear events, to which we will
later apply our Radon transforms. We use the convenience function
pylops.utils.seismicevents.linear2d
.
v = 1500 # m/s
t0 = [0.2, 0.7, 1.6] # seconds
theta = [40, 0, -60] # degrees
amp = [1.0, 0.6, -2.0]
mlin, mlinwav = pylops.utils.seismicevents.linear2d(x, t, v, t0, theta, amp, wav)
Let’s now define the slowness axis and use pylops.signalprocessing.Radon2D
to implement our benchmark linear Radon. Refer to the documentation of the
operator for a more detailed mathematical description of linear Radon.
Note that pxmax
is in s/m, which explains the small value. Its highest value
corresponds to the lowest value of velocity in the transform. In this case we choose that
to be 1000 m/s.
npx, pxmax = 41, 1e-3
px = np.linspace(-pxmax, pxmax, npx)
RLop = pylops.signalprocessing.Radon2D(
t, x, px, centeredh=False, kind="linear", interp=False, engine="numpy"
)
# Compute adjoint = Radon transform
mlinwavR = RLop.H * mlinwav
Now, let’s try to reimplement this operator from scratch using pylops.basicoperators.Spread
.
Using the on-the-fly approach, and we need to create a function which takes
indices of the model domain, here \((p_x, t_0)\)
where \(p_x\) is the slope and \(t_0\) is the intercept of the
parametric curve \(t(x) = t_0 + p_x x\) we wish to spread the model over
in the data domain. The function must return an array of size nx
, containing
the indices corresponding to \(t(x)\).
The on-the-fly approach is useful when storing the indices in RAM may exhaust
resources, especially when computing the indices is fast. When there is
enough memory to store the full table of indices
(an array of size \(n_x \times n_t \times n_{p_x}\)) the
pylops.basicoperators.Spread
operator can be used with tables instead.
We will see an example of this later.
Returning to our on-the-fly example, we need to create a function which only depends on
ipx
and it0
, so we create a closure around it with all our other auxiliary
variables.
def create_radon_fh(xaxis, taxis, pxaxis):
ot = taxis[0]
dt = taxis[1] - taxis[0]
nt = len(taxis)
def fh(ipx, it0):
tx = t[it0] + xaxis * pxaxis[ipx]
it0_frac = (tx - ot) / dt
itx = np.rint(it0_frac)
# Indices outside time axis set to nan
itx[np.isin(itx, range(nt), invert=True)] = np.nan
return itx
return fh
fRad = create_radon_fh(x, t, px)
ROTFOp = pylops.Spread((npx, par["nt"]), (par["nx"], par["nt"]), fh=fRad)
mlinwavROTF = ROTFOp.H * mlinwav
Compare the results between the native Radon transform and the one using our
on-the-fly pylops.basicoperators.Spread
.
fig, axs = plt.subplots(1, 3, figsize=(9, 5), sharey=True)
axs[0].imshow(
mlinwav.T,
aspect="auto",
interpolation="nearest",
vmin=-1,
vmax=1,
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[0].set_title("Linear events", fontsize=12, fontweight="bold")
axs[0].set_xlabel(r"$x$ [m]")
axs[0].set_ylabel(r"$t$ [s]")
axs[1].imshow(
mlinwavR.T,
aspect="auto",
interpolation="nearest",
vmin=-10,
vmax=10,
cmap="gray",
extent=(px.min(), px.max(), t.max(), t.min()),
)
axs[1].set_title("Native Linear Radon", fontsize=12, fontweight="bold")
axs[1].set_xlabel(r"$p_x$ [s/m]")
axs[1].ticklabel_format(style="sci", axis="x", scilimits=(0, 0))
axs[2].imshow(
mlinwavROTF.T,
aspect="auto",
interpolation="nearest",
vmin=-10,
vmax=10,
cmap="gray",
extent=(px.min(), px.max(), t.max(), t.min()),
)
axs[2].set_title("On-the-fly Linear Radon", fontsize=12, fontweight="bold")
axs[2].set_xlabel(r"$p_x$ [s/m]")
axs[2].ticklabel_format(style="sci", axis="x", scilimits=(0, 0))
fig.tight_layout()

Finally, we will re-implement the example above using pre-computed tables.
This is useful when fh
is expensive to compute, or requires manual edition
prior to usage.
Using a table instead of a function is simple, we just need to apply fh
to
all our points and store the results.
def create_table(npx, nt, nx):
table = np.full((npx, nt, nx), fill_value=np.nan)
for ipx in range(npx):
for it0 in range(nt):
table[ipx, it0, :] = fRad(ipx, it0)
return table
table = create_table(npx, par["nt"], par["nx"])
RPCOp = pylops.Spread((npx, par["nt"]), (par["nx"], par["nt"]), table=table)
mlinwavRPC = RPCOp.H * mlinwav
Compare the results between the pre-computed or on-the-fly Radon transforms
fig, axs = plt.subplots(1, 3, figsize=(9, 5), sharey=True)
axs[0].imshow(
mlinwav.T,
aspect="auto",
interpolation="nearest",
vmin=-1,
vmax=1,
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[0].set_title("Linear events", fontsize=12, fontweight="bold")
axs[0].set_xlabel(r"$x$ [m]")
axs[0].set_ylabel(r"$t$ [s]")
axs[1].imshow(
mlinwavRPC.T,
aspect="auto",
interpolation="nearest",
vmin=-10,
vmax=10,
cmap="gray",
extent=(px.min(), px.max(), t.max(), t.min()),
)
axs[1].set_title("Pre-computed Linear Radon", fontsize=12, fontweight="bold")
axs[1].set_xlabel(r"$p_x$ [s/m]")
axs[1].ticklabel_format(style="sci", axis="x", scilimits=(0, 0))
axs[2].imshow(
mlinwavROTF.T,
aspect="auto",
interpolation="nearest",
vmin=-10,
vmax=10,
cmap="gray",
extent=(px.min(), px.max(), t.max(), t.min()),
)
axs[2].set_title("On-the-fly Linear Radon", fontsize=12, fontweight="bold")
axs[2].set_xlabel(r"$p_x$ [s/m]")
axs[2].ticklabel_format(style="sci", axis="x", scilimits=(0, 0))
fig.tight_layout()

Total running time of the script: ( 0 minutes 7.838 seconds)
Note
Click here to download the full example code
Sum¶
This example shows how to use the pylops.Sum
operator to stack
values along an axis of a multi-dimensional array
import matplotlib.gridspec as pltgs
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s start by defining a 2-dimensional data
ny, nx = 5, 7
x = (np.arange(ny * nx)).reshape(ny, nx)
We can now create the operator and peform forward and adjoint
Sop = pylops.Sum(dims=(ny, nx), axis=0)
y = Sop * x
xadj = Sop.H * y
gs = pltgs.GridSpec(1, 7)
fig = plt.figure(figsize=(7, 4))
ax = plt.subplot(gs[0, 0:3])
im = ax.imshow(x, cmap="rainbow", vmin=0, vmax=ny * nx)
ax.set_title("x", size=20, fontweight="bold")
ax.set_xticks(np.arange(nx - 1) + 0.5)
ax.set_yticks(np.arange(ny - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax.axis("tight")
ax = plt.subplot(gs[0, 3])
ax.imshow(y[:, np.newaxis], cmap="rainbow", vmin=0, vmax=ny * nx)
ax.set_title("y", size=20, fontweight="bold")
ax.set_xticks([])
ax.set_yticks(np.arange(nx - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax.axis("tight")
ax = plt.subplot(gs[0, 4:])
ax.imshow(xadj, cmap="rainbow", vmin=0, vmax=ny * nx)
ax.set_title("xadj", size=20, fontweight="bold")
ax.set_xticks(np.arange(nx - 1) + 0.5)
ax.set_yticks(np.arange(ny - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax.axis("tight")
plt.tight_layout()

Note that since the Sum operator creates and under-determined system of equations (data has always lower dimensionality than the model), an exact inverse is not possible for this operator.
Total running time of the script: ( 0 minutes 0.185 seconds)
Note
Click here to download the full example code
Symmetrize¶
This example shows how to use the pylops.Symmetrize
operator which takes an input signal and returns a symmetric signal
by pre-pending the input signal in reversed order. Such an operation can be
inverted as we will see in this example.
Moreover the pylops.Symmetrize
can be used as preconditioning
to any inverse problem where we are after inverting for a signal that we
want to ensure is symmetric. Refer to Wavelet estimation
for an example of such a type.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s start with a 1D example. Define an input signal composed of
nt
samples
nt = 10
x = np.arange(nt)
We can now create our flip operator and apply it to the input signal. We can also apply the adjoint to the flipped signal and we can see how for this operator the adjoint is effectively equivalent to the inverse.
Sop = pylops.Symmetrize(nt)
y = Sop * x
xadj = Sop.H * y
xinv = Sop / y
plt.figure(figsize=(7, 3))
plt.plot(x, "k", lw=3, label=r"$x$")
plt.plot(y, "r", lw=3, label=r"$y=Fx$")
plt.plot(xadj, "--g", lw=3, label=r"$x_{adj} = F^H y$")
plt.plot(xinv, "--m", lw=3, label=r"$x_{inv} = F^{-1} y$")
plt.title("Symmetrize in 1st direction", fontsize=14, fontweight="bold")
plt.legend()
plt.tight_layout()

Let’s now repeat the same exercise on a two dimensional signal. We will first flip the model along the first axis and then along the second axis
nt, nx = 10, 6
x = np.outer(np.arange(nt), np.ones(nx))
Sop = pylops.Symmetrize((nt, nx), axis=0)
y = Sop * x
xadj = Sop.H * y
xinv = Sop / y.ravel()
xinv = xinv.reshape(Sop.dims)
fig, axs = plt.subplots(1, 3, figsize=(7, 3))
fig.suptitle(
"Symmetrize in 2nd direction for 2d data", fontsize=14, fontweight="bold", y=0.95
)
axs[0].imshow(x, cmap="rainbow", vmin=0, vmax=9)
axs[0].set_title(r"$x$")
axs[0].axis("tight")
axs[1].imshow(y, cmap="rainbow", vmin=0, vmax=9)
axs[1].set_title(r"$y=Fx$")
axs[1].axis("tight")
axs[2].imshow(xinv, cmap="rainbow", vmin=0, vmax=9)
axs[2].set_title(r"$x_{adj}=F^{-1}y$")
axs[2].axis("tight")
plt.tight_layout()
plt.subplots_adjust(top=0.8)
x = np.outer(np.ones(nt), np.arange(nx))
Sop = pylops.Symmetrize((nt, nx), axis=1)
y = Sop * x
xadj = Sop.H * y
xinv = Sop / y.ravel()
xinv = xinv.reshape(Sop.dims)
# sphinx_gallery_thumbnail_number = 3
fig, axs = plt.subplots(1, 3, figsize=(7, 3))
fig.suptitle(
"Symmetrize in 2nd direction for 2d data", fontsize=14, fontweight="bold", y=0.95
)
axs[0].imshow(x, cmap="rainbow", vmin=0, vmax=9)
axs[0].set_title(r"$x$")
axs[0].axis("tight")
axs[1].imshow(y, cmap="rainbow", vmin=0, vmax=9)
axs[1].set_title(r"$y=Fx$")
axs[1].axis("tight")
axs[2].imshow(xinv, cmap="rainbow", vmin=0, vmax=9)
axs[2].set_title(r"$x_{adj}=F^{-1}y$")
axs[2].axis("tight")
plt.tight_layout()
plt.subplots_adjust(top=0.8)
Total running time of the script: ( 0 minutes 0.777 seconds)
Note
Click here to download the full example code
Synthetic seismic¶
This example shows how to use the pylops.utils.seismicevents
module
to quickly create synthetic seismic data to be used for toy examples and tests.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s first define the time and space axes as well as some auxiliary input parameters that we will use to create a Ricker wavelet
par = {
"ox": -200,
"dx": 2,
"nx": 201,
"oy": -100,
"dy": 2,
"ny": 101,
"ot": 0,
"dt": 0.004,
"nt": 501,
"f0": 20,
"nfmax": 210,
}
# Create axis
t, t2, x, y = pylops.utils.seismicevents.makeaxis(par)
# Create wavelet
wav = pylops.utils.wavelets.ricker(np.arange(41) * par["dt"], f0=par["f0"])[0]
We want to create a 2d data with a number of crossing linear events using the
pylops.utils.seismicevents.linear2d
routine.
v = 1500
t0 = [0.2, 0.7, 1.6]
theta = [40, 0, -60]
amp = [1.0, 0.6, -2.0]
mlin, mlinwav = pylops.utils.seismicevents.linear2d(x, t, v, t0, theta, amp, wav)
We can also create a 2d data with a number of crossing parabolic events using the
pylops.utils.seismicevents.parabolic2d
routine.
px = [0, 0, 0]
pxx = [1e-5, 5e-6, 1e-6]
mpar, mparwav = pylops.utils.seismicevents.parabolic2d(x, t, t0, px, pxx, amp, wav)
And similarly we can create a 2d data with a number of crossing hyperbolic
events using the pylops.utils.seismicevents.hyperbolic2d
routine.
vrms = [500, 700, 1700]
mhyp, mhypwav = pylops.utils.seismicevents.hyperbolic2d(x, t, t0, vrms, amp, wav)
We can now visualize the different events
# sphinx_gallery_thumbnail_number = 2
fig, axs = plt.subplots(1, 3, figsize=(9, 5))
axs[0].imshow(
mlinwav.T,
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[0].set_title("Linear events", fontsize=12, fontweight="bold")
axs[0].set_xlabel(r"$x(m)$")
axs[0].set_ylabel(r"$t(s)$")
axs[1].imshow(
mparwav.T,
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[1].set_title("Parabolic events", fontsize=12, fontweight="bold")
axs[1].set_xlabel(r"$x(m)$")
axs[1].set_ylabel(r"$t(s)$")
axs[2].imshow(
mhypwav.T,
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[2].set_title("Hyperbolic events", fontsize=12, fontweight="bold")
axs[2].set_xlabel(r"$x(m)$")
axs[2].set_ylabel(r"$t(s)$")
plt.tight_layout()

Let’s finally repeat the same exercise in 3d
phi = [20, 0, -10]
mlin, mlinwav = pylops.utils.seismicevents.linear3d(
x, y, t, v, t0, theta, phi, amp, wav
)
fig, axs = plt.subplots(1, 2, figsize=(7, 5), sharey=True)
fig.suptitle("Linear events in 3d", fontsize=12, fontweight="bold", y=0.95)
axs[0].imshow(
mlinwav[par["ny"] // 2].T,
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[0].set_xlabel(r"$x(m)$")
axs[0].set_ylabel(r"$t(s)$")
axs[1].imshow(
mlinwav[:, par["nx"] // 2].T,
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(y.min(), y.max(), t.max(), t.min()),
)
axs[1].set_xlabel(r"$y(m)$")
mhyp, mhypwav = pylops.utils.seismicevents.hyperbolic3d(
x, y, t, t0, vrms, vrms, amp, wav
)
fig, axs = plt.subplots(1, 2, figsize=(7, 5), sharey=True)
fig.suptitle("Hyperbolic events in 3d", fontsize=12, fontweight="bold", y=0.95)
axs[0].imshow(
mhypwav[par["ny"] // 2].T,
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(x.min(), x.max(), t.max(), t.min()),
)
axs[0].set_xlabel(r"$x(m)$")
axs[0].set_ylabel(r"$t(s)$")
axs[1].imshow(
mhypwav[:, par["nx"] // 2].T,
aspect="auto",
interpolation="nearest",
vmin=-2,
vmax=2,
cmap="gray",
extent=(y.min(), y.max(), t.max(), t.min()),
)
axs[1].set_xlabel(r"$y(m)$")
plt.tight_layout()
Total running time of the script: ( 0 minutes 2.052 seconds)
Note
Click here to download the full example code
Tapers¶
This example shows how to create some basic tapers in 1d, 2d, and 3d
using the pylops.utils.tapers
module.
import matplotlib.pyplot as plt
import pylops
plt.close("all")
Let’s first define the time and space axes
par = {
"ox": -200,
"dx": 2,
"nx": 201,
"oy": -100,
"dy": 2,
"ny": 101,
"ot": 0,
"dt": 0.004,
"nt": 501,
"ntapx": 21,
"ntapy": 31,
}
We can now create tapers in 1d
tap_han = pylops.utils.tapers.hanningtaper(par["nx"], par["ntapx"])
tap_cos = pylops.utils.tapers.cosinetaper(par["nx"], par["ntapx"], False)
tap_cos2 = pylops.utils.tapers.cosinetaper(par["nx"], par["ntapx"], True)
plt.figure(figsize=(7, 3))
plt.plot(tap_han, "r", label="hanning")
plt.plot(tap_cos, "k", label="cosine")
plt.plot(tap_cos2, "b", label="cosine square")
plt.title("Tapers")
plt.legend()
plt.tight_layout()

Similarly we can create 2d and 3d tapers with any of the tapers above
tap2d = pylops.utils.tapers.taper2d(par["nt"], par["nx"], par["ntapx"])
plt.figure(figsize=(7, 3))
plt.plot(tap2d[:, par["nt"] // 2], "k", lw=2)
plt.title("Taper")
plt.tight_layout()
tap3d = pylops.utils.tapers.taper3d(
par["nt"], (par["ny"], par["nx"]), (par["ntapy"], par["ntapx"])
)
plt.figure(figsize=(7, 3))
plt.imshow(tap3d[:, :, par["nt"] // 2], "jet")
plt.title("Taper in y-x slice")
plt.xlabel("x")
plt.ylabel("y")
plt.tight_layout()
Total running time of the script: ( 0 minutes 0.584 seconds)
Note
Click here to download the full example code
Total Variation (TV) Regularization¶
This set of examples shows how to add Total Variation (TV) regularization to an inverse problem in order to enforce blockiness in the reconstructed model.
To do so we will use the generalizated Split Bregman iterations by means of
pylops.optimization.sparsity.SplitBregman
solver.
The first example is concerned with denoising of a piece-wise step function which has been contaminated by noise. The forward model is:
meaning that we have an identity operator (\(\mathbf{I}\)) and inverting for \(\mathbf{x}\) from \(\mathbf{y}\) is impossible without adding prior information. We will enforce blockiness in the solution by adding a regularization term that enforces sparsity in the first derivative of the solution:
import matplotlib.pyplot as plt
# sphinx_gallery_thumbnail_number = 5
import numpy as np
import pylops
plt.close("all")
np.random.seed(1)
Let’s start by creating the model and data
nx = 101
x = np.zeros(nx)
x[: nx // 2] = 10
x[nx // 2 : 3 * nx // 4] = -5
Iop = pylops.Identity(nx)
n = np.random.normal(0, 1, nx)
y = Iop * (x + n)
plt.figure(figsize=(10, 5))
plt.plot(x, "k", lw=3, label="x")
plt.plot(y, ".k", label="y=x+n")
plt.legend()
plt.title("Model and data")
plt.tight_layout()

To start we will try to use a simple L2 regularization that enforces smoothness in the solution. We can see how denoising is succesfully achieved but the solution is much smoother than we wish for.
D2op = pylops.SecondDerivative(nx, edge=True)
lamda = 1e2
xinv = pylops.optimization.leastsquares.regularized_inversion(
Iop, y, [D2op], epsRs=[np.sqrt(lamda / 2)], **dict(iter_lim=30)
)[0]
plt.figure(figsize=(10, 5))
plt.plot(x, "k", lw=3, label="x")
plt.plot(y, ".k", label="y=x+n")
plt.plot(xinv, "r", lw=5, label="xinv")
plt.legend()
plt.title("L2 inversion")
plt.tight_layout()

Now we impose blockiness in the solution using the Split Bregman solver
Dop = pylops.FirstDerivative(nx, edge=True, kind="backward")
mu = 0.01
lamda = 0.3
niter_out = 50
niter_in = 3
xinv = pylops.optimization.sparsity.splitbregman(
Iop,
y,
[Dop],
niter_outer=niter_out,
niter_inner=niter_in,
mu=mu,
epsRL1s=[lamda],
tol=1e-4,
tau=1.0,
**dict(iter_lim=30, damp=1e-10)
)[0]
plt.figure(figsize=(10, 5))
plt.plot(x, "k", lw=3, label="x")
plt.plot(y, ".k", label="y=x+n")
plt.plot(xinv, "r", lw=5, label="xinv")
plt.legend()
plt.title("TV inversion")
plt.tight_layout()

Finally, we repeat the same exercise on a 2-dimensional image. In this case we mock a medical imaging problem: the data is created by appling a 2D Fourier Transform to the input model and by randomly sampling 60% of its values.
x = np.load("../testdata/optimization/shepp_logan_phantom.npy")
x = x / x.max()
ny, nx = x.shape
perc_subsampling = 0.6
nxsub = int(np.round(ny * nx * perc_subsampling))
iava = np.sort(np.random.permutation(np.arange(ny * nx))[:nxsub])
Rop = pylops.Restriction(ny * nx, iava, axis=0, dtype=np.complex128)
Fop = pylops.signalprocessing.FFT2D(dims=(ny, nx))
n = np.random.normal(0, 0.0, (ny, nx))
y = Rop * Fop * (x.ravel() + n.ravel())
yfft = Fop * (x.ravel() + n.ravel())
yfft = np.fft.fftshift(yfft.reshape(ny, nx))
ymask = Rop.mask(Fop * (x.ravel()) + n.ravel())
ymask = ymask.reshape(ny, nx)
ymask.data[:] = np.fft.fftshift(ymask.data)
ymask.mask[:] = np.fft.fftshift(ymask.mask)
fig, axs = plt.subplots(1, 3, figsize=(14, 5))
axs[0].imshow(x, vmin=0, vmax=1, cmap="gray")
axs[0].set_title("Model")
axs[0].axis("tight")
axs[1].imshow(np.abs(yfft), vmin=0, vmax=1, cmap="rainbow")
axs[1].set_title("Full data")
axs[1].axis("tight")
axs[2].imshow(np.abs(ymask), vmin=0, vmax=1, cmap="rainbow")
axs[2].set_title("Sampled data")
axs[2].axis("tight")
plt.tight_layout()

Let’s attempt now to reconstruct the model using the Split Bregman with anisotropic TV regularization (aka sum of L1 norms of the first derivatives over x and y):
Dop = [
pylops.FirstDerivative(
(ny, nx), axis=0, edge=False, kind="backward", dtype=np.complex128
),
pylops.FirstDerivative(
(ny, nx), axis=1, edge=False, kind="backward", dtype=np.complex128
),
]
# TV
mu = 1.5
lamda = [0.1, 0.1]
niter_out = 20
niter_in = 10
xinv = pylops.optimization.sparsity.splitbregman(
Rop * Fop,
y.ravel(),
Dop,
niter_outer=niter_out,
niter_inner=niter_in,
mu=mu,
epsRL1s=lamda,
tol=1e-4,
tau=1.0,
show=False,
**dict(iter_lim=5, damp=1e-4)
)[0]
xinv = np.real(xinv.reshape(ny, nx))
fig, axs = plt.subplots(1, 2, figsize=(9, 5))
axs[0].imshow(x, vmin=0, vmax=1, cmap="gray")
axs[0].set_title("Model")
axs[0].axis("tight")
axs[1].imshow(xinv, vmin=0, vmax=1, cmap="gray")
axs[1].set_title("TV Inversion")
axs[1].axis("tight")
plt.tight_layout()
fig, axs = plt.subplots(2, 1, figsize=(10, 5))
axs[0].plot(x[ny // 2], "k", lw=5, label="x")
axs[0].plot(xinv[ny // 2], "r", lw=3, label="xinv TV")
axs[0].set_title("Horizontal section")
axs[0].legend()
axs[1].plot(x[:, nx // 2], "k", lw=5, label="x")
axs[1].plot(xinv[:, nx // 2], "r", lw=3, label="xinv TV")
axs[1].set_title("Vertical section")
axs[1].legend()
plt.tight_layout()
Note that more optimized variations of the Split Bregman algorithm have been proposed in the literature for this specific problem, both improving the overall quality of the inversion and the speed of convergence.
In PyLops we however prefer to implement the generalized Split Bergman algorithm as this can used for any sort of problem where we wish to add any number of L1 and/or L2 regularization terms to the cost function to minimize.
Total running time of the script: ( 0 minutes 17.360 seconds)
Note
Click here to download the full example code
Transpose¶
This example shows how to use the pylops.Transpose
operator. For arrays that are 2-dimensional in nature this operator
simply transposes rows and columns. For multi-dimensional arrays, this
operator can be used to permute dimensions
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
np.random.seed(0)
Let’s start by creating a 2-dimensional array
dims = (20, 40)
x = np.arange(800).reshape(dims)
We use now the pylops.Transpose
operator to swap the two
dimensions. As you will see the adjoint of this operator brings the data
back to its original model, or in other words the adjoint operator is equal
in this case to the inverse operator.
Top = pylops.Transpose(dims=dims, axes=(1, 0))
y = Top * x
xadj = Top.H * y
fig, axs = plt.subplots(1, 3, figsize=(10, 4))
fig.suptitle("Transpose for 2d data", fontsize=14, fontweight="bold", y=1.15)
axs[0].imshow(x, cmap="rainbow", vmin=0, vmax=800)
axs[0].set_title(r"$x$")
axs[0].axis("tight")
axs[1].imshow(y, cmap="rainbow", vmin=0, vmax=800)
axs[1].set_title(r"$y = F x$")
axs[1].axis("tight")
axs[2].imshow(xadj, cmap="rainbow", vmin=0, vmax=800)
axs[2].set_title(r"$x_{adj} = F^H y$")
axs[2].axis("tight")
plt.tight_layout()

A similar approach can of course be taken two swap multiple axes of multi-dimensional arrays for any number of dimensions.
Total running time of the script: ( 0 minutes 0.327 seconds)
Note
Click here to download the full example code
Wavelet estimation¶
This example shows how to use the pylops.avo.prestack.PrestackWaveletModelling
to
estimate a wavelet from pre-stack seismic data. This problem can be written in mathematical
form as:
where \(\mathbf{G}\) is an operator that convolves an angle-variant reflectivity series with the wavelet \(\mathbf{w}\) that we aim to retrieve.
import matplotlib.pyplot as plt
import numpy as np
from scipy.signal import filtfilt
import pylops
from pylops.utils.wavelets import ricker
plt.close("all")
np.random.seed(0)
Let’s start by creating the input elastic property profiles and wavelet
nt0 = 501
dt0 = 0.004
ntheta = 21
t0 = np.arange(nt0) * dt0
thetamin, thetamax = 0, 40
theta = np.linspace(thetamin, thetamax, ntheta)
# Elastic property profiles
vp = (
2000
+ 5 * np.arange(nt0)
+ 2 * filtfilt(np.ones(5) / 5.0, 1, np.random.normal(0, 160, nt0))
)
vs = 600 + vp / 2 + 3 * filtfilt(np.ones(5) / 5.0, 1, np.random.normal(0, 100, nt0))
rho = 1000 + vp + filtfilt(np.ones(5) / 5.0, 1, np.random.normal(0, 120, nt0))
vp[201:] += 1500
vs[201:] += 500
rho[201:] += 100
# Wavelet
ntwav = 41
wavoff = 10
wav, twav, wavc = ricker(t0[: ntwav // 2 + 1], 20)
wav_phase = np.hstack((wav[wavoff:], np.zeros(wavoff)))
# vs/vp profile
vsvp = vs / vp
# Model
m = np.stack((np.log(vp), np.log(vs), np.log(rho)), axis=1)
fig, axs = plt.subplots(1, 3, figsize=(9, 7), sharey=True)
axs[0].plot(vp, t0, "k", lw=3)
axs[0].set(xlabel="[m/s]", ylabel=r"$t$ [s]", ylim=[t0[0], t0[-1]], title="Vp")
axs[0].grid()
axs[1].plot(vp / vs, t0, "k", lw=3)
axs[1].set(title="Vp/Vs")
axs[1].grid()
axs[2].plot(rho, t0, "k", lw=3)
axs[2].set(xlabel="[kg/m³]", title="Rho")
axs[2].invert_yaxis()
axs[2].grid()
plt.tight_layout()

We create now the operators to model a synthetic pre-stack seismic gather with a zero-phase as well as a mixed phase wavelet.
# Create operators
Wavesop = pylops.avo.prestack.PrestackWaveletModelling(
m, theta, nwav=ntwav, wavc=wavc, vsvp=vsvp, linearization="akirich"
)
Wavesop_phase = pylops.avo.prestack.PrestackWaveletModelling(
m, theta, nwav=ntwav, wavc=wavc, vsvp=vsvp, linearization="akirich"
)
Let’s apply those operators to the elastic model and create some synthetic data
d = (Wavesop * wav).reshape(ntheta, nt0).T
d_phase = (Wavesop_phase * wav_phase).reshape(ntheta, nt0).T
# add noise
dn = d + np.random.normal(0, 3e-2, d.shape)
fig, axs = plt.subplots(1, 3, figsize=(13, 7), sharey=True)
axs[0].imshow(
d, cmap="gray", extent=(theta[0], theta[-1], t0[-1], t0[0]), vmin=-0.1, vmax=0.1
)
axs[0].axis("tight")
axs[0].set(xlabel=r"$\theta$ [°]", ylabel=r"$t$ [s]")
axs[0].set_title("Data with zero-phase wavelet", fontsize=10)
axs[1].imshow(
d_phase,
cmap="gray",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=-0.1,
vmax=0.1,
)
axs[1].axis("tight")
axs[1].set_title("Data with non-zero-phase wavelet", fontsize=10)
axs[1].set_xlabel(r"$\theta$ [°]")
axs[2].imshow(
dn, cmap="gray", extent=(theta[0], theta[-1], t0[-1], t0[0]), vmin=-0.1, vmax=0.1
)
axs[2].axis("tight")
axs[2].set_title("Noisy Data with zero-phase wavelet", fontsize=10)
axs[2].set_xlabel(r"$\theta$ [°]")
plt.tight_layout()

We can invert the data. First we will consider noise-free data, subsequently we will add some noise and add a regularization terms in the inversion process to obtain a well-behaved wavelet also under noise conditions.
wav_est = Wavesop / d.T.ravel()
wav_phase_est = Wavesop_phase / d_phase.T.ravel()
wavn_est = Wavesop / dn.T.ravel()
# Create regularization operator
D2op = pylops.SecondDerivative(ntwav, dtype="float64")
# Invert for wavelet
(
wavn_reg_est,
istop,
itn,
r1norm,
r2norm,
) = pylops.optimization.leastsquares.regularized_inversion(
Wavesop,
dn.T.ravel(),
[D2op],
epsRs=[np.sqrt(0.1)],
**dict(damp=np.sqrt(1e-4), iter_lim=200, show=0)
)
As expected, the regularization helps to retrieve a smooth wavelet even under noisy conditions.
# sphinx_gallery_thumbnail_number = 3
fig, axs = plt.subplots(2, 1, sharex=True, figsize=(8, 6))
axs[0].plot(wav, "k", lw=6, label="True")
axs[0].plot(wav_est, "--r", lw=4, label="Estimated (noise-free)")
axs[0].plot(wavn_est, "--g", lw=4, label="Estimated (noisy)")
axs[0].plot(wavn_reg_est, "--m", lw=4, label="Estimated (noisy regularized)")
axs[0].set_title("Zero-phase wavelet")
axs[0].grid()
axs[0].legend(loc="upper right")
axs[0].axis("tight")
axs[1].plot(wav_phase, "k", lw=6, label="True")
axs[1].plot(wav_phase_est, "--r", lw=4, label="Estimated")
axs[1].set_title("Wavelet with phase")
axs[1].grid()
axs[1].legend(loc="upper right")
axs[1].axis("tight")
plt.tight_layout()

Finally we repeat the same exercise, but this time we use a preconditioner.
Initially, our preconditioner is a pylops.Symmetrize
operator
to ensure that our estimated wavelet is zero-phase. After we chain
the pylops.Symmetrize
and the pylops.Smoothing1D
operators to also guarantee a smooth wavelet.
# Create symmetrize operator
Sop = pylops.Symmetrize((ntwav + 1) // 2)
# Create smoothing operator
Smop = pylops.Smoothing1D(5, dims=((ntwav + 1) // 2,), dtype="float64")
# Invert for wavelet
wavn_prec_est = pylops.optimization.leastsquares.preconditioned_inversion(
Wavesop, dn.T.ravel(), Sop, **dict(damp=np.sqrt(1e-4), iter_lim=200, show=0)
)[0]
wavn_smooth_est = pylops.optimization.leastsquares.preconditioned_inversion(
Wavesop, dn.T.ravel(), Sop * Smop, **dict(damp=np.sqrt(1e-4), iter_lim=200, show=0)
)[0]
fig, ax = plt.subplots(1, 1, sharex=True, figsize=(8, 3))
ax.plot(wav, "k", lw=6, label="True")
ax.plot(wav_est, "--r", lw=4, label="Estimated (noise-free)")
ax.plot(wavn_prec_est, "--g", lw=4, label="Estimated (noisy symmetric)")
ax.plot(wavn_smooth_est, "--m", lw=4, label="Estimated (noisy smoothed)")
ax.set_title("Zero-phase wavelet")
ax.grid()
ax.legend(loc="upper right")
plt.tight_layout()

Total running time of the script: ( 0 minutes 1.937 seconds)
Note
Click here to download the full example code
Wavelet transform¶
This example shows how to use the pylops.DWT
and
pylops.DWT2D
operators to perform 1- and 2-dimensional DWT.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s start with a 1-dimensional signal. We apply the 1-dimensional wavelet transform, keep only the first 30 coefficients and perform the inverse transform.
nt = 200
dt = 0.004
t = np.arange(nt) * dt
freqs = [10, 7, 9]
amps = [1, -2, 0.5]
x = np.sum([amp * np.sin(2 * np.pi * f * t) for (f, amp) in zip(freqs, amps)], axis=0)
Wop = pylops.signalprocessing.DWT(nt, wavelet="dmey", level=5)
y = Wop * x
yf = y.copy()
yf[25:] = 0
xinv = Wop.H * yf
plt.figure(figsize=(8, 2))
plt.plot(y, "k", label="Full")
plt.plot(yf, "r", label="Extracted")
plt.title("Discrete Wavelet Transform")
plt.tight_layout()
plt.figure(figsize=(8, 2))
plt.plot(x, "k", label="Original")
plt.plot(xinv, "r", label="Reconstructed")
plt.title("Reconstructed signal")
plt.tight_layout()
We repeat the same procedure with an image. In this case the 2-dimensional DWT will be applied instead. Only a quarter of the coefficients of the DWT will be retained in this case.
im = np.load("../testdata/python.npy")[::5, ::5, 0]
Nz, Nx = im.shape
Wop = pylops.signalprocessing.DWT2D((Nz, Nx), wavelet="haar", level=5)
y = Wop * im
yf = y.copy()
yf.flat[y.size // 4 :] = 0
iminv = Wop.H * yf
fig, axs = plt.subplots(2, 2, figsize=(6, 6))
axs[0, 0].imshow(im, cmap="gray")
axs[0, 0].set_title("Image")
axs[0, 0].axis("tight")
axs[0, 1].imshow(y, cmap="gray_r", vmin=-1e2, vmax=1e2)
axs[0, 1].set_title("DWT2 coefficients")
axs[0, 1].axis("tight")
axs[1, 0].imshow(iminv, cmap="gray")
axs[1, 0].set_title("Reconstructed image")
axs[1, 0].axis("tight")
axs[1, 1].imshow(yf, cmap="gray_r", vmin=-1e2, vmax=1e2)
axs[1, 1].set_title("DWT2 coefficients (zeroed)")
axs[1, 1].axis("tight")
plt.tight_layout()

Total running time of the script: ( 0 minutes 0.656 seconds)
Note
Click here to download the full example code
Wavelets¶
This example shows how to use the different wavelets available PyLops.
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s start with defining a time axis and creating the FFT operator
dt = 0.004
nt = 1001
t = np.arange(nt) * dt
Fop = pylops.signalprocessing.FFT(2 * nt - 1, sampling=dt, real=True)
f = Fop.f
We can now create the different wavelets and display them
# Gaussian
wg, twg, wgc = pylops.utils.wavelets.gaussian(t, std=2)
# Gaussian
wk, twk, wgk = pylops.utils.wavelets.klauder(t, f=[4, 30], taper=np.hanning)
# Ormsby
wo, two, woc = pylops.utils.wavelets.ormsby(t, f=[5, 9, 25, 30], taper=np.hanning)
# Ricker
wr, twr, wrc = pylops.utils.wavelets.ricker(t, f0=17)
# Frequency domain
wgf = Fop @ wg
wkf = Fop @ wk
wof = Fop @ wo
wrf = Fop @ wr
fig, axs = plt.subplots(1, 2, figsize=(14, 6))
axs[0].plot(twg, wg, "k", lw=2, label="Gaussian")
axs[0].plot(twk, wk, "r", lw=2, label="Klauder")
axs[0].plot(two, wo, "b", lw=2, label="Ormsby")
axs[0].plot(twr, wr, "y--", lw=2, label="Ricker")
axs[0].set(xlim=(-0.4, 0.4), xlabel="Time [s]")
axs[0].legend()
axs[1].plot(f, np.abs(wgf) / np.abs(wgf).max(), "k", lw=2, label="Gaussian")
axs[1].plot(f, np.abs(wkf) / np.abs(wkf).max(), "r", lw=2, label="Klauder")
axs[1].plot(f, np.abs(wof) / np.abs(wof).max(), "b", lw=2, label="Ormsby")
axs[1].plot(f, np.abs(wrf) / np.abs(wrf).max(), "y--", lw=2, label="Ricker")
axs[1].set(xlim=(0, 50), xlabel="Frequency [Hz]")
axs[1].legend()
plt.tight_layout()

Total running time of the script: ( 0 minutes 0.307 seconds)
Note
Click here to download the full example code
Zero¶
This example shows how to use the pylops.basicoperators.Zero
operator.
This operators simply zeroes the data in forward mode and the model in adjoint mode.
import matplotlib.gridspec as pltgs
import matplotlib.pyplot as plt
import numpy as np
import pylops
plt.close("all")
Let’s define an zero operator \(\mathbf{0}\) with same number of elements for data \(N\) and model \(M\).
N, M = 5, 5
x = np.arange(M)
Zop = pylops.basicoperators.Zero(M, dtype="int")
y = Zop * x
xadj = Zop.H * y
gs = pltgs.GridSpec(1, 6)
fig = plt.figure(figsize=(7, 4))
ax = plt.subplot(gs[0, 0:3])
ax.imshow(np.zeros((N, N)), cmap="rainbow", vmin=-M, vmax=M)
ax.set_title("A", size=20, fontweight="bold")
ax.set_xticks(np.arange(N - 1) + 0.5)
ax.set_yticks(np.arange(M - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax = plt.subplot(gs[0, 3])
im = ax.imshow(x[:, np.newaxis], cmap="rainbow", vmin=-M, vmax=M)
ax.set_title("x", size=20, fontweight="bold")
ax.set_xticks([])
ax.set_yticks(np.arange(M - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax = plt.subplot(gs[0, 4])
ax.text(
0.35,
0.5,
"=",
horizontalalignment="center",
verticalalignment="center",
size=40,
fontweight="bold",
)
ax.axis("off")
ax = plt.subplot(gs[0, 5])
ax.imshow(y[:, np.newaxis], cmap="rainbow", vmin=-M, vmax=M)
ax.set_title("y", size=20, fontweight="bold")
ax.set_xticks([])
ax.set_yticks(np.arange(N - 1) + 0.5)
ax.grid(linewidth=3, color="white")
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
fig.colorbar(im, ax=ax, ticks=[0], pad=0.3, shrink=0.7)
plt.tight_layout()

Similarly we can consider the case with data bigger than model
N, M = 10, 5
x = np.arange(M)
Zop = pylops.Zero(N, M, dtype="int")
y = Zop * x
xadj = Zop.H * y
print(f"x = {x}")
print(f"0*x = {y}")
print(f"0'*y = {xadj}")
x = [0 1 2 3 4]
0*x = [0 0 0 0 0 0 0 0 0 0]
0'*y = [0 0 0 0 0]
and model bigger than data
N, M = 5, 10
x = np.arange(M)
Zop = pylops.Zero(N, M, dtype="int")
y = Zop * x
xadj = Zop.H * y
print(f"x = {x}")
print(f"0*x = {y}")
print(f"0'*y = {xadj}")
x = [0 1 2 3 4 5 6 7 8 9]
0*x = [0 0 0 0 0]
0'*y = [0 0 0 0 0 0 0 0 0 0]
Note that this operator can be useful in many real-life applications when for example we want to manipulate a subset of the model array and keep intact the rest of the array. For example:
\[\begin{split}\begin{bmatrix} \mathbf{A} \quad \mathbf{0} \end{bmatrix} \begin{bmatrix} \mathbf{x_1} \\ \mathbf{x_2} \end{bmatrix} = \mathbf{A} \mathbf{x_1}\end{split}\]
Refer to the tutorial on Optimization for more details on this.
Total running time of the script: ( 0 minutes 0.209 seconds)
Frequenty Asked Questions¶
1. Can I visualize my operator?
Yes, you can. Every operator has a method called todense
that will return the dense matrix equivalent of
the operator. Note, however, that in order to do so we need to allocate a numpy
array of the size of your
operator and apply the operator N
times, where N
is the number of columns of the operator. The allocation can
be very heavy on your memory and the computation may take long time, so use it with care only for small toy
examples to understand what your operator looks like. This method should however not be abused, as the reason of
working with linear operators is indeed that you don’t really need to access the explicit matrix representation
of an operator.
2. Can I have an older version of cupy
or cusignal
installed in my system ( cupy-cudaXX<8.1.0
or cusignal>=0.16.0
)?
Yes. Nevertheless you need to tell PyLops that you don’t want to use its cupy
backend by setting the environment variable CUPY_PYLOPS=0
or CUPY_SIGNAL=0
.
Failing to do so will lead to an error when you import pylops
because some of the cupyx
routines that we use are not available in earlier version of cupy
.
PyLops API¶
The Application Programming Interface (API) of PyLops can be loosely seen as composed of a stack of three main layers:
Linear operators: building blocks for the setting up of inverse problems
Solvers: interfaces to a variety of solvers, providing an easy way to augment an inverse problem with additional regularization and/or preconditioning term
Applications: high-level interfaces allowing users to easily setup and solve specific problems (while hiding the non-needed details - i.e., creation and setup of linear operators and solvers).
Linear operators¶
Templates¶
|
Common interface for performing matrix-vector products. |
|
Function Operator. |
|
Memoize Operator. |
|
Wrap a PyLops operator into a Torch function. |
Basic operators¶
|
Matrix multiplication. |
|
Identity operator. |
|
Zero operator. |
|
Diagonal operator. |
|
Transpose operator. |
|
Flip along an axis. |
|
Roll along an axis. |
|
Pad operator. |
|
Sum operator. |
|
Symmetrize along an axis. |
|
Restriction (or sampling) operator. |
|
Polynomial regression. |
|
Linear regression. |
|
Causal integration. |
|
Spread operator. |
|
Vertical stacking. |
|
Horizontal stacking. |
|
Block operator. |
|
Block-diagonal operator. |
|
Kronecker operator. |
|
Real operator. |
|
Imag operator. |
|
Complex conjugate operator. |
Smoothing and derivatives¶
|
1D Smoothing. |
|
2D Smoothing. |
|
First derivative. |
|
Second derivative. |
|
Laplacian. |
|
Gradient. |
|
First Directional derivative. |
|
Second Directional derivative. |
Signal processing¶
|
1D convolution operator. |
|
2D convolution operator. |
|
ND convolution operator. |
|
Interpolation operator. |
|
Bilinear interpolation operator. |
|
One dimensional Fast-Fourier Transform. |
|
Two dimensional Fast-Fourier Transform. |
|
N-dimensional Fast-Fourier Transform. |
|
Shift operator |
|
One dimensional Wavelet operator. |
|
Two dimensional Wavelet operator. |
|
Two dimensional Seislet operator. |
|
Two dimensional Radon transform. |
|
Three dimensional Radon transform. |
|
2D Chirp Radon transform |
|
3D Chirp Radon transform |
|
1D Sliding transform operator. |
|
2D Sliding transform operator. |
|
3D Sliding transform operator.w |
|
2D Patch transform operator. |
|
3D Patch transform operator. |
|
Fredholm integral of first kind. |
Wave-Equation processing¶
|
Pressure to Vertical velocity conversion. |
|
2D Up-down wavefield composition. |
|
3D Up-down wavefield composition. |
|
Multi-dimensional convolution. |
|
Phase shift operator |
|
Kirchhoff Demigration operator. |
|
Devito Acoustic propagator. |
Geophysical subsurface characterization¶
|
AVO Linearized modelling. |
|
Post-stack linearized seismic modelling operator. |
|
Pre-stack linearized seismic modelling operator. |
|
Pre-stack linearized seismic modelling operator for wavelet. |
Solvers¶
Template¶
|
This is a template class which a user must subclass when implementing a new solver. |
Basic¶
|
Conjugate gradient |
|
Conjugate gradient least squares |
|
Solve an overdetermined system of equations given an operator |
|
Conjugate gradient |
|
Conjugate gradient least squares |
|
LSQR |
Least-squares¶
|
Inversion of normal equations. |
|
Regularized inversion. |
|
Preconditioned inversion. |
|
Inversion of normal equations. |
|
Regularized inversion. |
|
Preconditioned inversion. |
Sparsity¶
|
Iteratively reweighted least squares. |
|
Orthogonal Matching Pursuit (OMP). |
|
Iterative Shrinkage-Thresholding Algorithm (ISTA). |
|
Fast Iterative Shrinkage-Thresholding Algorithm (FISTA). |
|
Spectral Projected-Gradient for L1 norm. |
|
Split Bregman for mixed L2-L1 norms. |
|
Iteratively reweighted least squares. |
|
Orthogonal Matching Pursuit (OMP). |
|
Iterative Shrinkage-Thresholding Algorithm (ISTA). |
|
Fast Iterative Shrinkage-Thresholding Algorithm (FISTA). |
|
Spectral Projected-Gradient for L1 norm. |
|
Split Bregman for mixed L2-L1 norms. |
Callbacks¶
This is a template class which a user must subclass when implementing callbacks for a solver. |
|
|
Metrics callback |
Applications¶
Wave-Equation processing¶
|
Seismic interpolation (or regularization). |
|
Wavefield deghosting. |
|
Up-down wavefield decomposition. |
|
Multi-dimensional deconvolution. |
|
Marchenko redatuming |
|
Least-squares Migration (LSM). |
Geophysical subsurface characterization¶
|
Post-stack linearized seismic inversion. |
|
Pre-stack linearized seismic inversion. |
PyLops Utilities¶
Alongside with its Linear Operators and Solvers, PyLops contains also a number of auxiliary routines performing universal tasks that are used by several operators or simply within one or more Tutorials for the preparation of input data and subsequent visualization of results.
Dot-test¶
|
Dot test. |
Decorators¶
Decorator which converts a solver-type function that only supports a 1d-array into one that supports one (dimsd-shaped) ndarray. |
|
Decorator which disables ndarray multiplication. |
|
|
Decorator for the common reshape/flatten pattern used in many operators. |
Describe¶
|
Describe a PyLops operator |
Estimators¶
|
Trace of linear operator using the Hutchinson method. |
|
Trace of linear operator using the Hutch++ method. |
|
Trace of linear operator using the NA-Hutch++ method. |
Metrics¶
|
Mean Absolute Error (MAE) |
|
Mean Square Error (MSE) |
|
Signal to Noise Ratio (SNR) |
|
Peak Signal to Noise Ratio (PSNR) |
Geophysical Reservoir characterization¶
|
Zoeppritz solution. |
|
Single element of Zoeppritz solution. |
|
PP reflection coefficient from the Zoeppritz scattering matrix. |
|
PP reflection coefficient from the approximate Zoeppritz equation. |
|
Three terms Aki-Richards approximation. |
|
Three terms Fatti approximation. |
|
PS reflection coefficient |
Scalability test¶
|
Scalability test. |
Sliding and Patching¶
|
Design Sliding1D operator |
|
Design Sliding2D operator |
|
Design Sliding3D operator |
|
Design Patch2D operator |
|
Design Patch3D operator |
Synthetics¶
Create axes t, x, and y axes |
|
|
Linear 2D events |
|
Parabolic 2D events |
|
Hyperbolic 2D events |
|
Linear 3D events |
|
Hyperbolic 3D events |
|
Analytical direct wave in acoustic media |
Signal-processing¶
|
Convolution matrix |
Convolution matrix from a bank of filters |
|
|
Local dip estimation |
|
Local slope estimation |
Tapers¶
|
2D taper |
|
3D taper |
|
ND taper |
Wavelets¶
|
Gaussian wavelet |
|
Klauder wavelet |
|
Ormsby wavelet |
|
Ricker wavelet |
Implementing new operators¶
Users are welcome to create new operators and add them to the PyLops library.
In this tutorial, we will go through the key steps in the definition of an operator, using the
pylops.Diagonal
as an example. This is a very simple operator that applies a diagonal matrix to the model
in forward mode and to the data in adjoint mode.
Creating the operator¶
The first thing we need to do is to create a new file with the name of the operator we would like to implement. Note that as the operator will be a class, we need to follow the UpperCaseCamelCase convention both for the class itself and for the filename.
Once we have created the file, we will start by importing the modules that will be needed by the operator.
While this varies from operator to operator, you will always need to import the pylops.LinearOperator
class,
which will be used as parent class for any of our operators:
from pylops import LinearOperator
This class is a child of the
scipy.sparse.linalg.LinearOperator
class itself which implements the same methods of its parent class
as well as an additional method for quick inversion: such method can be easily accessed by using \
between the
operator and the data (e.g., A \ y
).
After that we define our new object:
class Diagonal(LinearOperator):
followed by a numpydoc docstring
(starting with r"""
and ending with """
) containing the documentation of the operator. Such docstring should
contain at least a short description of the operator, a Parameters
section with a detailed description of the
input parameters and a Notes
section providing a mathematical explanation of the operator. Take a look at
some of the core operators of PyLops to get a feeling of the level of details of the mathematical explanation.
Initialization (__init__
)¶
We then need to create the __init__
where the input parameters are passed and saved as members of our class.
While the input parameters change from operator to operator, it is always required to create three members, the first
called shape
with a tuple containing the dimensions of the operator in the data and model space, the second
called dtype
with the data type object (np.dtype
) of the model and data, and the third
called explicit
with a boolean (True
or False
) identifying if the operator can be inverted by a direct
solver or requires an iterative solver. This member is True
if the operator has also a member A
that contains
the matrix to be inverted like for example in the pylops.MatrixMult
operator, and it will be False
otherwise.
In this case we have another member called d
which is equal to the input vector containing the diagonal elements
of the matrix we want to multiply to the model and data.
def __init__(self, d, dtype=None):
self.d = d.ravel()
self.shape = (len(self.d), len(self.d))
self.dtype = np.dtype(dtype)
self.explicit = False
Alternatively, since version 2.0.0, the recommended way of initializing operators derived from the base
pylops.LinearOperator
class is to invoke super
to assign the required attributes:
def __init__(self, d, dtype=None):
self.d = d.ravel()
super().__init__(dtype=np.dtype(dtype), shape=(len(self.d), len(self.d)))
In this case, there is no need to declare explicit
as it already defaults to False
.
Since version 2.0.0, every pylops.LinearOperator
class is imbued with dims
,
dimsd
, clinear
and explicit
, in addition to the required dtype
and shape
.
See the docs of pylops.LinearOperator
for more information about what these
attributes mean.
Forward mode (_matvec
)¶
We can then move onto writing the forward mode in the method _matvec
. In other words, we will need to write
the piece of code that will implement the following operation \(\mathbf{y} = \mathbf{A}\mathbf{x}\).
Such method is always composed of two inputs (the object itself self
and the input model x
).
In our case the code to be added to the forward is very simple, we will just need to apply element-wise multiplication
between the model \(\mathbf{x}\) and the elements along the diagonal contained in the array \(\mathbf{d}\).
We will finally need to return
the result of this operation:
def _matvec(self, x):
return self.d * x
Adjoint mode (_rmatvec
)¶
Finally we need to implement the adjoint mode in the method _rmatvec
. In other words, we will need to write
the piece of code that will implement the following operation \(\mathbf{x} = \mathbf{A}^H\mathbf{y}\).
Such method is also composed of two inputs (the object itself self
and the input data y
).
In our case the code to be added to the forward is the same as the one from the forward (but this will obviously be
different from operator to operator):
def _rmatvec(self, x):
return self.d * x
And that’s it, we have implemented our first linear operator!
Testing the operator¶
Being able to write an operator is not yet a guarantee of the fact that the operator is correct, or in other words that the adjoint code is actually the adjoint of the forward code. Luckily for us, a simple test can be performed to check the validity of forward and adjoint operators, the so called dot-test.
We can generate random vectors \(\mathbf{u}\) and \(\mathbf{v}\) and verify the the following equality within a numerical tolerance:
The method pylops.utils.dottest
implements such a test for you, all you need to do is create a new test
within an existing test_*.py
file in the pytests
folder (or in a new file).
Generally a test file will start with a number of dictionaries containing different parameters we would like to use in the testing of one or more operators. The test itself starts with a decorator that contains a list of all (or some) of dictionaries that will would like to use for our specific operator, followed by the definition of the test
@pytest.mark.parametrize("par", [(par1),(par2)])
def test_Diagonal(par):
At this point we can first of all create the operator and run the pylops.utils.dottest
preceded by the
assert
command. Moreover, the forward and adjoint methods should tested towards expected outputs or even
better, when the operator allows it (i.e., operator is invertible), a small inversion should be run and the inverted
model tested towards the input model.
"""Dot-test and inversion for diagonal operator
"""
d = np.arange(par['nx']) + 1.
Dop = Diagonal(d)
assert dottest(Dop, par['nx'], par['nx'],
complexflag=0 if par['imag'] == 1 else 3)
x = np.ones(par['nx'])
xlsqr = lsqr(Dop, Dop * x, damp=1e-20, iter_lim=300, show=0)[0]
assert_array_almost_equal(x, xlsqr, decimal=4)
Documenting the operator¶
Once the operator has been created, we can add it to the documentation of PyLops. To do so, simply add the name of
the operator within the index.rst
file in docs/source/api
directory.
Moreover, in order to facilitate the user of your operator by other users, a simple example should be provided as part of the
Sphinx-gallery of the documentation of the PyLops library. The directory examples
contains several scripts that
can be used as template.
Final checklist¶
Before submitting your new operator for review, use the following checklist to ensure that your code adheres to the guidelines of PyLops:
you have created a new file containing a single class (or a function when the new operator is a simple combination of existing operators - see
pylops.Laplacian
for an example of such operator) and added to a new or existing directory within thepylops
package.the new class contains at least
__init__
,_matvec
and_matvec
methods.the new class (or function) has a numpydoc docstring documenting at least the input
Parameters
and with aNotes
section providing a mathematical explanation of the operatora new test has been added to an existing
test_*.py
file within thepytests
folder. The test should verify that the new operator passes thepylops.utils.dottest
. Moreover it is advisable to create a small toy example where the operator is applied in forward mode and the resulting data is inverted using\
frompylops.LinearOperator
.the new operator is used within at least one example (in
examples
directory) or one tutorial (intutorials
directory).
Implementing new solvers¶
Users are welcome to create new solvers and add them to the PyLops library.
In this tutorial, we will go through the key steps in the definition of a solver, using the
pylops.optimization.basic.CG
as an example.
Note
In case the solver that you are planning to create falls within the category of proximal solvers, we encourage to consider adding it to the PyProximal project.
Creating the solver¶
The first thing we need to do is to locate a file containing solvers in the same family of the solver we plan to include, or create a new file with the name of the solver we would like to implement (or preferably its family). Note that as the solver will be a class, we need to follow the UpperCaseCamelCase convention for the class itself but not for the filename.
At this point we can start by importing the modules that will be needed by the solver.
This varies from solver to solver, however you will always need to import the
pylops.optimization.basesolver.Solver
which will be used as parent class for any of our solvers.
Moreover, we always recommend to import pylops.utils.backend.get_array_module
as solvers should be written
in such a way that it can work both with numpy
and cupy
arrays. See later for details.
import time
import numpy as np
from pylops.optimization.basesolver import Solver
from pylops.utils.backend import get_array_module
After that we define our new object:
class CG(Solver):
followed by a numpydoc docstring
(starting with r"""
and ending with """
) containing the documentation of the solver. Such docstring should
contain at least a short description of the solver, a Parameters
section with a description of the
input parameters of the associated _init__
method and a Notes
section providing a reference to the original
solver and possibly a concise mathematical explanation of the solver. Take a look at some of the core solver of PyLops
to get a feeling of the level of details of the mathematical explanation.
As for any Python class, our solver will need an __init__
method. In this case, however, we will just rely on that
of the base class. Two input parameters are passed to the __init__
method and saved as members of our class,
namely the operator \(\mathbf{Op}\) associated with the system of equations we wish to solve,
\(\mathbf{y}=\mathbf{Op}\,\mathbf{x}\), and optionally a pylops.optimization.callback.Callbacks
object. Moreover,
an additional parameters is created that contains the current time (this is used later to report the execution time
of the solver). Here is the __init__
method of the base class:
def __init__(self, Op, callbacks=None):
self.Op = Op
self.callbacks = callbacks
self._registercallbacks()
self.tstart = time.time()
We can now move onto writing the setup of the solver in the method setup
. We will need to write
a piece of code that prepares the solver prior to being able to apply a step. In general, this requires defining the
data vector y
and the initial guess of the solver x0
(if not provided, this will be automatically set to be a zero
vector), alongside various hyperparameters of the solver — e.g., those involved in the stopping criterion. For example in
this case we only have two parameters: niter
refers to the maximum allowed number of iterations, and tol
to
tolerance on the residual norm (the solver will be stopped if this is smaller than the chosen tolerance). Moreover,
we always have the possibility to decide whether we want to operate the solver (in this case its setup part) in verbose
or silent mode. This is driven by the show
parameter. We will soon discuss how to choose what to print on screen in
case of verbose mode (show=True
).
The setup method can be loosely seen as composed of three parts. First, the data
vector and hyperparameters are stored as members of the class. Moreover the type of the y
vector is checked to
evaluate whether to use numpy
or cupy
for algebraic operations (this is done by self.ncp = get_array_module(y)
).
Second, the starting guess is initialized using either the provided vector x0
or a zero vector. Finally, a number
of variables are initialized to be used inside the step
method to keep track of the optimization process. Moreover,
note that the setup
method returns the created starting guess x
(does not store it as member of the class).
def setup(self, y, x0=None, niter=None, tol=1e-4, show=False):
self.y = y
self.tol = tol
self.niter = niter
self.ncp = get_array_module(y)
# initialize solver
if x0 is None:
x = self.ncp.zeros(self.Op.shape[1], dtype=self.y.dtype)
self.r = self.y.copy()
else:
x = x0.copy()
self.r = self.y - self.Op.matvec(x)
self.c = self.r.copy()
self.kold = self.ncp.abs(self.r.dot(self.r.conj()))
# create variables to track the residual norm and iterations
self.cost = []
self.cost.append(np.sqrt(self.kold))
self.iiter = 0
# print setup
if show:
self._print_setup(np.iscomplexobj(x))
return x
At this point, we need to implement the core of the solver, the step
method. Here, we take the input at the previous iterate,
update it following the rule of the solver of choice, and return it. The other input parameter required by this method
is show
to choose whether we want to print a report of the step on screen or not. However, if appropriate, a user
can add additional input parameters. For CG, the step is:
def step(self, x, show=False):
Opc = self.Op.matvec(self.c)
cOpc = self.ncp.abs(self.c.dot(Opc.conj()))
a = self.kold / cOpc
x += a * self.c
self.r -= a * Opc
k = self.ncp.abs(self.r.dot(self.r.conj()))
b = k / self.kold
self.c = self.r + b * self.c
self.kold = k
self.iiter += 1
self.cost.append(np.sqrt(self.kold))
if show:
self._print_step(x)
return x
Similarly, we also implement a run
method that is in charge of running a number of iterations by repeatedly
calling the step
method. It is also usually convenient to implement a finalize method; this method can do any required post-processing that should
not be applied at the end of each step, rather at the end of the entire optimization process. For CG, this is as simple
as converting the cost
variable from a list to a numpy
array. For more details, see our implementations for CG.
Last but not least, we can wrap it all up in the solve
method. This method takes as input the data, the initial
model and the same hyperparameters of the setup method and runs the entire optimization process. For CG:
def solve(self, y, x0=None, niter=10, tol=1e-4, show=False, itershow=[10, 10, 10]):
x = self.setup(y=y, x0=x0, niter=niter, tol=tol, show=show)
x = self.run(x, niter, show=show, itershow=itershow)
self.finalize(show)
return x, self.iiter, self.cost
And that’s it, we have implemented our first solver operator!
Although the methods that we just described are enough to implement any solver of choice, we find important to provide
users with feedback during the inversion process. Imagine that the modelling operator is very expensive and can take
minutes (or even hours to run), we don’t want to leave a user waiting for hours before they can tell if the solver has
done something meaningful. To avoid such scenario, we can implement so called _print_* methods where
*=solver, setup, step, finalize
that print on screen some useful information (e.g., first value of the current
estimate, norm of residual, etc.). The solver
and finalize
print are already implemented in the base class,
the other two must be implemented when creating a new solver. When these methods are implemented and a user passes
show=True
to the associated method, our solver will provide such information on screen throughout the inverse
process. To better understand how to write such methods, we suggest to look into the source code of the CG method.
Finally, to be backward compatible with versions of PyLops <v2.0.0, we also want to create a function with the same name of the class-based solver (but in small letters) which simply instantiates the solver and runs it. This function is usually placed in the same file of the class-based solver and snake_case should be used for its name. This function generally takes all the mandatory and optional parameters of the solver as input and returns some of the most valuable properties of the class-based solver object. An example for CG is:
def cg(Op, y, x0, niter=10, tol=1e-4, show=False, itershow=[10, 10, 10], callback=None):
cgsolve = CG(Op)
if callback is not None:
cgsolve.callback = callback
x, iiter, cost = cgsolve.solve(
y=y, x0=x0, tol=tol, niter=niter, show=show, itershow=itershow
)
return x, iiter, cost
Testing the solver¶
Being able to write a solver is not yet a guarantee of the fact that the solver is correct, or in other words that the solver can converge to a correct solution (at least in the case of full rank operator).
We encourage to create a new test within an existing test_*.py
file in the pytests
folder (or in a new file).
We also encourage to test the function-bases solver, as this will implicitly test the underlying class-based solver.
Generally a test file will start with a number of dictionaries containing different parameters we would like to use in the testing of one or more solvers. The test itself starts with a decorator that contains a list of all (or some) of dictionaries that will would like to use for our specific operator, followed by the definition of the test:
@pytest.mark.parametrize("par", [(par1),(par2)])
def test_CG(par):
At this point we can first create a full-rank operator, an input vector and compute the associated data. We can then run the solver for a certain number of iterations, checking that the solution agrees with the true x within a certain tolerance:
"""CG with linear operator
"""
np.random.seed(10)
A = np.random.normal(0, 10, (par["ny"], par["nx"]))
A = np.conj(A).T @ A # to ensure definite positive matrix
Aop = MatrixMult(A, dtype=par["dtype"])
x = np.ones(par["nx"])
x0 = np.random.normal(0, 10, par["nx"])
y = Aop * x
xinv = cg(Aop, y, x0=x0, niter=par["nx"], tol=1e-5, show=True)[0]
assert_array_almost_equal(x, xinv, decimal=4)
Documenting the solver¶
Once the solver has been created, we can add it to the documentation of PyLops. To do so, simply add the name of
the operator within the index.rst
file in docs/source/api
directory.
Moreover, in order to facilitate the user of your operator by other users, a simple example should be provided as part of the
Sphinx-gallery of the documentation of the PyLops library. The directory examples
contains several scripts that
can be used as template.
Final checklist¶
Before submitting your new solver for review, use the following checklist to ensure that your code adheres to the guidelines of PyLops:
you have added the new solver to a new or existing file in the
optimization
directory within thepylops
package.the new class contains at least
__init__
,setup
,step
,run
,finalize
, andsolve
methods.each of the above methods have a numpydoc docstring documenting at least the input
Parameters
and the__init__
method contains also aNotes
section providing a mathematical explanation of the solver.a new test has been added to an existing
test_*.py
file within thepytests
folder. The test should verify that the new solver converges to the true solution for a well-designed inverse problem (i.e., full rank operator).the new solver is used within at least one example (in
examples
directory) or one tutorial (intutorials
directory).
Contributing¶
Contributions are welcome and greatly appreciated!
The best way to get in touch with the core developers and maintainers is to join the PyLops slack channel as well as open new Issues directly from the GitHub repo.
Moreover, take a look at the Roadmap page for a list of current ideas for improvements and additions to the PyLops library.
Welcomed contributions¶
Bug reports¶
Report bugs at https://github.com/PyLops/pylops/issues.
If you are playing with the PyLops library and find a bug, please report it including:
Your operating system name and version.
Any details about your Python environment.
Detailed steps to reproduce the bug.
New operators and features¶
Open an issue at https://github.com/PyLops/pylops/issues with tag enhancement.
If you are proposing a new operator or a new feature:
Explain in detail how it should work.
Keep the scope as narrow as possible, to make it easier to implement.
Fix issues¶
There is always a backlog of issues that need to be dealt with. Look through the GitHub Issues for operator/feature requests or bugfixes.
Add examples or improve documentation¶
Writing new operators is not the only way to get involved and contribute. Create examples with existing operators as well as improving the documentation of existing operators is as important as making new operators and very much encouraged.
Step-by-step instructions for contributing¶
Ready to contribute?
Follow all instructions in Step-by-step installation for developers.
Create a branch for local development, usually starting from the dev branch:
>> git checkout -b name-of-your-branch dev
Now you can make your changes locally.
3. When you’re done making changes, check that your code follows the guidelines for Implementing new operators and that the both old and new tests pass successfully:
>> make tests
Run flake8 to check the quality of your code:
>> make lint
Note that PyLops does not enforce full compliance with flake8, rather this is used as a guideline and will also be run as part of our CI. Make sure to limit to a minimum flake8 warnings before making a PR.
Update the docs
>> make docupdate
Commit your changes and push your branch to GitHub:
>> git add .
>> git commit -m "Your detailed description of your changes."
>> git push origin name-of-your-branch
Remember to add -u
when pushing the branch for the first time.
We recommend using Conventional Commits to
format your commit messages, but this is not enforced.
Submit a pull request through the GitHub website.
Pull Request Guidelines¶
Before you submit a pull request, check that it meets these guidelines:
The pull request should include new tests for all the core routines that have been developed.
If the pull request adds functionality, the docs should be updated accordingly.
Ensure that the updated code passes all tests.
Project structure¶
This repository is organized as follows: * pylops: Python library containing various linear operators and auxiliary routines * pytests: set of pytests * testdata: sample datasets used in pytests and documentation * docs: Sphinx documentation * examples: set of python script examples for each linear operator to be embedded in documentation using sphinx-gallery * tutorials: set of python script tutorials to be embedded in documentation using sphinx-gallery
Changelog¶
Version 2.0.0¶
Released on: 12/08/2022
PyLops has undergone significant changes in this release, including new LinearOperator
s, more features, new examples and bugfixes.
To aid users in navigating the breaking changes, we provide the following document
MIGRATION_V1_V2.md.
New Features
Multiplication of linear operators by N-dimensional arrays is now supported via the new
dims
/dimsd
properties. Users do not need to use.ravel
and.reshape
as often anymore. See the migration guide for more information.Typing annotations for several submodules (
avo
,basicoperators
,signalprocessing
,utils
,optimization
,waveeqprocessing
)New
pylops.TorchOperator
wraps a Pylops operator into a PyTorch functionNew
pylops.signalprocessing.Patch3D
applies a linear operator repeatedly to patches of the model vectorEach of
pylops.signalprocessing.Sliding1D
,pylops.signalprocessing.Sliding2D
,pylops.signalprocessing.Sliding3D
,pylops.signalprocessing.Patch2D
andpylops.signalprocessing.Patch3D
have an associatedslidingXd_design
orpatchXd_design
functions associated with them to aid the user in designing the windowspylops.FirstDerivative
andpylops.SecondDerivative
, and therefore other derivative operators which rely on the (e.g.,pylops.Gradient
) support higher order stencilspylops.waveeqprocessing.Kirchhoff
substitutespylops.waveeqprocessing.Demigration
and incorporates a variety of new functionalitiesNew
pylops.waveeqprocessing.AcousticWave2D
wraps the Devito acoutic wave propagator providing a wave-equation based Born modeling operator with a reverse-time migration adjointSolvers can now be implemented via the
pylops.optimization.basesolver.Solver
class. They can now be used through a functional interface with lowercase name (e.g.,pylops.optimization.sparsity.splitbregman
) or via class interface with CamelCase name (e.g.,pylops.optimization.cls_sparsity.SplitBregman
. Moreover, solvers now accept callbacks defined by thepylops.optimization.callback.Callbacks
interface (see e.g.,pylops.optimization.callback.MetricsCallback
).Metrics such as
pylops.utils.metrics.mae
andpylops.utils.metrics.mse
and othersNew
pylops.utils.signalprocessing.dip_estimate
estimates local dips in an image (measured in radians) in a stabler way than the oldpylops.utils.signalprocessing.dip_estimate
did for slopes.New
pylops.utils.tapers.tapernd
for N-dimensional tapersNew wavelets
pylops.utils.wavelets.klauder
andpylops.utils.wavelets.ormsby
Documentation
Installation has been revamped
Revamped guide on how to implement a new LinearOperator from scratch
New guide on how to implement a new solver from scratch
New tutorials:
New gallery examples:
Version 1.18.3¶
Released on: 30/07/2022
Refractored
pylops.utils.dottest
, and added two new optional input parameters (atol and rtol)Added optional parameter densesolver to
pylops.LinearOperator.div
Fixed
pylops.optimization.basic.lsqr
,pylops.optimization.sparsity.ISTA
, andpylops.optimization.sparsity.FISTA
to work with cupy arrays. This change was required by how recent cupy versions handle scalars, which are not converted directly into float types, rather kept as cupy arrays.Fix bug in
pylops.waveeqprocessing.Deghosting
introduced in commit 7e596d4.
Version 1.18.2¶
Released on: 29/04/2022
Refractored
pylops.utils.dottest
, and added two new optional input parameters (atol and rtol)Added optional parameter densesolver to
pylops.LinearOperator.div
Version 1.18.1¶
Released on: 29/04/2022
!DELETED! due to a mistake in the release process
Version 1.18.0¶
Released on: 19/02/2022
Added NMO example to gallery
Extended
pylops.Laplacian
to N-dimensional arraysAdded forward kind to
pylops.SecondDerivative
andpylops.Laplacian
Added chirp-sliding kind to
pylops.waveeqprocessing.seismicinterpolation.SeismicInterpolation
Fixed bug due to the new internal structure of LinearOperator submodule introduced in scipy1.8.0
Version 1.17.0¶
Released on: 29/01/2022
Added
pylops.utils.describe.describe
methodAdded
fftengine
topylops.waveeqprocessing.Marchenko
Added
ifftshift_before
andfftshift_after
optional input parameters inpylops.signalprocessing.FFT
Added
norm
optional input parameter topylops.signalprocessing.FFT2D
andpylops.signalprocessing.FFTND
Added
scipy
backend topylops.signalprocessing.FFT
andpylops.signalprocessing.FFT2D
andpylops.signalprocessing.FFTND
Added
eps
optional input parameter inpylops.utils.signalprocessing.slope_estimate
Added pre-commit hooks
Improved pre-commit hooks
Vectorized
pylops.utils.signalprocessing.slope_estimate
Handlexd
nfft<nt
case inpylops.signalprocessing.FFT
andpylops.signalprocessing.FFT2D
andpylops.signalprocessing.FFTND
Introduced automatic casting of dtype in
pylops.MatrixMult
Improved documentation and definition of optinal parameters of
pylops.Spread
Major clean up of documentation and mathematical formulas
Major refractoring of the inner structure of
pylops.signalprocessing.FFT
andpylops.signalprocessing.FFT2D
andpylops.signalprocessing.FFTND
Reduced warnings in test suite
Reduced computational time of
test_wavedecomposition
in the test suiteFixed bug in
pylops.signalprocessing.Sliding1D
,pylops.signalprocessing.Sliding2D
andpylops.signalprocessing.Sliding3D
where thedtype
of the Restriction operator is inffered fromOp
Fixed bug in
pylops.signalprocessing.Radon2D
andpylops.signalprocessing.Radon3D
when using centered spatial axesFixed scaling in
pylops.signalprocessing.FFT
withreal=True
to pass the dot-test
Version 1.16.0¶
Released on: 11/12/2021
Added
pylops.utils.estimators
submodule for trace estimationAdded x0 in
pylops.optimization.sparsity.ISTA
andpylops.optimization.sparsity.FISTA
to handle non-zero initial guessModified
pylops.optimization.sparsity.ISTA
andpylops.optimization.sparsity.FISTA
to handle multiple right hand sidesModified creation of haxis in
pylops.signalprocessing.Radon2D
andpylops.signalprocessing.Radon3D
to allow for uncentered spatial axesFixed _rmatvec for explicit in
pylops.LinearOperator._ColumnLinearOperator
Version 1.15.0¶
Released on: 23/10/2021
Added
pylops.signalprocessing.Shift
operator.Added option to choose derivative kind in
pylops.avo.poststack.PoststackInversion
andpylops.avo.prestack.PrestackInversion
.Improved efficiency of adjoint of
pylops.signalprocessing.Fredholm1
by applying complex conjugation to the vectors.Added vsvp to
pylops.avo.prestack.PrestackInversion
allowing to use user defined VS/VP ratio.Added kind to
pylops.basicoperators.CausalIntegration
allowingfull
,half
, ortrapezoidal
integration.Fixed _hardthreshold_percentile in
pylops.optimization.sparsity
- Issue #249.Fixed r2norm in
pylops.optimization.solver.cgls
.
Version 1.14.0¶
Released on: 09/07/2021
Added
pylops.optimization.solver.lsqr
solverAdded utility routine
pylops.utils.scalability_test
for scalability tests when usingmultiprocessing
Added
pylops.avo.avo.ps
AVO modelling option and restructuredpylops.avo.prestack.PrestackLinearModelling
to allow passing any function handle that can perform AVO modelling apart from those directly availableAdded R-linear operators (when setting the property clinear=False of a linear operator).
pylops.basicoperators.Real
,pylops.basicoperators.Imag
, andpylops.basicoperators.Conj
Added possibility to run operators
pylops.basicoperators.HStack
,pylops.basicoperators.VStack
,pylops.basicoperators.Block
pylops.basicoperators.BlockDiag
, andpylops.signalprocessing.Sliding3D
usingmultiprocessing
Added dtype to vector X when using
scipy.sparse.linalg.lobpcg
in eigs method ofpylops.LinearOperator
Use kind=forward fot FirstDerivative in
pylops.avo.poststack.PoststackInversion
inversion when dealing with L1 regularized inversion as it makes the inverse problem more stable (no ringing in solution)Changed cost in
pylops.optimization.solver.cg
andpylops.optimization.solver.cgls
to be L2 norms of residualsFixed
pylops.utils.dottest.dottest
for imaginary vectors and to ensure u and v vectors are of same dtype of the operator
Version 1.13.0¶
Released on: 26/03/2021
Added
pylops.signalprocessing.Sliding1D
andpylops.signalprocessing.Patch2D
operatorsAdded
pylops.basicoperators.MemoizeOperator
operatorAdded decay and analysis option in
pylops.optimization.sparsity.ISTA
andpylops.optimization.sparsity.FISTA
solversAdded toreal and toimag methods to
pylops.LinearOperator
Make nr and nc optional in
pylops.utils.dottest.dottest
Fixed complex check in
pylops.basicoperators.MatrixMult
when working with complex-valued cupy arraysFixed bug in data reshaping in check in
pylops.avo.prestack.PrestackInversion
Fixed loading error when using old cupy and/or cusignal (see Issue #201)
Version 1.12.0¶
Released on: 22/11/2020
Modified all operators and solvers to work with cupy arrays
Added
eigs
andsolver
submodules topylops.optimization
Added
deps
andbackend
submodules topylops.utils
Fixed bug in
pylops.signalprocessing.Convolve2D
. andpylops.signalprocessing.ConvolveND
. when dealing with filters that have less dimensions than the input vector.
Version 1.11.1¶
Released on: 24/10/2020
Fixed import of
pyfttw
when not available in :py:class:``pylops.signalprocessing.ChirpRadon3D`
Version 1.11.0¶
Released on: 24/10/2020
Added
pylops.signalprocessing.ChirpRadon2D
andpylops.signalprocessing.ChirpRadon3D
operators.Fixed bug in the inferred dimensions for regularization data creation in
pylops.optimization.leastsquares.NormalEquationsInversion
,pylops.optimization.leastsquares.RegularizedInversion
, andpylops.optimization.sparsity.SplitBregman
.Changed dtype of
pylops.HStack
to allow automatic inference from dtypes of input operator.Modified dtype of
pylops.waveeqprocessing.Marchenko
operator to ensure that outputs of forward and adjoint are real arrays.Reverted to previous complex-friendly implementation of
pylops.optimization.sparsity._softthreshold
to avoid division by 0.
Version 1.10.0¶
Released on: 13/08/2020
Added
tosparse
method topylops.LinearOperator
.Added
kind=linear
inpylops.signalprocessing.Seislet
operator.Added
kind
topylops.FirstDerivative
. operator to perform forward and backward (as well as centered) derivatives.Added
kind
topylops.optimization.sparsity.IRLS
solver to choose between data or model sparsity.Added possibility to use
scipy.sparse.linalg.lobpcg
inpylops.LinearOperator.eigs
andpylops.LinearOperator.cond
Added possibility to use
scipy.signal.oaconvolve
inpylops.signalprocessing.Convolve1D
.Added
NRegs
topylops.optimization.leastsquares.NormalEquationsInversion
to allow providing regularization terms directly in the form ofH^T H
.
Version 1.9.1¶
Released on: 25/05/2020
Changed internal behaviour of
pylops.sparsity.OMP
whenniter_inner=0
. Automatically reverts to Matching Pursuit algorithm.Changed handling of
dtype
inpylops.signalprocessing.FFT
andpylops.signalprocessing.FFT2D
to ensure that the type of the input vector is retained when applying forward and adjoint.Added
dtype
parameter to theFFT
calls in the definition of thepylops.waveeqprocessing.MDD
operation. This ensure that the type of the real part ofG
input is enforced to the output vectors of the forward and adjoint operations.
Version 1.9.0¶
Released on: 13/04/2020
Added
pylops.waveeqprocessing.Deghosting
andpylops.signalprocessing.Seislet
operatorsAdded hard and half thresholds in
pylops.optimization.sparsity.ISTA
andpylops.optimization.sparsity.FISTA
solversAdded
prescaled
input parameter topylops.waveeqprocessing.MDC
andpylops.waveeqprocessing.Marchenko
Added sinc interpolation to
pylops.signalprocessing.Interp
(kind == 'sinc'
)Modified
pylops.waveeqprocessing.marchenko.directwave
to to model analytical responses from both sources of volume injection (derivative=False
) and source of volume injection rate (derivative=True
)Added
pylops.LinearOperator.asoperator
method topylops.LinearOperator
Added
pylops.utils.signalprocessing.slope_estimate
functionFix bug in
pylops.signalprocessing.Radon2D
andpylops.signalprocessing.Radon3D
whenonthefly=True
returning the same result as whenonthefly=False
Version 1.8.0¶
Released on: 12/01/2020
Added
pylops.LinearOperator.todense
method topylops.LinearOperator
Added
pylops.signalprocessing.Bilinear
,pylops.signalprocessing.DWT
, andpylops.signalprocessing.DWT2
operatorsAdded
pylops.waveeqprocessing.PressureToVelocity
,pylops.waveeqprocessing.UpDownComposition3Doperator
, andpylops.waveeqprocessing.PhaseShift
operatorsFix bug in
pylops.basicoperators.Kronecker
(see Issue #125)
Version 1.7.0¶
Released on: 10/11/2019
Added
pylops.Gradient
,pylops.Sum
,pylops.FirstDirectionalDerivative
, andpylops.SecondDirectionalDerivative
operatorsAdded
pylops.LinearOperator._ColumnLinearOperator
private operatorAdded possibility to directly mix Linear operators and numpy/scipy 2d arrays in
pylops.VStack
andpylops.HStack
andpylops.BlockDiag
operatorsAdded
pylops.optimization.sparsity.OMP
solver
Version 1.6.0¶
Released on: 10/08/2019
Added
pylops.signalprocessing.ConvolveND
operatorAdded
pylops.utils.signalprocessing.nonstationary_convmtx
to create matrix for non-stationary convolutionAdded possibility to perform seismic modelling (and inversion) with non-stationary wavelet in
pylops.avo.poststack.PoststackLinearModelling
Create private methods for
pylops.Block
,pylops.avo.poststack.PoststackLinearModelling
,pylops.waveeqprocessing.MDC
to allow calling different operators (e.g., from pylops-distributed or pylops-gpu) within the method
Version 1.5.0¶
Released on: 30/06/2019
Added
conj
method topylops.LinearOperator
Added
pylops.Kronecker
,pylops.Roll
, andpylops.Transpose
operatorsAdded
pylops.signalprocessing.Fredholm1
operatorAdded
pylops.optimization.sparsity.SPGL1
andpylops.optimization.sparsity.SplitBregman
solversSped up
pylops.signalprocessing.Convolve1D
usingscipy.signal.fftconvolve
for multi-dimensional signalsChanges in implementation of
pylops.waveeqprocessing.MDC
andpylops.waveeqprocessing.Marchenko
to take advantage of primitives operatorsAdded
epsRL1
option topylops.avo.poststack.PoststackInversion
andpylops.avo.prestack.PrestackInversion
to include TV-regularization terms by means ofpylops.optimization.sparsity.SplitBregman
solver
Version 1.4.0¶
Released on: 01/05/2019
Added
numba
engine topylops.Spread
andpylops.signalprocessing.Radon2D
operatorsAdded
pylops.signalprocessing.Radon3D
operatorAdded
pylops.signalprocessing.Sliding2D
andpylops.signalprocessing.Sliding3D
operatorsAdded
pylops.signalprocessing.FFTND
operatorAdded
pylops.signalprocessing.Radon3D
operatorAdded
niter
option topylops.LinearOperator.eigs
methodAdded
show
option topylops.optimization.sparsity.ISTA
andpylops.optimization.sparsity.FISTA
solversAdded
pylops.waveeqprocessing.seismicinterpolation
,pylops.waveeqprocessing.waveeqdecomposition
andpylops.waveeqprocessing.lsm
submodulesAdded tests for
engine
in various operatorsAdded documentation regarding usage of
pylops
Docker container
Version 1.3.0¶
Released on: 24/02/2019
Added
fftw
engine topylops.signalprocessing.FFT
operatorAdded
pylops.optimization.sparsity.ISTA
andpylops.optimization.sparsity.FISTA
sparse solversAdded possibility to broadcast (handle multi-dimensional arrays) to
pylops.Diagonal
andpylops..Restriction
operatorsAdded
pylops.signalprocessing.Interp
operatorAdded
pylops.Spread
operatorAdded
pylops.signalprocessing.Radon2D
operator
Version 1.2.0¶
Released on: 13/01/2019
Added
pylops.LinearOperator.eigs
andpylops.LinearOperator.cond
methods to estimate estimate eigenvalues and conditioning number using scipy wrapping of ARPACKModified default
dtype
for all operators to befloat64
(orcomplex128
) to be consistent with default dtypes used by numpy (and scipy) for real and complex floating point numbers.Added
pylops.Flip
operatorAdded
pylops.Symmetrize
operatorAdded
pylops.Block
operatorAdded
pylops.Regression
operator performing polynomial regression and modifiedpylops.LinearRegression
to be a simple wrapper ofpylops.Regression
whenorder=1
Modified
pylops.MatrixMult
operator to work with both numpy ndarrays and scipy sparse matricesAdded
pylops.avo.prestack.PrestackInversion
routineAdded possibility to have a data weight via
Weight
input parameter topylops.optimization.leastsquares.NormalEquationsInversion
andpylops.optimization.leastsquares.RegularizedInversion
solversAdded
pylops.optimization.sparsity.IRLS
solver
Version 1.1.0¶
Released on: 13/12/2018
Added
pylops.CausalIntegration
operator
Version 1.0.1¶
Released on: 09/12/2018
Changed module from
lops
topylops
for consistency with library name (and pip install).Removed quickplots from utilities and
matplotlib
from requirements of PyLops.
Version 1.0.0¶
Released on: 04/12/2018
First official release.
Roadmap¶
This roadmap is aimed at providing an high-level overview on the bug fixes, improvements and new functionality that are planned for the PyLops library.
Any of the fixes/additions mentioned in the roadmap are directly linked to a Github Issue that provides more details onto the reason and initial thoughts for the implementation of such a fix/addition.
Striked tasks have been completed and related github issue closed with more details regarding how this task has been carried out.
Library structure¶
Create a child repository and python library called
geolops
(just a suggestion) where geoscience-related operators and examples are moved across, keeping the corepylops
library very generic and multi-purpose - Issue #22.
Code cleaning¶
Code optimization¶
Modules¶
avo¶
Add possibility to choose different damping factors for each elastic parameter to invert for in
pylops.avo.prestack.PrestackInversion
- Issue #25.
basicoperators¶
optimization¶
Sparse solvers - Issue #44.
signalprocessing¶
Compare performance in FTT operator of performing np.swap+np.fft.fft(…, axis=-1) versus np.fft.fft(…, axis=chosen) - Issue #33.
Add Wavelet operator performing the wavelet transform - Issue #21.
Fredholm1 operator applying Fredholm integrals of first kind - Issue #31.
Fredholm2 operators applying Fredholm integrals of second kind - Issue #31.
utils¶
Nothing so far
waveeqprocessing¶
Papers using PyLops¶
This section lists various conference abstracts and papers using the PyLops framework. If you publish a paper using PyLops, we’d love to hear about it!
2021
- Vakalis, S., D. Chen, M. Yan, and J. Nanzer, 2021, Image enhancement in active incoherent millimeter-wave imaging. Passive and Active Millimeter-Wave Imaging XXIV. doi: 10.1117/12.2585650
- Li, X., T. Becker, M. Ravasi, J. Robertsson, and D.-J. van Manen, 2021, Closed-aperture unbounded acoustics experimentation using multidimensional deconvolution. The Journal of the Acoustical Society of America, 149, 1813–1828. doi: 10.1121/10.0003706
- Kuijpers, D., I. Vasconcelos, and P. Putzky, 2021, Reconstructing missing seismic data using Deep Learning. arXiv: 2101.09554
- Ravasi, M., and C. Birnie, 2021, A Joint Inversion-Segmentation approach to Assisted Seismic Interpretation. arXiv: 2102.03860
- Haindl, C., M. Ravasi, and F. Broggini, 2021, Handling gaps in acquisition geometries — Improving Marchenko-based imaging using sparsity-promoting inversion and joint inversion of time-lapse data. Geophysics, 86 (2), S143-S154. doi: 10.1190/geo2020-0036.1
- Ulrich, I. E., A. Zunino, C. Boehm, and A. Fichtner, 2021, Sparsifying regularizations for stochastic sample average minimization in ultrasound computed tomography. Medical Imaging 2021: Ultrasonic Imaging and Tomography. doi: 10.1117/12.2580926
- Nightingale, James., R. Hayes, A. Kelly, A. Amvrosiadis, A. Etherington, Q. He, N. Li, X. Cao, J. Frawley, S. Cole, A. Enia, C. Frenk, D. Harvey, R. Li, R. Massey, M. Negrello, and A. Robertson, 2020, PyAutoLens: Open-Source Strong Gravitational Lensing. Journal of Open Source Software, 6, 2825. doi: 10.21105/joss.02825
2020
- Feng, R., T. Mejer Hansen, D. Grana, and N. Balling, 2020, An unsupervised deep-learning method for porosity estimation based on poststack seismic data. Geophysics, 85 (6), M97-M105. doi: 10.1190/geo2020-0121.1
- Zhang, M., 2020, Marchenko Green’s functions from compressive sensing acquisition. SEG Technical Program Expanded Abstracts 2020. doi: 10.1190/segam2020-3424845.1
- Vargas, D., and I. Vasconcelos, 2020, Rayleigh-Marchenko Redatuming Using Scattered Fields in Highly Complex Media. EAGE 2020 Annual Conference & Exhibition Online. doi: 10.3997/2214-4609.202011347
- Ravasi, M., and I. Vasconcelos, 2020, Implementation of Large-Scale Integral Operators with Modern HPC Solutions. EAGE 2020 Annual Conference & Exhibition Online. doi: 10.3997/2214-4609.202010529
- Ravasi, M., and I. Vasconcelos, 2020, PyLops—A linear-operator Python library for scalable algebra and optimization. SoftwareX, 11, 100361. doi: 10.1016/j.softx.2019.100361
2019
How to cite¶
When using PyLops in scientific publications, please cite the following paper:
Contributors¶
Matteo Ravasi, mrava87
Carlos da Costa, cako
Dieter Werthmüller, prisae
Tristan van Leeuwen, TristanvanLeeuwen
Leonardo Uieda, leouieda
Filippo Broggini, filippo82
Tyler Hughes, twhughes
Lyubov Skopintseva, lskopintseva
Francesco Picetti, fpicetti
Alan Richardson, ar4
BurningKarl, BurningKarl
Nick Luiken, NickLuiken