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.
Files changed (52) hide show
  1. pyimcom/__init__.py +1 -0
  2. pyimcom/_version.py +24 -0
  3. pyimcom/analysis.py +1480 -0
  4. pyimcom/coadd.py +2331 -0
  5. pyimcom/compress/__init__.py +0 -0
  6. pyimcom/compress/compressutils.py +506 -0
  7. pyimcom/compress/compressutils_wrapper.py +116 -0
  8. pyimcom/compress/i24.py +514 -0
  9. pyimcom/config.py +1245 -0
  10. pyimcom/diagnostics/__init__.py +0 -0
  11. pyimcom/diagnostics/context_figure.py +58 -0
  12. pyimcom/diagnostics/dynrange.py +274 -0
  13. pyimcom/diagnostics/layer_diagnostics.py +208 -0
  14. pyimcom/diagnostics/mosaicimage.py +80 -0
  15. pyimcom/diagnostics/noise/stability.py +126 -0
  16. pyimcom/diagnostics/noise_diagnostics.py +709 -0
  17. pyimcom/diagnostics/outimage_utils/__init__.py +0 -0
  18. pyimcom/diagnostics/outimage_utils/helper.py +82 -0
  19. pyimcom/diagnostics/report.py +366 -0
  20. pyimcom/diagnostics/run.py +64 -0
  21. pyimcom/diagnostics/starcube_nonoise.py +264 -0
  22. pyimcom/diagnostics/starcube_nonoise_coldescr.txt +24 -0
  23. pyimcom/diagnostics/stars.py +469 -0
  24. pyimcom/imdestripe.py +2454 -0
  25. pyimcom/lakernel.py +805 -0
  26. pyimcom/layer.py +1439 -0
  27. pyimcom/layer_wrapper.py +96 -0
  28. pyimcom/meta/__init__.py +0 -0
  29. pyimcom/meta/distortimage.py +748 -0
  30. pyimcom/meta/ginterp.py +340 -0
  31. pyimcom/pictures/__init__.py +0 -0
  32. pyimcom/pictures/genpic.py +229 -0
  33. pyimcom/psfutil.py +2199 -0
  34. pyimcom/routine.py +588 -0
  35. pyimcom/splitpsf/__init__.py +0 -0
  36. pyimcom/splitpsf/imsubtract.py +793 -0
  37. pyimcom/splitpsf/imsubtract_wrapper.py +107 -0
  38. pyimcom/splitpsf/splitpsf.py +497 -0
  39. pyimcom/splitpsf/splitpsf_wrapper.py +161 -0
  40. pyimcom/splitpsf/update_cube.py +136 -0
  41. pyimcom/truthcats.py +396 -0
  42. pyimcom/utils/__init__.py +0 -0
  43. pyimcom/utils/compareutils.py +207 -0
  44. pyimcom/utils/piffutils.py +223 -0
  45. pyimcom/wcsutil.py +839 -0
  46. pyimcom-1.2.1.dist-info/METADATA +67 -0
  47. pyimcom-1.2.1.dist-info/RECORD +52 -0
  48. pyimcom-1.2.1.dist-info/WHEEL +5 -0
  49. pyimcom-1.2.1.dist-info/licenses/LICENSE +21 -0
  50. pyimcom-1.2.1.dist-info/scm_file_list.json +285 -0
  51. pyimcom-1.2.1.dist-info/scm_version.json +8 -0
  52. 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