Commit 8c50ce87 authored by Martin Reinecke's avatar Martin Reinecke
Browse files

Merge branch 'NIFTy_5' into model_documentation

parents 0dccab5d 8e516415
...@@ -98,11 +98,11 @@ ...@@ -98,11 +98,11 @@
def Curvature(R, N, Sh): def Curvature(R, N, Sh):
IC = ift.GradientNormController(iteration_limit=50000, IC = ift.GradientNormController(iteration_limit=50000,
tol_abs_gradnorm=0.1) tol_abs_gradnorm=0.1)
# WienerFilterCurvature is (R.adjoint*N.inverse*R + Sh.inverse) plus some handy # WienerFilterCurvature is (R.adjoint*N.inverse*R + Sh.inverse) plus some handy
# helper methods. # helper methods.
return ift.library.WienerFilterCurvature(R,N,Sh,iteration_controller=IC,iteration_controller_sampling=IC) return ift.WienerFilterCurvature(R,N,Sh,iteration_controller=IC,iteration_controller_sampling=IC)
``` ```
%% Cell type:markdown id: tags: %% Cell type:markdown id: tags:
### Conjugate Gradient Preconditioning ### Conjugate Gradient Preconditioning
......
...@@ -2,7 +2,9 @@ import nifty5 as ift ...@@ -2,7 +2,9 @@ import nifty5 as ift
import numpy as np import numpy as np
from global_newton.models_other.apply_data import ApplyData from global_newton.models_other.apply_data import ApplyData
from global_newton.models_energy.hamiltonian import Hamiltonian from global_newton.models_energy.hamiltonian import Hamiltonian
from nifty5.library.unit_log_gauss import UnitLogGauss from nifty5 import GaussianEnergy
if __name__ == '__main__': if __name__ == '__main__':
# s_space = ift.RGSpace([1024]) # s_space = ift.RGSpace([1024])
s_space = ift.RGSpace([128,128]) s_space = ift.RGSpace([128,128])
...@@ -45,7 +47,7 @@ if __name__ == '__main__': ...@@ -45,7 +47,7 @@ if __name__ == '__main__':
NWR = ApplyData(data, ift.Field(d_space,val=noise), Rs) NWR = ApplyData(data, ift.Field(d_space,val=noise), Rs)
INITIAL_POSITION = ift.from_random('normal',total_domain) INITIAL_POSITION = ift.from_random('normal',total_domain)
likelihood = UnitLogGauss(INITIAL_POSITION, NWR) likelihood = GaussianEnergy(INITIAL_POSITION, NWR)
IC = ift.GradientNormController(iteration_limit=500, tol_abs_gradnorm=1e-3) IC = ift.GradientNormController(iteration_limit=500, tol_abs_gradnorm=1e-3)
inverter = ift.ConjugateGradient(controller=IC) inverter = ift.ConjugateGradient(controller=IC)
......
...@@ -13,8 +13,10 @@ recognized from a large distance, ignoring all technical details. ...@@ -13,8 +13,10 @@ recognized from a large distance, ignoring all technical details.
From such a perspective, From such a perspective,
- IFT problems largely consist of *minimization* problems involving a large - IFT problems largely consist of the combination of several high dimensional
number of equations. *minimization* problems.
- Within NIFTy, *models* are used to define the characteristic equations and
properties of the problems.
- The equations are built mostly from the application of *linear operators*, - The equations are built mostly from the application of *linear operators*,
but there may also be nonlinear functions involved. but there may also be nonlinear functions involved.
- The unknowns in the equations represent either continuous physical *fields*, - The unknowns in the equations represent either continuous physical *fields*,
...@@ -232,6 +234,62 @@ The properties :attr:`~LinearOperator.adjoint` and ...@@ -232,6 +234,62 @@ The properties :attr:`~LinearOperator.adjoint` and
were the original operator's adjoint or inverse, respectively. were the original operator's adjoint or inverse, respectively.
Models
======
Model classes (represented by NIFTy5's abstract :class:`Model` class) are used to construct
the equations of a specific inference problem.
Most models are defined via a position, which is a :class:`MultiField` object,
their value at this position, which is again a :class:`MultiField` object and a Jacobian derivative,
which is a :class:`LinearOperator` and is needed for the minimization procedure.
Using the existing basic model classes one can construct more complicated models, as
NIFTy allows for easy and self-consinstent combination via point-wise multiplication,
addition and subtraction. The model resulting from these operations then automatically
contains the correct Jacobians, positions and values.
Notably, :class:`Constant` and :class:`Variable` allow for an easy way to turn
inference of specific quantities on and off.
The basic model classes also allow for more complex operations on models such as
the application of :class:`LinearOperators` or local non-linearities.
As an example one may consider the following combination of ``x``, which is a model of type
:class:`Variable` and ``y``, which is a model of type :class:`Constant`::
z = x*x + y
``z`` will then be a model with the following properties::
z.value = x.value*x.value + y.value
z.position = Union(x.position, y.position)
z.jacobian = 2*makeOp(x.value)
Basic models
------------
Basic model classes provided by NIFTy are
- :class:`Constant` contains a constant value and has a zero valued Jacobian.
Like other models, it has a position, but its value does not depend on it.
- :class:`Variable` returns the position as its value, its derivative is one.
- :class:`LinearModel` applies a :class:`LinearOperator` on the model.
- :class:`LocalModel` applies a non-linearity locally on the model.
- :class:`MultiModel` combines various models into one. In this case the position,
value and Jacobian are combined into corresponding :class:`MultiFields` and operators.
Advanced models
---------------
NIFTy also provides a library of more sophisticated models which are used for more
specific inference problems. Currently these are:
- :class:`AmplitudeModel`, which returns a smooth power spectrum.
- :class:`PointModel`, which models point sources which follow a inverse gamma distribution.
- :class:`SmoothSkyModel`, which models a diffuse lognormal field. It takes an amplitude model
to specify the correlation structure of the field.
.. _minimization: .. _minimization:
......
...@@ -16,7 +16,7 @@ from .minimization import * ...@@ -16,7 +16,7 @@ from .minimization import *
from .sugar import * from .sugar import *
from .plotting.plot import plot from .plotting.plot import plot
from . import library from .library import *
from . import extra from . import extra
from .utilities import memo from .utilities import memo
......
...@@ -26,7 +26,7 @@ except ImportError: ...@@ -26,7 +26,7 @@ except ImportError:
from .data_objects.numpy_do import * from .data_objects.numpy_do import *
__all__ = ["ntask", "rank", "master", "local_shape", "data_object", "full", __all__ = ["ntask", "rank", "master", "local_shape", "data_object", "full",
"empty", "zeros", "ones", "empty_like", "vdot", "abs", "exp", "empty", "zeros", "ones", "empty_like", "vdot", "exp",
"log", "tanh", "sqrt", "from_object", "from_random", "log", "tanh", "sqrt", "from_object", "from_random",
"local_data", "ibegin", "ibegin_from_shape", "np_allreduce_sum", "local_data", "ibegin", "ibegin_from_shape", "np_allreduce_sum",
"distaxis", "from_local_data", "from_global_data", "to_global_data", "distaxis", "from_local_data", "from_global_data", "to_global_data",
......
...@@ -34,7 +34,7 @@ class GLSpace(StructuredDomain): ...@@ -34,7 +34,7 @@ class GLSpace(StructuredDomain):
pixelization. pixelization.
nlon : int, optional nlon : int, optional
Number of longitudinal bins that are used for this pixelization. Number of longitudinal bins that are used for this pixelization.
Default value is 2*nlat + 1. Default value is 2*nlat - 1.
""" """
_needed_for_hash = ["_nlat", "_nlon"] _needed_for_hash = ["_nlat", "_nlon"]
......
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright(C) 2013-2018 Max-Planck-Society
#
# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik
# and financially supported by the Studienstiftung des deutschen Volkes.
from ..library.gaussian_energy import GaussianEnergy
from ..minimization.energy import Energy from ..minimization.energy import Energy
from ..operators import InversionEnabler, SamplingEnabler
from ..models.variable import Variable from ..models.variable import Variable
from ..operators import InversionEnabler, SamplingEnabler
from ..utilities import memo from ..utilities import memo
from ..library.unit_log_gauss import UnitLogGauss
class Hamiltonian(Energy): class Hamiltonian(Energy):
...@@ -15,11 +33,8 @@ class Hamiltonian(Energy): ...@@ -15,11 +33,8 @@ class Hamiltonian(Energy):
super(Hamiltonian, self).__init__(lh.position) super(Hamiltonian, self).__init__(lh.position)
self._lh = lh self._lh = lh
self._ic = iteration_controller self._ic = iteration_controller
if iteration_controller_sampling is None: self._ic_samp = iteration_controller_sampling
self._ic_samp = iteration_controller self._prior = GaussianEnergy(Variable(self.position))
else:
self._ic_samp = iteration_controller_sampling
self._prior = UnitLogGauss(Variable(self.position))
self._precond = self._prior.curvature self._precond = self._prior.curvature
def at(self, position): def at(self, position):
...@@ -39,8 +54,11 @@ class Hamiltonian(Energy): ...@@ -39,8 +54,11 @@ class Hamiltonian(Energy):
@memo @memo
def curvature(self): def curvature(self):
prior_curv = self._prior.curvature prior_curv = self._prior.curvature
c = SamplingEnabler(self._lh.curvature, prior_curv.inverse, if self._ic_samp is None:
self._ic_samp, prior_curv.inverse) c = self._lh.curvature + prior_curv
else:
c = SamplingEnabler(self._lh.curvature, prior_curv.inverse,
self._ic_samp, prior_curv.inverse)
return InversionEnabler(c, self._ic, self._precond) return InversionEnabler(c, self._ic, self._precond)
def __str__(self): def __str__(self):
......
from .operator_tests import consistency_check from .operator_tests import consistency_check
from .energy_tests import * from .energy_and_model_tests import *
...@@ -18,19 +18,42 @@ ...@@ -18,19 +18,42 @@
import numpy as np import numpy as np
from ..sugar import from_random from ..sugar import from_random
from ..minimization.energy import Energy
from ..models.model import Model
__all__ = ["check_value_gradient_consistency", __all__ = ["check_value_gradient_consistency",
"check_value_gradient_curvature_consistency"] "check_value_gradient_curvature_consistency"]
def _get_acceptable_model(M):
val = M.value
if not np.isfinite(val.sum()):
raise ValueError('Initial Model value must be finite')
dir = from_random("normal", M.position.domain)
dirder = M.gradient(dir)
dir *= val/(dirder).norm()*1e-5
# Find a step length that leads to a "reasonable" Model
for i in range(50):
try:
M2 = M.at(M.position+dir)
if np.isfinite(M2.value.sum()) and abs(M2.value.sum()) < 1e20:
break
except FloatingPointError:
pass
dir *= 0.5
else:
raise ValueError("could not find a reasonable initial step")
return M2
def _get_acceptable_energy(E): def _get_acceptable_energy(E):
val = E.value val = E.value
if not np.isfinite(val): if not np.isfinite(val):
raise ValueError raise ValueError('Initial Energy must be finite')
dir = from_random("normal", E.position.domain) dir = from_random("normal", E.position.domain)
dirder = E.gradient.vdot(dir) dirder = E.gradient.vdot(dir)
dir *= np.abs(val)/np.abs(dirder)*1e-5 dir *= np.abs(val)/np.abs(dirder)*1e-5
# find a step length that leads to a "reasonable" energy # Find a step length that leads to a "reasonable" energy
for i in range(50): for i in range(50):
try: try:
E2 = E.at(E.position+dir) E2 = E.at(E.position+dir)
...@@ -46,31 +69,45 @@ def _get_acceptable_energy(E): ...@@ -46,31 +69,45 @@ def _get_acceptable_energy(E):
def check_value_gradient_consistency(E, tol=1e-8, ntries=100): def check_value_gradient_consistency(E, tol=1e-8, ntries=100):
for _ in range(ntries): for _ in range(ntries):
E2 = _get_acceptable_energy(E) if isinstance(E, Energy):
E2 = _get_acceptable_energy(E)
else:
E2 = _get_acceptable_model(E)
val = E.value val = E.value
dir = E2.position - E.position dir = E2.position - E.position
# Enext = E2 Enext = E2
dirnorm = dir.norm() dirnorm = dir.norm()
for i in range(50): for i in range(50):
Emid = E.at(E.position + 0.5*dir) Emid = E.at(E.position + 0.5*dir)
dirder = Emid.gradient.vdot(dir)/dirnorm if isinstance(E, Energy):
xtol = tol*Emid.gradient_norm dirder = Emid.gradient.vdot(dir)/dirnorm
if abs((E2.value-val)/dirnorm - dirder) < xtol: else:
break dirder = Emid.gradient(dir)/dirnorm
numgrad = (E2.value-val)/dirnorm
if isinstance(E, Model):
xtol = tol * dirder.norm() / np.sqrt(dirder.size)
if (abs(numgrad-dirder) < xtol).all():
break
else:
xtol = tol*Emid.gradient_norm
if abs(numgrad-dirder) < xtol:
break
dir *= 0.5 dir *= 0.5
dirnorm *= 0.5 dirnorm *= 0.5
E2 = Emid E2 = Emid
else: else:
raise ValueError("gradient and value seem inconsistent") raise ValueError("gradient and value seem inconsistent")
# E = Enext E = Enext
def check_value_gradient_curvature_consistency(E, tol=1e-8, ntries=100): def check_value_gradient_curvature_consistency(E, tol=1e-8, ntries=100):
if isinstance(E, Model):
raise ValueError('Models have no curvature, thus it cannot be tested.')
for _ in range(ntries): for _ in range(ntries):
E2 = _get_acceptable_energy(E) E2 = _get_acceptable_energy(E)
val = E.value val = E.value
dir = E2.position - E.position dir = E2.position - E.position
# Enext = E2 Enext = E2
dirnorm = dir.norm() dirnorm = dir.norm()
for i in range(50): for i in range(50):
Emid = E.at(E.position + 0.5*dir) Emid = E.at(E.position + 0.5*dir)
...@@ -85,4 +122,4 @@ def check_value_gradient_curvature_consistency(E, tol=1e-8, ntries=100): ...@@ -85,4 +122,4 @@ def check_value_gradient_curvature_consistency(E, tol=1e-8, ntries=100):
E2 = Emid E2 = Emid
else: else:
raise ValueError("gradient, value and curvature seem inconsistent") raise ValueError("gradient, value and curvature seem inconsistent")
# E = Enext E = Enext
...@@ -24,8 +24,6 @@ from .domain_tuple import DomainTuple ...@@ -24,8 +24,6 @@ from .domain_tuple import DomainTuple
from functools import reduce from functools import reduce
from . import dobj from . import dobj
__all__ = ["Field", "sqrt", "exp", "log", "conjugate"]
class Field(object): class Field(object):
""" The discrete representation of a continuous field over multiple spaces. """ The discrete representation of a continuous field over multiple spaces.
......
from .amplitude_model import make_amplitude_model from .amplitude_model import make_amplitude_model
from .apply_data import ApplyData from .apply_data import ApplyData
from .gaussian_energy import GaussianEnergy
from .los_response import LOSResponse from .los_response import LOSResponse
from .nonlinear_wiener_filter_energy import NonlinearWienerFilterEnergy
from .unit_log_gauss import UnitLogGauss
from .point_sources import PointSources from .point_sources import PointSources
from .poisson_log_likelihood import PoissonLogLikelihood from .poissonian_energy import PoissonianEnergy
from .smooth_sky import make_smooth_mf_sky_model, make_smooth_sky_model from .smooth_sky import make_smooth_mf_sky_model, make_smooth_sky_model
from .wiener_filter_curvature import WienerFilterCurvature from .wiener_filter_curvature import WienerFilterCurvature
from .wiener_filter_energy import WienerFilterEnergy from .wiener_filter_energy import WienerFilterEnergy
def ApplyData(data, var, model_data): def ApplyData(data, var, model_data):
from .. import DiagonalOperator, Constant, sqrt # TODO This is rather confusing. Delete that eventually.
from ..operators.diagonal_operator import DiagonalOperator
from ..models.constant import Constant
from ..sugar import sqrt
sqrt_n = DiagonalOperator(sqrt(var)) sqrt_n = DiagonalOperator(sqrt(var))
data = Constant(model_data.position, data) data = Constant(model_data.position, data)
return sqrt_n.inverse(model_data - data) return sqrt_n.inverse(model_data - data)
...@@ -17,45 +17,50 @@ ...@@ -17,45 +17,50 @@
# and financially supported by the Studienstiftung des deutschen Volkes. # and financially supported by the Studienstiftung des deutschen Volkes.
from ..minimization.energy import Energy from ..minimization.energy import Energy
from ..operators.inversion_enabler import InversionEnabler
from ..operators.sandwich_operator import SandwichOperator from ..operators.sandwich_operator import SandwichOperator
from ..utilities import memo from ..utilities import memo
class UnitLogGauss(Energy): class GaussianEnergy(Energy):
def __init__(self, s, inverter=None): def __init__(self, inp, mean=None, covariance=None):
""" """
s: Sky model object inp: Model object
value = 0.5 * s.vdot(s), i.e. a log-Gauss distribution with unit value = 0.5 * s.vdot(s), i.e. a log-Gauss distribution with unit
covariance covariance
""" """
super(UnitLogGauss, self).__init__(s.position) super(GaussianEnergy, self).__init__(inp.position)
self._s = s self._inp = inp
self._inverter = inverter self._mean = mean
self._cov = covariance
def at(self, position): def at(self, position):
return self.__class__(self._s.at(position), self._inverter) return self.__class__(self._inp.at(position), self._mean, self._cov)
@property @property
@memo @memo
def _gradient_helper(self): def residual(self):
return self._s.gradient if self._mean is not None:
return self._inp.value - self._mean
return self._inp.value
@property @property
@memo @memo
def value(self): def value(self):
return .5 * self._s.value.squared_norm() if self._cov is None:
return .5 * self.residual.vdot(self.residual).real
return .5 * self.residual.vdot(self._cov.inverse(self.residual)).real
@property @property
@memo @memo
def gradient(self): def gradient(self):
return self._gradient_helper.adjoint(self._s.value) if self._cov is None:
return self._inp.gradient.adjoint(self.residual)
return self._inp.gradient.adjoint(self._cov.inverse(self.residual))
@property @property
@memo @memo
def curvature(self): def curvature(self):
c = SandwichOperator.make(self._gradient_helper) if self._cov is None:
if self._inverter is None: return SandwichOperator.make(self._inp.gradient, None)
return c return SandwichOperator.make(self._inp.gradient, self._cov.inverse)
return InversionEnabler(c, self._inverter)
...@@ -23,7 +23,7 @@ from ..operators.sandwich_operator import SandwichOperator ...@@ -23,7 +23,7 @@ from ..operators.sandwich_operator import SandwichOperator
from ..sugar import log, makeOp from ..sugar import log, makeOp
class PoissonLogLikelihood(Energy): class PoissonianEnergy(Energy):
def __init__(self, lamb, d): def __init__(self, lamb, d):
""" """
lamb: Sky model object lamb: Sky model object
...@@ -31,7 +31,7 @@ class PoissonLogLikelihood(Energy): ...@@ -31,7 +31,7 @@ class PoissonLogLikelihood(Energy):
value = 0.5 * s.vdot(s), i.e. a log-Gauss distribution with unit value = 0.5 * s.vdot(s), i.e. a log-Gauss distribution with unit
covariance covariance
""" """
super(PoissonLogLikelihood, self).__init__(lamb.position) super(PoissonianEnergy, self).__init__(lamb.position)
self._lamb = lamb self._lamb = lamb
self._d = d self._d = d
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik # NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik
# and financially supported by the Studienstiftung des deutschen Volkes. # and financially supported by the Studienstiftung des deutschen Volkes.
import numpy as np
from ..field import Field from ..field import Field
from ..multi import MultiField from ..multi import MultiField
from ..utilities import NiftyMetaBase, memo from ..utilities import NiftyMetaBase, memo
...@@ -128,30 +129,27 @@ class Energy(NiftyMetaBase()): ...@@ -128,30 +129,27 @@ class Energy(NiftyMetaBase()):
""" """
return None return None
def __mul__(self, factor):
from .energy_sum import EnergySum
if isinstance(factor, (float, int)):
return EnergySum.make([self], [factor])
return NotImplemented
def __rmul__(self, factor):
return self.__mul__(factor)
def __add__(self, other): def __add__(self, other):
if not isinstance(other, Energy): from .energy_sum import EnergySum
raise TypeError if isinstance(other, Energy):
return Add(self, other) return EnergySum.make([self, other])
return NotImplemented
def __sub__(self, other): def __sub__(self, other):
if not isinstance(other, Energy): from .energy_sum import EnergySum
raise TypeError if isinstance(other, Energy):
return Add(self, (-1) * other) return EnergySum.make([self, other], [1., -1.])
return NotImplemented
def Add(energy1, energy2): def __neg__(self):
if (isinstance(energy1.position, MultiField) and from .energy_sum import EnergySum
isinstance(energy2.position, MultiField)): return EnergySum.make([self], [-1.])