energy_operators.py 13.5 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
30
from .scaling_operator import ScalingOperator
Martin Reinecke's avatar
Martin Reinecke committed
31
from .simple_linear_operators import VdotOperator
Martin Reinecke's avatar
Martin Reinecke committed
32
33
34


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

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

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


48
49
class Squared2NormOperator(EnergyOperator):
    """Computes the square of the L2-norm of the output of an operator.
50

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

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

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

Martin Reinecke's avatar
Martin Reinecke committed
68

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

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

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

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

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


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

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

Philipp Arras's avatar
Philipp Arras committed
104
105
    .. 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
106

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

Philipp Arras's avatar
Philipp Arras committed
110
111
112
113
    Parameters
    ----------
    mean : Field
        Mean of the Gaussian. Default is 0.
114
115
    inverse_covariance : LinearOperator
        Inverse covariance of the Gaussian. Default is the identity operator.
Philipp Arras's avatar
Fixup    
Philipp Arras committed
116
    domain : Domain, DomainTuple, tuple of Domain or MultiDomain
Philipp Arras's avatar
Philipp Arras committed
117
118
119
120
121
122
        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
123
    """
Martin Reinecke's avatar
Martin Reinecke committed
124

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

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

    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
        if not isinstance(d, Field) or not np.issubdtype(d.dtype, np.integer):
            raise TypeError
Martin Reinecke's avatar
stage2    
Martin Reinecke committed
187
        if np.any(d.val < 0):
Philipp Arras's avatar
Philipp Arras committed
188
            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)
Martin Reinecke's avatar
Martin Reinecke committed
194
        res = x.sum()
Martin Reinecke's avatar
stage2    
Martin Reinecke committed
195
        tmp = res.val.val if isinstance(res, Linearization) else res
Martin Reinecke's avatar
Martin Reinecke committed
196
197
        # if we have no infinity here, we can continue with the calculation;
        # otherwise we know that the result must also be infinity
Martin Reinecke's avatar
Martin Reinecke committed
198
        if not np.isinf(tmp):
Martin Reinecke's avatar
Martin Reinecke committed
199
            res = res - x.log().vdot(self._d)
Martin Reinecke's avatar
Martin Reinecke committed
200
        if not isinstance(x, Linearization):
Martin Reinecke's avatar
Martin Reinecke committed
201
            return Field.scalar(res)
202
203
        if not x.want_metric:
            return res
Martin Reinecke's avatar
Martin Reinecke committed
204
205
206
        metric = SandwichOperator.make(x.jac, makeOp(1./x.val))
        return res.add_metric(metric)

207

208
class InverseGammaLikelihood(EnergyOperator):
Philipp Arras's avatar
Docs    
Philipp Arras committed
209
    """Computes the negative log-likelihood of the inverse gamma distribution.
210
211
212

    It negative log-pdf(x) is given by

Martin Reinecke's avatar
Martin Reinecke committed
213
214
215
216
217
218
219
    .. 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`.
220
221
222
223
224
225
226

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

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

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


251
class StudentTEnergy(EnergyOperator):
Lukas Platz's avatar
Lukas Platz committed
252
    """Computes likelihood energy corresponding to Student's t-distribution.
253
254

    .. math ::
Lukas Platz's avatar
Lukas Platz committed
255
256
        E_\\theta(f) = -\\log \\text{StudentT}_\\theta(f)
                     = \\frac{\\theta + 1}{2} \\log(1 + \\frac{f^2}{\\theta}),
257

Lukas Platz's avatar
Lukas Platz committed
258
    where f is a field defined on `domain`.
259
260
261

    Parameters
    ----------
Lukas Platz's avatar
Lukas Platz committed
262
263
    domain : `Domain` or `DomainTuple`
        Domain of the operator
264
265
266
267
268
269
270
271
272
273
    theta : Scalar
        Degree of freedom parameter for the student t distribution
    """

    def __init__(self, domain, theta):
        self._domain = DomainTuple.make(domain)
        self._theta = theta

    def apply(self, x):
        self._check_input(x)
274
        v = ((self._theta+1)/2)*(x**2/self._theta).log1p().sum()
275
276
277
278
        if not isinstance(x, Linearization):
            return Field.scalar(v)
        if not x.want_metric:
            return v
279
        met = ScalingOperator(self.domain, (self._theta+1) / (self._theta+3))
280
281
282
283
        met = SandwichOperator.make(x.jac, met)
        return v.add_metric(met)


Martin Reinecke's avatar
Martin Reinecke committed
284
class BernoulliEnergy(EnergyOperator):
Philipp Arras's avatar
Philipp Arras committed
285
    """Computes likelihood energy of expected event frequency constrained by
286
287
    event data.

Philipp Arras's avatar
Philipp Arras committed
288
289
290
291
292
293
294
    .. 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.

295
296
    Parameters
    ----------
Martin Reinecke's avatar
Martin Reinecke committed
297
    d : Field
Philipp Arras's avatar
Philipp Arras committed
298
        Data field with events (1) or non-events (0).
Martin Reinecke's avatar
Martin Reinecke committed
299
    """
Philipp Arras's avatar
Philipp Arras committed
300

301
    def __init__(self, d):
Philipp Arras's avatar
Philipp Arras committed
302
303
        if not isinstance(d, Field) or not np.issubdtype(d.dtype, np.integer):
            raise TypeError
Martin Reinecke's avatar
stage2    
Martin Reinecke committed
304
        if not np.all(np.logical_or(d.val == 0, d.val == 1)):
Philipp Arras's avatar
Philipp Arras committed
305
            raise ValueError
Martin Reinecke's avatar
Martin Reinecke committed
306
        self._d = d
307
        self._domain = DomainTuple.make(d.domain)
Martin Reinecke's avatar
Martin Reinecke committed
308
309

    def apply(self, x):
310
        self._check_input(x)
Philipp Arras's avatar
Philipp Arras committed
311
        v = -(x.log().vdot(self._d) + (1. - x).log().vdot(1. - self._d))
Martin Reinecke's avatar
Martin Reinecke committed
312
        if not isinstance(x, Linearization):
Martin Reinecke's avatar
Martin Reinecke committed
313
            return Field.scalar(v)
314
315
        if not x.want_metric:
            return v
Philipp Arras's avatar
Philipp Arras committed
316
        met = makeOp(1./(x.val*(1. - x.val)))
Martin Reinecke's avatar
Martin Reinecke committed
317
318
319
320
        met = SandwichOperator.make(x.jac, met)
        return v.add_metric(met)


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

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

Philipp Arras's avatar
Philipp Arras committed
327
328
    .. math ::
         H(f) = 0.5 f^\\dagger f + E_{lh}(f):
329

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

Martin Reinecke's avatar
Martin Reinecke committed
333
    By implementing prior information this way, the field prior is represented
334
335
336
    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
337
338
339
340
341
342
343
344
    The metric of this operator can be used as covariance for drawing Gaussian
    samples.

    Parameters
    ----------
    lh : EnergyOperator
        The likelihood energy.
    ic_samp : IterationController
345
        Tells an internal :class:`SamplingEnabler` which convergence criterion
Philipp Arras's avatar
Philipp Arras committed
346
347
348
349
350
351
        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
352
    `<https://arxiv.org/abs/1812.04403>`_
Martin Reinecke's avatar
Martin Reinecke committed
353
    """
Philipp Arras's avatar
Philipp Arras committed
354

355
    def __init__(self, lh, ic_samp=None, _c_inp=None):
Martin Reinecke's avatar
Martin Reinecke committed
356
357
        self._lh = lh
        self._prior = GaussianEnergy(domain=lh.domain)
358
359
        if _c_inp is not None:
            _, self._prior = self._prior.simplify_for_constant_input(_c_inp)
Martin Reinecke's avatar
Martin Reinecke committed
360
        self._ic_samp = ic_samp
Martin Reinecke's avatar
Martin Reinecke committed
361
        self._domain = lh.domain
Martin Reinecke's avatar
Martin Reinecke committed
362
363

    def apply(self, x):
364
        self._check_input(x)
Philipp Arras's avatar
Philipp Arras committed
365
366
367
        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
368
        else:
369
            lhx, prx = self._lh(x), self._prior(x)
370
371
            mtr = SamplingEnabler(lhx.metric, prx.metric,
                                  self._ic_samp)
Philipp Arras's avatar
Philipp Arras committed
372
            return (lhx + prx).add_metric(mtr)
Martin Reinecke's avatar
Martin Reinecke committed
373

Philipp Arras's avatar
Philipp Arras committed
374
375
    def __repr__(self):
        subs = 'Likelihood:\n{}'.format(utilities.indent(self._lh.__repr__()))
376
        subs += '\nPrior:\n{}'.format(self._prior)
Martin Reinecke's avatar
Martin Reinecke committed
377
        return 'StandardHamiltonian:\n' + utilities.indent(subs)
Philipp Arras's avatar
Philipp Arras committed
378

379
380
381
382
    def _simplify_for_constant_input_nontrivial(self, c_inp):
        out, lh1 = self._lh.simplify_for_constant_input(c_inp)
        return out, StandardHamiltonian(lh1, self._ic_samp, _c_inp=c_inp)

Martin Reinecke's avatar
Martin Reinecke committed
383

Martin Reinecke's avatar
Martin Reinecke committed
384
class AveragedEnergy(EnergyOperator):
Philipp Arras's avatar
Docs    
Philipp Arras committed
385
    """Averages an energy over samples.
Martin Reinecke's avatar
Martin Reinecke committed
386

387
388
389
    Parameters
    ----------
    h: Hamiltonian
Philipp Arras's avatar
Philipp Arras committed
390
       The energy to be averaged.
Martin Reinecke's avatar
Martin Reinecke committed
391
    res_samples : iterable of Fields
Torsten Ensslin's avatar
Torsten Ensslin committed
392
393
       Set of residual sample points to be added to mean field for
       approximate estimation of the KL.
394

Philipp Arras's avatar
Docs    
Philipp Arras committed
395
396
397
398
399
    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
400

Philipp Arras's avatar
Docs    
Philipp Arras committed
401
402
403
    - :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
404
    """
Martin Reinecke's avatar
Martin Reinecke committed
405
406
407

    def __init__(self, h, res_samples):
        self._h = h
Martin Reinecke's avatar
Martin Reinecke committed
408
        self._domain = h.domain
Martin Reinecke's avatar
Martin Reinecke committed
409
410
411
        self._res_samples = tuple(res_samples)

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