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.
- pz_rail_astro_tools-0.0.1.dist-info/LICENSE +21 -0
- pz_rail_astro_tools-0.0.1.dist-info/METADATA +83 -0
- pz_rail_astro_tools-0.0.1.dist-info/RECORD +17 -0
- pz_rail_astro_tools-0.0.1.dist-info/WHEEL +5 -0
- pz_rail_astro_tools-0.0.1.dist-info/top_level.txt +1 -0
- rail/astro_tools/__init__.py +7 -0
- rail/astro_tools/_version.py +4 -0
- rail/creation/degradation/grid_selection.py +212 -0
- rail/creation/degradation/observing_condition_degrader.py +405 -0
- rail/creation/degradation/spectroscopic_degraders.py +139 -0
- rail/creation/degradation/spectroscopic_selections.py +617 -0
- rail/examples_data/creation_data/data/hsc_ratios_and_specz.hdf5 +0 -0
- rail/examples_data/creation_data/data/survey_conditions/DC2-dr6-galcounts-i20-i25.3-nside-128.fits +0 -0
- rail/examples_data/creation_data/data/survey_conditions/DC2-mask-neg-nside-128.fits +0 -0
- rail/examples_data/creation_data/data/survey_conditions/minion_1016_dc2_Median_airmass_i_and_nightlt1825_HEAL.fits +0 -0
- rail/examples_data/creation_data/data/survey_conditions/minion_1016_dc2_Median_fiveSigmaDepth_i_and_nightlt1825_HEAL.fits +0 -0
- rail/tools/utilPhotometry.py +488 -0
|
@@ -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])
|