diff --git a/demos/critical_filtering.py b/demos/critical_filtering.py index acc270d5b1002b83d6d929bbb04b1ccce8a1f713..edf136e3bb990faa50e36c6ee1594cb98f21ec36 100644 --- a/demos/critical_filtering.py +++ b/demos/critical_filtering.py @@ -97,10 +97,8 @@ if __name__ == "__main__": # The information source j = R.adjoint_times(N.inverse_times(d)) - realized_power = log(sh.power_analyze(logarithmic=p_space.config["logarithmic"], - nbin=p_space.config["nbin"])) - data_power = log(fft(d).power_analyze(logarithmic=p_space.config["logarithmic"], - nbin=p_space.config["nbin"])) + realized_power = log(sh.power_analyze(binbounds=p_space.binbounds)) + data_power = log(fft(d).power_analyze(binbounds=p_space.binbounds)) d_data = d.val.get_full_data().real if rank == 0: pl.plot([go.Heatmap(z=d_data)], filename='data.html') diff --git a/nifty/field.py b/nifty/field.py index 60710495aecd7e0efde606612286de8d0b91619f..ee12368b7421bef1747cca1a6a08401e9d0253da 100644 --- a/nifty/field.py +++ b/nifty/field.py @@ -274,7 +274,7 @@ class Field(Loggable, Versionable, object): # ---Powerspectral methods--- - def power_analyze(self, spaces=None, logarithmic=False, nbin=None, + def power_analyze(self, spaces=None, logarithmic=None, nbin=None, binbounds=None, keep_phase_information=False): """ Computes the square root power spectrum for a subspace of `self`. @@ -291,14 +291,15 @@ class Field(Loggable, Versionable, object): (default : None). logarithmic : boolean *optional* True if the output PowerSpace should use logarithmic binning. - {default : False} + {default : None} nbin : int *optional* The number of bins the resulting PowerSpace shall have (default : None). if nbin==None : maximum number of bins is used binbounds : array-like *optional* Inner bounds of the bins (default : None). - if binbounds==None : bins are inferred. Overwrites nbins and log + Overrides nbin and logarithmic. + if binbounds==None : bins are inferred. keep_phase_information : boolean, *optional* If False, return a real-valued result containing the power spectrum of the input Field. @@ -401,14 +402,9 @@ class Field(Loggable, Versionable, object): logarithmic=logarithmic, nbin=nbin, binbounds=binbounds) - # extract pindex and rho from power_domain - pindex = power_domain.pindex - rho = power_domain.rho - power_spectrum = cls._calculate_power_spectrum( field_val=work_field.val, - pindex=pindex, - rho=rho, + pdomain=power_domain, axes=work_field.domain_axes[space_index]) # create the result field and put power_spectrum into it @@ -425,8 +421,11 @@ class Field(Loggable, Versionable, object): return result_field @classmethod - def _calculate_power_spectrum(cls, field_val, pindex, rho, axes=None): + def _calculate_power_spectrum(cls, field_val, pdomain, axes=None): + pindex = pdomain.pindex + # MR FIXME: how about iterating over slices, instead of replicating + # pindex? Would save memory and probably isn't slower. if axes is not None: pindex = cls._shape_up_pindex( pindex=pindex, @@ -435,6 +434,7 @@ class Field(Loggable, Versionable, object): axes=axes) power_spectrum = pindex.bincount(weights=field_val, axis=axes) + rho = pdomain.rho if axes is not None: new_rho_shape = [1, ] * len(power_spectrum.shape) new_rho_shape[axes[0]] = len(rho) @@ -759,7 +759,7 @@ class Field(Loggable, Versionable, object): Returns ------- out : tuple - The output object. The tuple contains the dimansions of the spaces + The output object. The tuple contains the dimensions of the spaces in domain. See Also diff --git a/nifty/library/critical_filter/critical_power_energy.py b/nifty/library/critical_filter/critical_power_energy.py index 7848aee5901b71af033225fb1404a329bfe73411..e94e2d35c97be477d11f96792300897da6e1dd55 100644 --- a/nifty/library/critical_filter/critical_power_energy.py +++ b/nifty/library/critical_filter/critical_power_energy.py @@ -103,14 +103,12 @@ class CriticalPowerEnergy(Energy): posterior_sample = generate_posterior_sample( self.m, self.D) projected_sample = posterior_sample.power_analyze( - logarithmic=self.position.domain[0].config["logarithmic"], - nbin=self.position.domain[0].config["nbin"]) + binbounds=self.position.domain[0].binbounds) w += (projected_sample) * self.rho w /= float(self.samples) else: w = self.m.power_analyze( - logarithmic=self.position.domain[0].config["logarithmic"], - nbin=self.position.domain[0].config["nbin"]) + binbounds=self.position.domain[0].binbounds) w *= self.rho self._w = w return self._w diff --git a/nifty/operators/invertible_operator_mixin/invertible_operator_mixin.py b/nifty/operators/invertible_operator_mixin/invertible_operator_mixin.py index 4517ff24d12d5aa00e21a21fe19d30dcfd680950..10b52362331aabfd87b6fef49e3ad07774c7a7b8 100644 --- a/nifty/operators/invertible_operator_mixin/invertible_operator_mixin.py +++ b/nifty/operators/invertible_operator_mixin/invertible_operator_mixin.py @@ -39,7 +39,7 @@ class InvertibleOperatorMixin(object): (default: ConjugateGradient) preconditioner : LinearOperator - Preconditioner that is used by ConjugateGraduent if no minimizer was + Preconditioner that is used by ConjugateGradient if no minimizer was given. Attributes diff --git a/nifty/operators/response_operator/response_operator.py b/nifty/operators/response_operator/response_operator.py index b23ab13dbf2a43771327d7d57d45ebf019a1bea8..2c86031721b440e7ceb2ef941c12686ef1557a80 100644 --- a/nifty/operators/response_operator/response_operator.py +++ b/nifty/operators/response_operator/response_operator.py @@ -34,8 +34,7 @@ class ResponseOperator(LinearOperator): domain : tuple of DomainObjects, i.e. Spaces and FieldTypes The domain on which the Operator's input Field lives. target : tuple of DomainObjects, i.e. Spaces and FieldTypes - The domain in which the outcome of the operator lives. As the Operator - is endomorphic this is the same as its domain. + The domain in which the outcome of the operator lives. unitary : boolean Indicates whether the Operator is unitary or not. diff --git a/nifty/spaces/lm_space/lm_space.py b/nifty/spaces/lm_space/lm_space.py index 7fa584220093e264f528a0bfca94a0e5405a61bb..66c9f08608dff9fff48cfbf4a2e6ea6f7c132cb0 100644 --- a/nifty/spaces/lm_space/lm_space.py +++ b/nifty/spaces/lm_space/lm_space.py @@ -127,6 +127,22 @@ class LMSpace(Space): return x.copy() def get_distance_array(self, distribution_strategy): + if distribution_strategy == 'not': # short cut + lmax = self.lmax + ldist = np.empty((self.dim,), dtype=np.float64) + ldist[0:lmax+1] = np.arange(lmax+1, dtype=np.float64) + tmp = np.empty((2*lmax+2), dtype=np.float64) + tmp[0::2] = np.arange(lmax+1) + tmp[1::2] = np.arange(lmax+1) + idx = lmax+1 + for l in range(1, lmax+1): + ldist[idx:idx+2*(lmax+1-l)] = tmp[2*l:] + idx += 2*(lmax+1-l) + dists = arange(start=0, stop=self.shape[0], + distribution_strategy=distribution_strategy) + dists.set_local_data(ldist) + return dists + dists = arange(start=0, stop=self.shape[0], distribution_strategy=distribution_strategy) @@ -136,6 +152,12 @@ class LMSpace(Space): return dists + def get_unique_distances(self): + return np.arange(self.lmax+1, dtype=np.float64) + + def get_natural_binbounds(self): + return np.arange(self.lmax, dtype=np.float64) + 0.5 + @staticmethod def _distance_array_helper(index_array, lmax): u = 2*lmax + 1 diff --git a/nifty/spaces/power_space/__init__.py b/nifty/spaces/power_space/__init__.py index 8f160171c3147480f2848074c3a8b61e3c134a6e..36c265fcfa9ad642f552148a589e7eb5bd440568 100644 --- a/nifty/spaces/power_space/__init__.py +++ b/nifty/spaces/power_space/__init__.py @@ -17,4 +17,3 @@ # and financially supported by the Studienstiftung des deutschen Volkes. from .power_space import PowerSpace -from .power_index_factory import PowerIndexFactory diff --git a/nifty/spaces/power_space/power_index_factory.py b/nifty/spaces/power_space/power_index_factory.py deleted file mode 100644 index 545e47744a908b35440edc4a9fe1446d3008180d..0000000000000000000000000000000000000000 --- a/nifty/spaces/power_space/power_index_factory.py +++ /dev/null @@ -1,44 +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-2017 Max-Planck-Society -# -# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik -# and financially supported by the Studienstiftung des deutschen Volkes. - -from builtins import object -from .power_indices import PowerIndices - - -class _PowerIndexFactory(object): - def __init__(self): - self.power_indices_storage = {} - - def get_power_index(self, domain, distribution_strategy, - logarithmic=False, nbin=None, binbounds=None): - key = (domain, distribution_strategy) - - if key not in self.power_indices_storage: - self.power_indices_storage[key] = \ - PowerIndices(domain, distribution_strategy, - logarithmic=logarithmic, - nbin=nbin, - binbounds=binbounds) - power_indices = self.power_indices_storage[key] - power_index = power_indices.get_index_dict(logarithmic=logarithmic, - nbin=nbin, - binbounds=binbounds) - return power_index - - -PowerIndexFactory = _PowerIndexFactory() diff --git a/nifty/spaces/power_space/power_indices.py b/nifty/spaces/power_space/power_indices.py deleted file mode 100644 index bb210be857d71b018ea320383ef4794925feff47..0000000000000000000000000000000000000000 --- a/nifty/spaces/power_space/power_indices.py +++ /dev/null @@ -1,422 +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-2017 Max-Planck-Society -# -# NIFTy is being developed at the Max-Planck-Institut fuer Astrophysik -# and financially supported by the Studienstiftung des deutschen Volkes. - -from __future__ import division -from builtins import range -from builtins import object -import numpy as np -from d2o import distributed_data_object,\ - STRATEGIES as DISTRIBUTION_STRATEGIES - -from d2o.config import dependency_injector as d2o_di -from d2o.config import configuration as d2o_config - - -class PowerIndices(object): - """Computes helpful quantities to deal with power spectra. - - Given the shape and the density of a underlying rectangular grid this - class provides the user - with the pindex, kindex, rho and pundex. The indices are binned - according to the supplied parameter scheme. If wanted, computed - results are stored for future reuse. - - Parameters - ---------- - domain : NIFTy harmonic space - The space for which the power indices get computed - distribution_strategy : str - The distribution_strategy that will be used for the k_array and pindex - distributed_data_object. - logarithmic : bool *optional* - Flag specifying if the binning of the default indices is - performed on logarithmic scale. - nbin : integer *optional* - Number of used bins for the binning of the default indices. - binbounds : {list, array} - Array-like inner boundaries of the used bins of the default - indices. - """ - def __init__(self, domain, distribution_strategy, - logarithmic=False, nbin=None, binbounds=None): - self.domain = domain - self.distribution_strategy = distribution_strategy - - # Compute the global k_array - self.k_array = self.domain.get_distance_array(distribution_strategy) - # Initialize the dictionary which stores all individual index-dicts - self.global_dict = {} - # Set self.default_parameters - self.set_default(config_dict={'logarithmic': logarithmic, - 'nbin': nbin, - 'binbounds': binbounds}) - - # Redirect the direct calls approaching a power_index instance to the - # default_indices dict - @property - def default_indices(self): - return self.get_index_dict(**self.default_parameters) - - def __getitem__(self, x): - return self.default_indices.get(x) - - def __contains__(self, x): - return self.default_indices.__contains__(x) - - def __iter__(self): - return self.default_indices.__iter__() - - def __getattr__(self, x): - return self.default_indices.__getattribute__(x) - - def set_default(self, **kwargs): - """ - Sets the index-set which is specified by the parameters as the - default for the power_index instance. - - Parameters - ---------- - logarithmic : bool - Flag specifying if the binning is performed on logarithmic - scale. - nbin : integer - Number of used bins. - binbounds : {list, array} - Array-like inner boundaries of the used bins. - - Returns - ------- - None - """ - parsed_kwargs = self._cast_config(**kwargs) - self.default_parameters = parsed_kwargs - - def _cast_config(self, **kwargs): - """ - internal helper function which casts the various combinations of - possible parameters into a properly defaulted dictionary - """ - temp_config_dict = kwargs.get('config_dict', None) - if temp_config_dict is not None: - return self._cast_config_helper(**temp_config_dict) - else: - defaults = self.default_parameters - temp_logarithmic = kwargs.get("logarithmic", - defaults['logarithmic']) - temp_nbin = kwargs.get("nbin", defaults['nbin']) - temp_binbounds = kwargs.get("binbounds", defaults['binbounds']) - - return self._cast_config_helper(logarithmic=temp_logarithmic, - nbin=temp_nbin, - binbounds=temp_binbounds) - - def _cast_config_helper(self, logarithmic, nbin, binbounds): - """ - internal helper function which sets the defaults for the - _cast_config function - """ - - try: - temp_logarithmic = bool(logarithmic) - except(TypeError): - temp_logarithmic = False - - try: - temp_nbin = int(nbin) - except(TypeError): - temp_nbin = None - - try: - temp_binbounds = tuple(np.array(binbounds)) - except(TypeError): - temp_binbounds = None - - temp_dict = {"logarithmic": temp_logarithmic, - "nbin": temp_nbin, - "binbounds": temp_binbounds} - return temp_dict - - def get_index_dict(self, **kwargs): - """ - Returns a dictionary containing the pindex, kindex, rho and pundex - binned according to the supplied parameter scheme and a - configuration dict containing this scheme. - - Parameters - ---------- - store : bool - Flag specifying if the calculated index dictionary should be - stored in the global_dict for future use. - logarithmic : bool - Flag specifying if the binning is performed on logarithmic - scale. - nbin : integer - Number of used bins. - binbounds : {list, array} - Array-like inner boundaries of the used bins. - - Returns - ------- - index_dict : dict - Contains the keys: 'config', 'pindex', 'kindex', 'rho' and - 'pundex' - """ - # Cast the input arguments - temp_config_dict = self._cast_config(**kwargs) - # Compute a hashable identifier from the config which will be used - # as dict key - temp_key = self._freeze_config(temp_config_dict) - # Check if the result should be stored for future use. - storeQ = kwargs.get("store", True) - # Try to find the requested index dict in the global_dict - try: - return self.global_dict[temp_key] - except(KeyError): - # If it is not found, calculate it. - temp_index_dict = self._compute_index_dict(temp_config_dict) - # Store it, if required - if storeQ: - self.global_dict[temp_key] = temp_index_dict - # Important: If the result is stored, return a reference to - # the dictionary entry, not anly a plain copy. Otherwise, - # set_default breaks! - return self.global_dict[temp_key] - else: - # Return the plain result. - return temp_index_dict - - def _freeze_config(self, config_dict): - """ - a helper function which forms a hashable identifying object from - a config dictionary which can be used as key of a dict - """ - return frozenset(list(config_dict.items())) - - def _compute_index_dict(self, config_dict): - """ - Internal helper function which takes a config_dict, asks for the - pindex/kindex/rho/pundex set, and bins them according to the config - """ - # if no binning is requested, compute the indices, build the dict, - # and return it straight. - if not config_dict["logarithmic"] and config_dict["nbin"] is None and \ - config_dict["binbounds"] is None: - (temp_pindex, temp_kindex, temp_rho, temp_pundex) =\ - self._compute_indices(self.k_array) - temp_k_array = self.k_array - - # if binning is required, make a recursive call to get the unbinned - # indices, bin them, compute the pundex and then return everything. - else: - # Get the unbinned indices - temp_unbinned_indices = self.get_index_dict(nbin=None, - binbounds=None, - logarithmic=False, - store=False) - # Bin them - (temp_pindex, temp_kindex, temp_rho, temp_pundex) = \ - self._bin_power_indices( - temp_unbinned_indices, **config_dict) - # Make a binned version of k_array - temp_k_array = self._compute_k_array_from_pindex_kindex( - temp_pindex, temp_kindex) - - temp_index_dict = {"config": config_dict, - "pindex": temp_pindex, - "kindex": temp_kindex, - "rho": temp_rho, - "pundex": temp_pundex, - "k_array": temp_k_array} - return temp_index_dict - - def _compute_k_array_from_pindex_kindex(self, pindex, kindex): - tempindex = pindex.copy(dtype=kindex.dtype) - result = tempindex.apply_scalar_function( - lambda x: kindex[x.astype(np.dtype('int'))]) - return result - - def _compute_indices(self, k_array): - """ - Internal helper function which computes pindex, kindex, rho and pundex - from a given k_array - """ - ########## - # kindex # - ########## - global_kindex = k_array.unique() - - ########## - # pindex # - ########## - # compute the local pindex slice on basis of the local k_array data - local_pindex = np.searchsorted(global_kindex, k_array.get_local_data()) - # prepare the distributed_data_object - global_pindex = distributed_data_object( - global_shape=k_array.shape, - dtype=local_pindex.dtype, - distribution_strategy=self.distribution_strategy) - # store the local pindex data in the global_pindex d2o - global_pindex.set_local_data(local_pindex) - - ####### - # rho # - ####### - global_rho = global_pindex.bincount().get_full_data() - - ########## - # pundex # - ########## - global_pundex = self._compute_pundex(global_pindex, - global_kindex) - - return global_pindex, global_kindex, global_rho, global_pundex - - def _compute_pundex(self, global_pindex, global_kindex): - """ - Internal helper function which computes the pundex array from a - pindex and a kindex array. This function is separated from the - _compute_indices function as it is needed in _bin_power_indices, - too. - """ - if self.distribution_strategy in DISTRIBUTION_STRATEGIES['slicing']: - ########## - # pundex # - ########## - # Prepare the local data - local_pindex = global_pindex.get_local_data() - # Compute the local pundices for the local pindices - (temp_uniqued_pindex, local_temp_pundex) = np.unique( - local_pindex, - return_index=True) - # cure a bug in numpy - # https://github.com/numpy/numpy/issues/9405 - local_temp_pundex = np.asarray(local_temp_pundex, dtype=np.int) - # Shift the local pundices by the nodes' local_dim_offset - local_temp_pundex += global_pindex.distributor.local_dim_offset - - # Prepare the pundex arrays used for the Allreduce operation - # pundex has the same length as the kindex array - local_pundex = np.zeros(shape=global_kindex.shape, dtype=np.int) - # Set the default value higher than the maximal possible pundex - # value so that MPI.MIN can sort out the default - local_pundex += np.prod(global_pindex.shape) + 1 - # Set the default value higher than the length - global_pundex = np.empty_like(local_pundex) - # Store the individual pundices in the local_pundex array - local_pundex[temp_uniqued_pindex] = local_temp_pundex - # Extract the MPI module from the global_pindex d2o - MPI = d2o_di[d2o_config['mpi_module']] - # Use Allreduce to find the first occurences/smallest pundices - global_pindex.comm.Allreduce(local_pundex, - global_pundex, - op=MPI.MIN) - return global_pundex - - elif self.distribution_strategy in DISTRIBUTION_STRATEGIES['not']: - ########## - # pundex # - ########## - pundex = np.unique(global_pindex.get_local_data(), - return_index=True)[1] - return pundex - else: - raise NotImplementedError( - "_compute_pundex_d2o not implemented for given " - "distribution_strategy") - - def _bin_power_indices(self, index_dict, **kwargs): - """ - Returns the binned power indices associated with the Fourier grid. - - Parameters - ---------- - pindex : distributed_data_object - Index of the Fourier grid points in a distributed_data_object. - kindex : ndarray - Array of all k-vector lengths. - rho : ndarray - Degeneracy factor of the individual k-vectors. - logarithmic : bool - Flag specifying if the binning is performed on logarithmic - scale. - nbin : integer - Number of used bins. - binbounds : {list, array} - Array-like inner boundaries of the used bins. - - Returns - ------- - pindex : distributed_data_object - kindex, rho, pundex : ndarrays - The (re)binned power indices. - - """ - # Cast the given config - temp_config_dict = self._cast_config(**kwargs) - logarithmic = temp_config_dict['logarithmic'] - nbin = temp_config_dict['nbin'] - binbounds = temp_config_dict['binbounds'] - - # Extract the necessary indices from the supplied index dict - pindex = index_dict["pindex"] - kindex = index_dict["kindex"] - rho = index_dict["rho"] - - # boundaries - if(binbounds is not None): - binbounds = np.sort(binbounds) - # equal binning - else: - if(logarithmic is None): - logarithmic = False - if(logarithmic): - k = np.r_[0, np.log(kindex[1:])] - else: - k = kindex - dk = np.max(k[2:] - k[1:-1]) # minimal dk - if(nbin is None): - nbin = int((k[-1] - 0.5 * (k[2] + k[1])) / - dk - 0.5) # maximal nbin - else: - nbin = min(int(nbin), int( - (k[-1] - 0.5 * (k[2] + k[1])) / dk + 2.5)) - dk = (k[-1] - 0.5 * (k[2] + k[1])) / (nbin - 2.5) - binbounds = np.r_[0.5 * (3 * k[1] - k[2]), - 0.5 * (k[1] + k[2]) + dk * np.arange(nbin - 2)] - if(logarithmic): - binbounds = np.exp(binbounds) - # reordering - reorder = np.searchsorted(binbounds, kindex) - rho_ = np.zeros(len(binbounds) + 1, dtype=rho.dtype) - kindex_ = np.empty(len(binbounds) + 1, dtype=kindex.dtype) - for ii in range(len(reorder)): - if(rho_[reorder[ii]] == 0): - kindex_[reorder[ii]] = kindex[ii] - rho_[reorder[ii]] += rho[ii] - else: - kindex_[reorder[ii]] = ((kindex_[reorder[ii]] * - rho_[reorder[ii]] + - kindex[ii] * rho[ii]) / - (rho_[reorder[ii]] + rho[ii])) - rho_[reorder[ii]] += rho[ii] - - pindex_ = pindex.copy_empty() - pindex_.set_local_data(reorder[pindex.get_local_data()]) - pundex_ = self._compute_pundex(pindex_, kindex_) - - return pindex_, kindex_, rho_, pundex_ diff --git a/nifty/spaces/power_space/power_space.py b/nifty/spaces/power_space/power_space.py index d96ff42cd09dc5dd4e4d622c4c8dafc607fef136..84ebcf186ff183887fbc31e4c85472c3963e34f4 100644 --- a/nifty/spaces/power_space/power_space.py +++ b/nifty/spaces/power_space/power_space.py @@ -19,9 +19,7 @@ # from builtins import str import numpy as np -import d2o - -from .power_index_factory import PowerIndexFactory +from d2o import distributed_data_object from ..space import Space from functools import reduce @@ -39,18 +37,24 @@ class PowerSpace(Space): derived from this PowerSpace, e.g. the pindex. (default : 'not') logarithmic : bool *optional* - True if logarithmic binning should be used (default : False). + True if logarithmic binning should be used (default : None). nbin : {int, None} *optional* The number of bins that should be used for power spectrum binning (default : None). if nbin == None, then nbin is set to the length of kindex. binbounds : {list, array-like} *optional* - Array-like inner boundaries of the used bins of the default - indices. + Boundaries between the power spectrum bins. + (If binbounds has n entries, there will be n+1 bins, the first bin + starting at -inf, the last bin ending at +inf.) (default : None) - if binbounds == None : + if binbounds == None: Calculates the bounds from the kindex while applying the logarithmic and nbin keywords. + Note: if "bindounds" is not None, both "logarithmic" and "nbin" must be + None! + Note: if "binbounds", "logarithmic", and "nbin" are all None, then + "natural" binning is performed, i.e. there will be one bin for every + distinct k-vector length. Attributes ---------- @@ -59,9 +63,6 @@ class PowerSpace(Space): mapped to which power bin kindex : numpy.ndarray Sorted array of all k-modes. - pundex : numpy.ndarray - Flat index of the first occurence of a k-vector with length==kindex[n] - in the k_array. rho : numpy.ndarray The amount of k-modes that get mapped to one power bin is given by rho. @@ -73,9 +74,8 @@ class PowerSpace(Space): The total volume of the space. shape : tuple of np.ints The shape of the space's data array. - config : {logarithmic, nbin, binbounds} - Dictionary storing the values for `logarithmic`, `nbin`, and - `binbounds` that were used during initialization. + binbounds : tuple or None + Boundaries between the power spectrum bins Notes ----- @@ -84,42 +84,107 @@ class PowerSpace(Space): """ + _powerIndexCache = {} + # ---Overwritten properties and methods--- - def __init__(self, harmonic_partner, - distribution_strategy='not', - logarithmic=False, nbin=None, binbounds=None): + def __init__(self, harmonic_partner, distribution_strategy='not', + logarithmic=None, nbin=None, binbounds=None): super(PowerSpace, self).__init__() - self._ignore_for_hash += ['_pindex', '_kindex', '_rho', '_pundex', - '_k_array'] + self._ignore_for_hash += ['_pindex', '_kindex', '_rho'] - if not isinstance(harmonic_partner, Space): - raise ValueError( - "harmonic_partner must be a Space.") - if not harmonic_partner.harmonic: - raise ValueError( - "harmonic_partner must be a harmonic space.") + if not (isinstance(harmonic_partner, Space) and + harmonic_partner.harmonic): + raise ValueError("harmonic_partner must be a harmonic space.") self._harmonic_partner = harmonic_partner - power_index = PowerIndexFactory.get_power_index( - domain=self.harmonic_partner, - distribution_strategy=distribution_strategy, - logarithmic=logarithmic, - nbin=nbin, - binbounds=binbounds) - - self._config = power_index['config'] + # sanity check + if binbounds is not None and not(nbin is None and logarithmic is None): + raise ValueError( + "if binbounds is defined, nbin and logarithmic must be None") + + if binbounds is not None: + binbounds = tuple(binbounds) + + key = (harmonic_partner, distribution_strategy, logarithmic, nbin, + binbounds) + if self._powerIndexCache.get(key) is None: + distance_array = \ + self.harmonic_partner.get_distance_array(distribution_strategy) + temp_binbounds = self._compute_binbounds( + harmonic_partner=self.harmonic_partner, + distribution_strategy=distribution_strategy, + logarithmic=logarithmic, + nbin=nbin, + binbounds=binbounds) + temp_pindex = self._compute_pindex( + harmonic_partner=self.harmonic_partner, + distance_array=distance_array, + binbounds=temp_binbounds, + distribution_strategy=distribution_strategy) + temp_rho = temp_pindex.bincount().get_full_data() + temp_kindex = \ + (temp_pindex.bincount(weights=distance_array).get_full_data() / + temp_rho) + self._powerIndexCache[key] = (temp_binbounds, + temp_pindex, + temp_kindex, + temp_rho) + + (self._binbounds, self._pindex, self._kindex, self._rho) = \ + self._powerIndexCache[key] + + @staticmethod + def _compute_binbounds(harmonic_partner, distribution_strategy, + logarithmic, nbin, binbounds): + + if logarithmic is None and nbin is None and binbounds is None: + result = None + else: + if binbounds is not None: + bb = np.sort(np.array(binbounds)) + else: + if logarithmic is not None: + logarithmic = bool(logarithmic) + if nbin is not None: + nbin = int(nbin) + + # equidistant binning (linear or log) + # MR FIXME: this needs to improve + kindex = harmonic_partner.get_unique_distances() + if (logarithmic): + k = np.r_[0, np.log(kindex[1:])] + else: + k = kindex + dk = np.max(k[2:] - k[1:-1]) # minimum dk to avoid empty bins + if(nbin is None): + nbin = int((k[-1] - 0.5 * (k[2] + k[1])) / + dk - 0.5) # maximal nbin + else: + nbin = min(int(nbin), int( + (k[-1] - 0.5 * (k[2] + k[1])) / dk + 2.5)) + dk = (k[-1] - 0.5 * (k[2] + k[1])) / (nbin - 2.5) + bb = np.r_[0.5 * (3 * k[1] - k[2]), + 0.5 * (k[1] + k[2]) + dk * np.arange(nbin-2)] + if(logarithmic): + bb = np.exp(bb) + result = tuple(bb) + return result - self._pindex = power_index['pindex'] - self._kindex = power_index['kindex'] - self._rho = power_index['rho'] - self._pundex = power_index['pundex'] - self._k_array = power_index['k_array'] + @staticmethod + def _compute_pindex(harmonic_partner, distance_array, binbounds, + distribution_strategy): - if self.config['nbin'] is not None: - if self.config['nbin'] > len(self.kindex): - self.logger.warn("nbin was set to a value being larger than " - "the length of kindex!") + # Compute pindex, kindex and rho according to bb + pindex = distributed_data_object( + global_shape=distance_array.shape, + dtype=np.int, + distribution_strategy=distribution_strategy) + if binbounds is None: + binbounds = harmonic_partner.get_natural_binbounds() + pindex.set_local_data( + np.searchsorted(binbounds, distance_array.get_local_data())) + return pindex def pre_cast(self, x, axes): """ Casts power spectrum functions to discretized power spectra. @@ -144,19 +209,15 @@ class PowerSpace(Space): """ - if callable(x): - return x(self.kindex) - else: - return x + return x(self.kindex) if callable(x) else x # ---Mandatory properties and methods--- def __repr__(self): return ("PowerSpace(harmonic_partner=%r, distribution_strategy=%r, " - "logarithmic=%r, nbin=%r, binbounds=%r)" + "binbounds=%r)" % (self.harmonic_partner, self.pindex.distribution_strategy, - self.config['logarithmic'], self.config['nbin'], - self.config['binbounds'])) + self._binbounds)) @property def harmonic(self): @@ -179,11 +240,9 @@ class PowerSpace(Space): distribution_strategy = self.pindex.distribution_strategy return self.__class__(harmonic_partner=self.harmonic_partner, distribution_strategy=distribution_strategy, - logarithmic=self.config["logarithmic"], - nbin=self.config["nbin"], - binbounds=self.config["binbounds"]) + binbounds=self._binbounds) - def weight(self, x, power=1, axes=None, inplace=False): + def weight(self, x, power, axes, inplace=False): reshaper = [1, ] * len(x.shape) # we know len(axes) is always 1 reshaper[axes[0]] = self.shape[0] @@ -201,10 +260,9 @@ class PowerSpace(Space): return result_x def get_distance_array(self, distribution_strategy): - result = d2o.distributed_data_object( + return distributed_data_object( self.kindex, dtype=np.float64, distribution_strategy=distribution_strategy) - return result def get_fft_smoothing_kernel_function(self, sigma): raise NotImplementedError( @@ -219,11 +277,8 @@ class PowerSpace(Space): return self._harmonic_partner @property - def config(self): - """ Returns the configuration which was used for `logarithmic`, `nbin` - and `binbounds` during initialization. - """ - return self._config + def binbounds(self): + return self._binbounds @property def pindex(self): @@ -244,67 +299,20 @@ class PowerSpace(Space): """ return self._rho - @property - def pundex(self): - """ An array for which the n-th entry gives the flat index of the - first occurence of a k-vector with length==kindex[n] in the - k_array. - """ - return self._pundex - - @property - def k_array(self): - """ An array containing distances to the grid center (i.e. zero-mode) - for every k-mode in the grid of the harmonic partner space. - """ - return self._k_array - # ---Serialization--- def _to_hdf5(self, hdf5_group): - hdf5_group['kindex'] = self.kindex - hdf5_group['rho'] = self.rho - hdf5_group['pundex'] = self.pundex - hdf5_group['logarithmic'] = self.config["logarithmic"] - # Store nbin as string, since it can be None - hdf5_group.attrs['nbin'] = str(self.config["nbin"]) - hdf5_group.attrs['binbounds'] = str(self.config["binbounds"]) - - #MR FIXME: why not "return None" as happens everywhere else? + hdf5_group.attrs['binbounds'] = str(self._binbounds) + hdf5_group.attrs['distribution_strategy'] = \ + self._pindex.distribution_strategy + return { 'harmonic_partner': self.harmonic_partner, - 'pindex': self.pindex, - 'k_array': self.k_array } @classmethod def _from_hdf5(cls, hdf5_group, repository): - # make an empty PowerSpace object - new_ps = EmptyPowerSpace() - # reset class - new_ps.__class__ = cls - # call instructor so that classes are properly setup - super(PowerSpace, new_ps).__init__() - # set all values - new_ps._harmonic_partner = repository.get('harmonic_partner', - hdf5_group) - - new_ps._config = {} - new_ps._config['logarithmic'] = hdf5_group['logarithmic'][()] - exec("new_ps._config['nbin'] = " + hdf5_group.attrs['nbin']) - exec("new_ps._config['binbounds'] = " + hdf5_group.attrs['binbounds']) - - new_ps._pindex = repository.get('pindex', hdf5_group) - new_ps._kindex = hdf5_group['kindex'][:] - new_ps._rho = hdf5_group['rho'][:] - new_ps._pundex = hdf5_group['pundex'][:] - new_ps._k_array = repository.get('k_array', hdf5_group) - new_ps._ignore_for_hash += ['_pindex', '_kindex', '_rho', '_pundex', - '_k_array'] - - return new_ps - - -class EmptyPowerSpace(PowerSpace): - def __init__(self): - pass + hp = repository.get('harmonic_partner', hdf5_group) + exec("bb = " + hdf5_group.attrs['binbounds']) + ds = hdf5_group.attrs['distribution_strategy'] + return PowerSpace(hp, ds, binbounds=bb) diff --git a/nifty/spaces/rg_space/rg_space.py b/nifty/spaces/rg_space/rg_space.py index d9109607645fce4094e767bbf3e03c73ebe9d1e7..2c2ae67d5cff890a719165e440487d7699ff49d4 100644 --- a/nifty/spaces/rg_space/rg_space.py +++ b/nifty/spaces/rg_space/rg_space.py @@ -249,6 +249,36 @@ class RGSpace(Space): dists = np.sqrt(dists) return dists + def get_unique_distances(self): + dimensions = len(self.shape) + if dimensions == 1: # extra easy + maxdist = self.shape[0]//2 + return np.arange(maxdist+1, dtype=np.float64) * self.distances[0] + if np.all(self.distances == self.distances[0]): # shortcut + maxdist = np.asarray(self.shape)//2 + tmp = np.sum(maxdist*maxdist) + tmp = np.zeros(tmp+1, dtype=np.bool) + t2 = np.arange(maxdist[0]+1, dtype=np.int64) + t2 *= t2 + for i in range(1, dimensions): + t3 = np.arange(maxdist[i]+1, dtype=np.int64) + t3 *= t3 + t2 = np.add.outer(t2, t3) + tmp[t2] = True + return np.sqrt(np.nonzero(tmp)[0])*self.distances[0] + else: # do it the hard way + tmp = self.get_distance_array('not').unique() # expensive! + tol = 1e-12*tmp[-1] + # remove all points that are closer than tol to their right + # neighbors. + # I'm appending the last value*2 to the array to treat the + # rightmost point correctly. + return tmp[np.diff(np.r_[tmp, 2*tmp[-1]]) > tol] + + def get_natural_binbounds(self): + tmp = self.get_unique_distances() + return 0.5*(tmp[:-1]+tmp[1:]) + def get_fft_smoothing_kernel_function(self, sigma): return lambda x: np.exp(-2. * np.pi*np.pi * x*x * sigma*sigma) diff --git a/nifty/spaces/space/space.py b/nifty/spaces/space/space.py index 73421ad317d344c024e5dae204e9b35a1baa6cac..da60dde377e7eaee6aace767b8d449a2a76220a3 100644 --- a/nifty/spaces/space/space.py +++ b/nifty/spaces/space/space.py @@ -117,6 +117,20 @@ class Space(DomainObject): raise NotImplementedError( "There is no generic distance structure for Space base class.") + def get_unique_distances(self): + raise NotImplementedError + + def get_natural_binbounds(self): + """ The boundaries for natural power spectrum binning. + + Returns + ------- + distributed_data_object + A numpy array containing the binbounds + + """ + raise NotImplementedError + def get_fft_smoothing_kernel_function(self, sigma): """ This method returns a smoothing kernel function. diff --git a/test/test_operators/test_fft_operator.py b/test/test_operators/test_fft_operator.py index a43bbea45e91cfc05976e81abe7fbf8858280b75..48122b9d95657311824996886d99879180078054 100644 --- a/test/test_operators/test_fft_operator.py +++ b/test/test_operators/test_fft_operator.py @@ -78,10 +78,13 @@ class FFTOperatorTests(unittest.TestCase): b = RGSpace(dim1, zerocenter=zc2, distances=1./(dim1*d), harmonic=True) fft = FFTOperator(domain=a, target=b, domain_dtype=itp, target_dtype=_harmonic_type(itp), module=module) + np.random.seed(16) inp = Field.from_random(domain=a, random_type='normal', std=7, mean=3, dtype=itp) out = fft.adjoint_times(fft.times(inp)) - assert_allclose(inp.val, out.val, rtol=tol, atol=tol) + assert_allclose(inp.val.get_full_data(), + out.val.get_full_data(), + rtol=tol, atol=tol) @expand(product(["numpy", "fftw", "fftw_mpi"], [10, 11], [9, 12], [False, True], diff --git a/test/test_spaces/test_power_space.py b/test/test_spaces/test_power_space.py index 237055b9ef62c3c7e53f345e44474ee9190e6746..4d75ad467e6e01000689f1195dad24769466dcc1 100644 --- a/test/test_spaces/test_power_space.py +++ b/test/test_spaces/test_power_space.py @@ -47,14 +47,12 @@ HARMONIC_SPACES = [RGSpace((8,), harmonic=True), #Try all sensible kinds of combinations of spaces, distributuion strategy and #binning parameters -_maybe_fftw = ["fftw"] if ('pyfftw' in gdi) else [] - CONSISTENCY_CONFIGS_IMPLICIT = product(HARMONIC_SPACES, ["not", "equal", "fftw"], [None], [None, 3, 4], [True, False]) CONSISTENCY_CONFIGS_EXPLICIT = product(HARMONIC_SPACES, ["not", "equal", "fftw"], - [[0., 1.3]], [None], [False]) + [[0., 1.3]], [None], [None]) CONSISTENCY_CONFIGS = chain(CONSISTENCY_CONFIGS_IMPLICIT, CONSISTENCY_CONFIGS_EXPLICIT) @@ -63,18 +61,16 @@ CONSISTENCY_CONFIGS = chain(CONSISTENCY_CONFIGS_IMPLICIT, CONSTRUCTOR_CONFIGS = [ [1, 'not', False, None, None, {'error': ValueError}], [RGSpace((8,)), 'not', False, None, None, {'error': ValueError}], - [RGSpace((8,), harmonic=True), 'not', False, None, None, { + [RGSpace((8,), harmonic=True), 'not', None, None, None, { 'harmonic': True, 'shape': (5,), 'dim': 5, 'total_volume': 8.0, 'harmonic_partner': RGSpace((8,), harmonic=True), - 'config': {'logarithmic': False, 'nbin': None, 'binbounds': None}, + 'binbounds': None, 'pindex': distributed_data_object([0, 1, 2, 3, 4, 3, 2, 1]), 'kindex': np.array([0., 1., 2., 3., 4.]), 'rho': np.array([1, 2, 2, 2, 1]), - 'pundex': np.array([0, 1, 2, 3, 4]), - 'k_array': np.array([0., 1., 2., 3., 4., 3., 2., 1.]), }], [RGSpace((8,), harmonic=True), 'not', True, None, None, { 'harmonic': True, @@ -82,13 +78,10 @@ CONSTRUCTOR_CONFIGS = [ 'dim': 2, 'total_volume': 8.0, 'harmonic_partner': RGSpace((8,), harmonic=True), - 'config': {'logarithmic': True, 'nbin': None, 'binbounds': None}, + 'binbounds': (0.70710678118654757,), 'pindex': distributed_data_object([0, 1, 1, 1, 1, 1, 1, 1]), 'kindex': np.array([0., 2.28571429]), 'rho': np.array([1, 7]), - 'pundex': np.array([0, 1]), - 'k_array': np.array([0., 2.28571429, 2.28571429, 2.28571429, - 2.28571429, 2.28571429, 2.28571429, 2.28571429]), }], ] @@ -121,12 +114,10 @@ def get_weight_configs(): class PowerSpaceInterfaceTest(unittest.TestCase): @expand([ ['harmonic_partner', Space], - ['config', dict], + ['binbounds', type(None)], ['pindex', distributed_data_object], ['kindex', np.ndarray], ['rho', np.ndarray], - ['pundex', np.ndarray], - ['k_array', distributed_data_object], ]) def test_property_ret_type(self, attribute, expected_type): r = RGSpace((4, 4), harmonic=True) @@ -136,23 +127,8 @@ class PowerSpaceInterfaceTest(unittest.TestCase): class PowerSpaceConsistencyCheck(unittest.TestCase): @expand(CONSISTENCY_CONFIGS) - def test_pipundexInversion(self, harmonic_partner, distribution_strategy, - binbounds, nbin, logarithmic): - if distribution_strategy == "fftw": - if not hasattr(gdi.get('fftw'), 'FFTW_MPI'): - raise SkipTest - p = PowerSpace(harmonic_partner=harmonic_partner, - distribution_strategy=distribution_strategy, - logarithmic=logarithmic, nbin=nbin, - binbounds=binbounds) - assert_equal(p.pindex.flatten().get_full_data()[p.pundex], - np.arange(p.dim), - err_msg='pundex is not right-inverse of pindex!') - - @expand(CONSISTENCY_CONFIGS) - def test_rhopindexConsistency(self, harmonic_partner, - distribution_strategy, binbounds, nbin, - logarithmic): + def test_rhopindexConsistency(self, harmonic_partner, distribution_strategy, + binbounds, nbin, logarithmic): if distribution_strategy == "fftw": if not hasattr(gdi.get('fftw'), 'FFTW_MPI'): raise SkipTest @@ -160,8 +136,6 @@ class PowerSpaceConsistencyCheck(unittest.TestCase): distribution_strategy=distribution_strategy, logarithmic=logarithmic, nbin=nbin, binbounds=binbounds) - assert_equal(p.pindex.flatten()[p.pundex],np.arange(p.dim), - err_msg='pundex is not right-inverse of pindex!') assert_equal(p.pindex.flatten().bincount(), p.rho, err_msg='rho is not equal to pindex degeneracy') @@ -173,7 +147,6 @@ class PowerSpaceFunctionalityTest(unittest.TestCase): if distribution_strategy == "fftw": if not hasattr(gdi.get('fftw'), 'FFTW_MPI'): raise SkipTest - raise SkipTest if 'error' in expected: with assert_raises(expected['error']): PowerSpace(harmonic_partner=harmonic_partner,