Commit 71ed8ec8 authored by Martin Reinecke's avatar Martin Reinecke
Browse files

Merge branch 'redesign' into 'NIFTy_5'

Redesign

See merge request ift/nifty-dev!73
parents 76e46fd8 eedcfc5d
......@@ -37,18 +37,18 @@ build_docker_from_cache:
test_python2_with_coverage:
stage: test
script:
- mpiexec -n 2 --bind-to none nosetests -q 2> /dev/null
- nosetests -q --with-coverage --cover-package=nifty5 --cover-erase
- mpiexec -n 2 --bind-to none pytest -q test 2> /dev/null
- pytest -q --cov=nifty5 test
- >
coverage report --omit "*plotting*,*distributed_do*"
python -m coverage report --omit "*plotting*,*distributed_do*"
- >
coverage report --omit "*plotting*,*distributed_do*" | grep TOTAL | awk '{ print "TOTAL: "$4; }'
python -m coverage report --omit "*plotting*,*distributed_do*" | grep TOTAL | awk '{ print "TOTAL: "$4; }'
test_python3:
stage: test
script:
- nosetests3 -q
- mpiexec -n 2 --bind-to none nosetests3 -q 2> /dev/null
- pytest-3 -q
- mpiexec -n 2 --bind-to none pytest-3 -q 2> /dev/null
pages:
stage: release
......
......@@ -10,8 +10,8 @@ RUN apt-get update && apt-get install -y \
# Documentation build dependencies
python-sphinx python-sphinx-rtd-theme python-numpydoc \
# Testing dependencies
python-nose python-parameterized \
python3-nose python3-parameterized \
python-nose python-coverage python-parameterized python-pytest python-pytest-cov \
python3-nose python3-coverage python3-parameterized python3-pytest python3-pytest-cov \
# Optional NIFTy dependencies
openmpi-bin libopenmpi-dev python-mpi4py python3-mpi4py \
# Packages needed for NIFTy
......@@ -21,7 +21,6 @@ RUN apt-get update && apt-get install -y \
&& pip install git+https://gitlab.mpcdf.mpg.de/ift/pyHealpix.git \
&& pip3 install git+https://gitlab.mpcdf.mpg.de/ift/pyHealpix.git \
# Testing dependencies
&& pip install coverage \
&& rm -rf /var/lib/apt/lists/*
# Needed for demos to be running
......
......@@ -429,8 +429,8 @@
"mask[l:h] = 0\n",
"mask = ift.Field.from_global_data(s_space, mask)\n",
"\n",
"R = ift.DiagonalOperator(mask)*HT\n",
"n = n.to_global_data().copy()\n",
"R = ift.DiagonalOperator(mask)(HT)\n",
"n = n.to_global_data_rw()\n",
"n[l:h] = 0\n",
"n = ift.Field.from_global_data(s_space, n)\n",
"\n",
......@@ -501,7 +501,7 @@
"m_data = HT(m).to_global_data()\n",
"m_var_data = m_var.to_global_data()\n",
"uncertainty = np.sqrt(m_var_data)\n",
"d_data = d.to_global_data().copy()\n",
"d_data = d.to_global_data_rw()\n",
"\n",
"# Set lost data to NaN for proper plotting\n",
"d_data[d_data == 0] = np.nan"
......@@ -585,8 +585,8 @@
"mask[l:h,l:h] = 0.\n",
"mask = ift.Field.from_global_data(s_space, mask)\n",
"\n",
"R = ift.DiagonalOperator(mask)*HT\n",
"n = n.to_global_data().copy()\n",
"R = ift.DiagonalOperator(mask)(HT)\n",
"n = n.to_global_data_rw()\n",
"n[l:h, l:h] = 0\n",
"n = ift.Field.from_global_data(s_space, n)\n",
"curv = Curvature(R=R, N=N, Sh=Sh)\n",
......
......@@ -33,7 +33,7 @@ if __name__ == '__main__':
# Two-dimensional regular grid with inhomogeneous exposure
position_space = ift.RGSpace([512, 512])
# # Sphere with with uniform exposure
# Sphere with uniform exposure
# position_space = ift.HPSpace(128)
# exposure = ift.Field.full(position_space, 1.)
......@@ -41,38 +41,30 @@ if __name__ == '__main__':
harmonic_space = position_space.get_default_codomain()
HT = ift.HarmonicTransformOperator(harmonic_space, position_space)
domain = ift.MultiDomain.make({'xi': harmonic_space})
position = ift.from_random('normal', domain)
position = ift.from_random('normal', harmonic_space)
# Define power spectrum and amplitudes
def sqrtpspec(k):
return 1. / (20. + k**2)
p_space = ift.PowerSpace(harmonic_space)
pd = ift.PowerDistributor(harmonic_space, p_space)
a = ift.PS_field(p_space, sqrtpspec)
A = pd(a)
A = ift.create_power_operator(harmonic_space, sqrtpspec)
# Set up a sky model
xi = ift.Variable(position)['xi']
logsky_h = xi * A
logsky = HT(logsky_h)
sky = ift.PointwisePositiveTanh(logsky)
sky = ift.positive_tanh(HT(A))
GR = ift.GeometryRemover(position_space)
# Set up instrumental response
R = GR
# Generate mock data
d_space = R.target[0]
p = R(sky)
mock_position = ift.from_random('normal', p.position.domain)
pp = p.at(mock_position).value
data = np.random.binomial(1, pp.to_global_data().astype(np.float64))
data = ift.Field.from_global_data(d_space, data)
mock_position = ift.from_random('normal', harmonic_space)
tmp = p(mock_position).to_global_data().astype(np.float64)
data = np.random.binomial(1, tmp)
data = ift.Field.from_global_data(R.target, data)
# Compute likelihood and Hamiltonian
position = ift.from_random('normal', p.position.domain)
position = ift.from_random('normal', harmonic_space)
likelihood = ift.BernoulliEnergy(p, data)
ic_cg = ift.GradientNormController(iteration_limit=50)
ic_newton = ift.GradientNormController(name='Newton', iteration_limit=30,
......@@ -82,14 +74,14 @@ if __name__ == '__main__':
# Minimize the Hamiltonian
H = ift.Hamiltonian(likelihood, ic_sampling)
H = H.make_invertible(ic_cg)
# minimizer = ift.SteepestDescent(ic_newton)
H = ift.EnergyAdapter(position, H, ic_cg)
# minimizer = ift.L_BFGS(ic_newton)
H, convergence = minimizer(H)
reconstruction = sky.at(H.position).value
reconstruction = sky(H.position)
ift.plot(reconstruction, title='reconstruction')
ift.plot(GR.adjoint_times(data), title='data')
ift.plot(sky.at(mock_position).value, title='truth')
ift.plot(sky(mock_position), title='truth')
ift.plot_finish(nx=3, xsize=16, ysize=5, title="results",
name="bernoulli.png")
......@@ -78,7 +78,7 @@ if __name__ == '__main__':
GR = ift.GeometryRemover(position_space)
mask = ift.Field.from_global_data(position_space, mask)
Mask = ift.DiagonalOperator(mask)
R = GR * Mask * HT
R = GR(Mask(HT))
data_space = GR.target
......@@ -93,7 +93,7 @@ if __name__ == '__main__':
# Build propagator D and information source j
j = R.adjoint_times(N.inverse_times(data))
D_inv = R.adjoint * N.inverse * R + S.inverse
D_inv = R.adjoint(N.inverse(R)) + S.inverse
# Make it invertible
IC = ift.GradientNormController(iteration_limit=500, tol_abs_gradnorm=1e-3)
D = ift.InversionEnabler(D_inv, IC, approximation=S.inverse).inverse
......@@ -107,13 +107,14 @@ if __name__ == '__main__':
ift.plot([HT(MOCK_SIGNAL), GR.adjoint(data), HT(m)],
label=['Mock signal', 'Data', 'Reconstruction'],
alpha=[1, .3, 1])
ift.plot(mask_to_nan(mask, HT(m-MOCK_SIGNAL)))
ift.plot(mask_to_nan(mask, HT(m-MOCK_SIGNAL)), title='Residuals')
ift.plot_finish(nx=2, ny=1, xsize=10, ysize=4,
title="getting_started_1")
else:
ift.plot(HT(MOCK_SIGNAL), title='Mock Signal')
ift.plot(mask_to_nan(mask, (GR*Mask).adjoint(data)), title='Data')
ift.plot(mask_to_nan(mask, (GR(Mask)).adjoint(data)),
title='Data')
ift.plot(HT(m), title='Reconstruction')
ift.plot(mask_to_nan(mask, HT(m-MOCK_SIGNAL)))
ift.plot(mask_to_nan(mask, HT(m-MOCK_SIGNAL)), title='Residuals')
ift.plot_finish(nx=2, ny=2, xsize=10, ysize=10,
title="getting_started_1")
......@@ -43,13 +43,13 @@ if __name__ == '__main__':
#
# # One dimensional regular grid with uniform exposure
# position_space = ift.RGSpace(1024)
# exposure = np.ones(position_space.shape)
# exposure = ift.Field.full(position_space, 1.)
# Two-dimensional regular grid with inhomogeneous exposure
position_space = ift.RGSpace([512, 512])
exposure = get_2D_exposure()
# # Sphere with with uniform exposure
# Sphere with uniform exposure
# position_space = ift.HPSpace(128)
# exposure = ift.Field.full(position_space, 1.)
......@@ -57,7 +57,7 @@ if __name__ == '__main__':
harmonic_space = position_space.get_default_codomain()
HT = ift.HarmonicTransformOperator(harmonic_space, position_space)
domain = ift.MultiDomain.make({'xi': harmonic_space})
domain = ift.DomainTuple.make(harmonic_space)
position = ift.from_random('normal', domain)
# Define power spectrum and amplitudes
......@@ -70,21 +70,18 @@ if __name__ == '__main__':
A = pd(a)
# Set up a sky model
xi = ift.Variable(position)['xi']
logsky_h = xi * A
logsky = HT(logsky_h)
sky = ift.PointwiseExponential(logsky)
sky = ift.exp(HT(ift.makeOp(A)))
M = ift.DiagonalOperator(exposure)
GR = ift.GeometryRemover(position_space)
# Set up instrumental response
R = GR * M
R = GR(M)
# Generate mock data
d_space = R.target[0]
lamb = R(sky)
mock_position = ift.from_random('normal', lamb.position.domain)
data = lamb.at(mock_position).value
mock_position = ift.from_random('normal', domain)
data = lamb(mock_position)
data = np.random.poisson(data.to_global_data().astype(np.float64))
data = ift.Field.from_global_data(d_space, data)
......@@ -97,11 +94,14 @@ if __name__ == '__main__':
# Minimize the Hamiltonian
H = ift.Hamiltonian(likelihood)
H = H.make_invertible(ic_cg)
H = ift.EnergyAdapter(position, H, ic_cg)
H, convergence = minimizer(H)
# Plot results
result_sky = sky.at(H.position).value
ift.plot(result_sky, title='Reconstruction')
signal = sky(mock_position)
reconst = sky(H.position)
ift.plot(signal, title='Signal')
ift.plot(GR.adjoint(data), title='Data')
ift.plot(reconst, title='Reconstruction')
ift.plot(reconst - signal, title='Residuals')
ift.plot_finish(name='getting_started_2.png', xsize=16, ysize=16)
......@@ -32,26 +32,25 @@ if __name__ == '__main__':
position_space = ift.RGSpace([128, 128])
# Setting up an amplitude model
A, amplitude_internals = ift.make_amplitude_model(
position_space, 16, 1, 10, -4., 1, 0., 1.)
A = ift.AmplitudeModel(position_space, 16, 1, 10, -4., 1, 0., 1.)
dummy = ift.from_random('normal', A.domain)
# Building the model for a correlated signal
harmonic_space = position_space.get_default_codomain()
ht = ift.HarmonicTransformOperator(harmonic_space, position_space)
power_space = A.value.domain[0]
power_space = A.target[0]
power_distributor = ift.PowerDistributor(harmonic_space, power_space)
position = ift.MultiField.from_dict(
{'xi': ift.Field.from_random('normal', harmonic_space)})
dummy = ift.Field.from_random('normal', harmonic_space)
domain = ift.MultiDomain.union(
(A.domain, ift.MultiDomain.make({'xi': harmonic_space})))
xi = ift.Variable(position)['xi']
Amp = power_distributor(A)
correlated_field_h = Amp * xi
correlated_field = ht(correlated_field_h)
correlated_field = ht(
power_distributor(A)*ift.FieldAdapter(domain, "xi"))
# alternatively to the block above one can do:
# correlated_field,_ = ift.make_correlated_field(position_space, A)
# correlated_field = ift.CorrelatedField(position_space, A)
# apply some nonlinearity
signal = ift.PointwisePositiveTanh(correlated_field)
signal = ift.positive_tanh(correlated_field)
# Building the Line of Sight response
LOS_starts, LOS_ends = get_random_LOS(100)
......@@ -65,55 +64,57 @@ if __name__ == '__main__':
N = ift.ScalingOperator(noise, data_space)
# generate mock data
MOCK_POSITION = ift.from_random('normal', signal.position.domain)
data = signal_response.at(MOCK_POSITION).value + N.draw_sample()
MOCK_POSITION = ift.from_random('normal', domain)
data = signal_response(MOCK_POSITION) + N.draw_sample()
# set up model likelihood
likelihood = ift.GaussianEnergy(signal_response, mean=data, covariance=N)
likelihood = ift.GaussianEnergy(
mean=data, covariance=N)(signal_response)
# set up minimization and inversion schemes
ic_cg = ift.GradientNormController(iteration_limit=10)
ic_sampling = ift.GradientNormController(iteration_limit=100)
ic_newton = ift.GradientNormController(name='Newton', iteration_limit=100)
ic_newton = ift.DeltaEnergyController(
name='Newton', tol_rel_deltaE=1e-8, iteration_limit=100)
minimizer = ift.RelaxedNewton(ic_newton)
# minimizer = ift.VL_BFGS(ic_newton)
# minimizer = ift.NewtonCG(xtol=1e-10, maxiter=100, disp=True)
# minimizer = ift.L_BFGS_B(ftol=1e-10, gtol=1e-5, maxiter=100, maxcor=20, disp=True)
# build model Hamiltonian
H = ift.Hamiltonian(likelihood, ic_sampling)
INITIAL_POSITION = ift.from_random('normal', H.position.domain)
INITIAL_POSITION = ift.from_random('normal', domain)
position = INITIAL_POSITION
ift.plot(signal.at(MOCK_POSITION).value, title='ground truth')
ift.plot(signal(MOCK_POSITION), title='ground truth')
ift.plot(R.adjoint_times(data), title='data')
ift.plot([A.at(MOCK_POSITION).value], title='power')
ift.plot([A(MOCK_POSITION)], title='power')
ift.plot_finish(nx=3, xsize=16, ysize=5, title="setup", name="setup.png")
# number of samples used to estimate the KL
N_samples = 20
for i in range(2):
H = H.at(position)
samples = [H.metric.draw_sample(from_inverse=True)
metric = H(ift.Linearization.make_var(position)).metric
samples = [metric.draw_sample(from_inverse=True)
for _ in range(N_samples)]
KL = ift.SampledKullbachLeiblerDivergence(H, samples)
KL = KL.make_invertible(ic_cg)
KL = ift.EnergyAdapter(position, KL, ic_cg)
KL, convergence = minimizer(KL)
position = KL.position
ift.plot(signal.at(position).value, title="reconstruction")
ift.plot([A.at(position).value, A.at(MOCK_POSITION).value],
title="power")
ift.plot(signal(position), title="reconstruction")
ift.plot([A(position), A(MOCK_POSITION)], title="power")
ift.plot_finish(nx=2, xsize=12, ysize=6, title="loop", name="loop.png")
sc = ift.StatCalculator()
for sample in samples:
sc.add(signal.at(sample+position).value)
sc.add(signal(sample+position))
ift.plot(sc.mean, title="mean")
ift.plot(ift.sqrt(sc.var), title="std deviation")
powers = [A.at(s+position).value for s in samples]
ift.plot([A.at(position).value, A.at(MOCK_POSITION).value]+powers,
title="power")
powers = [A(s+position) for s in samples]
ift.plot([A(position), A(MOCK_POSITION)]+powers, title="power")
ift.plot_finish(nx=3, xsize=16, ysize=5, title="results",
name="results.png")
......@@ -4,7 +4,7 @@ import numpy as np
def plot_test():
rg_space1 = ift.makeDomain(ift.RGSpace((100,)))
rg_space2 = ift.makeDomain(ift.RGSpace((80, 80)))
rg_space2 = ift.makeDomain(ift.RGSpace((80, 60), distances=1))
hp_space = ift.makeDomain(ift.HPSpace(64))
gl_space = ift.makeDomain(ift.GLSpace(128))
......@@ -13,7 +13,7 @@ def plot_test():
field_rg1_1 = ift.Field.from_global_data(rg_space1, np.random.randn(100))
field_rg1_2 = ift.Field.from_global_data(rg_space1, np.random.randn(100))
field_rg2 = ift.Field.from_global_data(
rg_space2, np.random.randn(80 ** 2).reshape((80, 80)))
rg_space2, np.random.randn(80*60).reshape((80, 60)))
field_hp = ift.Field.from_global_data(hp_space, np.random.randn(12*64**2))
field_gl = ift.Field.from_global_data(gl_space, np.random.randn(32640))
field_ps = ift.power_analyze(fft.times(field_rg2))
......
......@@ -12,14 +12,14 @@ def polynomial(coefficients, sampling_points):
Parameters
----------
coefficients: Model
coefficients: Field
sampling_points: Numpy array
"""
if not (isinstance(coefficients, ift.Model)
if not (isinstance(coefficients, ift.Field)
and isinstance(sampling_points, np.ndarray)):
raise TypeError
params = coefficients.value.to_global_data()
params = coefficients.to_global_data()
out = np.zeros_like(sampling_points)
for ii in range(len(params)):
out += params[ii] * sampling_points**ii
......@@ -38,13 +38,13 @@ class PolynomialResponse(ift.LinearOperator):
"""
def __init__(self, domain, sampling_points):
super(PolynomialResponse, self).__init__()
if not (isinstance(domain, ift.UnstructuredDomain)
and isinstance(x, np.ndarray)):
raise TypeError
self._domain = ift.DomainTuple.make(domain)
tgt = ift.UnstructuredDomain(sampling_points.shape)
self._target = ift.DomainTuple.make(tgt)
self._capability = self.TIMES | self.ADJOINT_TIMES
sh = (self.target.size, domain.size)
self._mat = np.empty(sh)
......@@ -53,7 +53,7 @@ class PolynomialResponse(ift.LinearOperator):
def apply(self, x, mode):
self._check_input(x, mode)
val = x.to_global_data()
val = x.to_global_data_rw()
if mode == self.TIMES:
# FIXME Use polynomial() here
out = self._mat.dot(val)
......@@ -62,18 +62,6 @@ class PolynomialResponse(ift.LinearOperator):
out = self._mat.conj().T.dot(val)
return ift.from_global_data(self._tgt(mode), out)
@property
def domain(self):
return self._domain
@property
def target(self):
return self._target
@property
def capability(self):
return self.TIMES | self.ADJOINT_TIMES
# Generate some mock data
N_params = 10
......@@ -88,8 +76,7 @@ y[5] -= 0
# Set up minimization problem
p_space = ift.UnstructuredDomain(N_params)
params = ift.Variable(ift.MultiField.from_dict(
{'params': ift.full(p_space, 0.)}))['params']
params = ift.full(p_space, 0.)
R = PolynomialResponse(p_space, x)
ift.extra.consistency_check(R)
......@@ -98,8 +85,9 @@ d = ift.from_global_data(d_space, y)
N = ift.DiagonalOperator(ift.from_global_data(d_space, var))
IC = ift.GradientNormController(tol_abs_gradnorm=1e-8)
H = ift.Hamiltonian(ift.GaussianEnergy(R(params), d, N), IC)
H = H.make_invertible(IC)
likelihood = ift.GaussianEnergy(d, N)(R)
H = ift.Hamiltonian(likelihood, IC)
H = ift.EnergyAdapter(params, H, IC)
# Minimize
minimizer = ift.RelaxedNewton(IC)
......@@ -116,13 +104,13 @@ xs = np.linspace(xmin, xmax, 100)
sc = ift.StatCalculator()
for ii in range(len(samples)):
sc.add(params.at(samples[ii]).value)
ys = polynomial(params.at(samples[ii]), xs)
sc.add(samples[ii])
ys = polynomial(samples[ii], xs)
if ii == 0:
plt.plot(xs, ys, 'k', alpha=.05, label='Posterior samples')
continue
plt.plot(xs, ys, 'k', alpha=.05)
ys = polynomial(params.at(H.position), xs)
ys = polynomial(H.position, xs)
plt.plot(xs, ys, 'r', linewidth=2., label='Interpolation')
plt.legend()
plt.savefig('fit.png')
......@@ -131,6 +119,7 @@ plt.close()
# Print parameters
mean = sc.mean.to_global_data()
sigma = np.sqrt(sc.var.to_global_data())
for ii in range(len(mean)):
print('Coefficient x**{}: {:.2E} +/- {:.2E}'.format(ii, mean[ii],
sigma[ii]))
if ift.dobj.master:
for ii in range(len(mean)):
print('Coefficient x**{}: {:.2E} +/- {:.2E}'.format(ii, mean[ii],
sigma[ii]))
......@@ -99,6 +99,7 @@ Combinations of domains
The fundamental classes described above are often sufficient to specify the
domain of a field. In some cases, however, it will be necessary to have the
field live on a product of elementary domains instead of a single one.
More sophisticated models also require a set of several such fields.
Some examples are:
- sky emission depending on location and energy. This could be represented by
......@@ -107,6 +108,9 @@ Some examples are:
- a polarised field, which could be modeled as a product of any structured
domain (representing location) with a four-element
:class:`UnstructuredDomain` holding Stokes I, Q, U and V components.
- a model for the sky emission, which holds both the current realization
(on a harmonic domain) and a few inferred model parameters (e.g. on an
unstructured grid).
Consequently, NIFTy defines a class called :class:`DomainTuple` holding
a sequence of :class:`Domain` objects, which is used to specify full field
......@@ -117,10 +121,15 @@ A :class:`DomainTuple` supports iteration and indexing, and also provides the
properties :attr:`~DomainTuple.shape`, :attr:`~DomainTuple.size` in analogy to
the elementary :class:`Domain`.
An aggregation of several :class:`DomainTuple`s, each member identified by a
name, is described by the :class:`MultiDomain` class.
Fields
======
Fields on a single DomainTuple
------------------------------
A :class:`Field` object consists of the following components:
- a domain in form of a :class:`DomainTuple` object
......@@ -148,14 +157,44 @@ that are not covered by the provided standard operations, its data content must
be extracted first, then changed, and a new field has to be created from the
result.
Fields living on a MultiDomain
------------------------------
The :class:`MultiField` class can be seen as a dictionary of individual
:class:`Field`s, each identified by a name, which lives on an associated
:class:`MultiDomain`.
Operators
=========
All transformations between different NIFTy fields are expressed (explicitly
or implicitly) in the form of :class:`Operator` objects. The interface of this
class is very minimalistic: it has a property called `domain` which returns
a `Domaintuple` or `MultiDomain` object specifying the structure of the
`Field`s or `MultiField`s it expects as input, another property `target`
describing its output, and finally an overloaded `apply` method, which can
take
- a `Field`/`MultiField`object, in which case it returns the transformed
`Field`/`MultiField`
- a `Linearization` object, in which case it returns the transformed
`Linearization`
This is the interface that all objects derived from `Operator` must implement.
In addition, `Operator` objects can be added/subtracted, multiplied, chained
(via the `__call__` method) and support pointwise application of functions like
`exp()`, `log()`, `sqrt()`, `conjugate()` etc.
Linear Operators
================
A linear operator (represented by NIFTy5's abstract :class:`LinearOperator`
class) can be interpreted as an (implicitly defined) matrix.
It can be applied to :class:`Field` instances, resulting in other :class:`Field`
instances that potentially live on other domains.
class) is derived from `Operator` and can be interpreted as an
(implicitly defined) matrix. Since its operation is linear, it can provide some
additional functionality which is not available for the more generic `Operator`
class.
Operator basics
......@@ -163,13 +202,13 @@ Operator basics
There are four basic ways of applying an operator :math:`A` to a field :math:`f`:
- direct multiplication: :math:`A\cdot f`
- adjoint multiplication: :math:`A^\dagger \cdot f`
- inverse multiplication: :math:`A^{-1}\cdot f`
- adjoint inverse multiplication: :math:`(A^\dagger)^{-1}\cdot f`
- direct application: :math:`A\cdot f`
- adjoint application: :math:`A^\dagger \cdot f`
- inverse application: :math:`A^{-1}\cdot f`
- adjoint inverse application: :math:`(A^\dagger)^{-1}\cdot f`
(For linear operators, inverse adjoint multiplication and adjoint inverse
multiplication are equivalent.)
(Because of the linearity, inverse adjoint and adjoint inverse application
are equivalent.)
These different actions of an operator ``Op`` on a field ``f`` can be invoked
in various ways:
......@@ -190,9 +229,6 @@ enhanced by this approach to support the complete set. This functionality is
provided by NIFTy's :class:`InversionEnabler` class, which is itself a linear
operator.
There are two :class:`DomainTuple` objects associated with a
:class:`LinearOperator`: a :attr:`~LinearOperator.domain` and a
:attr:`~LinearOperator.target`.
Direct multiplication and adjoint inverse multiplication transform a field
living on the operator's :attr:`~LinearOperator.domain` to one living on the operator's :attr:`~LinearOperator.target`, whereas adjoint multiplication
and inverse multiplication transform from :attr:`~LinearOperator.target` to :attr:`~LinearOperator.domain`.
......@@ -221,7 +257,7 @@ operators.
As an example, if ``A``, ``B`` and ``C`` are of type :class:`LinearOperator`
and ``f1`` and ``f2`` are of type :class:`Field`, writing::