diff --git a/demos/wiener_filter.py b/demos/wiener_filter.py index b9d432b46c40bf1d5779120ac8e1b2ffa9081022..53a73e10daf5c228afb2087a7a0df50248e640fb 100644 --- a/demos/wiener_filter.py +++ b/demos/wiener_filter.py @@ -11,16 +11,36 @@ rank = comm.rank if __name__ == "__main__": distribution_strategy = 'not' - + + #Setting up physical constants + #total length of Interval or Volume the field lives on, e.g. in meters + L = 2. + #typical distance over which the field is correlated (in same unit as L) + correlation_length = 0.1 + #variance of field in position space sqrt(<|s_x|^2>) (in unit of s) + field_variance = 2. + #smoothing length that response (in same unit as L) + response_sigma = 0.1 + + #defining resolution (pixels per dimension) + N_pixels = 512 + + #Setting up derived constants + k_0 = 1./correlation_length + #note that field_variance**2 = a*k_0/4. for this analytic form of power + #spectrum + a = field_variance**2/k_0*4. + pow_spec = (lambda k: a / (1 + k/k_0) ** 4) + pixel_width = L/N_pixels + # Setting up the geometry - s_space = RGSpace([512, 512]) + s_space = RGSpace([N_pixels, N_pixels], distances = pixel_width) fft = FFTOperator(s_space) h_space = fft.target[0] p_space = PowerSpace(h_space, distribution_strategy=distribution_strategy) # Creating the mock data - pow_spec = (lambda k: 42 / (k + 1) ** 3) S = create_power_operator(h_space, power_spectrum=pow_spec, distribution_strategy=distribution_strategy) @@ -30,7 +50,7 @@ if __name__ == "__main__": sh = sp.power_synthesize(real_signal=True) ss = fft.inverse_times(sh) - R = SmoothingOperator(s_space, sigma=0.1) + R = SmoothingOperator(s_space, sigma=response_sigma) signal_to_noise = 1 N = DiagonalOperator(s_space, diagonal=ss.var()/signal_to_noise, bare=True) diff --git a/demos/wiener_filter_harmonic.py b/demos/wiener_filter_harmonic.py new file mode 100644 index 0000000000000000000000000000000000000000..692f0a6479d3b26b0b21ed67cddf924c1b04bb5f --- /dev/null +++ b/demos/wiener_filter_harmonic.py @@ -0,0 +1,138 @@ +from nifty import * +from mpi4py import MPI +import plotly.offline as py +import plotly.graph_objs as go + + +comm = MPI.COMM_WORLD +rank = comm.rank + + +def plot_maps(x, name): + + trace = [None]*len(x) + + keys = x.keys() + field = x[keys[0]] + domain = field.domain[0] + shape = len(domain.shape) + max_n = domain.shape[0]*domain.distances[0] + step = domain.distances[0] + x_axis = np.arange(0, max_n, step) + + if shape == 1: + for ii in xrange(len(x)): + trace[ii] = go.Scatter(x= x_axis, y=x[keys[ii]].val.get_full_data(), name=keys[ii]) + fig = go.Figure(data=trace) + + py.plot(fig, filename=name) + + elif shape == 2: + for ii in xrange(len(x)): + py.plot([go.Heatmap(z=x[keys[ii]].val.get_full_data())], filename=keys[ii]) + else: + raise TypeError("Only 1D and 2D field plots are supported") + +def plot_power(x, name): + + layout = go.Layout( + xaxis=dict( + type='log', + autorange=True + ), + yaxis=dict( + type='log', + autorange=True + ) + ) + + trace = [None]*len(x) + + keys = x.keys() + field = x[keys[0]] + domain = field.domain[0] + x_axis = domain.kindex + + for ii in xrange(len(x)): + trace[ii] = go.Scatter(x= x_axis, y=x[keys[ii]].val.get_full_data(), name=keys[ii]) + + fig = go.Figure(data=trace, layout=layout) + py.plot(fig, filename=name) + +np.random.seed(42) + + + +if __name__ == "__main__": + + distribution_strategy = 'not' + + # setting spaces + npix = np.array([500]) # number of pixels + total_volume = 1. # total length + + # setting signal parameters + lambda_s = .05 # signal correlation length + sigma_s = 10. # signal variance + + + #setting response operator parameters + length_convolution = .025 + exposure = 1. + + # calculating parameters + k_0 = 4. / (2 * np.pi * lambda_s) + a_s = sigma_s ** 2. * lambda_s * total_volume + + # creation of spaces + # x1 = RGSpace([npix,npix], distances=total_volume / npix, + # zerocenter=False) + # k1 = RGRGTransformation.get_codomain(x1) + + x1 = HPSpace(64) + k1 = HPLMTransformation.get_codomain(x1) + p1 = PowerSpace(harmonic_partner=k1, logarithmic=False) + + # creating Power Operator with given spectrum + spec = (lambda k: a_s / (1 + (k / k_0) ** 2) ** 2) + p_field = Field(p1, val=spec) + S_op = create_power_operator(k1, spec) + + # creating FFT-Operator and Response-Operator with Gaussian convolution + # adjust dtype_target probperly + Fft_op = FFTOperator(domain=x1, target=k1, + domain_dtype=np.float64, + target_dtype=np.float64) + R_op = ResponseOperator(x1, sigma=[length_convolution], + exposure=[exposure]) + + # drawing a random field + sk = p_field.power_synthesize(real_power=True, mean=0.) + s = Fft_op.adjoint_times(sk) + + signal_to_noise = 1 + N_op = DiagonalOperator(R_op.target, diagonal=s.var()/signal_to_noise, bare=True) + n = Field.from_random(domain=R_op.target, + random_type='normal', + std=s.std()/np.sqrt(signal_to_noise), + mean=0) + + d = R_op(s) + n + + # Wiener filter + j = Fft_op.times(R_op.adjoint_times(N_op.inverse_times(d))) + D = HarmonicPropagatorOperator(S=S_op, N=N_op, R=R_op) + + mk = D(j) + + m = Fft_op.adjoint_times(mk) + + # z={} + # z["signal"] = s + # z["reconstructed_map"] = m + # z["data"] = d + # z["lambda"] = R_op(s) + # + # plot_maps(z, "Wiener_filter.html") + + diff --git a/demos/wiener_filter_unit.py b/demos/wiener_filter_unit.py new file mode 100644 index 0000000000000000000000000000000000000000..2065626903e52a9e57dd4c2a6ef5613982a50552 --- /dev/null +++ b/demos/wiener_filter_unit.py @@ -0,0 +1,130 @@ +from nifty import * +from mpi4py import MPI +import plotly.offline as py +import plotly.graph_objs as go + + +comm = MPI.COMM_WORLD +rank = comm.rank + + +def plot_maps(x, name): + + trace = [None]*len(x) + + keys = x.keys() + field = x[keys[0]] + domain = field.domain[0] + shape = len(domain.shape) + max_n = domain.shape[0]*domain.distances[0] + step = domain.distances[0] + x_axis = np.arange(0, max_n, step) + + if shape == 1: + for ii in xrange(len(x)): + trace[ii] = go.Scatter(x= x_axis, y=x[keys[ii]].val.get_full_data(), name=keys[ii]) + fig = go.Figure(data=trace) + + py.plot(fig, filename=name) + + elif shape == 2: + for ii in xrange(len(x)): + py.plot([go.Heatmap(z=x[keys[ii]].val.get_full_data())], filename=keys[ii]) + else: + raise TypeError("Only 1D and 2D field plots are supported") + +def plot_power(x, name): + + layout = go.Layout( + xaxis=dict( + type='log', + autorange=True + ), + yaxis=dict( + type='log', + autorange=True + ) + ) + + trace = [None]*len(x) + + keys = x.keys() + field = x[keys[0]] + domain = field.domain[0] + x_axis = domain.kindex + + for ii in xrange(len(x)): + trace[ii] = go.Scatter(x= x_axis, y=x[keys[ii]].val.get_full_data(), name=keys[ii]) + + fig = go.Figure(data=trace, layout=layout) + py.plot(fig, filename=name) + +np.random.seed(42) + +if __name__ == "__main__": + + distribution_strategy = 'not' + + # setting spaces + npix = np.array([500]) # number of pixels + total_volume = 1. # total length + + # setting signal parameters + lambda_s = .05 # signal correlation length + sigma_s = 10. # signal variance + + + #setting response operator parameters + length_convolution = .025 + exposure = 1. + + # calculating parameters + k_0 = 4. / (2 * np.pi * lambda_s) + a_s = sigma_s ** 2. * lambda_s * total_volume + + # creation of spaces + x1 = RGSpace(npix, distances=total_volume / npix, + zerocenter=False) + k1 = RGRGTransformation.get_codomain(x1) + p1 = PowerSpace(harmonic_domain=k1, log=False) + + # creating Power Operator with given spectrum + spec = (lambda k: a_s / (1 + (k / k_0) ** 2) ** 2) + p_field = Field(p1, val=spec) + S_op = create_power_operator(k1, spec) + + # creating FFT-Operator and Response-Operator with Gaussian convolution + Fft_op = FFTOperator(domain=x1, target=k1, + domain_dtype=np.float64, + target_dtype=np.complex128) + R_op = ResponseOperator(x1, sigma=[length_convolution], + exposure=[exposure]) + + # drawing a random field + sk = p_field.power_synthesize(real_signal=True, mean=0.) + s = Fft_op.inverse_times(sk) + + signal_to_noise = 1 + N_op = DiagonalOperator(R_op.target, diagonal=s.var()/signal_to_noise, bare=True) + n = Field.from_random(domain=R_op.target, + random_type='normal', + std=s.std()/np.sqrt(signal_to_noise), + mean=0) + + d = R_op(s) + n + + # Wiener filter + j = R_op.adjoint_times(N_op.inverse_times(d)) + D = PropagatorOperator(S=S_op, N=N_op, R=R_op) + + m = D(j) + + z={} + z["signal"] = s + z["reconstructed_map"] = m + z["data"] = d + z["lambda"] = R_op(s) + + plot_maps(z, "Wiener_filter.html") + + diff --git a/nifty/operators/__init__.py b/nifty/operators/__init__.py index 2621c7cf0294c81d6272e28c048b0a7c81ef53da..1a8940dc1dd33b1193e00fa0341add77202e6e38 100644 --- a/nifty/operators/__init__.py +++ b/nifty/operators/__init__.py @@ -34,4 +34,8 @@ from projection_operator import ProjectionOperator from propagator_operator import PropagatorOperator +from propagator_operator import HarmonicPropagatorOperator + from composed_operator import ComposedOperator + +from response_operator import ResponseOperator diff --git a/nifty/operators/composed_operator/composed_operator.py b/nifty/operators/composed_operator/composed_operator.py index 9557c3c3fcb93489c82c7827f6b421355dd9909a..b2e3ecd94a8a0d42e2e04399132e5ac848b35686 100644 --- a/nifty/operators/composed_operator/composed_operator.py +++ b/nifty/operators/composed_operator/composed_operator.py @@ -20,6 +20,59 @@ from nifty.operators.linear_operator import LinearOperator class ComposedOperator(LinearOperator): + """ NIFTY class for composed operators. + + The NIFTY composed operator class combines multiple linear operators. + + Parameters + ---------- + operators : tuple of NIFTy Operators + The tuple of LinearOperators. + + Attributes + ---------- + domain : tuple of DomainObjects, i.e. Spaces and FieldTypes + The NIFTy.space in which the operator is defined. + target : tuple of DomainObjects, i.e. Spaces and FieldTypes + The NIFTy.space in which the outcome of the operator lives + + Raises + ------ + TypeError + Raised if + * an element of the operator list is not an instance of the + LinearOperator-baseclass. + + Notes + ----- + Very usefull in case one has to transform a Field living over a product + space (see example below). + + Examples + -------- + Minimal example of transforming a Field living on two domains into its + harmonic space. + + >>> x1 = RGSpace(5) + >>> x2 = RGSpace(10) + >>> k1 = RGRGTransformation.get_codomain(x1) + >>> k2 = RGRGTransformation.get_codomain(x2) + >>> FFT1 = FFTOperator(domain=x1, target=k1, + domain_dtype=np.float64, target_dtype=np.complex128) + >>> FFT2 = FFTOperator(domain=x2, target=k2, + domain_dtype=np.float64, target_dtype=np.complex128) + >>> FFT = ComposedOperator((FFT1, FFT2) + >>> f = Field.from_random('normal', domain=(x1,x2)) + >>> FFT.times(f) + + See Also + -------- + EndomorphicOperator, ProjectionOperator, + DiagonalOperator, SmoothingOperator, ResponseOperator, + PropagatorOperator, ComposedOperator + + """ + # ---Overwritten properties and methods--- def __init__(self, operators, default_spaces=None): super(ComposedOperator, self).__init__(default_spaces) diff --git a/nifty/operators/diagonal_operator/diagonal_operator.py b/nifty/operators/diagonal_operator/diagonal_operator.py index 84dd3e48e6a494db81bee7a1908ae519b95a0aa4..dec42da42dc9a788b0d64912d9a5224474f346a5 100644 --- a/nifty/operators/diagonal_operator/diagonal_operator.py +++ b/nifty/operators/diagonal_operator/diagonal_operator.py @@ -28,6 +28,63 @@ from nifty.operators.endomorphic_operator import EndomorphicOperator class DiagonalOperator(EndomorphicOperator): + """ NIFTY class for diagonal operators. + + The NIFTY DiagonalOperator class is a subclass derived from the + EndomorphicOperator. It multiplies an input field pixel-wise with its + diagonal. + + + Parameters + ---------- + domain : tuple of DomainObjects, i.e. Spaces and FieldTypes + The domain on which the Operator's input Field lives. + diagonal : {scalar, list, array, Field, d2o-object} + The diagonal entries of the operator. + bare : boolean + Indicates whether the input for the diagonal is bare or not + (default: False). + copy : boolean + Internal copy of the diagonal (default: True) + distribution_strategy : string + setting the prober distribution_strategy of the + diagonal (default : None). In case diagonal is d2o-object or Field, + their distribution_strategy is used as a fallback. + + Attributes + ---------- + distribution_strategy : string + Defines the distribution_strategy of the distributed_data_object + in which the diagonal entries are stored in. + + Raises + ------ + + Notes + ----- + The ambiguity of bare or non-bare diagonal entries is based on the choice + of a matrix representation of the operator in question. The naive choice + of absorbing the volume weights into the matrix leads to a matrix-vector + calculus with the non-bare entries which seems intuitive, though. + The choice of keeping matrix entries and volume weights separate + deals with the bare entries that allow for correct interpretation + of the matrix entries; e.g., as variance in case of an covariance operator. + + Examples + -------- + >>> x_space = RGSpace(5) + >>> D = DiagonalOperator(x_space, diagonal=[1., 3., 2., 4., 6.]) + >>> f = Field(x_space, val=2.) + >>> res = D.times(f) + >>> res.val + <distributed_data_object> + array([ 2., 6., 4., 8., 12.]) + + See Also + -------- + EndomorphicOperator + + """ # ---Overwritten properties and methods--- @@ -64,6 +121,21 @@ class DiagonalOperator(EndomorphicOperator): operation=lambda z: z.adjoint().__rdiv__) def diagonal(self, bare=False, copy=True): + """ Returns the diagonal of the Operator. + + Parameters + ---------- + bare : boolean + Whether the returned Field values should be bare or not. + copy : boolean + Whether the returned Field should be copied or not. + + Returns + ------- + out : Field + The diagonal of the Operator. + + """ if bare: diagonal = self._diagonal.weight(power=-1) elif copy: @@ -73,25 +145,100 @@ class DiagonalOperator(EndomorphicOperator): return diagonal def inverse_diagonal(self, bare=False): + """ Returns the inverse-diagonal of the operator. + + Parameters + ---------- + bare : boolean + Whether the returned Field values should be bare or not. + + Returns + ------- + out : Field + The inverse of the diagonal of the Operator. + + """ return 1./self.diagonal(bare=bare, copy=False) def trace(self, bare=False): + """ Returns the trace the operator. + + Parameters + ---------- + bare : boolean + Whether the returned Field values should be bare or not. + + Returns + ------- + out : scalar + The trace of the Operator. + + """ return self.diagonal(bare=bare, copy=False).sum() def inverse_trace(self, bare=False): + """ Returns the inverse-trace of the operator. + + Parameters + ---------- + bare : boolean + Whether the returned Field values should be bare or not. + + Returns + ------- + out : scalar + The inverse of the trace of the Operator. + + """ return self.inverse_diagonal(bare=bare).sum() def trace_log(self): + """ Returns the trave-log of the operator. + + Returns + ------- + out : scalar + the trace of the logarithm of the Operator. + + """ log_diagonal = nifty_log(self.diagonal(copy=False)) return log_diagonal.sum() def determinant(self): + """ Returns the determinant of the operator. + + Returns + ------- + out : scalar + out : scalar + the determinant of the Operator + + """ + return self.diagonal(copy=False).val.prod() def inverse_determinant(self): + """ Returns the inverse-determinant of the operator. + + Returns + ------- + out : scalar + the inverse-determinant of the Operator + + """ + return 1/self.determinant() def log_determinant(self): + """ Returns the log-eterminant of the operator. + + Returns + ------- + out : scalar + the log-determinant of the Operator + + """ + return np.log(self.determinant()) # ---Mandatory properties and methods--- @@ -117,6 +264,17 @@ class DiagonalOperator(EndomorphicOperator): @property def distribution_strategy(self): + """ + distribution_strategy : string + Defines the way how the diagonal operator is distributed + among the nodes. Available distribution_strategies are: + 'fftw', 'equal' and 'not'. + + Notes : + https://arxiv.org/abs/1606.05385 + + """ + return self._distribution_strategy def _parse_distribution_strategy(self, distribution_strategy, val): @@ -134,6 +292,20 @@ class DiagonalOperator(EndomorphicOperator): return distribution_strategy def set_diagonal(self, diagonal, bare=False, copy=True): + """ Sets the diagonal of the Operator. + + Parameters + ---------- + diagonal : {scalar, list, array, Field, d2o-object} + The diagonal entries of the operator. + bare : boolean + Indicates whether the input for the diagonal is bare or not + (default: False). + copy : boolean + Specifies if a copy of the input shall be made (default: True). + + """ + # use the casting functionality from Field to process `diagonal` f = Field(domain=self.domain, val=diagonal, diff --git a/nifty/operators/endomorphic_operator/endomorphic_operator.py b/nifty/operators/endomorphic_operator/endomorphic_operator.py index 0d9cc49cafc55636fdcb3776c572a3130e84a045..1340708be765601e6dfcffa274f523292b330e39 100644 --- a/nifty/operators/endomorphic_operator/endomorphic_operator.py +++ b/nifty/operators/endomorphic_operator/endomorphic_operator.py @@ -22,6 +22,45 @@ from nifty.operators.linear_operator import LinearOperator class EndomorphicOperator(LinearOperator): + """ NIFTY class for endomorphic operators. + + The NIFTY EndomorphicOperator class is a class derived from the + LinearOperator. By definition, domain and target are the same in + EndomorphicOperator. + + Parameters + ---------- + #TODO: Copy Parameters from LinearOperator + + Attributes + ---------- + 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. + self_adjoint : boolean + Indicates whether the operator is self_adjoint or not. + + Raises + ------ + NotImplementedError + Raised if + * self_adjoint is not defined + + Notes + ----- + + Examples + -------- + + + See Also + -------- + DiagonalOperator, SmoothingOperator, + PropagatorOperator, ProjectionOperator + + """ # ---Overwritten properties and methods--- @@ -67,4 +106,7 @@ class EndomorphicOperator(LinearOperator): @abc.abstractproperty def self_adjoint(self): + """ States whether the Operator is self_adjoint or not. + """ + raise NotImplementedError diff --git a/nifty/operators/invertible_operator_mixin/invertible_operator_mixin.py b/nifty/operators/invertible_operator_mixin/invertible_operator_mixin.py index e4e50e8d6a28696211d4206e01a6c9a0143dfcad..4786a4f9375c7e56e6b52d38005263f2b6f69057 100644 --- a/nifty/operators/invertible_operator_mixin/invertible_operator_mixin.py +++ b/nifty/operators/invertible_operator_mixin/invertible_operator_mixin.py @@ -22,6 +22,43 @@ from nifty.field import Field class InvertibleOperatorMixin(object): + """ Mixin class to invert implicit defined operators. + + To invert the application of a given implicitly defined operator on a + field, this class gives the necessary functionality. Inheriting + functionality from this class provides the derived class with the inverse + to the given implicitly definied application of the operator on a field. + (e.g. .inverse_times vs. .times and + .adjoint_times vs. .adjoint_inverse_times) + + Parameters + ---------- + inverter : Inverter + An instance of an Inverter class. + (default: ConjugateGradient) + + preconditioner : LinearOperator + Preconditions the minimizaion problem + + Attributes + ---------- + + Raises + ------ + + Notes + ----- + + Examples + -------- + The PropagatorOperator inherits from InvertibleOperatorMixin. + + See Also + -------- + PropagatorOperator + + """ + def __init__(self, inverter=None, preconditioner=None, *args, **kwargs): self.__preconditioner = preconditioner if inverter is not None: diff --git a/nifty/operators/linear_operator/linear_operator.py b/nifty/operators/linear_operator/linear_operator.py index 24d231a48494e5f6b7f7a3fca451170b83f72060..bf0160ba964562ca2d80fceb4da95f8491642cd9 100644 --- a/nifty/operators/linear_operator/linear_operator.py +++ b/nifty/operators/linear_operator/linear_operator.py @@ -25,6 +25,51 @@ import nifty.nifty_utilities as utilities class LinearOperator(Loggable, object): + """NIFTY base class for linear operators. + + The base NIFTY operator class is an abstract class from which + other specific operator subclasses, including those preimplemented + in NIFTY (e.g. the EndomorphicOperator, ProjectionOperator, + DiagonalOperator, SmoothingOperator, ResponseOperator, + PropagatorOperator, ComposedOperator) are derived. + + Parameters + ---------- + default_spaces : tuple of ints *optional* + Defines on which space(s) of a given field the Operator acts by + default (default: None) + + Attributes + ---------- + 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 Operators result lives. + unitary : boolean + Indicates whether the Operator is unitary or not. + + Raises + ------ + NotImplementedError + Raised if + * domain is not defined + * target is not defined + * unitary is not set to (True/False) + + Notes + ----- + All Operators wihtin NIFTy are linear and must therefore be a subclasses of + the LinearOperator. A LinearOperator must have the attributes domain, + target and unitary to be properly defined. + + See Also + -------- + EndomorphicOperator, ProjectionOperator, + DiagonalOperator, SmoothingOperator, ResponseOperator, + PropagatorOperator, ComposedOperator + + """ + __metaclass__ = NiftyMeta def __init__(self, default_spaces=None): @@ -35,14 +80,38 @@ class LinearOperator(Loggable, object): @abc.abstractproperty def domain(self): + """ + domain : tuple of DomainObjects, i.e. Spaces and FieldTypes + The domain on which the Operator's input Field lives. + Every Operator which inherits from the abstract LinearOperator + base class must have this attribute. + + """ + raise NotImplementedError @abc.abstractproperty def target(self): + """ + target : tuple of DomainObjects, i.e. Spaces and FieldTypes + The domain on which the Operator's output Field lives. + Every Operator which inherits from the abstract LinearOperator + base class must have this attribute. + + """ + raise NotImplementedError @abc.abstractproperty def unitary(self): + """ + unitary : boolean + States whether the Operator is unitary or not. + Every Operator which inherits from the abstract LinearOperator + base class must have this attribute. + + """ + raise NotImplementedError @property @@ -56,55 +125,132 @@ class LinearOperator(Loggable, object): def __call__(self, *args, **kwargs): return self.times(*args, **kwargs) - def times(self, x, spaces=None, **kwargs): - spaces = self._check_input_compatibility(x, spaces) + def times(self, x, spaces=None): + """ Applies the Operator to a given Field. + + Operator and Field have to live over the same domain. - y = self._times(x, spaces, **kwargs) + Parameters + ---------- + x : Field + The input Field. + spaces : tuple of ints + Defines on which space(s) of the given Field the Operator acts. + + Returns + ------- + out : Field + The processed Field living on the target-domain. + + """ + + spaces = self._check_input_compatibility(x, spaces) + y = self._times(x, spaces) return y - def inverse_times(self, x, spaces=None, **kwargs): + def inverse_times(self, x, spaces=None): + """ Applies the inverse-Operator to a given Field. + + Operator and Field have to live over the same domain. + + Parameters + ---------- + x : Field + The input Field. + spaces : tuple of ints + Defines on which space(s) of the given Field the Operator acts. + + Returns + ------- + out : Field + The processed Field living on the target-domain. + + """ + spaces = self._check_input_compatibility(x, spaces, inverse=True) try: - y = self._inverse_times(x, spaces, **kwargs) + y = self._inverse_times(x, spaces) except(NotImplementedError): if (self.unitary): - y = self._adjoint_times(x, spaces, **kwargs) + y = self._adjoint_times(x, spaces) else: raise return y - def adjoint_times(self, x, spaces=None, **kwargs): + def adjoint_times(self, x, spaces=None): + """ Applies the adjoint-Operator to a given Field. + + Operator and Field have to live over the same domain. + + Parameters + ---------- + x : Field + applies the Operator to the given Field + spaces : tuple of ints + defines on which space of the given Field the Operator acts + + Returns + ------- + out : Field + The processed Field living on the target-domain. + + """ + + if self.unitary: + return self.inverse_times(x, spaces) + spaces = self._check_input_compatibility(x, spaces, inverse=True) try: - y = self._adjoint_times(x, spaces, **kwargs) + y = self._adjoint_times(x, spaces) except(NotImplementedError): if (self.unitary): - y = self._inverse_times(x, spaces, **kwargs) + y = self._inverse_times(x, spaces) else: raise return y - # If the operator supports inverse() then the inverse adjoint is identical - # to the adjoint inverse. We provide both names for convenience. - def adjoint_inverse_times(self, x, spaces=None, **kwargs): + def adjoint_inverse_times(self, x, spaces=None): + """ Applies the adjoint-inverse Operator to a given Field. + + Operator and Field have to live over the same domain. + + Parameters + ---------- + x : Field + applies the Operator to the given Field + spaces : tuple of ints + defines on which space of the given Field the Operator acts + + Returns + ------- + out : Field + The processed Field living on the target-domain. + + Notes + ----- + If the operator has an `inverse` then the inverse adjoint is identical + to the adjoint inverse. We provide both names for convenience. + + See Also + -------- + + """ + spaces = self._check_input_compatibility(x, spaces) try: - y = self._adjoint_inverse_times(x, spaces, **kwargs) + y = self._adjoint_inverse_times(x, spaces) except(NotImplementedError): - try: - y = self._inverse_adjoint_times(x, spaces, **kwargs) - except(NotImplementedError): - if self.unitary: - y = self._times(x, spaces, **kwargs) - else: - raise + if self.unitary: + y = self._times(x, spaces) + else: + raise return y - def inverse_adjoint_times(self, x, spaces=None, **kwargs): - return adjoint_inverse_times(x, spaces, **kwargs) + def inverse_adjoint_times(self, x, spaces=None): + return self.adjoint_inverse_times(x, spaces) def _times(self, x, spaces): raise NotImplementedError( @@ -129,7 +275,7 @@ class LinearOperator(Loggable, object): def _check_input_compatibility(self, x, spaces, inverse=False): if not isinstance(x, Field): raise ValueError( - "supplied object is not a `nifty.Field`.") + "supplied object is not a `Field`.") if spaces is None: spaces = self.default_spaces diff --git a/nifty/operators/projection_operator/projection_operator.py b/nifty/operators/projection_operator/projection_operator.py index 2217d6aa248b3b2a846a91ef8a38531ffca6849e..9a51f363634dc7bf0c1112b3ae60ee0b74b785ce 100644 --- a/nifty/operators/projection_operator/projection_operator.py +++ b/nifty/operators/projection_operator/projection_operator.py @@ -24,6 +24,45 @@ from nifty.operators.endomorphic_operator import EndomorphicOperator class ProjectionOperator(EndomorphicOperator): + """ NIFTY class for projection operators. + + The NIFTY ProjectionOperator class is a class derived from the + EndomorphicOperator. + + Parameters + ---------- + projection_field : Field + Field on which the operator projects + + Attributes + ---------- + + Raises + ------ + TypeError + Raised if + * if projection_field is not a Field + + Notes + ----- + + + Examples + -------- + >>> x_space = RGSpace(5) + >>> f1 = Field(x_space, val=3.) + >>> f2 = Field(x_space, val=5.) + >>> P = ProjectionOperator(f1) + >>> res = P.times(f2) + >>> res.val + <distributed_data_object> + array([ 225., 225., 225., 225., 225.]) + + See Also + -------- + EndomorphicOperator + + """ # ---Overwritten properties and methods--- diff --git a/nifty/operators/propagator_operator/__init__.py b/nifty/operators/propagator_operator/__init__.py index 48672e0631900024d2cb9ea2aeb155cd5bc7d3e0..61d962c4299059adbe8f5fab63d8f7dd8a27239b 100644 --- a/nifty/operators/propagator_operator/__init__.py +++ b/nifty/operators/propagator_operator/__init__.py @@ -17,3 +17,4 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from propagator_operator import PropagatorOperator +from harmonic_propagator_operator import HarmonicPropagatorOperator \ No newline at end of file diff --git a/nifty/operators/propagator_operator/harmonic_propagator_operator.py b/nifty/operators/propagator_operator/harmonic_propagator_operator.py new file mode 100644 index 0000000000000000000000000000000000000000..3ad6501fe217cd4610ef7e1b8787aeb7f3eb4b0e --- /dev/null +++ b/nifty/operators/propagator_operator/harmonic_propagator_operator.py @@ -0,0 +1,144 @@ +# NIFTy +# Copyright (C) 2017 Theo Steininger +# +# Author: Theo Steininger +# +# 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/>. + +from nifty.operators import EndomorphicOperator,\ + FFTOperator,\ + InvertibleOperatorMixin + + +class HarmonicPropagatorOperator(InvertibleOperatorMixin, EndomorphicOperator): + """ NIFTY Harmonic Propagator Operator D. + + The propagator operator D, is known from the Wiener Filter. + Its inverse functional form might look like: + D = (S^(-1) + M)^(-1) + D = (S^(-1) + N^(-1))^(-1) + D = (S^(-1) + R^(\dagger) N^(-1) R)^(-1) + In contrast to the PropagatorOperator the inference is done in the + harmonic space. + + Parameters + ---------- + S : LinearOperator + Covariance of the signal prior. + M : LinearOperator + Likelihood contribution. + R : LinearOperator + Response operator translating signal to (noiseless) data. + N : LinearOperator + Covariance of the noise prior or the likelihood, respectively. + inverter : class to invert explicitly defined operators + (default:ConjugateGradient) + preconditioner : Field + numerical preconditioner to speed up convergence + + Attributes + ---------- + + Raises + ------ + ValueError + is raised if + * neither N nor M is given + + Notes + ----- + + Examples + -------- + + See Also + -------- + Scientific reference + https://arxiv.org/abs/0806.3474 + + """ + + # ---Overwritten properties and methods--- + + def __init__(self, S=None, M=None, R=None, N=None, inverter=None, + preconditioner=None): + """ + Sets the standard operator properties and `codomain`, `_A1`, `_A2`, + and `RN` if required. + + Parameters + ---------- + S : operator + Covariance of the signal prior. + M : operator + Likelihood contribution. + R : operator + Response operator translating signal to (noiseless) data. + N : operator + Covariance of the noise prior or the likelihood, respectively. + + """ + # infer domain, and target + # infer domain, and target + if M is not None: + self._codomain = M.domain + self._likelihood = M.times + + elif N is None: + raise ValueError("Either M or N must be given!") + + elif R is not None: + self._codomain = R.domain + self._likelihood = \ + lambda z: R.adjoint_times(N.inverse_times(R.times(z))) + else: + self._codomain = N.domain + self._likelihood = lambda z: N.inverse_times(z) + + self._domain = S.domain + self._S = S + self._fft_S = FFTOperator(self._domain, target=self._codomain) + + super(HarmonicPropagatorOperator, self).__init__(inverter=inverter, + preconditioner=preconditioner) + + # ---Mandatory properties and methods--- + + @property + def domain(self): + return self._domain + + @property + def self_adjoint(self): + return True + + @property + def unitary(self): + return False + + # ---Added properties and methods--- + def _likelihood_times(self, x, spaces=None): + transformed_x = self._fft_S.times(x, spaces=spaces) + y = self._likelihood(transformed_x) + transformed_y = self._fft_S.adjoint_times(y, spaces=spaces) + result = x.copy_empty() + result.set_val(transformed_y, copy=False) + return result + + def _inverse_times(self, x, spaces): + pre_result = self._S.inverse_times(x, spaces) + pre_result += self._likelihood_times(x) + result = x.copy_empty() + result.set_val(pre_result, copy=False) + return result diff --git a/nifty/operators/propagator_operator/propagator_operator.py b/nifty/operators/propagator_operator/propagator_operator.py index 506cce30b171102c6c954c321f87d3c310a9ff60..cb70e7b7cb9fbaa179517b8fe61abe0ac44ab1af 100644 --- a/nifty/operators/propagator_operator/propagator_operator.py +++ b/nifty/operators/propagator_operator/propagator_operator.py @@ -22,6 +22,59 @@ from nifty.operators import EndomorphicOperator,\ class PropagatorOperator(InvertibleOperatorMixin, EndomorphicOperator): + """ NIFTY Propagator Operator D. + + The propagator operator D, is known from the Wiener Filter. + Its inverse functional form might look like: + D = (S^(-1) + M)^(-1) + D = (S^(-1) + N^(-1))^(-1) + D = (S^(-1) + R^(\dagger) N^(-1) R)^(-1) + + Parameters + ---------- + S : LinearOperator + Covariance of the signal prior. + M : LinearOperator + Likelihood contribution. + R : LinearOperator + Response operator translating signal to (noiseless) data. + N : LinearOperator + Covariance of the noise prior or the likelihood, respectively. + inverter : class to invert explicitly defined operators + (default:ConjugateGradient) + preconditioner : Field + numerical preconditioner to speed up convergence + + Attributes + ---------- + + Raises + ------ + ValueError + is raised if + * neither N nor M is given + + Notes + ----- + + Examples + -------- + >>> x_space = RGSpace(4) + >>> k_space = RGRGTransformation.get_codomain(x_space) + >>> f = Field(x_space, val=[2, 4, 6, 8]) + >>> S = create_power_operator(k_space, spec=1) + >>> N = DiagonalOperaor(f.domain, diag=1) + >>> D = PropagatorOperator(S=S, N=N) # D^{-1} = S^{-1} + N^{-1} + >>> D(f).val + <distributed_data_object> + array([ 1., 2., 3., 4.] + + See Also + -------- + Scientific reference + https://arxiv.org/abs/0806.3474 + + """ # ---Overwritten properties and methods--- diff --git a/nifty/operators/response_operator/__init__.py b/nifty/operators/response_operator/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2ae4e4775341b0c8dde48b9b0ffde30da8e14abe --- /dev/null +++ b/nifty/operators/response_operator/__init__.py @@ -0,0 +1 @@ +from response_operator import ResponseOperator diff --git a/nifty/operators/response_operator/response_operator.py b/nifty/operators/response_operator/response_operator.py new file mode 100644 index 0000000000000000000000000000000000000000..f5ac8b05ab578ea34cf60918b2f9b675ecaa813b --- /dev/null +++ b/nifty/operators/response_operator/response_operator.py @@ -0,0 +1,121 @@ +import numpy as np +from nifty import Field,\ + FieldArray +from nifty.operators.linear_operator import LinearOperator +from nifty.operators.smoothing_operator import SmoothingOperator +from nifty.operators.composed_operator import ComposedOperator +from nifty.operators.diagonal_operator import DiagonalOperator + +class ResponseOperator(LinearOperator): + """ NIFTy ResponseOperator (example) + + This NIFTy ResponseOperator provides the user with an example how a + ResponseOperator can look like. It smoothes and exposes a field. The + outcome of the Operator is geometrically not ordered as typical data + set are. + + Parameters + ---------- + domain : tuple of DomainObjects, i.e. Spaces and FieldTypes + The domains on which the operator lives. Either one space or a list + of spaces + sigma : list(np.float) + Defines the smoothing length of the operator for each space it lives on + exposure : list(np.float) + Defines the exposure of the operator for each space it lives on + + + Attributes + ---------- + + Raises + ------ + ValueError: + raised if: + * len of sigma-list and exposure-list are not equal + + Notes + ----- + + Examples + -------- + >>> x1 = RGSpace(5) + >>> x2 = RGSpace(10) + >>> R = ResponseOperator(domain=(x1,x2), sigma=[.5, .25], + exposure=[2.,3.]) + >>> f = Field((x1,x2), val=4.) + >>> R.times(f) + <distributed_data_object> + array([[ 24., 24., 24., 24., 24., 24., 24., 24., 24., 24.], + [ 24., 24., 24., 24., 24., 24., 24., 24., 24., 24.], + [ 24., 24., 24., 24., 24., 24., 24., 24., 24., 24.], + [ 24., 24., 24., 24., 24., 24., 24., 24., 24., 24.], + [ 24., 24., 24., 24., 24., 24., 24., 24., 24., 24.]]) + + See Also + -------- + + """ + + def __init__(self, domain, + sigma=[1.], exposure=[1.], + default_spaces=None): + super(ResponseOperator, self).__init__(default_spaces) + + self._domain = self._parse_domain(domain) + + shapes = len(self._domain)*[None] + shape_target = [] + for ii in xrange(len(shapes)): + shapes[ii] = self._domain[ii].shape + shape_target = np.append(shape_target, self._domain[ii].shape) + + self._target = self._parse_domain(FieldArray(shape_target)) + self._sigma = sigma + self._exposure = exposure + + self._kernel = len(self._domain)*[None] + + for ii in xrange(len(self._kernel)): + self._kernel[ii] = SmoothingOperator(self._domain[ii], + sigma=self._sigma[ii]) + + self._composed_kernel = ComposedOperator(self._kernel) + + self._exposure_op = len(self._domain)*[None] + if len(self._exposure_op) != len(self._kernel): + raise ValueError("Definition of kernel and exposure do not suit " + "each other") + else: + for ii in xrange(len(self._exposure_op)): + self._exposure_op[ii] = DiagonalOperator( + self._domain[ii], + diagonal=self._exposure[ii]) + self._composed_exposure = ComposedOperator(self._exposure_op) + + @property + def domain(self): + return self._domain + + @property + def target(self): + return self._target + + @property + def unitary(self): + return False + + def _times(self, x, spaces): + res = self._composed_kernel.times(x, spaces) + res = self._composed_exposure.times(res, spaces) + # res = res.weight(power=1) + # removing geometric information + return Field(self._target, val=res.val) + + def _adjoint_times(self, x, spaces): + # setting correct spaces + res = Field(self.domain, val=x.val) + res = self._composed_exposure.adjoint_times(res, spaces) + res = res.weight(power=-1) + res = self._composed_kernel.adjoint_times(res, spaces) + return res diff --git a/nifty/operators/smoothing_operator/smoothing_operator.py b/nifty/operators/smoothing_operator/smoothing_operator.py index fe27a16e3e14fff42142bca530c5ae63f553a5b3..abd6ea3f0bbb05a5a1b713d194a4d2c7bb049e79 100644 --- a/nifty/operators/smoothing_operator/smoothing_operator.py +++ b/nifty/operators/smoothing_operator/smoothing_operator.py @@ -25,6 +25,56 @@ from d2o import STRATEGIES class SmoothingOperator(EndomorphicOperator): + """ NIFTY class for smoothing operators. + + The NIFTy SmoothingOperator smooths Fields, with a given kernel length. + Fields which are not living over a PowerSpace are smoothed + via a gaussian convolution. Fields living over the PowerSpace are directly + smoothed. + + Parameters + ---------- + domain : tuple of DomainObjects, i.e. Spaces and FieldTypes + The Space on which the operator acts + sigma : float + Sets the length of the Gaussian convolution kernel + log_distances : boolean + States whether the convolution happens on the logarithmic grid or not. + + Attributes + ---------- + sigma : float + Sets the length of the Gaussian convolution kernel + log_distances : boolean + States whether the convolution happens on the logarithmic grid or not. + + Raises + ------ + ValueError + Raised if + * the given domain inherits more than one space. The + SmoothingOperator acts only on one Space. + + Notes + ----- + + Examples + -------- + >>> x = RGSpace(5) + >>> S = SmoothingOperator(x, sigma=1.) + >>> f = Field(x, val=[1,2,3,4,5]) + >>> S.times(f).val + <distributed_data_object> + array([ 3., 3., 3., 3., 3.]) + + See Also + -------- + DiagonalOperator, SmoothingOperator, + PropagatorOperator, ProjectionOperator, + ComposedOperator + + """ + # ---Overwritten properties and methods--- def __init__(self, domain, sigma, log_distances=False, default_spaces=None): @@ -260,7 +310,7 @@ class SmoothingOperator(EndomorphicOperator): transformed_x.val.set_local_data(local_transformed_x, copy=False) - smoothed_x = Transformator.inverse_times(transformed_x, spaces=spaces) + smoothed_x = Transformator.adjoint_times(transformed_x, spaces=spaces) result = x.copy_empty() result.set_val(smoothed_x, copy=False) diff --git a/nifty/spaces/rg_space/rg_space.py b/nifty/spaces/rg_space/rg_space.py index cbce79be5e88828b4e4fd3421051b957615eb9dc..cb70c66452be8bc762b53ea91801ac077ee91304 100644 --- a/nifty/spaces/rg_space/rg_space.py +++ b/nifty/spaces/rg_space/rg_space.py @@ -53,19 +53,19 @@ class RGSpace(Space): shape : {int, numpy.ndarray} Number of grid points or numbers of gridpoints along each axis. zerocenter : {bool, numpy.ndarray}, *optional* - Whether x==0 (or k==0, respectively) is located in the center of - the grid (or the center of each axis speparately) or not. - (default: False). + Whether x==0 (or k==0, respectively) is located in the center of + the grid (or the center of each axis speparately) or not. + (default: False). distances : {float, numpy.ndarray}, *optional* Distance between two grid points along each axis (default: None). If distances==None: if harmonic==True, all distances will be set to 1 if harmonic==False, the distance along each axis will be - set to the inverse of the number of points along that - axis. + set to the inverse of the number of points along that + axis. harmonic : bool, *optional* - Whether the space represents a grid in position or harmonic space. + Whether the space represents a grid in position or harmonic space. (default: False). Attributes @@ -92,15 +92,6 @@ class RGSpace(Space): def __init__(self, shape, zerocenter=False, distances=None, harmonic=False): - """ - Sets the attributes for an RGSpace class instance. - - - - Returns - ------- - None - """ self._harmonic = bool(harmonic) super(RGSpace, self).__init__() @@ -227,7 +218,8 @@ class RGSpace(Space): Returns ------- distributed_data_object - A d2o containing the distances + A d2o containing the distances. + """ shape = self.shape diff --git a/nifty/sugar.py b/nifty/sugar.py index 2eba6e7a2fa7f36e6643e61fc56a3c9cb240cfbb..5bf7dbc6f8b23f2753bebe1cde110462549dfcbb 100644 --- a/nifty/sugar.py +++ b/nifty/sugar.py @@ -37,8 +37,8 @@ def create_power_operator(domain, power_spectrum, dtype=None, val=power_spectrum, dtype=dtype, distribution_strategy=distribution_strategy) - f = fp.power_synthesize(mean=1, std=0, real_signal=True) + f = fp.power_synthesize(mean=1, std=0, real_signal=False) power_operator = DiagonalOperator(domain, diagonal=f, bare=True) - return power_operator + return power_operator \ No newline at end of file diff --git a/test/test_init.py b/test/test_init.py new file mode 100644 index 0000000000000000000000000000000000000000..7d23ef5c98a197262e6c75ecf90aebb078991ff8 --- /dev/null +++ b/test/test_init.py @@ -0,0 +1,4 @@ +from nifty import * + +#This tests if it is possible to import all of nifties methods. Experience shows this is not always possible. +pass \ No newline at end of file diff --git a/test/test_operators/test_diagonal_operator.py b/test/test_operators/test_diagonal_operator.py new file mode 100644 index 0000000000000000000000000000000000000000..90f1ef7c4ac2f095d400d4d60e51314b8324cc08 --- /dev/null +++ b/test/test_operators/test_diagonal_operator.py @@ -0,0 +1,134 @@ +import unittest + +import numpy as np +from numpy.testing import assert_equal,\ + assert_allclose,\ + assert_approx_equal + +from nifty import Field,\ + DiagonalOperator + +from test.common import generate_spaces + +from itertools import product +from test.common import expand + +class DiagonalOperator_Tests(unittest.TestCase): + spaces = generate_spaces() + + @expand(product(spaces, [True, False], [True, False])) + def test_property(self, space, bare, copy): + diag = Field.from_random('normal', domain=space) + D = DiagonalOperator(space, diagonal=diag) + if D.domain[0] != space: + raise TypeError + if D.unitary != False: + raise TypeError + if D.self_adjoint != True: + raise TypeError + + @expand(product(spaces, [True, False], [True, False])) + def test_times_adjoint(self, space, bare, copy): + rand1 = Field.from_random('normal', domain=space) + rand2 = Field.from_random('normal', domain=space) + diag = Field.from_random('normal', domain=space) + D = DiagonalOperator(space, diagonal=diag, bare=bare, copy=copy) + tt1 = rand1.dot(D.times(rand2)) + tt2 = rand2.dot(D.times(rand1)) + assert_approx_equal(tt1, tt2) + + @expand(product(spaces, [True, False], [True, False])) + def test_times_inverse(self, space, bare, copy): + rand1 = Field.from_random('normal', domain=space) + diag = Field.from_random('normal', domain=space) + D = DiagonalOperator(space, diagonal=diag, bare=bare, copy=copy) + tt1 = D.times(D.inverse_times(rand1)) + assert_allclose(rand1.val.get_full_data(), tt1.val.get_full_data()) + + @expand(product(spaces, [True, False], [True, False])) + def test_times(self, space, bare, copy): + rand1 = Field.from_random('normal', domain=space) + diag = Field.from_random('normal', domain=space) + D = DiagonalOperator(space, diagonal=diag, bare=bare, copy=copy) + tt = D.times(rand1) + assert_equal(tt.domain[0], space) + + @expand(product(spaces, [True, False], [True, False])) + def test_adjoint_times(self, space, bare, copy): + rand1 = Field.from_random('normal', domain=space) + diag = Field.from_random('normal', domain=space) + D = DiagonalOperator(space, diagonal=diag, bare=bare, copy=copy) + tt = D.adjoint_times(rand1) + assert_equal(tt.domain[0], space) + + @expand(product(spaces, [True, False], [True, False])) + def test_inverse_times(self, space, bare, copy): + rand1 = Field.from_random('normal', domain=space) + diag = Field.from_random('normal', domain=space) + D = DiagonalOperator(space, diagonal=diag, bare=bare, copy=copy) + tt = D.inverse_times(rand1) + assert_equal(tt.domain[0], space) + + @expand(product(spaces, [True, False], [True, False])) + def test_adjoint_inverse_times(self, space, bare, copy): + rand1 = Field.from_random('normal', domain=space) + diag = Field.from_random('normal', domain=space) + D = DiagonalOperator(space, diagonal=diag, bare=bare, copy=copy) + tt = D.adjoint_inverse_times(rand1) + assert_equal(tt.domain[0], space) + + @expand(product(spaces, [True, False])) + def test_diagonal(self, space, copy): + diag = Field.from_random('normal', domain=space) + D = DiagonalOperator(space, diagonal=diag, copy=copy) + diag_op = D.diagonal() + assert_allclose(diag.val.get_full_data(), diag_op.val.get_full_data()) + + @expand(product(spaces, [True, False])) + def test_inverse(self, space, copy): + diag = Field.from_random('normal', domain=space) + D = DiagonalOperator(space, diagonal=diag, copy=copy) + diag_op = D.inverse_diagonal() + assert_allclose(1./diag.val.get_full_data(), diag_op.val.get_full_data()) + + @expand(product(spaces, [True, False])) + def test_trace(self, space, copy): + diag = Field.from_random('normal', domain=space) + D = DiagonalOperator(space, diagonal=diag, copy=copy) + trace_op = D.trace() + assert_allclose(trace_op, np.sum(diag.val.get_full_data())) + + @expand(product(spaces, [True, False])) + def test_inverse_trace(self, space, copy): + diag = Field.from_random('normal', domain=space) + D = DiagonalOperator(space, diagonal=diag, copy=copy) + trace_op = D.inverse_trace() + assert_allclose(trace_op, np.sum(1./diag.val.get_full_data())) + + @expand(product(spaces, [True, False])) + def test_trace_log(self, space, copy): + diag = Field.from_random('normal', domain=space) + D = DiagonalOperator(space, diagonal=diag, copy=copy) + trace_log = D.trace_log() + assert_allclose(trace_log, np.sum(np.log(diag.val.get_full_data()))) + + @expand(product(spaces, [True, False])) + def test_determinant(self, space, copy): + diag = Field.from_random('normal', domain=space) + D = DiagonalOperator(space, diagonal=diag, copy=copy) + det = D.determinant() + assert_allclose(det, np.prod(diag.val.get_full_data())) + + @expand(product(spaces, [True, False], [True, False])) + def test_inverse_determinant(self, space, bare, copy): + diag = Field.from_random('normal', domain=space) + D = DiagonalOperator(space, diagonal=diag, bare=bare, copy=copy) + inv_det = D.inverse_determinant() + assert_allclose(inv_det, 1./D.determinant()) + + @expand(product(spaces, [True, False], [True, False])) + def test_log_determinant(self, space, bare, copy): + diag = Field.from_random('normal', domain=space) + D = DiagonalOperator(space, diagonal=diag, bare=bare, copy=copy) + log_det = D.log_determinant() + assert_allclose(log_det, np.log(D.determinant())) \ No newline at end of file