PyLops¶
This Python library is inspired by the 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. When dealing with small-scale problems, the Python numerical scientific libraries numpy and scipy allow to perform most of the underlying matrix operations (e.g., computation of matrix-vector products and manipulation of matrices) in a simple and expressive way.
Many useful operators, however, do not lend themselves to an explicit matrix representation when used to solve large-scale problems. PyLops operators, on the other hand, still represent a matrix and can be treated in a similar way, but do not rely on the explicit creation of a dense (or sparse) matrix itself. Conversely, the forward and adjoint operators are represented by small pieces of codes that mimic the effect of the matrix on a vector or another matrix.
Luckily, many iterative methods (e.g. cg, lsqr) do not need to know the individual entries of a matrix to solve a linear system. Such solvers only require the computation of forward and adjoint matrix-vector products as done for any of the PyLops operators.
Here is a simple example showing how a dense first-order first derivative operator can be created, applied and inverted using numpy/scipy commands:
import numpy as np
from scipy.linalg import lstsq
nx = 7
x = np.arange(nx) - (nx-1)/2
D = np.diag(0.5*np.ones(nx-1), k=1) - \
np.diag(0.5*np.ones(nx-1), k=-1)
D[0] = D[-1] = 0 # take away edge effects
# y = Dx
y = np.dot(D, x)
# x = D'y
xadj = np.dot(D.T, y)
# xinv = D^-1 y
xinv = lstsq(D, y)[0]
and similarly using PyLops commands:
from pylops import FirstDerivative
nx = 7
x = range(-(nx // 2), nx // 2 + (1 if nx % 2 else 0))
Dlop = FirstDerivative(nx, dtype='float64')
# y = Dx
y = Dlop*x
# x = D'y
xadj = Dlop.H*y
# xinv = D^-1 y
xinv = Dlop / y
Note how this second approach does not require creating a dense matrix, reducing both the memory load and the computational cost of applying a derivative to an input vector \(\mathbf{x}\). Moreover, the code becomes even more compact and espressive than in the previous case letting the user focus on the formulation of equations of the forward problem to be solved by inversion.
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 modelling (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¶
The PyLops project strives to create a library that is easy to install in any environment and has a very limited number of dependencies. However, since Python2 will retire soon, we have decided to only focus on a Python3 implementation. If you are still using Python2, hurry up!
For this reason you will need Python 3.6 or greater to get started.
Dependencies¶
Our mandatory dependencies are limited to:
We advise using the Anaconda Python distribution
to ensure that these dependencies are installed via the Conda
package manager. This
is not just a pure stylistic choice but comes with some hidden advantages, such as the linking to
Intel MKL
library (i.e., a highly optimized BLAS library created by Intel).
If you simply want to use PyLops for teaching purposes or for small-scale examples, this should not really affect you. However, if you are interested in getting better code performance, read carefully the Advanced installation page.
Optional dependencies¶
PyLops’s optional dependencies refer to those dependencies that we do not include
in our requirements.txt
and environment.yml
files and thus are not strictly
needed nor installed directly as part of a standar installation (see below for details)
However, we sometimes implement additional back-ends (referred to as engine
in the code)
for some of our operators in order to improve their performance.
To do so, we rely on third-party libraries. Those libraries are generally added to the
list of our optional dependencies.
If you are not after code performance, you may simply stick to the mandatory dependencies
and pylops will ensure to always fallback to one of those for any linear operator.
If you are instead after code performance, take a look at the Optional dependencies section in the Advanced installation page.
Step-by-step installation for users¶
Python environment¶
Activate your Python environment, and simply type the following command in your terminal to install the PyPi distribution:
>> pip install pylops
If using Conda, you can also install our conda-forge distribution via:
>> conda install -c conda-forge pylops
Note that using the conda-forge
distribution is recommended as all the dependencies (both mandatory
and optional) will be correctly installed for you, while only mandatory dependencies are installed
using the pip
distribution.
Alternatively, to access the latest source from github:
>> pip install git+https://git@github.com/PyLops/pylops.git@master
or just clone the repository
>> git clone https://github.com/PyLops/pylops.git
or download the zip file from the repository (green button in the top right corner of the main github repo page) and install PyLops from terminal using the command:
>> make install
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 enviroment 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 distribution is also available. Simply use conda_notebook instead of notebook in the previous command.
Step-by-step installation for developers¶
Fork and clone the repository by executing the following in your terminal:
>> git clone https://github.com/your_name_here/pylops.git
The first time you clone the repository run the following command:
>> make dev-install
If you prefer to build a new Conda enviroment just for PyLops, run the following command:
>> make dev-install_conda
To ensure that everything has been setup correctly, run tests:
>> make tests
Make sure no tests fail, this guarantees that the installation has been successfull.
Finally, 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
Later on, pre-commit
will automatically run for you and propose additional stylistic changes to your commits.
If using Conda environment, always remember to activate the conda environment every time you open a new bash shell by typing:
>> source activate pylops
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 (e.g., allow multithreading) in your systemt.
Dependencies¶
PyLops relies on the numpy and scipy libraries and being able to link these to the most performant BLAS will ensure optimal performance of PyLops when using only required dependencies.
As already mentioned in the Installation page, we strongly encourage using
the Anaconda Python distribution as
numpy and scipy will be automatically linked to the Intel MKL
library, which is per today the most performant library for basic linear algebra
operations (if you don’t believe it, take a read at this
blog post).
The best way to understand which BLAS
library is currently linked to your
numpy and scipy libraries is to run the following commands in ipython:
import numpy as np
import scipy as sp
print(np.__config__.show())
print(sp.__config__.show())
You should be able to understand if your numpy and scipy are
linked to Intel MKL
or something else.
Note
Unfortunately, PyLops is so far only shipped with PyPI, meaning that if you
have not already installed numpy and scipy in your environment they will be installed as
part of the installation process of the pylops library, all of those using pip
. This comes with
the disadvantage that numpy and scipy are linked to OpenBlas
instead of Intel MKL
,
leading to a loss of performance. To prevent this, we suggest the following strategy:
- create conda environment, e.g.
conda create -n envname python=3.6.4 numpy scipy
- install pylops using
pip install pylops
Finally, it is always important to make sure that your environment variable OMP_NUM_THREADS
is
correctly set to the maximum number of threads you would like to use in your code. If that is not the
case numpy and scipy will underutilize your hardware even if linked to a performant BLAS
library.
For example, first set OMP_NUM_THREADS=1
(single-threaded) in your terminal:
>> export OMP_NUM_THREADS=1
and run the following code in python:
import os
import numpy as np
from timeit import timeit
size = 4096
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 OMP_NUM_THREADS=2
, or any higher number of threads available
in your hardware (multi-threaded):
>> export OMP_NUM_THREADS=2
and run the same python code. By both looking at your processes (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']=str(2)
.
Moreover, note that when using Intel MKL
you can alternatively set
the MKL_NUM_THREADS
instead of OMP_NUM_THREADS
: this could
be useful if your code runs other parallel processes which you can
control indipendently from the Intel MKL
ones using OMP_NUM_THREADS
.
Note
Always remember to set OMP_NUM_THREADS
(or MKL_NUM_THREADS
)
in your enviroment 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 reccomend using the Conda package manager and install all the mandatory and optional dependencies of PyLops at once using the command:
>> conda install -c 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 compuations with the aid
of GPUs should install it prior to installing pylops
(see below for more
details).
numba¶
Although we always stive 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.
In this case 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
Finally, it is also advised to install the additional package icc_rt.
>> conda install -c numba icc_rt
or pip equivalent. Similarly to Intel MKL
, you need to set the environment variable
NUMBA_NUM_THREADS
to tell numba how many threads to use. If this variable is not
present in your environment, numba code will be compiled with parallel=False
.
fft routines¶
Two different engines are provided by the pylops.signalprocessing.FFT
operator for
fft
and ifft
routines in the forward and adjoint modes: engine='numpy'
(default)
and engine='fftw'
.
The first engine comes as default as numpy is part of the dependencies of PyLops and automatically installed when PyLops is installed if not already available in your Python distribution.
The second engine implements the well-known FFTW
via the python wrapper pyfftw.FFTW
. This optimized fft tends to
outperform the one from numpy in many cases, however it has not been inserted
in the mandatory requirements of PyLops, meaning that when installing PyLops with
pip
, pyfftw.FFTW
will not be installed automatically.
Again, if interested to use FFTW
backend from conda, you will need to manually install it:
>> conda install -c conda-forge pyfftw
or pip equivalent.
skfmm¶
This library is used to compute traveltime tables with the fast-marching method in the
initialization of the pylops.waveeqprocessing.Demigration
operator
when choosing mode == 'eikonal'
.
As this may not be of interest for many users, this library has not been inserted
in the mandatory requirements of PyLops. If interested to use skfmm
,
you will need to manually install it:
>> conda install -c conda-forge scikit-fmm
or pip equivalent.
spgl1¶
This library is used to solve sparsity-promoting BP, BPDN, and LASSO problems
in pylops.optimization.sparsity.SPGL1
solver.
If interested to use spgl1
, you can manually install it:
>> pip install spgl1
pywt¶
This library is used to implement the Wavelet operators.
If interested to use pywt
, you can manually install it:
>> conda install pywavelets
or pip equivalent.
sympy¶
This library is used to implement the describe
method, which transforms
PyLops operators into their mathematical expression.
If interested to use sympy
, you can manually install it:
>> conda install sympy
or pip equivalent.
Note
If you are a developer, all the above optional dependencies can also
be installed automatically by cloning the repository and installing
pylops via make dev-install
or make dev-install_conda
.
Optional Dependencies (GPU)¶
cupy¶
This library is 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
. PyLops will automatically check if cupy
is
installed and in that case use it any time the input vector passed to an
operator is of cupy
type. Users can however disabilitate this option
even if cupy is installed. For more details of GPU-accelerated PyLops read
the GPU support section.
cusignal¶
This 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
. PyLops will automatically
check if cusignal
is installed and in that case use it any time the
input vector passed to an operator is of cusignal
type. Users can however
disabilitate this option even if cupy is installed. For more details
of GPU-accelerated PyLops read the GPU support section.
GPU support¶
From v1.12.0
, PyLops supports computations on GPUs powered by
cupy
and cusignal
.
Note
For this to work, ensure to have installed cupy-cudaXX>=8.1.0
and cusignal>=0.16.0
. If neither cupy
nor cusignal
is
installed, pylops will work just fine using its CPU backend.
Note, however, that setting the environment variable
CUPY_PYLOPS
(or CUSIGNAL_PYLOPS
) to 0
will force pylops not
to use the cupy
backend. 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 a cupy array.
pylops.LinearOperator
methods that are currently not available for
GPU computations are:
eigs
,cond
, andtosparse
, andestimate_spectral_norm
Operators that are currently not available for GPU computations are:
pylops.Spread
pylops.signalprocessing.Radon2D
pylops.signalprocessing.Radon3D
pylops.signalprocessing.DWT
pylops.signalprocessing.DWT2D
pylops.signalprocessing.Seislet
pylops.waveeqprocessing.Demigration
pylops.waveeqprocessing.LSM
Solvers that are currently not available for GPU computations are:
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! For more examples head over to these
notebooks.
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:
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()

Out:
<matplotlib.legend.Legend object at 0x7f4dec8c92b0>
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()

Out:
<matplotlib.legend.Legend object at 0x7f4de8be6278>
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
# +
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())
Out:
<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]
(9.999999999999986+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("y = Dx = ", Dop * x)
print("y = conj(D)x = ", Dop.conj() * x)
Out:
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()

Out:
<matplotlib.colorbar.Colorbar object at 0x7f4dec722e10>
Finally 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("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()

Out:
x = (Dop - conj(Dop))/y = [1.+0.j 1.+0.j 1.+0.j 1.+0.j 1.+0.j]
<matplotlib.colorbar.Colorbar object at 0x7f4dec89c898>
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.112 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("Dot-test %e" % np.abs((yy - xx) / ((yy + xx + 1e-15) / 2)))
Out:
Dot-test 1.366060e-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([])

Out:
[Text(0, 0.5, ''), Text(0, 1.5, ''), Text(0, 2.5, ''), Text(0, 3.5, '')]
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, tol=1e-6, complexflag=0, verb=True)
Out:
Dot test passed, v^T(Opu)=-6.919654 - u^T(Op^Tv)=-6.919654
True
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)
Out:
Dot test passed, v^T(Opu)=-17.139104+3.828834i - u^T(Op^Tv)=-17.139104+3.828834i
Dot test passed, v^T(Opu)=11.420800-3.688151i - u^T(Op^Tv)=11.420800-3.688151i
True
Total running time of the script: ( 0 minutes 0.467 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")

Out:
(-0.0398, 0.8358000000000001, -0.5341666861485157, 1.007579366007072)
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")

Out:
Text(0.5, 1.0, 'Data restriction')
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.RegularizedInversion
(without regularization term for now) and customize our solvers using
kwargs
.
xinv = pylops.optimization.leastsquares.RegularizedInversion(
Rop, [], y, **dict(damp=0, iter_lim=10, show=1)
)
Out:
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-08 conlim = 1.00e+08
btol = 1.00e-08 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.RegularizedInversion(
Rop, [], y, x0=np.ones(N), **dict(damp=0, iter_lim=10, show=0)
)
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.NormalEquationsInversion
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.NormalEquationsInversion(Rop, [], y)
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")

Out:
Text(0.5, 1.0, 'Data reconstruction without regularization')
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.NormalEquationsInversion
orpylops.optimization.leastsquares.RegularizedInversion
) - preconditioning via
pylops.optimization.leastsquares.PreconditionedInversion
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, dims=None, dtype="float64")
# Regularized inversion
epsR = np.sqrt(0.1)
epsI = np.sqrt(1e-4)
xne = pylops.optimization.leastsquares.NormalEquationsInversion(
Rop, [D2op], y, epsI=epsI, epsRs=[epsR], returninfo=False, **dict(maxiter=50)
)
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.NormalEquationsInversion
as
follows:
ND2op = pylops.MatrixMult((D2op.H * D2op).tosparse()) # mimic fast D^T D
xne1 = pylops.optimization.leastsquares.NormalEquationsInversion(
Rop,
[],
y,
NRegs=[ND2op],
epsI=epsI,
epsNRs=[epsR],
returninfo=False,
**dict(maxiter=50)
)
We can do the same while using
pylops.optimization.leastsquares.RegularizedInversion
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.RegularizedInversion(
Rop,
[D2op],
y,
epsRs=[np.sqrt(0.1)],
returninfo=False,
**dict(damp=np.sqrt(1e-4), iter_lim=50, show=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.PreconditionedInversion
.
# Create regularization operator
Sop = pylops.Smoothing1D(nsmooth=11, dims=[N], dtype="float64")
# Invert for interpolated signal
xprec = pylops.optimization.leastsquares.PreconditionedInversion(
Rop, Sop, y, returninfo=False, **dict(damp=np.sqrt(1e-9), iter_lim=20, show=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)

Out:
(0.05, 0.3)
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, returninfo=True
)
xista = FFTop.H * pista
pfista, niterf, costf = pylops.optimization.sparsity.FISTA(
Rop * FFTop.H, y, niter=1000, eps=0.1, tol=1e-7, returninfo=True
)
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, 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 6.622 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'")
Out:
Text(0.5, 1.0, "Averaged covariance 'filter'")
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)

Out:
(0.0, 0.8)
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")
Out:
(0.0, 0.796, 0.796, 0.0)
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 2.603 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), dims=(Nz, Nx), 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.ravel()
imdeblur = pylops.optimization.leastsquares.NormalEquationsInversion(
Cop, None, imblur, maxiter=50
)
Wop = pylops.signalprocessing.DWT2D((Nz, Nx), wavelet="haar", level=3)
Dop = [
pylops.FirstDerivative(Nz * Nx, dims=(Nz, Nx), dir=0, edge=False),
pylops.FirstDerivative(Nz * Nx, dims=(Nz, Nx), dir=1, edge=False),
]
DWop = Dop + [
Wop,
]
imdeblurfista = pylops.optimization.sparsity.FISTA(
Cop * Wop.H, imblur, eps=1e-1, niter=100
)[0]
imdeblurfista = Wop.H * imdeblurfista
imdeblurtv = pylops.optimization.sparsity.SplitBregman(
Cop,
Dop,
imblur.ravel(),
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]
imdeblurtv1 = pylops.optimization.sparsity.SplitBregman(
Cop,
DWop,
imblur.ravel(),
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]
# Reshape images
imblur = imblur.reshape((Nz, Nx))
imdeblur = imdeblur.reshape((Nz, Nx))
imdeblurfista = imdeblurfista.reshape((Nz, Nx))
imdeblurtv = imdeblurtv.reshape((Nz, Nx))
imdeblurtv1 = imdeblurtv1.reshape((Nz, Nx))
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 12.941 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.NormalEquationsInversion(
Rop, [D2op], y, epsRs=[np.sqrt(0.1)], returninfo=False, **dict(maxiter=200)
)
# Invert for interpolated signal, lsqrt
(
xlsqr_reg_lop,
istop,
itn,
r1norm,
r2norm,
) = pylops.optimization.leastsquares.RegularizedInversion(
Rop,
[D2op],
y,
epsRs=[np.sqrt(0.1)],
returninfo=True,
**dict(damp=0, iter_lim=200, show=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, 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, 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 26.242 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)

Out:
<matplotlib.legend.Legend object at 0x7f4dec168be0>
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)
Out:
<matplotlib.legend.Legend object at 0x7f4dec165400>
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 24.918 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")
Out:
<matplotlib.legend.Legend object at 0x7f4deb734080>
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: ( 1 minutes 0.351 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"]]
MDCop = pylops.waveeqprocessing.MDC(
Gwav_fft, nt=2 * par["nt"] - 1, nv=1, dt=0.004, dr=1.0, dtype="float32"
)
# Create data
d = MDCop * m.ravel()
d = d.reshape(par["ny"], 2 * par["nt"] - 1)
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,
dtype="complex64",
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,
dtype="complex64",
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 16.476 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
Out:
Dot test passed, v^T(Opu)=405.165096 - u^T(Op^Tv)=405.165096
Dot test passed, v^T(Opu)=172.065076 - u^T(Op^Tv)=172.065076
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-08 conlim = 1.00e+08
btol = 1.00e-08 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 17.404 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
t0 = [0.1, 0.2, 0.3]
theta = [0, 0, 0]
amp = [1.0, -2, 0.5]
# parabolic event
tp0 = [0.13]
px = [0]
pxx = [5e-7]
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
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.ravel()
xadj = xadj.reshape(npx, par["nt"])
# sparse Radon transform
xinv, niter, cost = pylops.optimization.sparsity.FISTA(
Rop, y.ravel(), 15, eps=1e1, returninfo=True
)
xinv = xinv.reshape(npx, par["nt"])
# filtering
xfilt = np.zeros_like(xadj)
xfilt[npx // 2 - 3 : npx // 2 + 4] = xadj[npx // 2 - 3 : npx // 2 + 4]
yfilt = Rop * xfilt.ravel()
yfilt = yfilt.reshape(par["nx"], par["nt"])
# 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.ravel()
yinvfilt = yinvfilt.reshape(par["nx"], par["nt"])
Finally we visualize our results.
fig, axs = plt.subplots(1, 5, sharey=True, figsize=(12, 5))
axs[0].imshow(
y.T,
cmap="gray",
vmin=-np.abs(y).max(),
vmax=np.abs(y).max(),
extent=(xaxis[0], xaxis[-1], taxis[-1], taxis[0]),
)
axs[0].set_title("Data")
axs[0].axis("tight")
axs[1].imshow(
xadj.T,
cmap="gray",
vmin=-np.abs(xadj).max(),
vmax=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_title("Radon")
axs[1].axis("tight")
axs[2].imshow(
yfilt.T,
cmap="gray",
vmin=-np.abs(yfilt).max(),
vmax=np.abs(yfilt).max(),
extent=(xaxis[0], xaxis[-1], taxis[-1], taxis[0]),
)
axs[2].set_title("Filtered data")
axs[2].axis("tight")
axs[3].imshow(
xinv.T,
cmap="gray",
vmin=-np.abs(xinv).max(),
vmax=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_title("Sparse Radon")
axs[3].axis("tight")
axs[4].imshow(
yinvfilt.T,
cmap="gray",
vmin=-np.abs(y).max(),
vmax=np.abs(y).max(),
extent=(xaxis[0], xaxis[-1], taxis[-1], taxis[0]),
)
axs[4].set_title("Sparse filtered data")
axs[4].axis("tight")

Out:
(0.0, 240.0, 0.396, 0.0)
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 47.231 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, dims=(par["nx"], par["nt"]), dir=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")

Out:
(0.0, 138.0, 0.316, 0.0)
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"], dims=(par["nx"], par["nt"]), dir=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, returninfo=True)
)
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")
Out:
Text(0.5, 1.0, 'FISTA convergence')
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, returninfo=True)
)
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")

Out:
(-0.001, 0.001, 0.316, 0.0)
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, dims=(par["nx"], par["nt"]), dir=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", design=True
)
# 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")

Out:
(-0.0005, 0.0005, 1.995, 0.0)
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 11.571 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([])

Out:
[]
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)

Out:
(1.0, 0.0)
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.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()
Out:
<matplotlib.legend.Legend object at 0x7f4debeb9358>
To see more examples head over to the following notebook: notebook1.
Total running time of the script: ( 0 minutes 10.997 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,
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")

Out:
(-220.0, 220.0, 0.796, 0.0)
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 11.898 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="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("Velocity")
plt.xlim(x[0], x[-1])
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])
Out:
(0.0, 320.0)
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"
)
d = lsm.Demop * refl.ravel()
d = d.reshape(ns, nr, nt)
madj = lsm.Demop.H * d.ravel()
madj = madj.reshape(nx, nz)
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
dadj = lsm.Demop * madj.ravel()
dadj = dadj.reshape(ns, nr, nt)
dinv = lsm.Demop * minv.ravel()
dinv = dinv.reshape(ns, nr, nt)
dinv_sparse = lsm.Demop * minv_sparse.ravel()
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}$")
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")
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")
Out:
(-0.5, 10.5, 299.5, -0.5)
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 8.000 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(np.deg2rad(theta)) + 1e-15)
+ np.tan(np.deg2rad(90 - theta)) * x
+ ny // 2
)
x = np.load("../testdata/optimization/shepp_logan_phantom.npy")
x = x / x.max()
ny, nx = x.shape
ntheta = 150
theta = np.linspace(0.0, 180.0, 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.T.ravel()
y = y.reshape(ntheta, ny).T
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.T.ravel()
xrec = xrec.reshape(nx, ny).T
fig, axs = plt.subplots(1, 3, figsize=(10, 4))
axs[0].imshow(x, vmin=0, vmax=1, cmap="gray")
axs[0].set_title("Model")
axs[0].axis("tight")
axs[1].imshow(y, cmap="gray")
axs[1].set_title("Data")
axs[1].axis("tight")
axs[2].imshow(xrec, 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(
ny * nx, dims=(nx, ny), dir=0, edge=True, kind="backward", dtype=np.float64
),
pylops.FirstDerivative(
ny * nx, dims=(nx, ny), dir=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.RegularizedInversion(
RLop.H, [D2op], y.T.ravel(), epsRs=[1e1], **dict(iter_lim=20)
)
xinv_sm = np.real(xinv_sm.reshape(nx, ny)).T
# TV
mu = 1.5
lamda = [1.0, 1.0]
niter = 3
niterinner = 4
xinv, niter = pylops.optimization.sparsity.SplitBregman(
RLop.H,
Dop,
y.T.ravel(),
niter,
niterinner,
mu=mu,
epsRL1s=lamda,
tol=1e-4,
tau=1.0,
show=False,
**dict(iter_lim=20, damp=1e-2)
)
xinv = np.real(xinv.reshape(nx, ny)).T
fig, axs = plt.subplots(1, 3, figsize=(10, 4))
axs[0].imshow(x, vmin=0, vmax=1, cmap="gray")
axs[0].set_title("Model")
axs[0].axis("tight")
axs[1].imshow(xinv_sm, vmin=0, vmax=1, cmap="gray")
axs[1].set_title("L2 Inversion")
axs[1].axis("tight")
axs[2].imshow(xinv, 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 18.772 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("xinv=%s\n" % xinv)
Out:
xinv=[1. 2. 3. 4. 5.]
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("xinv1=%s\n" % xinv1)
Out:
xinv1=[1. 2. 3. 4. 5.]
Total running time of the script: ( 0 minutes 0.019 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()

Out:
<matplotlib.legend.Legend object at 0x7f4df588b9e8>
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()

Out:
<matplotlib.legend.Legend object at 0x7f4df5744710>
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), dir=0, dtype="float64")
B = np.reshape(Sop * np.ndarray.flatten(A), (11, 21))
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 1.618 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
dim = (nop + 2) // 2 * nwins
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)
Slid = pylops.signalprocessing.Sliding1D(
Op.H, dim, dimd, nwin, nover, tapertype=None, design=False
)
x = Slid.H * data.ravel()
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", design=False
)
reconstructed_data = Slid * x.ravel()
fig, axs = plt.subplots(1, 2, figsize=(15, 3))
axs[0].plot(data, "k", label="Data")
axs[0].plot(reconstructed_data, "--r", label="Rec Data")
axs[1].set_title("Original domain")
axs[1].plot(np.abs(x), "k")
axs[1].set_title("Transformed domain")
plt.tight_layout()

Out:
/home/docs/checkouts/readthedocs.org/user_builds/pylops/envs/v1.17.0/lib/python3.6/site-packages/numpy/core/_asarray.py:83: ComplexWarning: Casting complex values to real discards the imaginary part
return array(a, dtype, copy=False, order=order)
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
nwins = 5
winsize = 36
overlap = 10
npx = 61
px = np.linspace(-5e-3, 5e-3, npx)
dimsd = data.shape
dims = (nwins * npx, par["nt"])
# 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",
)
Slid = pylops.signalprocessing.Sliding2D(
Op, dims, dimsd, winsize, overlap, tapertype=None
)
radon = Slid.H * data.ravel()
radon = radon.reshape(dims)
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.ravel()
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
nwins = (4, 5)
winsize = (5, 6)
overlap = (2, 3)
npx = 21
px = np.linspace(-5e-3, 5e-3, npx)
dimsd = data.shape
dims = (nwins[0] * npx, nwins[1] * npx, par["nt"])
# 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",
)
Slid = pylops.signalprocessing.Sliding3D(
Op, dims, dimsd, winsize, overlap, (npx, npx), tapertype=None
)
radon = Slid.H * data.ravel()
radon = radon.reshape(nwins[0], nwins[1], npx, npx, par["nt"])
Slid = pylops.signalprocessing.Sliding3D(
Op, dims, dimsd, winsize, overlap, (npx, npx), tapertype="cosine", design=False
)
reconstructed_data = Slid * radon.ravel()
reconstructed_data = reconstructed_data.reshape(dimsd)
radoninv = pylops.LinearOperator(Slid, explicit=False).div(data.ravel(), niter=10)
reconstructed_datainv = Slid * radoninv.ravel()
radoninv = radoninv.reshape(nwins[0], nwins[1], npx, npx, par["nt"])
reconstructed_datainv = reconstructed_datainv.reshape(dimsd)
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 14.739 seconds)
Note
Click here to download the full example code
2D Patching¶
This example shows how to use the pylops.signalprocessing.Patch2D
operator to perform repeated transforms over small patches of a 2-dimensional
array. The transform that we apply in this example is the
pylops.signalprocessing.FFT2D
but this operator has been
design to allow a variety of transforms as long as they operate with signals
that are 2-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.
nwins = (13, 6)
nwin = (20, 34)
nop = (128, 128)
nover = (10, 4)
dimsd = data.shape
dims = (nwins[0] * nop[0], nwins[1] * nop[1])
# Sliding window transform without taper
Op = pylops.signalprocessing.FFT2D(nwin, nffts=nop)
Slid = pylops.signalprocessing.Patch2D(
Op.H, dims, dimsd, nwin, nover, nop, tapertype=None, design=False
)
fftdata = Slid.H * data.ravel()
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.
Slid = pylops.signalprocessing.Patch2D(
Op.H, dims, dimsd, nwin, nover, nop, tapertype="hanning", design=False
)
reconstructed_data = Slid * fftdata.ravel()
reconstructed_data = np.real(reconstructed_data.reshape(dimsd))
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[nop[0] * nop[1] * (iwin - 1) : nop[0] * nop[1] * iwin].reshape(nop)
)
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.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()

Total running time of the script: ( 0 minutes 1.085 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.ravel()
B = np.reshape(B, (N, M))
After applying smoothing, we will also try to invert it.
Aest = Sop / B.ravel()
Aest = np.reshape(Aest, (N, M))
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])

Out:
<matplotlib.colorbar.Colorbar object at 0x7f4dec6e7a58>
Total running time of the script: ( 0 minutes 0.594 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 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, 40
theta = np.linspace(thetamin, thetamax, ntheta)
# Elastic property profiles
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[201:] += 500
vs[201:] += 200
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 = np.linspace(0.4, 0.6, nt0)
# 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(m[:, 0], t0, "k", lw=6)
axs[0].set_title("Vp")
axs[0].set_ylabel(r"$t(s)$")
axs[0].invert_yaxis()
axs[0].grid()
axs[1].plot(m[:, 1], t0, "k", lw=6)
axs[1].set_title("Vs")
axs[1].invert_yaxis()
axs[1].grid()
axs[2].plot(m[:, 2], t0, "k", lw=6, label="true")
axs[2].set_title("Rho")
axs[2].invert_yaxis()
axs[2].grid()
axs[2].legend()

Out:
<matplotlib.legend.Legend object at 0x7f4dec41ce80>
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.ravel()
dPP_const = dPP_const.reshape(nt0, ntheta)
dPP_variant = PPop_variant * m.ravel()
dPP_variant = dPP_variant.reshape(nt0, ntheta)
fig, axs = plt.subplots(1, 2, figsize=(10, 5), sharey=True)
axs[0].imshow(
dPP_const,
cmap="gray",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=dPP_const.min(),
vmax=dPP_const.max(),
)
axs[0].set_title("Data with constant VP/VS")
axs[0].axis("tight")
axs[1].imshow(
dPP_variant,
cmap="gray",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=dPP_variant.min(),
vmax=dPP_variant.max(),
)
axs[1].set_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.ravel()
dPS = dPS.reshape(nt0, ntheta)
fig, axs = plt.subplots(1, 2, figsize=(10, 5), sharey=True)
axs[0].imshow(
dPP_const,
cmap="gray",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=dPP_const.min(),
vmax=dPP_const.max(),
)
axs[0].set_title("PP Data")
axs[0].axis("tight")
axs[1].imshow(
dPS,
cmap="gray",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=dPS.min(),
vmax=dPS.max(),
)
axs[1].set_title("PS Data")
axs[1].axis("tight")
plt.tight_layout()

Total running time of the script: ( 0 minutes 1.492 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.gridspec as pltgs
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.ravel()
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.NormalEquationsInversion(
Bop, [D2op], y, epsRs=[np.sqrt(0.1)], returninfo=False, **dict(maxiter=100)
)
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.948 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.solver.cgls(
Aop, y, x0=np.zeros_like(x), niter=10, tol=1e-10, show=True
)
print("x= %s" % x)
print("cgls solution xest= %s" % xest)
Out:
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.solver.lsqr(Aop, y, x0=np.zeros_like(x), niter=10, show=True)
print("x= %s" % x)
print("lsqr solution xest= %s" % xest)
Out:
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.00000e+00 1.650e+01 1.650e+01 1.0e+00 3.4e-01
1 9.13620e-01 3.521e+00 3.521e+00 2.1e-01 1.4e-01 5.7e+00 1.0e+00
2 1.13279e+00 1.917e+00 1.917e+00 1.2e-01 8.8e-02 7.3e+00 2.1e+00
3 1.10304e+00 7.921e-01 7.921e-01 4.8e-02 3.9e-02 9.0e+00 3.5e+00
4 1.03663e+00 3.992e-01 3.992e-01 2.4e-02 1.3e-02 1.1e+01 4.7e+00
5 1.00858e+00 1.463e-01 1.463e-01 8.9e-03 5.1e-03 1.1e+01 6.2e+00
6 1.00687e+00 8.099e-02 8.099e-02 4.9e-03 3.3e-03 1.2e+01 7.4e+00
7 9.99808e-01 3.898e-02 3.898e-02 2.4e-03 1.5e-03 1.3e+01 8.8e+00
8 9.99356e-01 1.930e-02 1.930e-02 1.2e-03 7.2e-04 1.4e+01 1.0e+01
9 1.00062e+00 3.082e-03 3.082e-03 1.9e-04 8.3e-05 1.4e+01 1.2e+01
10 1.00000e+00 4.480e-15 4.480e-15 2.7e-16 3.1e-16 1.4e+01 1.3e+01
LSQR finished, Opx - 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.278 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, halfcurrent=True)
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")

Out:
Text(0.5, 1.0, 'Inverse causal integration = Derivative')
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.RegularizedInversion(
Cop, [Rop], yn, epsRs=[1e0], **dict(iter_lim=100, atol=1e-5)
)
# Preconditioned derivative
Sop = pylops.Smoothing1D(41, nt)
xp = pylops.PreconditionedInversion(Cop, Sop, yn, **dict(iter_lim=10, atol=1e-3))
# 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")

Out:
Text(0.5, 1.0, 'Inverse causal integration')
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(
nt * nx, dims=(nt, nx), sampling=dt, dir=0, halfcurrent=True
)
y = Cop * x.ravel()
y = y.reshape(nt, nx)
yn = y + np.random.normal(0, 4e-1, y.shape)
# Numerical derivative
Dop = pylops.FirstDerivative(nt * nx, dims=(nt, nx), dir=0, sampling=dt)
xder = Dop * yn.ravel()
xder = xder.reshape(nt, nx)
# Regularized derivative
Rop = pylops.Laplacian(dims=(nt, nx))
xreg = pylops.RegularizedInversion(
Cop, [Rop], yn.ravel(), epsRs=[1e0], **dict(iter_lim=100, atol=1e-5)
)
xreg = xreg.reshape(nt, nx)
# Preconditioned derivative
Sop = pylops.Smoothing2D((11, 21), dims=(nt, nx))
xp = pylops.PreconditionedInversion(
Cop, Sop, yn.ravel(), **dict(iter_lim=10, atol=1e-2)
)
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")
# 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")
Out:
Text(0.5, 1.0, 'Inverse causal integration')
Total running time of the script: ( 0 minutes 2.556 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,
]
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.ravel()
dadj_chirp = R2Op.H * dL_chirp
dinv_chirp = R2Op.inverse(dL_chirp)
dL_chirp = dL_chirp.reshape(par["nx"], par["nt"])
dadj_chirp = dadj_chirp.reshape(par["nx"], par["nt"])
dinv_chirp = dinv_chirp.reshape(par["nx"], par["nt"])
fig, axs = plt.subplots(1, 4, figsize=(12, 4))
axs[0].imshow(d.T, vmin=-1, vmax=1, cmap="seismic_r", extent=(x[0], x[-1], t[-1], t[0]))
axs[0].set_title("Input model")
axs[0].axis("tight")
axs[1].imshow(
dL_chirp.T,
cmap="seismic_r",
vmin=-dL_chirp.max(),
vmax=dL_chirp.max(),
extent=(px[0], px[-1], t[-1], t[0]),
)
axs[1].set_title("Radon Chirp")
axs[1].axis("tight")
axs[2].imshow(
dadj_chirp.T,
cmap="seismic_r",
vmin=-dadj_chirp.max(),
vmax=dadj_chirp.max(),
extent=(px[0], px[-1], t[-1], t[0]),
)
axs[2].set_title("Adj Radon Chirp")
axs[2].axis("tight")
axs[3].imshow(
dinv_chirp.T,
cmap="seismic_r",
vmin=-d.max(),
vmax=d.max(),
extent=(px[0], px[-1], t[-1], t[0]),
)
axs[3].set_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.ravel()
dadj_chirp = R3Op.H * dL_chirp
dinv_chirp = R3Op.inverse(dL_chirp)
dL_chirp = dL_chirp.reshape(par["ny"], par["nx"], par["nt"])
dadj_chirp = dadj_chirp.reshape(par["ny"], par["nx"], par["nt"])
dinv_chirp = dinv_chirp.reshape(par["ny"], par["nx"], par["nt"])
fig, axs = plt.subplots(1, 4, figsize=(12, 4))
axs[0].imshow(
d[par["ny"] // 2].T,
vmin=-1,
vmax=1,
cmap="seismic_r",
extent=(x[0], x[-1], t[-1], t[0]),
)
axs[0].set_title("Input model")
axs[0].axis("tight")
axs[1].imshow(
dL_chirp[par["ny"] // 2].T,
cmap="seismic_r",
vmin=-dL_chirp.max(),
vmax=dL_chirp.max(),
extent=(px[0], px[-1], t[-1], t[0]),
)
axs[1].set_title("Radon Chirp")
axs[1].axis("tight")
axs[2].imshow(
dadj_chirp[par["ny"] // 2].T,
cmap="seismic_r",
vmin=-dadj_chirp.max(),
vmax=dadj_chirp.max(),
extent=(px[0], px[-1], t[-1], t[0]),
)
axs[2].set_title("Adj Radon Chirp")
axs[2].axis("tight")
axs[3].imshow(
dinv_chirp[par["ny"] // 2].T,
cmap="seismic_r",
vmin=-d.max(),
vmax=d.max(),
extent=(px[0], px[-1], t[-1], t[0]),
)
axs[3].set_title("Inv Radon Chirp")
axs[3].axis("tight")
plt.tight_layout()
fig, axs = plt.subplots(1, 4, figsize=(12, 4))
axs[0].imshow(
d[:, par["nx"] // 2].T,
vmin=-1,
vmax=1,
cmap="seismic_r",
extent=(x[0], x[-1], t[-1], t[0]),
)
axs[0].set_title("Input model")
axs[0].axis("tight")
axs[1].imshow(
dL_chirp[:, 2 * par["nx"] // 3].T,
cmap="seismic_r",
vmin=-dL_chirp.max(),
vmax=dL_chirp.max(),
extent=(px[0], px[-1], t[-1], t[0]),
)
axs[1].set_title("Radon Chirp")
axs[1].axis("tight")
axs[2].imshow(
dadj_chirp[:, par["nx"] // 2].T,
cmap="seismic_r",
vmin=-dadj_chirp.max(),
vmax=dadj_chirp.max(),
extent=(px[0], px[-1], t[-1], t[0]),
)
axs[2].set_title("Adj Radon Chirp")
axs[2].axis("tight")
axs[3].imshow(
dinv_chirp[:, par["nx"] // 2].T,
cmap="seismic_r",
vmin=-d.max(),
vmax=d.max(),
extent=(px[0], px[-1], t[-1], t[0]),
)
axs[3].set_title("Inv Radon Chirp")
axs[3].axis("tight")
plt.tight_layout()
Total running time of the script: ( 0 minutes 2.751 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.gridspec as pltgs
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")

Out:
Text(0.5, 1.0, 'Adjoint of Forward')
Total running time of the script: ( 0 minutes 0.457 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)

Out:
(1.9, 2.1)
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()

Out:
<matplotlib.legend.Legend object at 0x7f4dece100b8>
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),
dims=(nt, nx),
dtype="float32",
)
y = Cop * x.ravel()
xinv = Cop / y
y = y.reshape(nt, nx)
xinv = xinv.reshape(nt, nx)
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(
nx * ny * nz, h=h, offset=offset, dims=[ny, nx, nz], dirs=[0, 1, 2], dtype="float32"
)
y = Cop * x.ravel()
xinv = lsqr(Cop, y, damp=0, iter_lim=300, show=0)[0]
y = y.reshape(ny, nx, nz)
xlsqr = xinv.reshape(ny, nx, nz)
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")

Out:
(-0.5, 9.5, 12.5, -0.5)
Total running time of the script: ( 0 minutes 3.235 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)

Out:
<matplotlib.colorbar.Colorbar object at 0x7f4deca710b8>
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))
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, dims=(nx, ny), dir=0, dtype="float64")
B = np.reshape(D1op * A.ravel(), (nx, ny))
fig, axs = plt.subplots(1, 2, figsize=(10, 3))
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(nx * ny, dims=(nx, ny), dir=0, dtype="float64")
B = np.reshape(D2op * A.ravel(), (nx, ny))
fig, axs = plt.subplots(1, 2, figsize=(10, 3))
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 (dir=1
)
D2op = pylops.SecondDerivative(nx * ny, dims=(nx, ny), dir=1, dtype="float64")
B = np.reshape(D2op * np.ndarray.flatten(A), (nx, ny))
fig, axs = plt.subplots(1, 2, figsize=(10, 3))
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 = np.reshape(L2symop * A.ravel(), (nx, ny))
Basym = np.reshape(L2asymop * A.ravel(), (nx, ny))
fig, axs = plt.subplots(1, 3, figsize=(10, 3))
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 = np.reshape(Gop * A.ravel(), (2 * nx, ny))
C = np.reshape(Gop.H * B.ravel(), (nx, ny))
fig, axs = plt.subplots(1, 3, figsize=(10, 3))
fig.suptitle("Gradient", 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])
im = axs[2].imshow(C, interpolation="nearest", cmap="rainbow")
axs[2].axis("tight")
axs[2].set_title("xadj")
plt.colorbar(im, ax=axs[2])
plt.tight_layout()
plt.subplots_adjust(top=0.8)

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.ravel()
dirder = dirder.reshape(nz, nx)
dir2der = D2dop * A.ravel()
dir2der = dir2der.reshape(nz, nx)
jump = 4
fig, axs = plt.subplots(3, 1, figsize=(4, 9))
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 4.599 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)
Out:
A
where: {'A': 'MatrixMult'}
A.T
where: {'A': 'MatrixMult'}
Adjoint(A)
where: {'A': 'MatrixMult'}
3*A
where: {'A': 'MatrixMult'}
A + U
where: {'A': 'MatrixMult', 'U': '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)
Out:
The user has used the same name A for two distinct operators, changing name of operator Diagonal to S...
A*S
where: {'A': 'MatrixMult', 'S': '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)
Out:
Matrix([[A*S, U*S]])
where: {'A': 'MatrixMult', 'S': 'Diagonal', 'U': 'MatrixMult'}
Matrix([
[Matrix([[A*S, U]])],
[ Matrix([[A, A]])]])
where: {'A': 'MatrixMult', 'S': 'Diagonal', 'U': '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.483 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, 3))
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)

Out:
<matplotlib.colorbar.Colorbar object at 0x7f4dec02da58>
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("x =\n%s" % x)
d = np.arange(nx * ny).reshape(nx, ny)
Dop = pylops.Diagonal(d)
y = Dop * x.ravel()
y1 = Dop.H * x.ravel()
print("y = D*x =\n%s" % y.reshape(nx, ny))
print("xadj = D'*x =\n%s " % y1.reshape(nx, ny))
Out:
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("x =\n%s" % x)
# 1st dim
d = np.arange(nx)
Dop = pylops.Diagonal(d, dims=(nx, ny), dir=0)
y = Dop * x.ravel()
y1 = Dop.H * x.ravel()
print("1st dim: y = D*x =\n%s" % y.reshape(nx, ny))
print("1st dim: xadj = D'*x =\n%s " % y1.reshape(nx, ny))
# 2nd dim
d = np.arange(ny)
Dop = pylops.Diagonal(d, dims=(nx, ny), dir=1)
y = Dop * x.ravel()
y1 = Dop.H * x.ravel()
print("2nd dim: y = D*x =\n%s" % y.reshape(nx, ny))
print("2nd dim: xadj = D'*x =\n%s " % y1.reshape(nx, ny))
Out:
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.388 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()

Out:
<matplotlib.legend.Legend object at 0x7f4dec4f8fd0>
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, dims=(nt, nx), dir=0)
y = Fop * x.ravel()
xadj = Fop.H * y.ravel()
y = y.reshape(nt, nx)
xadj = xadj.reshape(nt, nx)
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(nt * nx, dims=(nt, nx), dir=1)
y = Fop * x.ravel()
xadj = Fop.H * y.ravel()
y = y.reshape(nt, nx)
xadj = xadj.reshape(nt, nx)
# 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 1.092 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, "--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])

Out:
(0.0, 30.0)
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, "--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])

Out:
(0.0, 30.0)
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), dir=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="seismic")
axs[0][0].set_title("Signal")
axs[0][0].axis("tight")
axs[0][1].imshow(np.abs(D.reshape(nfft, nx)[:200, :]), cmap="seismic")
axs[0][1].set_title("Fourier Transform")
axs[0][1].axis("tight")
axs[1][0].imshow(dinv, vmin=-20, vmax=20, cmap="seismic")
axs[1][0].set_title("Inverted")
axs[1][0].axis("tight")
axs[1][1].imshow(d - dinv, vmin=-20, vmax=20, cmap="seismic")
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="seismic")
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="seismic"
)
axs[0][1].set_title("Fourier Transform")
axs[0][1].axis("tight")
axs[1][0].imshow(dinv, vmin=-100, vmax=100, cmap="seismic")
axs[1][0].set_title("Inverted")
axs[1][0].axis("tight")
axs[1][1].imshow(d - dinv, vmin=-100, vmax=100, cmap="seismic")
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="seismic")
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="seismic",
)
axs[0][1].set_title("Fourier Transform")
axs[0][1].axis("tight")
axs[1][0].imshow(dinv[:, :, ny // 2], vmin=-20, vmax=20, cmap="seismic")
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="seismic"
)
axs[1][1].set_title("Error")
axs[1][1].axis("tight")
fig.tight_layout()

Total running time of the script: ( 0 minutes 3.281 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, 3))
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)

Out:
<matplotlib.colorbar.Colorbar object at 0x7f4dec39d5c0>
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("x = %s " % x)
print("I*x = %s " % y)
print("I'*y = %s " % xadj)
Out:
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("x = %s " % x)
print("I*x = %s " % y)
print("I'*y = %s " % xadj)
Out:
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.290 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.gridspec as pltgs
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")

Out:
Text(0.5, 1.0, 'Adjoint of Forward')
Total running time of the script: ( 0 minutes 0.441 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=r"true: $x_0$ = %.2f, $x_1$ = %.2f" % (x[0], x[1]),
)
plt.plot(
np.array([t.min(), t.max()]),
np.array([t.min(), t.max()]) * xest[1] + xest[0],
"--r",
lw=4,
label=r"est noise-free: $x_0$ = %.2f, $x_1$ = %.2f" % (xest[0], xest[1]),
)
plt.plot(
np.array([t.min(), t.max()]),
np.array([t.min(), t.max()]) * xnest[1] + xnest[0],
"--g",
lw=4,
label=r"est noisy: $x_0$ = %.2f, $x_1$ = %.2f" % (xnest[0], xnest[1]),
)
plt.scatter(t, y, c="r", s=70)
plt.scatter(t, yn, c="g", s=70)
plt.legend()

Out:
<matplotlib.legend.Legend object at 0x7f4debeb9e10>
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()

Out:
<matplotlib.legend.Legend object at 0x7f4deca28da0>
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 = LRop / yn
xirls, nouter, xirls_hist, rw_hist = pylops.optimization.sparsity.IRLS(
LRop,
yn,
nouter,
threshR=False,
epsR=epsR,
epsI=epsI,
tolIRLS=tolIRLS,
returnhistory=True,
)
print("IRLS converged at %d iterations..." % nouter)
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=r"true: $x_0$ = %.2f, $x_1$ = %.2f" % (x[0], x[1]),
)
plt.plot(
np.array([t.min(), t.max()]),
np.array([t.min(), t.max()]) * xnest[1] + xnest[0],
"--r",
lw=4,
label=r"L2: $x_0$ = %.2f, $x_1$ = %.2f" % (xnest[0], xnest[1]),
)
plt.plot(
np.array([t.min(), t.max()]),
np.array([t.min(), t.max()]) * xirls[1] + xirls[0],
"--g",
lw=4,
label=r"L1 - IRSL: $x_0$ = %.2f, $x_1$ = %.2f" % (xirls[0], xirls[1]),
)
plt.scatter(t, y, c="r", s=70)
plt.scatter(t, yn, c="g", s=70)
plt.legend()

Out:
IRLS converged at 14 iterations...
<matplotlib.legend.Legend object at 0x7f4dec3b6320>
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.786 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, maxit, niter_inner=0, sigma=1e-4)[0]
x_omp = pylops.optimization.sparsity.OMP(Aop, y, maxit, sigma=1e-4)[0]
# IRLS
x_irls = pylops.optimization.sparsity.IRLS(
Aop, y, 50, epsI=1e-5, kind="model", **dict(iter_lim=10)
)[0]
# ISTA
x_ista = pylops.optimization.sparsity.ISTA(
Aop, y, maxit, eps=eps, tol=1e-3, returninfo=True
)[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.RegularizedInversion
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, returninfo=True
)
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.RegularizedInversion(
Cop, [], yn, returninfo=False, **dict(damp=1e-1, atol=1e-3, iter_lim=100, show=0)
)
xista, niteri, costi = pylops.optimization.sparsity.ISTA(
Cop, yn, niter=100, eps=5e-1, tol=1e-5, returninfo=True
)
xfista, niterf, costf = pylops.optimization.sparsity.FISTA(
Cop, yn, niter=100, eps=5e-1, tol=1e-5, returninfo=True
)
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 3.023 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([])
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([])
Out:
[Text(0, 0.5, ''), Text(0, 1.5, ''), Text(0, 2.5, ''), Text(0, 3.5, ''), Text(0, 4.5, ''), Text(0, 5.5, ''), Text(0, 6.5, ''), Text(0, 7.5, ''), Text(0, 8.5, ''), Text(0, 9.5, ''), Text(0, 10.5, ''), Text(0, 11.5, ''), Text(0, 12.5, ''), Text(0, 13.5, ''), Text(0, 14.5, ''), Text(0, 15.5, ''), Text(0, 16.5, ''), Text(0, 17.5, ''), Text(0, 18.5, '')]
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()

Out:
<matplotlib.legend.Legend object at 0x7f4decbc5c88>
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("A= %s" % Aop.A.todense())
print("A^-1=", Aop.inv().todense())
print("eigs=", Aop.eigs())
print("x= %s" % x)
print("y= %s" % y)
print("lsqr solution xest= %s" % xest)
Out:
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 dims
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, dims=(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("A= %s" % A)
print("x= %s" % x)
print("y= %s" % y)
print("lsqr solution xest= %s" % xest)
Out:
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.532 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,
transpose=False,
dtype="float32",
)
# 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.918 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(11 * 21, dims=[11, 21], dir=1, dtype="float32")
D2vop = pylops.SecondDerivative(11 * 21, dims=[11, 21], dir=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 = np.reshape(D2op * X.ravel(), (Nv, Nh))
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)

Out:
BD Operator multiprocessing pool <multiprocessing.pool.Pool object at 0x7f4de9654a20>
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("AB = \n", AB)
print("x = ", x)
print("y = ", y)
print("yop = ", yop)
print("xinv = ", x)
Out:
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 dir
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 3.893 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()

Out:
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()

Out:
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 1.281 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.gridspec as pltgs
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("x = %s " % x)
print("P*x = %s " % y)
print("P'*y = %s " % xadj)
Out:
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.ravel()
xadj = Pop.H * y
y = y.reshape(Pop.dimsd)
xadj = xadj.reshape(dims)
fig, axs = plt.subplots(1, 3, figsize=(10, 2))
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")

Out:
(-0.5, 3.5, 4.5, -0.5)
Total running time of the script: ( 0 minutes 0.363 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")

Out:
Text(0.5, 1.0, 'Backward propagation')
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")

Out:
Text(0.5, 1.0, 'Backward propagation')
Total running time of the script: ( 0 minutes 1.626 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")

Out:
<matplotlib.legend.Legend object at 0x7f4dec2b4550>
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, xirls_hist, rw_hist = pylops.optimization.sparsity.IRLS(
PRop,
yn,
nouter,
threshR=False,
epsR=epsR,
epsI=epsI,
tolIRLS=tolIRLS,
returnhistory=True,
)
print("IRLS converged at %d iterations..." % nouter)
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")

Out:
IRLS converged at 4 iterations...
<matplotlib.legend.Legend object at 0x7f4dec7695f8>
Total running time of the script: ( 0 minutes 0.644 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 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 = (
1200 + np.arange(nt0) + filtfilt(np.ones(5) / 5.0, 1, np.random.normal(0, 160, nt0))
)
vs = 600 + vp / 2 + 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:] += 500
vs[201:] += 200
rho[201:] += 100
# Wavelet
ntwav = 81
wav, twav, wavc = ricker(t0[: ntwav // 2 + 1], 5)
# vs/vp profile
vsvp = 0.5
vsvp_z = np.linspace(0.4, 0.6, nt0)
# Model
m = np.stack((np.log(vp), np.log(vs), np.log(rho)), axis=1)
fig, axs = plt.subplots(1, 3, figsize=(13, 7), sharey=True)
axs[0].plot(vp, t0, "k")
axs[0].set_title("Vp")
axs[0].set_ylabel(r"$t(s)$")
axs[0].invert_yaxis()
axs[0].grid()
axs[1].plot(vs, t0, "k")
axs[1].set_title("Vs")
axs[1].invert_yaxis()
axs[1].grid()
axs[2].plot(rho, t0, "k")
axs[2].set_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.ravel()
dPP_const = dPP_const.reshape(nt0, ntheta)
dPP_variant = PPop_variant * m.ravel()
dPP_variant = dPP_variant.reshape(nt0, ntheta)
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)
ax3 = plt.subplot2grid((3, 2), (2, 0))
ax4 = plt.subplot2grid((3, 2), (2, 1))
ax1.imshow(
dPP_const,
cmap="bwr",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=-0.1,
vmax=0.1,
)
ax1.set_xlabel(r"$\Theta$")
ax1.set_ylabel(r"$t(s)$")
ax1.set_title(r"Data with constant $VP/VS$", fontsize=10)
ax1.axis("tight")
ax2.imshow(
dPP_variant,
cmap="bwr",
extent=(theta[0], theta[-1], t0[-1], t0[0]),
vmin=-0.1,
vmax=0.1,
)
ax2.set_title(r"Data with depth-variant $VP/VS$", fontsize=10)
ax2.set_xlabel(r"$\Theta$")
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_title("AVO curve at t=%.2f s" % t0[nt0 // 4], fontsize=10)
ax3.set_xlabel(r"$\Theta$")
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_title("AVO curve at t=%.2f s" % t0[nt0 // 2], fontsize=10)
ax4.set_xlabel(r"$\Theta$")
ax4.legend()
plt.tight_layout()

Total running time of the script: ( 0 minutes 1.271 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.ravel()
yP = RPop * x.ravel()
yH = RHop * x.ravel()
yL = yL.reshape(nh, nt)
yP = yP.reshape(nh, nt)
yH = yH.reshape(nh, nt)
# adjoint
xadjL = RLop.H * yL.ravel()
xadjP = RPop.H * yP.ravel()
xadjH = RHop.H * yH.ravel()
xadjL = xadjL.reshape(npx, nt)
xadjP = xadjP.reshape(npx, nt)
xadjH = xadjH.reshape(npx, nt)
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))
axs[0][0].imshow(
x.T, vmin=-1, vmax=1, cmap="seismic_r", extent=(px[0], px[-1], t[-1], t[0])
)
axs[0][0].set_title("Input model")
axs[0][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].set_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_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_title("Hyperbolic data")
axs[0][3].axis("tight")
axs[1][1].imshow(
xadjL.T, vmin=-20, vmax=20, cmap="seismic_r", extent=(px[0], px[-1], t[-1], t[0])
)
axs[1][0].axis("off")
axs[1][1].set_title("Linear adjoint")
axs[1][1].axis("tight")
axs[1][2].imshow(
xadjP.T, vmin=-20, vmax=20, cmap="seismic_r", extent=(px[0], px[-1], t[-1], t[0])
)
axs[1][2].set_title("Parabolic adjoint")
axs[1][2].axis("tight")
axs[1][3].imshow(
xadjH.T, vmin=-20, vmax=20, cmap="seismic_r", extent=(px[0], px[-1], t[-1], t[0])
)
axs[1][3].set_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.ravel()
yP = RPop * x.ravel()
yH = RHop * x.ravel()
yL = yL.reshape(ny, nx, nt)
yP = yP.reshape(ny, nx, nt)
yH = yH.reshape(ny, nx, nt)
# adjoint
xadjL = RLop.H * yL.ravel()
xadjP = RPop.H * yP.ravel()
xadjH = RHop.H * yH.ravel()
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))
axs[0][0].imshow(
x[npy // 2].T,
vmin=-1,
vmax=1,
cmap="seismic_r",
extent=(px[0], px[-1], t[-1], t[0]),
)
axs[0][0].set_title("Input model")
axs[0][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].set_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_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_title("Hyperbolic data")
axs[0][3].axis("tight")
axs[1][1].imshow(
xadjL[npy // 2].T,
vmin=-100,
vmax=100,
cmap="seismic_r",
extent=(px[0], px[-1], t[-1], t[0]),
)
axs[1][0].axis("off")
axs[1][1].set_title("Linear adjoint")
axs[1][1].axis("tight")
axs[1][2].imshow(
xadjP[npy // 2].T,
vmin=-100,
vmax=100,
cmap="seismic_r",
extent=(px[0], px[-1], t[-1], t[0]),
)
axs[1][2].set_title("Parabolic adjoint")
axs[1][2].axis("tight")
axs[1][3].imshow(
xadjH[npy // 2].T,
vmin=-100,
vmax=100,
cmap="seismic_r",
extent=(px[0], px[-1], t[-1], t[0]),
)
axs[1][3].set_title("Hyperbolic adjoint")
axs[1][3].axis("tight")
fig.tight_layout()
fig, axs = plt.subplots(2, 4, figsize=(10, 6))
axs[0][0].imshow(
x[:, npx // 2 - 2].T,
vmin=-1,
vmax=1,
cmap="seismic_r",
extent=(py[0], py[-1], t[-1], t[0]),
)
axs[0][0].set_title("Input model")
axs[0][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].set_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_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_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=(py[0], py[-1], t[-1], t[0]),
)
axs[1][0].axis("off")
axs[1][1].set_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=(py[0], py[-1], t[-1], t[0]),
)
axs[1][2].set_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=(py[0], py[-1], t[-1], t[0]),
)
axs[1][3].set_title("Hyperbolic adjoint")
axs[1][3].axis("tight")
fig.tight_layout()
Total running time of the script: ( 0 minutes 4.346 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.gridspec as pltgs
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")

Out:
Text(0.5, 1.0, 'Adjoint of Forward')
Total running time of the script: ( 0 minutes 0.441 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])

Out:
(-0.5, 0.5)
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, dims=(nx, nt), dir=0, dtype="float64")
y = (Rop * x.ravel()).reshape(nxsub, nt)
ymask = Rop.mask(x)
fig, axs = plt.subplots(1, 3, figsize=(10, 5))
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")

Out:
(-0.5, 99.5, 49.5, -0.5)
Total running time of the script: ( 0 minutes 0.891 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()

Out:
<matplotlib.legend.Legend object at 0x7f4de961e4e0>
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(ny * nx, dims=(ny, nx), dir=1, shift=-2)
y = Rop * x.ravel()
xadj = Rop.H * y
y = y.reshape(ny, nx)
xadj = xadj.reshape(ny, nx)
fig, axs = plt.subplots(1, 3, figsize=(10, 2))
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")

Out:
(-0.5, 4.5, 9.5, -0.5)
Total running time of the script: ( 0 minutes 0.541 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 *= -1 # t-axis points down
# 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, 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.T, sampling=(dx, dt))
seis = Sop * d.ravel()
seis = seis.reshape(nx, nt)
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.T), sampling=(dx, dt))
dwt = Wop * d.ravel()
dwt = dwt.reshape(nx, nt)
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)
d_seis = d_seis.reshape(nx, nt)
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)
d_dwt = d_dwt.reshape(nx, nt)
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, nt * nx, nt * nx, verb=True)
Out:
Dot test passed, v^T(Opu)=-84.409959 - u^T(Op^Tv)=-84.409959
True
Total running time of the script: ( 1 minutes 58.433 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*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 dir
wav2d = np.outer(wav, np.ones(10))
Op = pylops.signalprocessing.Shift(
(nt, 10), shift, dir=0, sampling=dt, real=True, dtype=np.float64
)
wav2dshift = (Op * wav2d.ravel()).reshape(nt, 10)
wav2dshiftback = (Op.H * wav2dshift.ravel()).reshape(nt, 10)
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 dir
wav2d = np.outer(wav, np.ones(10)).T
Op = pylops.signalprocessing.Shift(
(10, nt), shift, dir=1, sampling=dt, real=True, dtype=np.float64
)
wav2dshift = (Op * wav2d.ravel()).reshape(10, nt)
wav2dshiftback = (Op.H * wav2dshift.ravel()).reshape(10, nt)
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.308 seconds)
Note
Click here to download the full example code
Slope estimation via Structure Tensor algorithm¶
This example shows how to estimate local slopes of a two-dimensional array
using pylops.utils.signalprocessing.slope_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.
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.axes_grid1 import make_axes_locatable
import pylops
from pylops.signalprocessing.Seislet import _predict_trace
plt.close("all")
np.random.seed(10)
To start we import a 2d image and estimate the local slopes of the image.
im = np.load("../testdata/python.npy")[..., 0]
im = im / 255.0 - 0.5
slopes, anisotropy = pylops.utils.signalprocessing.slope_estimate(im, smooth=7)
angles = -np.rad2deg(np.arctan(slopes))
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="RdBu_r", 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, 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()
![Data, Angle of incline [°], Anisotropy](_images/sphx_glr_plot_slopeest_001.png)
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, _ = pylops.utils.signalprocessing.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="RdBu_r", 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()

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 nature.
Total running time of the script: ( 0 minutes 2.228 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.ravel()
mlinwavR = mlinwavR.reshape(npx, par["nt"])
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.ravel()
mlinwavROTF = mlinwavROTF.reshape(npx, par["nt"])
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.ravel()
mlinwavRPC = mlinwavRPC.reshape(npx, par["nt"])
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 17.071 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), dir=0)
y = Sop * x.ravel()
xadj = Sop.H * y
xadj = xadj.reshape(ny, nx)
gs = pltgs.GridSpec(1, 7)
fig = plt.figure(figsize=(7, 3))
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")

Out:
(-0.5, 6.5, 4.5, -0.5)
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.247 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()

Out:
<matplotlib.legend.Legend object at 0x7f4dec9be668>
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, dims=(nt, nx), dir=0)
y = Sop * x.ravel()
xadj = Sop.H * y.ravel()
xinv = Sop / y
y = y.reshape(2 * nt - 1, nx)
xadj = xadj.reshape(nt, nx)
xinv = xinv.reshape(nt, nx)
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, dims=(nt, nx), dir=1)
y = Sop * x.ravel()
xadj = Sop.H * y.ravel()
xinv = Sop / y
y = y.reshape(nt, 2 * nx - 1)
xadj = xadj.reshape(nt, nx)
xinv = xinv.reshape(nt, nx)
# 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 1.218 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)$")
Out:
Text(0.5, 25.722222222222214, '$y(m)$')
Total running time of the script: ( 0 minutes 4.073 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()
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()

Out:
<matplotlib.legend.Legend object at 0x7f4debeb95c0>
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")
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")
Out:
Text(87.24078657865785, 0.5, 'y')
Total running time of the script: ( 0 minutes 0.612 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")

Out:
Text(0.5, 1.0, 'Model and data')
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.RegularizedInversion(
Iop, [D2op], y, epsRs=[np.sqrt(lamda / 2)], **dict(iter_lim=30)
)
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")

Out:
Text(0.5, 1.0, 'L2 inversion')
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, niter = pylops.optimization.sparsity.SplitBregman(
Iop,
[Dop],
y,
niter_out,
niter_in,
mu=mu,
epsRL1s=[lamda],
tol=1e-4,
tau=1.0,
**dict(iter_lim=30, damp=1e-10)
)
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")

Out:
Text(0.5, 1.0, 'TV inversion')
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, 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")

Out:
(-0.5, 169.5, 194.5, -0.5)
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, dims=(ny, nx), dir=0, edge=False, kind="backward", dtype=np.complex128
),
pylops.FirstDerivative(
ny * nx, dims=(ny, nx), dir=1, edge=False, kind="backward", dtype=np.complex128
),
]
# TV
mu = 1.5
lamda = [0.1, 0.1]
niter = 20
niterinner = 10
xinv, niter = pylops.optimization.sparsity.SplitBregman(
Rop * Fop,
Dop,
y.ravel(),
niter,
niterinner,
mu=mu,
epsRL1s=lamda,
tol=1e-4,
tau=1.0,
show=False,
**dict(iter_lim=5, damp=1e-4)
)
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")
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()
Out:
<matplotlib.legend.Legend object at 0x7f4dec22c4a8>
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 33.108 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.ravel()
xadj = Top.H * y
y = y.reshape(Top.dimsd)
xadj = xadj.reshape(Top.dims)
fig, axs = plt.subplots(1, 3, figsize=(10, 2))
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")

Out:
(-0.5, 39.5, 19.5, -0.5)
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.343 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 = 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[201:] += 500
vs[201:] += 200
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 = np.linspace(0.4, 0.6, nt0)
# Model
m = np.stack((np.log(vp), np.log(vs), np.log(rho)), axis=1)
fig, axs = plt.subplots(1, 3, figsize=(13, 7), sharey=True)
axs[0].plot(vp, t0, "k")
axs[0].set_title("Vp")
axs[0].set_ylabel(r"$t(s)$")
axs[0].invert_yaxis()
axs[0].grid()
axs[1].plot(vs, t0, "k")
axs[1].set_title("Vs")
axs[1].invert_yaxis()
axs[1].grid()
axs[2].plot(rho, t0, "k")
axs[2].set_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 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$")
axs[0].set_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$")

Out:
Text(0.5, 47.7222222222222, '$\\Theta$')
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.RegularizedInversion(
Wavesop,
[D2op],
dn.T.ravel(),
epsRs=[np.sqrt(0.1)],
returninfo=True,
**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")

Out:
(-2.0, 42.0, -0.517181247702497, 1.0722467265486844)
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.PreconditionedInversion(
Wavesop,
Sop,
dn.T.ravel(),
returninfo=False,
**dict(damp=np.sqrt(1e-4), iter_lim=200, show=0)
)
wavn_smooth_est = pylops.optimization.leastsquares.PreconditionedInversion(
Wavesop,
Sop * Smop,
dn.T.ravel(),
returninfo=False,
**dict(damp=np.sqrt(1e-4), iter_lim=200, show=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")

Out:
<matplotlib.legend.Legend object at 0x7f4debefae10>
Total running time of the script: ( 0 minutes 2.764 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.figure(figsize=(8, 2))
plt.plot(x, "k", label="Original")
plt.plot(xinv, "r", label="Reconstructed")
plt.title("Reconstructed signal")
Out:
Text(0.5, 1.0, 'Reconstructed signal')
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.ravel()
yf = y.copy()
yf[len(y) // 4 :] = 0
iminv = Wop.H * yf
iminv = iminv.reshape(Nz, Nx)
fig, axs = plt.subplots(1, 3, figsize=(12, 4))
axs[0].imshow(im, cmap="gray")
axs[0].set_title("Image")
axs[0].axis("tight")
axs[1].imshow(y.reshape(Wop.dimsd), cmap="gray_r", vmin=-1e2, vmax=1e2)
axs[1].set_title("DWT2 coefficients")
axs[1].axis("tight")
axs[2].imshow(iminv, cmap="gray")
axs[2].set_title("Reconstructed image")
axs[2].axis("tight")

Out:
(-0.5, 125.5, 125.5, -0.5)
Total running time of the script: ( 0 minutes 0.756 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, 3))
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)

Out:
<matplotlib.colorbar.Colorbar object at 0x7f4dec13f710>
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("x = %s" % x)
print("0*x = %s" % y)
print("0'*y = %s" % xadj)
Out:
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("x = %s" % x)
print("0*x = %s" % y)
print("0'*y = %s" % xadj)
Out:
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.290 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 operaotor. 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 installed in my system (**``cupy-cudaXX<8.1.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
. 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¶
LinearOperator ([Op, explicit, clinear]) |
Common interface for performing matrix-vector products. |
FunctionOperator (f, *args, **kwargs) |
Function Operator. |
MemoizeOperator (Op[, max_neval]) |
Memoize Operator. |
Basic operators¶
MatrixMult (A[, dims, dtype]) |
Matrix multiplication. |
Identity (N[, M, dtype, inplace]) |
Identity operator. |
Zero (N[, M, dtype]) |
Zero operator. |
Diagonal (diag[, dims, dir, dtype]) |
Diagonal operator. |
Transpose (dims, axes[, dtype]) |
Transpose operator. |
Flip (N[, dims, dir, dtype]) |
Flip along an axis. |
Roll (N[, dims, dir, shift, dtype]) |
Roll along an axis. |
Pad (dims, pad[, dtype]) |
Pad operator. |
Sum (dims, dir[, dtype]) |
Sum operator. |
Symmetrize (N[, dims, dir, dtype]) |
Symmetrize along an axis. |
Restriction (M, iava[, dims, dir, dtype, inplace]) |
Restriction (or sampling) operator. |
Regression (taxis, order[, dtype]) |
Polynomial regression. |
LinearRegression (taxis[, dtype]) |
Linear regression. |
CausalIntegration (N[, dims, dir, sampling, …]) |
Causal integration. |
Spread (dims, dimsd[, table, dtable, fh, …]) |
Spread operator. |
VStack (ops[, nproc, dtype]) |
Vertical stacking. |
HStack (ops[, nproc, dtype]) |
Horizontal stacking. |
Block (ops[, nproc, dtype]) |
Block operator. |
BlockDiag (ops[, nproc, dtype]) |
Block-diagonal operator. |
Kronecker (Op1, Op2[, dtype]) |
Kronecker operator. |
Real (dims[, dtype]) |
Real operator. |
Imag (dims[, dtype]) |
Imag operator. |
Conj (dims[, dtype]) |
Complex conjugate operator. |
Smoothing and derivatives¶
Smoothing1D (nsmooth, dims[, dir, dtype]) |
1D Smoothing. |
Smoothing2D (nsmooth, dims[, nodir, dtype]) |
2D Smoothing. |
FirstDerivative (N[, dims, dir, sampling, …]) |
First derivative. |
SecondDerivative (N[, dims, dir, sampling, …]) |
Second derivative. |
Laplacian (dims[, dirs, weights, sampling, …]) |
Laplacian. |
Gradient (dims[, sampling, edge, dtype, kind]) |
Gradient. |
FirstDirectionalDerivative (dims, v[, …]) |
First Directional derivative. |
SecondDirectionalDerivative (dims, v[, …]) |
Second Directional derivative. |
Signal processing¶
Convolve1D (N, h[, offset, dims, dir, dtype, …]) |
1D convolution operator. |
Convolve2D (N, h, dims[, offset, nodir, …]) |
2D convolution operator. |
ConvolveND (N, h, dims[, offset, dirs, …]) |
ND convolution operator. |
Interp (M, iava[, dims, dir, kind, dtype]) |
Interpolation operator. |
Bilinear (iava, dims[, dtype]) |
Bilinear interpolation operator. |
FFT (dims[, dir, nfft, sampling, norm, real, …]) |
One dimensional Fast-Fourier Transform. |
FFT2D (dims[, dirs, nffts, sampling, norm, …]) |
Two dimensional Fast-Fourier Transform. |
FFTND (dims[, dirs, nffts, sampling, norm, …]) |
N-dimensional Fast-Fourier Transform. |
Shift (dims, shift[, dir, nfft, sampling, …]) |
Shift operator |
DWT (dims[, dir, wavelet, level, dtype]) |
One dimensional Wavelet operator. |
DWT2D (dims[, dirs, wavelet, level, dtype]) |
Two dimensional Wavelet operator. |
Seislet (slopes[, sampling, level, kind, …]) |
Two dimensional Seislet operator. |
Radon2D (taxis, haxis, pxaxis[, kind, …]) |
Two dimensional Radon transform. |
Radon3D (taxis, hyaxis, hxaxis, pyaxis, pxaxis) |
Three dimensional Radon transform. |
ChirpRadon2D (taxis, haxis, pmax[, dtype]) |
2D Chirp Radon transform |
ChirpRadon3D (taxis, hyaxis, hxaxis, pmax[, …]) |
3D Chirp Radon transform |
Sliding1D (Op, dim, dimd, nwin, nover[, …]) |
1D Sliding transform operator. |
Sliding2D (Op, dims, dimsd, nwin, nover[, …]) |
2D Sliding transform operator. |
Sliding3D (Op, dims, dimsd, nwin, nover, nop) |
3D Sliding transform operator. |
Patch2D (Op, dims, dimsd, nwin, nover, nop[, …]) |
2D Patch transform operator. |
Fredholm1 (G[, nz, saveGt, usematmul, dtype]) |
Fredholm integral of first kind. |
Wave-Equation processing¶
PressureToVelocity (nt, nr, dt, dr, rho, vel) |
Pressure to Vertical velocity conversion. |
UpDownComposition2D (nt, nr, dt, dr, rho, vel) |
2D Up-down wavefield composition. |
UpDownComposition3D (nt, nr, dt, dr, rho, vel) |
3D Up-down wavefield composition. |
MDC (G, nt, nv[, dt, dr, twosided, fast, …]) |
Multi-dimensional convolution. |
PhaseShift (vel, dz, nt, freq, kx[, ky, dtype]) |
Phase shift operator |
Demigration (z, x, t, srcs, recs, vel, wav, …) |
Kirchoff Demigration operator. |
Geophysical subsurface characterization¶
avo.AVOLinearModelling (theta[, vsvp, nt0, …]) |
AVO Linearized modelling. |
poststack.PoststackLinearModelling (wav, nt0) |
Post-stack linearized seismic modelling operator. |
prestack.PrestackLinearModelling (wav, theta) |
Pre-stack linearized seismic modelling operator. |
prestack.PrestackWaveletModelling (m, theta, nwav) |
Pre-stack linearized seismic modelling operator for wavelet. |
Solvers¶
Basic¶
solver.cg (Op, y, x0[, niter, damp, tol, …]) |
Conjugate gradient |
solver.cgls (Op, y, x0[, niter, damp, tol, …]) |
Conjugate gradient least squares |
solver.lsqr (Op, y, x0[, damp, atol, btol, …]) |
LSQR |
Least-squares¶
leastsquares.NormalEquationsInversion (Op, …) |
Inversion of normal equations. |
leastsquares.RegularizedInversion (Op, Regs, data) |
Regularized inversion. |
leastsquares.PreconditionedInversion (Op, P, data) |
Preconditioned inversion. |
Sparsity¶
sparsity.IRLS (Op, data, nouter[, threshR, …]) |
Iteratively reweighted least squares. |
sparsity.OMP (Op, data[, niter_outer, …]) |
Orthogonal Matching Pursuit (OMP). |
sparsity.ISTA (Op, data, niter[, eps, alpha, …]) |
Iterative Shrinkage-Thresholding Algorithm (ISTA). |
sparsity.FISTA (Op, data, niter[, eps, …]) |
Fast Iterative Shrinkage-Thresholding Algorithm (FISTA). |
sparsity.SPGL1 (Op, data[, SOp, tau, sigma, x0]) |
Spectral Projected-Gradient for L1 norm. |
sparsity.SplitBregman (Op, RegsL1, data[, …]) |
Split Bregman for mixed L2-L1 norms. |
Applications¶
Wave-Equation processing¶
SeismicInterpolation (data, nrec, iava[, …]) |
Seismic interpolation (or regularization). |
Deghosting (p, nt, nr, dt, dr, vel, zrec[, …]) |
Wavefield deghosting. |
WavefieldDecomposition (p, vz, nt, nr, dt, …) |
Up-down wavefield decomposition. |
MDD (G, d[, dt, dr, nfmax, wav, twosided, …]) |
Multi-dimensional deconvolution. |
Marchenko (R[, R1, dt, nt, dr, nfmax, wav, …]) |
Marchenko redatuming |
LSM (z, x, t, srcs, recs, vel, wav, wavcenter) |
Least-squares Migration (LSM). |
Geophysical subsurface characterization¶
poststack.PoststackInversion (data, wav[, …]) |
Post-stack linearized seismic inversion. |
prestack.PrestackInversion (data, theta, wav) |
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.
Estimators¶
trace_hutchinson (Op[, neval, batch_size, …]) |
Trace of linear operator using the Hutchinson method. |
trace_hutchpp (Op[, neval, sampler, backend]) |
Trace of linear operator using the Hutch++ method. |
trace_nahutchpp (Op[, neval, sampler, c1, …]) |
Trace of linear operator using the NA-Hutch++ method. |
Scalability test¶
scalability_test (Op, x[, workers, forward]) |
Scalability test. |
Synthetics¶
seismicevents.makeaxis (par) |
Create axes t, x, and y axes |
seismicevents.linear2d (x, t, v, t0, theta, …) |
Linear 2D events |
seismicevents.parabolic2d (x, t, t0, px, pxx, …) |
Parabolic 2D events |
seismicevents.hyperbolic2d (x, t, t0, vrms, …) |
Hyperbolic 2D events |
seismicevents.linear3d (x, y, t, v, t0, …) |
Linear 3D events |
seismicevents.hyperbolic3d (x, y, t, t0, …) |
Hyperbolic 3D events |
marchenko.directwave (wav, trav, nt, dt[, …]) |
Analytical direct wave in acoustic media |
Signal-processing¶
signalprocessing.convmtx (h, n) |
Convolution matrix |
signalprocessing.nonstationary_convmtx (H, n) |
Convolution matrix from a bank of filters |
signalprocessing.slope_estimate (d[, dz, dx, …]) |
Local slope estimation |
Tapers¶
tapers.taper2d (nt, nmask, ntap[, tapertype]) |
2D taper |
tapers.taper3d (nt, nmask, ntap[, tapertype]) |
3D taper |
Wavelets¶
wavelets.ricker (t[, f0]) |
Ricker wavelet |
wavelets.gaussian (t[, std]) |
Gaussian wavelet |
Geophysical Reservoir characterization¶
avo.zoeppritz_scattering (vp1, vs1, rho1, …) |
Zoeppritz solution. |
avo.zoeppritz_element (vp1, vs1, rho1, vp0, …) |
Single element of Zoeppritz solution. |
avo.zoeppritz_pp (vp1, vs1, rho1, vp0, vs0, …) |
PP reflection coefficient from the Zoeppritz scattering matrix. |
avo.approx_zoeppritz_pp (vp1, vs1, rho1, vp0, …) |
PP reflection coefficient from the approximate Zoeppritz equation. |
avo.akirichards (theta, vsvp[, n]) |
Three terms Aki-Richards approximation. |
avo.fatti (theta, vsvp[, n]) |
Three terms Fatti approximation. |
avo.ps (theta, vsvp[, n]) |
PS reflection coefficient |
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.
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
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
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
containes 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 operator - a 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).
Contributing¶
Contributions are welcome and greatly appreciated!
The best way to get in touch with the core developers and mantainers 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.
Types of Contributions¶
Report Bugs¶
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.
Propose New Operators or 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.
Implement Operators or Features¶
Look through the Git issues for operator or feature requests. Anything tagged with enhancement is open to whoever wants to implement it.
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.
Getting Started to contribute¶
Ready to contribute?
- Fork the PyLops repo.
- Clone your fork locally:
>> git clone https://github.com/your_name_here/pylops.git
- Follow the installation instructions for developers that you find in Installation page. Ensure that you are able to pass all the tests before moving forward.
- Add the main repository to the list of your remotes (this will be important to ensure you pull the latest changes before tyring to merge your local changes):
>> git remote add upstream https://github.com/equinor/pylops
- Create a branch for local development:
>> git checkout -b name-of-your-branch
Now you can make your changes locally.
6. 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
- Commit your changes and push your branch to GitLab:
>> 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.
- 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.
Changelog¶
Version 1.17.0¶
Released on: 29/01/2022
- Added
pylops.utils.describe.describe
method - Added
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 suite - Fixed 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 axes - Fixed 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 estimation - Added x0 in
pylops.optimization.sparsity.ISTA
andpylops.optimization.sparsity.FISTA
to handle non-zero initial guess - Modified
pylops.optimization.sparsity.ISTA
andpylops.optimization.sparsity.FISTA
to handle multiple right hand sides - Modified creation of haxis in
pylops.signalprocessing.Radon2D
andpylops.signalprocessing.Radon3D
to allow for uncentered spatial axes - Fixed _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
solver - Added 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 available - Added 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 residuals - Fixed
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
operators - Added
pylops.basicoperators.MemoizeOperator
operator - Added decay and analysis option in
pylops.optimization.sparsity.ISTA
andpylops.optimization.sparsity.FISTA
solvers - Added 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 arrays - Fixed 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
operators - Added hard and half thresholds in
pylops.optimization.sparsity.ISTA
andpylops.optimization.sparsity.FISTA
solvers - Added
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
function - Fix 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
operators - Added
pylops.waveeqprocessing.PressureToVelocity
,pylops.waveeqprocessing.UpDownComposition3Doperator
, andpylops.waveeqprocessing.PhaseShift
operators - Fix 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
operators - Added
pylops.LinearOperator._ColumnLinearOperator
private operator - Added possibility to directly mix Linear operators and numpy/scipy
2d arrays in
pylops.VStack
andpylops.HStack
andpylops.BlockDiag
operators - Added
pylops.optimization.sparsity.OMP
solver
Version 1.6.0¶
Released on: 10/08/2019
- Added
pylops.signalprocessing.ConvolveND
operator - Added
pylops.utils.signalprocessing.nonstationary_convmtx
to create matrix for non-stationary convolution - Added 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
operators - Added
pylops.signalprocessing.Fredholm1
operator - Added
pylops.optimization.sparsity.SPGL1
andpylops.optimization.sparsity.SplitBregman
solvers - Sped up
pylops.signalprocessing.Convolve1D
usingscipy.signal.fftconvolve
for multi-dimensional signals - Changes in implementation of
pylops.waveeqprocessing.MDC
andpylops.waveeqprocessing.Marchenko
to take advantage of primitives operators - Added
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
operators - Added
pylops.signalprocessing.Radon3D
operator - Added
pylops.signalprocessing.Sliding2D
andpylops.signalprocessing.Sliding3D
operators - Added
pylops.signalprocessing.FFTND
operator - Added
pylops.signalprocessing.Radon3D
operator - Added
niter
option topylops.LinearOperator.eigs
method - Added
show
option topylops.optimization.sparsity.ISTA
andpylops.optimization.sparsity.FISTA
solvers - Added
pylops.waveeqprocessing.seismicinterpolation
,pylops.waveeqprocessing.waveeqdecomposition
andpylops.waveeqprocessing.lsm
submodules - Added tests for
engine
in various operators - Added documentation regarding usage of
pylops
Docker container
Version 1.3.0¶
Released on: 24/02/2019
- Added
fftw
engine topylops.signalprocessing.FFT
operator - Added
pylops.optimization.sparsity.ISTA
andpylops.optimization.sparsity.FISTA
sparse solvers - Added possibility to broadcast (handle multi-dimensional arrays)
to
pylops.Diagonal
andpylops..Restriction
operators - Added
pylops.signalprocessing.Interp
operator - Added
pylops.Spread
operator - Added
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 ARPACK - Modified 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
operator - Added
pylops.Symmetrize
operator - Added
pylops.Block
operator - Added
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 matrices - Added
pylops.avo.prestack.PrestackInversion
routine - Added possibility to have a data weight via
Weight
input parameter topylops.optimization.leastsquares.NormalEquationsInversion
andpylops.optimization.leastsquares.RegularizedInversion
solvers - Added
pylops.optimization.sparsity.IRLS
solver
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.
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¶
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
Papers¶
This section lists various conference abstracts and papers using the PyLops framework:
- Vakalis, S.Chen, D., Yan, M., and Nanzer, J. A., Image enhancement in active incoherent millimeter-wave imaging. Passive and Active Millimeter-Wave Imaging XXIV 11745 link (2021).
- Li, X., Becker, T., Ravasi, M., Robertsson, J., and van Manen D.J., Closed-aperture unbounded acoustics experimentation using multidimensional deconvolution. The Journal of the Acoustical Society of America 149 (3), 1813-1828 link (2021).
- Kuijpers, D., Vasconcelos, I., and Putzky, P., Reconstructing missing seismic data using Deep Learning. arXiv, link (2021).
- Ravasi, M., and Birnie, C., A Joint Inversion-Segmentation approach to Assisted Seismic Interpretation. arXiv, link - code (2021).
- Haindl, C., Ravasi, M., and Broggini, F., 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 link - code (2021).
- Ulrich, I. E., Zunino, A., Boehm, C., and Fichtner, A., Sparsifying regularizations for stochastic sample average minimization in ultrasound computed tomography. Medical Imaging 2021: Ultrasonic Imaging and Tomography. International Society for Optics and Photonics, link (2021).
- R Feng, R., Mejer Hansen, T., Grana, D., and Balling, N., An unsupervised deep-learning method for porosity estimation based on poststack seismic data. Geophysics, 85 (6), M97-M105. link (2020).
- Nightingale J. W., Hayes, R.G, et al., PyAutoLens: Open-Source Strong Gravitational Lensing, JOSS, link - code (2020).
- Zhang, M., Marchenko Green’s functions from compressive sensing acquisition SEG Technical Program Expanded Abstracts, link (2020).
- Vargas, D., and Vasconcelos I., Rayleigh-Marchenko Redatuming Using Scattered Fields in Highly Complex Media, EAGE Technical Program Expanded Abstracts, link (2020).
- Ravasi, M., and Vasconcelos I., Implementation of Large-Scale Integral Operators with Modern HPC Solutions, EAGE Technical Program Expanded Abstracts, link - code (2020).
- Ruan, J., and Vasconcelos I., Data-and prior-driven sampling and wavefield reconstruction for sparse, irregularly-sampled, higher-order gradient data, SEG Technical Program Expanded Abstracts, link - code (2019).
Citing¶
When using pylops
in scientific publications, please cite the following paper:
- Ravasi, M., and Vasconcelos I., PyLops–A linear-operator Python library for scalable algebra and optimization, Software X, (2020). link.
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