pyimcom 1.2.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.
- pyimcom/__init__.py +1 -0
- pyimcom/_version.py +24 -0
- pyimcom/analysis.py +1480 -0
- pyimcom/coadd.py +2331 -0
- pyimcom/compress/__init__.py +0 -0
- pyimcom/compress/compressutils.py +506 -0
- pyimcom/compress/compressutils_wrapper.py +116 -0
- pyimcom/compress/i24.py +514 -0
- pyimcom/config.py +1245 -0
- pyimcom/diagnostics/__init__.py +0 -0
- pyimcom/diagnostics/context_figure.py +58 -0
- pyimcom/diagnostics/dynrange.py +274 -0
- pyimcom/diagnostics/layer_diagnostics.py +208 -0
- pyimcom/diagnostics/mosaicimage.py +80 -0
- pyimcom/diagnostics/noise/stability.py +126 -0
- pyimcom/diagnostics/noise_diagnostics.py +709 -0
- pyimcom/diagnostics/outimage_utils/__init__.py +0 -0
- pyimcom/diagnostics/outimage_utils/helper.py +82 -0
- pyimcom/diagnostics/report.py +366 -0
- pyimcom/diagnostics/run.py +64 -0
- pyimcom/diagnostics/starcube_nonoise.py +264 -0
- pyimcom/diagnostics/starcube_nonoise_coldescr.txt +24 -0
- pyimcom/diagnostics/stars.py +469 -0
- pyimcom/imdestripe.py +2454 -0
- pyimcom/lakernel.py +805 -0
- pyimcom/layer.py +1439 -0
- pyimcom/layer_wrapper.py +96 -0
- pyimcom/meta/__init__.py +0 -0
- pyimcom/meta/distortimage.py +748 -0
- pyimcom/meta/ginterp.py +340 -0
- pyimcom/pictures/__init__.py +0 -0
- pyimcom/pictures/genpic.py +229 -0
- pyimcom/psfutil.py +2199 -0
- pyimcom/routine.py +588 -0
- pyimcom/splitpsf/__init__.py +0 -0
- pyimcom/splitpsf/imsubtract.py +793 -0
- pyimcom/splitpsf/imsubtract_wrapper.py +107 -0
- pyimcom/splitpsf/splitpsf.py +497 -0
- pyimcom/splitpsf/splitpsf_wrapper.py +161 -0
- pyimcom/splitpsf/update_cube.py +136 -0
- pyimcom/truthcats.py +396 -0
- pyimcom/utils/__init__.py +0 -0
- pyimcom/utils/compareutils.py +207 -0
- pyimcom/utils/piffutils.py +223 -0
- pyimcom/wcsutil.py +839 -0
- pyimcom-1.2.1.dist-info/METADATA +67 -0
- pyimcom-1.2.1.dist-info/RECORD +52 -0
- pyimcom-1.2.1.dist-info/WHEEL +5 -0
- pyimcom-1.2.1.dist-info/licenses/LICENSE +21 -0
- pyimcom-1.2.1.dist-info/scm_file_list.json +285 -0
- pyimcom-1.2.1.dist-info/scm_version.json +8 -0
- pyimcom-1.2.1.dist-info/top_level.txt +1 -0
pyimcom/analysis.py
ADDED
|
@@ -0,0 +1,1480 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tools to analyze coadded images.
|
|
3
|
+
|
|
4
|
+
Classes
|
|
5
|
+
-------
|
|
6
|
+
OutImage
|
|
7
|
+
Wrapper for coadded images (blocks).
|
|
8
|
+
NoiseAnal
|
|
9
|
+
Analysis of noise frames.
|
|
10
|
+
StarsAnal
|
|
11
|
+
Analysis of point sources.
|
|
12
|
+
_BlkGrp
|
|
13
|
+
Abstract base class for groups of blocks (mosiacs or suites).
|
|
14
|
+
Mosaic
|
|
15
|
+
Wrapper for coadded mosaics (2D arrays of blocks).
|
|
16
|
+
Suite
|
|
17
|
+
Wrapper for coadded suites (hashed arrays of blocks).
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from enum import Enum
|
|
23
|
+
from os.path import exists
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
import galsim
|
|
27
|
+
import healpy
|
|
28
|
+
import numpy as np
|
|
29
|
+
from astropy import constants as const
|
|
30
|
+
from astropy import units as u
|
|
31
|
+
from astropy import wcs
|
|
32
|
+
from astropy.io import fits
|
|
33
|
+
from scipy import ndimage
|
|
34
|
+
|
|
35
|
+
from .coadd import Block, OutStamp
|
|
36
|
+
from .compress.compressutils import ReadFile
|
|
37
|
+
from .config import Config, Timer
|
|
38
|
+
from .config import Settings as Stn
|
|
39
|
+
from .diagnostics.outimage_utils.helper import HDU_to_bels
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class OutImage:
|
|
43
|
+
"""
|
|
44
|
+
Wrapper for coadded images (blocks).
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
fpath : str or str-like
|
|
49
|
+
Path to the output FITS file.
|
|
50
|
+
cfg : Config, optional
|
|
51
|
+
Configuration used for this output image.
|
|
52
|
+
If provided, no consistency check is performed.
|
|
53
|
+
If None, it will be extracted from FITS file.
|
|
54
|
+
hdu_names : list of str, optional
|
|
55
|
+
List of HDU names of this FITS file.
|
|
56
|
+
If provided, no consistency check is performed.
|
|
57
|
+
If None, it will be derived from `cfg`.
|
|
58
|
+
|
|
59
|
+
Methods
|
|
60
|
+
-------
|
|
61
|
+
get_hdu_names
|
|
62
|
+
Parse outmaps to get a list of HDU names.
|
|
63
|
+
__init__
|
|
64
|
+
Constructor.
|
|
65
|
+
get_last_line
|
|
66
|
+
Get last line of a text file.
|
|
67
|
+
get_time_consump
|
|
68
|
+
Parse terminal output to get time consumption.
|
|
69
|
+
_load_or_save_hdu_list
|
|
70
|
+
Load data from or save data to FITS file.
|
|
71
|
+
get_coadded_layer
|
|
72
|
+
Extract a coadded layer from the primary HDU.
|
|
73
|
+
get_T_weightmap
|
|
74
|
+
Extract T_weightmap from an additional HDU.
|
|
75
|
+
get_mean_coverage
|
|
76
|
+
Compute mean coverage based on T_weightmap.
|
|
77
|
+
get_output_map
|
|
78
|
+
Extract an output map from the additional HDUs.
|
|
79
|
+
_update_hdu_data
|
|
80
|
+
Update data using data provided by a neighbor.
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def get_hdu_names(outmaps: str) -> list[str]:
|
|
86
|
+
"""
|
|
87
|
+
Parse outmaps to get a list of HDU names.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
outmaps : str
|
|
92
|
+
outmaps attribute of a Config instance.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
list of str
|
|
97
|
+
A list of HDU names.
|
|
98
|
+
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
hdu_names = ["PRIMARY", "CONFIG", "INDATA", "INWEIGHT", "INWTFLAT"]
|
|
102
|
+
if "U" in outmaps:
|
|
103
|
+
hdu_names.append("FIDELITY")
|
|
104
|
+
if "S" in outmaps:
|
|
105
|
+
hdu_names.append("SIGMA")
|
|
106
|
+
if "K" in outmaps:
|
|
107
|
+
hdu_names.append("KAPPA")
|
|
108
|
+
if "T" in outmaps:
|
|
109
|
+
hdu_names.append("INWTSUM")
|
|
110
|
+
if "N" in outmaps:
|
|
111
|
+
hdu_names.append("EFFCOVER")
|
|
112
|
+
return hdu_names
|
|
113
|
+
|
|
114
|
+
def __init__(self, fpath: str, cfg: Config = None, hdu_names: list[str] = None) -> None:
|
|
115
|
+
fpath = str(fpath)
|
|
116
|
+
self.fpath = fpath
|
|
117
|
+
|
|
118
|
+
self.cfg = cfg
|
|
119
|
+
if cfg is None:
|
|
120
|
+
with ReadFile(fpath) as hdu_list:
|
|
121
|
+
self.cfg = Config("".join(hdu_list["CONFIG"].data["text"].tolist()))
|
|
122
|
+
self.cfg() # update configuration parameters
|
|
123
|
+
|
|
124
|
+
self.hdu_names = hdu_names
|
|
125
|
+
if hdu_names is None:
|
|
126
|
+
self.hdu_names = OutImage.get_hdu_names(self.cfg.outmaps)
|
|
127
|
+
|
|
128
|
+
with fits.open(self.fpath) as _hdul:
|
|
129
|
+
_header = _hdul["CONFIG"].header
|
|
130
|
+
|
|
131
|
+
if ("BLOCKX" in _header) and ("BLOCKY" in _header):
|
|
132
|
+
self.ibx = int(_header["BLOCKX"])
|
|
133
|
+
self.iby = int(_header["BLOCKY"])
|
|
134
|
+
else:
|
|
135
|
+
# Get file indices.
|
|
136
|
+
fstem = Path(fpath).stem
|
|
137
|
+
# This part is for legacy file names that had a '_map'.
|
|
138
|
+
# Summer 2025 run and later don't have this.
|
|
139
|
+
if fstem[-4:] == "_map":
|
|
140
|
+
fstem = fstem[:-4]
|
|
141
|
+
self.ibx, self.iby = map(int, fstem.split("_")[-2:])
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def get_last_line(fname: str) -> str:
|
|
145
|
+
"""
|
|
146
|
+
Get last line of a text file.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
fname : str
|
|
151
|
+
Path to the text file.
|
|
152
|
+
|
|
153
|
+
Returns
|
|
154
|
+
-------
|
|
155
|
+
str
|
|
156
|
+
Last line of the text file.
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
with open(fname, "r") as f:
|
|
161
|
+
for line in f:
|
|
162
|
+
last_line = line
|
|
163
|
+
return last_line
|
|
164
|
+
|
|
165
|
+
def get_time_consump(self) -> float:
|
|
166
|
+
"""
|
|
167
|
+
Parse terminal output to get time consumption.
|
|
168
|
+
|
|
169
|
+
Returns
|
|
170
|
+
-------
|
|
171
|
+
float
|
|
172
|
+
Time consumption in seconds (if found), otherwise nan.
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
fname = self.fpath.replace(".fits", ".out")
|
|
177
|
+
try:
|
|
178
|
+
last_line = OutImage.get_last_line(fname)
|
|
179
|
+
m = re.match("finished at t = ([0-9.]+) s", last_line)
|
|
180
|
+
return float(m.group(1))
|
|
181
|
+
except FileNotFoundError:
|
|
182
|
+
return np.nan
|
|
183
|
+
|
|
184
|
+
def _load_or_save_hdu_list(
|
|
185
|
+
self, load_mode: bool = True, save_file: bool = False, auto_to_all: bool = False
|
|
186
|
+
) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Load data from or save data to FITS file.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
load_mode : bool, optional
|
|
193
|
+
If True, load data from FITS file (if not already loaded);
|
|
194
|
+
if False, remove current data from memory (if data exist).
|
|
195
|
+
save_file : bool, optional
|
|
196
|
+
Only used when `load_mode` == False. If (`save_file` ==) True,
|
|
197
|
+
save current data to FITS file (overwriting the existing file).
|
|
198
|
+
auto_to_all : bool, optional
|
|
199
|
+
Only used when load_mode == False and save_file == True.
|
|
200
|
+
If (auto_to_all ==) True, change 'PADSIDES' from 'auto' to 'all'
|
|
201
|
+
in the 'CONFIG' HDU of FITS file.
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
None
|
|
206
|
+
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
if load_mode:
|
|
210
|
+
if not hasattr(self, "hdu_list"):
|
|
211
|
+
self.hdu_list = ReadFile(self.fpath)
|
|
212
|
+
|
|
213
|
+
else:
|
|
214
|
+
if save_file:
|
|
215
|
+
assert hasattr(self, "hdu_list"), "no hdu_list to save"
|
|
216
|
+
|
|
217
|
+
if auto_to_all:
|
|
218
|
+
my_cfg = Config("".join(self.hdu_list["CONFIG"].data["text"].tolist()))
|
|
219
|
+
my_cfg.pad_sides = "all"
|
|
220
|
+
config_hdu = fits.TableHDU.from_columns(
|
|
221
|
+
[
|
|
222
|
+
fits.Column(
|
|
223
|
+
name="text",
|
|
224
|
+
array=my_cfg.to_file(None).splitlines(),
|
|
225
|
+
format="A512",
|
|
226
|
+
ascii=True,
|
|
227
|
+
)
|
|
228
|
+
]
|
|
229
|
+
)
|
|
230
|
+
config_hdu.header["EXTNAME"] = "CONFIG"
|
|
231
|
+
self.hdu_list["CONFIG"] = config_hdu
|
|
232
|
+
|
|
233
|
+
self.hdu_list.writeto(self.fpath, overwrite=True)
|
|
234
|
+
|
|
235
|
+
if hasattr(self, "hdu_list"):
|
|
236
|
+
self.hdu_list.close()
|
|
237
|
+
del self.hdu_list
|
|
238
|
+
|
|
239
|
+
def get_coadded_layer(self, layer: str, j_out: int = 0) -> np.array:
|
|
240
|
+
"""
|
|
241
|
+
Extract a coadded layer from the primary HDU.
|
|
242
|
+
|
|
243
|
+
Parameters
|
|
244
|
+
----------
|
|
245
|
+
layer : str
|
|
246
|
+
Name of the layer to be extracted.
|
|
247
|
+
j_out : int or None, optional
|
|
248
|
+
Index of the output PSF. If None, return results based on all output PSFs.
|
|
249
|
+
|
|
250
|
+
Returns
|
|
251
|
+
-------
|
|
252
|
+
data : np.array
|
|
253
|
+
Requested coadded layer.
|
|
254
|
+
The shape is either (NsideP, NsideP) or (n_out, NsideP, NsideP) (all output PSFs)
|
|
255
|
+
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
assert layer in ["SCI"] + self.cfg.extrainput[1:], f"Error: layer '{layer}' not found"
|
|
259
|
+
idx = self.cfg.extrainput.index(layer if layer != "SCI" else None)
|
|
260
|
+
|
|
261
|
+
data_loaded = hasattr(self, "hdu_list")
|
|
262
|
+
if not data_loaded:
|
|
263
|
+
self.hdu_list = ReadFile(self.fpath)
|
|
264
|
+
|
|
265
|
+
if j_out is not None:
|
|
266
|
+
data = (self.hdu_list["PRIMARY"].data[j_out, idx]).astype(np.float32)
|
|
267
|
+
else:
|
|
268
|
+
data = (self.hdu_list["PRIMARY"].data[:, idx]).astype(np.float32)
|
|
269
|
+
|
|
270
|
+
if not data_loaded:
|
|
271
|
+
self.hdu_list.close()
|
|
272
|
+
del self.hdu_list
|
|
273
|
+
return data
|
|
274
|
+
|
|
275
|
+
def get_T_weightmap(self, flat: bool = False, j_out: int = 0) -> np.array:
|
|
276
|
+
"""
|
|
277
|
+
Extract T_weightmap from an additional HDU.
|
|
278
|
+
|
|
279
|
+
Parameters
|
|
280
|
+
----------
|
|
281
|
+
flat : bool, optional
|
|
282
|
+
Whether to read the flat version of T_weightmap.
|
|
283
|
+
j_out : int or None, optional
|
|
284
|
+
Only used when `flat` is False. Index of the output PSF.
|
|
285
|
+
If None, return results based on all output PSFs.
|
|
286
|
+
|
|
287
|
+
Returns
|
|
288
|
+
-------
|
|
289
|
+
data : np.array
|
|
290
|
+
Requested T_weightmap.
|
|
291
|
+
Shape is either (n_inimage, n1P, n1P) (1)
|
|
292
|
+
or (n_out, n_inimage, n1P, n1P) (all output PSFs)
|
|
293
|
+
or (n_out*n1P, n_inimage*n1P) (flat version)
|
|
294
|
+
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
data_loaded = hasattr(self, "hdu_list")
|
|
298
|
+
if not data_loaded:
|
|
299
|
+
self.hdu_list = ReadFile(self.fpath)
|
|
300
|
+
|
|
301
|
+
if not flat: # read T_hdu
|
|
302
|
+
if j_out is not None:
|
|
303
|
+
data = (self.hdu_list["INWEIGHT"].data[j_out, ...]).astype(np.float32)
|
|
304
|
+
else:
|
|
305
|
+
data = (self.hdu_list["INWEIGHT"].data[:, ...]).astype(np.float32)
|
|
306
|
+
|
|
307
|
+
else: # read T_hdu2
|
|
308
|
+
data = (self.hdu_list["INWTFLAT"].data).astype(np.float32)
|
|
309
|
+
|
|
310
|
+
if not data_loaded:
|
|
311
|
+
self.hdu_list.close()
|
|
312
|
+
del self.hdu_list
|
|
313
|
+
return data
|
|
314
|
+
|
|
315
|
+
def get_mean_coverage(self, padding: bool = False) -> float:
|
|
316
|
+
"""
|
|
317
|
+
Compute mean coverage based on T_weightmap.
|
|
318
|
+
|
|
319
|
+
We assume that mean coverage is the same for all output PSFs.
|
|
320
|
+
|
|
321
|
+
Parameters
|
|
322
|
+
----------
|
|
323
|
+
padding : bool, optional
|
|
324
|
+
Whether to include padding postage stamps. The default is False.
|
|
325
|
+
|
|
326
|
+
Returns
|
|
327
|
+
-------
|
|
328
|
+
mean_coverage : float
|
|
329
|
+
Mean coverage based on T_weightmap.
|
|
330
|
+
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
T_weightmap = self.get_T_weightmap(j_out=0) # shape: (n_inimage, n1P, n1P)
|
|
334
|
+
if not padding:
|
|
335
|
+
pad = self.cfg.postage_pad # shortcut
|
|
336
|
+
if pad > 0:
|
|
337
|
+
T_weightmap = T_weightmap[:, pad:-pad, pad:-pad]
|
|
338
|
+
|
|
339
|
+
mean_coverage = np.mean(np.sum(T_weightmap.astype(bool), axis=0))
|
|
340
|
+
del T_weightmap
|
|
341
|
+
return mean_coverage
|
|
342
|
+
|
|
343
|
+
def get_output_map(self, outmap: str, j_out: int = 0) -> np.array:
|
|
344
|
+
"""
|
|
345
|
+
Extract an output map from the additional HDUs.
|
|
346
|
+
|
|
347
|
+
Parameters
|
|
348
|
+
----------
|
|
349
|
+
outmap : str
|
|
350
|
+
Name of the output map to be extracted.
|
|
351
|
+
j_out : int or None, optional
|
|
352
|
+
Index of the output PSF.
|
|
353
|
+
If None, return results based on all output PSFs.
|
|
354
|
+
|
|
355
|
+
Returns
|
|
356
|
+
-------
|
|
357
|
+
data : np.array
|
|
358
|
+
Requested output map. shape is either (NsideP, NsideP)
|
|
359
|
+
or (n_out, NsideP, NsideP) (all output PSFs).
|
|
360
|
+
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
assert outmap in self.hdu_names, f"Error: map '{outmap}' not found"
|
|
364
|
+
assert outmap in [
|
|
365
|
+
"FIDELITY",
|
|
366
|
+
"SIGMA",
|
|
367
|
+
"KAPPA",
|
|
368
|
+
"INWTSUM",
|
|
369
|
+
"EFFCOVER",
|
|
370
|
+
], f"Error: map '{outmap}' not supported by get_output_map"
|
|
371
|
+
|
|
372
|
+
data_loaded = hasattr(self, "hdu_list")
|
|
373
|
+
if not data_loaded:
|
|
374
|
+
self.hdu_list = ReadFile(self.fpath)
|
|
375
|
+
|
|
376
|
+
coef = 1.0 / HDU_to_bels(self.hdu_list[outmap])
|
|
377
|
+
slice_ = np.s_[j_out] if j_out is not None else np.s_[:]
|
|
378
|
+
data = np.power(10.0, self.hdu_list[outmap].data[slice_] / coef).astype(np.float32)
|
|
379
|
+
|
|
380
|
+
dtype = self.hdu_list[outmap].data.dtype
|
|
381
|
+
if dtype == np.dtype("uint16"):
|
|
382
|
+
a_min, a_max = 0, 65535
|
|
383
|
+
elif dtype == np.dtype(">i2"):
|
|
384
|
+
a_min, a_max = -32768, 32767
|
|
385
|
+
a_zero = a_min if coef > 0 else a_max
|
|
386
|
+
data[data == np.power(10.0, a_zero / coef)] = 0.0
|
|
387
|
+
|
|
388
|
+
if not data_loaded:
|
|
389
|
+
self.hdu_list.close()
|
|
390
|
+
del self.hdu_list
|
|
391
|
+
return data
|
|
392
|
+
|
|
393
|
+
def _update_hdu_data(self, neighbor: "OutImage", direction: str, add_mode: bool = True) -> None:
|
|
394
|
+
"""
|
|
395
|
+
Update data using data provided by a neighbor.
|
|
396
|
+
|
|
397
|
+
This method is developed for postprocessing, i.e.,
|
|
398
|
+
sharing padding postage stamps between adjacent blocks.
|
|
399
|
+
This method neither loads nor saves hdu_list.
|
|
400
|
+
|
|
401
|
+
Parameters
|
|
402
|
+
----------
|
|
403
|
+
neighbor : OutImage
|
|
404
|
+
Neighboring output image (block) who shares data with "me."
|
|
405
|
+
direction : str
|
|
406
|
+
Which side to update. Must be 'left', 'right', 'bottom', or 'top'.
|
|
407
|
+
add_mode : bool, optional
|
|
408
|
+
If True, update "my" data by adding neighbor's to "mine;"
|
|
409
|
+
if False, replace "my" data with neighbor's.
|
|
410
|
+
|
|
411
|
+
Returns
|
|
412
|
+
-------
|
|
413
|
+
None
|
|
414
|
+
|
|
415
|
+
"""
|
|
416
|
+
|
|
417
|
+
assert direction in [
|
|
418
|
+
"left",
|
|
419
|
+
"right",
|
|
420
|
+
"bottom",
|
|
421
|
+
"top",
|
|
422
|
+
], f"Error: direction '{direction}' not supported by _update_hdu_data"
|
|
423
|
+
|
|
424
|
+
# not necessarily the same as self.cfg
|
|
425
|
+
my_cfg = Config("".join(self.hdu_list["CONFIG"].data["text"].tolist()))
|
|
426
|
+
assert my_cfg.pad_sides == "auto", "Error: _update_hdu_data only supports pad_sides == 'auto'"
|
|
427
|
+
del my_cfg
|
|
428
|
+
|
|
429
|
+
# update PRIMARY
|
|
430
|
+
NsideP = self.cfg.NsideP # shortcut
|
|
431
|
+
width = self.cfg.postage_pad * self.cfg.n2 # width of padding region
|
|
432
|
+
fk = self.cfg.fade_kernel # shortcut
|
|
433
|
+
|
|
434
|
+
if direction == "left":
|
|
435
|
+
my_slice = np.s_[:, :, :, 0 : width + fk]
|
|
436
|
+
ur_slice = np.s_[:, :, :, NsideP - width * 2 : NsideP - width + fk]
|
|
437
|
+
elif direction == "right":
|
|
438
|
+
my_slice = np.s_[:, :, :, NsideP - width - fk : NsideP]
|
|
439
|
+
ur_slice = np.s_[:, :, :, width - fk : width * 2]
|
|
440
|
+
elif direction == "bottom":
|
|
441
|
+
my_slice = np.s_[:, :, 0 : width + fk, :]
|
|
442
|
+
ur_slice = np.s_[:, :, NsideP - width * 2 : NsideP - width + fk, :]
|
|
443
|
+
elif direction == "top":
|
|
444
|
+
my_slice = np.s_[:, :, NsideP - width - fk : NsideP, :]
|
|
445
|
+
ur_slice = np.s_[:, :, width - fk : width * 2, :]
|
|
446
|
+
|
|
447
|
+
self.hdu_list["PRIMARY"].data[my_slice] = (
|
|
448
|
+
self.hdu_list["PRIMARY"].data[my_slice] * add_mode + neighbor.hdu_list["PRIMARY"].data[ur_slice]
|
|
449
|
+
)
|
|
450
|
+
del my_slice, ur_slice
|
|
451
|
+
|
|
452
|
+
# update INWEIGHT and INWTFLAT
|
|
453
|
+
n1P = self.cfg.n1P # shortcuts
|
|
454
|
+
pad = self.cfg.postage_pad
|
|
455
|
+
|
|
456
|
+
my_idscas = list(
|
|
457
|
+
zip(self.hdu_list["INDATA"].data["obsid"], self.hdu_list["INDATA"].data["sca"], strict=False)
|
|
458
|
+
)
|
|
459
|
+
ur_idscas = list(
|
|
460
|
+
zip(
|
|
461
|
+
neighbor.hdu_list["INDATA"].data["obsid"],
|
|
462
|
+
neighbor.hdu_list["INDATA"].data["sca"],
|
|
463
|
+
strict=False,
|
|
464
|
+
)
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
for idsca in set(my_idscas) & set(ur_idscas):
|
|
468
|
+
my_idx = my_idscas.index(idsca)
|
|
469
|
+
ur_idx = ur_idscas.index(idsca)
|
|
470
|
+
|
|
471
|
+
if direction == "left":
|
|
472
|
+
my_slice = np.s_[:, my_idx, :, 0:pad]
|
|
473
|
+
ur_slice = np.s_[:, ur_idx, :, n1P - pad * 2 : n1P - pad]
|
|
474
|
+
elif direction == "right":
|
|
475
|
+
my_slice = np.s_[:, my_idx, :, n1P - pad : n1P]
|
|
476
|
+
ur_slice = np.s_[:, ur_idx, :, pad : pad * 2]
|
|
477
|
+
elif direction == "bottom":
|
|
478
|
+
my_slice = np.s_[:, my_idx, 0:pad, :]
|
|
479
|
+
ur_slice = np.s_[:, ur_idx, n1P - pad * 2 : n1P - pad, :]
|
|
480
|
+
elif direction == "top":
|
|
481
|
+
my_slice = np.s_[:, my_idx, n1P - pad : n1P, :]
|
|
482
|
+
ur_slice = np.s_[:, ur_idx, pad : pad * 2, :]
|
|
483
|
+
|
|
484
|
+
self.hdu_list["INWEIGHT"].data[my_slice] = neighbor.hdu_list["INWEIGHT"].data[ur_slice]
|
|
485
|
+
del my_slice, ur_slice
|
|
486
|
+
|
|
487
|
+
del my_idscas, ur_idscas
|
|
488
|
+
|
|
489
|
+
n_out, n_inimage = self.hdu_list["INWEIGHT"].data.shape[:2]
|
|
490
|
+
self.hdu_list["INWTFLAT"].data[:, :] = np.transpose(
|
|
491
|
+
self.hdu_list["INWEIGHT"].data, axes=(0, 2, 1, 3)
|
|
492
|
+
).reshape((n_out * n1P, n_inimage * n1P))
|
|
493
|
+
|
|
494
|
+
# update output maps
|
|
495
|
+
fk = self.cfg.fade_kernel
|
|
496
|
+
|
|
497
|
+
for outmap in self.hdu_names[5:]:
|
|
498
|
+
my_maps = self.get_output_map(outmap, None)
|
|
499
|
+
ur_maps = neighbor.get_output_map(outmap, None)
|
|
500
|
+
|
|
501
|
+
if direction == "left":
|
|
502
|
+
if add_mode:
|
|
503
|
+
OutStamp.trapezoid(my_maps, fk, False, (0, 0, width - fk, 0), "L")
|
|
504
|
+
OutStamp.trapezoid(ur_maps, fk, False, (0, 0, 0, width - fk), "R")
|
|
505
|
+
my_slice = np.s_[:, :, 0 : width + fk]
|
|
506
|
+
ur_slice = np.s_[:, :, NsideP - width * 2 : NsideP - width + fk]
|
|
507
|
+
|
|
508
|
+
elif direction == "right":
|
|
509
|
+
if add_mode:
|
|
510
|
+
OutStamp.trapezoid(my_maps, fk, False, (0, 0, 0, width - fk), "R")
|
|
511
|
+
OutStamp.trapezoid(ur_maps, fk, False, (0, 0, width - fk, 0), "L")
|
|
512
|
+
my_slice = np.s_[:, :, NsideP - width - fk : NsideP]
|
|
513
|
+
ur_slice = np.s_[:, :, width - fk : width * 2]
|
|
514
|
+
|
|
515
|
+
elif direction == "bottom":
|
|
516
|
+
if add_mode:
|
|
517
|
+
OutStamp.trapezoid(my_maps, fk, False, (width - fk, 0, 0, 0), "B")
|
|
518
|
+
OutStamp.trapezoid(ur_maps, fk, False, (0, width - fk, 0, 0), "T")
|
|
519
|
+
my_slice = np.s_[:, 0 : width + fk, :]
|
|
520
|
+
ur_slice = np.s_[:, NsideP - width * 2 : NsideP - width + fk, :]
|
|
521
|
+
|
|
522
|
+
elif direction == "top":
|
|
523
|
+
if add_mode:
|
|
524
|
+
OutStamp.trapezoid(my_maps, fk, False, (0, width - fk, 0, 0), "T")
|
|
525
|
+
OutStamp.trapezoid(ur_maps, fk, False, (width - fk, 0, 0, 0), "B")
|
|
526
|
+
my_slice = np.s_[:, NsideP - width - fk : NsideP, :]
|
|
527
|
+
ur_slice = np.s_[:, width - fk : width * 2, :]
|
|
528
|
+
|
|
529
|
+
coef = int(self.hdu_list[outmap].header.comments["UNIT"].partition("*")[0])
|
|
530
|
+
dtype = self.hdu_list[outmap].data.dtype
|
|
531
|
+
if dtype == np.dtype(">i2"):
|
|
532
|
+
dtype = np.dtype("int16")
|
|
533
|
+
self.hdu_list[outmap].data[my_slice] = Block.compress_map(
|
|
534
|
+
my_maps[my_slice] * add_mode + ur_maps[ur_slice], coef, dtype
|
|
535
|
+
)
|
|
536
|
+
del my_maps, ur_maps, my_slice, ur_slice
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
class NoiseAnal:
|
|
540
|
+
"""
|
|
541
|
+
Analysis of noise frames.
|
|
542
|
+
|
|
543
|
+
Largely based on diagnostics/noise/noisespecs.py.
|
|
544
|
+
|
|
545
|
+
Parameters
|
|
546
|
+
----------
|
|
547
|
+
outim : OutImage
|
|
548
|
+
Output image to analyze.
|
|
549
|
+
layer : str
|
|
550
|
+
Layer name of noise frame to analyze.
|
|
551
|
+
|
|
552
|
+
Methods
|
|
553
|
+
-------
|
|
554
|
+
__init__
|
|
555
|
+
Constructor.
|
|
556
|
+
get_norm
|
|
557
|
+
Get norm for 2D noise power spectrum (classmethod).
|
|
558
|
+
azimuthal_average
|
|
559
|
+
Compute radial profile of image (staticmethod)).
|
|
560
|
+
_get_wavenumbers
|
|
561
|
+
Calculate wavenumbers for the input image (staticmethod).
|
|
562
|
+
__call__
|
|
563
|
+
Analyze specified noise frame of given output image.
|
|
564
|
+
clear
|
|
565
|
+
Free up memory space.
|
|
566
|
+
|
|
567
|
+
"""
|
|
568
|
+
|
|
569
|
+
# from noise/noisespecs.py
|
|
570
|
+
AREA = {"Y106": 7006, "J129": 7111, "H158": 7340, "F184": 4840, "K213": 4654, "W146": 22085} # cm^2
|
|
571
|
+
|
|
572
|
+
# Set useful constants
|
|
573
|
+
tfr = 3.08 # sec
|
|
574
|
+
gain = 1.458 # electrons/DN
|
|
575
|
+
ABstd = 3.631e-20 # erg/cm^2
|
|
576
|
+
h = const.h.to("erg/Hz").value # 6.62607015e-27
|
|
577
|
+
m_ab = 23.9 # sample mag for PS
|
|
578
|
+
s_in = 0.11 # arcsec
|
|
579
|
+
|
|
580
|
+
# following make_plot_ln.py
|
|
581
|
+
PS1D_COLORS = ["orange", "darksalmon", "palevioletred", "mediumvioletred", "darkviolet"]
|
|
582
|
+
PS1D_STYLES = ["solid", "dotted", "dashed", "solid", "dashdot"]
|
|
583
|
+
|
|
584
|
+
def __init__(self, outim: OutImage, layer: str) -> None:
|
|
585
|
+
self.outim = outim
|
|
586
|
+
self.layer = layer
|
|
587
|
+
|
|
588
|
+
self.cfg = outim.cfg
|
|
589
|
+
assert layer in ["SCI"] + self.cfg.extrainput[1:], f"Error: layer '{layer}' not found"
|
|
590
|
+
|
|
591
|
+
@classmethod
|
|
592
|
+
def get_norm(cls, layer: str, L: int, filtername: str, s_out: float) -> float:
|
|
593
|
+
"""
|
|
594
|
+
Get norm for 2D noise power spectrum.
|
|
595
|
+
|
|
596
|
+
Parameters
|
|
597
|
+
----------
|
|
598
|
+
layer : str
|
|
599
|
+
Layer name of noise frame to analyze.
|
|
600
|
+
L : int
|
|
601
|
+
Side length of noise frame in px.
|
|
602
|
+
filtername : str
|
|
603
|
+
Name of filter used for this output image.
|
|
604
|
+
s_out : float
|
|
605
|
+
Output pixel scale in arcsec.
|
|
606
|
+
|
|
607
|
+
Returns
|
|
608
|
+
-------
|
|
609
|
+
float
|
|
610
|
+
Norm for 2D noise power spectrum.
|
|
611
|
+
|
|
612
|
+
Notes
|
|
613
|
+
-----
|
|
614
|
+
For simulated noise frames, dividing by s_in**2
|
|
615
|
+
converts from units of S_in^2 to arcsec^2.
|
|
616
|
+
|
|
617
|
+
"""
|
|
618
|
+
|
|
619
|
+
if layer.startswith("white"):
|
|
620
|
+
return (L / s_out) ** 2 # (L * (cls.s_in/s_out)) ** 2
|
|
621
|
+
elif layer.startswith("1f"):
|
|
622
|
+
return (L / s_out) ** 2 # (L * (cls.s_in/s_out)) ** 2
|
|
623
|
+
elif layer.startswith("lab"):
|
|
624
|
+
return (
|
|
625
|
+
cls.tfr
|
|
626
|
+
/ cls.gain
|
|
627
|
+
* cls.ABstd
|
|
628
|
+
/ cls.h
|
|
629
|
+
* cls.AREA[filtername]
|
|
630
|
+
* 10 ** (-0.4 * cls.m_ab)
|
|
631
|
+
* s_out**2
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
@staticmethod
|
|
635
|
+
def azimuthal_average(
|
|
636
|
+
image: np.array, nradbins: int, rbin: np.array = None, ridx: np.array = None
|
|
637
|
+
) -> np.array:
|
|
638
|
+
"""
|
|
639
|
+
Compute radial profile of image.
|
|
640
|
+
|
|
641
|
+
Parameters
|
|
642
|
+
----------
|
|
643
|
+
image : np.array
|
|
644
|
+
Input image, shape (L, L).
|
|
645
|
+
nradbins : int
|
|
646
|
+
Number of radial bins in profile.
|
|
647
|
+
rbin: np.array, optional
|
|
648
|
+
"labels" parameter for ndimage utilities.
|
|
649
|
+
If provided, has shape (L, L).
|
|
650
|
+
The default is None. If not provided, derive from `image`.shape.
|
|
651
|
+
ridx: np.array, optional
|
|
652
|
+
"index" parameter for ndimage utilities.
|
|
653
|
+
If provided, has shape (`nradbins`,).
|
|
654
|
+
The default is None. If not provided, derive from rbin.
|
|
655
|
+
|
|
656
|
+
Returns
|
|
657
|
+
-------
|
|
658
|
+
radial_mean : np.array
|
|
659
|
+
Mean intensity within each annulus. Main result. Shape is (`nradbins`,)
|
|
660
|
+
radial_err : np.array
|
|
661
|
+
Standard error on the mean: sigma / sqrt(N). Shape is (`nradbins`,)
|
|
662
|
+
|
|
663
|
+
"""
|
|
664
|
+
|
|
665
|
+
if rbin is None:
|
|
666
|
+
ny, nx = image.shape
|
|
667
|
+
yy, xx = np.mgrid[:ny, :nx]
|
|
668
|
+
r = np.hypot(xx - nx / 2, yy - ny / 2)
|
|
669
|
+
rbin = (nradbins * r / r.max()).astype(int)
|
|
670
|
+
if ridx is None:
|
|
671
|
+
ridx = np.arange(1, rbin.max() + 1)
|
|
672
|
+
|
|
673
|
+
radial_mean = ndimage.mean(image, labels=rbin, index=ridx)
|
|
674
|
+
radial_stddev = ndimage.standard_deviation(image, labels=rbin, index=ridx)
|
|
675
|
+
npix = ndimage.sum(np.ones_like(image), labels=rbin, index=ridx)
|
|
676
|
+
radial_err = radial_stddev / np.sqrt(npix)
|
|
677
|
+
del npix
|
|
678
|
+
return radial_mean, radial_err
|
|
679
|
+
|
|
680
|
+
@staticmethod
|
|
681
|
+
def _get_wavenumbers(
|
|
682
|
+
window_length: int, nradbins: int, rbin: np.array = None, ridx: np.array = None
|
|
683
|
+
) -> np.array:
|
|
684
|
+
"""
|
|
685
|
+
Calculate wavenumbers for the input image.
|
|
686
|
+
|
|
687
|
+
Parameters
|
|
688
|
+
----------
|
|
689
|
+
window_length : int
|
|
690
|
+
the length of one axis of the image.
|
|
691
|
+
nradbins : int
|
|
692
|
+
number of radial bins the image should be averaged into
|
|
693
|
+
rbin: np.array, optional
|
|
694
|
+
"labels" parameter for ndimage utilities.
|
|
695
|
+
If provided, shape is (L,L).
|
|
696
|
+
The default is None. If not provided, derive from image.shape.
|
|
697
|
+
ridx: np.array, optional
|
|
698
|
+
"index" parameter for ndimage utilities.
|
|
699
|
+
If provided, shape is (`nradbins`,).
|
|
700
|
+
The default is None. If not provided, derive from `rbin`.
|
|
701
|
+
|
|
702
|
+
Returns
|
|
703
|
+
-------
|
|
704
|
+
kmean : np.array
|
|
705
|
+
the wavenumbers for the image, shape (`nradbins`,)
|
|
706
|
+
|
|
707
|
+
"""
|
|
708
|
+
|
|
709
|
+
k = np.fft.fftshift(np.fft.fftfreq(window_length))
|
|
710
|
+
kx, ky = np.meshgrid(k, k)
|
|
711
|
+
k = np.sqrt(np.square(kx) + np.square(ky))
|
|
712
|
+
|
|
713
|
+
if rbin is None or ridx is None:
|
|
714
|
+
kmean, kerr = NoiseAnal.azimuthal_average(k, nradbins)
|
|
715
|
+
else:
|
|
716
|
+
kmean = ndimage.mean(k, labels=rbin, index=ridx)
|
|
717
|
+
return kmean
|
|
718
|
+
|
|
719
|
+
def __call__(
|
|
720
|
+
self, padding: bool = False, bin_: bool = True, rbin: np.array = None, ridx: np.array = None
|
|
721
|
+
) -> None:
|
|
722
|
+
"""
|
|
723
|
+
Analyze specified noise frame of given output image.
|
|
724
|
+
|
|
725
|
+
Parameters
|
|
726
|
+
----------
|
|
727
|
+
padding : bool, optional
|
|
728
|
+
Whether to include padding postage stamps. (to be implemented)
|
|
729
|
+
bin_ : bool, optional
|
|
730
|
+
Whether to bin the 2D spectrum into L/8 x L/8 image.
|
|
731
|
+
Currently this is ignored, as only bin_ == True is supported.
|
|
732
|
+
rbin: np.array, optional, shape : (L, L)
|
|
733
|
+
"labels" parameter for ndimage utilities.
|
|
734
|
+
The default is None. If not provided, derive from image.shape.
|
|
735
|
+
ridx: np.array, optional, shape : (nradbins,)
|
|
736
|
+
"index" parameter for ndimage utilities.
|
|
737
|
+
The default is None. If not provided, derive from rbin.
|
|
738
|
+
|
|
739
|
+
Returns
|
|
740
|
+
-------
|
|
741
|
+
None
|
|
742
|
+
|
|
743
|
+
Notes
|
|
744
|
+
-----
|
|
745
|
+
If the image side length is not a multiple of 8, the extra pixels (`L` // 8)
|
|
746
|
+
are clipped.
|
|
747
|
+
|
|
748
|
+
"""
|
|
749
|
+
|
|
750
|
+
L = self.cfg.NsideP # side length in px
|
|
751
|
+
indata = self.outim.get_coadded_layer(self.layer)
|
|
752
|
+
if not padding:
|
|
753
|
+
L = self.cfg.Nside
|
|
754
|
+
# padding region around the edge
|
|
755
|
+
bdpad = self.cfg.n2 * self.cfg.postage_pad
|
|
756
|
+
indata = indata[bdpad:-bdpad, bdpad:-bdpad]
|
|
757
|
+
|
|
758
|
+
s_out = self.cfg.dtheta * u.degree.to("arcsec") # in arcsec
|
|
759
|
+
Lcut = L // 8 * 8 # "extra" pixels will be trimmed to get a multiple of 8
|
|
760
|
+
norm = NoiseAnal.get_norm(self.layer, Lcut, Stn.RomanFilters[self.cfg.use_filter], s_out)
|
|
761
|
+
|
|
762
|
+
# Measure the 2D power spectrum of image.
|
|
763
|
+
ps = np.empty((Lcut, Lcut), dtype=np.float64)
|
|
764
|
+
rps = np.square(np.abs(np.fft.fftshift(np.fft.rfft2(indata[:Lcut, :Lcut]), 0))) / norm
|
|
765
|
+
ps[:, Lcut // 2 :] = rps[:, :-1]
|
|
766
|
+
ps[1:, : Lcut // 2] = rps[Lcut - 1 : 0 : -1, Lcut // 2 : 0 : -1]
|
|
767
|
+
ps[0, : Lcut // 2] = rps[0, Lcut // 2 : 0 : -1]
|
|
768
|
+
self.ps2d = np.average(np.reshape(ps, (Lcut // 8, 8, Lcut // 8, 8)), axis=(1, 3))
|
|
769
|
+
del rps, ps
|
|
770
|
+
|
|
771
|
+
# Calculate the azimuthally-averaged 1D power spectrum of the image.
|
|
772
|
+
nradbins = (
|
|
773
|
+
Lcut // 16
|
|
774
|
+
) # Number of radial bins is side length div. into 8 from binning and then (floor) div. by 2.
|
|
775
|
+
ps_1d, ps_image_err = NoiseAnal.azimuthal_average(self.ps2d, nradbins, rbin, ridx)
|
|
776
|
+
# wavenumbers = NoiseAnal._get_wavenumbers(Lcut, nradbins)
|
|
777
|
+
|
|
778
|
+
self.ps1d = np.zeros((Lcut // 16, 2))
|
|
779
|
+
# self.ps1d[:, 0] = wavenumbers # powerspectrum.k
|
|
780
|
+
self.ps1d[:, 0] = ps_1d # powerspectrum.ps_image
|
|
781
|
+
self.ps1d[:, 1] = ps_image_err # powerspectrum.ps_image_err
|
|
782
|
+
|
|
783
|
+
def clear(self) -> None:
|
|
784
|
+
"""Free up memory space."""
|
|
785
|
+
|
|
786
|
+
if hasattr(self, "ps2d"):
|
|
787
|
+
del self.ps2d, self.ps1d
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
# diagnostics/starcube_nonoise_coldescr.txt
|
|
791
|
+
|
|
792
|
+
ColDescr = Enum(
|
|
793
|
+
"ColDescr",
|
|
794
|
+
[
|
|
795
|
+
"RA", # 0: right ascension
|
|
796
|
+
"DEC", # 1: declination
|
|
797
|
+
# 'BLOCK_IX', # 2: block ix
|
|
798
|
+
# 'BLOCK_IY', # 3: block iy
|
|
799
|
+
"X_POS", # 4: x position in block image (float)
|
|
800
|
+
"Y_POS", # 5: y position in block image (float)
|
|
801
|
+
# 'X_INT', # 6: int part of x
|
|
802
|
+
# 'Y_INT', # 7: int part of y
|
|
803
|
+
# 'X_FRAC', # 8: frac part of x
|
|
804
|
+
# 'Y_FRAC', # 9: frac part of y
|
|
805
|
+
"AMPLITUDE", # 10: star fit -> amplitude
|
|
806
|
+
"OFFSET_X", # 11: star fit -> centroid offset (in output pixels), x
|
|
807
|
+
"OFFSET_Y", # 12: star fit -> centroid offset (in output pixels), y
|
|
808
|
+
"WIDTH", # 13: star fit -> sigma (in output pixels)
|
|
809
|
+
"SHAPE_G1", # 14: star fit -> g1 shape
|
|
810
|
+
"SHAPE_G2", # 15: star fit -> g2 shape
|
|
811
|
+
"M42_REAL", # 16: star 4th moment Re M42 (Zhang et al. 2023 MNRAS 525, 2441 convention)
|
|
812
|
+
"M42_IMAG", # 17: star 4th moment Im M42
|
|
813
|
+
"FORCED_PLUS", # 18: star moment with forced sigma=0.40 arcsec scale length [+ component] (in units of forced sigma**2) # noqa: E501
|
|
814
|
+
"FORCED_CROSS", # 19: star moment with forced sigma=0.40 arcsec scale length [x component]
|
|
815
|
+
"FIDELITY", # 20: fidelity (mean in 0.5 arcsec box)
|
|
816
|
+
"COVERAGE", # 21: coverage at star center
|
|
817
|
+
"MEAN_UC", # new: mean PSF leakage U/C (in linear space)
|
|
818
|
+
"MEAN_SIGMA", # new: mean noise amplification Sigma
|
|
819
|
+
"STD_TSUM", # new: standard deviation of total weight
|
|
820
|
+
"MEAN_NEFF", # new: mean effective coverage
|
|
821
|
+
],
|
|
822
|
+
start=0,
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
class StarsAnal:
|
|
827
|
+
"""
|
|
828
|
+
Analysis of point sources.
|
|
829
|
+
|
|
830
|
+
Largely based on diagnostics/starcube_nonoise.py.
|
|
831
|
+
|
|
832
|
+
Parameters
|
|
833
|
+
----------
|
|
834
|
+
outim : OutImage
|
|
835
|
+
Output image to analyze.
|
|
836
|
+
layer : str, optional
|
|
837
|
+
Layer name of injected stars to analyze.
|
|
838
|
+
|
|
839
|
+
Methods
|
|
840
|
+
-------
|
|
841
|
+
__init__
|
|
842
|
+
Constructor.
|
|
843
|
+
__call__
|
|
844
|
+
Analyze given point source frame of given output image.
|
|
845
|
+
clear
|
|
846
|
+
Free up memory space.
|
|
847
|
+
|
|
848
|
+
"""
|
|
849
|
+
|
|
850
|
+
bd = 40 # padding size
|
|
851
|
+
bd2 = 8
|
|
852
|
+
ncol = len(ColDescr)
|
|
853
|
+
|
|
854
|
+
def __init__(self, outim: OutImage, layer: str = "gsstar14") -> None:
|
|
855
|
+
self.outim = outim
|
|
856
|
+
self.layer = layer
|
|
857
|
+
assert layer == "gsstar14", "Error: currently only 'gsstar14' is supported"
|
|
858
|
+
|
|
859
|
+
self.cfg = outim.cfg
|
|
860
|
+
assert layer in ["SCI"] + self.cfg.extrainput[1:], f"Error: layer '{layer}' not found"
|
|
861
|
+
|
|
862
|
+
def __call__(
|
|
863
|
+
self,
|
|
864
|
+
n: int = None,
|
|
865
|
+
search_radius: float = None,
|
|
866
|
+
forced_scale: float = None,
|
|
867
|
+
bdpad: int = None,
|
|
868
|
+
res: int = None,
|
|
869
|
+
) -> None:
|
|
870
|
+
"""
|
|
871
|
+
Analyze given point source frame of given output image.
|
|
872
|
+
|
|
873
|
+
Parameters
|
|
874
|
+
----------
|
|
875
|
+
n : int or None, optional
|
|
876
|
+
Size of output images.
|
|
877
|
+
If not provided, derive from self.cfg. Same for other parameters.
|
|
878
|
+
search_radius : float or None, optional
|
|
879
|
+
Search radius for injected point sources.
|
|
880
|
+
forced_scale : float or None, optional
|
|
881
|
+
Forced scale length for star moments.
|
|
882
|
+
bdpad : int or None, optional
|
|
883
|
+
Padding region around the edge.
|
|
884
|
+
res : int or None, optional
|
|
885
|
+
Resolution of HEALPix grid.
|
|
886
|
+
|
|
887
|
+
Returns
|
|
888
|
+
-------
|
|
889
|
+
None
|
|
890
|
+
|
|
891
|
+
"""
|
|
892
|
+
|
|
893
|
+
if None in [n, search_radius, forced_scale, bdpad, res]:
|
|
894
|
+
n = self.cfg.NsideP # size of output images
|
|
895
|
+
blocksize = self.cfg.n1 * self.cfg.n2 * self.cfg.dtheta * Stn.degree # radians
|
|
896
|
+
search_radius = 1.5 * blocksize / np.sqrt(2.0) # search radius
|
|
897
|
+
forced_scale = 0.40 * u.arcsec.to("degree") / self.cfg.dtheta # in output pixels
|
|
898
|
+
bdpad = self.cfg.n2 * self.cfg.postage_pad # padding region around the edge
|
|
899
|
+
res = int(re.match(r"^gsstar(\d+)$", self.layer).group(1))
|
|
900
|
+
# print(n, search_radius, forced_scale, bdpad, res)
|
|
901
|
+
|
|
902
|
+
data_loaded = hasattr(self.outim, "hdu_list")
|
|
903
|
+
if not data_loaded:
|
|
904
|
+
self.outim._load_or_save_hdu_list(True)
|
|
905
|
+
|
|
906
|
+
f = self.outim.hdu_list # alias
|
|
907
|
+
use_slice = (["SCI"] + self.cfg.extrainput[1:]).index(self.layer)
|
|
908
|
+
|
|
909
|
+
mywcs = wcs.WCS(f[0].header)
|
|
910
|
+
map_ = f[0].data[0, use_slice, :, :]
|
|
911
|
+
wt = np.sum(np.where(f["INWEIGHT"].data[0, :, :, :] > 0.01, 1, 0), axis=0)
|
|
912
|
+
fmap = (
|
|
913
|
+
f["FIDELITY"].data[0, :, :].astype(np.float32) * HDU_to_bels(f["FIDELITY"]) / (-0.1)
|
|
914
|
+
) # convert to dB, inverse scale
|
|
915
|
+
fmap = np.floor(fmap).astype(np.int16) # and round to integer
|
|
916
|
+
del f
|
|
917
|
+
|
|
918
|
+
outmaps = self.cfg.outmaps # shortcut
|
|
919
|
+
if "U" in outmaps:
|
|
920
|
+
UC_map = self.outim.get_output_map("FIDELITY")
|
|
921
|
+
if "S" in outmaps:
|
|
922
|
+
Sigma_map = self.outim.get_output_map("SIGMA")
|
|
923
|
+
if "T" in outmaps:
|
|
924
|
+
Tsum_map = self.outim.get_output_map("INWTSUM")
|
|
925
|
+
if "N" in outmaps:
|
|
926
|
+
Neff_map = self.outim.get_output_map("EFFCOVER")
|
|
927
|
+
|
|
928
|
+
if not data_loaded:
|
|
929
|
+
self.outim._load_or_save_hdu_list(False)
|
|
930
|
+
|
|
931
|
+
ra_cent, dec_cent = mywcs.all_pix2world(
|
|
932
|
+
[(n - 1) / 2], [(n - 1) / 2], [0.0], [0.0], 0, ra_dec_order=True
|
|
933
|
+
)
|
|
934
|
+
ra_cent = ra_cent[0]
|
|
935
|
+
dec_cent = dec_cent[0]
|
|
936
|
+
vec = healpy.ang2vec(ra_cent, dec_cent, lonlat=True)
|
|
937
|
+
qp = healpy.query_disc(2**res, vec, search_radius, nest=False)
|
|
938
|
+
ra_hpix, dec_hpix = healpy.pix2ang(2**res, qp, nest=False, lonlat=True)
|
|
939
|
+
npix = len(ra_hpix)
|
|
940
|
+
x, y, z1, z2 = mywcs.all_world2pix(ra_hpix, dec_hpix, np.zeros((npix,)), np.zeros((npix,)), 0)
|
|
941
|
+
xi = np.rint(x).astype(np.int16)
|
|
942
|
+
yi = np.rint(y).astype(np.int16)
|
|
943
|
+
grp = np.where(
|
|
944
|
+
np.logical_and(
|
|
945
|
+
np.logical_and(xi >= bdpad, xi < n - bdpad), np.logical_and(yi >= bdpad, yi < n - bdpad)
|
|
946
|
+
)
|
|
947
|
+
)
|
|
948
|
+
ra_hpix = ra_hpix[grp]
|
|
949
|
+
dec_hpix = dec_hpix[grp]
|
|
950
|
+
x = x[grp]
|
|
951
|
+
y = y[grp]
|
|
952
|
+
npix = len(x)
|
|
953
|
+
del vec, qp, z1, z2, grp
|
|
954
|
+
|
|
955
|
+
self.sub_cat = np.zeros((npix, StarsAnal.ncol))
|
|
956
|
+
xi = np.rint(x).astype(np.int16)
|
|
957
|
+
yi = np.rint(y).astype(np.int16)
|
|
958
|
+
# position information
|
|
959
|
+
self.sub_cat[:, ColDescr.RA.value] = ra_hpix
|
|
960
|
+
self.sub_cat[:, ColDescr.DEC.value] = dec_hpix
|
|
961
|
+
# self.sub_cat[:, ColDescr.BLOCK_IX.value] = self.outim.ibx
|
|
962
|
+
# self.sub_cat[:, ColDescr.BLOCK_IY.value] = self.outim.iby
|
|
963
|
+
self.sub_cat[:, ColDescr.X_POS.value] = x
|
|
964
|
+
self.sub_cat[:, ColDescr.Y_POS.value] = y
|
|
965
|
+
# self.sub_cat[:, ColDescr.X_INT .value] = xi
|
|
966
|
+
# self.sub_cat[:, ColDescr.Y_INT .value] = yi
|
|
967
|
+
dx = x - xi # self.sub_cat[:, ColDescr.X_FRAC .value] = dx = x-xi
|
|
968
|
+
dy = y - yi # self.sub_cat[:, ColDescr.Y_FRAC .value] = dy = y-yi
|
|
969
|
+
del ra_hpix, dec_hpix
|
|
970
|
+
|
|
971
|
+
bd = StarsAnal.bd # shortcut
|
|
972
|
+
# print(self.outim.ibx, self.outim.iby, self.outim.fpath, npix)
|
|
973
|
+
print(npix, end=" ")
|
|
974
|
+
for k in range(npix):
|
|
975
|
+
newimage = map_[yi[k] + 1 - bd : yi[k] + bd, xi[k] + 1 - bd : xi[k] + bd]
|
|
976
|
+
|
|
977
|
+
# PSF shape
|
|
978
|
+
moms = galsim.Image(newimage).FindAdaptiveMom(strict=False)
|
|
979
|
+
if moms.error_message != "":
|
|
980
|
+
continue
|
|
981
|
+
|
|
982
|
+
self.sub_cat[k, ColDescr.AMPLITUDE.value] = moms.moments_amp
|
|
983
|
+
self.sub_cat[k, ColDescr.OFFSET_X.value] = moms.moments_centroid.x - bd - dx[k]
|
|
984
|
+
self.sub_cat[k, ColDescr.OFFSET_Y.value] = moms.moments_centroid.y - bd - dy[k]
|
|
985
|
+
self.sub_cat[k, ColDescr.WIDTH.value] = moms.moments_sigma
|
|
986
|
+
self.sub_cat[k, ColDescr.SHAPE_G1.value] = moms.observed_shape.g1
|
|
987
|
+
self.sub_cat[k, ColDescr.SHAPE_G2.value] = moms.observed_shape.g2
|
|
988
|
+
|
|
989
|
+
# higher moments
|
|
990
|
+
x_, y_ = np.meshgrid(
|
|
991
|
+
np.arange(1, bd * 2) - moms.moments_centroid.x, np.arange(1, bd * 2) - moms.moments_centroid.y
|
|
992
|
+
)
|
|
993
|
+
e1 = moms.observed_shape.e1
|
|
994
|
+
e2 = moms.observed_shape.e2
|
|
995
|
+
Mxx = moms.moments_sigma**2 * (1 + e1) / np.sqrt(1 - e1**2 - e2**2)
|
|
996
|
+
Myy = moms.moments_sigma**2 * (1 - e1) / np.sqrt(1 - e1**2 - e2**2)
|
|
997
|
+
Mxy = moms.moments_sigma**2 * e2 / np.sqrt(1 - e1**2 - e2**2)
|
|
998
|
+
D = Mxx * Myy - Mxy**2
|
|
999
|
+
zeta = D * (Mxx + Myy + 2 * np.sqrt(D))
|
|
1000
|
+
u_ = ((Myy + np.sqrt(D)) * x_ - Mxy * y_) / zeta**0.5
|
|
1001
|
+
v_ = ((Mxx + np.sqrt(D)) * y_ - Mxy * x_) / zeta**0.5
|
|
1002
|
+
wti = newimage * np.exp(-0.5 * (u_**2 + v_**2))
|
|
1003
|
+
self.sub_cat[k, ColDescr.M42_REAL.value] = np.sum(wti * (u_**4 - v_**4)) / np.sum(wti)
|
|
1004
|
+
self.sub_cat[k, ColDescr.M42_IMAG.value] = (
|
|
1005
|
+
2 * np.sum(wti * (u_**3 * v_ + u_ * v_**3)) / np.sum(wti)
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
# moments with forced scale length
|
|
1009
|
+
wti2 = newimage * np.exp(-0.5 * (x_**2 + y_**2) / forced_scale**2)
|
|
1010
|
+
self.sub_cat[k, ColDescr.FORCED_PLUS.value] = (
|
|
1011
|
+
np.sum(wti2 * (x_**2 - y_**2)) / np.sum(wti2) / forced_scale**2
|
|
1012
|
+
)
|
|
1013
|
+
self.sub_cat[k, ColDescr.FORCED_CROSS.value] = (
|
|
1014
|
+
np.sum(wti2 * (2 * x_ * y_)) / np.sum(wti2) / forced_scale**2
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
# fidelity and coverage
|
|
1018
|
+
central = np.s_[
|
|
1019
|
+
yi[k] + 1 - StarsAnal.bd2 : yi[k] + StarsAnal.bd2,
|
|
1020
|
+
xi[k] + 1 - StarsAnal.bd2 : xi[k] + StarsAnal.bd2,
|
|
1021
|
+
] # central region of the star
|
|
1022
|
+
self.sub_cat[k, ColDescr.FIDELITY.value] = np.mean(fmap[central])
|
|
1023
|
+
self.sub_cat[k, ColDescr.COVERAGE.value] = wt[yi[k] // self.cfg.n2, xi[k] // self.cfg.n2]
|
|
1024
|
+
|
|
1025
|
+
# new columns based on output maps
|
|
1026
|
+
self.sub_cat[k, ColDescr.MEAN_UC.value] = np.mean(UC_map[central]) if "U" in outmaps else -1
|
|
1027
|
+
self.sub_cat[k, ColDescr.MEAN_SIGMA.value] = np.mean(Sigma_map[central]) if "S" in outmaps else -1
|
|
1028
|
+
self.sub_cat[k, ColDescr.STD_TSUM.value] = np.std(Tsum_map[central]) if "T" in outmaps else -1
|
|
1029
|
+
if self.cfg.linear_algebra == "Empirical":
|
|
1030
|
+
self.sub_cat[k, ColDescr.STD_TSUM.value] = 0
|
|
1031
|
+
self.sub_cat[k, ColDescr.MEAN_NEFF.value] = np.mean(Neff_map[central]) if "N" in outmaps else -1
|
|
1032
|
+
|
|
1033
|
+
del newimage, x_, y_, u_, v_, wti, wti2
|
|
1034
|
+
|
|
1035
|
+
del map_, wt, fmap
|
|
1036
|
+
del x, y, xi, yi, dx, dy
|
|
1037
|
+
|
|
1038
|
+
if "U" in outmaps:
|
|
1039
|
+
del UC_map
|
|
1040
|
+
if "S" in outmaps:
|
|
1041
|
+
del Sigma_map
|
|
1042
|
+
if "T" in outmaps:
|
|
1043
|
+
del Tsum_map
|
|
1044
|
+
if "N" in outmaps:
|
|
1045
|
+
del Neff_map
|
|
1046
|
+
|
|
1047
|
+
def clear(self) -> None:
|
|
1048
|
+
"""
|
|
1049
|
+
Free up memory space.
|
|
1050
|
+
|
|
1051
|
+
Returns
|
|
1052
|
+
-------
|
|
1053
|
+
None.
|
|
1054
|
+
|
|
1055
|
+
"""
|
|
1056
|
+
|
|
1057
|
+
if hasattr(self, "sub_cat"):
|
|
1058
|
+
del self.sub_cat
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
class _BlkGrp:
|
|
1062
|
+
"""
|
|
1063
|
+
Abstract base class for groups of blocks (mosiacs or suites).
|
|
1064
|
+
|
|
1065
|
+
Methods
|
|
1066
|
+
-------
|
|
1067
|
+
__call__
|
|
1068
|
+
Run all the analyses below.
|
|
1069
|
+
get_consump_map
|
|
1070
|
+
Get map of time consumption.
|
|
1071
|
+
get_coverage_map
|
|
1072
|
+
Get map of mean coverages.
|
|
1073
|
+
get_noise_power_spectra
|
|
1074
|
+
Analyze noise power spectra of this mosaic.
|
|
1075
|
+
get_star_catalog
|
|
1076
|
+
Analyze injected point sources of this mosaic.
|
|
1077
|
+
clear
|
|
1078
|
+
Free up memory space.
|
|
1079
|
+
|
|
1080
|
+
"""
|
|
1081
|
+
|
|
1082
|
+
def __call__(self, overwrite: bool = False) -> None:
|
|
1083
|
+
"""
|
|
1084
|
+
Run all the analyses below.
|
|
1085
|
+
|
|
1086
|
+
Parameters
|
|
1087
|
+
----------
|
|
1088
|
+
overwrite : bool, optional
|
|
1089
|
+
Whether to overwrite existing results.
|
|
1090
|
+
|
|
1091
|
+
Returns
|
|
1092
|
+
-------
|
|
1093
|
+
None
|
|
1094
|
+
|
|
1095
|
+
"""
|
|
1096
|
+
|
|
1097
|
+
self.get_consump_map(overwrite=overwrite) # Get map of time consumption.
|
|
1098
|
+
self.get_coverage_map(overwrite=overwrite) # Get map of mean coverages.
|
|
1099
|
+
self.get_noise_power_spectra(overwrite=overwrite) # Analyze noise power spectra of this mosaic.
|
|
1100
|
+
self.get_star_catalog(overwrite=overwrite) # Analyze injected point sources of this mosaic.
|
|
1101
|
+
|
|
1102
|
+
def get_consump_map(self, overwrite: bool = False) -> None:
|
|
1103
|
+
"""
|
|
1104
|
+
Get map of time consumption.
|
|
1105
|
+
|
|
1106
|
+
Parameters
|
|
1107
|
+
----------
|
|
1108
|
+
overwrite : bool, optional
|
|
1109
|
+
Whether to overwrite existing results.
|
|
1110
|
+
|
|
1111
|
+
Returns
|
|
1112
|
+
-------
|
|
1113
|
+
None
|
|
1114
|
+
|
|
1115
|
+
"""
|
|
1116
|
+
|
|
1117
|
+
fname = self.cfg.outstem + "_Consump.npy"
|
|
1118
|
+
if not overwrite and exists(fname):
|
|
1119
|
+
with open(fname, "rb") as f:
|
|
1120
|
+
self.consump_map = np.load(f)
|
|
1121
|
+
return
|
|
1122
|
+
|
|
1123
|
+
if self.ndim == 2: # Mosaic
|
|
1124
|
+
nblock = self.cfg.nblock # shortcut
|
|
1125
|
+
self.consump_map = np.zeros((nblock, nblock))
|
|
1126
|
+
for iby in range(nblock):
|
|
1127
|
+
for ibx in range(nblock):
|
|
1128
|
+
self.consump_map[iby][ibx] = self.outimages[iby][ibx].get_time_consump()
|
|
1129
|
+
|
|
1130
|
+
elif self.ndim == 1: # Suite
|
|
1131
|
+
nrun = self.nrun
|
|
1132
|
+
self.consump_map = np.zeros((nrun,))
|
|
1133
|
+
for ib in range(nrun):
|
|
1134
|
+
self.consump_map[ib] = self.outimages[ib].get_time_consump()
|
|
1135
|
+
|
|
1136
|
+
with open(fname, "wb") as f:
|
|
1137
|
+
np.save(f, self.consump_map)
|
|
1138
|
+
|
|
1139
|
+
def get_coverage_map(self, overwrite: bool = False) -> None:
|
|
1140
|
+
"""
|
|
1141
|
+
Get map of mean coverages.
|
|
1142
|
+
|
|
1143
|
+
Parameters
|
|
1144
|
+
----------
|
|
1145
|
+
overwrite : bool, optional
|
|
1146
|
+
Whether to overwrite existing results.
|
|
1147
|
+
|
|
1148
|
+
Returns
|
|
1149
|
+
-------
|
|
1150
|
+
None
|
|
1151
|
+
|
|
1152
|
+
"""
|
|
1153
|
+
|
|
1154
|
+
fname = self.cfg.outstem + "_Coverage.npy"
|
|
1155
|
+
if not overwrite and exists(fname):
|
|
1156
|
+
with open(fname, "rb") as f:
|
|
1157
|
+
self.coverage_map = np.load(f)
|
|
1158
|
+
return
|
|
1159
|
+
|
|
1160
|
+
if self.ndim == 2: # Mosaic
|
|
1161
|
+
nblock = self.cfg.nblock # shortcut
|
|
1162
|
+
self.coverage_map = np.zeros((nblock, nblock))
|
|
1163
|
+
for iby in range(nblock):
|
|
1164
|
+
for ibx in range(nblock):
|
|
1165
|
+
self.coverage_map[iby][ibx] = self.outimages[iby][ibx].get_mean_coverage()
|
|
1166
|
+
|
|
1167
|
+
elif self.ndim == 1: # Suite
|
|
1168
|
+
nrun = self.nrun
|
|
1169
|
+
self.coverage_map = np.zeros((nrun,))
|
|
1170
|
+
for ib in range(nrun):
|
|
1171
|
+
self.coverage_map[ib] = self.outimages[ib].get_mean_coverage()
|
|
1172
|
+
|
|
1173
|
+
with open(fname, "wb") as f:
|
|
1174
|
+
np.save(f, self.coverage_map)
|
|
1175
|
+
|
|
1176
|
+
def get_noise_power_spectra(self, bins: int = 5, overwrite: bool = False) -> None:
|
|
1177
|
+
"""
|
|
1178
|
+
Analyze noise power spectra of this mosaic.
|
|
1179
|
+
|
|
1180
|
+
The output noise power spectra are written to ``self.cfg.outstem + "_NoisePS.npz"``. These
|
|
1181
|
+
are in the format:
|
|
1182
|
+
|
|
1183
|
+
* `ps2d_all` = 2D power spectrum, [which_noise_layer, ybin, xbin]
|
|
1184
|
+
* `ps1d_all` = 1D power spectrum, [which_noise_layer, coverage_bin, wavenumber_bin, value_or_err]
|
|
1185
|
+
* `wavenumber` = 1D array of wavenumbers
|
|
1186
|
+
|
|
1187
|
+
Parameters
|
|
1188
|
+
----------
|
|
1189
|
+
bins : int, optional
|
|
1190
|
+
Number of coverage bins for 1D power spectra.
|
|
1191
|
+
overwrite : bool, optional
|
|
1192
|
+
Whether to overwrite existing results.
|
|
1193
|
+
|
|
1194
|
+
Returns
|
|
1195
|
+
-------
|
|
1196
|
+
None
|
|
1197
|
+
|
|
1198
|
+
"""
|
|
1199
|
+
|
|
1200
|
+
fname = self.cfg.outstem + "_NoisePS.npz"
|
|
1201
|
+
if not overwrite and exists(fname):
|
|
1202
|
+
with np.load(fname) as f:
|
|
1203
|
+
self.ps2d_all = f["ps2d_all"]
|
|
1204
|
+
self.ps1d_all = f["ps1d_all"]
|
|
1205
|
+
self.wavenumbers = f["wavenumbers"]
|
|
1206
|
+
return
|
|
1207
|
+
|
|
1208
|
+
timer = Timer()
|
|
1209
|
+
|
|
1210
|
+
# identify noise layers
|
|
1211
|
+
noiseinput = [layer for layer in self.cfg.extrainput[1:] if "noise" in layer]
|
|
1212
|
+
n_innoise = len(noiseinput)
|
|
1213
|
+
print(noiseinput)
|
|
1214
|
+
|
|
1215
|
+
# mean coverage bins
|
|
1216
|
+
if not hasattr(self, "coverage_map"):
|
|
1217
|
+
self.get_coverage_map()
|
|
1218
|
+
|
|
1219
|
+
mc_max = self.coverage_map.max() + 1e-12
|
|
1220
|
+
mc_min = self.coverage_map.min() - 1e-12
|
|
1221
|
+
coverage_idx = ((self.coverage_map - mc_min) / (mc_max - mc_min) * bins).astype(np.uint8)
|
|
1222
|
+
unique, counts = np.unique(coverage_idx, return_counts=True)
|
|
1223
|
+
|
|
1224
|
+
# create storage
|
|
1225
|
+
L = self.cfg.Nside if not self.padding else self.cfg.NsideP
|
|
1226
|
+
|
|
1227
|
+
self.ps2d_all = np.zeros((n_innoise, L // 8, L // 8))
|
|
1228
|
+
self.ps1d_all = np.zeros((n_innoise, bins, L // 16, 2))
|
|
1229
|
+
|
|
1230
|
+
# derive rbin and ridx for NoiseAnal.azimuthal_average
|
|
1231
|
+
nradbins = (
|
|
1232
|
+
L // 16
|
|
1233
|
+
) # Number of radial bins is side length div. into 8 from binning and then (floor) div. by 2.
|
|
1234
|
+
yy, xx = np.mgrid[: L // 8, : L // 8]
|
|
1235
|
+
r = np.hypot(xx - L // 8 / 2, yy - L // 8 / 2)
|
|
1236
|
+
rbin = (nradbins * r / r.max()).astype(int)
|
|
1237
|
+
ridx = np.arange(1, rbin.max() + 1)
|
|
1238
|
+
del yy, xx, r
|
|
1239
|
+
|
|
1240
|
+
self.wavenumbers = NoiseAnal._get_wavenumbers(L, nradbins)
|
|
1241
|
+
# dividing by s_out converts from units of cyc/s_out to cyc/arcsec
|
|
1242
|
+
self.wavenumbers /= self.cfg.dtheta * u.degree.to("arcsec")
|
|
1243
|
+
|
|
1244
|
+
# loop over noise layers and output images
|
|
1245
|
+
if self.ndim == 2: # Mosaic
|
|
1246
|
+
for iby in range(self.cfg.nblock):
|
|
1247
|
+
print(f" > row {iby:2d} t= {timer():9.2f} s")
|
|
1248
|
+
for inl, layer in enumerate(noiseinput):
|
|
1249
|
+
for ibx in range(self.cfg.nblock):
|
|
1250
|
+
noise = NoiseAnal(self.outimages[iby][ibx], layer)
|
|
1251
|
+
noise(padding=self.padding, rbin=rbin, ridx=ridx)
|
|
1252
|
+
self.ps2d_all[inl, :, :] += noise.ps2d
|
|
1253
|
+
self.ps1d_all[inl, coverage_idx[iby][ibx], :, :] += noise.ps1d[:, :]
|
|
1254
|
+
noise.clear()
|
|
1255
|
+
del noise
|
|
1256
|
+
|
|
1257
|
+
elif self.ndim == 1: # Suite
|
|
1258
|
+
nrun = self.nrun
|
|
1259
|
+
for inl, layer in enumerate(noiseinput):
|
|
1260
|
+
for ib in range(nrun):
|
|
1261
|
+
noise = NoiseAnal(self.outimages[ib], layer)
|
|
1262
|
+
noise(padding=self.padding, rbin=rbin, ridx=ridx)
|
|
1263
|
+
self.ps2d_all[inl, :, :] += noise.ps2d
|
|
1264
|
+
self.ps1d_all[inl, coverage_idx[ib], :, :] += noise.ps1d[:, :]
|
|
1265
|
+
noise.clear()
|
|
1266
|
+
del noise
|
|
1267
|
+
|
|
1268
|
+
del rbin, ridx
|
|
1269
|
+
|
|
1270
|
+
# postprocessing
|
|
1271
|
+
if self.ndim == 2: # Mosaic
|
|
1272
|
+
self.ps2d_all /= self.cfg.nblock**2
|
|
1273
|
+
elif self.ndim == 1: # Suite
|
|
1274
|
+
self.ps2d_all /= self.nrun
|
|
1275
|
+
|
|
1276
|
+
for idx, count in zip(unique, counts, strict=False):
|
|
1277
|
+
self.ps1d_all[:, idx, :, :] /= count
|
|
1278
|
+
del coverage_idx, counts
|
|
1279
|
+
|
|
1280
|
+
np.savez(fname, ps2d_all=self.ps2d_all, ps1d_all=self.ps1d_all, wavenumbers=self.wavenumbers)
|
|
1281
|
+
print(f"finished at t = {timer():.2f} s")
|
|
1282
|
+
|
|
1283
|
+
def get_star_catalog(self, layer: str = "gsstar14", overwrite: bool = False) -> None:
|
|
1284
|
+
"""
|
|
1285
|
+
Analyze injected point sources of this mosaic.
|
|
1286
|
+
|
|
1287
|
+
Parameters
|
|
1288
|
+
----------
|
|
1289
|
+
layer : str, optional
|
|
1290
|
+
Layer name of injected stars to analyze.
|
|
1291
|
+
overwrite : bool, optional
|
|
1292
|
+
Whether to overwrite existing results.
|
|
1293
|
+
|
|
1294
|
+
Returns
|
|
1295
|
+
-------
|
|
1296
|
+
None
|
|
1297
|
+
|
|
1298
|
+
"""
|
|
1299
|
+
|
|
1300
|
+
fname = self.cfg.outstem + "_StarCat.npy"
|
|
1301
|
+
if not overwrite and exists(fname):
|
|
1302
|
+
with open(fname, "rb") as f:
|
|
1303
|
+
self.star_cat = np.load(f)
|
|
1304
|
+
return
|
|
1305
|
+
|
|
1306
|
+
timer = Timer()
|
|
1307
|
+
self.star_cat = np.zeros((0, StarsAnal.ncol))
|
|
1308
|
+
|
|
1309
|
+
n = self.cfg.NsideP # size of output images
|
|
1310
|
+
blocksize = self.cfg.n1 * self.cfg.n2 * self.cfg.dtheta * Stn.degree # radians
|
|
1311
|
+
search_radius = 1.5 * blocksize / np.sqrt(2.0) # search radius
|
|
1312
|
+
forced_scale = 0.40 * u.arcsec.to("degree") / self.cfg.dtheta # in output pixels
|
|
1313
|
+
bdpad = self.cfg.n2 * self.cfg.postage_pad # padding region around the edge
|
|
1314
|
+
res = int(re.match(r"^gsstar(\d+)$", layer).group(1))
|
|
1315
|
+
# print(n, search_radius, forced_scale, bdpad, res)
|
|
1316
|
+
|
|
1317
|
+
# loop over output images
|
|
1318
|
+
if self.ndim == 2: # Mosaic
|
|
1319
|
+
for iby in range(self.cfg.nblock):
|
|
1320
|
+
print(f" > row {iby:2d} t= {timer():9.2f} s")
|
|
1321
|
+
|
|
1322
|
+
print("star counts:", end=" ")
|
|
1323
|
+
for ibx in range(self.cfg.nblock):
|
|
1324
|
+
stars = StarsAnal(self.outimages[iby][ibx], layer)
|
|
1325
|
+
stars(n, search_radius, forced_scale, bdpad, res)
|
|
1326
|
+
self.star_cat = np.concatenate((self.star_cat, stars.sub_cat), axis=0)
|
|
1327
|
+
stars.clear()
|
|
1328
|
+
del stars
|
|
1329
|
+
print()
|
|
1330
|
+
|
|
1331
|
+
elif self.ndim == 1: # Suite
|
|
1332
|
+
nrun = self.nrun
|
|
1333
|
+
print("star counts:", end=" ")
|
|
1334
|
+
for ib in range(nrun):
|
|
1335
|
+
stars = StarsAnal(self.outimages[ib], layer)
|
|
1336
|
+
stars(n, search_radius, forced_scale, bdpad, res)
|
|
1337
|
+
self.star_cat = np.concatenate((self.star_cat, stars.sub_cat), axis=0)
|
|
1338
|
+
stars.clear()
|
|
1339
|
+
del stars
|
|
1340
|
+
print()
|
|
1341
|
+
|
|
1342
|
+
with open(fname, "wb") as f:
|
|
1343
|
+
np.save(f, self.star_cat)
|
|
1344
|
+
|
|
1345
|
+
print(f"finished at t = {timer():.2f} s")
|
|
1346
|
+
|
|
1347
|
+
def clear(self) -> None:
|
|
1348
|
+
"""Free up memory space."""
|
|
1349
|
+
|
|
1350
|
+
if self.ndim == 2: # Mosaic
|
|
1351
|
+
for ibx in range(self.cfg.nblock):
|
|
1352
|
+
for iby in range(self.cfg.nblock):
|
|
1353
|
+
self.outimages[iby][ibx] = None
|
|
1354
|
+
|
|
1355
|
+
elif self.ndim == 1: # Suite
|
|
1356
|
+
for ib in range(self.nrun):
|
|
1357
|
+
self.outimages[ib] = None
|
|
1358
|
+
|
|
1359
|
+
if hasattr(self, "consump_map"):
|
|
1360
|
+
del self.consump_map
|
|
1361
|
+
if hasattr(self, "coverage_map"):
|
|
1362
|
+
del self.coverage_map
|
|
1363
|
+
if hasattr(self, "ps2d_all"):
|
|
1364
|
+
del self.ps2d_all, self.ps1d_all
|
|
1365
|
+
if hasattr(self, "star_cat"):
|
|
1366
|
+
del self.star_cat
|
|
1367
|
+
|
|
1368
|
+
|
|
1369
|
+
class Mosaic(_BlkGrp):
|
|
1370
|
+
"""
|
|
1371
|
+
Wrapper for coadded mosaics (2D arrays of blocks).
|
|
1372
|
+
|
|
1373
|
+
Parameters
|
|
1374
|
+
----------
|
|
1375
|
+
cfg : Config
|
|
1376
|
+
Configuration used for this output mosaic.
|
|
1377
|
+
|
|
1378
|
+
Methods
|
|
1379
|
+
-------
|
|
1380
|
+
__init__
|
|
1381
|
+
Constructor.
|
|
1382
|
+
share_padding_stamps
|
|
1383
|
+
Share padding postage stamps between adjacent blocks.
|
|
1384
|
+
|
|
1385
|
+
"""
|
|
1386
|
+
|
|
1387
|
+
ndim = 2
|
|
1388
|
+
padding = False # for get_noise_power_spectra
|
|
1389
|
+
|
|
1390
|
+
def __init__(self, cfg: Config) -> None:
|
|
1391
|
+
"""Constructor."""
|
|
1392
|
+
|
|
1393
|
+
cfg()
|
|
1394
|
+
self.cfg = cfg
|
|
1395
|
+
self.hdu_names = OutImage.get_hdu_names(cfg.outmaps)
|
|
1396
|
+
|
|
1397
|
+
self.outimages = [[None for ibx in range(cfg.nblock)] for iby in range(cfg.nblock)]
|
|
1398
|
+
for ibx in range(cfg.nblock):
|
|
1399
|
+
for iby in range(cfg.nblock):
|
|
1400
|
+
fpath = cfg.outstem + f"_{ibx:02d}_{iby:02d}.fits"
|
|
1401
|
+
self.outimages[iby][ibx] = OutImage(fpath, cfg, self.hdu_names)
|
|
1402
|
+
|
|
1403
|
+
def share_padding_stamps(self) -> None:
|
|
1404
|
+
"""
|
|
1405
|
+
Share padding postage stamps between adjacent blocks.
|
|
1406
|
+
|
|
1407
|
+
Returns
|
|
1408
|
+
-------
|
|
1409
|
+
None
|
|
1410
|
+
|
|
1411
|
+
"""
|
|
1412
|
+
|
|
1413
|
+
assert self.cfg.pad_sides == "auto", "Error: share_padding_stamps only supports pad_sides == 'auto'"
|
|
1414
|
+
nblock = self.cfg.nblock # shortcut
|
|
1415
|
+
timer = Timer()
|
|
1416
|
+
|
|
1417
|
+
print(" > horizontal sharing")
|
|
1418
|
+
for iby in range(nblock):
|
|
1419
|
+
print(f" > row {iby:2d} t= {timer():9.2f} s")
|
|
1420
|
+
self.outimages[iby][0]._load_or_save_hdu_list(True)
|
|
1421
|
+
for ibx in range(nblock - 1):
|
|
1422
|
+
self.outimages[iby][ibx + 1]._load_or_save_hdu_list(True)
|
|
1423
|
+
self.outimages[iby][ibx]._update_hdu_data(self.outimages[iby][ibx + 1], "right", True)
|
|
1424
|
+
self.outimages[iby][ibx + 1]._update_hdu_data(self.outimages[iby][ibx], "left", False)
|
|
1425
|
+
self.outimages[iby][ibx]._load_or_save_hdu_list(False, save_file=True)
|
|
1426
|
+
self.outimages[iby][nblock - 1]._load_or_save_hdu_list(False, save_file=True)
|
|
1427
|
+
print(flush=True)
|
|
1428
|
+
|
|
1429
|
+
print(" > vertical sharing")
|
|
1430
|
+
for ibx in range(nblock):
|
|
1431
|
+
print(f" > column {ibx:2d} t= {timer():9.2f} s")
|
|
1432
|
+
self.outimages[0][ibx]._load_or_save_hdu_list(True)
|
|
1433
|
+
for iby in range(nblock - 1):
|
|
1434
|
+
self.outimages[iby + 1][ibx]._load_or_save_hdu_list(True)
|
|
1435
|
+
self.outimages[iby][ibx]._update_hdu_data(self.outimages[iby + 1][ibx], "top", True)
|
|
1436
|
+
self.outimages[iby + 1][ibx]._update_hdu_data(self.outimages[iby][ibx], "bottom", False)
|
|
1437
|
+
self.outimages[iby][ibx]._load_or_save_hdu_list(False, save_file=True, auto_to_all=True)
|
|
1438
|
+
self.outimages[nblock - 1][ibx]._load_or_save_hdu_list(False, save_file=True, auto_to_all=True)
|
|
1439
|
+
print(flush=True)
|
|
1440
|
+
|
|
1441
|
+
print(f"finished at t = {timer():.2f} s")
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
class Suite(_BlkGrp):
|
|
1445
|
+
"""
|
|
1446
|
+
Wrapper for coadded suites (hashed arrays of blocks).
|
|
1447
|
+
|
|
1448
|
+
Parameters
|
|
1449
|
+
----------
|
|
1450
|
+
cfg : Config
|
|
1451
|
+
Configuration used for this output mosaic.
|
|
1452
|
+
prime : int, optional
|
|
1453
|
+
Prime number for hashing (Paper IV).
|
|
1454
|
+
nrun : int, optional
|
|
1455
|
+
Number of coadded blocks (Paper IV).
|
|
1456
|
+
|
|
1457
|
+
Methods
|
|
1458
|
+
-------
|
|
1459
|
+
__init__
|
|
1460
|
+
Constructor.
|
|
1461
|
+
|
|
1462
|
+
"""
|
|
1463
|
+
|
|
1464
|
+
ndim = 1
|
|
1465
|
+
padding = True # for get_noise_power_spectra
|
|
1466
|
+
|
|
1467
|
+
def __init__(self, cfg: Config, prime: int = 691, nrun: int = 16) -> None:
|
|
1468
|
+
"""Constructor."""
|
|
1469
|
+
|
|
1470
|
+
cfg()
|
|
1471
|
+
self.cfg = cfg
|
|
1472
|
+
self.hdu_names = OutImage.get_hdu_names(cfg.outmaps)
|
|
1473
|
+
|
|
1474
|
+
self.prime = prime
|
|
1475
|
+
self.nrun = nrun
|
|
1476
|
+
self.outimages = [None for ib in range(nrun)]
|
|
1477
|
+
for ib in range(nrun):
|
|
1478
|
+
ibx, iby = divmod(ib * prime % cfg.nblock**2, cfg.nblock)
|
|
1479
|
+
fpath = cfg.outstem + f"_{ibx:02d}_{iby:02d}.fits"
|
|
1480
|
+
self.outimages[ib] = OutImage(fpath, cfg, self.hdu_names)
|