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
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Report section for noise diagnostics.
|
|
3
|
+
|
|
4
|
+
Classes
|
|
5
|
+
-------
|
|
6
|
+
NoiseReport
|
|
7
|
+
Noise report section.
|
|
8
|
+
|
|
9
|
+
Notes
|
|
10
|
+
-----
|
|
11
|
+
This incorporates functionality that was previously in noisespecs.py.
|
|
12
|
+
|
|
13
|
+
It also includes the updated functionality from the Laliotis et al. analysis (August 2024).
|
|
14
|
+
|
|
15
|
+
This version is trying to implement some things to reduce imcom-related correlations, including:
|
|
16
|
+
|
|
17
|
+
#. only FFTing the interior postage stamp region (throwing out padded regions)
|
|
18
|
+
|
|
19
|
+
#. convolving noise images with a window function before FFTing
|
|
20
|
+
|
|
21
|
+
We have changed the clipping to use the full unique region ``[bdpad:L+bdpad,bdpad:L+bdpad]`` in each image.
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import re
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
from collections import namedtuple
|
|
31
|
+
from os.path import exists
|
|
32
|
+
|
|
33
|
+
import matplotlib
|
|
34
|
+
import matplotlib.colors as colors
|
|
35
|
+
import matplotlib.pyplot as plt
|
|
36
|
+
import numpy as np
|
|
37
|
+
from astropy.io import fits
|
|
38
|
+
from scipy import ndimage
|
|
39
|
+
from scipy.fft import fft2 as scipy_fft2
|
|
40
|
+
from skimage.filters import window
|
|
41
|
+
|
|
42
|
+
from ..compress.compressutils import ReadFile
|
|
43
|
+
from ..config import Settings
|
|
44
|
+
from .context_figure import ReportFigContext
|
|
45
|
+
from .report import ReportSection
|
|
46
|
+
|
|
47
|
+
RomanFilters = ["W146", "F184", "H158", "J129", "Y106", "Z087", "R062", "PRSM", "DARK", "GRSM", "K213"]
|
|
48
|
+
|
|
49
|
+
AreaArray = [22085, 4840, 7340, 7111, 7006, 6635, 9011, 0, 0, 0, 4654]
|
|
50
|
+
# in cm^2; 0 for prism/dark/grism as these are not imaging elements
|
|
51
|
+
|
|
52
|
+
# Set up a named tuple for the results that will contain relevant information
|
|
53
|
+
PspecResults = namedtuple("PspecResults", "ps_image ps_image_err npix k ps_2d noiselayer")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class NoiseReport(ReportSection):
|
|
57
|
+
"""
|
|
58
|
+
The noise section of the report.
|
|
59
|
+
|
|
60
|
+
Inherits from pyimcom.diagnostics.report.ReportSection. Overrides build.
|
|
61
|
+
|
|
62
|
+
Attributes
|
|
63
|
+
----------
|
|
64
|
+
nblock : int
|
|
65
|
+
Number of blocks in the (sub)mosaic used.
|
|
66
|
+
psfiles : list of str
|
|
67
|
+
List of power spectrum file names.
|
|
68
|
+
outslab : list of int
|
|
69
|
+
Indices of the layers used for the power spectrum computation.
|
|
70
|
+
orignames : list of str
|
|
71
|
+
Names of the layers used for the power spectrum computation.
|
|
72
|
+
L : int
|
|
73
|
+
Side length of the unique region in a block, in output pixels.
|
|
74
|
+
noiselayers : dict
|
|
75
|
+
A dictionary with values that are integers corresponding to the input layer
|
|
76
|
+
corresponding to the type of noise indicated in the key. (Keys should be strings.)
|
|
77
|
+
NLK : list of str
|
|
78
|
+
The keys of `noiselayers`, sorted in the same order as EXTRAINPUT in the configuration.
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def build(self, nblockmax=100, m_ab=23.9, bin_flag=1, alpha=0.9, tarfiles=True):
|
|
83
|
+
"""
|
|
84
|
+
Builds the noise section of the report.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
nblockmax : int, optional
|
|
89
|
+
Maximum size of mosaic to build.
|
|
90
|
+
m_ab : float, optional
|
|
91
|
+
Scaling magnitude (not currently used).
|
|
92
|
+
bin_flag : int, optional
|
|
93
|
+
Whether to bin? (1 = bin 8x8, 0 = do not bin)
|
|
94
|
+
alpha : float, optional
|
|
95
|
+
Tukey window width for noise power spectrum.
|
|
96
|
+
tarfiles : bool, optional
|
|
97
|
+
Generate a tarball of the data files?
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
None
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
# pulled the reference magnitude back up here
|
|
106
|
+
# also the binning flag (1 = bin 8x8, 0 = do not bin)
|
|
107
|
+
|
|
108
|
+
self.nblock = min(nblockmax, self.cfg.nblock)
|
|
109
|
+
# will keep appending so all the power spectrum files with all information in their names is in here
|
|
110
|
+
self.psfiles = []
|
|
111
|
+
# added to block output file
|
|
112
|
+
self.suffix = ""
|
|
113
|
+
|
|
114
|
+
self.tex += "\\section{Injected noise layers section}\n\n"
|
|
115
|
+
|
|
116
|
+
# example output slabs for white, 1/f, lab, and simulated noise
|
|
117
|
+
self.outslab = [None, None, None, None]
|
|
118
|
+
|
|
119
|
+
# there are several sets of files to build here
|
|
120
|
+
self.build_noisespec(m_ab, bin_flag, alpha)
|
|
121
|
+
self.average_spectra(bin_flag)
|
|
122
|
+
|
|
123
|
+
# make one example figure, the 2D power spectrum
|
|
124
|
+
self.gen_overview_fig()
|
|
125
|
+
|
|
126
|
+
# add variances
|
|
127
|
+
filter = Settings.RomanFilters[self.cfg.use_filter][0]
|
|
128
|
+
for k in range(len(self.orignames)):
|
|
129
|
+
with fits.open(self.datastem + "_" + filter + self.suffix + "_ps_avg.fits") as f:
|
|
130
|
+
self.data += (
|
|
131
|
+
f"LAYER{k:02d}"
|
|
132
|
+
+ " "
|
|
133
|
+
+ f"{self.orignames[k]:24s}"
|
|
134
|
+
+ f" {np.average(f[0].data[k, :, :]) / self.s_out**2:11.5E}\n"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# tarball the files if requested
|
|
138
|
+
if tarfiles:
|
|
139
|
+
tarfile_head, tarfile_tail = os.path.split(self.datastem + "_blockps" + self.suffix + ".tar")
|
|
140
|
+
lf = []
|
|
141
|
+
for f in self.psfiles:
|
|
142
|
+
pdir, pname = os.path.split(f)
|
|
143
|
+
lf += [pname]
|
|
144
|
+
pwd = os.getcwd()
|
|
145
|
+
os.chdir(pdir)
|
|
146
|
+
proc = subprocess.run(["tar", "--create", "--file=" + tarfile_tail] + lf, capture_output=True)
|
|
147
|
+
print("tar output -->\n", proc.stdout)
|
|
148
|
+
print("errors -->\n", proc.stderr)
|
|
149
|
+
for f in lf:
|
|
150
|
+
os.remove(f)
|
|
151
|
+
os.chdir(pwd)
|
|
152
|
+
|
|
153
|
+
# --- noisespec --- #
|
|
154
|
+
|
|
155
|
+
def build_noisespec(self, m_ab, bin_flag, alpha):
|
|
156
|
+
"""
|
|
157
|
+
Computes the noise power spectrum.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
m_ab : float
|
|
162
|
+
Reference star brightness (not used)
|
|
163
|
+
bin_flag : int, optional
|
|
164
|
+
Whether to bin? (1 = bin 8x8, 0 = do not bin)
|
|
165
|
+
Note that binning is disabled if the input image is too small.
|
|
166
|
+
alpha : float, optional
|
|
167
|
+
Tukey window width for noise power spectrum.
|
|
168
|
+
|
|
169
|
+
Returns
|
|
170
|
+
-------
|
|
171
|
+
str
|
|
172
|
+
Status string ('Completed').
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
# pars = [] # list of parameters, replaces global construction when wrapped
|
|
177
|
+
|
|
178
|
+
# Set useful constants for te lab noise data
|
|
179
|
+
tfr = 3.08 # sec
|
|
180
|
+
gain = 1.458 # electrons/DN
|
|
181
|
+
# ABstd = 3.631e-20 # erg/cm^2
|
|
182
|
+
h_erg = 6.62607015e-27 # erg/Hz
|
|
183
|
+
h_jy = h_erg * 1e29 # microJy*cm^2*s
|
|
184
|
+
s_in = Settings.pixscale_native * 648000.0 / np.pi # arcsec # updated to refer back to Settings
|
|
185
|
+
B0 = 0.38 # e/px/s, background estimate
|
|
186
|
+
t_exp = 139.8 # s
|
|
187
|
+
|
|
188
|
+
area = AreaArray[self.cfg.use_filter]
|
|
189
|
+
filter = Settings.RomanFilters[self.cfg.use_filter][0]
|
|
190
|
+
|
|
191
|
+
# extra background to add for lab noise
|
|
192
|
+
B1 = 0.0
|
|
193
|
+
if filter == "K":
|
|
194
|
+
B1 = 4.65
|
|
195
|
+
whitenoisekey = None # avoids error if there isn't a "whitenoise" layer
|
|
196
|
+
|
|
197
|
+
# which blocks?
|
|
198
|
+
is_first = True
|
|
199
|
+
for iby in range(self.nblock):
|
|
200
|
+
for ibx in range(self.nblock):
|
|
201
|
+
blockid = f"{filter:s}_{ibx:02d}_{iby:02d}"
|
|
202
|
+
if alpha > 0:
|
|
203
|
+
win = True
|
|
204
|
+
blockid += "_alpha_" + str(alpha)
|
|
205
|
+
else:
|
|
206
|
+
win = False
|
|
207
|
+
if bin_flag == 0:
|
|
208
|
+
blockid += "_nobin"
|
|
209
|
+
|
|
210
|
+
# loop over blocks
|
|
211
|
+
infile = self.infile(ibx, iby)
|
|
212
|
+
|
|
213
|
+
if not exists(infile):
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
# the first time
|
|
217
|
+
if is_first:
|
|
218
|
+
is_first = False
|
|
219
|
+
|
|
220
|
+
with ReadFile(infile, layers=[]) as f:
|
|
221
|
+
# n = np.shape(f[0].data)[-1] # size of output images
|
|
222
|
+
config = ""
|
|
223
|
+
for g in f["CONFIG"].data["text"].tolist():
|
|
224
|
+
config += g + " "
|
|
225
|
+
configStruct = json.loads(config)
|
|
226
|
+
configdata = f["CONFIG"].data
|
|
227
|
+
|
|
228
|
+
# blocksize = (
|
|
229
|
+
# int(configStruct["OUTSIZE"][0])
|
|
230
|
+
# * int(configStruct["OUTSIZE"][1])
|
|
231
|
+
# * float(configStruct["OUTSIZE"][2])
|
|
232
|
+
# / 3600.0
|
|
233
|
+
# * np.pi
|
|
234
|
+
# / 180
|
|
235
|
+
# ) # block size in radians
|
|
236
|
+
L = self.L = int(configStruct["OUTSIZE"][0]) * int(
|
|
237
|
+
configStruct["OUTSIZE"][1]
|
|
238
|
+
) # side length in px
|
|
239
|
+
# snap to nearest multiple of 2 or 16
|
|
240
|
+
if L >= 32:
|
|
241
|
+
L = (L // 16) * 16
|
|
242
|
+
else:
|
|
243
|
+
L = (L // 2) * 2
|
|
244
|
+
bin_flag = 0
|
|
245
|
+
|
|
246
|
+
self.s_out = s_out = float(configStruct["OUTSIZE"][2]) # in arcsec
|
|
247
|
+
|
|
248
|
+
# size of padding region around each edge (in px)
|
|
249
|
+
bdpad = int(configStruct["OUTSIZE"][1]) * int(configStruct["PAD"])
|
|
250
|
+
|
|
251
|
+
# figure out which noise layers are there
|
|
252
|
+
layers = [""] + configStruct["EXTRAINPUT"]
|
|
253
|
+
print("# Layers:", layers)
|
|
254
|
+
noiselayers = {}
|
|
255
|
+
for i in range(len(layers)):
|
|
256
|
+
m = re.match(r"^whitenoise(\d+)$", layers[i])
|
|
257
|
+
if m:
|
|
258
|
+
noiselayers[str(m[0])] = i
|
|
259
|
+
whitenoisekey = str(m[0])
|
|
260
|
+
m = re.match(r"^1fnoise(\d+)$", layers[i])
|
|
261
|
+
if m:
|
|
262
|
+
noiselayers[str(m[0])] = i
|
|
263
|
+
m = re.match(r"^labnoise$", layers[i])
|
|
264
|
+
if m:
|
|
265
|
+
noiselayers[str(m[0])] = i
|
|
266
|
+
m = re.match(r"^noise,(\S+)$", layers[i])
|
|
267
|
+
if m:
|
|
268
|
+
noiselayers[str(m[0])] = i
|
|
269
|
+
print("# Noise Layers (format is layer:use_slice): ", noiselayers)
|
|
270
|
+
NLK = list(noiselayers.keys()) # save for shorthand -- note this is in insertion order!
|
|
271
|
+
|
|
272
|
+
print("# Running file: " + infile, "whitenoisekey =", whitenoisekey)
|
|
273
|
+
|
|
274
|
+
# read layers and get mean coverage
|
|
275
|
+
pad = int(configStruct["PAD"])
|
|
276
|
+
with ReadFile(infile, layers=sorted([noiselayers[q] for q in NLK])) as f:
|
|
277
|
+
infile_data = np.copy(
|
|
278
|
+
f[0].data[0, :, bdpad : L + bdpad, bdpad : L + bdpad].astype(np.float32)
|
|
279
|
+
)
|
|
280
|
+
n = np.shape(f["INWEIGHT"].data)[-1]
|
|
281
|
+
mean_coverage = np.mean(
|
|
282
|
+
np.sum(np.where(f["INWEIGHT"].data[0, :, :, :] > 0, 1, 0), axis=0)[
|
|
283
|
+
pad : n - pad, pad : n - pad
|
|
284
|
+
]
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if bin_flag == 0:
|
|
288
|
+
ps2d_all = np.zeros((L, L, len(noiselayers)))
|
|
289
|
+
ps1d_all = np.zeros((L // 2, 4, len(noiselayers)))
|
|
290
|
+
elif bin_flag == 1:
|
|
291
|
+
ps2d_all = np.zeros((L // 8, L // 8, len(noiselayers)))
|
|
292
|
+
ps1d_all = np.zeros((L // 16, 4, len(noiselayers)))
|
|
293
|
+
else:
|
|
294
|
+
raise Exception("Error: bin flag must be 0 (no binning) or 1 (8x8 binning)")
|
|
295
|
+
|
|
296
|
+
for i_layer, noiselayer in enumerate(NLK):
|
|
297
|
+
use_slice = noiselayers[noiselayer]
|
|
298
|
+
indata = infile_data[use_slice, :, :]
|
|
299
|
+
# Number of radial bins is side length div. into 8 from binning and then (floor) div. by 2
|
|
300
|
+
nradbins = L // 16
|
|
301
|
+
# Note that with the new clipping this is L again, the Aug. 2024 clipping needed (L-bdpad)
|
|
302
|
+
if bin_flag == 0:
|
|
303
|
+
nradbins *= 8
|
|
304
|
+
|
|
305
|
+
norm = (
|
|
306
|
+
(L / s_out) ** 2
|
|
307
|
+
) # normalization factor to get power spectrum in units of [image units]^2 * arcsec^2
|
|
308
|
+
# special normalization for the lab data
|
|
309
|
+
m = re.search(r"lab", noiselayer)
|
|
310
|
+
if m:
|
|
311
|
+
norm_LN = (
|
|
312
|
+
(s_in**2) * area * tfr / (h_jy * gain)
|
|
313
|
+
) # factor to convert LN from flux DN/fr/s to intensity microJy/arcsec^2
|
|
314
|
+
if filter == "K" and whitenoisekey is not None:
|
|
315
|
+
wndata = np.copy(infile_data[noiselayers[whitenoisekey]])
|
|
316
|
+
wndata *= np.sqrt((B1 - B0) / t_exp) * tfr / gain # convert WN to DN/fr
|
|
317
|
+
indata += wndata # add to lab noise
|
|
318
|
+
indata = indata / norm_LN
|
|
319
|
+
|
|
320
|
+
powerspectrum = NoiseReport.get_powerspectra(
|
|
321
|
+
indata,
|
|
322
|
+
L,
|
|
323
|
+
norm,
|
|
324
|
+
nradbins,
|
|
325
|
+
use_slice=use_slice,
|
|
326
|
+
bin_flag=bin_flag,
|
|
327
|
+
win=win,
|
|
328
|
+
alpha=alpha,
|
|
329
|
+
)
|
|
330
|
+
# norm and nradbins now required arguments; use_slice is needed to get the correct format
|
|
331
|
+
# in the output file
|
|
332
|
+
ps2d_all[:, :, i_layer] = powerspectrum.ps_2d
|
|
333
|
+
ps1d_all[:, 0, i_layer] = powerspectrum.k
|
|
334
|
+
ps1d_all[:, 1, i_layer] = powerspectrum.ps_image
|
|
335
|
+
ps1d_all[:, 2, i_layer] = powerspectrum.ps_image_err
|
|
336
|
+
ps1d_all[:, 3, i_layer] = powerspectrum.noiselayer
|
|
337
|
+
|
|
338
|
+
# Reshape things for fits files
|
|
339
|
+
ps2d_all = np.transpose(ps2d_all, (2, 0, 1))
|
|
340
|
+
# print("# TRANSPOSED ps2d shape:", np.shape(ps2d_all))
|
|
341
|
+
# reshape P1D's:
|
|
342
|
+
ps1d_all = np.transpose(ps1d_all, (2, 0, 1)).reshape(-1, np.shape(ps1d_all)[1])
|
|
343
|
+
# print("# TRANSPOSED ps1d shape:", np.shape(ps1d_all))
|
|
344
|
+
|
|
345
|
+
# Save power spectra data in a fits file
|
|
346
|
+
# Two HDUs: Primary contains 2D spectrum, second is a table with 1D spectrum and MC values
|
|
347
|
+
hdu_ps2d = fits.PrimaryHDU(ps2d_all)
|
|
348
|
+
hdr = hdu_ps2d.header
|
|
349
|
+
hdr["INSTEM"] = infile[:-11] # updated from original script
|
|
350
|
+
hdr["MEANCOVG"] = mean_coverage
|
|
351
|
+
hdr["LAYERKEY"] = str(noiselayers)
|
|
352
|
+
hdr["NLAYERS"] = (len(noiselayers), "Number of layers with noise")
|
|
353
|
+
key_layer2 = [""] * (1 + len(configStruct["EXTRAINPUT"]))
|
|
354
|
+
for d in NLK:
|
|
355
|
+
d_ = noiselayers[d]
|
|
356
|
+
key_layer2[d_] = d
|
|
357
|
+
key_layer = []
|
|
358
|
+
self.orignames = []
|
|
359
|
+
for k in range(len(key_layer2)):
|
|
360
|
+
if key_layer2[k] != "":
|
|
361
|
+
key_layer.append(key_layer2[k])
|
|
362
|
+
self.orignames.append(configStruct["EXTRAINPUT"][k - 1])
|
|
363
|
+
del key_layer2
|
|
364
|
+
for il in range(len(NLK)):
|
|
365
|
+
key_ = f"LAYER{il:02d}"
|
|
366
|
+
hdr[key_] = (key_layer[il], f"Noise layer {il:d} in intermediate file")
|
|
367
|
+
if key_layer[il][:10] == "whitenoise":
|
|
368
|
+
self.outslab[0] = il
|
|
369
|
+
if key_layer[il][:7] == "1fnoise":
|
|
370
|
+
self.outslab[1] = il
|
|
371
|
+
if key_layer[il][:8] == "labnoise":
|
|
372
|
+
self.outslab[2] = il
|
|
373
|
+
if key_layer[il][:6] == "noise," and "b" in key_layer[il]:
|
|
374
|
+
self.outslab[3] = il
|
|
375
|
+
if len(NLK) >= 1:
|
|
376
|
+
del key_
|
|
377
|
+
hdr["AREAUNIT"] = "arcsec**2"
|
|
378
|
+
|
|
379
|
+
col1 = fits.Column(name="Wavenumber", format="E", array=ps1d_all[:, 0])
|
|
380
|
+
col2 = fits.Column(name="Power", format="E", array=ps1d_all[:, 1])
|
|
381
|
+
col3 = fits.Column(name="Error", format="E", array=ps1d_all[:, 2])
|
|
382
|
+
col4 = fits.Column(name="NoiseLayerID", format="I", array=ps1d_all[:, 3])
|
|
383
|
+
p1d_cols = fits.ColDefs([col1, col2, col3, col4])
|
|
384
|
+
hdu_ps1d = fits.BinTableHDU.from_columns(p1d_cols, name="P1D_TABLE")
|
|
385
|
+
|
|
386
|
+
hdu_config = fits.BinTableHDU(data=configdata, name="CONFIG")
|
|
387
|
+
hdul = fits.HDUList([hdu_ps2d, hdu_config, hdu_ps1d])
|
|
388
|
+
self.psfiles.append(self.datastem + "_" + blockid + "_ps.fits")
|
|
389
|
+
hdul.writeto(self.psfiles[-1], overwrite=True)
|
|
390
|
+
print("# Results saved to ", self.datastem, "_", blockid, "_ps.fits")
|
|
391
|
+
|
|
392
|
+
self.suffix = blockid[7:]
|
|
393
|
+
self.noiselayers = noiselayers # save this for reference later
|
|
394
|
+
self.NLK = NLK
|
|
395
|
+
return "Completed"
|
|
396
|
+
|
|
397
|
+
## Utility functions below here ##
|
|
398
|
+
|
|
399
|
+
@staticmethod
|
|
400
|
+
def measure_power_spectrum(noiseframe, L, norm=1.0, bin=True, win=True, alpha=0.9):
|
|
401
|
+
"""
|
|
402
|
+
Measure the 2D power spectrum of image.
|
|
403
|
+
|
|
404
|
+
Parameters
|
|
405
|
+
----------
|
|
406
|
+
noiseframe : np.array
|
|
407
|
+
The 2D input image to measure the power spectrum of.
|
|
408
|
+
In this case, a noise frame from the simulations
|
|
409
|
+
L : int
|
|
410
|
+
The length of the FFT (must be a multiple of 8).
|
|
411
|
+
norm : float, optional
|
|
412
|
+
The normalization to use (power spectrum is |FFT|^2/norm).
|
|
413
|
+
bin : bool, optional
|
|
414
|
+
Whether to bin the 2D spectrum.
|
|
415
|
+
Default=True, bins spectrum into L/8 x L/8 image.
|
|
416
|
+
Potential extra rows are cut off.
|
|
417
|
+
win : bool, optional
|
|
418
|
+
Whether to convolve the noise frame with a Tukey window function.
|
|
419
|
+
alpha : float, optional
|
|
420
|
+
Tukey window parameter.
|
|
421
|
+
|
|
422
|
+
Returns
|
|
423
|
+
-------
|
|
424
|
+
np.array
|
|
425
|
+
The 2D power spectrum of the image.
|
|
426
|
+
|
|
427
|
+
"""
|
|
428
|
+
|
|
429
|
+
# get the window function and its normalization
|
|
430
|
+
if win:
|
|
431
|
+
w = window(("tukey", alpha), (np.shape(noiseframe)))
|
|
432
|
+
norm = norm * np.average(w**2)
|
|
433
|
+
noiseframe = noiseframe * w
|
|
434
|
+
|
|
435
|
+
fft = np.fft.fftshift(scipy_fft2(noiseframe, workers=4))
|
|
436
|
+
ps = ((np.abs(fft)) ** 2) / norm
|
|
437
|
+
if bin:
|
|
438
|
+
# print('# 2D spectrum is 8x8 binned\n')
|
|
439
|
+
binned_ps = np.average(np.reshape(ps, (L // 8, 8, L // 8, 8)), axis=(1, 3))
|
|
440
|
+
# print('# Binned PS has shape ', np.shape(binned_ps))
|
|
441
|
+
return binned_ps
|
|
442
|
+
else:
|
|
443
|
+
return ps
|
|
444
|
+
|
|
445
|
+
@staticmethod
|
|
446
|
+
def _get_wavenumbers(window_length, num_radial_bins):
|
|
447
|
+
"""
|
|
448
|
+
Calculate wavenumbers for the input image.
|
|
449
|
+
|
|
450
|
+
Parameters
|
|
451
|
+
----------
|
|
452
|
+
window_length : int
|
|
453
|
+
The length of one axis of the image.
|
|
454
|
+
num_radial_bins: int
|
|
455
|
+
Number of radial bins the image should be averaged into.
|
|
456
|
+
|
|
457
|
+
Returns
|
|
458
|
+
-------
|
|
459
|
+
kmean : np.array
|
|
460
|
+
1D array of the wavenumbers for the image
|
|
461
|
+
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
k = np.fft.fftshift(np.fft.fftfreq(window_length))
|
|
465
|
+
kx, ky = np.meshgrid(k, k)
|
|
466
|
+
k = np.sqrt(kx**2 + ky**2)
|
|
467
|
+
k, kmean, kerr = NoiseReport.azimuthal_average(k, num_radial_bins)
|
|
468
|
+
|
|
469
|
+
return kmean
|
|
470
|
+
|
|
471
|
+
@staticmethod
|
|
472
|
+
def azimuthal_average(image, num_radial_bins):
|
|
473
|
+
"""
|
|
474
|
+
Compute radial profile of image.
|
|
475
|
+
|
|
476
|
+
Parameters
|
|
477
|
+
----------
|
|
478
|
+
image : np.array
|
|
479
|
+
Input image, 2D.
|
|
480
|
+
num_radial_bins : int
|
|
481
|
+
Number of radial bins in profile.
|
|
482
|
+
|
|
483
|
+
Returns
|
|
484
|
+
-------
|
|
485
|
+
r : np.array
|
|
486
|
+
Value of radius at each point
|
|
487
|
+
radial_mean : np.array
|
|
488
|
+
Mean intensity within each annulus. Main result
|
|
489
|
+
radial_err : np.array
|
|
490
|
+
Standard error on the mean: sigma / sqrt(N).
|
|
491
|
+
|
|
492
|
+
"""
|
|
493
|
+
|
|
494
|
+
ny, nx = image.shape
|
|
495
|
+
yy, xx = np.mgrid[:ny, :nx]
|
|
496
|
+
center = np.array(image.shape) / 2
|
|
497
|
+
|
|
498
|
+
r = np.hypot(xx - center[1], yy - center[0])
|
|
499
|
+
rbin = (num_radial_bins * r / r.max()).astype(int)
|
|
500
|
+
|
|
501
|
+
radial_mean = ndimage.mean(image, labels=rbin, index=np.arange(1, rbin.max() + 1))
|
|
502
|
+
radial_stddev = ndimage.standard_deviation(image, labels=rbin, index=np.arange(1, rbin.max() + 1))
|
|
503
|
+
npix = ndimage.sum(np.ones_like(image), labels=rbin, index=np.arange(1, rbin.max() + 1))
|
|
504
|
+
|
|
505
|
+
radial_err = radial_stddev / np.sqrt(npix)
|
|
506
|
+
return r, radial_mean, radial_err
|
|
507
|
+
|
|
508
|
+
@staticmethod
|
|
509
|
+
def get_powerspectra(noiseframe, L, norm, num_radial_bins, use_slice=-1, bin_flag=1, win=True, alpha=0.9):
|
|
510
|
+
"""
|
|
511
|
+
Calculate the azimuthally-averaged 1D power spectrum of the image.
|
|
512
|
+
|
|
513
|
+
Parameters
|
|
514
|
+
----------
|
|
515
|
+
noiseframe: np.array
|
|
516
|
+
The 2D input image to be averaged over.
|
|
517
|
+
L : int
|
|
518
|
+
Length of FFT (must be a multiple of 8).
|
|
519
|
+
norm : float
|
|
520
|
+
Normalization of |FFT|^2->power spectrum.
|
|
521
|
+
num_radial_bins : int
|
|
522
|
+
Number of bins, should match bin number in get_wavenumbers
|
|
523
|
+
use_slice : int, optional
|
|
524
|
+
Noise slice number used.
|
|
525
|
+
bin_flag : int, optional
|
|
526
|
+
Binning? (1=yes, 0=no).
|
|
527
|
+
win : bool, optional
|
|
528
|
+
Whether to convolve the noise frame with a Tukey window function.
|
|
529
|
+
alpha : float, optional
|
|
530
|
+
Tukey window parameter.
|
|
531
|
+
|
|
532
|
+
Returns
|
|
533
|
+
-------
|
|
534
|
+
results : collection.namedtuple
|
|
535
|
+
Power spectrum results.
|
|
536
|
+
|
|
537
|
+
"""
|
|
538
|
+
|
|
539
|
+
noise = noiseframe.copy()
|
|
540
|
+
if bin_flag == 0:
|
|
541
|
+
ps_2d = NoiseReport.measure_power_spectrum(noise, L, norm=norm, bin=False, win=win, alpha=alpha)
|
|
542
|
+
else:
|
|
543
|
+
ps_2d = NoiseReport.measure_power_spectrum(noise, L, norm=norm, bin=True, win=win, alpha=alpha)
|
|
544
|
+
ps_r, ps_1d, ps_image_err = NoiseReport.azimuthal_average(ps_2d, num_radial_bins)
|
|
545
|
+
wavenumbers = NoiseReport._get_wavenumbers(noise.shape[0], num_radial_bins)
|
|
546
|
+
npix = np.prod(noiseframe.shape)
|
|
547
|
+
comment = [use_slice] * num_radial_bins
|
|
548
|
+
|
|
549
|
+
# consolidate results
|
|
550
|
+
results = PspecResults(
|
|
551
|
+
ps_image=ps_1d,
|
|
552
|
+
ps_image_err=ps_image_err,
|
|
553
|
+
npix=npix,
|
|
554
|
+
k=wavenumbers,
|
|
555
|
+
ps_2d=ps_2d,
|
|
556
|
+
noiselayer=comment,
|
|
557
|
+
)
|
|
558
|
+
return results
|
|
559
|
+
|
|
560
|
+
# --- average_spectra --- #
|
|
561
|
+
|
|
562
|
+
def average_spectra(self, bin_flag):
|
|
563
|
+
"""
|
|
564
|
+
Averages together all the power spectra in one band.
|
|
565
|
+
|
|
566
|
+
Parameters
|
|
567
|
+
----------
|
|
568
|
+
bin_flag
|
|
569
|
+
Whether to bin? (1 = bin 8x8, 0 = do not bin)
|
|
570
|
+
|
|
571
|
+
Returns
|
|
572
|
+
-------
|
|
573
|
+
None
|
|
574
|
+
|
|
575
|
+
"""
|
|
576
|
+
|
|
577
|
+
for iblock in range(self.nblock**2):
|
|
578
|
+
ibx = iblock % self.nblock
|
|
579
|
+
iby = iblock // self.nblock
|
|
580
|
+
|
|
581
|
+
infile = self.psfiles[iblock]
|
|
582
|
+
print(ibx, iby, infile)
|
|
583
|
+
sys.stdout.flush()
|
|
584
|
+
|
|
585
|
+
# extract information from the header of the first file
|
|
586
|
+
if iblock == 0:
|
|
587
|
+
with fits.open(infile) as f:
|
|
588
|
+
n = np.shape(f["PRIMARY"])[0]
|
|
589
|
+
ll = (f["P1D_TABLE"].data).shape[0]
|
|
590
|
+
total_2D = np.zeros(np.shape(np.transpose(f["PRIMARY"].data, (1, 2, 0))))
|
|
591
|
+
total_1D = np.zeros((ll, 4))
|
|
592
|
+
# header = np.copy(f["PRIMARY"].header)
|
|
593
|
+
|
|
594
|
+
if not exists(infile):
|
|
595
|
+
continue
|
|
596
|
+
|
|
597
|
+
with fits.open(infile) as f:
|
|
598
|
+
indata_2D = np.copy(np.transpose(f["PRIMARY"].data, (1, 2, 0))).astype(np.float32)
|
|
599
|
+
indata_1D = np.copy(f["P1D_TABLE"].data)
|
|
600
|
+
|
|
601
|
+
for k in range(0, n):
|
|
602
|
+
total_2D[:, :, k] += indata_2D[:, :, k]
|
|
603
|
+
for k in range(0, ll):
|
|
604
|
+
for m in range(0, 4):
|
|
605
|
+
total_1D[k, m] += indata_1D[k][m]
|
|
606
|
+
|
|
607
|
+
for k in range(0, n):
|
|
608
|
+
total_2D[:, :, k] = total_2D[:, :, k] / self.nblock**2
|
|
609
|
+
total_1D = total_1D / self.nblock**2
|
|
610
|
+
|
|
611
|
+
hdu1 = fits.PrimaryHDU(np.transpose(total_2D, (2, 0, 1)))
|
|
612
|
+
hdr = hdu1.header
|
|
613
|
+
|
|
614
|
+
with fits.open(self.psfiles[0]) as g:
|
|
615
|
+
copykey = ["INSTEM", "LAYERKEY", "NLAYERS"]
|
|
616
|
+
for il in range(len(self.noiselayers)):
|
|
617
|
+
copykey.append(f"LAYER{il:02d}")
|
|
618
|
+
copykey.append("AREAUNIT")
|
|
619
|
+
for key in copykey:
|
|
620
|
+
hdr[key] = g[0].header[key]
|
|
621
|
+
|
|
622
|
+
col1 = fits.Column(name="Wavenumber", format="E", array=total_1D[:, 0])
|
|
623
|
+
col2 = fits.Column(name="Power", format="E", array=total_1D[:, 1])
|
|
624
|
+
col3 = fits.Column(name="Error", format="E", array=total_1D[:, 2])
|
|
625
|
+
col4 = fits.Column(name="NoiseLayerID", format="I", array=total_1D[:, 3])
|
|
626
|
+
p1d_cols = fits.ColDefs([col1, col2, col3, col4])
|
|
627
|
+
hdu_ps1d = fits.BinTableHDU.from_columns(p1d_cols, name="P1D_TABLE")
|
|
628
|
+
|
|
629
|
+
hdul = fits.HDUList([hdu1, hdu_ps1d])
|
|
630
|
+
filter = Settings.RomanFilters[self.cfg.use_filter][0]
|
|
631
|
+
outfile = self.datastem + "_" + filter + self.suffix + "_ps_avg.fits"
|
|
632
|
+
hdul.writeto(outfile, overwrite=True)
|
|
633
|
+
print("# Average power spectrum saved to " + outfile)
|
|
634
|
+
|
|
635
|
+
# --- figures --- #
|
|
636
|
+
def gen_overview_fig(self):
|
|
637
|
+
"""
|
|
638
|
+
Makes a simple overview figure.
|
|
639
|
+
|
|
640
|
+
Returns
|
|
641
|
+
-------
|
|
642
|
+
str
|
|
643
|
+
File name of the figure written.
|
|
644
|
+
|
|
645
|
+
"""
|
|
646
|
+
|
|
647
|
+
with ReportFigContext(matplotlib, plt):
|
|
648
|
+
filter = Settings.RomanFilters[self.cfg.use_filter][0]
|
|
649
|
+
print(self.outslab)
|
|
650
|
+
|
|
651
|
+
matplotlib.rcParams.update({"font.size": 10})
|
|
652
|
+
F = plt.figure(figsize=(9, 5.5))
|
|
653
|
+
ntypes = ["white", "1/f", "lab", "simulated"]
|
|
654
|
+
vmax = [0.01, 0.3, 0.05, 5e-5]
|
|
655
|
+
pos = ["Upper left", "Upper right", "Lower left", "Lower right"]
|
|
656
|
+
um = 0.5 / self.s_out
|
|
657
|
+
unit_ = ["arcsec$^2$", "arcsec$^2$", r"$\mu$Jy$^2$/arcsec$^2$", r"(DN/s)$^2$ arcsec$^2$"]
|
|
658
|
+
for k in range(4):
|
|
659
|
+
if self.outslab[k] is not None:
|
|
660
|
+
S = F.add_subplot(2, 2, k + 1)
|
|
661
|
+
S.set_title("Power spectrum: " + ntypes[k] + " noise\n" + unit_[k], usetex=True)
|
|
662
|
+
S.set_xlabel("u [cycles/arcsec]")
|
|
663
|
+
S.set_ylabel("v [cycles/arcsec]")
|
|
664
|
+
with fits.open(self.datastem + "_" + filter + self.suffix + "_ps_avg.fits") as f:
|
|
665
|
+
im = S.imshow(
|
|
666
|
+
f[0].data[self.outslab[k], :, :],
|
|
667
|
+
cmap="gnuplot",
|
|
668
|
+
aspect=1,
|
|
669
|
+
interpolation="nearest",
|
|
670
|
+
origin="lower",
|
|
671
|
+
extent=(-um, um, -um, um),
|
|
672
|
+
norm=colors.LogNorm(vmin=vmax[k] / 300.0, vmax=vmax[k] * 1.0000001, clip=True),
|
|
673
|
+
)
|
|
674
|
+
F.colorbar(im, location="right")
|
|
675
|
+
outfile = self.datastem + "_" + filter + self.suffix + "_3panel.pdf"
|
|
676
|
+
if hasattr(F, "set_layout_engine"):
|
|
677
|
+
F.set_layout_engine("tight")
|
|
678
|
+
else:
|
|
679
|
+
F.set_tight_layout(True)
|
|
680
|
+
F.savefig(outfile)
|
|
681
|
+
plt.close(F)
|
|
682
|
+
|
|
683
|
+
# the caption
|
|
684
|
+
self.tex += "\\begin{figure}\n"
|
|
685
|
+
self.tex += (
|
|
686
|
+
"\\includegraphics[width=6.5in]{"
|
|
687
|
+
+ self.datastem_from_dir
|
|
688
|
+
+ "_"
|
|
689
|
+
+ filter
|
|
690
|
+
+ self.suffix
|
|
691
|
+
+ "_3panel.pdf}\n"
|
|
692
|
+
)
|
|
693
|
+
self.tex += "\\caption{\\label{fig:noise3panel}The 2D power spectra of the noise realizations.\n"
|
|
694
|
+
for k in range(4):
|
|
695
|
+
self.tex += r" {\em " + pos[k] + " panel} (" + ntypes[k] + " noise): "
|
|
696
|
+
if self.outslab[k] is not None:
|
|
697
|
+
self.tex += (
|
|
698
|
+
f"layer {self.noiselayers[self.NLK[self.outslab[k]]]:d} "
|
|
699
|
+
f"(PyIMCOM) $\\rightarrow$ {self.outslab[k]:d} (PS table), name="
|
|
700
|
+
)
|
|
701
|
+
self.tex += "{\\tt " + self.orignames[self.outslab[k]] + "}."
|
|
702
|
+
else:
|
|
703
|
+
self.tex += "not run."
|
|
704
|
+
self.tex += " \n"
|
|
705
|
+
self.tex += "}\n\\end{figure}\n\n"
|
|
706
|
+
|
|
707
|
+
self.tex += "The noise power spectra are shown in Fig.~\\ref{fig:noise3panel}.\n"
|
|
708
|
+
|
|
709
|
+
return outfile
|