@@ -977,6 +977,15 @@ class NmtFieldCatalogClustering(NmtField):
977977 of discrete sources, with a survey footprint characterised by a set of
978978 random points or through a standard mask.
979979
980+ .. note::
981+ The ordering of arguments for this class will change in the next
982+ major version of NaMaster.
983+
984+ .. note::
985+ Currently only HEALPix format is accepted for the mask and
986+ templates passed to this field, but we hope to implement CAR
987+ pixelisation in the near future.
988+
980989 Args:
981990 positions (`array`): Source positions, provided as a list or array
982991 of 2 arrays. If ``lonlat`` is True, the arrays should contain the
@@ -991,12 +1000,55 @@ class NmtFieldCatalogClustering(NmtField):
9911000 weights_rand (`array`): As ``weights`` for the random catalog.
9921001 lmax (:obj:`int`): Maximum multipole up to which the spherical
9931002 harmonics of this field will be computed.
1003+ mask (`array`): Array containing a map corresponding to the
1004+ field's mask. Should be 1-dimensional for a HEALPix map or
1005+ 2-dimensional for a map with rectangular (CAR) pixelization.
1006+ If not ``None``, the random catalogue (``positions_rand`` and
1007+ ``weights_rand``) will be ignored. Note that, if deprojection
1008+ templates are provided but no mask is passed, a mask will be
1009+ constructed from the random catalogue for deprojection.
9941010 lonlat (:obj:`bool`): If ``True``, longitude and latitude in degrees
9951011 are provided as input. If ``False``, colatitude and longitude in
9961012 radians are provided instead.
1013+ templates (`array`): Array containing a set of contaminant templates
1014+ for this field. This array should have shape ``[ntemp,nmap,...]``,
1015+ where ``ntemp`` is the number of templates, ``nmap`` should be 1
1016+ for spin-0 fields and 2 otherwise. The other dimensions should be
1017+ ``[npix]`` for HEALPix maps or ``[ny,nx]`` for maps with
1018+ rectangular pixels. The best-fit contribution from each
1019+ contaminant is automatically removed from the maps unless
1020+ ``templates=None``.
1021+ masked_on_input (:obj:`bool`): Set to ``True`` if the input templates
1022+ are already multiplied by the mask.
1023+ n_iter (:obj:`int`): Number of iterations when computing the
1024+ :math:`a_{\\ ell m}` s of the input maps. See the documentation of
1025+ :meth:`~pymaster.utils.map2alm`. If ``None``, it will default to
1026+ the internal value (see documentation of
1027+ :class:`~pymaster.utils.NmtParams`), which can be accessed via
1028+ :meth:`~pymaster.utils.get_default_params`, and modified via
1029+ :meth:`~pymaster.utils.set_n_iter_default`.
1030+ n_iter_mask (:obj:`int`): Number of iterations when computing the
1031+ spherical harmonic transform of the mask. If ``None``, it will
1032+ default to the internal value (see documentation of
1033+ :class:`~pymaster.utils.NmtParams`), which can be accessed via
1034+ :meth:`~pymaster.utils.get_default_params`,
1035+ and modified via :meth:`~pymaster.utils.set_n_iter_default`.
1036+ wcs (`WCS`): A WCS object if using rectangular (CAR) pixels (see
1037+ `the astropy documentation
1038+ <http://docs.astropy.org/en/stable/wcs/index.html>`_).
1039+ tol_pinv (:obj:`float`): When computing the pseudo-inverse of the
1040+ contaminant covariance matrix. See documentation of
1041+ :meth:`~pymaster.utils.moore_penrose_pinvh`. Only relevant if
1042+ passing contaminant templates that are likely to be highly
1043+ correlated. If ``None``, it will default to the internal value,
1044+ which can be accessed via
1045+ :meth:`~pymaster.utils.get_default_params`, and modified via
1046+ :meth:`~pymaster.utils.set_tol_pinv_default`.
9971047 """
9981048 def __init__ (self , positions , weights , positions_rand , weights_rand ,
999- lmax , lonlat = False ):
1049+ lmax , lonlat = False , mask = None , templates = None ,
1050+ masked_on_input = False , n_iter = None , n_iter_mask = None ,
1051+ wcs = None , tol_pinv = None ):
10001052 # Preliminary initializations
10011053 if ut .HAVE_DUCC :
10021054 self .sht_calculator = 'ducc'
@@ -1005,10 +1057,10 @@ def __init__(self, positions, weights, positions_rand, weights_rand,
10051057
10061058 # These first attributes are compulsory for all fields
10071059 self .lite = True
1008- self .mask = None
1060+ self .mask = mask
10091061 self .beam = np .ones (lmax + 1 )
1010- self .n_iter = None
1011- self .n_iter_mask = None
1062+ self .n_iter = n_iter
1063+ self .n_iter_mask = n_iter_mask
10121064 self .pure_e = False
10131065 self .pure_b = False
10141066 self .alm = None
@@ -1020,10 +1072,11 @@ def __init__(self, positions, weights, positions_rand, weights_rand,
10201072 self .spin = 0
10211073 self .is_catalog = True
10221074
1023- # The remaining attributes are only required for non-lite maps
1024- self .maps = None
1075+ # These attributes only required if templates provided for deprojection
10251076 self .temp = None
10261077 self .alm_temp = None
1078+ # The remaining attributes are only required for non-lite maps
1079+ self .maps = None
10271080 self .minfo = None
10281081 self .n_temp = 0
10291082 self .anisotropic_mask = False
@@ -1067,22 +1120,52 @@ def process_pos_w(pos, w, kind):
10671120 return pos , w
10681121
10691122 positions , weights = process_pos_w (positions , weights , "data" )
1070- positions_rand , weights_rand = process_pos_w (positions_rand ,
1071- weights_rand , "random" )
1072-
1073- # Compute alpha
1074- self ._alpha = np .sum (weights )/ np .sum (weights_rand )
10751123
1076- # Compute mask shot noise
1077- self ._Nw = self ._alpha ** 2 * np .sum (weights_rand ** 2. )/ (4. * np .pi )
1078-
1079- # Compute mask alms
1080- # Sanity checks
1124+ # Define alm info for mask using same lmax as field
10811125 self .ainfo_mask = ut .NmtAlmInfo (lmax )
1082- # Mask alms
1083- self .alm_mask = ut ._catalog2alm_ducc0 (weights_rand * self ._alpha ,
1084- positions_rand ,
1085- spin = 0 , lmax = lmax )
1126+
1127+ # Check if mask provided, use randoms if not
1128+ if mask is None :
1129+ positions_rand , weights_rand = process_pos_w (positions_rand ,
1130+ weights_rand ,
1131+ "random" )
1132+ # Compute alpha
1133+ self ._alpha = np .sum (weights )/ np .sum (weights_rand )
1134+
1135+ # Compute mask shot noise
1136+ self ._Nw = self ._alpha ** 2 * np .sum (weights_rand ** 2. )/ (4. * np .pi )
1137+
1138+ # Compute mask alms
1139+ self .alm_mask = ut ._catalog2alm_ducc0 (weights_rand * self ._alpha ,
1140+ positions_rand ,
1141+ spin = 0 , lmax = lmax )
1142+ # If mask provided, ignore/replace randoms-related quantities
1143+ else :
1144+ # Get the number of pixels in the mask and convert to NSIDE
1145+ # NOTE: assumes HealPIX format for now
1146+ npix = len (mask )
1147+ nside = hp .npix2nside (npix )
1148+ # Pixel area in steradians
1149+ Apix = 4. * np .pi / npix
1150+
1151+ # Initialisation of parameters related to the mask
1152+ if n_iter_mask is None :
1153+ n_iter_mask = ut .nmt_params .n_iter_default
1154+ # No randoms, so set alpha to 1
1155+ self ._alpha = 1.
1156+ # No shot noise for map-based masks
1157+ self ._Nw = 0.
1158+
1159+ # Use mask to estimate expected mean number density in each pixel
1160+ nbar = mask * np .sum (weights ) / (Apix * np .sum (mask ))
1161+
1162+ # Get spatial info from the mask
1163+ self .minfo = ut .NmtMapInfo (wcs , mask .shape )
1164+ self .mask = self .minfo .reform_map (mask )
1165+ # Compute mask alms
1166+ self .alm_mask = ut .map2alm (np .array ([nbar ]), 0 ,
1167+ self .minfo , self .ainfo_mask ,
1168+ n_iter = n_iter_mask )
10861169
10871170 # Compute field alms
10881171 self .alm = ut ._catalog2alm_ducc0 (weights , positions ,
@@ -1091,3 +1174,90 @@ def process_pos_w(pos, w, kind):
10911174
10921175 # Compute field shot noise
10931176 self ._Nf = np .sum (weights ** 2 )/ (4 * np .pi )+ self ._Nw
1177+
1178+ # Systematics mitigation with mode deprojection
1179+ if templates is not None :
1180+ # Initialisation of parameters related to deprojection
1181+ if n_iter is None :
1182+ n_iter = ut .nmt_params .n_iter_default
1183+ if tol_pinv is None :
1184+ tol_pinv = ut .nmt_params .tol_pinv_default
1185+
1186+ # Check format of templates
1187+ if isinstance (templates , (list , tuple , np .ndarray )):
1188+ templates = np .array (templates , dtype = np .float64 )
1189+ if (len (templates [0 ]) != 1 ):
1190+ raise ValueError ("Templates must have length 1 "
1191+ "along axis 1." )
1192+ else :
1193+ raise ValueError ("Input templates can only be an array "
1194+ "or None" )
1195+ self .n_temp = len (templates )
1196+
1197+ # Method of deprojection depends on whether mask provided
1198+ if mask is None :
1199+ # Get spatial info from first template
1200+ self .minfo = ut .NmtMapInfo (wcs , templates .shape [2 :])
1201+ templates = self .minfo .reform_map (templates )
1202+ # Get the number of pixels per template and convert to NSIDE
1203+ npix = len (templates [0 ][0 ])
1204+ nside = hp .npix2nside (npix )
1205+ # Identify the pixels to which each random belongs
1206+ ipix_rand = hp .ang2pix (nside , * positions_rand )
1207+
1208+ # Apply mask to templates if not done already
1209+ if not masked_on_input :
1210+ # Generate a mask from the positions of the randoms
1211+ mask = np .bincount (ipix_rand ,
1212+ minlength = npix ).astype (np .float64 )
1213+ # Normalise to range [0,1]
1214+ mask /= mask .max ()
1215+ # Multiply the templates by the mask
1216+ templates *= mask [None , :]
1217+
1218+ # Get the template values at each random's position
1219+ temp_at_rand = templates [:, :, ipix_rand ]
1220+ # Multiply by the weights assigned to each random
1221+ temp_at_rand *= weights_rand [None , None , :]
1222+ # Sum across all randoms
1223+ S_rand = temp_at_rand .sum (axis = 2 ) * self ._alpha
1224+ else :
1225+ templates = self .minfo .reform_map (templates )
1226+ # Apply mask to templates if not done already
1227+ if not masked_on_input :
1228+ templates *= mask [None , :]
1229+ S_rand = np .sum (Apix * templates * nbar [None , :], axis = 2 )
1230+
1231+ self .n_temp = len (templates )
1232+
1233+ # Identify the pixels to which each source belongs
1234+ ipix_data = hp .ang2pix (nside , * positions )
1235+ # Get the template values at each source's position
1236+ temp_at_data = templates [:, :, ipix_data ]
1237+ # Multiply by the weights assigned to each source
1238+ temp_at_data *= weights [None , None , :]
1239+ # Sum across all sources
1240+ S_data = temp_at_data .sum (axis = 2 )
1241+
1242+ # Weighted difference between the two sums
1243+ dS = S_data - S_rand
1244+
1245+ # Compute alms of each template
1246+ alm_temp = np .array ([ut .map2alm (t , self .spin , self .minfo ,
1247+ self .ainfo , n_iter = n_iter )
1248+ for t in templates ])
1249+
1250+ # Compute template normalisation matrix
1251+ M = np .array ([[self .minfo .si .dot_map (t1 , t2 )
1252+ for t1 in templates ]
1253+ for t2 in templates ])
1254+ iM = ut .moore_penrose_pinvh (M , tol_pinv )
1255+
1256+ # Compute the alms of the deprojected component
1257+ alm_deproj = np .zeros (self .alm .shape , dtype = np .complex128 )
1258+ for i in range (self .n_temp ):
1259+ for j in range (self .n_temp ):
1260+ alm_deproj += alm_temp [i ] * iM [i ][j ] * dS [j ]
1261+
1262+ # Subtract from the alms of the field
1263+ self .alm -= alm_deproj
0 commit comments