def _load_inverse_variance_map(self, tube, output_units="uK_CMB", band=None): """ Internal function to return a preloaded inverse var map or load one from a from file. By default this just returns None so an inv_var map is computed from a white noise level and a hits map """ survey = self.get_survey(tube) noise_indices = self.get_noise_indices(tube, band) # If the survey has a set of preloaded invariance maps use them if hasattr(survey, "get_ivar_maps") and survey.get_ivar_maps() is not None: ret = np.array(survey.get_ivar_maps())[noise_indices] elif (hasattr(survey, "get_ivar_map_filenames") and survey.get_ivar_map_filenames() is not None): ivar_map_filenames = survey.get_ivar_map_filenames() if ivar_map_filenames is None: return None ivar_map_filenames = [ivar_map_filenames[i] for i in noise_indices] ivar_maps = [] for ivar_map_filename in ivar_map_filenames: ivar_maps.append(self._load_map(ivar_map_filename)) ret = np.array(ivar_maps) else: return None for i in range(self.channel_per_tube): freq = self.tubes[tube][i].center_frequency unit_conv = (1 * u.uK_CMB).to_value( u.Unit(output_units), equivalencies=u.cmb_equivalencies(freq)) ret[i] /= unit_conv**2.0 # divide by square since the default is 1/uK^2 return ret
def get_emission( self, freqs: u.GHz, fwhm: [u.arcmin, None] = None, weights=None, output_units=u.uK_RJ, ): """Return map in uK_RJ at given frequency or array of frequencies Parameters ---------- freqs : list or ndarray Frequency or frequencies in GHz at which compute the signal fwhm : float (optional) Smooth the input alms before computing the signal, this can only be used if the class was initialized with `precompute_output_map` to False. output_units : str Output units, as defined in `pysm.convert_units`, by default this is "uK_RJ" as expected by PySM. Returns ------- output_maps : ndarray Output maps array with the shape (num_freqs, 1 or 3 (I or IQU), npix) """ freqs = pysm.utils.check_freq_input(freqs) weights = pysm.utils.normalize_weights(freqs, weights) try: output_map = self.output_map except AttributeError: if fwhm is None: alm = self.alm else: alm = hp.smoothalm(self.alm, fwhm=fwhm.to_value(u.radian), pol=True, inplace=False) output_map = self.compute_output_map(alm) output_units = u.Unit(output_units) assert output_units in [u.uK_RJ, u.uK_CMB] if output_units == u.uK_RJ: convert_to_uK_RJ = (np.ones(len(freqs), dtype=np.double) * u.uK_CMB).to_value( u.uK_RJ, equivalencies=u.cmb_equivalencies(freqs * u.GHz)) if len(freqs) == 1: scaling_factor = convert_to_uK_RJ[0] else: scaling_factor = np.trapz(convert_to_uK_RJ * weights, x=freqs) return output_map.value * scaling_factor << u.uK_RJ elif output_units == output_map.unit: return output_map
def __init__( self, simulation, parameters: Union[Dict[str, Any], str, MbsParameters] = MbsParameters(), instrument=None, detector_list=None, channel_list=None, ): self.sim = simulation self.imo = self.sim.imo if isinstance(parameters, MbsParameters): self.params = parameters elif isinstance(parameters, str): self.params = MbsParameters.from_dict( simulation.parameters[parameters]) else: self.params = MbsParameters.from_dict(parameters) self.instrument = instrument self.det_list = detector_list self.ch_list = channel_list self.pysm_units = u.Unit(self.params.units)
def simulate( self, tube, output_units="uK_CMB", seed=None, nsplits=1, mask_value=None, atmosphere=True, hitmap=None, white_noise_rms=None, ): """Create a random realization of the noise power spectrum Parameters ---------- tube : str Specify a tube (for SO: ST0-ST3, LT0-LT6) see the `tubes` attribute output_units : str Output unit supported by PySM.units, e.g. uK_CMB or K_RJ seed : integer or tuple of integers, optional Specify a seed. The seed is converted to a tuple if not already one and appended to (0,0,6,tube_id) to avoid collisions between tubes, with the signal sims and with ACT noise sims, where tube_id is the integer ID of the tube. nsplits : integer, optional Number of splits to generate. The splits will have independent noise realizations, with noise power scaled by a factor of nsplits, i.e. atmospheric noise is assumed to average down with observing time the same way the white noise does. By default, only one split (the coadd) is generated. mask_value : float, optional The value to set in masked (unobserved) regions. By default, it uses the value in default_mask_value, which for healpix is healpy.UNSEEN and for CAR is numpy.nan. atmosphere : bool, optional Whether to include the correlated 1/f from the noise model. This is True by default. If it is set to False, then a pure white noise map is generated from the white noise power in the noise model, and the covariance between arrays is ignored. hitmap : string or map, optional Provide the path to a hitmap to override the default used for the tube. You could also provide the hitmap as an array directly. white_noise_rms : float or tuple of floats, optional Optionally scale the simulation so that the small-scale limit white noise level is white_noise_rms in uK-arcmin (either a single number or a pair for the dichroic array). Returns ------- output_map : ndarray or ndmap Numpy array with the HEALPix or CAR map realization of noise. The shape of the returned array is (2,3,nsplits,)+oshape, where oshape is (npix,) for HEALPix and (Ny,Nx) for CAR. The first dimension of size 2 corresponds to the two different bands within a dichroic tube. See the `band_id` attribute of the Channel class to identify which is the index of a Channel in the array. The second dimension corresponds to independent split realizations of the noise, e.g. it is 1 for full mission. The third dimension corresponds to the three polarization Stokes components I,Q,U The last dimension is the number of pixels """ assert nsplits >= 1 if mask_value is None: mask_value = (default_mask_value["healpix"] if self.healpix else default_mask_value["car"]) # This seed tuple prevents collisions with the signal sims # but we should eventually switch to centralized seed # tracking. if seed is not None: try: iter(seed) except: seed = (seed, ) tube_id = self.tubes[tube][0].tube_id seed = (0, 0, 6, tube_id) + seed np.random.seed(seed) # In the third row we return the correlation coefficient P12/sqrt(P11*P22) # since that can be used straightforwardly when the auto-correlations are re-scaled. ell, ps_T, ps_P, fsky, wnoise_power, weightsMap = self.get_noise_properties( tube, nsplits=nsplits, hitmap=hitmap, white_noise_rms=white_noise_rms, atmosphere=atmosphere, ) if not (atmosphere): if self.apply_beam_correction: raise NotImplementedError( "Beam correction is not currently implemented for pure-white-noise sims." ) # If no atmosphere is requested, we use a simpler/faster method # that generates white noise in real-space. if self.healpix: ashape = (hp.nside2npix(self.nside), ) sel = np.s_[:, None, None, None] pmap = self.pixarea_map else: ashape = self.shape[-2:] sel = np.s_[:, None, None, None, None] pmap = pixell.enmap.enmap(self.pixarea_map, self.wcs) spowr = np.sqrt(wnoise_power[sel] / pmap) output_map = spowr * np.random.standard_normal( (self.channel_per_tube, nsplits, 3) + ashape) output_map[:, :, 1:, :] = output_map[:, :, 1:, :] * np.sqrt(2.0) else: if self.healpix: npix = hp.nside2npix(self.nside) output_map = np.zeros( (self.channel_per_tube, nsplits, 3, npix)) for i in range(nsplits): for i_pol in range(3): output_map[:, i, i_pol] = np.array( hp.synfast( ps_T if i_pol == 0 else ps_P, nside=self.nside, pol=False, new=True, verbose=False, )) else: output_map = pixell.enmap.zeros((2, nsplits, 3) + self.shape, self.wcs) ps_T = pixell.powspec.sym_expand(np.asarray(ps_T), scheme="diag") ps_P = pixell.powspec.sym_expand(np.asarray(ps_P), scheme="diag") # TODO: These loops can probably be vectorized for i in range(nsplits): for i_pol in range(3): output_map[:, i, i_pol] = pixell.curvedsky.rand_map( (self.channel_per_tube, ) + self.shape, self.wcs, ps_T if i_pol == 0 else ps_P, spin=0, ) for i in range(self.channel_per_tube): freq = self.tubes[tube][i].center_frequency if not (self.homogeneous): good = weightsMap[i] != 0 # Normalize on the Effective sky fraction, see discussion in: # https://github.com/simonsobs/mapsims/pull/5#discussion_r244939311 output_map[i, :, :, good] /= np.sqrt(weightsMap[i][good][..., None, None]) output_map[i, :, :, np.logical_not(good)] = mask_value unit_conv = (1 * u.uK_CMB).to_value( u.Unit(output_units), equivalencies=u.cmb_equivalencies(freq)) output_map[i] *= unit_conv return output_map
def get_inverse_variance(self, tube, output_units="uK_CMB", hitmap=None, white_noise_rms=None): """Get the inverse noise variance in each pixel for the requested tube. In the noise model, all the splits and all the I,Q,U components have the same position dependence of the noise variance. Each split just has `nsplits` times the noise power (or `1/nsplits` the inverse noise variance) and the Q,U components have 2x times the noise power (or 1/2 times the inverse noise variance) of the intensity components. The inverse noise variance provided by this function is for the `nsplits=1` intensity component. Two maps are stored in the leading dimension, one for each of the two correlated arrays in the dichroic tube. Parameters ---------- tube : str Specify a tube (for SO: ST0-ST3, LT0-LT6) see the `tubes` attribute output_units : str Output unit supported by PySM.units, e.g. uK_CMB or K_RJ hitmap : string or map, optional Provide the path to a hitmap to override the default used for the tube. You could also provide the hitmap as an array directly. white_noise_rms : float or tuple of floats, optional Optionally scale the simulation so that the small-scale limit white noise level is white_noise_rms in uK-arcmin (either a single number or a pair for the dichroic array). Returns ------- ivar_map : ndarray or ndmap Numpy array with the HEALPix or CAR map of the inverse variance in each pixel. The default units are uK^(-2). This is an extensive quantity that depends on the size of pixels. See the `band_id` attribute of the Channel class to identify which is the index of a Channel in the array. """ ret = self._load_inverse_variance_map(tube, output_units=output_units) if ret is not None: return ret fsky, hitmaps = self._get_requested_hitmaps(tube, hitmap) wnoise_scale = self._get_wscale_factor(white_noise_rms, tube, fsky) sel = np.s_[:, None] if self.healpix else np.s_[:, None, None] whiteNoise = self.get_white_noise_power(tube, sky_fraction=1, units="sr") if whiteNoise is None: raise AssertionError( " Survey white noise level not specified. Cannot generate ivar_map" ) power = whiteNoise[sel] * fsky[sel] * wnoise_scale[:, 0][sel] """ We now have the physical white noise power uK^2-sr and the hitmap ivar = hitmap * pixel_area * fsky / <hitmap> / power """ avgNhits = np.asarray( [self._average(hitmaps[i]) for i in range(self.channel_per_tube)]) ret = hitmaps * self.pixarea_map * fsky[sel] / avgNhits[sel] / power # Convert to desired units for i in range(self.channel_per_tube): freq = self.tubes[tube][i].center_frequency unit_conv = (1 * u.uK_CMB).to_value( u.Unit(output_units), equivalencies=u.cmb_equivalencies(freq)) ret[i] /= unit_conv**2.0 # divide by square since the default is 1/uK^2 return ret
def __init__( self, target_nside, output_units, has_polarization=True, line="10", include_high_galactic_latitude_clouds=False, polarization_fraction=0.001, theta_high_galactic_latitude_deg=20.0, random_seed=1234567, verbose=False, run_mcmole3d=False, map_dist=None, coord="C", ): """Class defining attributes for CO line emission. CO templates are extracted from Type 1 CO Planck maps. See further details in https://www.aanda.org/articles/aa/abs/2014/11/aa21553-13/aa21553-13.html Parameters ---------- target_nside : int HEALPix NSIDE of the output maps output_units : str unit string as defined by `pysm.convert_units`, e.g. uK_RJ, K_CMB has_polarization : bool whether or not to simulate also polarization maps line : string CO rotational transitions. Accepted values : 10, 21, 32 polarization_fraction: float polarisation fraction for polarised CO emission. include_high_galactic_latitude_clouds: bool If True it includes a simulation from MCMole3D to include high Galactic Latitude clouds. (See more details at http://giuspugl.github.io/mcmole/index.html) run_mcmole3d: bool If True it simulates HGL cluds by running MCMole3D, otherwise it coadds a map of HGL emission. random_seed: int set random seed for mcmole3d simulations. theta_high_galactic_latitude_deg : float Angle in degree to identify High Galactic Latitude clouds (i.e. clouds whose latitude b is `|b|> theta_high_galactic_latitude_deg`). map_dist : mpi4py communicator Read inputs across a MPI communicator, see pysm.read_map """ self.line = line self.line_index = {"10": 0, "21": 1, "32": 2}[line] self.line_frequency = { "10": 115.271 * u.GHz, "21": 230.538 * u.GHz, "32": 345.796 * u.GHz, }[line] self.target_nside = target_nside self.template_nside = 512 if self.target_nside <= 512 else 2048 super().__init__(nside=target_nside, map_dist=map_dist) self.remote_data = RemoteData(coord) self.planck_templatemap_filename = "co/HFI_CompMap_CO-Type1_{}_R2.00_ring.fits".format( self.template_nside ) self.planck_templatemap = self.read_map( self.remote_data.get(self.planck_templatemap_filename), field=self.line_index, unit=u.K_CMB, ) self.include_high_galactic_latitude_clouds = ( include_high_galactic_latitude_clouds ) self.has_polarization = has_polarization self.polarization_fraction = polarization_fraction self.theta_high_galactic_latitude_deg = theta_high_galactic_latitude_deg self.random_seed = random_seed self.run_mcmole3d = run_mcmole3d self.output_units = u.Unit(output_units) self.verbose = verbose
def test_convert_units(self): a1 = 1 * u.uK_CMB.to(u.uK_RJ, equivalencies=u.cmb_equivalencies(300.0 * u.GHz)) a2 = 1 * u.uK_RJ.to(u.uK_CMB, equivalencies=u.cmb_equivalencies(300.0 * u.GHz)) self.assertAlmostEqual(1.0, a1 * a2) a1 = 1 * u.uK_CMB.to(u.Unit("MJy/sr"), equivalencies=u.cmb_equivalencies(300.0 * u.GHz)) a2 = 1 * u.Unit("MJy/sr").to( u.uK_CMB, equivalencies=u.cmb_equivalencies(300.0 * u.GHz)) self.assertAlmostEqual(1.0, a1 * a2) """Validation against ECRSC tables. https://irsasupport.ipac.caltech.edu/index.php?/Knowledgebase/ Article/View/181/20/what-are-the-intensity-units-of-the-planck -all-sky-maps-and-how-do-i-convert-between-them These tables are based on the following tables: h = 6.626176e-26 erg*s k = 1.380662e-16 erg/L c = 2.997792458e1- cm/s T_CMB = 2.726 The impact of the incorrect CMB temperature is especially impactful and limits some comparison to only ~2/3 s.f. """ freqs = [30, 143, 857] uK_CMB_2_K_RJ = dict() uK_CMB_2_K_RJ[30] = 9.77074e-7 uK_CMB_2_K_RJ[143] = 6.04833e-7 uK_CMB_2_K_RJ[857] = 6.37740e-11 for freq in freqs: self.assertAlmostEqual( uK_CMB_2_K_RJ[freq], 1 * u.uK_CMB.to( u.K_RJ, equivalencies=u.cmb_equivalencies(freq * u.GHz)), ) K_CMB_2_MJysr = dict() K_CMB_2_MJysr[30] = 27.6515 K_CMB_2_MJysr[143] = 628.272 K_CMB_2_MJysr[857] = 22565.1 for freq in freqs: self.assertAlmostEqual( K_CMB_2_MJysr[freq] / (1 * u.K_RJ.to(u.MJy / u.sr, equivalencies=u.cmb_equivalencies(freq * u.GHz))), 1.0, places=4, ) # Note that the MJysr definition seems to match comparatively poorly. The # definitions of h, k, c in the document linked above are in cgs and differ # from those on wikipedia. This may conflict with the scipy constants I use. uK_CMB_2_MJysr = dict() uK_CMB_2_MJysr[30] = 2.7e-5 uK_CMB_2_MJysr[143] = 0.0003800 uK_CMB_2_MJysr[857] = 1.43907e-6 for freq in freqs: self.assertAlmostEqual( uK_CMB_2_MJysr[freq] / (1 * u.uK_CMB.to(u.MJy / u.sr, equivalencies=u.cmb_equivalencies(freq * u.GHz))), 1.0, places=2, )
def test_read_map_unit_dimensionless(): m = pysm3.read_map("pysm_2/dust_temp.fits", nside=8, field=0) assert u.Unit("") == m.unit
def test_read_map_unit(): m = pysm3.read_map("pysm_2/dust_temp.fits", nside=8, field=0, unit="uK_RJ") assert u.Unit("uK_RJ") == m.unit
def __init__( self, filename, input_units="uK_CMB", input_reference_frequency=None, nside=None, target_shape=None, target_wcs=None, from_cl=False, from_cl_seed=None, precompute_output_map=True, has_polarization=True, map_dist=None, ): """Generic component based on Precomputed Alms Load a set of Alms from a FITS file and generate maps at the requested resolution and frequency assuming the CMB black body spectrum. A single set of Alms is used for all frequencies requested by PySM, consider that PySM expects the output of components to be in uK_RJ. See more details at https://so-pysm-models.readthedocs.io/en/latest/so_pysm_models/models.html Also note that the Alms are clipped to 3*nside-1 to avoid artifacts from high-ell components which cannot be properly represented by a low-nside map. Parameters ---------- filename : string Path to the input Alms in FITS format input_units : string Input unit strings as defined by pysm.convert_units, e.g. K_CMB, uK_RJ, MJysr input_reference_frequency: float If input units are K_RJ or Jysr, the reference frequency nside : int HEALPix NSIDE of the output maps from_cl : bool If True, the input file contains C_ell instead of a_lm, they should provided with the healpy old ordering TT, TE, TB, EE, EB, BB, sorry. from_cl_seed : int Seed set just before synalm to simulate the alms from the C_ell, necessary to set it in order to get the same input map for different runs only used if `from_cl` is True precompute_output_map : bool If True (default), Alms are transformed into a map in the constructor, if False, the object only stores the Alms and generate the map at each call of the signal method, this is useful to generate maps convolved with different beams has_polarization : bool whether or not to simulate also polarization maps Default: True """ self.nside = nside self.shape = target_shape self.wcs = target_wcs self.filename = filename self.input_units = u.Unit(input_units) self.has_polarization = has_polarization if from_cl: np.random.seed(from_cl_seed) cl = hp.read_cl(self.filename) if not self.has_polarization and cl.ndim > 1: cl = cl[0] # using healpy old ordering TT, TE, TB, EE, EB, BB alm = hp.synalm(cl, new=False, verbose=False) else: alm = np.complex128( hp.read_alm(self.filename, hdu=(1, 2, 3) if self.has_polarization else 1)) self.equivalencies = (None if input_reference_frequency is None else u.cmb_equivalencies(input_reference_frequency)) if precompute_output_map: self.output_map = self.compute_output_map(alm) else: self.alm = alm
def execute(self, write_outputs=False): """Run map simulations Execute simulations for all channels and write to disk the maps, unless `write_outputs` is False, then return them. """ if self.run_pysm: sky_config = [] preset_strings = [] if self.pysm_components_string is not None: for model in self.pysm_components_string.split(","): if model.startswith("SO"): sky_config.append(get_so_models(model, self.nside)) else: preset_strings.append(model) if len(preset_strings) > 0: input_reference_frame = "G" assert ( len(sky_config) == 0 ), "Cannot mix PySM and SO models, they are defined in G and C frames" else: input_reference_frame = "C" self.pysm_sky = pysm.Sky( nside=self.nside, preset_strings=preset_strings, component_objects=sky_config, output_unit=u.Unit(self.unit), ) if self.pysm_custom_components is not None: for comp_name, comp in self.pysm_custom_components.items(): self.pysm_sky.components.append(comp) if not write_outputs: output = {} # ch can be single channel or tuple of 2 channels (tube dichroic) for ch in self.channels: if not isinstance(ch, tuple): ch = [ch] output_map_shape = (len(ch), 3) + self.shape output_map = np.zeros(output_map_shape, dtype=np.float64) if self.run_pysm: for each, channel_map in zip(ch, output_map): bandpass_integrated_map = self.pysm_sky.get_emission( *each.bandpass).value beam_width_arcmin = each.beam # smoothing and coordinate rotation with 1 spherical harmonics transform smoothed_map = hp.ma( pysm.apply_smoothing_and_coord_transform( bandpass_integrated_map, fwhm=beam_width_arcmin, lmax=3 * self.nside - 1, rot=None if input_reference_frame == self.pysm_output_reference_frame else hp.Rotator(coord=( input_reference_frame, self.pysm_output_reference_frame, )), map_dist=None if COMM_WORLD is None else pysm.MapDistribution( nside=self.nside, smoothing_lmax=3 * self.nside - 1, mpi_comm=COMM_WORLD, ), )) if smoothed_map.shape[0] == 1: channel_map[0] += smoothed_map else: channel_map += smoothed_map output_map = output_map.reshape((len(ch), 1, 3) + self.shape) if self.other_components is not None: for comp in self.other_components.values(): kwargs = dict(tube=ch[0].tube, output_units=self.unit) if function_accepts_argument(comp.simulate, "ch"): kwargs.pop("tube") kwargs["ch"] = ch if function_accepts_argument(comp.simulate, "nsplits"): kwargs["nsplits"] = self.nsplits if function_accepts_argument(comp.simulate, "seed"): kwargs["seed"] = self.num component_map = comp.simulate(**kwargs) if self.nsplits == 1: component_map = component_map.reshape((len(ch), 1, 3) + self.shape) component_map[hp.mask_bad(component_map)] = np.nan output_map = output_map + component_map for each, channel_map in zip(ch, output_map): if write_outputs: for split, each_split_channel_map in enumerate( channel_map): filename = self.output_filename_template.format( telescope=each.telescope if each.tube is None else each.tube, band=each.band, nside=self.nside, tag=self.tag, num=self.num, nsplits=self.nsplits, split=split + 1, ) warnings.warn("Writing output map " + filename) if self.car: pixell.enmap.write_map( os.path.join(self.output_folder, filename), each_split_channel_map, extra=dict(units=self.unit), ) else: each_split_channel_map[np.isnan( each_split_channel_map)] = hp.UNSEEN each_split_channel_map = hp.reorder( each_split_channel_map, r2n=True) hp.write_map( os.path.join(self.output_folder, filename), each_split_channel_map, coord=self.pysm_output_reference_frame, column_units=self.unit, dtype=np.float32, overwrite=True, nest=True, ) else: if self.nsplits == 1: channel_map = channel_map[0] if not self.car: channel_map[np.isnan(channel_map)] = hp.UNSEEN output[each.tag] = channel_map if not write_outputs: return output