energy_operators.py 12.4 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):
Martin Reinecke's avatar
Martin Reinecke committed
125
126
        if mean is not None and not isinstance(mean, (Field, MultiField)):
            raise TypeError
Philipp Arras's avatar
Philipp Arras committed
127
128
129
130
        if covariance is not None and not isinstance(covariance,
                                                     LinearOperator):
            raise TypeError

Martin Reinecke's avatar
Martin Reinecke committed
131
132
133
134
135
136
137
138
139
140
        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
141
142
143
144
        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
145
146
147
        self._icov = None if covariance is None else covariance.inverse

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

    def apply(self, x):
156
        self._check_input(x)
Philipp Arras's avatar
Philipp Arras committed
157
        residual = x if self._mean is None else x - self._mean
Philipp Arras's avatar
Changes  
Philipp Arras committed
158
        res = self._op(residual).real
159
        if not isinstance(x, Linearization) or not x.want_metric:
Martin Reinecke's avatar
Martin Reinecke committed
160
161
162
163
164
165
            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
166
167
    """Computes likelihood Hamiltonians of expected count field constrained by
    Poissonian count data.
168

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

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

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

    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
182
    """
Philipp Arras's avatar
Philipp Arras committed
183

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

    def apply(self, x):
193
        self._check_input(x)
194
195
196
197
198
199
200
201
        if isinstance(x, Linearization):
            inp_vals = x.val.local_data
        else:
            inp_vals = x.local_data
        fix_inf = False
        if np.any(inp_vals==np.inf):
            if not np.any(np.isnan(inp_vals)):
                fix_inf=True #Note: This will break for MPI if there are NaNs in some threads but not others
Martin Reinecke's avatar
Martin Reinecke committed
202
        if not isinstance(x, Linearization):
203
204
            if fix_inf:
                res = np.inf
Martin Reinecke's avatar
Martin Reinecke committed
205
            return Field.scalar(res)
206
207
208
        res = x.sum() - x.log().vdot(self._d)
        if fix_inf:
            res = Linearization(Field.scalar(np.inf), res.jac)
209
210
        if not x.want_metric:
            return res
Martin Reinecke's avatar
Martin Reinecke committed
211
212
213
        metric = SandwichOperator.make(x.jac, makeOp(1./x.val))
        return res.add_metric(metric)

214

215
class InverseGammaLikelihood(EnergyOperator):
Philipp Arras's avatar
Docs  
Philipp Arras committed
216
    """Computes the negative log-likelihood of the inverse gamma distribution.
217
218
219

    It negative log-pdf(x) is given by

Martin Reinecke's avatar
Martin Reinecke committed
220
221
222
223
224
225
226
    .. 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`.
227
228
229
230
231
232
233

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

236
237
    def __init__(self, beta, alpha=-0.5):
        if not isinstance(beta, Field):
Philipp Arras's avatar
Philipp Arras committed
238
            raise TypeError
239
240
        self._beta = beta
        if np.isscalar(alpha):
Martin Reinecke's avatar
Martin Reinecke committed
241
242
            alpha = Field.from_local_data(
                beta.domain, np.full(beta.local_data.shape, alpha))
243
244
245
246
        elif not isinstance(alpha, Field):
            raise TypeError
        self._alphap1 = alpha+1
        self._domain = DomainTuple.make(beta.domain)
247
248

    def apply(self, x):
249
        self._check_input(x)
250
        res = x.log().vdot(self._alphap1) + (1./x).vdot(self._beta)
251
252
        if not isinstance(x, Linearization):
            return Field.scalar(res)
253
254
        if not x.want_metric:
            return res
255
        metric = SandwichOperator.make(x.jac, makeOp(self._alphap1/(x.val**2)))
256
257
258
        return res.add_metric(metric)


Martin Reinecke's avatar
Martin Reinecke committed
259
class BernoulliEnergy(EnergyOperator):
Philipp Arras's avatar
Philipp Arras committed
260
    """Computes likelihood energy of expected event frequency constrained by
261
262
    event data.

Philipp Arras's avatar
Philipp Arras committed
263
264
265
266
267
268
269
    .. 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.

270
271
    Parameters
    ----------
Martin Reinecke's avatar
Martin Reinecke committed
272
    d : Field
Philipp Arras's avatar
Philipp Arras committed
273
        Data field with events (1) or non-events (0).
Martin Reinecke's avatar
Martin Reinecke committed
274
    """
Philipp Arras's avatar
Philipp Arras committed
275

276
    def __init__(self, d):
Philipp Arras's avatar
Philipp Arras committed
277
278
279
280
        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
281
        self._d = d
282
        self._domain = DomainTuple.make(d.domain)
Martin Reinecke's avatar
Martin Reinecke committed
283
284

    def apply(self, x):
285
        self._check_input(x)
Philipp Arras's avatar
Philipp Arras committed
286
        v = -(x.log().vdot(self._d) + (1. - x).log().vdot(1. - self._d))
Martin Reinecke's avatar
Martin Reinecke committed
287
        if not isinstance(x, Linearization):
Martin Reinecke's avatar
Martin Reinecke committed
288
            return Field.scalar(v)
289
290
        if not x.want_metric:
            return v
Philipp Arras's avatar
Philipp Arras committed
291
        met = makeOp(1./(x.val*(1. - x.val)))
Martin Reinecke's avatar
Martin Reinecke committed
292
293
294
295
        met = SandwichOperator.make(x.jac, met)
        return v.add_metric(met)


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

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

Philipp Arras's avatar
Philipp Arras committed
302
303
    .. math ::
         H(f) = 0.5 f^\\dagger f + E_{lh}(f):
304

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

Martin Reinecke's avatar
Martin Reinecke committed
308
    By implementing prior information this way, the field prior is represented
309
310
311
    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
312
313
314
315
316
317
318
319
    The metric of this operator can be used as covariance for drawing Gaussian
    samples.

    Parameters
    ----------
    lh : EnergyOperator
        The likelihood energy.
    ic_samp : IterationController
320
        Tells an internal :class:`SamplingEnabler` which convergence criterion
Philipp Arras's avatar
Philipp Arras committed
321
322
323
324
325
326
        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
327
    `<https://arxiv.org/abs/1812.04403>`_
Martin Reinecke's avatar
Martin Reinecke committed
328
    """
Philipp Arras's avatar
Philipp Arras committed
329

Martin Reinecke's avatar
Martin Reinecke committed
330
331
332
333
    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
334
        self._domain = lh.domain
Martin Reinecke's avatar
Martin Reinecke committed
335
336

    def apply(self, x):
337
        self._check_input(x)
Philipp Arras's avatar
Philipp Arras committed
338
339
340
        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
341
        else:
342
            lhx, prx = self._lh(x), self._prior(x)
343
344
            mtr = SamplingEnabler(lhx.metric, prx.metric,
                                  self._ic_samp)
Philipp Arras's avatar
Philipp Arras committed
345
            return (lhx + prx).add_metric(mtr)
Martin Reinecke's avatar
Martin Reinecke committed
346

Philipp Arras's avatar
Philipp Arras committed
347
348
349
    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
350
        return 'StandardHamiltonian:\n' + utilities.indent(subs)
Philipp Arras's avatar
Philipp Arras committed
351

Martin Reinecke's avatar
Martin Reinecke committed
352

Martin Reinecke's avatar
Martin Reinecke committed
353
class AveragedEnergy(EnergyOperator):
Philipp Arras's avatar
Docs  
Philipp Arras committed
354
    """Averages an energy over samples.
Martin Reinecke's avatar
Martin Reinecke committed
355

356
357
358
    Parameters
    ----------
    h: Hamiltonian
Philipp Arras's avatar
Philipp Arras committed
359
       The energy to be averaged.
Martin Reinecke's avatar
Martin Reinecke committed
360
    res_samples : iterable of Fields
Torsten Ensslin's avatar
Torsten Ensslin committed
361
362
       Set of residual sample points to be added to mean field for
       approximate estimation of the KL.
363

Philipp Arras's avatar
Docs  
Philipp Arras committed
364
365
366
367
368
    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
369

Philipp Arras's avatar
Docs  
Philipp Arras committed
370
371
372
    - :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
373
    """
Martin Reinecke's avatar
Martin Reinecke committed
374
375
376

    def __init__(self, h, res_samples):
        self._h = h
Martin Reinecke's avatar
Martin Reinecke committed
377
        self._domain = h.domain
Martin Reinecke's avatar
Martin Reinecke committed
378
379
380
        self._res_samples = tuple(res_samples)

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