energy_operators.py 12 KB
Newer Older
Martin Reinecke's avatar
Martin Reinecke committed
1 2 3 4 5 6 7 8 9 10 11 12 13
# 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/>.
#
14
# Copyright(C) 2013-2019 Max-Planck-Society
Martin Reinecke's avatar
Martin Reinecke committed
15
#
16
# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik.
Martin Reinecke's avatar
Martin Reinecke committed
17

Philipp Arras's avatar
Philipp Arras committed
18 19
import numpy as np

Philipp Arras's avatar
Philipp Arras committed
20
from .. import utilities
Martin Reinecke's avatar
Martin Reinecke committed
21
from ..domain_tuple import DomainTuple
Philipp Arras's avatar
Philipp Arras committed
22
from ..field import Field
23
from ..multi_field import MultiField
Philipp Arras's avatar
Philipp Arras committed
24
from ..linearization import Linearization
Philipp Arras's avatar
Philipp Arras committed
25 26
from ..sugar import makeDomain, makeOp
from .linear_operator import LinearOperator
Martin Reinecke's avatar
Martin Reinecke committed
27
from .operator import Operator
Martin Reinecke's avatar
fix  
Martin Reinecke committed
28
from .sampling_enabler import SamplingEnabler
Philipp Arras's avatar
Philipp Arras committed
29
from .sandwich_operator import SandwichOperator
Martin Reinecke's avatar
Martin Reinecke committed
30
from .simple_linear_operators import VdotOperator
Martin Reinecke's avatar
Martin Reinecke committed
31 32 33


class EnergyOperator(Operator):
Philipp Arras's avatar
Philipp Arras committed
34
    """Operator which has a scalar domain as target domain.
35

Martin Reinecke's avatar
Martin Reinecke committed
36
    It is intended as an objective function for field inference.
37

Philipp Arras's avatar
Philipp Arras committed
38 39 40
    Examples
    --------
     - Information Hamiltonian, i.e. negative-log-probabilities.
Martin Reinecke's avatar
Martin Reinecke committed
41
     - Gibbs free energy, i.e. an averaged Hamiltonian, aka Kullback-Leibler
Philipp Arras's avatar
Philipp Arras committed
42
       divergence.
43
    """
Martin Reinecke's avatar
Martin Reinecke committed
44 45 46 47
    _target = DomainTuple.scalar_domain()


class SquaredNormOperator(EnergyOperator):
Philipp Arras's avatar
Philipp Arras committed
48
    """Computes the L2-norm of the output of an operator.
49

Philipp Arras's avatar
Philipp Arras committed
50 51 52
    Parameters
    ----------
    domain : Domain, DomainTuple or tuple of Domain
53
        Domain of the operator in which the L2-norm shall be computed.
Martin Reinecke's avatar
Martin Reinecke committed
54
    """
Philipp Arras's avatar
Philipp Arras committed
55

Martin Reinecke's avatar
Martin Reinecke committed
56 57 58 59
    def __init__(self, domain):
        self._domain = domain

    def apply(self, x):
60
        self._check_input(x)
Martin Reinecke's avatar
Martin Reinecke committed
61
        if isinstance(x, Linearization):
Martin Reinecke's avatar
Martin Reinecke committed
62
            val = Field.scalar(x.val.vdot(x.val))
Martin Reinecke's avatar
Martin Reinecke committed
63
            jac = VdotOperator(2*x.val)(x.jac)
64
            return x.new(val, jac)
Martin Reinecke's avatar
Martin Reinecke committed
65
        return Field.scalar(x.vdot(x))
Martin Reinecke's avatar
Martin Reinecke committed
66 67 68


class QuadraticFormOperator(EnergyOperator):
Philipp Arras's avatar
Philipp Arras committed
69
    """Computes the L2-norm of a Field or MultiField with respect to a
70
    specific kernel given by `endo`.
Philipp Arras's avatar
Philipp Arras committed
71 72 73

    .. math ::
        E(f) = \\frac12 f^\\dagger \\text{endo}(f)
74 75 76

    Parameters
    ----------
Philipp Arras's avatar
Philipp Arras committed
77
    endo : EndomorphicOperator
78
         Kernel of the quadratic form
Martin Reinecke's avatar
Martin Reinecke committed
79
    """
Philipp Arras's avatar
Philipp Arras committed
80 81

    def __init__(self, endo):
Martin Reinecke's avatar
Martin Reinecke committed
82
        from .endomorphic_operator import EndomorphicOperator
Philipp Arras's avatar
Philipp Arras committed
83
        if not isinstance(endo, EndomorphicOperator):
Martin Reinecke's avatar
Martin Reinecke committed
84
            raise TypeError("op must be an EndomorphicOperator")
Philipp Arras's avatar
Philipp Arras committed
85 86
        self._op = endo
        self._domain = endo.domain
Martin Reinecke's avatar
Martin Reinecke committed
87 88

    def apply(self, x):
89
        self._check_input(x)
Martin Reinecke's avatar
Martin Reinecke committed
90
        if isinstance(x, Linearization):
Martin Reinecke's avatar
Martin Reinecke committed
91 92
            t1 = self._op(x.val)
            jac = VdotOperator(t1)(x.jac)
Martin Reinecke's avatar
Martin Reinecke committed
93
            val = Field.scalar(0.5*x.val.vdot(t1))
94
            return x.new(val, jac)
Martin Reinecke's avatar
Martin Reinecke committed
95
        return Field.scalar(0.5*x.vdot(self._op(x)))
Martin Reinecke's avatar
Martin Reinecke committed
96 97 98


class GaussianEnergy(EnergyOperator):
Philipp Arras's avatar
Docs  
Philipp Arras committed
99
    """Computes a negative-log Gaussian.
100

Philipp Arras's avatar
Philipp Arras committed
101
    Represents up to constants in :math:`m`:
Martin Reinecke's avatar
Martin Reinecke committed
102

Philipp Arras's avatar
Philipp Arras committed
103 104
    .. math ::
        E(f) = - \\log G(f-m, D) = 0.5 (f-m)^\\dagger D^{-1} (f-m),
Martin Reinecke's avatar
cleanup  
Martin Reinecke committed
105

Philipp Arras's avatar
Philipp Arras committed
106 107
    an information energy for a Gaussian distribution with mean m and
    covariance D.
108

Philipp Arras's avatar
Philipp Arras committed
109 110 111 112 113 114
    Parameters
    ----------
    mean : Field
        Mean of the Gaussian. Default is 0.
    covariance : LinearOperator
        Covariance of the Gaussian. Default is the identity operator.
Philipp Arras's avatar
Fixup  
Philipp Arras committed
115
    domain : Domain, DomainTuple, tuple of Domain or MultiDomain
Philipp Arras's avatar
Philipp Arras committed
116 117 118 119 120 121
        Operator domain. By default it is inferred from `mean` or
        `covariance` if specified

    Note
    ----
    At least one of the arguments has to be provided.
Martin Reinecke's avatar
Martin Reinecke committed
122
    """
Martin Reinecke's avatar
Martin Reinecke committed
123

Martin Reinecke's avatar
Martin Reinecke committed
124
    def __init__(self, mean=None, covariance=None, domain=None):
Philipp Arras's avatar
Philipp Arras committed
125
        if mean is not None and not isinstance(mean, Field):
126 127
            if not isinstance(mean, MultiField):
                raise TypeError
Philipp Arras's avatar
Philipp Arras committed
128 129 130 131
        if covariance is not None and not isinstance(covariance,
                                                     LinearOperator):
            raise TypeError

Martin Reinecke's avatar
Martin Reinecke committed
132 133 134 135 136 137 138 139 140 141
        self._domain = None
        if mean is not None:
            self._checkEquivalence(mean.domain)
        if covariance is not None:
            self._checkEquivalence(covariance.domain)
        if domain is not None:
            self._checkEquivalence(domain)
        if self._domain is None:
            raise ValueError("no domain given")
        self._mean = mean
Martin Reinecke's avatar
Martin Reinecke committed
142 143 144 145
        if covariance is None:
            self._op = SquaredNormOperator(self._domain).scale(0.5)
        else:
            self._op = QuadraticFormOperator(covariance.inverse)
Martin Reinecke's avatar
Martin Reinecke committed
146 147 148
        self._icov = None if covariance is None else covariance.inverse

    def _checkEquivalence(self, newdom):
Martin Reinecke's avatar
fix  
Martin Reinecke committed
149
        newdom = makeDomain(newdom)
Martin Reinecke's avatar
Martin Reinecke committed
150
        if self._domain is None:
Philipp Arras's avatar
Philipp Arras committed
151
            self._domain = newdom
Martin Reinecke's avatar
Martin Reinecke committed
152
        else:
Philipp Arras's avatar
Philipp Arras committed
153
            if self._domain != newdom:
Martin Reinecke's avatar
Martin Reinecke committed
154 155 156
                raise ValueError("domain mismatch")

    def apply(self, x):
157
        self._check_input(x)
Philipp Arras's avatar
Philipp Arras committed
158
        residual = x if self._mean is None else x - self._mean
Philipp Arras's avatar
Changes  
Philipp Arras committed
159
        res = self._op(residual).real
160
        if not isinstance(x, Linearization) or not x.want_metric:
Martin Reinecke's avatar
Martin Reinecke committed
161 162 163 164 165 166
            return res
        metric = SandwichOperator.make(x.jac, self._icov)
        return res.add_metric(metric)


class PoissonianEnergy(EnergyOperator):
Philipp Arras's avatar
Docs  
Philipp Arras committed
167 168
    """Computes likelihood Hamiltonians of expected count field constrained by
    Poissonian count data.
169

Philipp Arras's avatar
Philipp Arras committed
170
    Represents up to an f-independent term :math:`log(d!)`:
171

Philipp Arras's avatar
Philipp Arras committed
172 173
    .. math ::
        E(f) = -\\log \\text{Poisson}(d|f) = \\sum f - d^\\dagger \\log(f),
174

Philipp Arras's avatar
Philipp Arras committed
175
    where f is a :class:`Field` in data space with the expectation values for
Martin Reinecke's avatar
Martin Reinecke committed
176
    the counts.
Philipp Arras's avatar
Philipp Arras committed
177 178 179 180 181 182

    Parameters
    ----------
    d : Field
        Data field with counts. Needs to have integer dtype and all field
        values need to be non-negative.
Martin Reinecke's avatar
Martin Reinecke committed
183
    """
Philipp Arras's avatar
Philipp Arras committed
184

185
    def __init__(self, d):
Philipp Arras's avatar
Philipp Arras committed
186 187 188 189
        if not isinstance(d, Field) or not np.issubdtype(d.dtype, np.integer):
            raise TypeError
        if np.any(d.local_data < 0):
            raise ValueError
190 191
        self._d = d
        self._domain = DomainTuple.make(d.domain)
Martin Reinecke's avatar
Martin Reinecke committed
192 193

    def apply(self, x):
194
        self._check_input(x)
Martin Reinecke's avatar
Martin Reinecke committed
195 196
        res = x.sum() - x.log().vdot(self._d)
        if not isinstance(x, Linearization):
Martin Reinecke's avatar
Martin Reinecke committed
197
            return Field.scalar(res)
198 199
        if not x.want_metric:
            return res
Martin Reinecke's avatar
Martin Reinecke committed
200 201 202
        metric = SandwichOperator.make(x.jac, makeOp(1./x.val))
        return res.add_metric(metric)

203

204
class InverseGammaLikelihood(EnergyOperator):
Philipp Arras's avatar
Docs  
Philipp Arras committed
205
    """Computes the negative log-likelihood of the inverse gamma distribution.
206 207 208

    It negative log-pdf(x) is given by

Martin Reinecke's avatar
Martin Reinecke committed
209 210 211 212 213 214 215
    .. math ::

        \\sum_i (\\alpha_i+1)*\\ln(x_i) + \\beta_i/x_i

    This is the likelihood for the variance :math:`x=S_k` given data
    :math:`\\beta = 0.5 |s_k|^2` where the Field :math:`s` is known to have
    the covariance :math:`S_k`.
216 217 218 219 220 221 222

    Parameters
    ----------
    beta : Field
        beta parameter of the inverse gamma distribution
    alpha : Scalar, Field, optional
        alpha parameter of the inverse gamma distribution
223
    """
Philipp Arras's avatar
Philipp Arras committed
224

225 226
    def __init__(self, beta, alpha=-0.5):
        if not isinstance(beta, Field):
Philipp Arras's avatar
Philipp Arras committed
227
            raise TypeError
228 229
        self._beta = beta
        if np.isscalar(alpha):
Martin Reinecke's avatar
Martin Reinecke committed
230 231
            alpha = Field.from_local_data(
                beta.domain, np.full(beta.local_data.shape, alpha))
232 233 234 235
        elif not isinstance(alpha, Field):
            raise TypeError
        self._alphap1 = alpha+1
        self._domain = DomainTuple.make(beta.domain)
236 237

    def apply(self, x):
238
        self._check_input(x)
239
        res = x.log().vdot(self._alphap1) + (1./x).vdot(self._beta)
240 241
        if not isinstance(x, Linearization):
            return Field.scalar(res)
242 243
        if not x.want_metric:
            return res
244
        metric = SandwichOperator.make(x.jac, makeOp(self._alphap1/(x.val**2)))
245 246 247
        return res.add_metric(metric)


Martin Reinecke's avatar
Martin Reinecke committed
248
class BernoulliEnergy(EnergyOperator):
Philipp Arras's avatar
Philipp Arras committed
249
    """Computes likelihood energy of expected event frequency constrained by
250 251
    event data.

Philipp Arras's avatar
Philipp Arras committed
252 253 254 255 256 257 258
    .. math ::
        E(f) = -\\log \\text{Bernoulli}(d|f)
             = -d^\\dagger \\log f  - (1-d)^\\dagger \\log(1-f),

    where f is a field defined on `d.domain` with the expected
    frequencies of events.

259 260
    Parameters
    ----------
Martin Reinecke's avatar
Martin Reinecke committed
261
    d : Field
Philipp Arras's avatar
Philipp Arras committed
262
        Data field with events (1) or non-events (0).
Martin Reinecke's avatar
Martin Reinecke committed
263
    """
Philipp Arras's avatar
Philipp Arras committed
264

265
    def __init__(self, d):
Philipp Arras's avatar
Philipp Arras committed
266 267 268 269
        if not isinstance(d, Field) or not np.issubdtype(d.dtype, np.integer):
            raise TypeError
        if not np.all(np.logical_or(d.local_data == 0, d.local_data == 1)):
            raise ValueError
Martin Reinecke's avatar
Martin Reinecke committed
270
        self._d = d
271
        self._domain = DomainTuple.make(d.domain)
Martin Reinecke's avatar
Martin Reinecke committed
272 273

    def apply(self, x):
274
        self._check_input(x)
Philipp Arras's avatar
Philipp Arras committed
275
        v = -(x.log().vdot(self._d) + (1. - x).log().vdot(1. - self._d))
Martin Reinecke's avatar
Martin Reinecke committed
276
        if not isinstance(x, Linearization):
Martin Reinecke's avatar
Martin Reinecke committed
277
            return Field.scalar(v)
278 279
        if not x.want_metric:
            return v
Philipp Arras's avatar
Philipp Arras committed
280
        met = makeOp(1./(x.val*(1. - x.val)))
Martin Reinecke's avatar
Martin Reinecke committed
281 282 283 284
        met = SandwichOperator.make(x.jac, met)
        return v.add_metric(met)


285
class StandardHamiltonian(EnergyOperator):
Philipp Arras's avatar
Philipp Arras committed
286 287
    """Computes an information Hamiltonian in its standard form, i.e. with the
    prior being a Gaussian with unit covariance.
288

Philipp Arras's avatar
Philipp Arras committed
289
    Let the likelihood energy be :math:`E_{lh}`. Then this operator computes:
290

Philipp Arras's avatar
Philipp Arras committed
291 292
    .. math ::
         H(f) = 0.5 f^\\dagger f + E_{lh}(f):
293

Martin Reinecke's avatar
Martin Reinecke committed
294
    Other field priors can be represented via transformations of a white
295 296
    Gaussian field into a field with the desired prior probability structure.

Martin Reinecke's avatar
Martin Reinecke committed
297
    By implementing prior information this way, the field prior is represented
298 299 300
    by a generative model, from which NIFTy can draw samples and infer a field
    using the Maximum a Posteriori (MAP) or the Variational Bayes (VB) method.

Philipp Arras's avatar
Philipp Arras committed
301 302 303 304 305 306 307 308
    The metric of this operator can be used as covariance for drawing Gaussian
    samples.

    Parameters
    ----------
    lh : EnergyOperator
        The likelihood energy.
    ic_samp : IterationController
309
        Tells an internal :class:`SamplingEnabler` which convergence criterion
Philipp Arras's avatar
Philipp Arras committed
310 311 312 313 314 315 316
        to use to draw Gaussian samples.


    See also
    --------
    `Encoding prior knowledge in the structure of the likelihood`,
    Jakob Knollmüller, Torsten A. Ensslin,
Martin Reinecke's avatar
Martin Reinecke committed
317
    `<https://arxiv.org/abs/1812.04403>`_
Martin Reinecke's avatar
Martin Reinecke committed
318
    """
Philipp Arras's avatar
Philipp Arras committed
319

Martin Reinecke's avatar
Martin Reinecke committed
320 321 322 323
    def __init__(self, lh, ic_samp=None):
        self._lh = lh
        self._prior = GaussianEnergy(domain=lh.domain)
        self._ic_samp = ic_samp
Martin Reinecke's avatar
Martin Reinecke committed
324
        self._domain = lh.domain
Martin Reinecke's avatar
Martin Reinecke committed
325 326

    def apply(self, x):
327
        self._check_input(x)
Philipp Arras's avatar
Philipp Arras committed
328 329 330
        if (self._ic_samp is None or not isinstance(x, Linearization)
                or not x.want_metric):
            return self._lh(x) + self._prior(x)
Martin Reinecke's avatar
Martin Reinecke committed
331
        else:
332
            lhx, prx = self._lh(x), self._prior(x)
333 334
            mtr = SamplingEnabler(lhx.metric, prx.metric,
                                  self._ic_samp)
Philipp Arras's avatar
Philipp Arras committed
335
            return (lhx + prx).add_metric(mtr)
Martin Reinecke's avatar
Martin Reinecke committed
336

Philipp Arras's avatar
Philipp Arras committed
337 338 339
    def __repr__(self):
        subs = 'Likelihood:\n{}'.format(utilities.indent(self._lh.__repr__()))
        subs += '\nPrior: Quadratic{}'.format(self._lh.domain.keys())
Martin Reinecke's avatar
Martin Reinecke committed
340
        return 'StandardHamiltonian:\n' + utilities.indent(subs)
Philipp Arras's avatar
Philipp Arras committed
341

Martin Reinecke's avatar
Martin Reinecke committed
342

Martin Reinecke's avatar
Martin Reinecke committed
343
class AveragedEnergy(EnergyOperator):
Philipp Arras's avatar
Docs  
Philipp Arras committed
344
    """Averages an energy over samples.
Martin Reinecke's avatar
Martin Reinecke committed
345

346 347 348
    Parameters
    ----------
    h: Hamiltonian
Philipp Arras's avatar
Philipp Arras committed
349
       The energy to be averaged.
Martin Reinecke's avatar
Martin Reinecke committed
350
    res_samples : iterable of Fields
Torsten Ensslin's avatar
Torsten Ensslin committed
351 352
       Set of residual sample points to be added to mean field for
       approximate estimation of the KL.
353

Philipp Arras's avatar
Docs  
Philipp Arras committed
354 355 356 357 358
    Notes
    -----
    - Having symmetrized residual samples, with both :math:`v_i` and
      :math:`-v_i` being present, ensures that the distribution mean is
      exactly represented.
Torsten Ensslin's avatar
Fix te  
Torsten Ensslin committed
359

Philipp Arras's avatar
Docs  
Philipp Arras committed
360 361 362
    - :class:`AveragedEnergy(h)` approximates
      :math:`\\left< H(f) \\right>_{G(f-m,D)}` if the residuals :math:`f-m`
      are drawn from a Gaussian distribution with covariance :math:`D`.
Martin Reinecke's avatar
Martin Reinecke committed
363
    """
Martin Reinecke's avatar
Martin Reinecke committed
364 365 366

    def __init__(self, h, res_samples):
        self._h = h
Martin Reinecke's avatar
Martin Reinecke committed
367
        self._domain = h.domain
Martin Reinecke's avatar
Martin Reinecke committed
368 369 370
        self._res_samples = tuple(res_samples)

    def apply(self, x):
371
        self._check_input(x)
Philipp Arras's avatar
Philipp Arras committed
372 373
        mymap = map(lambda v: self._h(x + v), self._res_samples)
        return utilities.my_sum(mymap)*(1./len(self._res_samples))