diff --git a/demos/misc/convolution.py b/demos/misc/convolution.py new file mode 100644 index 0000000000000000000000000000000000000000..3ffefbd97dbbc619c655b9ec9bcc7f75fd373e02 --- /dev/null +++ b/demos/misc/convolution.py @@ -0,0 +1,75 @@ +import numpy as np +import nifty5 as ift + + +def convtest(test_signal, delta, func): + domain = test_signal.domain + + codom = domain[0].get_default_codomain() + + # Create Convolution Operator + conv_op = ift.FuncConvolutionOperator(domain, func) + + # Convolve, Adjoint-Convolve + conv_signal = conv_op(signal) + cac_signal = conv_op.adjoint_times(conv_signal) + + print(signal.integrate(), conv_signal.integrate(), cac_signal.integrate()) + + # generate kernel image + conv_delta = conv_op(delta) + + # Plot results + plot = ift.Plot() + plot.add(signal, title='Signal') + plot.add(conv_signal, title='Signal Convolved') + plot.add(cac_signal, title='Signal, Conv, Adj-Conv') + plot.add(conv_delta, title='Kernel') + plot.output() + + +# Healpix test +nside = 64 +npix = 12 * nside * nside + +domain = ift.HPSpace(nside) + +# Define test signal (some point sources) +signal_vals = np.zeros(npix, dtype=np.float64) +for i in range(0, npix, npix//12 + 27): + signal_vals[i] = 1. + +signal = ift.from_global_data(domain, signal_vals) + +delta_vals = np.zeros(npix, dtype=np.float64) +delta_vals[0] = 1.0 +delta = ift.from_global_data(domain, delta_vals) + + +# Define kernel function +def func(theta): + ct = np.cos(theta) + return 1. * np.logical_and(ct > 0.7, ct <= 0.8) + + +convtest(signal, delta, func) + +domain = ift.RGSpace((100, 100)) +# Define test signal (some point sources) +signal_vals = np.zeros(domain.shape, dtype=np.float64) +signal_vals[35, 70] = 1 +signal_vals[45, 8] = 1 +signal = ift.from_global_data(domain, signal_vals) + +# Define delta signal, generate kernel image +delta_vals = np.zeros(domain.shape, dtype=np.float64) +delta_vals[0, 0] = 1.0 +delta = ift.from_global_data(domain, delta_vals) + + +# Define kernel function +def func(dist): + return 1. * np.logical_and(dist > 0.1, dist <= 0.2) + + +convtest(signal, delta, func) diff --git a/demos/misc/convolution_on_sphere.py b/demos/misc/convolution_on_sphere.py deleted file mode 100644 index 79e9a59151a433f73c020f8c1add05a9c2eed3a8..0000000000000000000000000000000000000000 --- a/demos/misc/convolution_on_sphere.py +++ /dev/null @@ -1,46 +0,0 @@ -import numpy as np -import nifty5 as ift - -# Define domains -nside = 64 -npix = 12 * nside * nside - -domain = ift.HPSpace(nside) -dom_tuple = ift.DomainTuple.make(domain) -codom = domain.get_default_codomain() - -# Define test signal (some point sources) -signal_vals = np.zeros(npix, dtype=np.float64) -for i in range(0, npix, npix//12 + 27): - signal_vals[i] = 1. -signal = ift.from_global_data(dom_tuple, signal_vals) - - -# Define kernel function -def func(theta): - ct = np.cos(theta) - return 1. * np.logical_and(ct > 0.7, ct <= 0.8) - - -# Create Convolution Operator -conv_op = ift.SphericalFuncConvolutionOperator(dom_tuple, func) - -# Convolve, Adjoint-Convolve -conv_signal = conv_op(signal) -cac_signal = conv_op.adjoint_times(conv_signal) - -print(signal.integrate(), conv_signal.integrate(), cac_signal.integrate()) - -# Define delta signal, generate kernel image -delta_vals = np.zeros(npix, dtype=np.float64) -delta_vals[0] = 1.0 -delta = ift.from_global_data(domain, delta_vals) -conv_delta = conv_op(delta) - -# Plot results -plot = ift.Plot() -plot.add(signal, title='Signal') -plot.add(conv_signal, title='Signal Convolved') -plot.add(cac_signal, title='Signal, Conv, Adj-Conv') -plot.add(conv_delta, title='Kernel') -plot.output() diff --git a/nifty5/__init__.py b/nifty5/__init__.py index 2cb37c0856223cbb6c3509c6f590100e0febf47f..f296569337b9c5ed44e73731d82f2ac2735ee857 100644 --- a/nifty5/__init__.py +++ b/nifty5/__init__.py @@ -50,7 +50,7 @@ from .operators.value_inserter import ValueInserter from .operators.energy_operators import ( EnergyOperator, GaussianEnergy, PoissonianEnergy, InverseGammaLikelihood, BernoulliEnergy, StandardHamiltonian, AveragedEnergy) -from .operators.convolution_operators import SphericalFuncConvolutionOperator +from .operators.convolution_operators import FuncConvolutionOperator from .probing import probe_with_posterior_samples, probe_diagonal, \ StatCalculator diff --git a/nifty5/domains/rg_space.py b/nifty5/domains/rg_space.py index 2e1d2c0296c7a80bbdfe2d53bd9c849069323bee..02d4f106da9af7137632e8c738efe7bb8f4e9cc3 100644 --- a/nifty5/domains/rg_space.py +++ b/nifty5/domains/rg_space.py @@ -90,9 +90,7 @@ class RGSpace(StructuredDomain): def scalar_dvol(self): return self._dvol - def get_k_length_array(self): - if (not self.harmonic): - raise NotImplementedError + def _get_dist_array(self): ibegin = dobj.ibegin_from_shape(self._shape) res = np.arange(self.local_shape[0], dtype=np.float64) + ibegin[0] res = np.minimum(res, self.shape[0]-res)*self.distances[0] @@ -106,6 +104,11 @@ class RGSpace(StructuredDomain): res = np.add.outer(res, tmp) return Field.from_local_data(self, np.sqrt(res)) + def get_k_length_array(self): + if (not self.harmonic): + raise NotImplementedError + return self._get_dist_array() + def get_unique_k_lengths(self): if (not self.harmonic): raise NotImplementedError @@ -146,6 +149,27 @@ class RGSpace(StructuredDomain): raise NotImplementedError return lambda x: self._kernel(x, sigma) + def get_conv_kernel_from_func(self, func): + """Creates a convolution kernel defined by a function. + + Parameters + ---------- + func: function + This function needs to take exactly one argument, which is + distance from center (in the same units as the RGSpace distances), + and return the kernel amplitude at that distance. + + Assumes the function to be radially symmetric, + e.g. only dependant on distance""" + from ..operators.harmonic_operators import HarmonicTransformOperator + if (not self.harmonic): + raise NotImplementedError + op = HarmonicTransformOperator(self, self.get_default_codomain()) + dist = op.target[0]._get_dist_array() + kernel = Field.from_local_data(op.target, func(dist.local_data)) + kernel = kernel / kernel.integrate() + return op.adjoint_times(kernel.weight(1)) + def get_default_codomain(self): """Returns a :class:`RGSpace` object representing the (position or harmonic) partner domain of `self`, depending on `self.harmonic`. diff --git a/nifty5/operators/convolution_operators.py b/nifty5/operators/convolution_operators.py index 6fd57764ad542bbacec2200d3aa782271f44067a..26a2faea3956423bb229a935153169644c7212bd 100644 --- a/nifty5/operators/convolution_operators.py +++ b/nifty5/operators/convolution_operators.py @@ -17,56 +17,57 @@ import numpy as np +from ..domains.rg_space import RGSpace from ..domains.lm_space import LMSpace from ..domains.hp_space import HPSpace from ..domains.gl_space import GLSpace from .endomorphic_operator import EndomorphicOperator from .harmonic_operators import HarmonicTransformOperator +from .diagonal_operator import DiagonalOperator +from .simple_linear_operators import WeightApplier from ..domain_tuple import DomainTuple from ..field import Field +from .. import utilities -def SphericalFuncConvolutionOperator(domain, func): +def FuncConvolutionOperator(domain, func, space=None): """Convolves input with a radially symmetric kernel defined by `func` Parameters ---------- domain: DomainTuple - Domain of the operator. Must have exactly one entry, which is - of type `HPSpace` or `GLSpace`. + Domain of the operator. func: function This function needs to take exactly one argument, which is colatitude in radians, and return the kernel amplitude at that colatitude. + space: int, optional + The index of the subdomain on which the operator should act + If None, it is set to 0 if `domain` contains exactly one space. + `domain[space]` must be of type `RGSpace`, `HPSpace`, or `GLSpace`. """ - if len(domain) != 1: - raise ValueError("need exactly one domain") - if not isinstance(domain[0], (HPSpace, GLSpace)): - raise TypeError("need a spherical domain") - kernel = domain[0].get_default_codomain().get_conv_kernel_from_func(func) - return _SphericalConvolutionOperator(domain, kernel) + domain = DomainTuple.make(domain) + space = utilities.infer_space(domain, space) + if not isinstance(domain[space], (RGSpace, HPSpace, GLSpace)): + raise TypeError("unsupported domain") + codomain = domain[space].get_default_codomain() + kernel = codomain.get_conv_kernel_from_func(func) + return _ConvolutionOperator(domain, kernel, space) -class _SphericalConvolutionOperator(EndomorphicOperator): - """Convolves with kernel living on the appropriate LMSpace""" - - def __init__(self, domain, kernel): - if len(domain) != 1: - raise ValueError("need exactly one domain") - if len(kernel.domain) != 1: - raise ValueError("kernel needs exactly one domain") - if not isinstance(domain[0], (HPSpace, GLSpace)): - raise TypeError("need a spherical domain") - self._domain = domain - self.lm = domain[0].get_default_codomain() - if self.lm != kernel.domain[0]: - raise ValueError("Input domain and kernel are incompatible") - self.kernel = kernel - self.HT = HarmonicTransformOperator(self.lm, domain[0]) - self._capability = self.TIMES | self.ADJOINT_TIMES - - def apply(self, x, mode): - self._check_input(x, mode) - x_lm = self.HT.adjoint_times(x.weight(1)) - x_lm = x_lm * self.kernel * (4. * np.pi) - return self.HT(x_lm) +def _ConvolutionOperator(domain, kernel, space=None): + domain = DomainTuple.make(domain) + space = utilities.infer_space(domain, space) + if len(kernel.domain) != 1: + raise ValueError("kernel needs exactly one domain") + if not isinstance(domain[space], (HPSpace, GLSpace, RGSpace)): + raise TypeError("need RGSpace, HPSpace, or GLSpace") + lm = [d for d in domain] + lm[space] = lm[space].get_default_codomain() + lm = DomainTuple.make(lm) + if lm[space] != kernel.domain[0]: + raise ValueError("Input domain and kernel are incompatible") + HT = HarmonicTransformOperator(lm, domain[space], space) + diag = DiagonalOperator(kernel*domain[space].total_volume, lm, (space,)) + wgt = WeightApplier(domain, space, 1) + return HT(diag(HT.adjoint(wgt))) diff --git a/nifty5/operators/simple_linear_operators.py b/nifty5/operators/simple_linear_operators.py index 1628f2ab92cc15fe183d11023fb127e3c3bbdc47..3c1b54f4be5a62f7e7a51483a672b87746312d4f 100644 --- a/nifty5/operators/simple_linear_operators.py +++ b/nifty5/operators/simple_linear_operators.py @@ -63,6 +63,35 @@ class ConjugationOperator(EndomorphicOperator): return x.conjugate() +class WeightApplier(EndomorphicOperator): + """Operator multiplying its input by a given power of dvol. + + Parameters + ---------- + domain: Domain, tuple of domains or DomainTuple + domain of the input field + spaces: list or tuple of int + indices of subdomains for which the weights shall be applied + power: int + the power of to be used for the volume factors + + """ + def __init__(self, domain, spaces, power): + from .. import utilities + self._domain = DomainTuple.make(domain) + if spaces is None: + self._spaces = None + else: + self._spaces = utilities.parse_spaces(spaces, len(self._domain)) + self._power = int(power) + self._capability = self._all_ops + + def apply(self, x, mode): + self._check_input(x, mode) + power = self._power if (mode & 3) else -self._power + return x.weight(power, spaces=self._spaces) + + class Realizer(EndomorphicOperator): """Operator returning the real component of its input.