pz-rail-astro-tools 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pz-rail-astro-tools might be problematic. Click here for more details.

@@ -0,0 +1,405 @@
1
+ """Degrader applied to the magnitude error based on a set of input observing condition maps"""
2
+
3
+ import os
4
+ from dataclasses import fields
5
+
6
+ import healpy as hp
7
+ import numpy as np
8
+ import pandas as pd
9
+ from ceci.config import StageParameter as Param
10
+ from photerr import LsstErrorModel, LsstErrorParams
11
+
12
+ from rail.creation.degrader import Degrader
13
+
14
+
15
+ class ObsCondition(Degrader):
16
+ """Photometric errors based on observation conditions
17
+
18
+ This degrader calculates spatially-varying photometric errors
19
+ using input survey condition maps. The error is based on the
20
+ LSSTErrorModel from the PhotErr python package.
21
+
22
+ Parameters
23
+ ----------
24
+ nside: int, optional
25
+ nside used for the HEALPIX maps.
26
+ mask: str, optional
27
+ Path to the mask covering the survey
28
+ footprint in HEALPIX format. Notice that
29
+ all negative values will be set to zero.
30
+ weight: str, optional
31
+ Path to the weights HEALPIX format, used
32
+ to assign sample galaxies to pixels. Default
33
+ is weight="", which uses uniform weighting.
34
+ tot_nVis_flag: bool, optional
35
+ If any map for nVisYr are provided, this flag
36
+ indicates whether the map shows the total number of
37
+ visits in nYrObs (tot_nVis_flag=True), or the average
38
+ number of visits per year (tot_nVis_flag=False). The
39
+ default is set to True.
40
+ random_seed: int, optional
41
+ A random seed for reproducibility.
42
+ map_dict: dict, optional
43
+ A dictionary that contains the paths to the
44
+ survey condition maps in HEALPIX format. This dictionary
45
+ uses the same arguments as LSSTErrorModel (from PhotErr).
46
+ The following arguments, if supplied, may contain either
47
+ a single number (as in the case of LSSTErrorModel), or a path:
48
+ [m5, nVisYr, airmass, gamma, msky, theta, km, tvis]
49
+ For the following keys:
50
+ [m5, nVisYr, gamma, msky, theta, km]
51
+ numbers/paths for specific bands must be passed.
52
+ Example:
53
+ {"m5": {"u": path, ...}, "theta": {"u": path, ...},}
54
+ Other LSSTErrorModel parameters can also be passed
55
+ in this dictionary (e.g. a necessary one may be [nYrObs]
56
+ for the survey condition maps).
57
+ If any argument is not passed, the default value in
58
+ PhotErr's LsstErrorModel is adopted.
59
+
60
+ """
61
+
62
+ name = "ObsCondition"
63
+ config_options = Degrader.config_options.copy()
64
+ config_options.update(
65
+ nside=Param(
66
+ int,
67
+ 128,
68
+ msg="nside for the input maps in HEALPIX format.",
69
+ ),
70
+ mask=Param(
71
+ str,
72
+ os.path.join(
73
+ os.path.dirname(__file__),
74
+ "../../examples_data/creation_data/data/survey_conditions/DC2-mask-neg-nside-128.fits",
75
+ ),
76
+ msg="mask for the input maps in HEALPIX format.",
77
+ ),
78
+ weight=Param(
79
+ str,
80
+ os.path.join(
81
+ os.path.dirname(__file__),
82
+ "../../examples_data/creation_data/data/survey_conditions/DC2-dr6-galcounts-i20-i25.3-nside-128.fits",
83
+ ),
84
+ msg="weight for assigning pixels to galaxies in HEALPIX format.",
85
+ ),
86
+ tot_nVis_flag=Param(
87
+ bool,
88
+ True,
89
+ msg="flag indicating whether nVisYr is the total or average per year if supplied.",
90
+ ),
91
+ random_seed=Param(int, 42, msg="random seed for reproducibility"),
92
+ map_dict=Param(
93
+ dict,
94
+ {
95
+ "m5": {
96
+ "i": os.path.join(
97
+ os.path.dirname(__file__),
98
+ "../../examples_data/creation_data/data/survey_conditions/minion_1016_dc2_Median_fiveSigmaDepth_i_and_nightlt1825_HEAL.fits",
99
+ ),
100
+ },
101
+ "nYrObs": 5.0,
102
+ },
103
+ msg="dictionary containing the paths to the survey condition maps and/or additional LSSTErrorModel parameters.",
104
+ ),
105
+ )
106
+
107
+ def __init__(self, args, comm=None):
108
+ Degrader.__init__(self, args, comm=comm)
109
+
110
+ # store a list of keys relevant for
111
+ # survey conditions;
112
+ # a path to the survey condition
113
+ # map or a float number should be
114
+ # provided if these keys are provided
115
+ self.obs_cond_keys = [
116
+ "m5",
117
+ "nVisYr",
118
+ "airmass",
119
+ "gamma",
120
+ "msky",
121
+ "theta",
122
+ "km",
123
+ "tvis",
124
+ ]
125
+
126
+ # validate input parameters
127
+ self._validate_obs_config()
128
+
129
+ # initiate self.maps
130
+ self.maps = {}
131
+
132
+ # load the maps
133
+ self._get_maps()
134
+
135
+ def _validate_obs_config(self):
136
+ """
137
+ Validate the input
138
+ """
139
+
140
+ ### Check nside type:
141
+ # check if nside < 0
142
+ if self.config["nside"] < 0:
143
+ raise ValueError("nside must be positive.")
144
+
145
+ # check if nside is powers of two
146
+ if not np.log2(self.config["nside"]).is_integer():
147
+ raise ValueError("nside must be powers of two.")
148
+
149
+ ### Check mask type:
150
+ # check if mask is provided
151
+ if self.config["mask"] == "":
152
+ raise ValueError("mask needs to be provided for the input maps.")
153
+
154
+ # check if the path exists
155
+ if not os.path.exists(self.config["mask"]):
156
+ raise ValueError("The mask file is not found: " + self.config["mask"])
157
+
158
+ ### Check weight type:
159
+ if self.config["weight"] != "":
160
+ # check if the path exists
161
+ if not os.path.exists(self.config["weight"]):
162
+ raise ValueError("The weight file is not found: " + self.config["weight"])
163
+
164
+ ### Check map_dict:
165
+
166
+ # Check if extra keys are passed
167
+ # get lsst_error_model keys
168
+ lsst_error_model_keys = [field.name for field in fields(LsstErrorParams)]
169
+ if len(set(self.config["map_dict"].keys()) - set(lsst_error_model_keys)) != 0:
170
+ extra_keys = set(self.config["map_dict"].keys()) - set(lsst_error_model_keys)
171
+ raise ValueError("Extra keywords are passed to the configuration: \n" + str(extra_keys))
172
+
173
+ # Check data type for the keys:
174
+ # Note that LSSTErrorModel checks
175
+ # the data type for its parameters,
176
+ # so here we only check the additional
177
+ # parameters and the file paths
178
+ # nYrObs may be used below, so we
179
+ # check its type as well
180
+
181
+ if len(self.config["map_dict"]) > 0:
182
+
183
+ for key in self.config["map_dict"]:
184
+
185
+ if key == "nYrObs":
186
+ if not isinstance(self.config["map_dict"][key], float):
187
+ raise TypeError("nYrObs must be a float.")
188
+
189
+ elif key in self.obs_cond_keys:
190
+
191
+ # band-independent keys:
192
+ if key in ["airmass", "tvis"]:
193
+
194
+ # check if the input is a string or number
195
+ if not (
196
+ isinstance(self.config["map_dict"][key], str)
197
+ or isinstance(self.config["map_dict"][key], float)
198
+ ):
199
+ raise TypeError(f"{key} must be a path (string) or a float.")
200
+
201
+ # check if the paths exist
202
+ if isinstance(self.config["map_dict"][key], str):
203
+ if not os.path.exists(self.config["map_dict"][key]):
204
+ raise ValueError(
205
+ "The following file is not found: " + self.config["map_dict"][key]
206
+ )
207
+
208
+ # band-dependent keys
209
+ else:
210
+
211
+ # they must be dictionaries:
212
+ if not isinstance(self.config["map_dict"][key], dict): # pragma: no cover
213
+ raise TypeError(f"{key} must be a dictionary.")
214
+
215
+ # the dictionary cannot be empty
216
+ if len(self.config["map_dict"][key]) == 0:
217
+ raise ValueError(f"{key} is empty.")
218
+
219
+ for band in self.config["map_dict"][key].keys():
220
+
221
+ # check if the input is a string or float:
222
+ if not (
223
+ isinstance(self.config["map_dict"][key][band], str)
224
+ or isinstance(self.config["map_dict"][key][band], float)
225
+ ):
226
+ raise TypeError(f"{key}['{band}'] must be a path (string) or a float.")
227
+
228
+ # check if the paths exist
229
+ if isinstance(self.config["map_dict"][key][band], str):
230
+ if not os.path.exists(self.config["map_dict"][key][band]):
231
+ raise ValueError(
232
+ "The following file is not found: "
233
+ + self.config["map_dict"][key][band]
234
+ )
235
+
236
+ def _get_maps(self):
237
+ """
238
+ Load in the maps from the paths provided by map_dict,
239
+ if it is not empty
240
+ A note on nVisYr: input map usually in terms of
241
+ total number of exposures, so
242
+ manually divide the map by nYrObs
243
+ """
244
+
245
+ maps = {}
246
+
247
+ # Load mask
248
+ mask = hp.read_map(self.config["mask"])
249
+ if (mask < 0).any():
250
+ # set negative values (if any) to zero
251
+ mask[mask < 0] = 0
252
+ pixels = np.arange(int(self.config["nside"] ** 2 * 12))[mask.astype(bool)]
253
+ maps["pixels"] = pixels
254
+
255
+ # Load weight if given
256
+ if self.config["weight"] != "":
257
+ maps["weight"] = hp.read_map(self.config["weight"])[pixels]
258
+
259
+ # Load all other maps in map_dict
260
+ if len(self.config["map_dict"]) > 0:
261
+ for key in self.config["map_dict"]:
262
+ if key in self.obs_cond_keys:
263
+ # band-independent keys:
264
+ if key in ["airmass", "tvis"]:
265
+ if isinstance(self.config["map_dict"][key], str):
266
+ maps[key] = hp.read_map(self.config["map_dict"][key])[pixels]
267
+ elif isinstance(self.config["map_dict"][key], float):
268
+ maps[key] = np.ones(len(pixels)) * self.config["map_dict"][key]
269
+ # band-dependent keys
270
+ else:
271
+ maps[key] = {}
272
+ for band in self.config["map_dict"][key].keys():
273
+ if isinstance(self.config["map_dict"][key][band], str):
274
+ maps[key][band] = hp.read_map(self.config["map_dict"][key][band])[pixels]
275
+ elif isinstance(self.config["map_dict"][key][band], float):
276
+ maps[key][band] = np.ones(len(pixels)) * self.config["map_dict"][key][band]
277
+ else:
278
+ # copy all other lsst_error_model parameters supplied
279
+ maps[key] = self.config["map_dict"][key]
280
+
281
+ if "nVisYr" in list(self.config["map_dict"].keys()):
282
+ if "nYrObs" not in list(maps.keys()):
283
+ # Set to default:
284
+ maps["nYrObs"] = 10.0
285
+ if self.config["tot_nVis_flag"] == True:
286
+ # For each band, compute the average number of visits per year
287
+ for band in maps["nVisYr"].keys():
288
+ maps["nVisYr"][band] /= float(maps["nYrObs"])
289
+
290
+ self.maps = maps
291
+
292
+ def get_pixel_conditions(self, pixel: int) -> dict:
293
+ """
294
+ get the map values at given pixel
295
+ output is a dictionary that only
296
+ contains the LSSTErrorModel keys
297
+ """
298
+
299
+ allpix = self.maps["pixels"]
300
+ ind = allpix == pixel
301
+
302
+ obs_conditions = {}
303
+ for key in (self.maps).keys():
304
+ # For keys that may contain the survey condition maps
305
+ if key in self.obs_cond_keys:
306
+ # band-independent keys:
307
+ if key in ["airmass", "tvis"]:
308
+ obs_conditions[key] = float(self.maps[key][ind])
309
+ # band-dependent keys:
310
+ else:
311
+ obs_conditions[key] = {}
312
+ for band in (self.maps[key]).keys():
313
+ obs_conditions[key][band] = float(self.maps[key][band][ind])
314
+ # For other keys in LSSTErrorModel:
315
+ elif key not in ["pixels", "weight"]:
316
+ obs_conditions[key] = self.maps[key]
317
+ # obs_conditions should now only contain the LSSTErrorModel keys
318
+ return obs_conditions
319
+
320
+ def assign_pixels(self, catalog: pd.DataFrame) -> pd.DataFrame:
321
+ """
322
+ assign the pixels to the input catalog
323
+ """
324
+ pixels = self.maps["pixels"]
325
+ if "weight" in list((self.maps).keys()):
326
+ weights = self.maps["weight"]
327
+ weights = weights / sum(weights)
328
+ else:
329
+ weights = None
330
+ assigned_pix = self.rng.choice(pixels, size=len(catalog), replace=True, p=weights)
331
+ # make it a DataFrame object
332
+ assigned_pix = pd.DataFrame(assigned_pix, columns=["pixel"])
333
+ catalog = pd.concat([catalog, assigned_pix], axis=1)
334
+
335
+ return catalog
336
+
337
+ def run(self):
338
+ """
339
+ Run the degrader.
340
+ """
341
+ self.rng = np.random.default_rng(seed=self.config["random_seed"])
342
+
343
+ catalog = self.get_data("input", allow_missing=True)
344
+
345
+ # if self.map_dict empty, call LsstErrorModel:
346
+ if len(self.config["map_dict"]) == 0:
347
+
348
+ print("Empty map_dict, using default parameters from LsstErrorModel.")
349
+ errorModel = LsstErrorModel()
350
+ catalog = errorModel(catalog, random_state=self.rng)
351
+ self.add_data("output", catalog)
352
+
353
+ # if maps are provided, compute mag err for each pixel
354
+ elif len(self.config["map_dict"]) > 0:
355
+
356
+ # assign each galaxy to a pixel
357
+ print("Assigning pixels.")
358
+ catalog = self.assign_pixels(catalog)
359
+
360
+ # loop over each pixel
361
+ pixel_cat_list = []
362
+ for pixel, pixel_cat in catalog.groupby("pixel"):
363
+ # get the observing conditions for this pixel
364
+ obs_conditions = self.get_pixel_conditions(pixel)
365
+
366
+ # creating the error model for this pixel
367
+ errorModel = LsstErrorModel(**obs_conditions)
368
+
369
+ # calculate the error model for this pixel
370
+ obs_cat = errorModel(pixel_cat, random_state=self.rng)
371
+
372
+ # add this pixel catalog to the list
373
+ pixel_cat_list.append(obs_cat)
374
+
375
+ # recombine all the pixels into a single catalog
376
+ catalog = pd.concat(pixel_cat_list)
377
+
378
+ # sort index
379
+ catalog = catalog.sort_index()
380
+
381
+ self.add_data("output", catalog)
382
+
383
+ def __repr__(self):
384
+ """
385
+ Define how the model is represented and printed.
386
+ """
387
+
388
+ # start message
389
+ printMsg = "Loaded observing conditions from configuration file: \n"
390
+
391
+ printMsg += f"nside = {self.config['nside']}, \n"
392
+
393
+ printMsg += f"mask file: {self.config['mask']}, \n"
394
+
395
+ printMsg += f"weight file: {self.config['weight']}, \n"
396
+
397
+ printMsg += f"tot_nVis_flag = {self.config['tot_nVis_flag']}, \n"
398
+
399
+ printMsg += f"random_seed = {self.config['random_seed']}, \n"
400
+
401
+ printMsg += "map_dict contains the following items: \n"
402
+
403
+ printMsg += str(self.config["map_dict"])
404
+
405
+ return printMsg
@@ -0,0 +1,139 @@
1
+ """Degraders that emulate spectroscopic effects on photometry"""
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ from rail.creation.degrader import Degrader
6
+
7
+
8
+ class LineConfusion(Degrader):
9
+ """Degrader that simulates emission line confusion.
10
+
11
+ .. code-block:: python
12
+
13
+ Example: degrader = LineConfusion(true_wavelen=3727,
14
+ wrong_wavelen=5007,
15
+ frac_wrong=0.05)
16
+
17
+ is a degrader that misidentifies 5% of OII lines (at 3727 angstroms)
18
+ as OIII lines (at 5007 angstroms), which results in a larger
19
+ spectroscopic redshift.
20
+
21
+ Note that when selecting the galaxies for which the lines are confused,
22
+ the degrader ignores galaxies for which this line confusion would result
23
+ in a negative redshift, which can occur for low redshift galaxies when
24
+ wrong_wavelen < true_wavelen.
25
+
26
+ Parameters
27
+ ----------
28
+ true_wavelen : positive float
29
+ The wavelength of the true emission line.
30
+ Wavelength unit assumed to be the same as wrong_wavelen.
31
+ wrong_wavelen : positive float
32
+ The wavelength of the wrong emission line, which is being confused
33
+ for the correct emission line.
34
+ Wavelength unit assumed to be the same as true_wavelen.
35
+ frac_wrong : float between zero and one
36
+ The fraction of galaxies with confused emission lines.
37
+ """
38
+
39
+ name = 'LineConfusion'
40
+ config_options = Degrader.config_options.copy()
41
+ config_options.update(true_wavelen=float,
42
+ wrong_wavelen=float,
43
+ frac_wrong=float)
44
+
45
+ def __init__(self, args, comm=None):
46
+ """
47
+ """
48
+ Degrader.__init__(self, args, comm=comm)
49
+ # validate parameters
50
+ if self.config.true_wavelen < 0:
51
+ raise ValueError("true_wavelen must be positive, not {self.config.true_wavelen}")
52
+ if self.config.wrong_wavelen < 0:
53
+ raise ValueError("wrong_wavelen must be positive, not {self.config.wrong_wavelen}")
54
+ if self.config.frac_wrong < 0 or self.config.frac_wrong > 1:
55
+ raise ValueError("frac_wrong must be between 0 and 1., not {self.config.wrong_wavelen}")
56
+
57
+ def run(self):
58
+ """ Run method
59
+
60
+ Applies line confusion
61
+
62
+ Notes
63
+ -----
64
+ Get the input data from the data store under this stages 'input' tag
65
+ Puts the data into the data store under this stages 'output' tag
66
+ """
67
+ data = self.get_data('input')
68
+
69
+ # convert to an array for easy manipulation
70
+ values, columns = data.values.copy(), data.columns.copy()
71
+
72
+ # get the minimum redshift
73
+ # if wrong_wavelen < true_wavelen, this is minimum the redshift for
74
+ # which the confused redshift is still positive
75
+ zmin = self.config.wrong_wavelen / self.config.true_wavelen - 1
76
+
77
+ # select the random fraction of galaxies whose lines are confused
78
+ rng = np.random.default_rng(self.config.seed)
79
+ idx = rng.choice(
80
+ np.where(values[:, 0] > zmin)[0],
81
+ size=int(self.config.frac_wrong * values.shape[0]),
82
+ replace=False,
83
+ )
84
+
85
+ # transform these redshifts
86
+ values[idx, 0] = (
87
+ 1 + values[idx, 0]
88
+ ) * self.config.true_wavelen / self.config.wrong_wavelen - 1
89
+
90
+ # return results in a data frame
91
+ outData = pd.DataFrame(values, columns=columns)
92
+ self.add_data('output', outData)
93
+
94
+
95
+ class InvRedshiftIncompleteness(Degrader):
96
+ """Degrader that simulates incompleteness with a selection function
97
+ inversely proportional to redshift.
98
+
99
+ The survival probability of this selection function is
100
+ p(z) = min(1, z_p/z),
101
+ where z_p is the pivot redshift.
102
+
103
+ Parameters
104
+ ----------
105
+ pivot_redshift : positive float
106
+ The redshift at which the incompleteness begins.
107
+ """
108
+
109
+ name = 'InvRedshiftIncompleteness'
110
+ config_options = Degrader.config_options.copy()
111
+ config_options.update(pivot_redshift=float)
112
+
113
+ def __init__(self, args, comm=None):
114
+ """
115
+ """
116
+ Degrader.__init__(self, args, comm=comm)
117
+ if self.config.pivot_redshift < 0:
118
+ raise ValueError("pivot redshift must be positive, not {self.config.pivot_redshift}")
119
+
120
+ def run(self):
121
+ """ Run method
122
+
123
+ Applies incompleteness
124
+
125
+ Notes
126
+ -----
127
+ Get the input data from the data store under this stages 'input' tag
128
+ Puts the data into the data store under this stages 'output' tag
129
+ """
130
+ data = self.get_data('input')
131
+
132
+ # calculate survival probability for each galaxy
133
+ survival_prob = np.clip(self.config.pivot_redshift / data["redshift"], 0, 1)
134
+
135
+ # probabalistically drop galaxies from the data set
136
+ rng = np.random.default_rng(self.config.seed)
137
+ mask = rng.random(size=data.shape[0]) <= survival_prob
138
+
139
+ self.add_data('output', data[mask])