diff --git a/.gitignore b/.gitignore index 368117a8f077d1bd83c1623cc95c65a9ecc0d308..15bb48beb44c8d1e41def32f99ca7c13ff1215eb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,13 @@ .document build +operators/* +!operators/__init__py +!operators/nifty_explicit.py +!operators/nifty_operators.py +!operators/nifty_probing.py + + rg/* !rg/__init__.py !rg/fft_rg.py diff --git a/operators/nifty_explicit.py b/operators/nifty_explicit.py new file mode 100644 index 0000000000000000000000000000000000000000..099875959750c5f8acb2bdea33c1995985a03361 --- /dev/null +++ b/operators/nifty_explicit.py @@ -0,0 +1,2379 @@ +## NIFTY (Numerical Information Field Theory) has been developed at the +## Max-Planck-Institute for Astrophysics. +## +## Copyright (C) 2013 Max-Planck-Society +## +## Author: Marco Selig +## Project homepage: <http://www.mpa-garching.mpg.de/ift/nifty/> +## +## 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/>. + +""" + .. __ ____ __ + .. /__/ / _/ / /_ + .. __ ___ __ / /_ / _/ __ __ + .. / _ | / / / _/ / / / / / / + .. / / / / / / / / / /_ / /_/ / + .. /__/ /__/ /__/ /__/ \___/ \___ / explicit + .. /______/ + + This module extends NIFTY's versatility to the usage of explicit matrix + representations of linear operator by the :py:class:`explicit_operator`. + In order to access explicit operators, this module provides the + :py:class:`explicit_probing` class and the :py:func:`explicify` function. + Those objects are supposed to support the user in solving information field + theoretical problems in low (or moderate) dimensions, or in debugging + algorithms by studying operators in detail. + +""" +from __future__ import division +from sys import stdout as so +import numpy as np +import pylab as pl +from matplotlib.colors import LogNorm as ln +from matplotlib.ticker import LogFormatter as lf +from multiprocessing import Pool as mp +from multiprocessing import Value as mv +from multiprocessing import Array as ma + +from nifty.nifty_about import about +from nifty.nifty_core import space, \ + field +from nifty_operators import operator,\ + diagonal_operator,\ + identity,\ + vecvec_operator +from nifty_probing import probing + + +##----------------------------------------------------------------------------- + +class explicit_operator(operator): + """ + .. + .. + .. __ __ __ __ + .. / / /__/ /__/ / /_ + .. _______ __ __ ______ / / __ _______ __ / _/ + .. / __ / \ \/ / / _ | / / / / / ____/ / / / / + .. / /____/ / / / /_/ / / /_ / / / /____ / / / /_ + .. \______/ /__/\__\ / ____/ \___/ /__/ \______/ /__/ \___/ operator class + .. /__/ + + NIFTY subclass for explicit linear operators. + + This class essentially supports linear operators with explicit matrix + representation in the NIFTY framework. Note that this class is not + suited for handling huge dimensionalities. + + Parameters + ---------- + domain : space + The space wherein valid arguments live. + matrix : {list, array} + The matrix representation of the operator, ideally shaped in 2D + according to dimensionality of target and domain (default: None). + bare : {bool, 2-tuple}, *optional* + Whether the matrix entries are `bare` or not + (mandatory for the correct incorporation of volume weights) + (default: True) + sym : bool, *optional* + Indicates whether the operator is self-adjoint or not + (default: False). + uni : bool, *optional* + Indicates whether the operator is unitary or not (default: False). + target : space, *optional* + The space wherein the operator output lives (default: domain). + + See Also + -------- + explicify : A function that returns an explicit oparator given an + implicit one. + + 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 = rg_space(2) # default `dist` == 0.5 + >>> A = explicit_operator(x_space, matrix=[[2, 0], [1, 1]], bare=False) + >>> A.get_matrix(bare=False) + array([[2, 0], + [1, 1]]) + >>> A.get_matrix(bare=True) + array([[4, 0], + [2, 2]]) + >>> c = field(x_space, val=[3, 5]) + >>> A(c).val + array([ 6., 8.]) + >>> A.inverse() + <nifty_explicit.explicit_operator> + >>> (A * A.inverse()).get_matrix(bare=False) # == identity + array([[ 1., 0.], + [ 0., 1.]]) + >>> B = A + diagonal_operator(x_space, diag=2, bare=False) + >>> B.get_matrix(bare=False) + array([[ 4., 0.], + [ 1., 3.]]) + >>> B(c).val + array([ 12., 18.]) + >>> B.tr() + 7.0 + >>> B.det() + 12.000000000000005 + + Attributes + ---------- + domain : space + The space wherein valid arguments live. + val : array + An array containing the `bare` matrix entries. + sym : bool + Indicates whether the operator is self-adjoint or not + uni : bool + Indicates whether the operator is unitary or not + imp : bool + Indicates whether the incorporation of volume weights in + multiplications is already implemented in the `multiply` + instance methods or not. + target : space + The space wherein the operator output lives. + _hidden : array + An array specifying hidden degrees of freedom in domain and target. + _inv : array + An array containing the inverse matrix; set when needed. + + """ + epsilon = 1E-12 ## absolute precision for comparisons to identity + + def __init__(self,domain,matrix=None,bare=True,sym=None,uni=None,target=None): + """ + Initializes the explicit operator and sets the standard operator + properties as well as `values`. + + Parameters + ---------- + domain : space + The space wherein valid arguments live. + matrix : {list, array} + The matrix representation of the operator, ideally shaped in 2D + according to dimensionality of target and domain (default: None). + bare : {bool, 2-tuple}, *optional* + Whether the matrix entries are `bare` or not + (mandatory for the correct incorporation of volume weights) + (default: True). + sym : bool, *optional* + Indicates whether the operator is self-adjoint or not + (default: False). + uni : bool, *optional* + Indicates whether the operator is unitary or not (default: False). + target : space, *optional* + The space wherein the operator output lives (default: domain). + + 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. + + Raises + ------ + TypeError + If invalid input is given. + ValueError + If dimensions of `domain`, `target`, and `matrix` mismatch; + or if `bare` is invalid. + + """ + ## check domain + if(not isinstance(domain,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + self.domain = domain + + ## check matrix and target + if(matrix is None): + if(target is None): + val = np.zeros((self.domain.dim(split=False),self.domain.dim(split=False)),dtype=np.int,order='C') + target = self.domain + else: + if(not isinstance(target,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + elif(target!=self.domain): + sym = False + uni = False + val = np.zeros((target.dim(split=False),self.domain.dim(split=False)),dtype=np.int,order='C') + elif(np.size(matrix,axis=None)%self.domain.dim(split=False)==0): + val = np.array(matrix).reshape((-1,self.domain.dim(split=False))) + if(target is not None): + if(not isinstance(target,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + elif(val.shape[0]!=target.dim(split=False)): + raise ValueError(about._errors.cstring("ERROR: dimension mismatch ( "+str(val.shape[0])+" <> "+str(target.dim(split=False))+" ).")) + elif(target!=self.domain): + sym = False + uni = False + elif(val.shape[0]==val.shape[1]): + target = self.domain + else: + raise TypeError(about._errors.cstring("ERROR: insufficient input.")) + else: + raise ValueError(about._errors.cstring("ERROR: dimension mismatch ( "+str(np.size(matrix,axis=None))+" <> "+str(self.domain.dim(split=False))+" ).")) +# if(val.size>1048576): +# about.infos.cprint("INFO: matrix size > 2 ** 20.") + self.target = target + + ## check datatype + if(np.any(np.iscomplex(val))): + datatype,purelyreal = max(min(val.dtype,self.domain.datatype),min(val.dtype,self.target.datatype)),False + else: + datatype,purelyreal = max(min(val.dtype,self.domain.vol.dtype),min(val.dtype,self.target.vol.dtype)),True + ## weight if ... (given `domain` and `target`) + if(isinstance(bare,tuple)): + if(len(bare)!=2): + raise ValueError(about._errors.cstring("ERROR: invalid input.")) + else: + val = self._calc_weight_rows(val,power=-int(not bare[0])) + val = self._calc_weight_cols(val,power=-int(not bare[1])) + elif(not bare): + val = self._calc_weight_rows(val,-1) + if(purelyreal): + self.val = np.real(val).astype(datatype) + else: + self.val = val.astype(datatype) + + ## check hidden degrees of freedom + self._hidden = np.array([self.domain.dim(split=False)<self.domain.dof(),self.target.dim(split=False)<self.target.dof()],dtype=np.bool) +# if(np.any(self._hidden)): +# about.infos.cprint("INFO: inappropriate space.") + + ## check flags + self.sym,self.uni = self._check_flags(sym=sym,uni=uni) + if(self.domain.discrete)and(self.target.discrete): + self.imp = True + else: + self.imp = False ## bare matrix is stored for efficiency + + self._inv = None ## defined when needed + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _check_flags(self,sym=None,uni=None): ## > determine `sym` and `uni` + if(self.val.shape[0]==self.val.shape[1]): + if(sym is None): + adj = np.conjugate(self.val.T) + sym = np.all(np.absolute(self.val-adj)<self.epsilon) + if(uni is None): + uni = np.all(np.absolute(self._calc_mul(adj,0)-np.diag(1/self.target.get_meta_volume(total=False).flatten(order='C'),k=0))<self.epsilon) + elif(uni is None): + adj = np.conjugate(self.val.T) + uni = np.all(np.absolute(self._calc_mul(adj,0)-np.diag(1/self.target.get_meta_volume(total=False).flatten(order='C'),k=0))<self.epsilon) + return bool(sym),bool(uni) + else: + return False,False + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _set_inverse(self): ## > define inverse matrix + if(self._inv is None): + if(np.any(self._hidden)): + about.warnings.cprint("WARNING: inappropriate inversion.") + self._inv = np.linalg.inv(self.weight(rowpower=1,colpower=1,overwrite=False)) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def cast_domain(self,newdomain): + """ + Casts the domain of the operator. + + Parameters + ---------- + newdomain : space + New space wherein valid argument lives. + + Returns + ------- + None + + Raises + ------ + TypeError + If `newdomain` is no instance of the nifty_core.space class + ValueError + If `newdomain` does not match the (unsplit) dimensionality of + the current domain. + + """ + if(not isinstance(newdomain,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + elif(newdomain.dim(split=False)!=self.domain.dim(split=False)): + raise ValueError(about._errors.cstring("ERROR: dimension mismatch ( "+str(newdomain.dim(split=False))+" <> "+str(self.domain.dim(split=False))+" ).")) + self.domain = newdomain + + def cast_target(self,newtarget): + """ + Casts the target of the operator. + + Parameters + ---------- + newtarget : space + New space wherein the operator output lives. + + Returns + ------- + None + + Raises + ------ + TypeError + If `newtarget` is no instance of the nifty_core.space class + ValueError + If `newtarget` does not match the (unsplit) dimensionality of + the current target. + + """ + if(not isinstance(newtarget,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + elif(newtarget.dim(split=False)!=self.target.dim(split=False)): + raise ValueError(about._errors.cstring("ERROR: dimension mismatch ( "+str(newtarget.dim(split=False))+" <> "+str(self.target.dim(split=False))+" ).")) + self.target = newtarget + + def cast_spaces(self,newdomain=None,newtarget=None): + """ + Casts the domain and/or the target of the operator. + + Parameters + ---------- + newdomain : space, *optional* + New space wherein valid argument lives (default: None). + newtarget : space, *optional* + New space wherein the operator output lives (default: None). + + Returns + ------- + None + + """ + if(newdomain is not None): + self.cast_domain(newdomain) + if(newtarget is not None): + self.cast_target(newtarget) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def set_matrix(self,newmatrix,bare=True,sym=None,uni=None): + """ + Resets the entire matrix. + + Parameters + ---------- + matrix : {list, array} + New matrix representation of the operator, ideally shaped in 2D + according to dimensionality of target and domain (default: None). + bare : {bool, 2-tuple}, *optional* + Whether the new matrix entries are `bare` or not + (mandatory for the correct incorporation of volume weights) + (default: True). + sym : bool, *optional* + Indicates whether the new operator is self-adjoint or not + (default: False). + uni : bool, *optional* + Indicates whether the new operator is unitary or not + (default: False). + + 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. + + Returns + ------- + None + + Raises + ------ + ValueError + If matrix' dimensions mismatch; + or if `bare` is invalid. + + """ + ## check matrix + if(np.size(newmatrix,axis=None)==self.domain.dim(split=False)*self.target.dim(split=False)): + val = np.array(newmatrix).reshape((self.target.dim(split=False),self.domain.dim(split=False))) + if(self.target!=self.domain): + sym = False + uni = False +# if(val.size>1048576): +# about.infos.cprint("INFO: matrix size > 2 ** 20.") + else: + raise ValueError(about._errors.cstring("ERROR: dimension mismatch ( "+str(np.size(newmatrix,axis=None))+" <> "+str(self.nrow())+" x "+str(self.ncol())+" ).")) + + ## check datatype + if(np.any(np.iscomplex(val))): + datatype,purelyreal = max(min(val.dtype,self.domain.datatype),min(val.dtype,self.target.datatype)),False + else: + datatype,purelyreal = max(min(val.dtype,self.domain.vol.dtype),min(val.dtype,self.target.vol.dtype)),True + ## weight if ... (given `domain` and `target`) + if(isinstance(bare,tuple)): + if(len(bare)!=2): + raise ValueError(about._errors.cstring("ERROR: invalid input.")) + else: + val = self._calc_weight_rows(val,power=-int(not bare[0])) + val = self._calc_weight_cols(val,power=-int(not bare[1])) + elif(not bare): + val = self._calc_weight_rows(val,-1) + if(purelyreal): + self.val = np.real(val).astype(datatype) + else: + self.val = val.astype(datatype) + + ## check flags + self.sym,self.uni = self._check_flags(sym=sym,uni=uni) + self._inv = None ## reset + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def get_matrix(self,bare=True): + """ + Returns the entire matrix. + + Parameters + ---------- + bare : {bool, 2-tuple}, *optional* + Whether the returned matrix entries are `bare` or not + (default: True). + + 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. + + Returns + ------- + matrix : numpy.array + The matrix representation of the operator, shaped in 2D + according to dimensionality of target and domain. + + Raises + ------ + ValueError + If `bare` is invalid. + + """ + if(bare==True)or(self.imp): + return self.val + ## weight if ... + elif(isinstance(bare,tuple)): + if(len(bare)!=2): + raise ValueError(about._errors.cstring("ERROR: invalid input.")) + else: + return self.weight(rowpower=int(not bare[0]),colpower=int(not bare[1]),overwrite=False) + elif(not bare): + return self.weight(rowpower=int(not bare),colpower=0,overwrite=False) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _calc_weight_row(self,x,power): ## > weight row and flatten + return self.domain.calc_weight(x,power=power).flatten(order='C') + + def _calc_weight_col(self,x,power): ## > weight column and flatten + return self.target.calc_weight(x,power=power).flatten(order='C') + + def _calc_weight_rows(self,X,power=1): ## > weight all rows + if(np.any(np.iscomplex(X)))and(not issubclass(self.domain.datatype,np.complexfloating)): + return (np.apply_along_axis(self._calc_weight_row,1,np.real(X),power)+np.apply_along_axis(self._calc_weight_row,1,np.imag(X),power)*1j) + else: + return np.apply_along_axis(self._calc_weight_row,1,X,power) + + def _calc_weight_cols(self,X,power=1): ## > weight all columns + if(np.any(np.iscomplex(X)))and(not issubclass(self.target.datatype,np.complexfloating)): + return (np.apply_along_axis(self._calc_weight_col,0,np.real(X),power)+np.apply_along_axis(self._calc_weight_col,0,np.imag(X),power)*1j) + else: + return np.apply_along_axis(self._calc_weight_col,0,X,power) + + def weight(self,rowpower=0,colpower=0,overwrite=False): + """ + Returns the entire matrix, weighted with the volume factors to a + given power. The matrix entries will optionally be overwritten. + + Parameters + ---------- + rowpower : scalar, *optional* + Specifies the power of the volume factors applied to the rows + of the matrix (default: 0). + rowpower : scalar, *optional* + Specifies the power of the volume factors applied to the columns + of the matrix (default: 0). + overwrite : bool, *optional* + Whether to overwrite the matrix or not (default: False). + + Returns + ------- + field : field, *optional* + If overwrite is ``False``, the weighted matrix is returned; + otherwise, nothing is returned. + + """ + if(overwrite): + if(not self.domain.discrete)and(rowpower): ## rowpower <> 0 + self.val = self._calc_weight_rows(self.val,rowpower) + if(not self.target.discrete)and(colpower): ## colpower <> 0 + self.val = self._calc_weight_cols(self.val,colpower) + else: + X = np.copy(self.val) + if(not self.domain.discrete)and(rowpower): ## rowpower <> 0 + X = self._calc_weight_rows(X,rowpower) + if(not self.target.discrete)and(colpower): ## colpower <> 0 + X = self._calc_weight_cols(X,colpower) + return X + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _multiply(self,x,**kwargs): ## > applies the matirx to a given field + if(self._hidden[0]): ## hidden degrees of freedom + x_ = np.apply_along_axis(self.domain.calc_dot,1,self.val,np.conjugate(x.val)) + else: + x_ = np.dot(self.val,x.val.flatten(order='C'),out=None) + return x_ + + def _adjoint_multiply(self,x,**kwargs): ## > applies the adjoint operator to a given field + if(self._hidden[1]): ## hidden degrees of freedom + x_ = np.apply_along_axis(self.target.calc_dot,0,np.conjugate(self.val),np.conjugate(x.val)) + else: + x_ = np.dot(np.conjugate(self.val.T),x.val.flatten(order='C'),out=None) + return x_ + + def _inverse_multiply(self,x,**kwargs): ## > applies the inverse operator to a given field + if(self._hidden[1]): ## hidden degrees of freedom + x_ = np.apply_along_axis(self.target.calc_dot,1,self._inv,np.conjugate(x.val)) + else: + x_ = np.dot(self._inv,x.val.flatten(order='C'),out=None) + return x_ + + def _inverse_adjoint_multiply(self,x,**kwargs): ## > applies the adjoint inverse operator to a given field + if(self._hidden[0]): ## hidden degrees of freedom + x_ = np.apply_along_axis(self.domain.calc_dot,0,np.conjugate(self.val),np.conjugate(x.val)) + else: + x_ = np.dot(np.conjugate(self._inv.T),x.val.flatten(order='C'),out=None) + return x_ + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _briefing(self,x,domain,inverse): ## > prepares x for `multiply` + ## inspect x + if(not isinstance(x,field)): + x_ = field(domain,val=x,target=None) + else: + ## check x.domain + if(domain==x.domain): + x_ = x + ## transform + else: + x_ = x.transform(target=domain,overwrite=False) + ## weight if ... + if(not self.imp)and(not domain.discrete): + x_ = x_.weight(power=1,overwrite=False) + return x_ + + def _debriefing(self,x,x_,target,inverse): ## > evaluates x and x_ after `multiply` + if(x_ is None): + return None + else: + ## inspect x_ + if(not isinstance(x_,field)): + x_ = field(target,val=x_,target=None) + elif(x_.domain!=target): + raise ValueError(about._errors.cstring("ERROR: invalid output domain.")) + ## inspect x + if(isinstance(x,field)): + ## repair ... + if(self.domain==self.target!=x.domain): + x_ = x_.transform(target=x.domain,overwrite=False) ## ... domain + if(x_.domain==x.domain)and(x_.target!=x.target): + x_.set_target(newtarget=x.target) ## ... codomain + return x_ + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def inverse_times(self,x,**kwargs): + """ + Applies the inverse operator to a given object. + + Parameters + ---------- + x : {scalar, ndarray, field} + Scalars are interpreted as constant arrays, and an array will + be interpreted as a field on the domain space of the operator. + + Returns + ------- + OIx : field + Mapped field on the target space of the operator. + + Raises + ------ + ValueError + If it is no square matrix. + + """ + ## check whether self-inverse + if(self.sym)and(self.uni): + return self.times(x,**kwargs) + + ## check whether square matrix + elif(self.nrow()!=self.ncol()): + raise ValueError(about._errors.cstring("ERROR: inverse ill-defined for "+str(self.nrow())+" x "+str(self.ncol())+" matrices.")) + + self._set_inverse() + + ## prepare + x_ = self._briefing(x,self.target,True) + ## apply operator + x_ = self._inverse_multiply(x_,**kwargs) + ## evaluate + return self._debriefing(x,x_,self.domain,True) + + def adjoint_inverse_times(self,x,**kwargs): + """ + Applies the inverse adjoint operator to a given object. + + Parameters + ---------- + x : {scalar, ndarray, field} + Scalars are interpreted as constant arrays, and an array will + be interpreted as a field on the target space of the operator. + + Returns + ------- + OAIx : field + Mapped field on the domain of the operator. + + Notes + ----- + For linear operators represented by square matrices, inversion and + adjungation and inversion commute. + + Raises + ------ + ValueError + If it is no square matrix. + + """ + return self.inverse_adjoint_times(x,**kwargs) + + def inverse_adjoint_times(self,x,**kwargs): + """ + Applies the adjoint inverse operator to a given object. + + Parameters + ---------- + x : {scalar, ndarray, field} + Scalars are interpreted as constant arrays, and an array will + be interpreted as a field on the target space of the operator. + + Returns + ------- + OIAx : field + Mapped field on the domain of the operator. + + Notes + ----- + For linear operators represented by square matrices, inversion and + adjungation and inversion commute. + + Raises + ------ + ValueError + If it is no square matrix. + + """ + ## check whether square matrix + if(self.nrow()!=self.ncol()): + raise ValueError(about._errors.cstring("ERROR: inverse ill-defined for "+str(self.nrow())+" x "+str(self.ncol())+" matrices.")) + + ## check whether self-adjoint + if(self.sym): + return self._inverse_times(x,**kwargs) + ## check whether unitary + if(self.uni): + return self.times(x,**kwargs) + + self._set_inverse() + + ## prepare + x_ = self._briefing(x,self.domain,True) + ## apply operator + x_ = self._inverse_adjoint_multiply(x_,**kwargs) + ## evaluate + return self._debriefing(x,x_,self.target,True) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def tr(self,domain=None,**kwargs): + """ + Computes the trace of the operator. + + Parameters + ---------- + domain : space, *optional* + Space wherein the probes live (default: self.domain). + target : space, *optional* + Space wherein the transform of the probes live (default: None). + random : string, *optional* + Specifies the pseudo random number generator (default: "pm1"); + supported distributions are: + + - "pm1" (uniform distribution over {+1,-1} or {+1,+i,-1,-i} + - "gau" (normal distribution with zero-mean and a given standard deviation or variance) + + ncpu : integer, *optional* + Number of used CPUs to use (default: 2). + nrun : integer, *optional* + Total number of probes (default: 8). + nper : integer, *optional* + Number of tasks performed by one worker (default: 1). + var : bool, *optional* + Whether to additionally return the probing variance or not + (default: False). + loop : bool, *optional* + Whether to perform a loop or to parallelise (default: False). + + Returns + ------- + tr : float + Trace of the operator. + delta : float, *optional* + Probing variance of the trace; returned if `var` is ``True`` in + case of probing. + + See Also + -------- + trace_probing + + Raises + ------ + ValueError + If `domain` and `target` are unequal. + + """ + if(self.domain!=self.target): + raise ValueError(about._errors.cstring("ERROR: trace ill-defined.")) + + if(domain is None)or(domain==self.domain): + diag = self.diag(bare=False,domain=self.domain) + if(self._hidden[0]): ## hidden degrees of freedom + return self.domain.calc_dot(np.ones(self.domain.dim(split=True),dtype=self.domain.datatype,order='C'),diag) ## discrete inner product + else: + return np.sum(diag,axis=None,dtype=None,out=None) + else: + return super(explicit_operator,self).tr(domain=domain,**kwargs) ## probing + + def inverse_tr(self,domain=None,**kwargs): + """ + Computes the trace of the inverse operator. + + Parameters + ---------- + domain : space, *optional* + Space wherein the probes live (default: self.domain). + target : space, *optional* + Space wherein the transform of the probes live (default: None). + random : string, *optional* + Specifies the pseudo random number generator (default: "pm1"); + supported distributions are: + + - "pm1" (uniform distribution over {+1,-1} or {+1,+i,-1,-i} + - "gau" (normal distribution with zero-mean and a given standard deviation or variance) + + ncpu : integer, *optional* + Number of used CPUs to use (default: 2). + nrun : integer, *optional* + Total number of probes (default: 8). + nper : integer, *optional* + Number of tasks performed by one worker (default: 1). + var : bool, *optional* + Whether to additionally return the probing variance or not + (default: False). + loop : bool, *optional* + Whether to perform a loop or to parallelise (default: False). + + Returns + ------- + tr : float + Trace of the operator. + delta : float, *optional* + Probing variance of the trace; returned if `var` is ``True`` in + case of probing. + + See Also + -------- + trace_probing + + Raises + ------ + ValueError + If `domain` and `target` are unequal. + + """ + if(self.domain!=self.target): + raise ValueError(about._errors.cstring("ERROR: trace ill-defined.")) + + if(domain is None)or(domain==self.domain): + diag = self.inverse_diag(bare=False,domain=self.domain) + if(self._hidden[0]): ## hidden degrees of freedom + return self.domain.calc_dot(np.ones(self.domain.dim(split=True),dtype=self.domain.datatype,order='C'),diag) ## discrete inner product + else: + return np.sum(diag,axis=None,dtype=None,out=None) + else: + return super(explicit_operator,self).tr(domain=domain,**kwargs) ## probing + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def diag(self,bare=False,domain=None,**kwargs): + """ + Computes the diagonal of the operator. + + Parameters + ---------- + bare : bool, *optional* + Whether the returned diagonal entries are `bare` or not + (default: True). + domain : space, *optional* + Space wherein the probes live (default: self.domain). + target : space, *optional* + Space wherein the transform of the probes live (default: None). + random : string, *optional* + Specifies the pseudo random number generator (default: "pm1"); + supported distributions are: + + - "pm1" (uniform distribution over {+1,-1} or {+1,+i,-1,-i} + - "gau" (normal distribution with zero-mean and a given standard deviation or variance) + + ncpu : integer, *optional* + Number of used CPUs to use (default: 2). + nrun : integer, *optional* + Total number of probes (default: 8). + nper : integer, *optional* + Number of tasks performed by one worker (default: 1). + var : bool, *optional* + Whether to additionally return the probing variance or not + (default: False). + save : bool, *optional* + Whether all individual probing results are saved or not + (default: False). + path : string, *optional* + Path wherein the results are saved (default: "tmp"). + prefix : string, *optional* + Prefix for all saved files (default: "") + loop : bool, *optional* + Whether to perform a loop or to parallelise (default: False). + + Returns + ------- + diag : ndarray + Diagonal of the operator. + delta : float, *optional* + Probing variance of the trace; returned if `var` is ``True`` in + case of probing. + + 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. + + See Also + -------- + diagonal_probing + + Raises + ------ + ValueError + If it is no square matrix. + + """ + if(self.val.shape[0]!=self.val.shape[1]): + raise ValueError(about._errors.cstring("ERROR: diagonal ill-defined for "+str(self.val.shape[0])+" x "+str(self.val.shape[1])+" matrices.")) + if(self.domain!=self.target)and(not bare): + about.warnings.cprint("WARNING: ambiguous non-bare diagonal.") + + if(domain is None)or(domain==self.domain): + diag = np.diagonal(self.val,offset=0,axis1=0,axis2=1) + ## weight if ... + if(not self.domain.discrete)and(not bare): + diag = self.domain.calc_weight(diag,power=1) + ## check complexity + if(np.all(np.imag(diag)==0)): + diag = np.real(diag) + return diag + elif(domain==self.target): + diag = np.diagonal(np.conjugate(self.val.T),offset=0,axis1=0,axis2=1) + ## weight if ... + if(not self.target.discrete)and(not bare): + diag = self.target.calc_weight(diag,power=1) + ## check complexity + if(np.all(np.imag(diag)==0)): + diag = np.real(diag) + return diag + else: + return super(explicit_operator,self).diag(bare=bare,domain=domain,**kwargs) ## probing + + def inverse_diag(self,bare=False,domain=None,**kwargs): + """ + Computes the diagonal of the inverse operator. + + Parameters + ---------- + bare : bool, *optional* + Whether the returned diagonal entries are `bare` or not + (default: True). + domain : space, *optional* + Space wherein the probes live (default: self.domain). + target : space, *optional* + Space wherein the transform of the probes live (default: None). + random : string, *optional* + Specifies the pseudo random number generator (default: "pm1"); + supported distributions are: + + - "pm1" (uniform distribution over {+1,-1} or {+1,+i,-1,-i} + - "gau" (normal distribution with zero-mean and a given standard deviation or variance) + + ncpu : integer, *optional* + Number of used CPUs to use (default: 2). + nrun : integer, *optional* + Total number of probes (default: 8). + nper : integer, *optional* + Number of tasks performed by one worker (default: 1). + var : bool, *optional* + Whether to additionally return the probing variance or not + (default: False). + save : bool, *optional* + Whether all individual probing results are saved or not + (default: False). + path : string, *optional* + Path wherein the results are saved (default: "tmp"). + prefix : string, *optional* + Prefix for all saved files (default: "") + loop : bool, *optional* + Whether to perform a loop or to parallelise (default: False). + + Returns + ------- + diag : ndarray + Diagonal of the inverse operator. + delta : float, *optional* + Probing variance of the trace; returned if `var` is ``True`` in + case of probing. + + 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. + + See Also + -------- + diagonal_probing + + Raises + ------ + ValueError + If it is no square matrix. + + """ + if(self.val.shape[0]!=self.val.shape[1]): + raise ValueError(about._errors.cstring("ERROR: inverse ill-defined for "+str(self.val.shape[0])+" x "+str(self.val.shape[1])+" matrices.")) + if(self.domain!=self.target)and(not bare): + about.warnings.cprint("WARNING: ambiguous non-bare diagonal.") + + if(domain is None)or(domain==self.target): + self._set_inverse() + diag = np.diagonal(self._inv,offset=0,axis1=0,axis2=1) + ## weight if ... + if(not self.target.discrete)and(not bare): + diag = self.target.calc_weight(diag,power=1) + ## check complexity + if(np.all(np.imag(diag)==0)): + diag = np.real(diag) + return diag + elif(domain==self.domain): + self._set_inverse() + diag = np.diagonal(np.conjugate(self._inv.T),offset=0,axis1=0,axis2=1) + ## weight if ... + if(not self.domain.discrete)and(not bare): + diag = self.domain.calc_weight(diag,power=1) + ## check complexity + if(np.all(np.imag(diag)==0)): + diag = np.real(diag) + return diag + else: + return super(explicit_operator,self).inverse_diag(bare=bare,domain=domain,**kwargs) ## probing + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def det(self): + """ + Computes the determinant of the operator. + + Parameters + ---------- + None + + Returns + ------- + det : float + Determinant of the operator. + + Raises + ------ + ValueError + If `domain` and `target` are unequal. + + """ + if(self.domain!=self.target): + raise ValueError(about._errors.cstring("ERROR: determinant ill-defined.")) + + if(np.any(self._hidden)): + about.warnings.cprint("WARNING: inappropriate determinant calculation.") + det = np.linalg.det(self.weight(rowpower=0.5,colpower=0.5,overwrite=False)) + if(np.isreal(det)): + return np.asscalar(np.real(det)) + else: + return det + + def inverse_det(self): + """ + Computes the determinant of the inverse operator. + + Parameters + ---------- + None + + Returns + ------- + det : float + Determinant of the inverse operator. + + Raises + ------ + ValueError + If it is a singular matrix. + + """ + det = self.det() + if(det<>0): + return 1/det + else: + raise ValueError(about._errors.cstring("ERROR: singular matrix.")) + + def log_det(self): + """ + Computes the logarithm of the determinant of the operator + (if applicable). + + Returns + ------- + logdet : float + The logarithm of the determinant + + See Also + -------- + numpy.linalg.slogdet + + Raises + ------ + ValueError + If `domain` and `target` are unequal or it is non-positive + definite matrix. + + """ + if(self.domain!=self.target): + raise ValueError(about._errors.cstring("ERROR: determinant ill-defined.")) + + if(np.any(self._hidden)): + about.warnings.cprint("WARNING: inappropriate determinant calculation.") + sign,logdet = np.linalg.slogdet(self.weight(rowpower=0.5,colpower=0.5,overwrite=False)) + if(abs(sign)<0.1): ## abs(sign) << 1 + raise ValueError(about._errors.cstring("ERROR: singular matrix.")) + if(sign==-1): + raise ValueError(about._errors.cstring("ERROR: non-positive definite matrix.")) + else: + logdet += np.log(sign) + if(np.isreal(logdet)): + return np.asscalar(np.real(logdet)) + else: + return logdet + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def __len__(self): + return int(self.nrow()[0]) + + def __getitem__(self,key): + return self.val[key] + + def __setitem__(self,key,value): + self.val[key] = self.val.dtype(value) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def __pos__(self): + return explicit_operator(self.domain,self.val,bare=True,sym=self.sym,uni=self.uni,target=self.target) + + def __neg__(self): + return explicit_operator(self.domain,-self.val,bare=True,sym=self.sym,uni=self.uni,target=self.target) + + def __abs__(self): + return explicit_operator(self.domain,np.absolute(self.val),bare=True,sym=self.sym,uni=self.uni,target=self.target) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def transpose(self): + """ + Computes the transposed matrix. + + Returns + ------- + T : explicit_operator + The transposed matrix. + + """ + return explicit_operator(self.target,self.val.T,bare=True,sym=self.sym,uni=self.uni,target=self.domain) + + def conjugate(self): + """ + Computes the complex conjugated matrix. + + Returns + ------- + CC : explicit_operator + The complex conjugated matrix. + + """ + return explicit_operator(self.domain,np.conjugate(self.val),bare=True,sym=self.sym,uni=self.uni,target=self.target) + + def adjoint(self): + """ + Computes the adjoint matrix. + + Returns + ------- + A : explicit_operator + The adjoint matrix. + + """ + return explicit_operator(self.target,np.conjugate(self.val.T),bare=True,sym=self.sym,uni=self.uni,target=self.domain) + + def inverse(self): + """ + Computes the inverted matrix. + + Returns + ------- + I : explicit_operator + The inverted matrix. + + Raises + ------ + ValueError + If it is no square matrix. + + """ + ## check whether square matrix + if(self.nrow()!=self.ncol()): + raise ValueError(about._errors.cstring("ERROR: inverse ill-defined for "+str(self.nrow())+" x "+str(self.ncol())+" matrices.")) + self._set_inverse() + return explicit_operator(self.target,self._inv,bare=True,sym=self.sym,uni=self.uni,target=self.domain) + + def adjoint_inverse(self): + """ + Computes the adjoint inverted matrix. + + Returns + ------- + AI : explicit_operator + The adjoint inverted matrix. + + Notes + ----- + For linear operators represented by square matrices, inversion and + adjungation and inversion commute. + + Raises + ------ + ValueError + If it is no square matrix. + + """ + return self.inverse_adjoint() + + def inverse_adjoint(self): + """ + Computes the inverted adjoint matrix. + + Returns + ------- + IA : explicit_operator + The inverted adjoint matrix. + + Notes + ----- + For linear operators represented by square matrices, inversion and + adjungation and inversion commute. + + Raises + ------ + ValueError + If it is no square matrix. + + """ + ## check whether square matrix + if(self.nrow()!=self.ncol()): + raise ValueError(about._errors.cstring("ERROR: inverse ill-defined for "+str(self.nrow())+" x "+str(self.ncol())+" matrices.")) + self._set_inverse() + return explicit_operator(self.target,np.conjugate(self._inv.T),bare=True,sym=self.sym,uni=self.uni,target=self.domain) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def __add__(self,X): ## __add__ : self + X + if(isinstance(X,operator)): + if(self.domain!=X.domain): + raise ValueError(about._errors.cstring("ERROR: inequal domains.")) + sym = (self.sym and X.sym) + uni = None + if(isinstance(X,explicit_operator)): + if(self.target!=X.target): + raise ValueError(about._errors.cstring("ERROR: inequal codomains.")) + matrix = self.val+X.val + elif(isinstance(X,diagonal_operator)): + if(self.target.dim(split=False)!=X.target.dim(split=False))or(not self.target.check_codomain(X.target)): + raise ValueError(about._errors.cstring("ERROR: incompatible codomains.")) + matrix = self.val+np.diag(X.diag(bare=True,domain=None),k=0) ## domain == X.domain + elif(isinstance(X,vecvec_operator)): + if(self.target!=X.target): + raise ValueError(about._errors.cstring("ERROR: inequal codomains.")) + matrix = self.val+np.tensordot(X.val,X.val,axes=0) + else: + raise TypeError(about._errors.cstring("ERROR: unsupported or incompatible operator.")) + elif(np.size(X)==1): + if(self.nrow()!=self.ncol()): + raise ValueError(about._errors.cstring("ERROR: identity ill-defined for "+str(self.nrow())+" x "+str(self.ncol())+" matrices.")) + sym = self.sym + uni = None + X = X*np.ones(self.domain.dim(split=False),dtype=np.int,order='C') + X = self.domain.calc_weight(X,power=-1).astype(max(min(X.dtype,self.domain.datatype),min(X.dtype,self.target.datatype))) + matrix = self.val+np.diag(X.flatten(order='C'),k=0) + elif(np.size(X)==np.size(self.val)): + sym = None + uni = None + X = np.array(X).reshape(self.val.shape) + if(np.all(np.isreal(X))): + X = np.real(X) + X = X.astype(max(min(X.dtype,self.domain.datatype),min(X.dtype,self.target.datatype))) + matrix = self.val+X + else: + raise ValueError(about._errors.cstring("ERROR: dimension mismatch ( "+str(np.size(X))+" <> "+str(self.nrow())+" x "+str(self.ncol())+" ).")) + return explicit_operator(self.domain,matrix,bare=True,sym=sym,uni=uni,target=self.target) + + __radd__ = __add__ ## __add__ : X + self + + def __iadd__(self,X): ## __iadd__ : self += X + if(isinstance(X,operator)): + if(self.domain!=X.domain): + raise ValueError(about._errors.cstring("ERROR: inequal domains.")) + self.sym = (self.sym and X.sym) + self.uni = None + if(isinstance(X,explicit_operator)): + if(self.target!=X.target): + raise ValueError(about._errors.cstring("ERROR: inequal codomains.")) + X = X.val + elif(isinstance(X,diagonal_operator)): + if(self.target.dim(split=False)!=X.target.dim(split=False))or(not self.target.check_codomain(X.target)): + raise ValueError(about._errors.cstring("ERROR: incompatible codomains.")) + X = np.diag(X.diag(bare=True,domain=None),k=0) ## domain == X.domain + elif(isinstance(X,vecvec_operator)): + if(self.target!=X.target): + raise ValueError(about._errors.cstring("ERROR: inequal codomains.")) + X = np.tensordot(X.val,X.val,axes=0) + else: + raise TypeError(about._errors.cstring("ERROR: unsupported or incompatible operator.")) + elif(np.size(X)==1): + if(self.nrow()!=self.ncol()): + raise ValueError(about._errors.cstring("ERROR: identity ill-defined for "+str(self.nrow())+" x "+str(self.ncol())+" matrices.")) + self.uni = None + X = X*np.ones(self.domain.dim(split=False),dtype=np.int,order='C') + X = np.diag(self.domain.calc_weight(X,power=-1).astype(max(min(X.dtype,self.domain.datatype),min(X.dtype,self.target.datatype))).flatten(order='C'),k=0) + elif(np.size(X)==np.size(self.val)): + self.sym = None + self.uni = None + X = np.array(X).reshape(self.val.shape) + if(np.all(np.isreal(X))): + X = np.real(X) + X = X.astype(max(min(X.dtype,self.domain.datatype),min(X.dtype,self.target.datatype))) + else: + raise ValueError(about._errors.cstring("ERROR: dimension mismatch ( "+str(np.size(X))+" <> "+str(self.nrow())+" x "+str(self.ncol())+" ).")) + + ## add + if(X.dtype>self.val.dtype): + about.warnings.cprint("WARNING: datatype reset.") + self.val += X + + ## check flags + self.sym,self.uni = self._check_flags(sym=self.sym,uni=self.uni) + + self._inv = None ## reset + + return self + + def __sub__(self,X): ## __sub__ : self - X + if(isinstance(X,operator)): + if(self.domain!=X.domain): + raise ValueError(about._errors.cstring("ERROR: inequal domains.")) + sym = (self.sym and X.sym) + uni = None + if(isinstance(X,explicit_operator)): + if(self.target!=X.target): + raise ValueError(about._errors.cstring("ERROR: inequal codomains.")) + matrix = self.val-X.val + elif(isinstance(X,diagonal_operator)): + if(self.target.dim(split=False)!=X.target.dim(split=False))or(not self.target.check_codomain(X.target)): + raise ValueError(about._errors.cstring("ERROR: incompatible codomains.")) + matrix = self.val-np.diag(X.diag(bare=True,domain=None),k=0) ## domain == X.domain + elif(isinstance(X,vecvec_operator)): + if(self.target!=X.target): + raise ValueError(about._errors.cstring("ERROR: inequal codomains.")) + matrix = self.val-np.tensordot(X.val,X.val,axes=0) + else: + raise TypeError(about._errors.cstring("ERROR: unsupported or incompatible operator.")) + elif(np.size(X)==1): + if(self.nrow()!=self.ncol()): + raise ValueError(about._errors.cstring("ERROR: identity ill-defined for "+str(self.nrow())+" x "+str(self.ncol())+" matrices.")) + sym = self.sym + uni = None + X = X*np.ones(self.domain.dim(split=False),dtype=np.int,order='C') + X = self.domain.calc_weight(X,power=-1).astype(max(min(X.dtype,self.domain.datatype),min(X.dtype,self.target.datatype))) + matrix = self.val-np.diag(X.flatten(order='C'),k=0) + elif(np.size(X)==np.size(self.val)): + sym = None + uni = None + X = np.array(X).reshape(self.val.shape) + if(np.all(np.isreal(X))): + X = np.real(X) + X = X.astype(max(min(X.dtype,self.domain.datatype),min(X.dtype,self.target.datatype))) + matrix = self.val-X + else: + raise ValueError(about._errors.cstring("ERROR: dimension mismatch ( "+str(np.size(X))+" <> "+str(self.nrow())+" x "+str(self.ncol())+" ).")) + return explicit_operator(self.domain,matrix,bare=True,sym=sym,uni=uni,target=self.target) + + def __rsub__(self,X): ## __rsub__ : X - self + if(isinstance(X,operator)): + if(self.domain!=X.domain): + raise ValueError(about._errors.cstring("ERROR: inequal domains.")) + sym = (self.sym and X.sym) + uni = None + if(isinstance(X,explicit_operator)): + if(self.target!=X.target): + raise ValueError(about._errors.cstring("ERROR: inequal codomains.")) + matrix = X.val-self.val + elif(isinstance(X,diagonal_operator)): + if(self.target.dim(split=False)!=X.target.dim(split=False))or(not self.target.check_codomain(X.target)): + raise ValueError(about._errors.cstring("ERROR: incompatible codomains.")) + matrix = np.diag(X.diag(bare=True,domain=None),k=0)-self.val ## domain == X.domain + elif(isinstance(X,vecvec_operator)): + if(self.target!=X.target): + raise ValueError(about._errors.cstring("ERROR: inequal codomains.")) + matrix = np.tensordot(X.val,X.val,axes=0)-self.val + else: + raise TypeError(about._errors.cstring("ERROR: unsupported or incompatible operator.")) + elif(np.size(X)==1): + if(self.nrow()!=self.ncol()): + raise ValueError(about._errors.cstring("ERROR: identity ill-defined for "+str(self.nrow())+" x "+str(self.ncol())+" matrices.")) + sym = self.sym + uni = None + X = X*np.ones(self.domain.dim(split=False),dtype=np.int,order='C') + X = self.domain.calc_weight(X,power=-1).astype(max(min(X.dtype,self.domain.datatype),min(X.dtype,self.target.datatype))) + matrix = np.diag(X.flatten(order='C'),k=0)-self.val + elif(np.size(X)==np.size(self.val)): + sym = None + uni = None + X = np.array(X).reshape(self.val.shape) + if(np.all(np.isreal(X))): + X = np.real(X) + X = X.astype(max(min(X.dtype,self.domain.datatype),min(X.dtype,self.target.datatype))) + matrix = X-self.val + else: + raise ValueError(about._errors.cstring("ERROR: dimension mismatch ( "+str(np.size(X))+" <> "+str(self.nrow())+" x "+str(self.ncol())+" ).")) + return explicit_operator(self.domain,matrix,bare=True,sym=sym,uni=uni,target=self.target) + + def __isub__(self,X): ## __isub__ : self -= X + if(isinstance(X,operator)): + if(self.domain!=X.domain): + raise ValueError(about._errors.cstring("ERROR: inequal domains.")) + self.sym = (self.sym and X.sym) + self.uni = None + if(isinstance(X,explicit_operator)): + if(self.target!=X.target): + raise ValueError(about._errors.cstring("ERROR: inequal codomains.")) + X = X.val + elif(isinstance(X,diagonal_operator)): + if(self.target.dim(split=False)!=X.target.dim(split=False))or(not self.target.check_codomain(X.target)): + raise ValueError(about._errors.cstring("ERROR: incompatible codomains.")) + X = np.diag(X.diag(bare=True,domain=None),k=0) ## domain == X.domain + elif(isinstance(X,vecvec_operator)): + if(self.target!=X.target): + raise ValueError(about._errors.cstring("ERROR: inequal codomains.")) + X = np.tensordot(X.val,X.val,axes=0) + else: + raise TypeError(about._errors.cstring("ERROR: unsupported or incompatible operator.")) + elif(np.size(X)==1): + if(self.nrow()!=self.ncol()): + raise ValueError(about._errors.cstring("ERROR: identity ill-defined for "+str(self.nrow())+" x "+str(self.ncol())+" matrices.")) + self.uni = None + X = X*np.ones(self.domain.dim(split=False),dtype=np.int,order='C') + X = np.diag(self.domain.calc_weight(X,power=-1).astype(max(min(X.dtype,self.domain.datatype),min(X.dtype,self.target.datatype))).flatten(order='C'),k=0) + elif(np.size(X)==np.size(self.val)): + self.sym = None + self.uni = None + X = np.array(X).reshape(self.val.shape) + if(np.all(np.isreal(X))): + X = np.real(X) + X = X.astype(max(min(X.dtype,self.domain.datatype),min(X.dtype,self.target.datatype))) + else: + raise ValueError(about._errors.cstring("ERROR: dimension mismatch ( "+str(np.size(X))+" <> "+str(self.nrow())+" x "+str(self.ncol())+" ).")) + + ## subtract + if(X.dtype>self.val.dtype): + about.warnings.cprint("WARNING: datatype reset.") + self.val -= X + + ## check flags + self.sym,self.uni = self._check_flags(sym=self.sym,uni=self.uni) + + self._inv = None ## reset + + return self + + def _calc_mul(self,X,side): ## > multiplies self with X ... + if(side==0): ## ... from right + if(self._hidden[0]): ## hidden degrees of freedom + return np.array([np.apply_along_axis(self.domain.calc_dot,0,X,np.conjugate(rr)) for rr in self.weight(rowpower=1,colpower=0,overwrite=False)]) + else: + return np.dot(self.weight(rowpower=1,colpower=0,overwrite=False),X,out=None) + elif(side==1): ## ... from left + if(self._hidden[1]): ## hidden degrees of freedom + return np.array([np.apply_along_axis(self.target.calc_dot,0,self.weight(rowpower=0,colpower=1,overwrite=False),np.conjugate(rr)) for rr in X]) + else: + return np.dot(X,self.weight(rowpower=0,colpower=1,overwrite=False),out=None) + else: + raise ValueError(about._errors.cstring("ERROR: invalid input.")) + + def __mul__(self,X): ## __mul__ : self * X + if(isinstance(X,operator)): + if(self.domain!=X.target): + raise ValueError(about._errors.cstring("ERROR: incompatible spaces.")) + newdomain = X.domain + sym = None + uni = (self.uni and X.uni) + if(isinstance(X,explicit_operator)): + X = X.val + elif(isinstance(X,diagonal_operator)): + X = np.diag(X.diag(bare=True,domain=None),k=0) ## domain == X.domain + elif(isinstance(X,vecvec_operator)): + X = np.tensordot(X.val,X.val,axes=0) + else: + raise TypeError(about._errors.cstring("ERROR: unsupported or incompatible operator.")) + elif(np.size(X)==1): + newdomain = self.domain + sym = None + uni = None + X = X*np.ones(self.domain.dim(split=False),dtype=np.int,order='C') + X = np.diag(self.domain.calc_weight(X,power=-1).astype(max(min(X.dtype,self.domain.datatype),min(X.dtype,self.target.datatype))).flatten(order='C'),k=0) + elif(np.size(X)==self.val.shape[1]**2): + newdomain = self.domain + sym = None + uni = None + X = np.array(X).reshape((self.val.shape[1],self.val.shape[1])) + if(np.all(np.isreal(X))): + X = np.real(X) + X = X.astype(max(min(X.dtype,self.domain.datatype),min(X.dtype,self.target.datatype))) + else: + raise ValueError(about._errors.cstring("ERROR: dimension mismatch ( "+str(np.size(X))+" <> "+str(self.nrow())+" x "+str(self.nrow())+" ).")) + return explicit_operator(newdomain,self._calc_mul(X,0),bare=True,sym=sym,uni=uni,target=self.target) + + def __rmul__(self,X): ## __mul__ : X * self + if(isinstance(X,operator)): + if(X.domain!=self.target): + raise ValueError(about._errors.cstring("ERROR: incompatible spaces.")) + newtarget = X.target + sym = None + uni = (self.uni and X.uni) + if(isinstance(X,explicit_operator)): + X = X.val + elif(isinstance(X,diagonal_operator)): + X = np.diag(X.diag(bare=True,domain=None),k=0) ## domain == X.domain + elif(isinstance(X,vecvec_operator)): + X = np.tensordot(X.val,X.val,axes=0) + else: + raise TypeError(about._errors.cstring("ERROR: unsupported or incompatible operator.")) + elif(np.size(X)==1): + newtarget = self.target + sym = None + uni = None + X = X*np.ones(self.domain.dim(split=False),dtype=np.int,order='C') + X = np.diag(self.domain.calc_weight(X,power=-1).astype(max(min(X.dtype,self.domain.datatype),min(X.dtype,self.target.datatype))).flatten(order='C'),k=0) + elif(np.size(X)==self.val.shape[0]**2): + newtarget = self.target + sym = None + uni = None + X = np.array(X).reshape((self.val.shape[0],self.val.shape[0])) + if(np.all(np.isreal(X))): + X = np.real(X) + X = X.astype(max(min(X.dtype,self.domain.datatype),min(X.dtype,self.target.datatype))) + else: + raise ValueError(about._errors.cstring("ERROR: dimension mismatch ( "+str(np.size(X))+" <> "+str(self.ncol())+" x "+str(self.ncol())+" ).")) + return explicit_operator(self.domain,self._calc_mul(X,1),bare=True,sym=sym,uni=uni,target=newtarget) + + def __imul__(self,X): ## __imul__ : self *= X + if(isinstance(X,operator)): + if(self.domain!=X.target): + raise ValueError(about._errors.cstring("ERROR: incompatible spaces.")) + if(self.domain!=X.domain): + raise ValueError(about._errors.cstring("ERROR: incompatible operator.")) + self.sym = None + self.uni = (self.uni and X.uni) + if(isinstance(X,explicit_operator)): + X = X.val + elif(isinstance(X,diagonal_operator)): + X = np.diag(X.diag(bare=True,domain=None),k=0) ## domain == X.domain + elif(isinstance(X,vecvec_operator)): + X = np.tensordot(X.val,X.val,axes=0) + else: + raise TypeError(about._errors.cstring("ERROR: unsupported or incompatible operator.")) + elif(np.size(X)==1): + self.sym = None + self.uni = None + X = X*np.ones(self.domain.dim(split=False),dtype=np.int,order='C') + X = np.diag(self.domain.calc_weight(X,power=-1).astype(max(min(X.dtype,self.domain.datatype),min(X.dtype,self.target.datatype))).flatten(order='C'),k=0) + elif(np.size(X)==self.val.shape[1]**2): + self.sym = None + self.uni = None + X = np.array(X).reshape((self.val.shape[1],self.val.shape[1])) + if(np.all(np.isreal(X))): + X = np.real(X) + X = X.astype(max(min(X.dtype,self.domain.datatype),min(X.dtype,self.target.datatype))) + else: + raise ValueError(about._errors.cstring("ERROR: dimension mismatch ( "+str(np.size(X))+" <> "+str(self.nrow())+" x "+str(self.nrow())+" ).")) + + ## multiply + if(X.dtype>self.val.dtype): + about.warnings.cprint("WARNING: datatype reset.") + self.val = self._calc_mul(X,0) + + ## check flags + self.sym,self.uni = self._check_flags(sym=self.sym,uni=self.uni) + + self._inv = None ## reset + + return self + + def __div__(self,X): + raise Exception(about._errors.cstring("ERROR: matrix division ill-defined.")) + + __rdiv__ = __div__ + __idiv__ = __div__ + __truediv__ = __div__ + __rtruediv__ = __rdiv__ + __itruediv__ = __idiv__ + + def __pow__(self,x): ## __pow__(): self ** x + if(not isinstance(x,(int,long))): + raise TypeError(about._errors.cstring("ERROR: non-integer exponent.")) + elif(self.domain<>self.target): + raise ValueError(about._errors.cstring("ERROR: incompatible spaces.")) + elif(x==0): + return identity(self.domain) + elif(x<0): + return self.inverse().__pow__(-x) + elif(x==1): + return self + else: + matrix = self._calc_mul(self.val,0) + for ii in xrange(x-1): + matrix = self._calc_mul(matrix,0) + return explicit_operator(self.domain,matrix,bare=True,sym=None,uni=self.uni,target=self.target) + + def __rpow__(self,X): ## __pow__(): X ** self + raise Exception(about._errors.cstring("ERROR: matrix exponential ill-defined.")) + + def __ipow__(self,x): ## __pow__(): self **= x + if(not isinstance(x,(int,long))): + raise TypeError(about._errors.cstring("ERROR: non-integer exponent.")) + elif(self.domain<>self.target): + raise ValueError(about._errors.cstring("ERROR: incompatible spaces.")) + elif(x==0): + self.val = np.diag(self.domain.calc_weight(np.ones(self.domain.dim(split=False),dtype=np.int,order='C'),power=-1).astype(self.val.dtype),k=0) + elif(x<0): + self.val = (self.inverse().__pow__(-x)).val + elif(x==1): + pass + else: + matrix = self._calc_mul(self.val,0) + for ii in xrange(x-1): + matrix = self._calc_mul(matrix,0) + self.val = matrix + + ## check flags + self.sym,self.uni = self._check_flags(sym=None,uni=self.uni) + + self._inv = None ## reset + + return self + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + @staticmethod + def get_plot(X,title="",vmin=None,vmax=None,unit="",norm=None,cmap=None,cbar=True,**kwargs): + """ + Creates a plot of the matrix according to the given specifications. + + Parameters + ---------- + X : numpy.ndarray + Array containing the matrix. + + Returns + ------- + None + + Other parameters + ---------------- + title : string, *optional* + Title of the plot (default: ""). + vmin : float, *optional* + Minimum value to be displayed (default: ``min(x)``). + vmax : float, *optional* + Maximum value to be displayed (default: ``max(x)``). + unit : string, *optional* + Unit of the field values (default: ""). + norm : string, *optional* + Scaling of the field values before plotting (default: None). + cmap : matplotlib.colors.LinearSegmentedColormap, *optional* + Color map to be used for two-dimensional plots (default: None). + cbar : bool, *optional* + Whether to show the color bar or not (default: True). + save : string, *optional* + Valid file name where the figure is to be stored, by default + the figure is not saved (default: False). + + Notes + ----- + This method is static and thus also plot a valid numpy.ndarray. + + See Also + -------- + explicit_operator.plot + + Raises + ------ + ValueError + If the matrix `X` is not two-dimensional; + or if the logarithmic normalisation encounters negative values. + + """ + if(not pl.isinteractive())and(not bool(kwargs.get("save",False))): + about.warnings.cprint("WARNING: interactive mode off.") + + if(len(X.shape)!=2): + raise ValueError(about._errors.cstring("ERROR: invalid matirx.")) + + if(vmin is None): + vmin = np.min(X,axis=None,out=None) + if(vmax is None): + vmax = np.max(X,axis=None,out=None) + if(norm=="log")and(vmin<=0): + raise ValueError(about._errors.cstring("ERROR: nonpositive value(s).")) + + s_ = np.array([X.shape[1]/max(X.shape),X.shape[0]/max(X.shape)*(1.0+0.159*bool(cbar))]) + fig = pl.figure(num=None,figsize=(6.4*s_[0],6.4*s_[1]),dpi=None,facecolor=None,edgecolor=None,frameon=False,FigureClass=pl.Figure) + ax0 = fig.add_axes([0.06/s_[0],0.06/s_[1],1.0-0.12/s_[0],1.0-0.12/s_[1]]) + if(norm=="log"): + n_ = ln(vmin=vmin,vmax=vmax) + else: + n_ = None + sub = ax0.pcolormesh(X[::-1,:],cmap=cmap,norm=n_,vmin=vmin,vmax=vmax) + ax0.set_xlim(0,X.shape[1]) + ax0.set_xticks([],minor=False) + ax0.set_ylim(0,X.shape[0]) + ax0.set_yticks([],minor=False) + ax0.set_aspect("equal") + if(cbar): + if(norm=="log"): + f_ = lf(10,labelOnlyBase=False) + b_ = sub.norm.inverse(np.linspace(0,1,sub.cmap.N+1)) + v_ = np.linspace(sub.norm.vmin,sub.norm.vmax,sub.cmap.N) + else: + f_ = None + b_ = None + v_ = None + cb0 = fig.colorbar(sub,ax=ax0,orientation="horizontal",fraction=0.1,pad=0.05,shrink=0.75,aspect=20,ticks=[vmin,vmax],format=f_,drawedges=False,boundaries=b_,values=v_) + cb0.ax.text(0.5,-1.0,unit,fontdict=None,withdash=False,transform=cb0.ax.transAxes,horizontalalignment="center",verticalalignment="center") + ax0.set_title(title) + + if(bool(kwargs.get("save",False))): + fig.savefig(str(kwargs.get("save")),dpi=None,facecolor=None,edgecolor=None,orientation='portrait',papertype=None,format=None,transparent=False,bbox_inches=None,pad_inches=0.1) + pl.close(fig) + else: + fig.canvas.draw() + + def plot(self,bare=True,**kwargs): + """ + Creates a plot of the matrix according to the given specifications. + + Parameters + ---------- + bare : {bool, 2-tuple}, *optional* + Whether the returned matrix entries are `bare` or not + (default: True). + + Returns + ------- + None + + Other parameters + ---------------- + title : string, *optional* + Title of the plot (default: ""). + vmin : float, *optional* + Minimum value to be displayed (default: ``min(x)``). + vmax : float, *optional* + Maximum value to be displayed (default: ``max(x)``). + unit : string, *optional* + Unit of the field values (default: ""). + norm : string, *optional* + Scaling of the field values before plotting (default: None). + cmap : matplotlib.colors.LinearSegmentedColormap, *optional* + Color map to be used for two-dimensional plots (default: None). + cbar : bool, *optional* + Whether to show the color bar or not (default: True). + save : string, *optional* + Valid file name where the figure is to be stored, by default + the figure is not saved (default: False). + + 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. + + See Also + -------- + explicit_operator.get_plot + + """ + interactive = pl.isinteractive() + pl.matplotlib.interactive(not bool(kwargs.get("save",False))) + + X = self.get_matrix(bare=bare) + + if(np.any(np.iscomplex(X))): + about.infos.cprint("INFO: absolute values and phases are plotted.") + if(kwargs.has_key("title")): + title = kwargs.get("title")+" " + kwargs.__delitem__("title") + else: + title = "" + self.get_plot(np.absolute(X),title=title+"(absolute)",**kwargs) + if(kwargs.has_key("vmin")): + kwargs.__delitem__("vmin") + if(kwargs.has_key("vmin")): + kwargs.__delitem__("vmax") + if(kwargs.has_key("unit")): + kwargs["unit"] = "rad" + if(kwargs.has_key("norm")): + kwargs["norm"] = None + if(not kwargs.has_key("cmap")): + kwargs["cmap"] = pl.cm.hsv_r + self.get_plot(np.angle(X,deg=False),title=title+"(phase)",vmin=-3.1416,vmax=3.1416,**kwargs) ## values in [-pi,pi] + else: + self.get_plot(np.real(X),**kwargs) + + pl.matplotlib.interactive(interactive) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def __repr__(self): + return "<nifty_explicit.explicit_operator>" + +##----------------------------------------------------------------------------- + + + + + +##----------------------------------------------------------------------------- + +class _share(object): + + __init__ = None + + @staticmethod + def _init_share(_mat,_num): + _share.mat = _mat + _share.num = _num + +##----------------------------------------------------------------------------- + +##----------------------------------------------------------------------------- + +class explicit_probing(probing): + """ + .. + .. + .. __ __ __ __ + .. / / /__/ /__/ / /_ + .. _______ __ __ ______ / / __ _______ __ / _/ + .. / __ / \ \/ / / _ | / / / / / ____/ / / / / + .. / /____/ / / / /_/ / / /_ / / / /____ / / / /_ + .. \______/ /__/\__\ / ____/ \___/ /__/ \______/ /__/ \___/ probing class + .. /__/ + + + NIFTY subclass for explicit probing (using multiprocessing) + + Called after initialization, an explicit matrix representation of a + linear operator or function is sampled by applying (weighted) canonical + vectors in a specified basis. + + Parameters + ---------- + op : operator, *optional* + Operator to be probed; if not given, the probing will resort to + `function` (default: None). + function : function, *optional* + Function applied to the probes; either `op` or `function` must + be given (default: `op.times`). + domain : space, *optional* + Space wherein the probes live, defines the domain of the + explicified operator (default: `op.domain`). + codomain : space, *optional* + Space wherein the output of the explicified operator lives + (default: `op.target`). + target : space, *optional* + Space wherein the transform of the probes live (default: None). + ncpu : integer, *optional* + Number of used CPUs to use (default: 2). + nper : integer, *optional* + Number of tasks performed by one worker (default: 1). + + See Also + -------- + probing + + Examples + -------- + >>> v = field(point_space(3), val=[1, 2, 3]) + >>> W = vecvec_operator(val=v) # implicit operator + >>> W_ij = explicit_probing(Wim)(loop=True) # explicit operator + >>> W_ij.get_matrix() + array([[ 1., 2., 3.], + [ 2., 4., 6.], + [ 3., 6., 9.]]) + + Attributes + ---------- + function : function + Function applied to the probes. + domain : space, *optional* + Space wherein the probes live, defines the domain of the + explicified operator. + codomain : space, *optional* + Space wherein the output of the explicified operator lives. + target : space + Space wherein the transform of the probes live. + ncpu : integer + Number of used CPUs to use. + nper : integer + Number of tasks performed by one worker. + quargs : dict + Keyword arguments passed to `function` in each call. + + """ + def __init__(self,op=None,function=None,domain=None,codomain=None,target=None,ncpu=2,nper=1,**quargs): + """ + Initializes the explicit probing and sets the standard probing + attributes except for `random`, `nrun`, and `var`. + + Parameters + ---------- + op : operator, *optional* + Operator to be probed; if not given, the probing will resort to + `function` (default: None). + function : function, *optional* + Function applied to the probes; either `op` or `function` must + be given (default: `op.times`). + domain : space, *optional* + Space wherein the probes live, defines the domain of the + explicified operator (default: `op.domain`). + codomain : space, *optional* + Space wherein the output of the explicified operator lives + (default: `op.target`). + target : space, *optional* + Space wherein the transform of the probes live (default: None). + ncpu : integer, *optional* + Number of used CPUs to use (default: 2). + nper : integer, *optional* + Number of tasks performed by one worker (default: 1). + + Other Parameters + ---------------- + quargs : dict + Keyword arguments passed to `function` in each call. + + Raises + ------ + TypeError + If input is invalid or insufficient. + NameError + If `function` is not an attribute of `op`. + ValueError + If spaces are incompatible. + + """ + if(op is None): + ## check whether callable + if(function is None)or(not hasattr(function,"__call__")): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + ## check given domain + if(domain is None): + raise TypeError(about._errors.cstring("ERROR: insufficient input.")) + elif(not isinstance(domain,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input domain.")) + ## check given codomain + if(codomain is None): + raise TypeError(about._errors.cstring("ERROR: insufficient input.")) + elif(not isinstance(codomain,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input codomain.")) + else: + if(not isinstance(op,operator)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + ## check whether callable + if(function is None)or(not hasattr(function,"__call__")): + function = op.times + elif(op==function): + function = op.times + ## check whether correctly bound + if(op!=function.im_self): + raise NameError(about._errors.cstring("ERROR: invalid input function.")) + ## check given domain + if(domain is None)or(not isinstance(domain,space)): + if(function in [op.inverse_times,op.adjoint_times]): + domain = op.target + else: + domain = op.domain + else: + if(function in [op.inverse_times,op.adjoint_times]): + op.target.check_codomain(domain) ## a bit pointless + else: + op.domain.check_codomain(domain) ## a bit pointless + ## check given codomain + if(codomain is None)or(not isinstance(codomain,space)): + if(function in [op.inverse_times,op.adjoint_times]): + codomain = op.domain + else: + codomain = op.target + else: + if(function in [op.inverse_times,op.adjoint_times]): + if(not op.domain.check_codomain(domain))and(op.domain.dim(split=False)!=codomain.dim(split=False)): + raise ValueError(about._errors.cstring("ERROR: incompatible input codomain.")) + else: + if(not op.target.check_codomain(domain))and(op.target.dim(split=False)!=codomain.dim(split=False)): + raise ValueError(about._errors.cstring("ERROR: incompatible input codomain.")) + + self.function = function + self.domain = domain + self.codomain = codomain + + ## check target + if(target is None): + target = self.domain.get_codomain() + else: + self.domain.check_codomain(target) ## a bit pointless + self.target = target + + ## check shape + if(self.domain.dim(split=False)*self.codomain.dim(split=False)>1048576): + about.warnings.cprint("WARNING: matrix size > 2 ** 20.") + + self.ncpu = int(max(1,ncpu)) + self.nrun = self.domain.dim(split=False) + if(nper is None): + self.nper = None + else: + self.nper = int(max(1,min(self.nrun//self.ncpu,nper))) + + self.quargs = quargs + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def configure(self,**kwargs): + """ + Changes the performance attributes of the instance. + + Parameters + ---------- + ncpu : integer, *optional* + Number of used CPUs to use. + nper : integer, *optional* + Number of tasks performed by one worker. + + """ + if("ncpu" in kwargs): + self.ncpu = int(max(1,kwargs.get("ncpu"))) + if("nper" in kwargs): + if(kwargs.get("nper") is None): + self.nper = None + else: + self.nper = int(max(1,min(self.nrun//self.ncpu,kwargs.get("nper")))) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def gen_probe(self,index,value): + """ + Generates a probe. + + For explicit probing, each probe is a (weighted) canonical base. + + Parameters + ---------- + index : int + Index where to put the value. + value : scalar + Weighted 1. + + Returns + ------- + probe : field + Weighted canonical base. + + """ + probe = field(self.domain,val=None,target=self.target) + probe[np.unravel_index(index,self.domain.dim(split=True),order='C')] = value + return probe + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def probing(self,idnum,probe): + """ + Computes a single probing result meaning a single column of the + linear operators matrix representation. + + Parameters + ---------- + probe : field + Weighted canonical base on which `function` will be applied. + idnum : int + Identification number; obsolete. + + Returns + ------- + result : ndarray + Result of function evaluation (equaling a column of the matrix). + + """ + f = self.function(probe,**self.quargs) ## field + if(f is None): + return None + elif(isinstance(f,field)): + if(f.domain!=self.codomain): + try: + f.transform(target=self.codomain,overwrite=True) + except(ValueError): ## unsupported transformation + pass ## checkless version of f.cast_domain(self.codomain,newtarget=None,force=True) + return f.val.flatten(order='C') + else: + return f.flatten(order='C') + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def evaluate(self,matrix,num): + """ + Evaluates the probing results. + + Parameters + ---------- + matrix : numpy.array + Matrix representation of the probed linear operator. + num : int + Number of successful probings (not returning ``None``). + + Returns + ------- + result : explicit_operator + The probed linear operator as explicit operator instance. + + """ + if(num<self.nrun): + about.infos.cflush(" ( %u probe(s) failed, effectiveness == %.1f%% )\n"%(self.nrun-num,100*num/self.nrun)) + if(num==0): + about.warnings.cprint("WARNING: probing failed.") + return None + else: + about.infos.cflush("\n") + + return explicit_operator(self.domain,matrix,bare=True,sym=None,uni=None,target=self.codomain) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _single_probing(self,zipped): ## > performs one probing operation + ## generate probe + probe = self.gen_probe(*zipped) + ## do the actual probing + return self.probing(zipped[0],probe) + + def _serial_probing(self,zipped): ## > performs the probing operation serially + try: + result = self._single_probing(zipped) + except Exception as exception: + raise exception + except BaseException: ## capture system-exiting exception including KeyboardInterrupt + raise Exception(about._errors.cstring("ERROR: unknown.")) + else: + if(result is not None): + result = np.array(result).flatten(order='C') + rindex = zipped[0]*self.codomain.dim(split=False) + if(isinstance(_share.mat,tuple)): + _share.mat[0].acquire(block=True,timeout=None) + _share.mat[0][rindex:rindex+self.codomain.dim(split=False)] = np.real(result) + _share.mat[0].release() + _share.mat[1].acquire(block=True,timeout=None) + _share.mat[1][rindex:rindex+self.codomain.dim(split=False)] = np.imag(result) + _share.mat[1].release() + else: + _share.mat.acquire(block=True,timeout=None) + _share.mat[rindex:rindex+self.codomain.dim(split=False)] = result + _share.mat.release() + _share.num.acquire(block=True,timeout=None) + _share.num.value += 1 + _share.num.release() + self._progress(_share.num.value) + + def _parallel_probing(self): ## > performs the probing operations in parallel + ## define weighted canonical base + base = self.domain.calc_weight(self.domain.enforce_values(1,extend=True),power=-1).flatten(order='C') + ## define shared objects + if(issubclass(self.codomain.datatype,np.complexfloating)): + _mat = (ma('d',np.empty(self.nrun*self.codomain.dim(split=False),dtype=np.float64,order='C'),lock=True),ma('d',np.empty(self.nrun*self.codomain.dim(split=False),dtype=np.float64,order='C'),lock=True)) ## tuple(real,imag) + else: + _mat = ma('d',np.empty(self.nrun*self.codomain.dim(split=False),dtype=np.float64,order='C'),lock=True) + _num = mv('I',0,lock=True) + ## build pool + if(about.infos.status): + so.write(about.infos.cstring("INFO: multiprocessing "+(' ')*10)) + so.flush() + pool = mp(processes=self.ncpu,initializer=_share._init_share,initargs=(_mat,_num),maxtasksperchild=self.nper) + try: + ## pooling + pool.map_async(self._serial_probing,zip(np.arange(self.nrun,dtype=np.int),base),chunksize=None,callback=None).get(timeout=None) + ## close and join pool + about.infos.cflush(" done.") + pool.close() + pool.join() + except BaseException as exception: + ## terminate and join pool + about._errors.cprint("\nERROR: terminating pool.") + pool.terminate() + pool.join() + ## re-raise exception + raise exception ## traceback by looping + ## evaluate + if(issubclass(self.codomain.datatype,np.complexfloating)): + _mat = (np.array(_mat[0][:])+np.array(_mat[1][:])*1j) ## comlpex array + else: + _mat = np.array(_mat[:]) + _mat = _mat.reshape((self.nrun,self.codomain.dim(split=False))).T + return self.evaluate(_mat,_num.value) + + def _nonparallel_probing(self): ## > performs the probing operations one after another + ## define weighted canonical base + base = self.domain.calc_weight(self.domain.enforce_values(1,extend=True),power=-1).flatten(order='C') + ## looping + if(about.infos.status): + so.write(about.infos.cstring("INFO: looping "+(' ')*10)) + so.flush() + _mat = np.empty((self.nrun,self.codomain.dim(split=False)),dtype=self.codomain.datatype,order='C') + _num = 0 + for ii in xrange(self.nrun): + result = self._single_probing((ii,base[ii])) ## zipped tuple + if(result is None): + _mat[ii] = np.zeros(self.codomain.dim(split=False),dtype=self.codomain.datatype) + else: + _mat[ii] = np.array(result,dtype=self.codomain.datatype) + _num += 1 + self._progress(_num) + about.infos.cflush(" done.") + ## evaluate + return self.evaluate(_mat.T,_num) + + def __call__(self,loop=False,**kwargs): + """ + Start the explicit probing procedure. + + Parameters + ---------- + loop : bool, *optional* + Whether to perform a loop or to parallelise (default: False). + + Returns + ------- + result : explicit_operator + The probed linear operator as explicit operator instance. + + Other Parameters + ---------------- + ncpu : integer, *optional* + Number of used CPUs to use. + nper : integer, *optional* + Number of tasks performed by one worker. + + + + """ + self.configure(**kwargs) + if(not about.multiprocessing.status)or(loop): + return self._nonparallel_probing() + else: + return self._parallel_probing() + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def __repr__(self): + return "<nifty_explicit.explicit_probing>" + +##----------------------------------------------------------------------------- + +##----------------------------------------------------------------------------- + +def explicify(op,newdomain=None,newtarget=None,ncpu=2,nper=1,loop=False,**quargs): + """ + Explicifys an (implicit) operator. + + This function wraps the :py:class:`explicit_probing` class with a more + user-friendly interface. + + Parameters + ---------- + op : operator + Operator to be explicified. + newdomain : space, *optional* + Space wherein the probes live, defines the domain of the + explicified operator (default: `op.domain`). + newtarget : space, *optional* + Space wherein the output of the explicified operator lives + (default: `op.target`). + ncpu : integer, *optional* + Number of used CPUs to use (default: 2). + nper : integer, *optional* + Number of tasks performed by one worker (default: 1). + + Returns + ------- + EO : explicit_operator + The explicified linear operator as explicit operator instance. + + Other Parameters + ---------------- + quargs : dict + Keyword arguments passed to `function` in each call. + + See Also + -------- + explicit_probing + + Examples + -------- + >>> x_space = rg_space(4) + >>> k_space = x_space.get_codomain() + >>> S = power_operator(k_space, spec=[9, 3, 1]) + >>> S.diag() # implicit operator in k_space + array([ 1., 3., 9., 3.]) + >>> S_kq = explicify(S) + >>> S_kq.get_matrix() # explicit operator in k_space + array([[ 1., 0., 0., 0.], + [ 0., 3., 0., 0.], + [ 0., 0., 9., 0.], + [ 0., 0., 0., 3.]]) + >>> S_xy = explicify(S, newdomain=x_space) + >>> S_xy.get_matrix() # explicit operator in x_space + array([[ 16., 8., 4., 8.], + [ 8., 16., 8., 4.], + [ 4., 8., 16., 8.], + [ 8., 4., 8., 16.]]) + + Raises + ------ + TypeError + If `op` is no operator instance. + + """ + if(not isinstance(op,operator)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + elif(newdomain is not None)and(newtarget is None)and(op.domain==op.target): + newtarget = newdomain + if(newdomain is None)or(newdomain==op.domain): + target_ = None + else: + target_ = op.domain + return explicit_probing(op=op,function=op.times,domain=newdomain,codomain=newtarget,target=target_,ncpu=ncpu,nper=nper,**quargs)(loop=loop) + +##----------------------------------------------------------------------------- + diff --git a/operators/nifty_operators.py b/operators/nifty_operators.py new file mode 100644 index 0000000000000000000000000000000000000000..894466601259186e243a4278725a8f6cee4d64cf --- /dev/null +++ b/operators/nifty_operators.py @@ -0,0 +1,3477 @@ +## NIFTY (Numerical Information Field Theory) has been developed at the +## Max-Planck-Institute for Astrophysics. +## +## Copyright (C) 2015 Max-Planck-Society +## +## Author: Marco Selig +## Project homepage: <http://www.mpa-garching.mpg.de/ift/nifty/> +## +## 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 __future__ import division +import numpy as np +from nifty.nifty_about import about +from nifty.nifty_core import space, \ + point_space, \ + nested_space, \ + field +from nifty.nifty_tools import conjugate_gradient +from nifty_probing import trace_probing, \ + diagonal_probing + +##============================================================================= + +class operator(object): + """ + .. __ + .. / /_ + .. ______ ______ _______ _____ ____ __ / _/ ______ _____ + .. / _ | / _ | / __ / / __/ / _ / / / / _ | / __/ + .. / /_/ / / /_/ / / /____/ / / / /_/ / / /_ / /_/ / / / + .. \______/ / ____/ \______/ /__/ \______| \___/ \______/ /__/ class + .. /__/ + + 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 diagonal operator class) must be derived. + + Parameters + ---------- + domain : space + The space wherein valid arguments live. + sym : bool, *optional* + Indicates whether the operator is self-adjoint or not + (default: False) + uni : bool, *optional* + Indicates whether the operator is unitary or not + (default: False) + imp : bool, *optional* + Indicates whether the incorporation of volume weights in + multiplications is already implemented in the `multiply` + instance methods or not (default: False) + target : space, *optional* + The space wherein the operator output lives (default: domain) + para : {single object, list of objects}, *optional* + This is a freeform list of parameters that derivatives of the + operator class can use. Not used in the base operators. + (default: None) + + See Also + -------- + diagonal_operator : An operator class for handling purely diagonal + operators. + power_operator : Similar to diagonal_operator but with handy features + for dealing with diagonal operators whose diagonal + consists of a power spectrum. + vecvec_operator : Operators constructed from the outer product of two + fields + response_operator : Implements a modeled instrument response which + translates a signal into data space. + projection_operator : An operator that projects out one or more + components in a basis, e.g. a spectral band + of Fourier components. + + Attributes + ---------- + domain : space + The space wherein valid arguments live. + sym : bool + Indicates whether the operator is self-adjoint or not + uni : bool + Indicates whether the operator is unitary or not + imp : bool + Indicates whether the incorporation of volume weights in + multiplications is already implemented in the `multiply` + instance methods or not + target : space + The space wherein the operator output lives + para : {single object, list of objects} + This is a freeform list of parameters that derivatives of the + operator class can use. Not used in the base operators. + """ + def __init__(self,domain,sym=False,uni=False,imp=False,target=None,para=None): + """ + Sets the attributes for an operator class instance. + + Parameters + ---------- + domain : space + The space wherein valid arguments live. + sym : bool, *optional* + Indicates whether the operator is self-adjoint or not + (default: False) + uni : bool, *optional* + Indicates whether the operator is unitary or not + (default: False) + imp : bool, *optional* + Indicates whether the incorporation of volume weights in + multiplications is already implemented in the `multiply` + instance methods or not (default: False) + target : space, *optional* + The space wherein the operator output lives (default: domain) + para : {object, list of objects}, *optional* + This is a freeform list of parameters that derivatives of the + operator class can use. Not used in the base operators. + (default: None) + + Returns + ------- + None + """ + if(not isinstance(domain,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + self.domain = domain + self.sym = bool(sym) + self.uni = bool(uni) + + if(target is None)or(self.sym)or(self.uni): + target = self.domain + elif(not isinstance(target,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + self.target = target + + if(self.domain.discrete)and(self.target.discrete): + self.imp = True + else: + self.imp = bool(imp) + + if(para is not None): + self.para = para + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def nrow(self): + """ + Computes the number of rows. + + Returns + ------- + nrow : int + number of rows (equal to the dimension of the codomain) + """ + return self.target.dim(split=False) + + def ncol(self): + """ + Computes the number of columns + + Returns + ------- + ncol : int + number of columns (equal to the dimension of the domain) + """ + return self.domain.dim(split=False) + + def dim(self,axis=None): + """ + Computes the dimension of the space + + Parameters + ---------- + axis : int, *optional* + Axis along which the dimension is to be calculated. + (default: None) + + Returns + ------- + dim : {int, ndarray} + The dimension(s) of the operator. + + """ + if(axis is None): + return np.array([self.nrow(),self.ncol()]) + elif(axis==0): + return self.nrow() + elif(axis==1): + return self.ncol() + else: + raise ValueError(about._errors.cstring("ERROR: invalid input axis.")) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def set_para(self,newpara): + """ + Sets the parameters and creates the `para` property if it does + not exist + + Parameters + ---------- + newpara : {object, list of objects} + A single parameter or a list of parameters. + + Returns + ------- + None + + """ + self.para = newpara + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _multiply(self,x,**kwargs): ## > applies the operator to a given field + raise NotImplementedError(about._errors.cstring("ERROR: no generic instance method 'multiply'.")) + + def _adjoint_multiply(self,x,**kwargs): ## > applies the adjoint operator to a given field + raise NotImplementedError(about._errors.cstring("ERROR: no generic instance method 'adjoint_multiply'.")) + + def _inverse_multiply(self,x,**kwargs): ## > applies the inverse operator to a given field + raise NotImplementedError(about._errors.cstring("ERROR: no generic instance method 'inverse_multiply'.")) + + def _adjoint_inverse_multiply(self,x,**kwargs): ## > applies the inverse adjoint operator to a given field + raise NotImplementedError(about._errors.cstring("ERROR: no generic instance method 'adjoint_inverse_multiply'.")) + + def _inverse_adjoint_multiply(self,x,**kwargs): ## > applies the adjoint inverse operator to a given field + raise NotImplementedError(about._errors.cstring("ERROR: no generic instance method 'inverse_adjoint_multiply'.")) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _briefing(self,x,domain,inverse): ## > prepares x for `multiply` + ## inspect x + if(not isinstance(x,field)): + x_ = field(domain,val=x,target=None) + else: + ## check x.domain + if(domain==x.domain): + x_ = x + ## transform + else: + x_ = x.transform(target=domain,overwrite=False) + ## weight if ... + if(not self.imp)and(not domain.discrete)and(not inverse): + x_ = x_.weight(power=1,overwrite=False) + return x_ + + def _debriefing(self,x,x_,target,inverse): ## > evaluates x and x_ after `multiply` + if(x_ is None): + return None + else: + ## inspect x_ + if(not isinstance(x_,field)): + x_ = field(target,val=x_,target=None) + elif(x_.domain!=target): + raise ValueError(about._errors.cstring("ERROR: invalid output domain.")) + ## weight if ... + if(not self.imp)and(not target.discrete)and(inverse): + x_ = x_.weight(power=-1,overwrite=False) + ## inspect x + if(isinstance(x,field)): + ## repair ... + if(self.domain==self.target!=x.domain): + x_ = x_.transform(target=x.domain,overwrite=False) ## ... domain + if(x_.domain==x.domain)and(x_.target!=x.target): + x_.set_target(newtarget=x.target) ## ... codomain + return x_ + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def times(self,x,**kwargs): + """ + Applies the operator to a given object + + Parameters + ---------- + x : {scalar, ndarray, field} + Scalars are interpreted as constant arrays, and an array will + be interpreted as a field on the domain of the operator. + + Returns + ------- + Ox : field + Mapped field on the target domain of the operator. + """ + ## prepare + x_ = self._briefing(x,self.domain,False) + ## apply operator + x_ = self._multiply(x_,**kwargs) + ## evaluate + return self._debriefing(x,x_,self.target,False) + + def __call__(self,x,**kwargs): + return self.times(x,**kwargs) + + def adjoint_times(self,x,**kwargs): + """ + Applies the adjoint operator to a given object. + + Parameters + ---------- + x : {scalar, ndarray, field} + Scalars are interpreted as constant arrays, and an array will + be interpreted as a field on the target space of the operator. + + Returns + ------- + OAx : field + Mapped field on the domain of the operator. + + """ + ## check whether self-adjoint + if(self.sym): + return self.times(x,**kwargs) + ## check whether unitary + if(self.uni): + return self.inverse_times(x,**kwargs) + + ## prepare + x_ = self._briefing(x,self.target,False) + ## apply operator + x_ = self._adjoint_multiply(x_,**kwargs) + ## evaluate + return self._debriefing(x,x_,self.domain,False) + + def inverse_times(self,x,**kwargs): + """ + Applies the inverse operator to a given object. + + Parameters + ---------- + x : {scalar, ndarray, field} + Scalars are interpreted as constant arrays, and an array will + be interpreted as a field on the domain space of the operator. + + Returns + ------- + OIx : field + Mapped field on the target space of the operator. + """ + ## check whether self-inverse + if(self.sym)and(self.uni): + return self.times(x,**kwargs) + + ## prepare + x_ = self._briefing(x,self.target,True) + ## apply operator + x_ = self._inverse_multiply(x_,**kwargs) + ## evaluate + return self._debriefing(x,x_,self.domain,True) + + def adjoint_inverse_times(self,x,**kwargs): + """ + Applies the inverse adjoint operator to a given object. + + Parameters + ---------- + x : {scalar, ndarray, field} + Scalars are interpreted as constant arrays, and an array will + be interpreted as a field on the target space of the operator. + + Returns + ------- + OAIx : field + Mapped field on the domain of the operator. + + """ + ## check whether self-adjoint + if(self.sym): + return self.inverse_times(x,**kwargs) + ## check whether unitary + if(self.uni): + return self.times(x,**kwargs) + + ## prepare + x_ = self._briefing(x,self.domain,True) + ## apply operator + x_ = self._adjoint_inverse_multiply(x_,**kwargs) + ## evaluate + return self._debriefing(x,x_,self.target,True) + + def inverse_adjoint_times(self,x,**kwargs): + """ + Applies the adjoint inverse operator to a given object. + + Parameters + ---------- + x : {scalar, ndarray, field} + Scalars are interpreted as constant arrays, and an array will + be interpreted as a field on the target space of the operator. + + Returns + ------- + OIAx : field + Mapped field on the domain of the operator. + + """ + ## check whether self-adjoint + if(self.sym): + return self.inverse_times(x,**kwargs) + ## check whether unitary + if(self.uni): + return self.times(x,**kwargs) + + ## prepare + x_ = self._briefing(x,self.domain,True) + ## apply operator + x_ = self._inverse_adjoint_multiply(x_,**kwargs) + ## evaluate + return self._debriefing(x,x_,self.target,True) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def tr(self,domain=None,target=None,random="pm1",ncpu=2,nrun=8,nper=1,var=False,loop=False,**kwargs): + """ + Computes the trace of the operator + + Parameters + ---------- + domain : space, *optional* + space wherein the probes live (default: self.domain) + target : space, *optional* + space wherein the transform of the probes live + (default: None, applies target of the domain) + random : string, *optional* + Specifies the pseudo random number generator. Valid + options are "pm1" for a random vector of +/-1, or "gau" + for a random vector with entries drawn from a Gaussian + distribution with zero mean and unit variance. + (default: "pm1") + ncpu : int, *optional* + number of used CPUs to use. (default: 2) + nrun : int, *optional* + total number of probes (default: 8) + nper : int, *optional* + number of tasks performed by one process (default: 1) + var : bool, *optional* + Indicates whether to additionally return the probing variance + or not (default: False). + loop : bool, *optional* + Indicates whether or not to perform a loop i.e., to + parallelise (default: False) + + Returns + ------- + tr : float + Trace of the operator + delta : float, *optional* + Probing variance of the trace. Returned if `var` is True in + of probing case. + + See Also + -------- + probing : The class used to perform probing operations + """ + if(domain is None): + domain = self.domain + return trace_probing(self,function=self.times,domain=domain,target=target,random=random,ncpu=(ncpu,1)[bool(loop)],nrun=nrun,nper=nper,var=var,**kwargs)(loop=loop) + + def inverse_tr(self,domain=None,target=None,random="pm1",ncpu=2,nrun=8,nper=1,var=False,loop=False,**kwargs): + """ + Computes the trace of the inverse operator + + Parameters + ---------- + domain : space, *optional* + space wherein the probes live (default: self.domain) + target : space, *optional* + space wherein the transform of the probes live + (default: None, applies target of the domain) + random : string, *optional* + Specifies the pseudo random number generator. Valid + options are "pm1" for a random vector of +/-1, or "gau" + for a random vector with entries drawn from a Gaussian + distribution with zero mean and unit variance. + (default: "pm1") + ncpu : int, *optional* + number of used CPUs to use. (default: 2) + nrun : int, *optional* + total number of probes (default: 8) + nper : int, *optional* + number of tasks performed by one process (default: 1) + var : bool, *optional* + Indicates whether to additionally return the probing variance + or not (default: False). + loop : bool, *optional* + Indicates whether or not to perform a loop i.e., to + parallelise (default: False) + + Returns + ------- + tr : float + Trace of the inverse operator + delta : float, *optional* + Probing variance of the trace. Returned if `var` is True in + of probing case. + + See Also + -------- + probing : The class used to perform probing operations + """ + if(domain is None): + domain = self.target + return trace_probing(self,function=self.inverse_times,domain=domain,target=target,random=random,ncpu=(ncpu,1)[bool(loop)],nrun=nrun,nper=nper,var=var,**kwargs)(loop=loop) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def diag(self,bare=False,domain=None,target=None,random="pm1",ncpu=2,nrun=8,nper=1,var=False,save=False,path="tmp",prefix="",loop=False,**kwargs): + """ + Computes the diagonal of the operator via probing. + + Parameters + ---------- + bare : bool, *optional* + Indicatese whether the diagonal entries are `bare` or not + (mandatory for the correct incorporation of volume weights) + (default: False) + domain : space, *optional* + space wherein the probes live (default: self.domain) + target : space, *optional* + space wherein the transform of the probes live + (default: None, applies target of the domain) + random : string, *optional* + Specifies the pseudo random number generator. Valid + options are "pm1" for a random vector of +/-1, or "gau" + for a random vector with entries drawn from a Gaussian + distribution with zero mean and unit variance. + (default: "pm1") + ncpu : int, *optional* + number of used CPUs to use. (default: 2) + nrun : int, *optional* + total number of probes (default: 8) + nper : int, *optional* + number of tasks performed by one process (default: 1) + var : bool, *optional* + Indicates whether to additionally return the probing variance + or not (default: False). + save : bool, *optional* + whether all individual probing results are saved or not + (default: False) + path : string, *optional* + path wherein the results are saved (default: "tmp") + prefix : string, *optional* + prefix for all saved files (default: "") + loop : bool, *optional* + Indicates whether or not to perform a loop i.e., to + parallelise (default: False) + + Returns + ------- + diag : ndarray + The matrix diagonal + delta : float, *optional* + Probing variance of the trace. Returned if `var` is True in + of probing case. + + See Also + -------- + probing : The class used to perform probing operations + + 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. + + """ + if(domain is None): + domain = self.domain + diag = diagonal_probing(self,function=self.times,domain=domain,target=target,random=random,ncpu=(ncpu,1)[bool(loop)],nrun=nrun,nper=nper,var=var,save=save,path=path,prefix=prefix,**kwargs)(loop=loop) + if(diag is None): +# about.warnings.cprint("WARNING: forwarding 'NoneType'.") + return None + ## weight if ... + elif(not domain.discrete)and(bare): + if(isinstance(diag,tuple)): ## diag == (diag,variance) + return domain.calc_weight(diag[0],power=-1),domain.calc_weight(diag[1],power=-1) + else: + return domain.calc_weight(diag,power=-1) + else: + return diag + + def inverse_diag(self,bare=False,domain=None,target=None,random="pm1",ncpu=2,nrun=8,nper=1,var=False,save=False,path="tmp",prefix="",loop=False,**kwargs): + """ + Computes the diagonal of the inverse operator via probing. + + Parameters + ---------- + bare : bool, *optional* + Indicatese whether the diagonal entries are `bare` or not + (mandatory for the correct incorporation of volume weights) + (default: False) + domain : space, *optional* + space wherein the probes live (default: self.domain) + target : space, *optional* + space wherein the transform of the probes live + (default: None, applies target of the domain) + random : string, *optional* + Specifies the pseudo random number generator. Valid + options are "pm1" for a random vector of +/-1, or "gau" + for a random vector with entries drawn from a Gaussian + distribution with zero mean and unit variance. + (default: "pm1") + ncpu : int, *optional* + number of used CPUs to use. (default: 2) + nrun : int, *optional* + total number of probes (default: 8) + nper : int, *optional* + number of tasks performed by one process (default: 1) + var : bool, *optional* + Indicates whether to additionally return the probing variance + or not (default: False). + save : bool, *optional* + whether all individual probing results are saved or not + (default: False) + path : string, *optional* + path wherein the results are saved (default: "tmp") + prefix : string, *optional* + prefix for all saved files (default: "") + loop : bool, *optional* + Indicates whether or not to perform a loop i.e., to + parallelise (default: False) + + Returns + ------- + diag : ndarray + The diagonal of the inverse matrix + delta : float, *optional* + Probing variance of the trace. Returned if `var` is True in + of probing case. + + See Also + -------- + probing : The class used to perform probing operations + + 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. + + """ + if(domain is None): + domain = self.target + diag = diagonal_probing(self,function=self.inverse_times,domain=domain,target=target,random=random,ncpu=(ncpu,1)[bool(loop)],nrun=nrun,nper=nper,var=var,save=save,path=path,prefix=prefix,**kwargs)(loop=loop) + if(diag is None): +# about.warnings.cprint("WARNING: forwarding 'NoneType'.") + return None + ## weight if ... + elif(not domain.discrete)and(bare): + if(isinstance(diag,tuple)): ## diag == (diag,variance) + return domain.calc_weight(diag[0],power=-1),domain.calc_weight(diag[1],power=-1) + else: + return domain.calc_weight(diag,power=-1) + else: + return diag + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def det(self): + """ + Computes the determinant of the operator. + + Returns + ------- + det : float + The determinant + + """ + raise NotImplementedError(about._errors.cstring("ERROR: no generic instance method 'det'.")) + + def inverse_det(self): + """ + Computes the determinant of the inverse operator. + + Returns + ------- + det : float + The determinant + + """ + raise NotImplementedError(about._errors.cstring("ERROR: no generic instance method 'inverse_det'.")) + + def log_det(self): + """ + Computes the logarithm of the determinant of the operator (if applicable). + + Returns + ------- + logdet : float + The logarithm of the determinant + + """ + raise NotImplementedError(about._errors.cstring("ERROR: no generic instance method 'log_det'.")) + + def tr_log(self): + """ + Computes the trace of the logarithm of the operator (if applicable). + + Returns + ------- + logdet : float + The trace of the logarithm + + """ + return self.log_det() + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def hat(self,bare=False,domain=None,target=None,**kwargs): + """ + Translates the operator's diagonal into a field + + Parameters + ---------- + bare : bool, *optional* + Indicatese whether the diagonal entries are `bare` or not + (mandatory for the correct incorporation of volume weights) + (default: False) + domain : space, *optional* + space wherein the probes live (default: self.domain) + target : space, *optional* + space wherein the transform of the probes live + (default: None, applies target of the domain) + random : string, *optional* + Specifies the pseudo random number generator. Valid + options are "pm1" for a random vector of +/-1, or "gau" + for a random vector with entries drawn from a Gaussian + distribution with zero mean and unit variance. + (default: "pm1") + ncpu : int, *optional* + number of used CPUs to use. (default: 2) + nrun : int, *optional* + total number of probes (default: 8) + nper : int, *optional* + number of tasks performed by one process (default: 1) + save : bool, *optional* + whether all individual probing results are saved or not + (default: False) + path : string, *optional* + path wherein the results are saved (default: "tmp") + prefix : string, *optional* + prefix for all saved files (default: "") + loop : bool, *optional* + Indicates whether or not to perform a loop i.e., to + parallelise (default: False) + + Returns + ------- + x : field + The matrix diagonal as a field living on the operator + domain space + + See Also + -------- + probing : The class used to perform probing operations + + 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. + + """ + if(domain is None): + domain = self.domain + diag = self.diag(bare=bare,domain=domain,target=target,var=False,**kwargs) + if(diag is None): + about.warnings.cprint("WARNING: forwarding 'NoneType'.") + return None + return field(domain,val=diag,target=target) + + def inverse_hat(self,bare=False,domain=None,target=None,**kwargs): + """ + Translates the inverse operator's diagonal into a field + + Parameters + ---------- + bare : bool, *optional* + Indicatese whether the diagonal entries are `bare` or not + (mandatory for the correct incorporation of volume weights) + (default: False) + domain : space, *optional* + space wherein the probes live (default: self.domain) + target : space, *optional* + space wherein the transform of the probes live + (default: None, applies target of the domain) + random : string, *optional* + Specifies the pseudo random number generator. Valid + options are "pm1" for a random vector of +/-1, or "gau" + for a random vector with entries drawn from a Gaussian + distribution with zero mean and unit variance. + (default: "pm1") + ncpu : int, *optional* + number of used CPUs to use. (default: 2) + nrun : int, *optional* + total number of probes (default: 8) + nper : int, *optional* + number of tasks performed by one process (default: 1) + save : bool, *optional* + whether all individual probing results are saved or not + (default: False) + path : string, *optional* + path wherein the results are saved (default: "tmp") + prefix : string, *optional* + prefix for all saved files (default: "") + loop : bool, *optional* + Indicates whether or not to perform a loop i.e., to + parallelise (default: False) + + Returns + ------- + x : field + The matrix diagonal as a field living on the operator + domain space + + See Also + -------- + probing : The class used to perform probing operations + + 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. + + """ + if(domain is None): + domain = self.target + diag = self.inverse_diag(bare=bare,domain=domain,target=target,var=False,**kwargs) + if(diag is None): + about.warnings.cprint("WARNING: forwarding 'NoneType'.") + return None + return field(domain,val=diag,target=target) + + def hathat(self,domain=None,**kwargs): + """ + Translates the operator's diagonal into a diagonal operator + + Parameters + ---------- + domain : space, *optional* + space wherein the probes live (default: self.domain) + target : space, *optional* + space wherein the transform of the probes live + (default: None, applies target of the domain) + random : string, *optional* + Specifies the pseudo random number generator. Valid + options are "pm1" for a random vector of +/-1, or "gau" + for a random vector with entries drawn from a Gaussian + distribution with zero mean and unit variance. + (default: "pm1") + ncpu : int, *optional* + number of used CPUs to use. (default: 2) + nrun : int, *optional* + total number of probes (default: 8) + nper : int, *optional* + number of tasks performed by one process (default: 1) + save : bool, *optional* + whether all individual probing results are saved or not + (default: False) + path : string, *optional* + path wherein the results are saved (default: "tmp") + prefix : string, *optional* + prefix for all saved files (default: "") + loop : bool, *optional* + Indicates whether or not to perform a loop i.e., to + parallelise (default: False) + + Returns + ------- + D : diagonal_operator + The matrix diagonal as an operator + + See Also + -------- + probing : The class used to perform probing operations + + 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. + + """ + if(domain is None): + domain = self.domain + diag = self.diag(bare=False,domain=domain,var=False,**kwargs) + if(diag is None): + about.warnings.cprint("WARNING: forwarding 'NoneType'.") + return None + return diagonal_operator(domain=domain,diag=diag,bare=False) + + def inverse_hathat(self,domain=None,**kwargs): + """ + Translates the inverse operator's diagonal into a diagonal + operator + + Parameters + ---------- + domain : space, *optional* + space wherein the probes live (default: self.domain) + target : space, *optional* + space wherein the transform of the probes live + (default: None, applies target of the domain) + random : string, *optional* + Specifies the pseudo random number generator. Valid + options are "pm1" for a random vector of +/-1, or "gau" + for a random vector with entries drawn from a Gaussian + distribution with zero mean and unit variance. + (default: "pm1") + ncpu : int, *optional* + number of used CPUs to use. (default: 2) + nrun : int, *optional* + total number of probes (default: 8) + nper : int, *optional* + number of tasks performed by one process (default: 1) + save : bool, *optional* + whether all individual probing results are saved or not + (default: False) + path : string, *optional* + path wherein the results are saved (default: "tmp") + prefix : string, *optional* + prefix for all saved files (default: "") + loop : bool, *optional* + Indicates whether or not to perform a loop i.e., to + parallelise (default: False) + + Returns + ------- + D : diagonal_operator + The diagonal of the inverse matrix as an operator + + See Also + -------- + probing : The class used to perform probing operations + + 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. + + """ + if(domain is None): + domain = self.target + diag = self.inverse_diag(bare=False,domain=domain,var=False,**kwargs) + if(diag is None): + about.warnings.cprint("WARNING: forwarding 'NoneType'.") + return None + return diagonal_operator(domain=domain,diag=diag,bare=False) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def __repr__(self): + return "<nifty_core.operator>" + +##============================================================================= + + + +##----------------------------------------------------------------------------- + +class diagonal_operator(operator): + """ + .. __ __ __ + .. / / /__/ / / + .. ____/ / __ ____ __ ____ __ ______ __ ___ ____ __ / / + .. / _ / / / / _ / / _ / / _ | / _ | / _ / / / + .. / /_/ / / / / /_/ / / /_/ / / /_/ / / / / / / /_/ / / /_ + .. \______| /__/ \______| \___ / \______/ /__/ /__/ \______| \___/ operator class + .. /______/ + + NIFTY subclass for diagonal operators + + Parameters + ---------- + domain : space, *optional* + The space wherein valid arguments live. If no domain is given + then the diag parameter *must* be a field and the domain + of that field is assumed. (default: None) + diag : {scalar, ndarray, field} + The diagonal entries of the operator. For a scalar, a constant + diagonal is defined having the value provided. If no domain + is given, diag must be a field. (default: 1) + bare : bool, *optional* + whether the diagonal entries are `bare` or not + (mandatory for the correct incorporation of volume weights) + (default: False) + + 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. + + The inverse applications of the diagonal operator feature a ``pseudo`` + flag indicating if zero divison shall be ignored and return zero + instead of causing an error. + + Attributes + ---------- + domain : space + The space wherein valid arguments live. + val : ndarray + A field containing the diagonal entries of the matrix. + sym : bool + Indicates whether the operator is self-adjoint or not + uni : bool + Indicates whether the operator is unitary or not + imp : bool + Indicates whether the incorporation of volume weights in + multiplications is already implemented in the `multiply` + instance methods or not + target : space + The space wherein the operator output lives + """ + def __init__(self,domain=None,diag=1,bare=False): + """ + Sets the standard operator properties and `values`. + + Parameters + ---------- + domain : space, *optional* + The space wherein valid arguments live. If no domain is given + then the diag parameter *must* be a field and the domain + of that field is assumed. (default: None) + diag : {scalar, ndarray, field}, *optional* + The diagonal entries of the operator. For a scalar, a constant + diagonal is defined having the value provided. If no domain + is given, diag must be a field. (default: 1) + bare : bool, *optional* + whether the diagonal entries are `bare` or not + (mandatory for the correct incorporation of volume weights) + (default: False) + + Returns + ------- + None + + 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. + + """ + if(domain is None)and(isinstance(diag,field)): + domain = diag.domain + if(not isinstance(domain,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + self.domain = domain + + diag = self.domain.enforce_values(diag,extend=True) + ## weight if ... + if(not self.domain.discrete)and(bare): + diag = self.domain.calc_weight(diag,power=1) + ## check complexity + if(np.all(np.imag(diag)==0)): + self.val = np.real(diag) + self.sym = True + else: + self.val = diag +# about.infos.cprint("INFO: non-self-adjoint complex diagonal operator.") + self.sym = False + + ## check whether identity + if(np.all(diag==1)): + self.uni = True + else: + self.uni = False + + self.imp = True ## correctly implemented for efficiency + self.target = self.domain + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def set_diag(self,newdiag,bare=False): + """ + Sets the diagonal of the diagonal operator + + Parameters + ---------- + newdiag : {scalar, ndarray, field} + The new diagonal entries of the operator. For a scalar, a + constant diagonal is defined having the value provided. If + no domain is given, diag must be a field. + + bare : bool, *optional* + whether the diagonal entries are `bare` or not + (mandatory for the correct incorporation of volume weights) + (default: False) + + Returns + ------- + None + """ + newdiag = self.domain.enforce_values(newdiag,extend=True) + ## weight if ... + if(not self.domain.discrete)and(bare): + newdiag = self.domain.calc_weight(newdiag,power=1) + ## check complexity + if(np.all(np.imag(newdiag)==0)): + self.val = np.real(newdiag) + self.sym = True + else: + self.val = newdiag +# about.infos.cprint("INFO: non-self-adjoint complex diagonal operator.") + self.sym = False + + ## check whether identity + if(np.all(newdiag==1)): + self.uni = True + else: + self.uni = False + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _multiply(self,x,**kwargs): ## > applies the operator to a given field + x_ = field(self.target,val=None,target=x.target) + x_.val = x.val*self.val ## bypasses self.domain.enforce_values + return x_ + + def _adjoint_multiply(self,x,**kwargs): ## > applies the adjoint operator to a given field + x_ = field(self.domain,val=None,target=x.target) + x_.val = x.val*np.conjugate(self.val) ## bypasses self.domain.enforce_values + return x_ + + def _inverse_multiply(self,x,pseudo=False,**kwargs): ## > applies the inverse operator to a given field + if(np.any(self.val==0)): + if(pseudo): + x_ = field(self.domain,val=None,target=x.target) + x_.val = np.ma.filled(x.val/np.ma.masked_where(self.val==0,self.val,copy=False),fill_value=0) ## bypasses self.domain.enforce_values + return x_ + else: + raise AttributeError(about._errors.cstring("ERROR: singular operator.")) + else: + x_ = field(self.domain,val=None,target=x.target) + x_.val = x.val/self.val ## bypasses self.domain.enforce_values + return x_ + + def _adjoint_inverse_multiply(self,x,pseudo=False,**kwargs): ## > applies the inverse adjoint operator to a given field + if(np.any(self.val==0)): + if(pseudo): + x_ = field(self.domain,val=None,target=x.target) + x_.val = np.ma.filled(x.val/np.ma.masked_where(self.val==0,np.conjugate(self.val),copy=False),fill_value=0) ## bypasses self.domain.enforce_values + return x_ + else: + raise AttributeError(about._errors.cstring("ERROR: singular operator.")) + else: + x_ = field(self.target,val=None,target=x.target) + x_.val = x.val/np.conjugate(self.val) ## bypasses self.domain.enforce_values + return x_ + + def _inverse_adjoint_multiply(self,x,pseudo=False,**kwargs): ## > applies the adjoint inverse operator to a given field + if(np.any(self.val==0)): + if(pseudo): + x_ = field(self.domain,val=None,target=x.target) + x_.val = np.ma.filled(x.val/np.conjugate(np.ma.masked_where(self.val==0,self.val,copy=False)),fill_value=0) ## bypasses self.domain.enforce_values + return x_ + else: + raise AttributeError(about._errors.cstring("ERROR: singular operator.")) + + else: + x_ = field(self.target,val=None,target=x.target) + x_.val = x.val*np.conjugate(1/self.val) ## bypasses self.domain.enforce_values + return x_ + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def tr(self,domain=None,**kwargs): + """ + Computes the trace of the operator + + Parameters + ---------- + domain : space, *optional* + space wherein the probes live (default: self.domain) + target : space, *optional* + space wherein the transform of the probes live + (default: None, applies target of the domain) + random : string, *optional* + Specifies the pseudo random number generator. Valid + options are "pm1" for a random vector of +/-1, or "gau" + for a random vector with entries drawn from a Gaussian + distribution with zero mean and unit variance. + (default: "pm1") + ncpu : int, *optional* + number of used CPUs to use. (default: 2) + nrun : int, *optional* + total number of probes (default: 8) + nper : int, *optional* + number of tasks performed by one process (default: 1) + var : bool, *optional* + Indicates whether to additionally return the probing variance + or not (default: False). + loop : bool, *optional* + Indicates whether or not to perform a loop i.e., to + parallelise (default: False) + + Returns + ------- + tr : float + Trace of the operator + delta : float, *optional* + Probing variance of the trace. Returned if `var` is True in + of probing case. + + """ + if(domain is None)or(domain==self.domain): + if(self.uni): ## identity + return (self.domain.datatype(self.domain.dof())).real + elif(self.domain.dim(split=False)<self.domain.dof()): ## hidden degrees of freedom + return self.domain.calc_dot(np.ones(self.domain.dim(split=True),dtype=self.domain.datatype,order='C'),self.val) ## discrete inner product + else: + return np.sum(self.val,axis=None,dtype=None,out=None) + else: + if(self.uni): ## identity + if(not isinstance(domain,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + ## check degrees of freedom + if(self.domain.dof()>domain.dof()): + about.infos.cprint("INFO: variant numbers of degrees of freedom ( "+str(self.domain.dof())+" / "+str(domain.dof())+" ).") + return (domain.datatype(domain.dof())).real + else: + return super(diagonal_operator,self).tr(domain=domain,**kwargs) ## probing + + def inverse_tr(self,domain=None,**kwargs): + """ + Computes the trace of the inverse operator + + Parameters + ---------- + domain : space, *optional* + space wherein the probes live (default: self.domain) + target : space, *optional* + space wherein the transform of the probes live + (default: None, applies target of the domain) + random : string, *optional* + Specifies the pseudo random number generator. Valid + options are "pm1" for a random vector of +/-1, or "gau" + for a random vector with entries drawn from a Gaussian + distribution with zero mean and unit variance. + (default: "pm1") + ncpu : int, *optional* + number of used CPUs to use. (default: 2) + nrun : int, *optional* + total number of probes (default: 8) + nper : int, *optional* + number of tasks performed by one process (default: 1) + var : bool, *optional* + Indicates whether to additionally return the probing variance + or not (default: False). + loop : bool, *optional* + Indicates whether or not to perform a loop i.e., to + parallelise (default: False) + + Returns + ------- + tr : float + Trace of the inverse operator + delta : float, *optional* + Probing variance of the trace. Returned if `var` is True in + of probing case. + + """ + if(np.any(self.val==0)): + raise AttributeError(about._errors.cstring("ERROR: singular operator.")) + + if(domain is None)or(domain==self.target): + if(self.uni): ## identity + return np.real(self.domain.datatype(self.domain.dof())) + elif(self.domain.dim(split=False)<self.domain.dof()): ## hidden degrees of freedom + return self.domain.calc_dot(np.ones(self.domain.dim(split=True),dtype=self.domain.datatype,order='C'),1/self.val) ## discrete inner product + else: + return np.sum(1/self.val,axis=None,dtype=None,out=None) + else: + if(self.uni): ## identity + if(not isinstance(domain,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + ## check degrees of freedom + if(self.domain.dof()>domain.dof()): + about.infos.cprint("INFO: variant numbers of degrees of freedom ( "+str(self.domain.dof())+" / "+str(domain.dof())+" ).") + return np.real(domain.datatype(domain.dof())) + else: + return super(diagonal_operator,self).inverse_tr(domain=domain,**kwargs) ## probing + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def diag(self,bare=False,domain=None,**kwargs): + """ + Computes the diagonal of the operator. + + Parameters + ---------- + bare : bool, *optional* + Indicatese whether the diagonal entries are `bare` or not + (mandatory for the correct incorporation of volume weights) + (default: False) + domain : space, *optional* + space wherein the probes live (default: self.domain) + target : space, *optional* + space wherein the transform of the probes live + (default: None, applies target of the domain) + random : string, *optional* + Specifies the pseudo random number generator. Valid + options are "pm1" for a random vector of +/-1, or "gau" + for a random vector with entries drawn from a Gaussian + distribution with zero mean and unit variance. + (default: "pm1") + ncpu : int, *optional* + number of used CPUs to use. (default: 2) + nrun : int, *optional* + total number of probes (default: 8) + nper : int, *optional* + number of tasks performed by one process (default: 1) + var : bool, *optional* + Indicates whether to additionally return the probing variance + or not (default: False). + save : bool, *optional* + whether all individual probing results are saved or not + (default: False) + path : string, *optional* + path wherein the results are saved (default: "tmp") + prefix : string, *optional* + prefix for all saved files (default: "") + loop : bool, *optional* + Indicates whether or not to perform a loop i.e., to + parallelise (default: False) + + Returns + ------- + diag : ndarray + The matrix diagonal + delta : float, *optional* + Probing variance of the trace. Returned if `var` is True in + of probing case. + + 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. + + """ + if(domain is None)or(domain==self.domain): + ## weight if ... + if(not self.domain.discrete)and(bare): + diag = self.domain.calc_weight(self.val,power=-1) + ## check complexity + if(np.all(np.imag(diag)==0)): + diag = np.real(diag) + return diag + else: + return self.val + else: + if(self.uni): ## identity + if(not isinstance(domain,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + ## weight if ... + if(not domain.discrete)and(bare): + return np.real(domain.calc_weight(domain.enforce_values(1,extend=True),power=-1)) + else: + return np.real(domain.enforce_values(1,extend=True)) + else: + return super(diagonal_operator,self).diag(bare=bare,domain=domain,**kwargs) ## probing + + def inverse_diag(self,bare=False,domain=None,**kwargs): + """ + Computes the diagonal of the inverse operator. + + Parameters + ---------- + bare : bool, *optional* + Indicatese whether the diagonal entries are `bare` or not + (mandatory for the correct incorporation of volume weights) + (default: False) + domain : space, *optional* + space wherein the probes live (default: self.domain) + target : space, *optional* + space wherein the transform of the probes live + (default: None, applies target of the domain) + random : string, *optional* + Specifies the pseudo random number generator. Valid + options are "pm1" for a random vector of +/-1, or "gau" + for a random vector with entries drawn from a Gaussian + distribution with zero mean and unit variance. + (default: "pm1") + ncpu : int, *optional* + number of used CPUs to use. (default: 2) + nrun : int, *optional* + total number of probes (default: 8) + nper : int, *optional* + number of tasks performed by one process (default: 1) + var : bool, *optional* + Indicates whether to additionally return the probing variance + or not (default: False). + save : bool, *optional* + whether all individual probing results are saved or not + (default: False) + path : string, *optional* + path wherein the results are saved (default: "tmp") + prefix : string, *optional* + prefix for all saved files (default: "") + loop : bool, *optional* + Indicates whether or not to perform a loop i.e., to + parallelise (default: False) + + Returns + ------- + diag : ndarray + The diagonal of the inverse matrix + delta : float, *optional* + Probing variance of the trace. Returned if `var` is True in + of probing case. + + See Also + -------- + probing : The class used to perform probing operations + + 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. + + """ + if(domain is None)or(domain==self.target): + ## weight if ... + if(not self.domain.discrete)and(bare): + diag = self.domain.calc_weight(1/self.val,power=-1) + ## check complexity + if(np.all(np.imag(diag)==0)): + diag = np.real(diag) + return diag + else: + return 1/self.val + else: + if(self.uni): ## identity + if(not isinstance(domain,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + ## weight if ... + if(not domain.discrete)and(bare): + return np.real(domain.calc_weight(domain.enforce_values(1,extend=True),power=-1)) + else: + return np.real(domain.enforce_values(1,extend=True)) + else: + return super(diagonal_operator,self).inverse_diag(bare=bare,domain=domain,**kwargs) ## probing + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def det(self): + """ + Computes the determinant of the matrix. + + Returns + ------- + det : float + The determinant + + """ + if(self.uni): ## identity + return 1 + elif(self.domain.dim(split=False)<self.domain.dof()): ## hidden degrees of freedom + return np.exp(self.domain.calc_dot(np.ones(self.domain.dim(split=True),dtype=self.domain.datatype,order='C'),np.log(self.val))) + else: + return np.prod(self.val,axis=None,dtype=None,out=None) + + def inverse_det(self): + """ + Computes the determinant of the inverse operator. + + Returns + ------- + det : float + The determinant + + """ + if(self.uni): ## identity + return 1 + det = self.det() + if(det<>0): + return 1/det + else: + raise ValueError(about._errors.cstring("ERROR: singular operator.")) + + def log_det(self): + """ + Computes the logarithm of the determinant of the operator. + + Returns + ------- + logdet : float + The logarithm of the determinant + + """ + if(self.uni): ## identity + return 0 + elif(self.domain.dim(split=False)<self.domain.dof()): ## hidden degrees of freedom + return self.domain.calc_dot(np.ones(self.domain.dim(split=True),dtype=self.domain.datatype,order='C'),np.log(self.val)) + else: + return np.sum(np.log(self.val),axis=None,dtype=None,out=None) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def get_random_field(self,domain=None,target=None): + """ + Generates a Gaussian random field with variance equal to the + diagonal. + + Parameters + ---------- + domain : space, *optional* + space wherein the field lives (default: None, indicates + to use self.domain) + target : space, *optional* + space wherein the transform of the field lives + (default: None, indicates to use target of domain) + + Returns + ------- + x : field + Random field. + + """ + ## weight if ... + if(not self.domain.discrete): + diag = self.domain.calc_weight(self.val,power=-1) + ## check complexity + if(np.all(np.imag(diag)==0)): + diag = np.real(diag) + else: + diag = self.val + + if(domain is None)or(domain==self.domain): + return field(self.domain,val=None,target=target,random="gau",var=self.diag(bare=True,domain=self.domain)) + else: + return field(self.domain,val=None,target=domain,random="gau",var=self.diag(bare=True,domain=self.domain)).transform(target=domain,overwrite=False) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def __repr__(self): + return "<nifty_core.diagonal_operator>" + +##----------------------------------------------------------------------------- + +##----------------------------------------------------------------------------- + +def identity(domain): + """ + Returns an identity operator. + + The identity operator is represented by a `diagonal_operator` instance, + which is applicable to a field-like object; i.e., a scalar, list, + array or field. (The identity operator is unrelated to PYTHON's + built-in function :py:func:`id`.) + + Parameters + ---------- + domain : space + The space wherein valid arguments live. + + Returns + ------- + id : diagonal_operator + The identity operator as a `diagonal_operator` instance. + + See Also + -------- + diagonal_operator + + Examples + -------- + >>> I = identity(rg_space(8,dist=0.2)) + >>> I.diag() + array([ 1., 1., 1., 1., 1., 1., 1., 1.]) + >>> I.diag(bare=True) + array([ 5., 5., 5., 5., 5., 5., 5., 5.]) + >>> I.tr() + 8.0 + >>> I(3) + <nifty.field> + >>> I(3).val + array([ 3., 3., 3., 3., 3., 3., 3., 3.]) + >>> I(np.arange(8))[:] + array([ 0., 1., 2., 3., 4., 5., 6., 7.]) + >>> f = I.get_random_field() + >>> print(I(f) - f) + nifty.field instance + - domain = <nifty.rg_space> + - val = [...] + - min.,max. = [0.0, 0.0] + - med.,mean = [0.0, 0.0] + - target = <nifty.rg_space> + >>> I.times(f) ## equal to I(f) + <nifty.field> + >>> I.inverse_times(f) + <nifty.field> + + """ + return diagonal_operator(domain=domain,diag=1,bare=False) + +##----------------------------------------------------------------------------- + +##----------------------------------------------------------------------------- + +class power_operator(diagonal_operator): + """ + .. ______ ______ __ __ _______ _____ + .. / _ | / _ | | |/\/ / / __ / / __/ + .. / /_/ / / /_/ / | / / /____/ / / + .. / ____/ \______/ |__/\__/ \______/ /__/ operator class + .. /__/ + + NIFTY subclass for (signal-covariance-type) diagonal operators containing a power spectrum + + Parameters + ---------- + domain : space, *optional* + The space wherein valid arguments live. If no domain is given + then the diag parameter *must* be a field and the domain + of that field is assumed. (default: None) + spec : {scalar, list, array, field, function} + The power spectrum. For a scalar, a constant power + spectrum is defined having the value provided. If no domain + is given, diag must be a field. (default: 1) + bare : bool, *optional* + whether the entries are `bare` or not + (mandatory for the correct incorporation of volume weights) + (default: True) + pindex : ndarray, *optional* + indexing array, obtainable from domain.get_power_indices + (default: None) + + Other Parameters + ---------------- + log : bool, *optional* + Flag specifying if the spectral binning is performed on logarithmic + scale or not; if set, the number of used bins is set + automatically (if not given otherwise); by default no binning + is done (default: None). + nbin : integer, *optional* + Number of used spectral bins; if given `log` is set to ``False``; + integers below the minimum of 3 induce an automatic setting; + by default no binning is done (default: None). + binbounds : {list, array}, *optional* + User specific inner boundaries of the bins, which are preferred + over the above parameters; by default no binning is done + (default: None). vmin : {scalar, list, ndarray, field}, *optional* + Lower limit of the uniform distribution if ``random == "uni"`` + (default: 0). + + 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. + + Attributes + ---------- + domain : space + The space wherein valid arguments live. + val : ndarray + A field containing the diagonal entries of the matrix. + sym : bool + Indicates whether the operator is self-adjoint or not + uni : bool + Indicates whether the operator is unitary or not + imp : bool + Indicates whether the incorporation of volume weights in + multiplications is already implemented in the `multiply` + instance methods or not + target : space + The space wherein the operator output lives + + """ + def __init__(self,domain,spec=1,bare=True,pindex=None,**kwargs): + """ + Sets the diagonal operator's standard properties + + Parameters + ---------- + domain : space, *optional* + The space wherein valid arguments live. If no domain is given + then the diag parameter *must* be a field and the domain + of that field is assumed. (default: None) + spec : {scalar, list, array, field, function} + The power spectrum. For a scalar, a constant power + spectrum is defined having the value provided. If no domain + is given, diag must be a field. (default: 1) + bare : bool, *optional* + whether the entries are `bare` or not + (mandatory for the correct incorporation of volume weights) + (default: True) + pindex : ndarray, *optional* + indexing array, obtainable from domain.get_power_indices + (default: None) + + Returns + ------- + None + + Other Parameters + ---------------- + log : bool, *optional* + Flag specifying if the spectral binning is performed on logarithmic + scale or not; if set, the number of used bins is set + automatically (if not given otherwise); by default no binning + is done (default: None). + nbin : integer, *optional* + Number of used spectral bins; if given `log` is set to ``False``; + integers below the minimum of 3 induce an automatic setting; + by default no binning is done (default: None). + binbounds : {list, array}, *optional* + User specific inner boundaries of the bins, which are preferred + over the above parameters; by default no binning is done + (default: None). vmin : {scalar, list, ndarray, field}, *optional* + Lower limit of the uniform distribution if ``random == "uni"`` + (default: 0). + + """ + if(not isinstance(domain,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + self.domain = domain + ## check implicit pindex + if(pindex is None): + try: + self.domain.set_power_indices(**kwargs) + except: + raise ValueError(about._errors.cstring("ERROR: invalid input.")) + else: + pindex = self.domain.power_indices.get("pindex") + ## check explicit pindex + else: + pindex = np.array(pindex,dtype=np.int) + if(not np.all(np.array(np.shape(pindex))==self.domain.dim(split=True))): + raise ValueError(about._errors.cstring("ERROR: shape mismatch ( "+str(np.array(np.shape(pindex)))+" <> "+str(self.domain.dim(split=True))+" ).")) + ## set diagonal + try: + #diag = self.domain.enforce_power(spec,size=np.max(pindex,axis=None,out=None)+1)[pindex] + temp_spec = self.domain.enforce_power( + spec,size=np.max(pindex,axis=None,out=None)+1) + diag = pindex.apply_scalar_function(lambda x: temp_spec[x]) + except(AttributeError): + raise ValueError(about._errors.cstring("ERROR: invalid input.")) + ## weight if ... + if(not self.domain.discrete)and(bare): + self.val = np.real(self.domain.calc_weight(diag,power=1)) + else: + self.val = diag + + self.sym = True + + ## check whether identity + if(np.all(spec==1)): + self.uni = True + else: + self.uni = False + + self.imp = True + self.target = self.domain + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def set_power(self,newspec,bare=True,pindex=None,**kwargs): + """ + Sets the power spectrum of the diagonal operator + + Parameters + ---------- + newspec : {scalar, list, array, field, function} + The entries of the operator. For a scalar, a constant + diagonal is defined having the value provided. If no domain + is given, diag must be a field. (default: 1) + bare : bool + whether the entries are `bare` or not + (mandatory for the correct incorporation of volume weights) + pindex : ndarray, *optional* + indexing array, obtainable from domain.get_power_indices + (default: None) + + Returns + ------- + None + + Other Parameters + ---------------- + log : bool, *optional* + Flag specifying if the spectral binning is performed on logarithmic + scale or not; if set, the number of used bins is set + automatically (if not given otherwise); by default no binning + is done (default: None). + nbin : integer, *optional* + Number of used spectral bins; if given `log` is set to ``False``; + integers below the minimum of 3 induce an automatic setting; + by default no binning is done (default: None). + binbounds : {list, array}, *optional* + User specific inner boundaries of the bins, which are preferred + over the above parameters; by default no binning is done + (default: None). + + """ +# if(bare is None): +# about.warnings.cprint("WARNING: bare keyword set to default.") +# bare = True + ## check implicit pindex + if(pindex is None): + try: + self.domain.set_power_indices(**kwargs) + except: + raise ValueError(about._errors.cstring("ERROR: invalid domain.")) + else: + pindex = self.domain.power_indices.get("pindex") + ## check explicit pindex + else: + pindex = np.array(pindex,dtype=np.int) + if(not np.all(np.array(np.shape(pindex))==self.domain.dim(split=True))): + raise ValueError(about._errors.cstring("ERROR: shape mismatch ( "+str(np.array(np.shape(pindex)))+" <> "+str(self.domain.dim(split=True))+" ).")) + ## set diagonal + try: + diag = self.domain.enforce_power(newspec,size=np.max(pindex,axis=None,out=None)+1)[pindex] + except(AttributeError): + raise ValueError(about._errors.cstring("ERROR: invalid input.")) + ## weight if ... + if(not self.domain.discrete)and(bare): + self.val = np.real(self.domain.calc_weight(diag,power=1)) + else: + self.val = diag + + ## check whether identity + if(np.all(newspec==1)): + self.uni = True + else: + self.uni = False + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def get_power(self,bare=True,pundex=None,pindex=None,**kwargs): + """ + Computes the power spectrum + + Parameters + ---------- + bare : bool, *optional* + whether the entries are `bare` or not + (mandatory for the correct incorporation of volume weights) + (default: True) + pundex : ndarray, *optional* + unindexing array, obtainable from domain.get_power_indices + (default: None) + pindex : ndarray, *optional* + indexing array, obtainable from domain.get_power_indices + (default: None) + + Returns + ------- + spec : ndarray + The power spectrum + + Other Parameters + ---------------- + log : bool, *optional* + Flag specifying if the spectral binning is performed on logarithmic + scale or not; if set, the number of used bins is set + automatically (if not given otherwise); by default no binning + is done (default: None). + nbin : integer, *optional* + Number of used spectral bins; if given `log` is set to ``False``; + integers below the minimum of 3 induce an automatic setting; + by default no binning is done (default: None). + binbounds : {list, array}, *optional* + User specific inner boundaries of the bins, which are preferred + over the above parameters; by default no binning is done + (default: None). vmin : {scalar, list, ndarray, field}, *optional* + Lower limit of the uniform distribution if ``random == "uni"`` + (default: 0). + + """ + ## weight if ... + if(not self.domain.discrete)and(bare): + diag = np.real(self.domain.calc_weight(self.val,power=-1)) + else: + diag = self.val + ## check implicit pundex + if(pundex is None): + if(pindex is None): + try: + self.domain.set_power_indices(**kwargs) + except: + raise ValueError(about._errors.cstring("ERROR: invalid domain.")) + else: + pundex = self.domain.power_indices.get("pundex") + else: + pindex = np.array(pindex,dtype=np.int) + if(not np.all(np.array(np.shape(pindex))==self.domain.dim(split=True))): + raise ValueError(about._errors.cstring("ERROR: shape mismatch ( "+str(np.array(np.shape(pindex)))+" <> "+str(self.domain.dim(split=True))+" ).")) + ## quick pundex + pundex = np.unique(pindex,return_index=True,return_inverse=False)[1] + ## check explicit pundex + else: + pundex = np.array(pundex,dtype=np.int) + + return diag.flatten(order='C')[pundex] + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def get_projection_operator(self,pindex=None,**kwargs): + """ + Generates a spectral projection operator + + Parameters + ---------- + pindex : ndarray + indexing array obtainable from domain.get_power_indices + (default: None) + + Returns + ------- + P : projection_operator + + Other Parameters + ---------------- + log : bool, *optional* + Flag specifying if the spectral binning is performed on logarithmic + scale or not; if set, the number of used bins is set + automatically (if not given otherwise); by default no binning + is done (default: None). + nbin : integer, *optional* + Number of used spectral bins; if given `log` is set to ``False``; + integers below the minimum of 3 induce an automatic setting; + by default no binning is done (default: None). + binbounds : {list, array}, *optional* + User specific inner boundaries of the bins, which are preferred + over the above parameters; by default no binning is done + (default: None). vmin : {scalar, list, ndarray, field}, *optional* + Lower limit of the uniform distribution if ``random == "uni"`` + (default: 0). + + """ + ## check implicit pindex + if(pindex is None): + try: + self.domain.set_power_indices(**kwargs) + except: + raise ValueError(about._errors.cstring("ERROR: invalid domain.")) + else: + pindex = self.domain.power_indices.get("pindex") + ## check explicit pindex + else: + pindex = np.array(pindex,dtype=np.int) + if(not np.all(np.array(np.shape(pindex))==self.domain.dim(split=True))): + raise ValueError(about._errors.cstring("ERROR: shape mismatch ( "+str(np.array(np.shape(pindex)))+" <> "+str(self.domain.dim(split=True))+" ).")) + + return projection_operator(self.domain,assign=pindex) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def __repr__(self): + return "<nifty_core.power_operator>" + +##----------------------------------------------------------------------------- + + + +##----------------------------------------------------------------------------- + +class projection_operator(operator): + """ + .. __ __ __ + .. /__/ / /_ /__/ + .. ______ _____ ______ __ _______ _______ / _/ __ ______ __ ___ + .. / _ | / __/ / _ | / / / __ / / ____/ / / / / / _ | / _ | + .. / /_/ / / / / /_/ / / / / /____/ / /____ / /_ / / / /_/ / / / / / + .. / ____/ /__/ \______/ / / \______/ \______/ \___/ /__/ \______/ /__/ /__/ operator class + .. /__/ /___/ + + NIFTY subclass for projection operators + + Parameters + ---------- + domain : space + The space wherein valid arguments live. + assign : ndarray, *optional* + Assignments of domain items to projection bands. An array + of integers, negative integers are associated with the + nullspace of the projection. (default: None) + + Other Parameters + ---------------- + log : bool, *optional* + Flag specifying if the spectral binning is performed on logarithmic + scale or not; if set, the number of used bins is set + automatically (if not given otherwise); by default no binning + is done (default: None). + nbin : integer, *optional* + Number of used spectral bins; if given `log` is set to ``False``; + integers below the minimum of 3 induce an automatic setting; + by default no binning is done (default: None). + binbounds : {list, array}, *optional* + User specific inner boundaries of the bins, which are preferred + over the above parameters; by default no binning is done + (default: None). vmin : {scalar, list, ndarray, field}, *optional* + Lower limit of the uniform distribution if ``random == "uni"`` + (default: 0). + + Notes + ----- + The application of the projection operator features a ``band`` keyword + specifying a single projection band (see examples), a ``bandsup`` + keyword specifying which projection bands to sum up, and a ``split`` + keyword. + + Examples + -------- + >>> space = point_space(3) + >>> P = projection_operator(space, assign=[0, 1, 0]) + >>> P.bands() + 2 + >>> P([1, 2, 3], band=0) # equal to P.times(field(space,val=[1, 2, 3])) + <nifty.field> + >>> P([1, 2, 3], band=0).domain + <nifty.point_space> + >>> P([1, 2, 3], band=0).val # projection on band 0 (items 0 and 2) + array([ 1., 0., 3.]) + >>> P([1, 2, 3], band=1).val # projection on band 1 (item 1) + array([ 0., 2., 0.]) + >>> P([1, 2, 3]) + <nifty.field> + >>> P([1, 2, 3]).domain + <nifty.nested_space> + >>> P([1, 2, 3]).val # projection on all bands + array([[ 1., 0., 3.], + [ 0., 2., 0.]]) + + Attributes + ---------- + domain : space + The space wherein valid arguments live. + ind : ndarray + Assignments of domain items to projection bands. + sym : bool + Indicates whether the operator is self-adjoint or not + uni : bool + Indicates whether the operator is unitary or not + imp : bool + Indicates whether the incorporation of volume weights in + multiplications is already implemented in the `multiply` + instance methods or not + target : space + The space wherein the operator output lives + + """ + def __init__(self,domain,assign=None,**kwargs): + """ + Sets the standard operator properties and `indexing`. + + Parameters + ---------- + domain : space + The space wherein valid arguments live. + assign : ndarray, *optional* + Assignments of domain items to projection bands. An array + of integers, negative integers are associated with the + nullspace of the projection. (default: None) + + Returns + ------- + None + + Other Parameters + ---------------- + log : bool, *optional* + Flag specifying if the spectral binning is performed on logarithmic + scale or not; if set, the number of used bins is set + automatically (if not given otherwise); by default no binning + is done (default: None). + nbin : integer, *optional* + Number of used spectral bins; if given `log` is set to ``False``; + integers below the minimum of 3 induce an automatic setting; + by default no binning is done (default: None). + binbounds : {list, array}, *optional* + User specific inner boundaries of the bins, which are preferred + over the above parameters; by default no binning is done + (default: None). vmin : {scalar, list, ndarray, field}, *optional* + Lower limit of the uniform distribution if ``random == "uni"`` + (default: 0). + + """ + if(not isinstance(domain,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + self.domain = domain + + ## check assignment(s) + if(assign is None): + try: + self.domain.set_power_indices(**kwargs) + except: + assign = np.arange(self.domain.dim(split=False),dtype=np.int) + else: + assign = self.domain.power_indices.get("pindex").flatten(order='C') + else: + assign = self.domain.enforce_shape(assign).astype(np.int).flatten(order='C') + ## build indexing + self.ind = [np.where(assign==ii)[0] for ii in xrange(np.max(assign,axis=None,out=None)+1) if ii in assign] + + self.sym = True +# about.infos.cprint("INFO: pseudo unitary projection operator.") + self.uni = False + self.imp = True + + self.target = nested_space([point_space(len(self.ind),datatype=self.domain.datatype),self.domain]) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def bands(self): + """ + Computes the number of projection bands + + Returns + ------- + bands : int + The number of projection bands + """ + return len(self.ind) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def rho(self): + """ + Computes the number of degrees of freedom per projection band. + + Returns + ------- + rho : ndarray + The number of degrees of freedom per projection band. + """ + rho = np.empty(len(self.ind),dtype=np.int,order='C') + if(self.domain.dim(split=False)==self.domain.dof()): ## no hidden degrees of freedom + for ii in xrange(len(self.ind)): + rho[ii] = len(self.ind[ii]) + else: ## hidden degrees of freedom + mof = np.round(np.real(self.domain.calc_weight(self.domain.get_meta_volume(total=False),power=-1).flatten(order='C')),decimals=0,out=None).astype(np.int) ## meta degrees of freedom + for ii in xrange(len(self.ind)): + rho[ii] = np.sum(mof[self.ind[ii]],axis=None,dtype=np.int,out=None) + return rho + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _multiply(self,x,band=None,bandsup=None,**kwargs): + """ + Applies the operator to a given field. + + Parameters + ---------- + x : field + Valid input field. + band : int, *optional* + Projection band whereon to project (default: None). + bandsup: {integer, list/array of integers}, *optional* + List of projection bands whereon to project and which to sum + up. The `band` keyword is prefered over `bandsup` + (default: None). + + Returns + ------- + Px : field + projected field(!) + """ + if(band is not None): + band = int(band) + if(band>self.bands()-1)or(band<0): + raise TypeError(about._errors.cstring("ERROR: invalid band.")) + Px = np.zeros(self.domain.dim(split=False),dtype=self.domain.datatype,order='C') + Px[self.ind[band]] += x.val.flatten(order='C')[self.ind[band]] + Px = field(self.domain,val=Px,target=x.target) + return Px + + elif(bandsup is not None): + if(np.isscalar(bandsup)): + bandsup = np.arange(int(bandsup+1),dtype=np.int) + else: + bandsup = np.array(bandsup,dtype=np.int) + if(np.any(bandsup>self.bands()-1))or(np.any(bandsup<0)): + raise ValueError(about._errors.cstring("ERROR: invalid input.")) + Px = np.zeros(self.domain.dim(split=False),dtype=self.domain.datatype,order='C') + x_ = x.val.flatten(order='C') + for bb in bandsup: + Px[self.ind[bb]] += x_[self.ind[bb]] + Px = field(self.domain,val=Px,target=x.target) + return Px + + else: + Px = np.zeros((len(self.ind),self.domain.dim(split=False)),dtype=self.target.datatype,order='C') + x_ = x.val.flatten(order='C') + for bb in xrange(self.bands()): + Px[bb][self.ind[bb]] += x_[self.ind[bb]] + Px = field(self.target,val=Px,target=nested_space([point_space(len(self.ind),datatype=x.target.datatype),x.target])) + return Px + + def _inverse_multiply(self,x,**kwargs): + raise AttributeError(about._errors.cstring("ERROR: singular operator.")) ## pseudo unitary + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _debriefing(self,x,x_,target,inverse): ## > evaluates x and x_ after `multiply` + if(x_ is None): + return None + else: + ## weight if ... + if(not self.imp)and(not target.discrete)and(inverse): + x_ = x_.weight(power=-1,overwrite=False) + ## inspect x + if(isinstance(x,field)): + if(x_.domain==self.target): + ## repair ... + if(x_.domain.nest[-1]!=x.domain): + x_ = x_.transform(target=nested_space([point_space(len(self.ind),datatype=x.domain.datatype),x.domain]),overwrite=False) ## ... domain + if(x_.target.nest[-1]!=x.target): + x_.set_target(newtarget=nested_space([point_space(len(self.ind),datatype=x.target.datatype),x.target])) ## ... codomain + else: + ## repair ... + if(x_.domain!=x.domain): + x_ = x_.transform(target=x.domain,overwrite=False) ## ... domain + if(x_.target!=x.target): + x_.set_target(newtarget=x.target) ## ... codomain + return x_ + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def times(self,x,**kwargs): + """ + Applies the operator to a given object + + Parameters + ---------- + x : {scalar, ndarray, field} + Scalars are interpreted as constant arrays, and an array will + be interpreted as a field on the domain of the operator. + + Returns + ------- + Ox : {field, tuple of fields} + Mapped field on the target domain of the operator. + + Other Parameters + ---------------- + band : int, *optional* + Projection band whereon to project (default: None). + bandsup: {integer, list/array of integers}, *optional* + List of projection bands whereon to project and which to sum + up. The `band` keyword is prefered over `bandsup` + (default: None). + split: bool, *optional* + Whether to return a tuple of the projected and residual field; + applys only if `band` or `bandsup` is given + (default: False). + + """ + ## prepare + x_ = self._briefing(x,self.domain,False) + ## apply operator + x_ = self._multiply(x_,**kwargs) + ## evaluate + y = self._debriefing(x,x_,self.target,False) + ## split if ... + if(kwargs.get("split",False))and((kwargs.has_key("band"))or(kwargs.has_key("bandsup"))): + return y,x-y + else: + return y + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def pseudo_tr(self,x,**kwargs): + """ + Computes the pseudo trace of a given object for all projection bands + + Parameters + ---------- + x : {field, operator} + The object whose pseudo-trace is to be computed. If the input is + a field, the pseudo trace equals the trace of + the projection operator mutliplied by a vector-vector operator + corresponding to the input field. This is also equal to the + pseudo inner product of the field with projected field itself. + If the input is a operator, the pseudo trace equals the trace of + the projection operator multiplied by the input operator. + random : string, *optional* + Specifies the pseudo random number generator. Valid + options are "pm1" for a random vector of +/-1, or "gau" + for a random vector with entries drawn from a Gaussian + distribution with zero mean and unit variance. + (default: "pm1") + ncpu : int, *optional* + number of used CPUs to use. (default: 2) + nrun : int, *optional* + total number of probes (default: 8) + nper : int, *optional* + number of tasks performed by one process (default: 1) + save : bool, *optional* + whether all individual probing results are saved or not + (default: False) + path : string, *optional* + path wherein the results are saved (default: "tmp") + prefix : string, *optional* + prefix for all saved files (default: "") + loop : bool, *optional* + Indicates whether or not to perform a loop i.e., to + parallelise (default: False) + + Returns + ------- + tr : float + Pseudo trace for all projection bands + """ + if(isinstance(x,operator)): + ## compute non-bare diagonal of the operator x + x = x.diag(bare=False,domain=self.domain,target=x.domain,var=False,**kwargs) + if(x is None): + raise TypeError(about._error.cstring("ERROR: 'NoneType' encountered.")) + + elif(isinstance(x,field)): + ## check domain + if(self.domain==x.domain): + x = x.val + else: + x = x.transform(target=self.domain,overwrite=False).val + ## compute non-bare diagonal of the vector-vector operator corresponding to the field x + x = x*np.conjugate(x) + ## weight + if(not self.domain.discrete): + x = self.domain.calc_weight(x,power=1) + + else: + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + + x = np.real(x.flatten(order='C')) + if(not self.domain.dim(split=False)==self.domain.dof()): + x *= np.round(np.real(self.domain.calc_weight(self.domain.get_meta_volume(total=False),power=-1).flatten(order='C')),decimals=0,out=None).astype(np.int) ## meta degrees of freedom + + tr = np.empty(self.bands(),dtype=x.dtype,order='C') + for bb in xrange(self.bands()): + tr[bb] = np.sum(x[self.ind[bb]],axis=None,dtype=None,out=None) + return tr + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def __repr__(self): + return "<nifty_core.projection_operator>" + +##----------------------------------------------------------------------------- + + + +##----------------------------------------------------------------------------- + +class vecvec_operator(operator): + """ + .. __ + .. __/ /__ + .. __ __ _______ _______ __ __ _______ _______ /__ __/ + .. | |/ / / __ / / ____/ | |/ / / __ / / ____/ /__/ + .. | / / /____/ / /____ | / / /____/ / /____ + .. |____/ \______/ \______/ |____/ \______/ \______/ operator class + + NIFTY subclass for vector-vector operators + + Parameters + ---------- + domain : space, *optional* + The space wherein valid arguments live. If none is given, the + space of the field given in val is used. (default: None) + val : {scalar, ndarray, field}, *optional* + The field from which to construct the operator. For a scalar, a constant + field is defined having the value provided. If no domain + is given, val must be a field. (default: 1) + + Attributes + ---------- + domain : space + The space wherein valid arguments live. + val : ndarray + The field from which the operator is derived. + sym : bool + Indicates whether the operator is self-adjoint or not + uni : bool + Indicates whether the operator is unitary or not + imp : bool + Indicates whether the incorporation of volume weights in + multiplications is already implemented in the `multiply` + instance methods or not + target : space + The space wherein the operator output lives. + """ + def __init__(self,domain=None,val=1): + """ + Sets the standard operator properties and `values`. + + Parameters + ---------- + domain : space, *optional* + The space wherein valid arguments live. If none is given, the + space of the field given in val is used. (default: None) + val : {scalar, ndarray, field}, *optional* + The field from which to construct the operator. For a scalar, a constant + field is defined having the value provided. If no domain + is given, val must be a field. (default: 1) + + Returns + ------- + None + """ + if(domain is None)and(isinstance(val,field)): + domain = val.domain + if(not isinstance(domain,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + self.domain = domain + self.val = self.domain.enforce_values(val,extend=True) + self.sym = True + self.uni = False + self.imp = False + self.target = self.domain + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def set_val(self,newval): + """ + Sets the field values of the operator + + Parameters + ---------- + newval : {scalar, ndarray, field} + The new field values. For a scalar, a constant + diagonal is defined having the value provided. If no domain + is given, diag must be a field. (default: 1) + + Returns + ------- + None + """ + self.val = self.domain.enforce_values(newval,extend=True) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _multiply(self,x,**kwargs): ## > applies the operator to a given field + x_ = field(self.target,val=None,target=x.target) + x_.val = self.val*self.domain.calc_dot(self.val,x.val) ## bypasses self.domain.enforce_values + return x_ + + def _inverse_multiply(self,x,**kwargs): + raise AttributeError(about._errors.cstring("ERROR: singular operator.")) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def tr(self,domain=None,**kwargs): + """ + Computes the trace of the operator + + Parameters + ---------- + domain : space, *optional* + space wherein the probes live (default: self.domain) + target : space, *optional* + space wherein the transform of the probes live + (default: None, applies target of the domain) + random : string, *optional* + Specifies the pseudo random number generator. Valid + options are "pm1" for a random vector of +/-1, or "gau" + for a random vector with entries drawn from a Gaussian + distribution with zero mean and unit variance. + (default: "pm1") + ncpu : int, *optional* + number of used CPUs to use. (default: 2) + nrun : int, *optional* + total number of probes (default: 8) + nper : int, *optional* + number of tasks performed by one process (default: 1) + var : bool, *optional* + Indicates whether to additionally return the probing variance + or not (default: False). + loop : bool, *optional* + Indicates whether or not to perform a loop i.e., to + parallelise (default: False) + + Returns + ------- + tr : float + Trace of the operator + delta : float, *optional* + Probing variance of the trace. Returned if `var` is True in + of probing case. + + """ + if(domain is None)or(domain==self.domain): + if(not self.domain.discrete): + return self.domain.calc_dot(self.val,self.domain.calc_weight(self.val,power=1)) + else: + return self.domain.calc_dot(self.val,self.val) + else: + return super(vecvec_operator,self).tr(domain=domain,**kwargs) ## probing + + def inverse_tr(self): + """ + Inverse is ill-defined for this operator. + """ + raise AttributeError(about._errors.cstring("ERROR: singular operator.")) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def diag(self,bare=False,domain=None,**kwargs): + """ + Computes the diagonal of the operator. + + Parameters + ---------- + bare : bool, *optional* + Indicatese whether the diagonal entries are `bare` or not + (mandatory for the correct incorporation of volume weights) + (default: False) + domain : space, *optional* + space wherein the probes live (default: self.domain) + target : space, *optional* + space wherein the transform of the probes live + (default: None, applies target of the domain) + random : string, *optional* + Specifies the pseudo random number generator. Valid + options are "pm1" for a random vector of +/-1, or "gau" + for a random vector with entries drawn from a Gaussian + distribution with zero mean and unit variance. + (default: "pm1") + ncpu : int, *optional* + number of used CPUs to use. (default: 2) + nrun : int, *optional* + total number of probes (default: 8) + nper : int, *optional* + number of tasks performed by one process (default: 1) + var : bool, *optional* + Indicates whether to additionally return the probing variance + or not (default: False). + save : bool, *optional* + whether all individual probing results are saved or not + (default: False) + path : string, *optional* + path wherein the results are saved (default: "tmp") + prefix : string, *optional* + prefix for all saved files (default: "") + loop : bool, *optional* + Indicates whether or not to perform a loop i.e., to + parallelise (default: False) + + Returns + ------- + diag : ndarray + The matrix diagonal + delta : float, *optional* + Probing variance of the trace. Returned if `var` is True in + of probing case. + + 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. + + """ + if(domain is None)or(domain==self.domain): + diag = np.real(self.val*np.conjugate(self.val)) ## bare diagonal + ## weight if ... + if(not self.domain.discrete)and(not bare): + return self.domain.calc_weight(diag,power=1) + else: + return diag + else: + return super(vecvec_operator,self).diag(bare=bare,domain=domain,**kwargs) ## probing + + def inverse_diag(self): + """ + Inverse is ill-defined for this operator. + + """ + raise AttributeError(about._errors.cstring("ERROR: singular operator.")) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def det(self): + """ + Computes the determinant of the operator. + + Returns + ------- + det : 0 + The determinant + + """ + return 0 + + def inverse_det(self): + """ + Inverse is ill-defined for this operator. + + """ + raise AttributeError(about._errors.cstring("ERROR: singular operator.")) + + def log_det(self): + """ + Logarithm of the determinant is ill-defined for this singular operator. + + """ + raise AttributeError(about._errors.cstring("ERROR: singular operator.")) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def __repr__(self): + return "<nifty_core.vecvec_operator>" + +##----------------------------------------------------------------------------- + + + +##----------------------------------------------------------------------------- + +class response_operator(operator): + """ + .. _____ _______ _______ ______ ______ __ ___ _______ _______ + .. / __/ / __ / / _____/ / _ | / _ | / _ | / _____/ / __ / + .. / / / /____/ /_____ / / /_/ / / /_/ / / / / / /_____ / / /____/ + .. /__/ \______/ /_______/ / ____/ \______/ /__/ /__/ /_______/ \______/ operator class + .. /__/ + + NIFTY subclass for response operators (of a certain family) + + Any response operator handles Gaussian convolutions, itemwise masking, + and selective mappings. + + Parameters + ---------- + domain : space + The space wherein valid arguments live. + sigma : float, *optional* + The standard deviation of the Gaussian kernel. Zero indicates + no convolution. (default: 0) + mask : {scalar, ndarray}, *optional* + Masking values for arguments (default: 1) + assign : {list, ndarray}, *optional* + Assignments of codomain items to domain items. A list of + indices/ index tuples or a one/ two-dimensional array. + (default: None) + den : bool, *optional* + Whether to consider the arguments as densities or not. + Mandatory for the correct incorporation of volume weights. + (default: False) + target : space, *optional* + The space wherein the operator output lives (default: domain) + + Attributes + ---------- + domain : space + The space wherein valid arguments live. + sym : bool + Indicates whether the operator is self-adjoint or not. + uni : bool + Indicates whether the operator is unitary or not. + imp : bool + Indicates whether the incorporation of volume weights in + multiplications is already implemented in the `multiply` + instance methods or not. + target : space + The space wherein the operator output lives + sigma : float + The standard deviation of the Gaussian kernel. Zero indicates + no convolution. + mask : {scalar, ndarray} + Masking values for arguments + assign : {list, ndarray} + Assignments of codomain items to domain items. A list of + indices/ index tuples or a one/ two-dimensional array. + den : bool + Whether to consider the arguments as densities or not. + Mandatory for the correct incorporation of volume weights. + """ + def __init__(self,domain,sigma=0,mask=1,assign=None,den=False,target=None): + """ + Sets the standard properties and `density`, `sigma`, `mask` and `assignment(s)`. + + Parameters + ---------- + domain : space + The space wherein valid arguments live. + sigma : float, *optional* + The standard deviation of the Gaussian kernel. Zero indicates + no convolution. (default: 0) + mask : {scalar, ndarray}, *optional* + Masking values for arguments (default: 1) + assign : {list, ndarray}, *optional* + Assignments of codomain items to domain items. A list of + indices/ index tuples or a one/ two-dimensional array. + (default: None) + den : bool, *optional* + Whether to consider the arguments as densities or not. + Mandatory for the correct incorporation of volume weights. + (default: False) + target : space, *optional* + The space wherein the operator output lives (default: domain) + + Returns + ------- + None + """ + if(not isinstance(domain,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + self.domain = domain + + self.sym = False + self.uni = False + self.imp = False + self.den = bool(den) + + self.mask = self.domain.enforce_values(mask,extend=False) + + ## check sigma + if(sigma<0): + raise ValueError(about._errors.cstring("ERROR: invalid sigma.")) + self.sigma = sigma + + ## check assignment(s) + if(assign is None): + ## 1:1 assignment + assignments = self.domain.dim(split=False) + self.assign = None + elif(np.size(self.domain.dim(split=True))==1): + if(np.isscalar(assign)): + ## X:1 assignment + assignments = 1 + if(assign[0]>=self.domain.dim(split=False))or(assign[0]<-self.domain.dim(split=False)): + raise IndexError(about._errors.cstring("ERROR: invalid bounds.")) + self.assign = [int(assign)] + else: + assign = np.array(assign,dtype=np.int) + assignments = len(assign) + if(np.ndim(assign)!=1): + raise ValueError(about._errors.cstring("ERROR: invalid input.")) + elif(np.any(assign>=self.domain.dim(split=False)))or(np.any(assign<-self.domain.dim(split=False))): + raise IndexError(about._errors.cstring("ERROR: invalid bounds.")) + if(assignments==len(np.unique(assign,return_index=False,return_inverse=False))): + self.assign = assign.tolist() + else: + self.assign = assign + else: + if(np.isscalar(assign)): + raise ValueError(about._errors.cstring("ERROR: invalid input.")) + else: + assign = np.array(assign,dtype=np.int) + assignments = np.size(assign,axis=0) + if(np.ndim(assign)!=2)or(np.size(assign,axis=1)!=np.size(self.domain.dim(split=True))): + raise ValueError(about._errors.cstring("ERROR: invalid input.")) + for ii in xrange(np.size(assign,axis=1)): + if(np.any(assign[:,ii]>=self.domain.dim(split=True)[ii]))or(np.any(assign[:,ii]<-self.domain.dim(split=True)[ii])): + raise IndexError(about._errors.cstring("ERROR: invalid bounds.")) + if(assignments==len(np.unique(np.ravel_multi_index(assign.T,self.domain.dim(split=True),mode="raise",order='C'),return_index=False,return_inverse=False))): + self.assign = assign.T.tolist() + else: + self.assign = assign + + if(target is None): + ## set target + target = point_space(assignments,datatype=self.domain.datatype) + else: + ## check target + if(not isinstance(target,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + elif(not target.discrete): + raise ValueError(about._errors.cstring("ERROR: continuous codomain.")) ## discrete(!) + elif(np.size(target.dim(split=True))!=1): + raise ValueError(about._errors.cstring("ERROR: structured codomain.")) ## unstructured(!) + elif(assignments!=target.dim(split=False)): + raise ValueError(about._errors.cstring("ERROR: dimension mismatch ( "+str(assignments)+" <> "+str(target.dim(split=False))+" ).")) + self.target = target + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def set_sigma(self,newsigma): + """ + Sets the standard deviation of the response operator, indicating + the amount of convolution. + + Parameters + ---------- + sigma : float + The standard deviation of the Gaussian kernel. Zero indicates + no convolution. + + Returns + ------- + None + """ + ## check sigma + if(newsigma<0): + raise ValueError(about._errors.cstring("ERROR: invalid sigma.")) + self.sigma = newsigma + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def set_mask(self,newmask): + """ + Sets the masking values of the response operator + + Parameters + ---------- + newmask : {scalar, ndarray} + masking values for arguments + + Returns + ------- + None + """ + self.mask = self.domain.enforce_values(newmask,extend=False) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _multiply(self,x,**kwargs): ## > applies the operator to a given field + ## smooth + x_ = self.domain.calc_smooth(x.val,sigma=self.sigma) + ## mask + x_ *= self.mask + ## assign and return + if(self.assign is None): + return field(self.target,val=x_,target=kwargs.get("target",None)) + elif(isinstance(self.assign,list)): + return field(self.target,val=x_[self.assign],target=kwargs.get("target",None)) + else: + return field(self.target,val=x_[self.assign.T.tolist()],target=kwargs.get("target",None)) + + def _adjoint_multiply(self,x,**kwargs): ## > applies the adjoint operator to a given field + x_ = np.zeros(self.domain.dim(split=True),dtype=self.domain.datatype,order='C') + ## assign (transposed) + if(self.assign is None): + x_ = np.copy(x.val.flatten(order='C')) + elif(isinstance(self.assign,list)): + x_[self.assign] += x.val.flatten(order='C') + else: + for ii in xrange(np.size(self.assign,axis=0)): + x_[np.array([self.assign[ii]]).T.tolist()] += x[ii] + ## mask + x_ *= self.mask + ## smooth + x_ = self.domain.calc_smooth(x_,sigma=self.sigma) + #return x_ + return field(self.domain,val=x_,target=kwargs.get("target",None)) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _briefing(self,x,domain,inverse): ## > prepares x for `multiply` + ## inspect x + if(not isinstance(x,field)): + x_ = field(domain,val=x,target=None) + else: + ## check x.domain + if(domain==x.domain): + x_ = x + ## transform + else: + x_ = x.transform(target=domain,overwrite=False) + ## weight if ... + if(not self.imp)and(not domain.discrete)and(not inverse)and(self.den): ## respect density + x_ = x_.weight(power=1,overwrite=False) + return x_ + + def _debriefing(self,x,x_,target,inverse): ## > evaluates x and x_ after `multiply` + if(x_ is None): + return None + else: + ## inspect x_ + if(not isinstance(x_,field)): + x_ = field(target,val=x_,target=None) + elif(x_.domain!=target): + raise ValueError(about._errors.cstring("ERROR: invalid output domain.")) + ## weight if ... + if(not self.imp)and(not target.discrete)and(not self.den^inverse): ## respect density + x_ = x_.weight(power=-1,overwrite=False) + ## inspect x + if(isinstance(x,field)): + ## repair ... + if(self.domain==self.target!=x.domain): + x_ = x_.transform(target=x.domain,overwrite=False) ## ... domain + if(x_.domain==x.domain)and(x_.target!=x.target): + x_.set_target(newtarget=x.target) ## ... codomain + return x_ + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def __repr__(self): + return "<nifty_core.response_operator>" + +##----------------------------------------------------------------------------- + + + +class invertible_operator(operator): + """ + .. __ __ __ __ __ + .. /__/ / /_ /__/ / / / / + .. __ __ ___ __ __ _______ _____ / _/ __ / /___ / / _______ + .. / / / _ || |/ / / __ / / __/ / / / / / _ | / / / __ / + .. / / / / / / | / / /____/ / / / /_ / / / /_/ / / /_ / /____/ + .. /__/ /__/ /__/ |____/ \______/ /__/ \___/ /__/ \______/ \___/ \______/ operator class + + NIFTY subclass for invertible, self-adjoint (linear) operators + + The invertible operator class is an abstract class for self-adjoint or + symmetric (linear) operators from which other more specific operator + subclassescan be derived. Such operators inherit an automated inversion + routine, namely conjugate gradient. + + Parameters + ---------- + domain : space + The space wherein valid arguments live. + uni : bool, *optional* + Indicates whether the operator is unitary or not. + (default: False) + imp : bool, *optional* + Indicates whether the incorporation of volume weights in + multiplications is already implemented in the `multiply` + instance methods or not (default: False). + para : {single object, tuple/list of objects}, *optional* + This is a freeform tuple/list of parameters that derivatives of + the operator class can use (default: None). + + See Also + -------- + operator + + Notes + ----- + This class is not meant to be instantiated. Operator classes derived + from this one only need a `_multiply` or `_inverse_multiply` instance + method to perform the other. However, one of them needs to be defined. + + Attributes + ---------- + domain : space + The space wherein valid arguments live. + sym : bool + Indicates whether the operator is self-adjoint or not. + uni : bool + Indicates whether the operator is unitary or not. + imp : bool + Indicates whether the incorporation of volume weights in + multiplications is already implemented in the `multiply` + instance methods or not. + target : space + The space wherein the operator output lives. + para : {single object, list of objects} + This is a freeform tuple/list of parameters that derivatives of + the operator class can use. Not used in the base operators. + + """ + def __init__(self,domain,uni=False,imp=False,para=None): + """ + Sets the standard operator properties. + + Parameters + ---------- + domain : space + The space wherein valid arguments live. + uni : bool, *optional* + Indicates whether the operator is unitary or not. + (default: False) + imp : bool, *optional* + Indicates whether the incorporation of volume weights in + multiplications is already implemented in the `multiply` + instance methods or not (default: False). + para : {single object, tuple/list of objects}, *optional* + This is a freeform tuple/list of parameters that derivatives of + the operator class can use (default: None). + + """ + if(not isinstance(domain,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + self.domain = domain + self.sym = True + self.uni = bool(uni) + + if(self.domain.discrete): + self.imp = True + else: + self.imp = bool(imp) + + self.target = self.domain + + if(para is not None): + self.para = para + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _multiply(self,x,force=False,W=None,spam=None,reset=None,note=False,x0=None,tol=1E-4,clevel=1,limii=None,**kwargs): + """ + Applies the invertible operator to a given field by invoking a + conjugate gradient. + + Parameters + ---------- + x : field + Valid input field. + force : bool + Indicates wheter to return a field instead of ``None`` in case + the conjugate gradient fails. + + Returns + ------- + OIIx : field + Mapped field with suitable domain. + + See Also + -------- + conjugate_gradient + + Other Parameters + ---------------- + W : {operator, function}, *optional* + Operator `W` that is a preconditioner on `A` and is applicable to a + field (default: None). + spam : function, *optional* + Callback function which is given the current `x` and iteration + counter each iteration (default: None). + reset : integer, *optional* + Number of iterations after which to restart; i.e., forget previous + conjugated directions (default: sqrt(b.dim())). + note : bool, *optional* + Indicates whether notes are printed or not (default: False). + x0 : field, *optional* + Starting guess for the minimization. + tol : scalar, *optional* + Tolerance specifying convergence; measured by current relative + residual (default: 1E-4). + clevel : integer, *optional* + Number of times the tolerance should be undershot before + exiting (default: 1). + limii : integer, *optional* + Maximum number of iterations performed (default: 10 * b.dim()). + + """ + x_,convergence = conjugate_gradient(self.inverse_times,x,W=W,spam=spam,reset=reset,note=note)(x0=x0,tol=tol,clevel=clevel,limii=limii) + ## check convergence + if(not convergence): + if(not force)or(x_ is None): + return None + about.warnings.cprint("WARNING: conjugate gradient failed.") + ## weight if ... + if(not self.imp): ## continiuos domain/target + x_.weight(power=-1,overwrite=True) + return x_ + + def _inverse_multiply(self,x,force=False,W=None,spam=None,reset=None,note=False,x0=None,tol=1E-4,clevel=1,limii=None,**kwargs): + """ + Applies the inverse of the invertible operator to a given field by + invoking a conjugate gradient. + + Parameters + ---------- + x : field + Valid input field. + force : bool + Indicates wheter to return a field instead of ``None`` in case + the conjugate gradient fails. + + Returns + ------- + OIx : field + Mapped field with suitable domain. + + See Also + -------- + conjugate_gradient + + Other Parameters + ---------------- + W : {operator, function}, *optional* + Operator `W` that is a preconditioner on `A` and is applicable to a + field (default: None). + spam : function, *optional* + Callback function which is given the current `x` and iteration + counter each iteration (default: None). + reset : integer, *optional* + Number of iterations after which to restart; i.e., forget previous + conjugated directions (default: sqrt(b.dim())). + note : bool, *optional* + Indicates whether notes are printed or not (default: False). + x0 : field, *optional* + Starting guess for the minimization. + tol : scalar, *optional* + Tolerance specifying convergence; measured by current relative + residual (default: 1E-4). + clevel : integer, *optional* + Number of times the tolerance should be undershot before + exiting (default: 1). + limii : integer, *optional* + Maximum number of iterations performed (default: 10 * b.dim()). + + """ + x_,convergence = conjugate_gradient(self.times,x,W=W,spam=spam,reset=reset,note=note)(x0=x0,tol=tol,clevel=clevel,limii=limii) + ## check convergence + if(not convergence): + if(not force)or(x_ is None): + return None + about.warnings.cprint("WARNING: conjugate gradient failed.") + ## weight if ... + if(not self.imp): ## continiuos domain/target + x_.weight(power=1,overwrite=True) + return x_ + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def __repr__(self): + return "<nifty_tools.invertible_operator>" + +##----------------------------------------------------------------------------- + +##----------------------------------------------------------------------------- + +class propagator_operator(operator): + """ + .. __ + .. / /_ + .. _______ _____ ______ ______ ____ __ ____ __ ____ __ / _/ ______ _____ + .. / _ / / __/ / _ | / _ | / _ / / _ / / _ / / / / _ | / __/ + .. / /_/ / / / / /_/ / / /_/ / / /_/ / / /_/ / / /_/ / / /_ / /_/ / / / + .. / ____/ /__/ \______/ / ____/ \______| \___ / \______| \___/ \______/ /__/ operator class + .. /__/ /__/ /______/ + + NIFTY subclass for propagator operators (of a certain family) + + The propagator operators :math:`D` implemented here have an inverse + formulation like :math:`(S^{-1} + M)`, :math:`(S^{-1} + N^{-1})`, or + :math:`(S^{-1} + R^\dagger N^{-1} R)` as appearing in Wiener filter + theory. + + 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. + + See Also + -------- + conjugate_gradient + + Notes + ----- + The propagator will puzzle the operators `S` and `M` or `R`, `N` or + only `N` together in the predefined from, a domain is set + automatically. The application of the inverse is done by invoking a + conjugate gradient. + Note that changes to `S`, `M`, `R` or `N` auto-update the propagator. + + Examples + -------- + >>> f = field(rg_space(4), val=[2, 4, 6, 8]) + >>> S = power_operator(f.target, spec=1) + >>> N = diagonal_operator(f.domain, diag=1) + >>> D = propagator_operator(S=S, N=N) # D^{-1} = S^{-1} + N^{-1} + >>> D(f).val + array([ 1., 2., 3., 4.]) + + Attributes + ---------- + domain : space + A space wherein valid arguments live. + codomain : space + An alternative space wherein valid arguments live; commonly the + codomain of the `domain` attribute. + sym : bool + Indicates that the operator is self-adjoint. + uni : bool + Indicates that the operator is not unitary. + imp : bool + Indicates that volume weights are implemented in the `multiply` + instance methods. + target : space + The space wherein the operator output lives. + _A1 : {operator, function} + Application of :math:`S^{-1}` to a field. + _A2 : {operator, function} + Application of all operations not included in `A1` to a field. + RN : {2-tuple of operators}, *optional* + Contains `R` and `N` if given. + + """ + def __init__(self,S=None,M=None,R=None,N=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. + + """ + ## check signal prior covariance + if(S is None): + raise Exception(about._errors.cstring("ERROR: insufficient input.")) + elif(not isinstance(S,operator)): + raise ValueError(about._errors.cstring("ERROR: invalid input.")) + space1 = S.domain + + ## check likelihood (pseudo) covariance + if(M is None): + if(N is None): + raise Exception(about._errors.cstring("ERROR: insufficient input.")) + elif(not isinstance(N,operator)): + raise ValueError(about._errors.cstring("ERROR: invalid input.")) + if(R is None): + space2 = N.domain + elif(not isinstance(R,operator)): + raise ValueError(about._errors.cstring("ERROR: invalid input.")) + else: + space2 = R.domain + elif(not isinstance(M,operator)): + raise ValueError(about._errors.cstring("ERROR: invalid input.")) + else: + space2 = M.domain + + ## set spaces + self.domain = space2 + if(self.domain.check_codomain(space1)): + self.codomain = space1 + else: + raise ValueError(about._errors.cstring("ERROR: invalid input.")) + self.target = self.domain + + ## define A1 == S_inverse + if(isinstance(S,diagonal_operator)): + self._A1 = S._inverse_multiply ## S.imp == True + else: + self._A1 = S.inverse_times + + ## define A2 == M == R_adjoint N_inverse R == N_inverse + if(M is None): + if(R is not None): + self.RN = (R,N) + if(isinstance(N,diagonal_operator)): + self._A2 = self._standard_M_times_1 + else: + self._A2 = self._standard_M_times_2 + elif(isinstance(N,diagonal_operator)): + self._A2 = N._inverse_multiply ## N.imp == True + else: + self._A2 = N.inverse_times + elif(isinstance(M,diagonal_operator)): + self._A2 = M._multiply ## M.imp == True + else: + self._A2 = M.times + + self.sym = True + self.uni = False + self.imp = True + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _standard_M_times_1(self,x,**kwargs): ## applies > R_adjoint N_inverse R assuming N is diagonal + return self.RN[0].adjoint_times(self.RN[1]._inverse_multiply(self.RN[0].times(x))) ## N.imp = True + + def _standard_M_times_2(self,x,**kwargs): ## applies > R_adjoint N_inverse R + return self.RN[0].adjoint_times(self.RN[1].inverse_times(self.RN[0].times(x))) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _inverse_multiply_1(self,x,**kwargs): ## > applies A1 + A2 in self.codomain + return self._A1(x,pseudo=True)+self._A2(x.transform(self.domain)).transform(self.codomain) + + def _inverse_multiply_2(self,x,**kwargs): ## > applies A1 + A2 in self.domain + return self._A1(x.transform(self.codomain),pseudo=True).transform(self.domain)+self._A2(x) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _briefing(self,x): ## > prepares x for `multiply` + ## inspect x + if(not isinstance(x,field)): + return field(self.domain,val=x,target=None),False + ## check x.domain + elif(x.domain==self.domain): + return x,False + elif(x.domain==self.codomain): + return x,True + ## transform + else: + return x.transform(target=self.codomain,overwrite=False),True + + def _debriefing(self,x,x_,in_codomain): ## > evaluates x and x_ after `multiply` + if(x_ is None): + return None + ## inspect x + elif(isinstance(x,field)): + ## repair ... + if(in_codomain)and(x.domain!=self.codomain): + x_ = x_.transform(target=x.domain,overwrite=False) ## ... domain + if(x_.target!=x.target): + x_.set_target(newtarget=x.target) ## ... codomain + return x_ + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def times(self,x,force=False,W=None,spam=None,reset=None,note=False,x0=None,tol=1E-4,clevel=1,limii=None,**kwargs): + """ + Applies the propagator to a given object by invoking a + conjugate gradient. + + Parameters + ---------- + x : {scalar, list, array, field} + Scalars are interpreted as constant arrays, and an array will + be interpreted as a field on the domain of the operator. + force : bool + Indicates wheter to return a field instead of ``None`` in case + the conjugate gradient fails. + + Returns + ------- + Dx : field + Mapped field with suitable domain. + + See Also + -------- + conjugate_gradient + + Other Parameters + ---------------- + W : {operator, function}, *optional* + Operator `W` that is a preconditioner on `A` and is applicable to a + field (default: None). + spam : function, *optional* + Callback function which is given the current `x` and iteration + counter each iteration (default: None). + reset : integer, *optional* + Number of iterations after which to restart; i.e., forget previous + conjugated directions (default: sqrt(b.dim())). + note : bool, *optional* + Indicates whether notes are printed or not (default: False). + x0 : field, *optional* + Starting guess for the minimization. + tol : scalar, *optional* + Tolerance specifying convergence; measured by current relative + residual (default: 1E-4). + clevel : integer, *optional* + Number of times the tolerance should be undershot before + exiting (default: 1). + limii : integer, *optional* + Maximum number of iterations performed (default: 10 * b.dim()). + + """ + ## prepare + x_,in_codomain = self._briefing(x) + ## apply operator + if(in_codomain): + A = self._inverse_multiply_1 + else: + A = self._inverse_multiply_2 + x_,convergence = conjugate_gradient(A,x_,W=W,spam=spam,reset=reset,note=note)(x0=x0,tol=tol,clevel=clevel,limii=limii) + ## evaluate + if(not convergence): + if(not force): + return None + about.warnings.cprint("WARNING: conjugate gradient failed.") + return self._debriefing(x,x_,in_codomain) + + def inverse_times(self,x,**kwargs): + """ + Applies the inverse propagator to a given object. + + Parameters + ---------- + x : {scalar, list, array, field} + Scalars are interpreted as constant arrays, and an array will + be interpreted as a field on the domain of the operator. + + Returns + ------- + DIx : field + Mapped field with suitable domain. + + """ + ## prepare + x_,in_codomain = self._briefing(x) + ## apply operator + if(in_codomain): + x_ = self._inverse_multiply_1(x_) + else: + x_ = self._inverse_multiply_2(x_) + ## evaluate + return self._debriefing(x,x_,in_codomain) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def __repr__(self): + return "<nifty_tools.propagator_operator>" + +##----------------------------------------------------------------------------- diff --git a/operators/nifty_probing.py b/operators/nifty_probing.py new file mode 100644 index 0000000000000000000000000000000000000000..ed8875365ac4a308603cdda4dd6d69a1c3c9fcc0 --- /dev/null +++ b/operators/nifty_probing.py @@ -0,0 +1,1330 @@ +## NIFTY (Numerical Information Field Theory) has been developed at the +## Max-Planck-Institute for Astrophysics. +## +## Copyright (C) 2015 Max-Planck-Society +## +## Author: Marco Selig +## Project homepage: <http://www.mpa-garching.mpg.de/ift/nifty/> +## +## 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 __future__ import division + +import os +from sys import stdout as so +import numpy as np +from multiprocessing import Pool as mp +from multiprocessing import Value as mv +from multiprocessing import Array as ma + +from nifty.nifty_about import about +from nifty.nifty_core import space, \ + field + + + +##----------------------------------------------------------------------------- + +class _share(object): + + __init__ = None + + @staticmethod + def _init_share(_sum,_num,_var): + _share.sum = _sum + _share.num = _num + _share.var = _var + +##----------------------------------------------------------------------------- + +##============================================================================= + +class probing(object): + """ + .. __ __ + .. / / /__/ + .. ______ _____ ______ / /___ __ __ ___ ____ __ + .. / _ | / __/ / _ | / _ | / / / _ | / _ / + .. / /_/ / / / / /_/ / / /_/ / / / / / / / / /_/ / + .. / ____/ /__/ \______/ \______/ /__/ /__/ /__/ \____ / class + .. /__/ /______/ + + NIFTY class for probing (using multiprocessing) + + This is the base NIFTY probing class from which other probing classes + (e.g. diagonal probing) are derived. + + When called, a probing class instance evaluates an operator or a + function using random fields, whose components are random variables + with mean 0 and variance 1. When an instance is called it returns the + mean value of f(probe), where probe is a random field with mean 0 and + variance 1. The mean is calculated as 1/N Sum[ f(probe_i) ]. + + Parameters + ---------- + op : operator + The operator specified by `op` is the operator to be probed. + If no operator is given, then probing will be done by applying + `function` to the probes. (default: None) + function : function, *optional* + If no operator has been specified as `op`, then specification of + `function` is non optional. This is the function, that is applied + to the probes. (default: `op.times`) + domain : space, *optional* + If no operator has been specified as `op`, then specification of + `domain` is non optional. This is the space that the probes live + in. (default: `op.domain`) + target : domain, *optional* + `target` is the codomain of `domain` + (default: `op.domain.get_codomain()`) + random : string, *optional* + the distribution from which the probes are drawn. `random` can be + either "pm1" or "gau". "pm1" is a uniform distribution over {+1,-1} + or {+1,+i,-1,-i}, respectively. "gau" is a normal distribution with + zero-mean and unit-variance (default: "pm1") + ncpu : int, *optional* + the number of cpus to be used from parallel probing. (default: 2) + nrun : int, *optional* + the number of probes to be evaluated. If `nrun<ncpu`, it will be + set to `ncpu`. (default: 8) + nper : int, *optional* + this number specifies how many probes will be evaluated by one + worker. Afterwards a new worker will be created to evaluate a chunk + of `nper` probes. + If for example `nper=nrun/ncpu`, then every worker will be created + for every cpu. This can lead to the case, that all workers but one + are already finished, but have to wait for the last worker that + might still have a considerable amount of evaluations left. This is + obviously not very effective. + If on the other hand `nper=1`, then for each evaluation a worker will + be created. In this case all cpus will work until nrun probes have + been evaluated. + It is recommended to leave `nper` as the default value. (default: 1) + var : bool, *optional* + If `var` is True, then the variance of the sampled function will + also be returned. The result is then a tuple with the mean in the + zeroth entry and the variance in the first entry. (default: False) + + + See Also + -------- + diagonal_probing : A probing class to get the diagonal of an operator + trace_probing : A probing class to get the trace of an operator + + + Attributes + ---------- + function : function + the function, that is applied to the probes + domain : space + the space, where the probes live in + target : space + the codomain of `domain` + random : string + the random number generator used to create the probes + (either "pm1" or "gau") + ncpu : int + the number of cpus used for probing + nrun : int + the number of probes to be evaluated, when the instance is called + nper : int + number of probes, that will be evaluated by one worker + var : bool + whether the variance will be additionally returned, when the + instance is called + quargs : dict + Keyword arguments passed to `function` in each call. + + """ + def __init__(self,op=None,function=None,domain=None,target=None,random="pm1",ncpu=2,nrun=8,nper=1,var=False,**quargs): + """ + initializes a probing instance + + Parameters + ---------- + op : operator + The operator specified by `op` is the operator to be probed. + If no operator is given, then probing will be done by applying + `function` to the probes. (default: None) + function : function, *optional* + If no operator has been specified as `op`, then specification of + `function` is non optional. This is the function, that is applied + to the probes. (default: `op.times`) + domain : space, *optional* + If no operator has been specified as `op`, then specification of + `domain` is non optional. This is the space that the probes live + in. (default: `op.domain`) + target : domain, *optional* + `target` is the codomain of `domain` + (default: `op.domain.get_codomain()`) + random : string, *optional* + the distribution from which the probes are drawn. `random` can be + either "pm1" or "gau". "pm1" is a uniform distribution over {+1,-1} + or {+1,+i,-1,-i}, respectively. "gau" is a normal distribution with + zero-mean and unit-variance (default: "pm1") + ncpu : int, *optional* + the number of cpus to be used from parallel probing. (default: 2) + nrun : int, *optional* + the number of probes to be evaluated. If `nrun<ncpu`, it will be + set to `ncpu`. (default: 8) + nper : int, *optional* + this number specifies how many probes will be evaluated by one + worker. Afterwards a new worker will be created to evaluate a chunk + of `nper` probes. + If for example `nper=nrun/ncpu`, then every worker will be created + for every cpu. This can lead to the case, that all workers but one + are already finished, but have to wait for the last worker that + might still have a considerable amount of evaluations left. This is + obviously not very effective. + If on the other hand `nper=1`, then for each evaluation a worker will + be created. In this case all cpus will work until nrun probes have + been evaluated. + It is recommended to leave `nper` as the default value. (default: 1) + var : bool, *optional* + If `var` is True, then the variance of the sampled function will + also be returned. The result is then a tuple with the mean in the + zeroth entry and the variance in the first entry. (default: False) + + """ + if(op is None): + ## check whether callable + if(function is None)or(not hasattr(function,"__call__")): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + ## check given domain + if(domain is None)or(not isinstance(domain,space)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + else: + from nifty_operators import operator + if(not isinstance(op,operator)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + ## check whether callable + if(function is None)or(not hasattr(function,"__call__")): + function = op.times + elif(op==function): + function = op.times + ## check whether correctly bound + if(op!=function.im_self): + raise NameError(about._errors.cstring("ERROR: invalid input.")) + ## check given shape and domain + if(domain is None)or(not isinstance(domain,space)): + if(function in [op.inverse_times,op.adjoint_times]): + domain = op.target + else: + domain = op.domain + else: + if(function in [op.inverse_times,op.adjoint_times]): + op.target.check_codomain(domain) ## a bit pointless + if(target is None)or(not isinstance(target,space)): + target = op.target + else: + op.domain.check_codomain(domain) ## a bit pointless + if(target is None)or(not isinstance(target,space)): + target = op.domain + + self.function = function + self.domain = domain + + ## check codomain + if(target is None): + target = self.domain.get_codomain() + else: + self.domain.check_codomain(target) ## a bit pointless + self.target = target + + if(random not in ["pm1","gau"]): + raise ValueError(about._errors.cstring("ERROR: unsupported random key '"+str(random)+"'.")) + self.random = random + + self.ncpu = int(max(1,ncpu)) + self.nrun = int(max(self.ncpu,nrun)) + if(nper is None): + self.nper = None + else: + self.nper = int(max(1,min(self.nrun//self.ncpu,nper))) + + self.var = bool(var) + + self.quargs = quargs + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def configure(self,**kwargs): + """ + changes the attributes of the instance + + Parameters + ---------- + random : string, *optional* + the random number generator used to create the probes (default: "pm1") + ncpu : int, *optional* + the number of cpus to be used for parallel probing. (default: 2) + nrun : int, *optional* + the number of probes to be evaluated. If `nrun<ncpu`, it will be + set to `ncpu`. (default: 8) + nper : int, *optional* + number of probes, that will be evaluated by one worker (default: 1) + var : bool, *optional* + whether the variance will be additionally returned (default: False) + + """ + if("random" in kwargs): + if(kwargs.get("random") not in ["pm1","gau"]): + raise ValueError(about._errors.cstring("ERROR: unsupported random key '"+str(kwargs.get("random"))+"'.")) + self.random = kwargs.get("random") + + if("ncpu" in kwargs): + self.ncpu = int(max(1,kwargs.get("ncpu"))) + if("nrun" in kwargs): + self.nrun = int(max(self.ncpu,kwargs.get("nrun"))) + if("nper" in kwargs): + if(kwargs.get("nper") is None): + self.nper = None + else: + self.nper = int(max(1,min(self.nrun//self.ncpu,kwargs.get("nper")))) + + if("var" in kwargs): + self.var = bool(kwargs.get("var")) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def gen_probe(self): + """ + Generates a single probe + + Returns + ------- + probe : field + a random field living in `domain` with mean 0 and variance 1 in + each component + + """ + return field(self.domain,val=None,target=self.target,random=self.random) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def probing(self,idnum,probe): + """ + Computes a single probing result given one probe + + Parameters + ---------- + probe : field + the field on which `function` will be applied + idnum : int + the identification number of the probing + + Returns + ------- + result : array-like + the result of applying `function` to `probe`. The exact type + depends on the function. + + """ + f = self.function(probe,**self.quargs) + if(isinstance(f,field)): + return f.val + else: + return f + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def evaluate(self,summa,num,var): + """ + Evaluates the probing results. + + Parameters + ---------- + summa : numpy.array + Sum of all probing results. + num : int + Number of successful probings (not returning ``None``). + var : numpy.array + Sum of all squared probing results + + Returns + ------- + final : numpy.array + The final probing result; 1/N Sum[ probing(probe_i) ] + var : array-like + The variance of the final probing result; + 1/(N(N-1)) Sum[( probing(probe_i) - final )^2]; + if the variance is returned, the return will be a tuple of + (`final`,`var`). + + """ + if(num<self.nrun): + about.infos.cflush(" ( %u probe(s) failed, effectiveness == %.1f%% )\n"%(self.nrun-num,100*num/self.nrun)) + if(num==0): + about.warnings.cprint("WARNING: probing failed.") + return None + else: + about.infos.cflush("\n") + + if(summa.size==1): + summa = summa.flatten(order='C')[0] + if(self.var): + var = var.flatten(order='C')[0] + if(np.iscomplexobj(summa))and(np.all(np.imag(summa)==0)): + summa = np.real(summa) + + final = summa*(1/num) + if(self.var): + if(num==1): + about.warnings.cprint("WARNING: infinite variance.") + return final,None + else: + var = var*(1/(num*(num-1)))-np.real(np.conjugate(final)*final)*(1/(num-1)) + return final,var + else: + return final + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _progress(self,irun): ## > prints progress status by in upto 10 dots + tenths = 1+(10*irun//self.nrun) + about.infos.cflush(("\b")*10+('.')*tenths+(' ')*(10-tenths)) + + def _single_probing(self,zipped): ## > performs one probing operation + ## generate probe + np.random.seed(zipped[0]) + probe = self.gen_probe() + ## do the actual probing + return self.probing(zipped[1],probe) + + def _serial_probing(self,zipped): ## > performs the probing operation serially + try: + result = self._single_probing(zipped) + except Exception as exception: + raise exception + except BaseException: ## capture system-exiting exception including KeyboardInterrupt + raise Exception(about._errors.cstring("ERROR: unknown.")) + else: + if(result is not None): + result = np.array(result).flatten(order='C') + if(np.iscomplexobj(result)): + _share.sum[0].acquire(block=True,timeout=None) + _share.sum[0][:] += np.real(result) + _share.sum[0].release() + _share.sum[1].acquire(block=True,timeout=None) + _share.sum[1][:] += np.imag(result) + _share.sum[1].release() + else: + _share.sum.acquire(block=True,timeout=None) + _share.sum[:] += result + _share.sum.release() + _share.num.acquire(block=True,timeout=None) + _share.num.value += 1 + _share.num.release() + if(self.var): + _share.var.acquire(block=True,timeout=None) + _share.var[:] += np.real(np.conjugate(result)*result) + _share.var.release() + self._progress(_share.num.value) + + def _parallel_probing(self): ## > performs the probing operations in parallel + ## define random seed + seed = np.random.randint(10**8,high=None,size=self.nrun) + ## get output shape + result = self.probing(0,field(self.domain,val=None,target=self.target)) + if(np.isscalar(result))or(np.size(result)==1): + shape = np.ones(1,dtype=np.int,order='C') + else: + shape = np.array(np.array(result).shape) + ## define shared objects + if(np.iscomplexobj(result)): + _sum = (ma('d',np.zeros(np.prod(shape,axis=0,dtype=np.int,out=None),dtype=np.float64,order='C'),lock=True),ma('d',np.zeros(np.prod(shape,axis=0,dtype=np.int,out=None),dtype=np.float64,order='C'),lock=True)) ## tuple(real,imag) + else: + _sum = ma('d',np.zeros(np.prod(shape,axis=0,dtype=np.int,out=None),dtype=np.float64,order='C'),lock=True) + _num = mv('I',0,lock=True) + if(self.var): + _var = ma('d',np.zeros(np.prod(shape,axis=0,dtype=np.int,out=None),dtype=np.float64,order='C'),lock=True) + else: + _var = None + ## build pool + if(about.infos.status): + so.write(about.infos.cstring("INFO: multiprocessing "+(' ')*10)) + so.flush() + pool = mp(processes=self.ncpu,initializer=_share._init_share,initargs=(_sum,_num,_var),maxtasksperchild=self.nper) + try: + ## pooling + pool.map_async(self._serial_probing,zip(seed,np.arange(self.nrun,dtype=np.int)),chunksize=None,callback=None).get(timeout=None) + ## close and join pool + about.infos.cflush(" done.") + pool.close() + pool.join() + except BaseException as exception: + ## terminate and join pool + about._errors.cprint("\nERROR: terminating pool.") + pool.terminate() + pool.join() + ## re-raise exception + raise exception ## traceback by looping + ## evaluate + if(np.iscomplexobj(result)): + _sum = (np.array(_sum[0][:])+np.array(_sum[1][:])*1j).reshape(shape) ## comlpex array + else: + _sum = np.array(_sum[:]).reshape(shape) + if(self.var): + _var = np.array(_var[:]).reshape(shape) + return self.evaluate(_sum,_num.value,_var) + + def _nonparallel_probing(self): ## > performs the probing operations one after another + ## looping + if(about.infos.status): + so.write(about.infos.cstring("INFO: looping "+(' ')*10)) + so.flush() + _sum = 0 + _num = 0 + _var = 0 + for ii in xrange(self.nrun): + result = self._single_probing((np.random.randint(10**8,high=None,size=None),ii)) ## tuple(seed,idnum) + if(result is not None): + _sum += result ## result: {scalar, np.array} + _num += 1 + if(self.var): + _var += np.real(np.conjugate(result)*result) + self._progress(_num) + about.infos.cflush(" done.") + ## evaluate + return self.evaluate(_sum,_num,_var) + + def __call__(self,loop=False,**kwargs): + """ + + Starts the probing process. + All keyword arguments that can be given to `configure` can also be + given to `__call__` and have the same effect. + + Parameters + ---------- + loop : bool, *optional* + if `loop` is True, then multiprocessing will be disabled and + all probes are evaluated by a single worker (default: False) + + Returns + ------- + results : see **Returns** in `evaluate` + + other parameters + ---------------- + kwargs : see **Parameters** in `configure` + + """ + self.configure(**kwargs) + if(not about.multiprocessing.status)or(loop): + return self._nonparallel_probing() + else: + return self._parallel_probing() + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def __repr__(self): + return "<nifty_core.probing>" + +##============================================================================= + + + +##----------------------------------------------------------------------------- + +class trace_probing(probing): + """ + .. __ + .. / /_ + .. / _/ _____ ____ __ _______ _______ + .. / / / __/ / _ / / ____/ / __ / + .. / /_ / / / /_/ / / /____ / /____/ + .. \___/ /__/ \______| \______/ \______/ probing class + + NIFTY subclass for trace probing (using multiprocessing) + + When called, a trace_probing class instance samples the trace of an + operator or a function using random fields, whose components are random + variables with mean 0 and variance 1. When an instance is called it + returns the mean value of the scalar product of probe and f(probe), + where probe is a random field with mean 0 and variance 1. + The mean is calculated as 1/N Sum[ probe_i.dot(f(probe_i)) ]. + + Parameters + ---------- + op : operator + The operator specified by `op` is the operator to be probed. + If no operator is given, then probing will be done by applying + `function` to the probes. (default: None) + function : function, *optional* + If no operator has been specified as `op`, then specification of + `function` is non optional. This is the function, that is applied + to the probes. (default: `op.times`) + domain : space, *optional* + If no operator has been specified as `op`, then specification of + `domain` is non optional. This is the space that the probes live + in. (default: `op.domain`) + target : domain, *optional* + `target` is the codomain of `domain` + (default: `op.domain.get_codomain()`) + random : string, *optional* + the distribution from which the probes are drawn. `random` can be + either "pm1" or "gau". "pm1" is a uniform distribution over {+1,-1} + or {+1,+i,-1,-i}, respectively. "gau" is a normal distribution with + zero-mean and unit-variance (default: "pm1") + ncpu : int, *optional* + the number of cpus to be used from parallel probing. (default: 2) + nrun : int, *optional* + the number of probes to be evaluated. If `nrun<ncpu`, it will be + set to `ncpu`. (default: 8) + nper : int, *optional* + this number specifies how many probes will be evaluated by one + worker. Afterwards a new worker will be created to evaluate a chunk + of `nper` probes. + If for example `nper=nrun/ncpu`, then every worker will be created + for every cpu. This can lead to the case, that all workers but one + are already finished, but have to wait for the last worker that + might still have a considerable amount of evaluations left. This is + obviously not very effective. + If on the other hand `nper=1`, then for each evaluation a worker will + be created. In this case all cpus will work until nrun probes have + been evaluated. + It is recommended to leave `nper` as the default value. (default: 1) + var : bool, *optional* + If `var` is True, then the variance of the sampled function will + also be returned. The result is then a tuple with the mean in the + zeroth entry and the variance in the first entry. (default: False) + + + See Also + -------- + probing : The base probing class + diagonal_probing : A probing class to get the diagonal of an operator + operator.tr : the trace function uses trace probing in the case of non + diagonal operators + + + Attributes + ---------- + function : function + the function, that is applied to the probes + domain : space + the space, where the probes live in + target : space + the codomain of `domain` + random : string + the random number generator used to create the probes + (either "pm1" or "gau") + ncpu : int + the number of cpus used for probing + nrun : int + the number of probes to be evaluated, when the instance is called + nper : int + number of probes, that will be evaluated by one worker + var : bool + whether the variance will be additionally returned, when the + instance is called + quargs : dict + Keyword arguments passed to `function` in each call. + + """ + def __init__(self,op,function=None,domain=None,target=None,random="pm1",ncpu=2,nrun=8,nper=1,var=False,**quargs): + """ + initializes a trace probing instance + + Parameters + ---------- + op : operator + The operator specified by `op` is the operator to be probed. + If no operator is given, then probing will be done by applying + `function` to the probes. (default: None) + function : function, *optional* + If no operator has been specified as `op`, then specification of + `function` is non optional. This is the function, that is applied + to the probes. (default: `op.times`) + domain : space, *optional* + If no operator has been specified as `op`, then specification of + `domain` is non optional. This is the space that the probes live + in. (default: `op.domain`) + target : domain, *optional* + `target` is the codomain of `domain` + (default: `op.domain.get_codomain()`) + random : string, *optional* + the distribution from which the probes are drawn. `random` can be + either "pm1" or "gau". "pm1" is a uniform distribution over {+1,-1} + or {+1,+i,-1,-i}, respectively. "gau" is a normal distribution with + zero-mean and unit-variance (default: "pm1") + ncpu : int, *optional* + the number of cpus to be used from parallel probing. (default: 2) + nrun : int, *optional* + the number of probes to be evaluated. If `nrun<ncpu`, it will be + set to `ncpu`. (default: 8) + nper : int, *optional* + this number specifies how many probes will be evaluated by one + worker. Afterwards a new worker will be created to evaluate a chunk + of `nper` probes. + If for example `nper=nrun/ncpu`, then every worker will be created + for every cpu. This can lead to the case, that all workers but one + are already finished, but have to wait for the last worker that + might still have a considerable amount of evaluations left. This is + obviously not very effective. + If on the other hand `nper=1`, then for each evaluation a worker will + be created. In this case all cpus will work until nrun probes have + been evaluated. + It is recommended to leave `nper` as the default value. (default: 1) + var : bool, *optional* + If `var` is True, then the variance of the sampled function will + also be returned. The result is then a tuple with the mean in the + zeroth entry and the variance in the first entry. (default: False) + + """ + from nifty_operators import operator + if(not isinstance(op,operator)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + elif(op.nrow()!=op.ncol()): + raise ValueError(about._errors.cstring("ERROR: trace ill-defined for "+str(op.nrow())+" x "+str(op.ncol())+" matrices.")) + + ## check whether callable + if(function is None)or(not hasattr(function,"__call__")): + function = op.times + elif(op==function): + function = op.times + ## check whether correctly bound + if(op!=function.im_self): + raise NameError(about._errors.cstring("ERROR: invalid input.")) + self.function = function + + ## check given domain + if(domain is None)or(not isinstance(domain,space)): + if(self.function in [op.inverse_times,op.adjoint_times]): + domain = op.target + else: + domain = op.domain + else: + if(not op.domain.check_codomain(domain)): ## restrictive + raise ValueError(about._errors.cstring("ERROR: incompatible domains.")) + if(not op.target.check_codomain(domain)): ## restrictive + raise ValueError(about._errors.cstring("ERROR: incompatible domains.")) + if(target is None)or(not isinstance(target,space)): + if(function in [op.inverse_times,op.adjoint_times]): + target = op.target + else: + target = op.domain + self.domain = domain + + ## check codomain + if(target is None): + target = self.domain.get_codomain() + else: + self.domain.check_codomain(target) ## a bit pointless + self.target = target + + ## check degrees of freedom + if(op.domain.dof()>self.domain.dof()): + about.infos.cprint("INFO: variant numbers of degrees of freedom ( "+str(op.domain.dof())+" / "+str(self.domain.dof())+" ).") + + if(random not in ["pm1","gau"]): + raise ValueError(about._errors.cstring("ERROR: unsupported random key '"+str(random)+"'.")) + self.random = random + + self.ncpu = int(max(1,ncpu)) + self.nrun = int(max(self.ncpu,nrun)) + if(nper is None): + self.nper = None + else: + self.nper = int(max(1,min(self.nrun//self.ncpu,nper))) + + self.var = bool(var) + + self.quargs = quargs + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def probing(self,idnum,probe): + """ + Computes a single probing result given one probe + + Parameters + ---------- + probe : field + the field on which `function` will be applied + idnum : int + the identification number of the probing + + Returns + ------- + result : float + the result of `probe.dot(function(probe))` + """ + f = self.function(probe,**self.quargs) ## field + if(f is None): + return None + else: + if(f.domain!=self.domain): + f.transform(target=self.domain,overwrite=True) + return self.domain.calc_dot(probe.val,f.val) ## discrete inner product + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def evaluate(self,summa,num,var): + """ + Evaluates the probing results. + + Parameters + ---------- + summa : scalar + Sum of all probing results. + num : int + Number of successful probings (not returning ``None``). + var : scalar + Sum of all squared probing results + + Returns + ------- + final : scalar + The final probing result; 1/N Sum[ probing(probe_i) ] + var : scalar + The variance of the final probing result; + 1/(N(N-1)) Sum[( probing(probe_i) - final )^2]; + if the variance is returned, the return will be a tuple of + (`final`,`var`). + + """ + if(num<self.nrun): + about.infos.cflush(" ( %u probe(s) failed, effectiveness == %.1f%% )\n"%(self.nrun-num,100*num/self.nrun)) + if(num==0): + about.warnings.cprint("WARNING: probing failed.") + return None + else: + about.infos.cflush("\n") + + if(issubclass(self.domain.datatype,np.complexfloating)): + summa = np.real(summa) + + final = summa/num + if(self.var): + if(num==1): + about.warnings.cprint("WARNING: infinite variance.") + return final,None + else: + var = var/(num*(num-1))-np.real(np.conjugate(final)*final)/(num-1) + return final,var + else: + return final + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _serial_probing(self,zipped): ## > performs the probing operation serially + try: + result = self._single_probing(zipped) + except Exception as exception: + raise exception + except BaseException: ## capture system-exiting exception including KeyboardInterrupt + raise Exception(about._errors.cstring("ERROR: unknown.")) + else: + if(result is not None): + if(isinstance(_share.sum,tuple)): + _share.sum[0].acquire(block=True,timeout=None) + _share.sum[0].value += np.real(result) + _share.sum[0].release() + _share.sum[1].acquire(block=True,timeout=None) + _share.sum[1].value += np.imag(result) + _share.sum[1].release() + else: + _share.sum.acquire(block=True,timeout=None) + _share.sum.value += result + _share.sum.release() + _share.num.acquire(block=True,timeout=None) + _share.num.value += 1 + _share.num.release() + if(self.var): + _share.var.acquire(block=True,timeout=None) + _share.var.value += np.real(np.conjugate(result)*result) + _share.var.release() + self._progress(_share.num.value) + + def _parallel_probing(self): ## > performs the probing operations in parallel + ## define random seed + seed = np.random.randint(10**8,high=None,size=self.nrun) + ## define shared objects + if(issubclass(self.domain.datatype,np.complexfloating)): + _sum = (mv('d',0,lock=True),mv('d',0,lock=True)) ## tuple(real,imag) + else: + _sum = mv('d',0,lock=True) + _num = mv('I',0,lock=True) + if(self.var): + _var = mv('d',0,lock=True) + else: + _var = None + ## build pool + if(about.infos.status): + so.write(about.infos.cstring("INFO: multiprocessing "+(' ')*10)) + so.flush() + pool = mp(processes=self.ncpu,initializer=_share._init_share,initargs=(_sum,_num,_var),maxtasksperchild=self.nper) + try: + ## pooling + pool.map_async(self._serial_probing,zip(seed,np.arange(self.nrun,dtype=np.int)),chunksize=None,callback=None).get(timeout=None) + ## close and join pool + about.infos.cflush(" done.") + pool.close() + pool.join() + except BaseException as exception: + ## terminate and join pool + about._errors.cprint("\nERROR: terminating pool.") + pool.terminate() + pool.join() + ## re-raise exception + raise exception ## traceback by looping + ## evaluate + if(issubclass(self.domain.datatype,np.complexfloating)): + _sum = np.complex(_sum[0].value,_sum[1].value) + else: + _sum = _sum.value + if(self.var): + _var = _var.value + return self.evaluate(_sum,_num.value,_var) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def __repr__(self): + return "<nifty_core.trace_probing>" + +##----------------------------------------------------------------------------- + + + +##----------------------------------------------------------------------------- + +class diagonal_probing(probing): + """ + .. __ __ __ + .. / / /__/ / / + .. ____/ / __ ____ __ ____ __ ______ __ ___ ____ __ / / + .. / _ / / / / _ / / _ / / _ | / _ | / _ / / / + .. / /_/ / / / / /_/ / / /_/ / / /_/ / / / / / / /_/ / / /_ + .. \______| /__/ \______| \___ / \______/ /__/ /__/ \______| \___/ probing class + .. /______/ + + NIFTY subclass for diagonal probing (using multiprocessing) + + When called, a diagonal_probing class instance samples the diagonal of + an operator or a function using random fields, whose components are + random variables with mean 0 and variance 1. When an instance is called + it returns the mean value of probe*f(probe), where probe is a random + field with mean 0 and variance 1. + The mean is calculated as 1/N Sum[ probe_i*f(probe_i) ] + ('*' denoting component wise multiplication) + + Parameters + ---------- + op : operator + The operator specified by `op` is the operator to be probed. + If no operator is given, then probing will be done by applying + `function` to the probes. (default: None) + function : function, *optional* + If no operator has been specified as `op`, then specification of + `function` is non optional. This is the function, that is applied + to the probes. (default: `op.times`) + domain : space, *optional* + If no operator has been specified as `op`, then specification of + `domain` is non optional. This is the space that the probes live + in. (default: `op.domain`) + target : domain, *optional* + `target` is the codomain of `domain` + (default: `op.domain.get_codomain()`) + random : string, *optional* + the distribution from which the probes are drawn. `random` can be + either "pm1" or "gau". "pm1" is a uniform distribution over {+1,-1} + or {+1,+i,-1,-i}, respectively. "gau" is a normal distribution with + zero-mean and unit-variance (default: "pm1") + ncpu : int, *optional* + the number of cpus to be used for parallel probing. (default: 2) + nrun : int, *optional* + the number of probes to be evaluated. If `nrun<ncpu`, it will be + set to `ncpu`. (default: 8) + nper : int, *optional* + this number specifies how many probes will be evaluated by one + worker. Afterwards a new worker will be created to evaluate a chunk + of `nper` probes. + If for example `nper=nrun/ncpu`, then every worker will be created + for every cpu. This can lead to the case, that all workers but one + are already finished, but have to wait for the last worker that + might still have a considerable amount of evaluations left. This is + obviously not very effective. + If on the other hand `nper=1`, then for each evaluation a worker will + be created. In this case all cpus will work until nrun probes have + been evaluated. + It is recommended to leave `nper` as the default value. (default: 1) + var : bool, *optional* + If `var` is True, then the variance of the sampled function will + also be returned. The result is then a tuple with the mean in the + zeroth entry and the variance in the first entry. (default: False) + save : bool, *optional* + If `save` is True, then the probing results will be written to the + hard disk instead of being saved in the RAM. This is recommended + for high dimensional fields whose probes would otherwise fill up + the memory. (default: False) + path : string, *optional* + the path, where the probing results are saved, if `save` is True. + (default: "tmp") + prefix : string, *optional* + a prefix for the saved probing results. The saved results will be + named using that prefix and an 8-digit number + (e.g. "<prefix>00000001.npy"). (default: "") + + + See Also + -------- + trace_probing : A probing class to get the trace of an operator + probing : The base probing class + operator.diag : The diag function uses diagonal probing in the case of + non diagonal operators + operator.hat : The hat function uses diagonal probing in the case of + non diagonal operators + + + Attributes + ---------- + function : function + the function, that is applied to the probes + domain : space + the space, where the probes live in + target : space + the codomain of `domain` + random : string + the random number generator used to create the probes + (either "pm1" or "gau") + ncpu : int + the number of cpus used for probing + nrun : int + the number of probes to be evaluated, when the instance is called + nper : int + number of probes, that will be evaluated by one worker + var : bool + whether the variance will be additionally returned, when the + instance is called + save : {string, None} + the path and prefix for saved probe files. None in the case where + the probing results are stored in the RAM. + quargs : dict + Keyword arguments passed to `function` in each call. + + """ + def __init__(self,op,function=None,domain=None,target=None,random="pm1",ncpu=2,nrun=8,nper=1,var=False,save=False,path="tmp",prefix="",**quargs): + """ + initializes a diagonal probing instance + + Parameters + ---------- + op : operator + The operator specified by `op` is the operator to be probed. + If no operator is given, then probing will be done by applying + `function` to the probes. (default: None) + function : function, *optional* + If no operator has been specified as `op`, then specification of + `function` is non optional. This is the function, that is applied + to the probes. (default: `op.times`) + domain : space, *optional* + If no operator has been specified as `op`, then specification of + `domain` is non optional. This is the space that the probes live + in. (default: `op.domain`) + target : domain, *optional* + `target` is the codomain of `domain` + (default: `op.domain.get_codomain()`) + random : string, *optional* + the distribution from which the probes are drawn. `random` can be + either "pm1" or "gau". "pm1" is a uniform distribution over {+1,-1} + or {+1,+i,-1,-i}, respectively. "gau" is a normal distribution with + zero-mean and unit-variance (default: "pm1") + ncpu : int, *optional* + the number of cpus to be used for parallel probing. (default: 2) + nrun : int, *optional* + the number of probes to be evaluated. If `nrun<ncpu`, it will be + set to `ncpu`. (default: 8) + nper : int, *optional* + this number specifies how many probes will be evaluated by one + worker. Afterwards a new worker will be created to evaluate a chunk + of `nper` probes. + If for example `nper=nrun/ncpu`, then every worker will be created + for every cpu. This can lead to the case, that all workers but one + are already finished, but have to wait for the last worker that + might still have a considerable amount of evaluations left. This is + obviously not very effective. + If on the other hand `nper=1`, then for each evaluation a worker will + be created. In this case all cpus will work until nrun probes have + been evaluated. + It is recommended to leave `nper` as the default value. (default: 8) + var : bool, *optional* + If `var` is True, then the variance of the sampled function will + also be returned. The result is then a tuple with the mean in the + zeroth entry and the variance in the first entry. (default: False) + save : bool, *optional* + If `save` is True, then the probing results will be written to the + hard disk instead of being saved in the RAM. This is recommended + for high dimensional fields whose probes would otherwise fill up + the memory. (default: False) + path : string, *optional* + the path, where the probing results are saved, if `save` is True. + (default: "tmp") + prefix : string, *optional* + a prefix for the saved probing results. The saved results will be + named using that prefix and an 8-digit number + (e.g. "<prefix>00000001.npy"). (default: "") + + """ + from nifty_operators import operator + if(not isinstance(op,operator)): + raise TypeError(about._errors.cstring("ERROR: invalid input.")) + elif(op.nrow()!=op.ncol()): + raise ValueError(about._errors.cstring("ERROR: diagonal ill-defined for "+str(op.nrow())+" x "+str(op.ncol())+" matrices.")) + + ## check whether callable + if(function is None)or(not hasattr(function,"__call__")): + function = op.times + elif(op==function): + function = op.times + ## check whether correctly bound + if(op!=function.im_self): + raise NameError(about._errors.cstring("ERROR: invalid input.")) + self.function = function + + ## check given domain + if(domain is None)or(not isinstance(domain,space)): + if(self.function in [op.inverse_times,op.adjoint_times]): + domain = op.target + else: + domain = op.domain + else: + if(not op.domain.check_codomain(domain)): ## restrictive + raise ValueError(about._errors.cstring("ERROR: incompatible domains.")) + if(not op.target.check_codomain(domain)): ## restrictive + raise ValueError(about._errors.cstring("ERROR: incompatible domains.")) + if(target is None)or(not isinstance(target,space)): + if(function in [op.inverse_times,op.adjoint_times]): + target = op.target + else: + target = op.domain + self.domain = domain + + ## check codomain + if(target is None): + target = self.domain.get_codomain() + else: + self.domain.check_codomain(target) ## a bit pointless + self.target = target + + ## check degrees of freedom + if(self.domain.dof()>op.domain.dof()): + about.infos.cprint("INFO: variant numbers of degrees of freedom ( "+str(self.domain.dof())+" / "+str(op.domain.dof())+" ).") + + if(random not in ["pm1","gau"]): + raise ValueError(about._errors.cstring("ERROR: unsupported random key '"+str(random)+"'.")) + self.random = random + + self.ncpu = int(max(1,ncpu)) + self.nrun = int(max(self.ncpu,nrun)) + if(nper is None): + self.nper = None + else: + self.nper = int(max(1,min(self.nrun//self.ncpu,nper))) + + self.var = bool(var) + + if(save): + path = os.path.expanduser(str(path)) + if(not os.path.exists(path)): + os.makedirs(path) + self.save = os.path.join(path,str(prefix)) ## (back)slash inserted if needed + else: + self.save = None + + self.quargs = quargs + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def configure(self,**kwargs): + """ + changes the attributes of the instance + + Parameters + ---------- + random : string, *optional* + the random number generator used to create the probes + (default: "pm1") + ncpu : int, *optional* + the number of cpus to be used for parallel probing + (default: 2) + nrun : int, *optional* + the number of probes to be evaluated. If `nrun<ncpu`, it will + be set to `ncpu`. (default: 8) + nper : int, *optional* + number of probes, that will be evaluated by one worker + (default: 1) + var : bool, *optional* + whether the variance will be additionally returned + (default: False) + save : bool, *optional* + whether the individual probing results will be saved to the HDD + (default: False) + path : string, *optional* + the path, where the probing results are saved (default: "tmp") + prefix : string, *optional* + a prefix for the saved probing results (default: "") + + """ + if("random" in kwargs): + if(kwargs.get("random") not in ["pm1","gau"]): + raise ValueError(about._errors.cstring("ERROR: unsupported random key '"+str(kwargs.get("random"))+"'.")) + self.random = kwargs.get("random") + + if("ncpu" in kwargs): + self.ncpu = int(max(1,kwargs.get("ncpu"))) + if("nrun" in kwargs): + self.nrun = int(max(self.ncpu,kwargs.get("nrun"))) + if("nper" in kwargs): + if(kwargs.get("nper") is None): + self.nper = None + else: + self.nper = int(max(1,min(self.nrun//self.ncpu,kwargs.get("nper")))) + + if("var" in kwargs): + self.var = bool(kwargs.get("var")) + + if("save" in kwargs): + if(kwargs.get("save")): + if("path" in kwargs): + path = kwargs.get("path") + else: + if(self.save is not None): + about.warnings.cprint("WARNING: save path set to default.") + path = "tmp" + if("prefix" in kwargs): + prefix = kwargs.get("prefix") + else: + if(self.save is not None): + about.warnings.cprint("WARNING: save prefix set to default.") + prefix = "" + path = os.path.expanduser(str(path)) + if(not os.path.exists(path)): + os.makedirs(path) + self.save = os.path.join(path,str(prefix)) ## (back)slash inserted if needed + else: + self.save = None + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def probing(self,idnum,probe): + + """ + Computes a single probing result given one probe + + Parameters + ---------- + probe : field + the field on which `function` will be applied + idnum : int + the identification number of the probing + + Returns + ------- + result : ndarray + the result of `probe*(function(probe))` + """ + f = self.function(probe,**self.quargs) ## field + if(f is None): + return None + else: + if(f.domain!=self.domain): + f.transform(target=self.domain,overwrite=True) + result = np.conjugate(probe.val)*f.val + if(self.save is not None): + np.save(self.save+"%08u"%idnum,result) + return result + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def _serial_probing(self,zipped): ## > performs the probing operation serially + try: + result = self._single_probing(zipped) + except Exception as exception: + raise exception + except BaseException: ## capture system-exiting exception including KeyboardInterrupt + raise Exception(about._errors.cstring("ERROR: unknown.")) + else: + if(result is not None): + result = np.array(result).flatten(order='C') + if(isinstance(_share.sum,tuple)): + _share.sum[0].acquire(block=True,timeout=None) + _share.sum[0][:] += np.real(result) + _share.sum[0].release() + _share.sum[1].acquire(block=True,timeout=None) + _share.sum[1][:] += np.imag(result) + _share.sum[1].release() + else: + _share.sum.acquire(block=True,timeout=None) + _share.sum[:] += result + _share.sum.release() + _share.num.acquire(block=True,timeout=None) + _share.num.value += 1 + _share.num.release() + if(self.var): + _share.var.acquire(block=True,timeout=None) + _share.var[:] += np.real(np.conjugate(result)*result) + _share.var.release() + self._progress(_share.num.value) + + def _parallel_probing(self): ## > performs the probing operations in parallel + ## define random seed + seed = np.random.randint(10**8,high=None,size=self.nrun) + ## define shared objects + if(issubclass(self.domain.datatype,np.complexfloating)): + _sum = (ma('d',np.zeros(self.domain.dim(split=False),dtype=np.float64,order='C'),lock=True),ma('d',np.zeros(self.domain.dim(split=False),dtype=np.float64,order='C'),lock=True)) ## tuple(real,imag) + else: + _sum = ma('d',np.zeros(self.domain.dim(split=False),dtype=np.float64,order='C'),lock=True) + _num = mv('I',0,lock=True) + if(self.var): + _var = ma('d',np.zeros(self.domain.dim(split=False),dtype=np.float64,order='C'),lock=True) + else: + _var = None + ## build pool + if(about.infos.status): + so.write(about.infos.cstring("INFO: multiprocessing "+(' ')*10)) + so.flush() + pool = mp(processes=self.ncpu,initializer=_share._init_share,initargs=(_sum,_num,_var),maxtasksperchild=self.nper) + try: + ## pooling + pool.map_async(self._serial_probing,zip(seed,np.arange(self.nrun,dtype=np.int)),chunksize=None,callback=None).get(timeout=None) + ## close and join pool + about.infos.cflush(" done.") + pool.close() + pool.join() + except BaseException as exception: + ## terminate and join pool + about._errors.cprint("\nERROR: terminating pool.") + pool.terminate() + pool.join() + ## re-raise exception + raise exception ## traceback by looping + ## evaluate + if(issubclass(self.domain.datatype,np.complexfloating)): + _sum = (np.array(_sum[0][:])+np.array(_sum[1][:])*1j).reshape(self.domain.dim(split=True)) ## comlpex array + else: + _sum = np.array(_sum[:]).reshape(self.domain.dim(split=True)) + if(self.var): + _var = np.array(_var[:]).reshape(self.domain.dim(split=True)) + return self.evaluate(_sum,_num.value,_var) + + ##+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + def __repr__(self): + return "<nifty_core.diagonal_probing>" + +##----------------------------------------------------------------------------- + +## IDEA: diagonal_inference