diff --git a/demos/getting_started_3.py b/demos/getting_started_3.py index e088f427ea8fdd6e2d34d815055442311a56f944..9ca2994970f3c825f232f3457c9ce2216a9f0a11 100644 --- a/demos/getting_started_3.py +++ b/demos/getting_started_3.py @@ -65,7 +65,7 @@ if __name__ == '__main__': 'im': .4, # y-intercept mean 'iv': .3 # relatively high y-intercept variance } - A = ift.AmplitudeOperator(**dct) + A = ift.SLAmplitude(**dct) # Build the operator for a correlated signal power_distributor = ift.PowerDistributor(harmonic_space, power_space) diff --git a/docs/source/code.rst b/docs/source/code.rst index cf462ced86ad79a8bc38c8c2ed11849855c39cb6..06bc84a559f6e0f5a8297a2f2ab5e924a7718435 100644 --- a/docs/source/code.rst +++ b/docs/source/code.rst @@ -212,7 +212,7 @@ specific inference problems. Currently these are: .. currentmodule:: nifty5.library -- :class:`~amplitude_operator.AmplitudeOperator`, which returns a smooth power spectrum. +- :class:`~smooth_linear_amplitude.SLAmplitude`, which returns a smooth power spectrum. - :class:`~inverse_gamma_operator.InverseGammaOperator`, which models point sources which are distributed according to a inverse-gamma distribution. - :class:`~correlated_fields.CorrelatedField`, which models a diffuse log-normal field. It takes an diff --git a/nifty5/__init__.py b/nifty5/__init__.py index 30f7d5b41c647e481537150f198a3f8bd67f4978..026e69f31f656a4a1b3289eccbefd1c7378c1ed1 100644 --- a/nifty5/__init__.py +++ b/nifty5/__init__.py @@ -73,7 +73,7 @@ from .minimization.kl_energy import KL_Energy from .sugar import * from .plot import Plot -from .library.amplitude_operator import AmplitudeOperator +from .library.smooth_linear_amplitude import (SLAmplitude, CepstrumOperator) from .library.inverse_gamma_operator import InverseGammaOperator from .library.los_response import LOSResponse from .library.dynamic_operator import (dynamic_operator, diff --git a/nifty5/data_objects/numpy_do.py b/nifty5/data_objects/numpy_do.py index 8160f3b34163283a146c977cdd14d4aa36f0d370..e582e36023bb914a0195d5e6deb9379a62a68548 100644 --- a/nifty5/data_objects/numpy_do.py +++ b/nifty5/data_objects/numpy_do.py @@ -15,7 +15,7 @@ # # NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik. -# Data object module for NIFTy that uses simple numpy ndarrays. +# Data object module that uses simple numpy ndarrays. import numpy as np from numpy import absolute, clip, cos, cosh, empty, empty_like, exp, full, log diff --git a/nifty5/domains/gl_space.py b/nifty5/domains/gl_space.py index 7f6a48d97fae0686e91614a1e562aca53dcd08d3..d6091d5388129f14515d1f5c2e0ce4659d688e25 100644 --- a/nifty5/domains/gl_space.py +++ b/nifty5/domains/gl_space.py @@ -21,7 +21,7 @@ from .structured_domain import StructuredDomain class GLSpace(StructuredDomain): - """NIFTy subclass for Gauss-Legendre pixelizations of the two-sphere. + """Represents a 2-sphere with Gauss-Legendre pixelization. Its harmonic partner domain is the :class:`~nifty5.domains.lm_space.LMSpace`. diff --git a/nifty5/domains/hp_space.py b/nifty5/domains/hp_space.py index 998fa7ac5548a170cd1f5a252817ab252c2a265e..6058647330d67e56de7aa0a212c3387a4edf0eda 100644 --- a/nifty5/domains/hp_space.py +++ b/nifty5/domains/hp_space.py @@ -21,7 +21,7 @@ from .structured_domain import StructuredDomain class HPSpace(StructuredDomain): - """NIFTy subclass for HEALPix discretizations of the two-sphere. + """Represents 2-sphere with HEALPix discretization. Its harmonic partner domain is the :class:`~nifty5.domains.lm_space.LMSpace`. diff --git a/nifty5/domains/lm_space.py b/nifty5/domains/lm_space.py index 80223a63053d1def496082a8387c0431c90ab9a5..4c224a6dec96764f18bc27019a1f88b2d33630b8 100644 --- a/nifty5/domains/lm_space.py +++ b/nifty5/domains/lm_space.py @@ -22,7 +22,7 @@ from .structured_domain import StructuredDomain class LMSpace(StructuredDomain): - """NIFTy subclass for sets of spherical harmonic coefficients. + """Represents a set of spherical harmonic coefficients. Its harmonic partner spaces are :class:`~nifty5.domains.hp_space.HPSpace` and :class:`~nifty5.domains.gl_space.GLSpace`. diff --git a/nifty5/domains/log_rg_space.py b/nifty5/domains/log_rg_space.py index 096e18ed213043db894638c612e36ccb940e1483..65cfb46a50aa3a9e16b26a32b6d6d5990ae798d1 100644 --- a/nifty5/domains/log_rg_space.py +++ b/nifty5/domains/log_rg_space.py @@ -16,30 +16,29 @@ # NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik. from functools import reduce + import numpy as np -from .. import dobj from ..field import Field -from ..sugar import exp from .structured_domain import StructuredDomain class LogRGSpace(StructuredDomain): - """NIFTy subclass for logarithmic Cartesian grids. + '''Represents a logarithmic Cartesian grid. Parameters ---------- shape : int or tuple of int Number of grid points or numbers of gridpoints along each axis. bindistances : float or tuple of float - Distance between two grid points along each axis. These are - measured on logarithmic scale and are constant therfore. + Logarithmic distance between two grid points along each axis. + Equidistant spacing of bins on logarithmic scale is assumed. t_0 : float or tuple of float - FIXME + Coordinate of pixel ndim*(1,). harmonic : bool, optional Whether the space represents a grid in position or harmonic space. Default: False. - """ + ''' _needed_for_hash = ['_shape', '_bindistances', '_t_0', '_harmonic'] def __init__(self, shape, bindistances, t_0, harmonic=False): @@ -77,6 +76,7 @@ class LogRGSpace(StructuredDomain): @property def t_0(self): + """np.ndarray : array of coordinates of pixel ndim*(1,).""" return np.array(self._t_0) def __repr__(self): @@ -84,16 +84,49 @@ class LogRGSpace(StructuredDomain): self.shape, self.harmonic)) def get_default_codomain(self): + """Returns a :class:`LogRGSpace` object representing the (position or + harmonic) partner domain of `self`, depending on `self.harmonic`. The + `bindistances` are transformed and `t_0` stays the same. + + Returns + ------- + LogRGSpace + The parter domain + """ codomain_bindistances = 1./(self.bindistances*self.shape) return LogRGSpace(self.shape, codomain_bindistances, self._t_0, True) def get_k_length_array(self): + """Generates array of distances to origin of the space. + + Returns + ------- + numpy.ndarray + Distances to origin of the space. If any index of the array is + zero then the distance is np.nan if self.harmonic True. + The dtype is float64, the shape is `self.shape`. + + Raises + ------ + NotImplementedError + If `self.harmonic` is False. + """ if not self.harmonic: raise NotImplementedError ks = self.get_k_array() return Field.from_global_data(self, np.linalg.norm(ks, axis=0)) def get_k_array(self): + """Generates coordinates of the space. + + Returns + ------- + numpy.ndarray + Coordinates of the space. If one index of the array is zero the + corresponding coordinate is -np.inf (np.nan) if self.harmonic is + False (True). + The dtype is float64 and shape: `(len(self.shape),) + self.shape`. + """ ndim = len(self.shape) k_array = np.zeros((ndim,) + self.shape) dist = self.bindistances diff --git a/nifty5/domains/power_space.py b/nifty5/domains/power_space.py index 22828e73a2a184b0e014be6ebf42be211057d001..f6c357fd34dfcd7dfd6f28d68d89f25f81fd2645 100644 --- a/nifty5/domains/power_space.py +++ b/nifty5/domains/power_space.py @@ -22,7 +22,7 @@ from .structured_domain import StructuredDomain class PowerSpace(StructuredDomain): - """NIFTy class for spaces of power spectra. + """Represents non-equidistantly binned spaces for power spectra. A power space is the result of a projection of a harmonic domain where k-modes of equal length get mapped to one power index. diff --git a/nifty5/domains/rg_space.py b/nifty5/domains/rg_space.py index af0d44e7e6ecb5ffeb0b964b6a262f1631555515..2e1d2c0296c7a80bbdfe2d53bd9c849069323bee 100644 --- a/nifty5/domains/rg_space.py +++ b/nifty5/domains/rg_space.py @@ -24,7 +24,7 @@ from .structured_domain import StructuredDomain class RGSpace(StructuredDomain): - """NIFTy subclass for regular Cartesian grids. + """Represents a regular Cartesian grid. Parameters ---------- diff --git a/nifty5/domains/structured_domain.py b/nifty5/domains/structured_domain.py index c2b858ee939f4aae49f9b582caab6ee475d2caf2..be5deb75b23ca6f7bbac1c3160da250557aacd51 100644 --- a/nifty5/domains/structured_domain.py +++ b/nifty5/domains/structured_domain.py @@ -21,7 +21,7 @@ from .domain import Domain class StructuredDomain(Domain): - """The abstract base class for all structured NIFTy domains. + """The abstract base class for all structured domains. An instance of a space contains information about the manifold's geometry and enhances the functionality of Domain by methods that @@ -50,7 +50,7 @@ class StructuredDomain(Domain): @property def total_volume(self): - """float : Total domain volume + """float : Total domain volume. Returns the sum over all the domain's pixel volumes. """ @@ -63,7 +63,7 @@ class StructuredDomain(Domain): raise NotImplementedError def get_k_length_array(self): - """k vector lengths, if applicable, + """k vector lengths, if applicable. Returns the length of the k vector for every pixel. This method is only implemented for harmonic domains. diff --git a/nifty5/field.py b/nifty5/field.py index 8876fb03d0e0122adbaadf506e11976f2c2890df..722c2a9c0fd34787377009d0fff771f0d58b0fa3 100644 --- a/nifty5/field.py +++ b/nifty5/field.py @@ -23,18 +23,15 @@ from .domain_tuple import DomainTuple class Field(object): - _scalar_dom = DomainTuple.scalar_domain() - """The discrete representation of a continuous field over multiple spaces. - In NIFTy, Fields are used to store data arrays and carry all the needed - metainformation (i.e. the domain) for operators to be able to work on them. + Stores data arrays and carries all the needed metainformation (i.e. the + domain) for operators to be able to operate on them. Parameters ---------- domain : DomainTuple the domain of the new Field - val : data_object This object's global shape must match the domain shape After construction, the object will no longer be writeable! @@ -42,9 +39,11 @@ class Field(object): Notes ----- If possible, do not invoke the constructor directly, but use one of the - many convenience functions for Field construction! + many convenience functions for instantiation! """ + _scalar_dom = DomainTuple.scalar_domain() + def __init__(self, domain, val): if not isinstance(domain, DomainTuple): raise TypeError("domain must be of type DomainTuple") @@ -337,7 +336,7 @@ class Field(object): """ if not isinstance(x, Field): raise TypeError("The multiplier must be an instance of " + - "the NIFTy field class") + "the Field class") from .operators.outer_product_operator import OuterProduct return OuterProduct(self, x.domain)(x) @@ -360,7 +359,7 @@ class Field(object): """ if not isinstance(x, Field): raise TypeError("The dot-partner must be an instance of " + - "the NIFTy field class") + "the Field class") if x._domain != self._domain: raise ValueError("Domain mismatch") diff --git a/nifty5/library/amplitude_operator.py b/nifty5/library/amplitude_operator.py deleted file mode 100644 index c9e6172447709b4d1e5cb6d4816e719ea1c55d76..0000000000000000000000000000000000000000 --- a/nifty5/library/amplitude_operator.py +++ /dev/null @@ -1,161 +0,0 @@ -# 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-2019 Max-Planck-Society -# -# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik. - -import numpy as np - -from ..domains.power_space import PowerSpace -from ..field import Field -from ..sugar import makeOp - - -def _ceps_kernel(dof_space, k, a, k0): - return a**2/(1 + (k/k0)**2)**2 - - -def _create_cepstrum_amplitude_field(domain, cepstrum): - dim = len(domain.shape) - shape = domain.shape - q_array = domain.get_k_array() - - # Fill all non-zero modes - no_zero_modes = (slice(1, None), )*dim - ks = q_array[(slice(None), ) + no_zero_modes] - cepstrum_field = np.zeros(shape) - cepstrum_field[no_zero_modes] = cepstrum(ks) - - # Fill zero-mode subspaces - for i in range(dim): - fst_dims = (slice(None), )*i - sl = fst_dims + (slice(1, None), ) - sl2 = fst_dims + (0, ) - cepstrum_field[sl2] = np.sum(cepstrum_field[sl], axis=i) - return Field.from_global_data(domain, cepstrum_field) - - -def CepstrumOperator(domain, a, k0): - ''' - .. math:: - C(k) = \\left(\\frac{a}{1+(k/k0)^2}\\right)^2 - ''' - from ..operators.qht_operator import QHTOperator - from ..operators.symmetrizing_operator import SymmetrizingOperator - - # FIXME a>0 k0>0 - qht = QHTOperator(target=domain) - dof_space = qht.domain[0] - sym = SymmetrizingOperator(domain) - kern = lambda k: _ceps_kernel(dof_space, k, a, k0) - cepstrum = _create_cepstrum_amplitude_field(dof_space, kern) - return sym @ qht @ makeOp(cepstrum.sqrt()) - - -def SlopeOperator(domain, sm, sv, im, iv): - ''' - Parameters - ---------- - - sm, sv : slope_mean = expected exponent of power law (e.g. -4), - slope_variance (default=1) - - im, iv : y-intercept_mean, y-intercept_std of power_slope - - ''' - from ..operators.slope_operator import SlopeOperator - from ..operators.offset_operator import OffsetOperator - - # sv, iv>0 - - phi_mean = np.array([sm, im + sm*domain.t_0[0]]) - phi_sig = np.array([sv, iv]) - slope = SlopeOperator(domain) - phi_mean = Field.from_global_data(slope.domain, phi_mean) - phi_sig = Field.from_global_data(slope.domain, phi_sig) - return slope(OffsetOperator(phi_mean)(makeOp(phi_sig))) - - -def AmplitudeOperator( - target, n_pix, a, k0, sm, sv, im, iv, keys=['tau', 'phi']): - '''Operator for parametrizing smooth power spectra. - - The general guideline for setting up generative models in IFT is to - transform the problem into the eigenbase of the prior and formulate the - generative model in this base. This is done here for the case of a power - spectrum which is smooth and has a linear component (both on - double-logarithmic scale). - - This function assembles an :class:`Operator` which maps two a-priori white - Gaussian random fields to a smooth power spectrum which is composed out of - a linear and a smooth component. - - On double-logarithmic scale, i.e. both x and y-axis on logarithmic scale, - the output of the generated operator is: - - AmplitudeOperator = 0.5*(smooth_component + linear_component) - - This is then exponentiated and exponentially binned (via a :class:`ExpTransform`). - - The prior on the linear component is parametrized by four real numbers, - being expected value and prior variance on the slope and the y-intercept - of the linear function. - - The prior on the smooth component is parametrized by two real numbers: the - strength and the cutoff of the smoothness prior (see :class:`CepstrumOperator`). - - Parameters - ---------- - n_pix : int - Number of pixels of the space in which the . - target : PowerSpace - Target of the Operator. - a : float - Strength of smoothness prior (see :class:`CepstrumOperator`). - k0 : float - Cutoff of smothness prior in quefrency space (see :class:`CepstrumOperator`). - sm : float - Expected exponent of power law (see :class:`SlopeOperator`). - sv : float - Prior variance of exponent of power law (see :class:`SlopeOperator`). - im : float - Expected y-intercept of power law (see :class:`SlopeOperator`). - iv : float - Prior variance of y-intercept of power law (see :class:`SlopeOperator`). - - Returns - ------- - Operator - Operator which is defined on the space of white excitations fields and - which returns on its target a power spectrum which consists out of a - smooth and a linear part. - ''' - from ..operators.exp_transform import ExpTransform - - if not (isinstance(n_pix, int) and isinstance(target, PowerSpace)): - raise TypeError - - a, k0 = float(a), float(k0) - sm, sv, im, iv = float(sm), float(sv), float(im), float(iv) - - et = ExpTransform(target, n_pix) - dom = et.domain[0] - - dct = {'a': a, 'k0': k0} - smooth = CepstrumOperator(dom, **dct).ducktape(keys[0]) - - dct = {'sm': sm, 'sv': sv, 'im': im, 'iv': iv} - linear = SlopeOperator(dom, **dct).ducktape(keys[1]) - - return et((0.5*(smooth + linear)).exp()) diff --git a/nifty5/library/correlated_fields.py b/nifty5/library/correlated_fields.py index a0e85ed0439a186f3ea82221d482ab030eb7928f..71567cda325c982fd514d7001763770a5691738a 100644 --- a/nifty5/library/correlated_fields.py +++ b/nifty5/library/correlated_fields.py @@ -15,64 +15,93 @@ # # NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik. +from functools import reduce + from ..domain_tuple import DomainTuple from ..operators.contraction_operator import ContractionOperator from ..operators.distributors import PowerDistributor from ..operators.harmonic_operators import HarmonicTransformOperator from ..operators.simple_linear_operators import ducktape -from ..operators.scaling_operator import ScalingOperator -def CorrelatedField(s_space, amplitude_operator, name='xi'): - ''' - Function for construction of correlated fields +def CorrelatedField(target, amplitude_operator, name='xi'): + '''Constructs an operator which turns a white Gaussian excitation field + into a correlated field. + + This function returns an operator which implements: + + ht @ (vol * A * xi), + + where `ht` is a harmonic transform operator, `A` is the sqare root of the + prior covariance an `xi` is the excitation field. Parameters ---------- - s_space : Domain - Field domain + target : Domain, DomainTuple or tuple of Domain + Target of the operator. Must contain exactly one space. amplitude_operator: Operator - operator for correlation structure name : string - MultiField component name + :class:`MultiField` key for xi-field. + + Returns + ------- + Correlated field : Operator ''' - h_space = s_space.get_default_codomain() - ht = HarmonicTransformOperator(h_space, s_space) + tgt = DomainTuple.make(target) + if len(tgt) > 1: + raise ValueError + h_space = tgt[0].get_default_codomain() + ht = HarmonicTransformOperator(h_space, tgt[0]) p_space = amplitude_operator.target[0] power_distributor = PowerDistributor(h_space, p_space) A = power_distributor(amplitude_operator) - vol = h_space.scalar_dvol - vol = ScalingOperator(vol**(-0.5), h_space) - return ht(vol(A)*ducktape(h_space, None, name)) + vol = h_space.scalar_dvol**-0.5 + return ht(vol*A*ducktape(h_space, None, name)) -def MfCorrelatedField(s_space_spatial, - s_space_energy, - amplitude_operator_spatial, - amplitude_operator_energy, - name="xi"): - ''' - Method for construction of correlated multi-frequency fields +def MfCorrelatedField(target, amplitudes, name='xi'): + '''Constructs an operator which turns white Gaussian excitation fields + into a correlated field defined on a DomainTuple with two entries and two + separate correlation structures. + + This operator may be used as a model for multi-frequency reconstructions + with a correlation structure in both spatial and energy direction. + + Parameters + ---------- + target : Domain, DomainTuple or tuple of Domain + Target of the operator. Must contain exactly one space. + amplitudes: iterable of Operator + List of two amplitude operators. + name : string + :class:`MultiField` key for xi-field. + + Returns + ------- + Correlated field : Operator ''' - h_space_spatial = s_space_spatial.get_default_codomain() - h_space_energy = s_space_energy.get_default_codomain() - h_space = DomainTuple.make((h_space_spatial, h_space_energy)) - ht1 = HarmonicTransformOperator(h_space, target=s_space_spatial, space=0) - ht2 = HarmonicTransformOperator(ht1.target, space=1) - ht = ht2(ht1) + tgt = DomainTuple.make(target) + if len(tgt) != 2: + raise ValueError + if len(amplitudes) != 2: + raise ValueError - p_space_spatial = amplitude_operator_spatial.target[0] - p_space_energy = amplitude_operator_energy.target[0] + hsp = DomainTuple.make([tt.get_default_codomain() for tt in tgt]) + ht1 = HarmonicTransformOperator(hsp, target=tgt[0], space=0) + ht2 = HarmonicTransformOperator(ht1.target, space=1) + ht = ht2 @ ht1 - pd_spatial = PowerDistributor(h_space, p_space_spatial, 0) - pd_energy = PowerDistributor(pd_spatial.domain, p_space_energy, 1) - pd = pd_spatial(pd_energy) + psp = [aa.target[0] for aa in amplitudes] + pd0 = PowerDistributor(hsp, psp[0], 0) + pd1 = PowerDistributor(pd0.domain, psp[1], 1) + pd = pd0 @ pd1 - dom_distr_spatial = ContractionOperator(pd.domain, 1).adjoint - dom_distr_energy = ContractionOperator(pd.domain, 0).adjoint + dd0 = ContractionOperator(pd.domain, 1).adjoint + dd1 = ContractionOperator(pd.domain, 0).adjoint + d = [dd0, dd1] - a_spatial = dom_distr_spatial(amplitude_operator_spatial) - a_energy = dom_distr_energy(amplitude_operator_energy) - a = a_spatial*a_energy - A = pd(a) - return ht(A*ducktape(h_space, None, name)) + a = [dd @ amplitudes[ii] for ii, dd in enumerate(d)] + a = reduce(lambda x, y: x*y, a) + A = pd @ a + vol = reduce(lambda x, y: x*y, [sp.scalar_dvol**-0.5 for sp in hsp]) + return ht(vol*A*ducktape(hsp, None, name)) diff --git a/nifty5/library/dynamic_operator.py b/nifty5/library/dynamic_operator.py index ea385106e4eaf4d88b248f6099e4e508f67f669c..aad8fb2afcf5c8c37ac46cee0e349a6b0f94df74 100644 --- a/nifty5/library/dynamic_operator.py +++ b/nifty5/library/dynamic_operator.py @@ -147,7 +147,7 @@ def dynamic_operator(domain, Parameters ---------- domain : RGSpace - The space under consideration. + The position space in which the Green's function shall be constructed. harmonic_padding : None, int, list of int Amount of central padding in harmonic space in pixels. If None the field is not padded at all. @@ -158,9 +158,9 @@ def dynamic_operator(domain, key : String key for dynamics encoding parameter. causal : boolean - Whether or not the reconstructed dynamics should be causal in time. + Whether or not the Green's function shall be causal in time. minimum_phase: boolean - Whether or not the reconstructed dynamics should be minimum phase. + Whether or not the Green's function shall be a minimum phase filter. Returns ------- @@ -197,14 +197,14 @@ def dynamic_lightcone_operator(domain, quant, causal=True, minimum_phase=False): - '''Constructs an operator encoding the Green's function of a linear - homogeneous dynamic system. The Green's function is constrained to be - within a light cone. + '''Extends the functionality of :function: dynamic_operator to a Green's + function which is constrained to be within a light cone. Parameters ---------- domain : RGSpace - The space under consideration. Must have dim > 1. + The position space in which the Green's function shall be constructed. + It needs to have at least two dimensions. harmonic_padding : None, int, list of int Amount of central padding in harmonic space in pixels. If None the field is not padded at all. @@ -221,18 +221,17 @@ def dynamic_lightcone_operator(domain, quant : float Quantization of the light cone in pixels. causal : boolean - Whether or not the reconstructed dynamics should be causal in time. + Whether or not the Green's function shall be causal in time. minimum_phase: boolean - Whether or not the reconstructed dynamics should be minimum phase. + Whether or not the Green's function shall be a minimum phase filter. Returns ------- Operator - The Operator encoding the dynamic Green's function in harmonic space - when evaluated. - dict - A collection of sub-chains of Operator which can be used - for plotting and evaluation. + The Operator encoding the dynamic Green's function in harmonic space. + Dictionary of Operator + A collection of sub-chains of Operator which can be used for plotting + and evaluation. Notes ----- diff --git a/nifty5/library/smooth_linear_amplitude.py b/nifty5/library/smooth_linear_amplitude.py new file mode 100644 index 0000000000000000000000000000000000000000..331b270c87db7e99a80dc9422eedb74398c412ad --- /dev/null +++ b/nifty5/library/smooth_linear_amplitude.py @@ -0,0 +1,198 @@ +# 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-2019 Max-Planck-Society +# +# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik. + +import numpy as np + +from ..domain_tuple import DomainTuple +from ..domains.power_space import PowerSpace +from ..field import Field +from ..operators.exp_transform import ExpTransform +from ..operators.offset_operator import OffsetOperator +from ..operators.qht_operator import QHTOperator +from ..operators.slope_operator import SlopeOperator +from ..operators.symmetrizing_operator import SymmetrizingOperator +from ..sugar import makeOp + + +def _ceps_kernel(k, a, k0): + return (a/(1+np.sum((k.T/k0)**2, axis=-1).T))**2 + + +def CepstrumOperator(target, a, k0): + '''Turns a white Gaussian random field into a smooth field on a LogRGSpace. + + Composed out of three operators: + + sym @ qht @ diag(sqrt_ceps), + + where sym is a :class:`SymmetrizingOperator`, qht is a :class:`QHTOperator` + and ceps is the so-called cepstrum: + + .. math:: + \\mathrm{sqrt\_ceps}(k) = \\frac{a}{1+(k/k0)^2} + + These operators are combined in this fashion in order to generate: + + - A field which is smooth, i.e. second derivatives are punished (note + that the sqrt-cepstrum is essentially proportional to 1/k**2). + + - A field which is symmetric around the pixel in the middle of the space. + This is result of the :class:`SymmetrizingOperator` and needed in order + to decouple the degrees of freedom at the beginning and the end of the + amplitude whenever :class:`CepstrumOperator` is used as in + :class:`SLAmplitude`. + + The prior on the zero mode (or zero subspaces for more than one dimensions) + is the integral of the prior over all other modes along the corresponding + axis. + + Parameters + ---------- + target : LogRGSpace + Target domain of the operator, needs to be non-harmonic. + a : float + Cutoff of smoothness prior (positive only). Controls the + regularization of the inverse laplace operator to be finite at zero. + Larger values for the cutoff results in a weaker constraining prior. + k0 : float, list of float + Strength of smothness prior in quefrency space (positive only) along + each axis. If float then the strength is the same along each axis. + Larger values result in a weaker constraining prior. + ''' + a = float(a) + target = DomainTuple.make(target) + if a <= 0: + raise ValueError + if len(target) > 1 or target[0].harmonic: + raise TypeError + if isinstance(k0, (float, int)): + k0 = np.array([k0]*len(target.shape)) + else: + k0 = np.array(k0) + if len(k0) != len(target.shape): + raise ValueError + if np.any(np.array(k0) <= 0): + raise ValueError + + qht = QHTOperator(target) + dom = qht.domain[0] + sym = SymmetrizingOperator(target) + + # Compute cepstrum field + dim = len(dom.shape) + shape = dom.shape + q_array = dom.get_k_array() + # Fill all non-zero modes + no_zero_modes = (slice(1, None),)*dim + ks = q_array[(slice(None),) + no_zero_modes] + cepstrum_field = np.zeros(shape) + cepstrum_field[no_zero_modes] = _ceps_kernel(ks, a, k0) + # Fill zero-mode subspaces + for i in range(dim): + fst_dims = (slice(None),)*i + sl = fst_dims + (slice(1, None),) + sl2 = fst_dims + (0,) + cepstrum_field[sl2] = np.sum(cepstrum_field[sl], axis=i) + cepstrum = Field.from_global_data(dom, cepstrum_field) + + return sym @ qht @ makeOp(cepstrum.sqrt()) + + +def SLAmplitude(target, n_pix, a, k0, sm, sv, im, iv, keys=['tau', 'phi']): + '''Operator for parametrizing smooth amplitudes (square roots of power + spectra). + + The general guideline for setting up generative models in IFT is to + transform the problem into the eigenbase of the prior and formulate the + generative model in this base. This is done here for the case of an + amplitude which is smooth and has a linear component (both on + double-logarithmic scale). + + This function assembles an :class:`Operator` which maps two a-priori white + Gaussian random fields to a smooth amplitude which is composed out of + a linear and a smooth component. + + On double-logarithmic scale, i.e. both x and y-axis on logarithmic scale, + the output of the generated operator is: + + AmplitudeOperator = 0.5*(smooth_component + linear_component) + + This is then exponentiated and exponentially binned (in this order). + + The prior on the linear component is parametrized by four real numbers, + being expected value and prior variance on the slope and the y-intercept + of the linear function. + + The prior on the smooth component is parametrized by two real numbers: the + strength and the cutoff of the smoothness prior (see :class:`CepstrumOperator`). + + Parameters + ---------- + n_pix : int + Number of pixels of the space in which the . + target : PowerSpace + Target of the Operator. + a : float + Strength of smoothness prior (see :class:`CepstrumOperator`). + k0 : float + Cutoff of smothness prior in quefrency space (see + :class:`CepstrumOperator`). + sm : float + Expected exponent of power law. + sv : float + Prior standard deviation of exponent of power law. + im : float + Expected y-intercept of power law. This is the value at t_0 of the + LogRGSpace (see :class:`ExpTransform`). + iv : float + Prior standard deviation of y-intercept of power law. + + Returns + ------- + Operator + Operator which is defined on the space of white excitations fields and + which returns on its target a power spectrum which consists out of a + smooth and a linear part. + ''' + if not (isinstance(n_pix, int) and isinstance(target, PowerSpace)): + raise TypeError + + a, k0 = float(a), float(k0) + sm, sv, im, iv = float(sm), float(sv), float(im), float(iv) + if sv <= 0 or iv <= 0: + raise ValueError + + et = ExpTransform(target, n_pix) + dom = et.domain[0] + + # Smooth component + dct = {'a': a, 'k0': k0} + smooth = CepstrumOperator(dom, **dct).ducktape(keys[0]) + + # Linear component + sl = SlopeOperator(dom) + mean = np.array([sm, im + sm*dom.t_0[0]]) + sig = np.array([sv, iv]) + mean = Field.from_global_data(sl.domain, mean) + sig = Field.from_global_data(sl.domain, sig) + linear = (sl @ OffsetOperator(mean) @ makeOp(sig)).ducktape(keys[1]) + + # Combine linear and smooth component + loglog_ampl = 0.5*(smooth + linear) + + # Go from loglog-space to linear-linear-space + return et @ loglog_ampl.exp() diff --git a/nifty5/operators/diagonal_operator.py b/nifty5/operators/diagonal_operator.py index ccb2d4196c3d1a0b725d9b069b53f50ea056ff9f..3b2812182e3ebdf4dbf22cabae289527e3579c7b 100644 --- a/nifty5/operators/diagonal_operator.py +++ b/nifty5/operators/diagonal_operator.py @@ -24,7 +24,7 @@ from .endomorphic_operator import EndomorphicOperator class DiagonalOperator(EndomorphicOperator): - """NIFTy class for diagonal operators. + """Represents a linear operator which is diagonal. The NIFTy DiagonalOperator class is a subclass derived from the EndomorphicOperator. It multiplies an input field pixel-wise with its diff --git a/nifty5/operators/endomorphic_operator.py b/nifty5/operators/endomorphic_operator.py index e97e98e7e7181293a09f75f90a6a16a65c8f67d1..9b1d797f4d2dc0bdaaac15486506f74b2f3b03e0 100644 --- a/nifty5/operators/endomorphic_operator.py +++ b/nifty5/operators/endomorphic_operator.py @@ -21,12 +21,9 @@ from .linear_operator import LinearOperator class EndomorphicOperator(LinearOperator): - """NIFTy class for endomorphic operators. - - The NIFTy EndomorphicOperator describes linear operators with identical - domain and target. + """Represents a linear operator which is endomorphic, i.e. one which + has identical domain and target. """ - @property def target(self): """DomainTuple : returns :attr:`domain` diff --git a/nifty5/operators/exp_transform.py b/nifty5/operators/exp_transform.py index 16c26ef67f4fb72fd007eb70fc7c0ad8d56d439d..78673d8160f3d422dd95163e25f7493de8eff2c0 100644 --- a/nifty5/operators/exp_transform.py +++ b/nifty5/operators/exp_transform.py @@ -34,6 +34,8 @@ class ExpTransform(LinearOperator): Then transforms between this log-space and its target, which is defined in normal units. + FIXME Write something on t_0 of domain space + E.g: A field in log-log-space can be transformed into log-norm-space, that is the y-axis stays logarithmic, but the x-axis is transfromed. diff --git a/nifty5/operators/offset_operator.py b/nifty5/operators/offset_operator.py index ca4ffea26aaf7ccaea04bd49218f82ac505e3e21..a8ebba807008a2e7a4f340614b101d54b0c4ce0f 100644 --- a/nifty5/operators/offset_operator.py +++ b/nifty5/operators/offset_operator.py @@ -19,6 +19,12 @@ from .operator import Operator class OffsetOperator(Operator): + '''Shifts the input by a fixed field. + + Parameters + ---------- + field : Field + The field by which the input is shifted.''' def __init__(self, field): self._field = field self._domain = self._target = field.domain diff --git a/nifty5/operators/operator.py b/nifty5/operators/operator.py index 9893c35fbf04305b789e499f08ab4a0de34fccb4..3b2c8a102bb8992576bf7134d9056dd35b23fa7f 100644 --- a/nifty5/operators/operator.py +++ b/nifty5/operators/operator.py @@ -26,16 +26,23 @@ class Operator(NiftyMetaBase()): @property def domain(self): - """DomainTuple or MultiDomain : the operator's input domain + """The domain on which the Operator's input Field is defined. - The domain on which the Operator's input Field is defined.""" + Returns + ------- + domain : DomainTuple or MultiDomain + """ return self._domain @property def target(self): - """DomainTuple or MultiDomain : the operator's output domain + """The domain on which the Operator's output Field is defined. + + Returns + ------- + target : DomainTuple or MultiDomain + """ - The domain on which the Operator's output Field is defined.""" return self._target @staticmethod @@ -103,10 +110,19 @@ class Operator(NiftyMetaBase()): return _OpChain.make((_Clipper(self.target, min, max), self)) def apply(self, x): + '''Applies the operator to a Field or MultiField. + + Parameters + ---------- + x : Field or MultiField + Input on which the operator shall act. Needs to be defined on + :attr:`domain`. + ''' raise NotImplementedError def force(self, x): - """Extract correct subset of domain of x and apply operator.""" + """Extract subset of domain of x according to `self.domain` and apply + operator.""" return self.apply(x.extract(self.domain)) def _check_input(self, x): diff --git a/nifty5/operators/qht_operator.py b/nifty5/operators/qht_operator.py index e327d6758a92e3b81eefbc172cf25a22e151fc5b..ff942521614f07a5944b0dd2547bc4e6c8d17758 100644 --- a/nifty5/operators/qht_operator.py +++ b/nifty5/operators/qht_operator.py @@ -26,9 +26,9 @@ from .linear_operator import LinearOperator class QHTOperator(LinearOperator): """Does a Hartley transform on LogRGSpace - This operator takes a field on a LogRGSpace and transforms it - according to the Hartley transform. The zero modes are not transformed - because they are infinitely far away. + This operator takes a field on a LogRGSpace and performs a Hartley + transform. The zero modes are not transformed because they are infinitely + far away in LogRGSpaces. Parameters ---------- diff --git a/nifty5/operators/regridding_operator.py b/nifty5/operators/regridding_operator.py index b6efcfac840ab720e71812c9d61fa51bcf645764..aad93b5cb18310c657c4ea34fe1e0cce83a3447c 100644 --- a/nifty5/operators/regridding_operator.py +++ b/nifty5/operators/regridding_operator.py @@ -26,6 +26,19 @@ from .linear_operator import LinearOperator class RegriddingOperator(LinearOperator): + '''Linearly interpolates a RGSpace to an RGSpace with coarser resolution. + + Parameters + ---------- + domain : Domain, DomainTuple or tuple of Domain + domain[space] needs to be an :class:`RGSpace`. + new_shape : tuple of int + Shape of the space which domain[space] is replaced by. Each entry must + be smaller or equal to the respective entry in `domain[space].shape`. + space : int + Index of space in `domain` on which the operator shall act. + Default is 0. + ''' def __init__(self, domain, new_shape, space=0): self._domain = DomainTuple.make(domain) self._space = infer_space(self._domain, space) @@ -38,6 +51,8 @@ class RegriddingOperator(LinearOperator): raise ValueError("Shape mismatch") if any([a > b for a, b in zip(new_shape, dom.shape)]): raise ValueError("New shape must not be larger than old shape") + if any([ii <= 0 for ii in new_shape]): + raise ValueError('New shape must not be zero or negative.') newdist = tuple(dom.distances[i]*dom.shape[i]/new_shape[i] for i in range(len(dom.shape))) diff --git a/nifty5/operators/scaling_operator.py b/nifty5/operators/scaling_operator.py index d0748d08806806495b8119460831197e81d1ce32..708301a1011a488a070662383690977f0ebd51fc 100644 --- a/nifty5/operators/scaling_operator.py +++ b/nifty5/operators/scaling_operator.py @@ -24,9 +24,6 @@ from .endomorphic_operator import EndomorphicOperator class ScalingOperator(EndomorphicOperator): """Operator which multiplies a Field with a scalar. - The NIFTy ScalingOperator class is a subclass derived from the - EndomorphicOperator. It multiplies an input field with a given factor. - Parameters ---------- factor : scalar diff --git a/nifty5/operators/slope_operator.py b/nifty5/operators/slope_operator.py index ac4aa8e18e39ed59769642123f2b2d4e62aa4fd0..1c84f6d5469684ccfccf0fb664c79ffc0787f8d0 100644 --- a/nifty5/operators/slope_operator.py +++ b/nifty5/operators/slope_operator.py @@ -25,30 +25,30 @@ from .linear_operator import LinearOperator class SlopeOperator(LinearOperator): - """Creates a slope on target. + """Evaluates a line on a LogRGSpace given slope and y-intercept - This operator creates a field on a LogRGSpace, which is created according - to a slope of given entries, (mean, y-intercept). The slope mean is the - power law of the field in normal-space. + Slope and y-intercept of this line are the two parameters which are + defined on an UnstructeredDomain (in this order) which is the domain of + the operator. Being a LogRGSpace each pixel has a well-defined coordinate + value. + + y-intercept is defined to be the value at t_0 of the target. Parameters ---------- - domain : domain or DomainTuple, shape=(2,) - It has to be an UnstructuredDomain. - The domain of the slope mean and the y-intercept mean. - target : domain or DomainTuple - The output domain has to a LogRGSpace - sigmas : np.array, shape=(2,) - The slope variance and the y-intercept variance. + target : LogRGSpace + The target of the operator which needs to be one-dimensional. """ def __init__(self, target): - if not isinstance(target, LogRGSpace): + self._target = DomainTuple.make(target) + if len(self._target) > 1: + raise TypeError + if len(self._target[0].shape) > 1: + raise TypeError + if not isinstance(self._target[0], LogRGSpace): raise TypeError - if len(target.shape) != 1: - raise ValueError("Slope Operator only works for ndim == 1") self._domain = DomainTuple.make(UnstructuredDomain((2,))) - self._target = DomainTuple.make(target) self._capability = self.TIMES | self.ADJOINT_TIMES pos = self.target[0].get_k_array() - self.target[0].t_0[0] self._pos = pos[0, 1:] diff --git a/nifty5/operators/symmetrizing_operator.py b/nifty5/operators/symmetrizing_operator.py index 6b149c20c165e1f0338457fdf6e23f8eed1c99dc..4f38603a4a2ad773737fafbdc8b0a989f81b128d 100644 --- a/nifty5/operators/symmetrizing_operator.py +++ b/nifty5/operators/symmetrizing_operator.py @@ -23,6 +23,17 @@ from .endomorphic_operator import EndomorphicOperator class SymmetrizingOperator(EndomorphicOperator): + '''Subtracts the field axes-wise in reverse order from itself. The slice of + all elements with at least one index being zero is not touched. + + Parameters + ---------- + domain : Domain, DomainTuple or tuple of Domain + Domain of the operator. `domain[space]` needs to be a non-harmonic + :class:`LogRGSpace`. + space : int + Index of space in domain on which the operator shall act. Default is 0. + ''' def __init__(self, domain, space=0): self._domain = DomainTuple.make(domain) self._capability = self.TIMES | self.ADJOINT_TIMES diff --git a/test/test_model_gradients.py b/test/test_model_gradients.py index 90fbdd4e5fe2dda2d341f69caaaf8a704cd92eb6..e30e0ce5497683551c5c301c20301ca9ec41c667 100644 --- a/test/test_model_gradients.py +++ b/test/test_model_gradients.py @@ -92,8 +92,7 @@ def testModelLibrary(space, seed): Npixdof, ceps_a, ceps_k, sm, sv, im, iv = 4, 0.5, 2., 3., 1.5, 1.75, 1.3 np.random.seed(seed) domain = ift.PowerSpace(space.get_default_codomain()) - model = ift.AmplitudeOperator(domain, Npixdof, ceps_a, ceps_k, sm, sv, im, - iv) + model = ift.SLAmplitude(domain, Npixdof, ceps_a, ceps_k, sm, sv, im, iv) assert_(isinstance(model, ift.Operator)) S = ift.ScalingOperator(1., model.domain) pos = S.draw_sample() @@ -104,6 +103,12 @@ def testModelLibrary(space, seed): pos = S.draw_sample() ift.extra.check_value_gradient_consistency(model2, pos, ntries=20) + domtup = ift.DomainTuple.make((space, space)) + model3 = ift.MfCorrelatedField(domtup, [model, model]) + S = ift.ScalingOperator(1., model3.domain) + pos = S.draw_sample() + ift.extra.check_value_gradient_consistency(model3, pos, ntries=20) + def testPointModel(space, seed): S = ift.ScalingOperator(1., space)