correlated_fields.py 15.2 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
Philipp Arras's avatar
Philipp Arras committed
15
# Authors: Philipp Frank, Philipp Arras
Martin Reinecke's avatar
Martin Reinecke committed
16
#
17
# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik.
Philipp Arras's avatar
Philipp Arras committed
18

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

Philipp Arras's avatar
Philipp Arras committed
21
from ..domain_tuple import DomainTuple
Philipp Arras's avatar
Philipp Arras committed
22 23
from ..domains.power_space import PowerSpace
from ..domains.unstructured_domain import UnstructuredDomain
24
from ..field import Field
Philipp Arras's avatar
Philipp Arras committed
25
from ..operators.adder import Adder
26
from ..operators.contraction_operator import ContractionOperator
Philipp Arras's avatar
Philipp Arras committed
27
from ..operators.distributors import PowerDistributor
Philipp Arras's avatar
Philipp Arras committed
28
from ..operators.endomorphic_operator import EndomorphicOperator
Martin Reinecke's avatar
Martin Reinecke committed
29
from ..operators.harmonic_operators import HarmonicTransformOperator
Philipp Arras's avatar
Philipp Arras committed
30
from ..operators.linear_operator import LinearOperator
Philipp Arras's avatar
Philipp Arras committed
31 32
from ..operators.operator import Operator
from ..operators.simple_linear_operators import VdotOperator, ducktape
Philipp Arras's avatar
Philipp Arras committed
33
from ..operators.value_inserter import ValueInserter
34
from ..sugar import from_global_data, full, makeDomain
35
from ..probing import StatCalculator
36

Philipp Arras's avatar
Philipp Arras committed
37

38 39
def _lognormal_moments(mean, sig):
    mean, sig = float(mean), float(sig)
Philipp Arras's avatar
Philipp Arras committed
40 41 42
    assert sig > 0
    logsig = np.sqrt(np.log((sig/mean)**2 + 1))
    logmean = np.log(mean) - logsig**2/2
43 44
    return logmean, logsig

45 46 47 48 49 50 51 52 53 54
class _lognormal_moment_matching(Operator):
    def __init__(self,mean, sig, key):
        key = str(key)
        logmean, logsig = _lognormal_moments(mean, sig)
        self._mean = mean
        self._sig = sig
        op = _normal(logmean, logsig, key).exp()
        self._domain = op.domain
        self._target = op.target
        self.apply = op.apply
55

56 57 58
    @property
    def mean(self):
        return self._mean
Philipp Arras's avatar
Philipp Arras committed
59

60 61 62
    @property
    def std(self):
        return self._sig
63

Philipp Arras's avatar
Philipp Arras committed
64 65 66 67 68
def _normal(mean, sig, key):
    return Adder(Field.scalar(mean)) @ (
        sig*ducktape(DomainTuple.scalar_domain(), None, key))


Philipp Arras's avatar
Philipp Arras committed
69 70 71 72
def _log_k_lengths(pspace):
    return np.log(pspace.k_lengths[1:])


Philipp Arras's avatar
Philipp Arras committed
73 74 75 76 77 78 79 80 81 82 83
def _logkl(power_space):
    power_space = DomainTuple.make(power_space)
    assert isinstance(power_space[0], PowerSpace)
    assert len(power_space) == 1
    logkl = _log_k_lengths(power_space[0])
    assert logkl.shape[0] == power_space[0].shape[0] - 1
    logkl -= logkl[0]
    logkl = np.insert(logkl, 0, 0)
    return logkl


Philipp Frank's avatar
Philipp Frank committed
84
class _SlopeRemover(EndomorphicOperator):
Philipp Arras's avatar
Philipp Arras committed
85
    def __init__(self, domain):
Philipp Frank's avatar
Philipp Frank committed
86
        self._domain = makeDomain(domain)
Philipp Arras's avatar
Philipp Arras committed
87 88
        assert len(self._domain) == 1
        assert isinstance(self._domain[0], PowerSpace)
Philipp Arras's avatar
Philipp Arras committed
89
        logkl = _logkl(self._domain)
90
        self._sc = logkl/float(logkl[-1])
Philipp Frank's avatar
Philipp Frank committed
91
        self._capability = self.TIMES | self.ADJOINT_TIMES
Philipp Arras's avatar
Philipp Arras committed
92

93 94
    def apply(self, x, mode):
        self._check_input(x, mode)
Philipp Frank's avatar
Philipp Frank committed
95 96
        x = x.to_global_data()
        if mode == self.TIMES:
97
            res = x - x[-1]*self._sc
Philipp Frank's avatar
Philipp Frank committed
98
        else:
99
            res = np.zeros(x.shape, dtype=x.dtype)
Philipp Frank's avatar
Philipp Frank committed
100 101
            res += x
            res[-1] -= (x*self._sc).sum()
102
        return from_global_data(self._tgt(mode), res)
Philipp Frank's avatar
Philipp Frank committed
103

104

Philipp Arras's avatar
Philipp Arras committed
105 106 107
class _TwoLogIntegrations(LinearOperator):
    def __init__(self, target):
        self._target = makeDomain(target)
Philipp Arras's avatar
Philipp Arras committed
108 109
        assert len(self._target) == 1
        assert isinstance(self._target[0], PowerSpace)
Philipp Arras's avatar
Philipp Arras committed
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
        self._domain = makeDomain(
            UnstructuredDomain((2, self.target.shape[0] - 2)))
        self._capability = self.TIMES | self.ADJOINT_TIMES
        if not isinstance(self._target[0], PowerSpace):
            raise TypeError
        logk_lengths = _log_k_lengths(self._target[0])
        self._logvol = logk_lengths[1:] - logk_lengths[:-1]

    def apply(self, x, mode):
        self._check_input(x, mode)
        if mode == self.TIMES:
            x = x.to_global_data()
            res = np.empty(self._target.shape)
            res[0] = 0
            res[1] = 0
            res[2:] = np.cumsum(x[1])
            res[2:] = (res[2:] + res[1:-1])/2*self._logvol + x[0]
            res[2:] = np.cumsum(res[2:])
            return from_global_data(self._target, res)
        else:
            x = x.to_global_data_rw()
            res = np.zeros(self._domain.shape)
            x[2:] = np.cumsum(x[2:][::-1])[::-1]
            res[0] += x[2:]
            x[2:] *= self._logvol/2.
135 136
            x[1:-1] += x[2:]
            res[1] += np.cumsum(x[2:][::-1])[::-1]
Philipp Arras's avatar
Philipp Arras committed
137 138 139 140 141 142
            return from_global_data(self._domain, res)


class _Normalization(Operator):
    def __init__(self, domain):
        self._domain = self._target = makeDomain(domain)
Philipp Arras's avatar
Philipp Arras committed
143
        assert len(self._domain) == 1
Philipp Arras's avatar
Philipp Arras committed
144
        assert isinstance(self._domain[0], PowerSpace)
Philipp Arras's avatar
Philipp Arras committed
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
        hspace = self._domain[0].harmonic_partner
        pd = PowerDistributor(hspace, power_space=self._domain[0])
        cst = pd.adjoint(full(pd.target, 1.)).to_global_data_rw()
        cst[0] = 0
        self._cst = from_global_data(self._domain, cst)
        self._specsum = _SpecialSum(self._domain)

    def apply(self, x):
        self._check_input(x)
        amp = x.exp()
        spec = (2*x).exp()
        # FIXME This normalizes also the zeromode which is supposed to be left
        # untouched by this operator
        return self._specsum(self._cst*spec)**(-0.5)*amp


class _SpecialSum(EndomorphicOperator):
    def __init__(self, domain):
        self._domain = makeDomain(domain)
Philipp Arras's avatar
Philipp Arras committed
164
        assert len(self._domain) == 1
Philipp Arras's avatar
Philipp Arras committed
165 166 167 168 169 170 171
        self._capability = self.TIMES | self.ADJOINT_TIMES

    def apply(self, x, mode):
        self._check_input(x, mode)
        return full(self._tgt(mode), x.sum())


172 173 174
class _Amplitude(Operator):
    def __init__(self, target, fluctuations, flexibility, asperity,
                 loglogavgslope, key):
Philipp Arras's avatar
Philipp Arras committed
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
        """
        fluctuations > 0
        flexibility > 0
        asperity > 0
        loglogavgslope probably negative
        """
        assert isinstance(fluctuations, Operator)
        assert isinstance(flexibility, Operator)
        assert isinstance(asperity, Operator)
        assert isinstance(loglogavgslope, Operator)
        target = makeDomain(target)
        assert len(target) == 1
        assert isinstance(target[0], PowerSpace)

        twolog = _TwoLogIntegrations(target)
        dt = twolog._logvol
        sc = np.zeros(twolog.domain.shape)
        sc[0] = sc[1] = np.sqrt(dt)
        sc = from_global_data(twolog.domain, sc)
        expander = VdotOperator(sc).adjoint
        sigmasq = expander @ flexibility

        dist = np.zeros(twolog.domain.shape)
        dist[0] += 1.
        dist = from_global_data(twolog.domain, dist)
        scale = VdotOperator(dist).adjoint @ asperity

        shift = np.ones(scale.target.shape)
        shift[0] = dt**2/12.
        shift = from_global_data(scale.target, shift)
        scale = sigmasq*(Adder(shift) @ scale).sqrt()

        smooth = twolog @ (scale*ducktape(scale.target, None, key))
Philipp Arras's avatar
Philipp Arras committed
208
        tg = smooth.target
Philipp Arras's avatar
Philipp Arras committed
209 210
        noslope = _SlopeRemover(tg) @ smooth
        _t = VdotOperator(from_global_data(tg, _logkl(tg))).adjoint
Philipp Arras's avatar
Philipp Arras committed
211
        smoothslope = _t @ loglogavgslope + noslope
Philipp Arras's avatar
Philipp Arras committed
212 213 214 215 216 217 218 219 220

        normal_ampl = _Normalization(target) @ smoothslope
        vol = target[0].harmonic_partner.get_default_codomain().total_volume
        arr = np.zeros(target.shape)
        arr[1:] = vol
        expander = VdotOperator(from_global_data(target, arr)).adjoint
        mask = np.zeros(target.shape)
        mask[0] = vol
        adder = Adder(from_global_data(target, mask))
Philipp Arras's avatar
Philipp Arras committed
221 222
        op = adder @ ((expander @ fluctuations)*normal_ampl)
        self.apply = op.apply
223
        self.fluctuation_amplitude = fluctuations
Philipp Arras's avatar
Philipp Arras committed
224
        self._domain, self._target = op.domain, op.target
Philipp Arras's avatar
Philipp Arras committed
225

226 227 228 229

class CorrelatedFieldMaker:
    def __init__(self):
        self._a = []
230
        self._azm = None
231 232 233 234 235 236 237 238 239 240 241

    def add_fluctuations(self,
                         target,
                         fluctuations_mean,
                         fluctuations_stddev,
                         flexibility_mean,
                         flexibility_stddev,
                         asperity_mean,
                         asperity_stddev,
                         loglogavgslope_mean,
                         loglogavgslope_stddev,
242 243
                         prefix='',
                         index = None):
Philipp Arras's avatar
Philipp Arras committed
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
        fluctuations_mean = float(fluctuations_mean)
        fluctuations_stddev = float(fluctuations_stddev)
        flexibility_mean = float(flexibility_mean)
        flexibility_stddev = float(flexibility_stddev)
        asperity_mean = float(asperity_mean)
        asperity_stddev = float(asperity_stddev)
        loglogavgslope_mean = float(loglogavgslope_mean)
        loglogavgslope_stddev = float(loglogavgslope_stddev)
        prefix = str(prefix)
        assert fluctuations_stddev > 0
        assert fluctuations_mean > 0
        assert flexibility_stddev > 0
        assert flexibility_mean > 0
        assert asperity_stddev > 0
        assert asperity_mean > 0
        assert loglogavgslope_stddev > 0

        fluct = _lognormal_moment_matching(fluctuations_mean,
                                           fluctuations_stddev,
                                           prefix + 'fluctuations')
        flex = _lognormal_moment_matching(flexibility_mean, flexibility_stddev,
                                          prefix + 'flexibility')
        asp = _lognormal_moment_matching(asperity_mean, asperity_stddev,
                                         prefix + 'asperity')
268
        avgsl = _normal(loglogavgslope_mean, loglogavgslope_stddev,
Philipp Arras's avatar
Philipp Arras committed
269
                        prefix + 'loglogavgslope')
270 271 272 273 274
        amp = _Amplitude(target, fluct, flex, asp, avgsl, prefix + 'spectrum')
        if index is not None:
            self._a.insert(index, amp)
        else:
            self._a.append(amp)
275 276 277

    def finalize_from_op(self, zeromode, prefix=''):
        assert isinstance(zeromode, Operator)
278
        self._azm = zeromode
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
        hspace = makeDomain([dd.target[0].harmonic_partner for dd in self._a])
        foo = np.ones(hspace.shape)
        zeroind = len(hspace.shape)*(0,)
        foo[zeroind] = 0
        azm = Adder(from_global_data(hspace, foo)) @ ValueInserter(
            hspace, zeroind) @ zeromode

        n_amplitudes = len(self._a)
        ht = HarmonicTransformOperator(hspace, space=0)
        for i in range(1, n_amplitudes):
            ht = HarmonicTransformOperator(ht.target, space=i) @ ht

        pd = PowerDistributor(hspace, self._a[0].target[0], 0)
        for i in range(1, n_amplitudes):
            foo = PowerDistributor(pd.domain, self._a[i].target[0], space=i)
            pd = pd @ foo

        spaces = tuple(range(n_amplitudes))
        a = ContractionOperator(pd.domain, spaces[1:]).adjoint @ self._a[0]
        for i in range(1, n_amplitudes):
            co = ContractionOperator(pd.domain, spaces[:i] + spaces[(i + 1):])
            a = a*(co.adjoint @ self._a[i])
Philipp Arras's avatar
Philipp Arras committed
301

302
        return ht(azm*(pd @ a)*ducktape(hspace, None, prefix + 'xi'))
Philipp Arras's avatar
Philipp Arras committed
303 304 305 306

    def finalize(self,
                 offset_amplitude_mean,
                 offset_amplitude_stddev,
307
                 prefix='',
Philipp Arras's avatar
Philipp Arras committed
308 309 310 311 312 313 314 315 316
                 offset=None):
        """
        offset vs zeromode: volume factor
        """
        offset_amplitude_stddev = float(offset_amplitude_stddev)
        offset_amplitude_mean = float(offset_amplitude_mean)
        assert offset_amplitude_stddev > 0
        assert offset_amplitude_mean > 0
        if offset is not None:
317
            raise NotImplementedError
Philipp Arras's avatar
Philipp Arras committed
318 319 320 321
            offset = float(offset)
        azm = _lognormal_moment_matching(offset_amplitude_mean,
                                         offset_amplitude_stddev,
                                         prefix + 'zeromode')
322
        return self.finalize_from_op(azm, prefix)
Philipp Arras's avatar
Philipp Arras committed
323 324 325

    @property
    def amplitudes(self):
326
        return self._a
327

328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414
    @property
    def amplitude_total_offset(self):
        return self._azm

    @property
    def total_fluctuation(self):
        if len(self._a) == 0:
            raise(NotImplementedError)
        if len(self._a) == 1:
            return self._a[0].fluctuation_amplitude
        q = 1.
        for a in self._a:
            fl = a.fluctuation_amplitude
            q = q * (Adder(full(fl.target,1.)) @ fl**2)
        return (Adder(full(q.target,-1.)) @ q).sqrt()

    def slice_fluctuation(self,space):
        if len(self._a) == 0:
            raise(NotImplementedError)
        assert space < len(self._a)
        if len(self._a) == 1:
            return self._a[0].fluctuation_amplitude
        q = 1.
        for j in range(len(self._a)):
            fl = self._a[j].fluctuation_amplitude
            if j == space:
                q = q * fl**2
            else:
                q = q * (Adder(full(fl.target,1.)) @ fl**2)
        return q.sqrt()
    
    def average_fluctuation(self,space):
        if len(self._a) == 0:
            raise(NotImplementedError)
        assert space < len(self._a)
        if len(self._a) == 1:
            return self._a[0].fluctuation_amplitude
        return self._a[space].fluctuation_amplitude

    def offset_amplitude_realized(self,samples):
        res = 0.
        for s in samples:
            res += s.mean()**2
        return np.sqrt(res/len(samples))
    
    def total_fluctuation_realized(self,samples):
        res = 0.
        for s in samples:
            res = res + (s-s.mean())**2
        res = res/len(samples)
        return np.sqrt(res.mean())
    
    def average_fluctuation_realized(self,samples,space):
        ldom = len(samples[0].domain)
        assert space < ldom
        if ldom == 1:
            return self.total_fluctuation_realized(samples)
        spaces=()
        for i in range(ldom):
            if i != space:
                spaces += (i,)
        res = 0.
        for s in samples:
            r = s.mean(spaces)
            res = res + (r-r.mean())**2
        res = res/len(samples)
        return np.sqrt(res.mean())
    
    def slice_fluctuation_realized(self,samples,space):
        ldom = len(samples[0].domain)
        assert space < ldom
        if ldom == 1:
            return self.total_fluctuation_realized(samples)
        res1 = 0.
        res2 = 0.
        for s in samples:
            res1 = res1 + s**2
            res2 = res2 + s.mean(space)**2
        res1 = res1/len(samples)
        res2 = res2/len(samples)
        res = res1.mean() -  res2.mean()
        return np.sqrt(res)

    def stats(self,op,samples):
        sc = StatCalculator()
        for s in samples:
            sc.add(op(s.extract(op.domain)))
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431
        return sc.mean.to_global_data(), sc.var.sqrt().to_global_data()
    
    def moment_slice_to_average(self,
                                fluctuations_slice_mean,
                                nsamples = 1000):
        fluctuations_slice_mean = float(fluctuations_slice_mean)
        assert fluctuations_slice_mean > 0
        scm = 1.
        for a in self._a:
            m, std = a.fluctuation_amplitude.mean, a.fluctuation_amplitude.std
            mu, sig = _lognormal_moments(m,std)
            flm = np.exp(mu + sig * np.random.normal(size=nsamples))
            scm *= flm**2 + 1.
        scm = np.mean(np.sqrt(scm))
        return fluctuations_slice_mean / scm