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/coadd.py
ADDED
|
@@ -0,0 +1,2331 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Driver to coadd a block (2D array of postage stamps).
|
|
3
|
+
|
|
4
|
+
Classes
|
|
5
|
+
-------
|
|
6
|
+
InImage
|
|
7
|
+
Input image attached to a Block instance.
|
|
8
|
+
InStamp
|
|
9
|
+
Data structure for input pixel positions and signals.
|
|
10
|
+
OutStamp
|
|
11
|
+
Driver for postage stamp coaddition.
|
|
12
|
+
Block
|
|
13
|
+
Driver for block coaddition.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import datetime
|
|
18
|
+
import gc
|
|
19
|
+
import sys
|
|
20
|
+
from itertools import combinations, product
|
|
21
|
+
from os.path import exists
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
# F401-flagged imports are important for getting a version number
|
|
25
|
+
import asdf
|
|
26
|
+
import astropy # noqa: F401
|
|
27
|
+
import fitsio
|
|
28
|
+
import matplotlib as mpl
|
|
29
|
+
import matplotlib.pyplot as plt
|
|
30
|
+
import numpy as np
|
|
31
|
+
import pytz
|
|
32
|
+
import scipy # noqa: F401
|
|
33
|
+
from astropy import units as u
|
|
34
|
+
from astropy import wcs
|
|
35
|
+
from astropy.io import fits
|
|
36
|
+
from astropy.table import Table
|
|
37
|
+
from filelock import FileLock, Timeout
|
|
38
|
+
from scipy.special import legendre
|
|
39
|
+
|
|
40
|
+
from .config import Config, Timer, format_axis, format_axis_pars
|
|
41
|
+
from .config import Settings as Stn
|
|
42
|
+
from .lakernel import CholKernel, EigenKernel, EmpirKernel, IterKernel
|
|
43
|
+
from .layer import Mask, check_if_idsca_exists, get_all_data
|
|
44
|
+
from .psfutil import PSFGrp, PSFInterpolator, PSFOvl, SysMatA, SysMatB
|
|
45
|
+
from .wcsutil import PyIMCOM_WCS
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class InImage:
|
|
49
|
+
"""
|
|
50
|
+
Input image attached to a Block instance.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
blk : Block
|
|
55
|
+
The Block instance to which this InImage instance is attached.
|
|
56
|
+
idsca : (int, int)
|
|
57
|
+
ID of observation and SCA used.
|
|
58
|
+
|
|
59
|
+
Methods
|
|
60
|
+
-------
|
|
61
|
+
__init__
|
|
62
|
+
Constructor.
|
|
63
|
+
generate_idx_grid
|
|
64
|
+
Generate a grid of indices (staticmethod).
|
|
65
|
+
_inpix2world2outpix
|
|
66
|
+
Composition of pix2world and world2pix.
|
|
67
|
+
outpix2world2inpix
|
|
68
|
+
Inverse function of _inpix2world2outpix.
|
|
69
|
+
partition_pixels
|
|
70
|
+
Partition input pixels into postage stamps.
|
|
71
|
+
extract_layers
|
|
72
|
+
Extract input layers.
|
|
73
|
+
clear
|
|
74
|
+
Free up memory space.
|
|
75
|
+
smooth_and_pad
|
|
76
|
+
Utility to smear a PSF with a tophat and a Gaussian (staticmethod).
|
|
77
|
+
LPolyArr
|
|
78
|
+
Utility to generate an array of Legendre polynomials (staticmethod).
|
|
79
|
+
psf_filename
|
|
80
|
+
PSF file name broker (staticmethod).
|
|
81
|
+
get_psf_pos
|
|
82
|
+
Get input PSF array at given position.
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(self, blk: "Block", idsca: tuple[int, int]) -> None:
|
|
87
|
+
self.blk = blk
|
|
88
|
+
self.idsca = idsca
|
|
89
|
+
|
|
90
|
+
self.exists_, self.infile = check_if_idsca_exists(blk.cfg, blk.obsdata, idsca)
|
|
91
|
+
if self.exists_:
|
|
92
|
+
# accept either a FITS Header or ASDF + GWCS
|
|
93
|
+
|
|
94
|
+
if self.infile[-5:] == ".fits":
|
|
95
|
+
with fits.open(self.infile) as f:
|
|
96
|
+
self.inwcs = wcs.WCS(f[Stn.hdu_with_wcs].header)
|
|
97
|
+
# print('read WCS:', self.infile, 'HDU', Stn.hdu_with_wcs)
|
|
98
|
+
|
|
99
|
+
if self.infile[-5:] == ".asdf":
|
|
100
|
+
with asdf.open(self.infile) as f:
|
|
101
|
+
self.inwcs = PyIMCOM_WCS(f["roman"]["meta"]["wcs"])
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def generate_idx_grid(xs: np.array, ys: np.array) -> np.array:
|
|
105
|
+
"""
|
|
106
|
+
Generate a grid of indices.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
xs : np.array
|
|
111
|
+
x values of the grid, length (nx,)
|
|
112
|
+
ys : np.array, shape : (ny,)
|
|
113
|
+
y values of the grid, length (ny,)
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
np.array
|
|
118
|
+
All combinations of xs elements and ys elements. Shape (nx*ny,2)
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
return np.moveaxis(np.array(np.meshgrid(xs, ys)), 0, -1).reshape(-1, 2)
|
|
123
|
+
|
|
124
|
+
def _inpix2world2outpix(self, inxys: np.array) -> np.array:
|
|
125
|
+
"""
|
|
126
|
+
Composition of pix2world and world2pix.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
inxys : np.array
|
|
131
|
+
x and y positions in the input image coordinates, shape (npix, 2)
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
np.array
|
|
136
|
+
x and y positions in the output block coordinates, shape (npix, 2).
|
|
137
|
+
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
return self.blk.outwcs.all_world2pix(self.inwcs.all_pix2world(inxys, 0), 0)
|
|
141
|
+
|
|
142
|
+
def outpix2world2inpix(self, outxys: np.array) -> np.array:
|
|
143
|
+
"""
|
|
144
|
+
Inverse function of _inpix2world2outpix.
|
|
145
|
+
|
|
146
|
+
Parameters
|
|
147
|
+
----------
|
|
148
|
+
outxys : np.array
|
|
149
|
+
x and y positions in the output block coordinates, shape (npix, 2).
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
np.array
|
|
154
|
+
x and y positions in the input image coordinates, shape (npix, 2).
|
|
155
|
+
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
return self.inwcs.all_world2pix(self.blk.outwcs.all_pix2world(outxys, 0), 0)
|
|
159
|
+
|
|
160
|
+
def partition_pixels(
|
|
161
|
+
self, sp_res: int = 90, relax_coef: float = 1.05, verbose: bool = False, visualize: bool = False
|
|
162
|
+
) -> None:
|
|
163
|
+
"""
|
|
164
|
+
Partition input pixels into postage stamps.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
sp_res : int, optional
|
|
169
|
+
Resolution of the sparse grid.
|
|
170
|
+
relax_coef : float, optional
|
|
171
|
+
Coefficient to create enough space for input pixels.
|
|
172
|
+
verbose : bool, optional
|
|
173
|
+
Whether to print verbose output.
|
|
174
|
+
visualize : bool, optional
|
|
175
|
+
Whether to visualize the partition process and results.
|
|
176
|
+
|
|
177
|
+
Returns
|
|
178
|
+
-------
|
|
179
|
+
None
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
if verbose:
|
|
184
|
+
print(f"> partitioning pixels from InImage {self.idsca}", "@", self.blk.timer(), "s")
|
|
185
|
+
|
|
186
|
+
# create a sparse grid of pixels to locate regions of interest
|
|
187
|
+
sp_arr = np.linspace(0, Stn.sca_nside, sp_res + 1, dtype=np.uint16)
|
|
188
|
+
sp_inxys = InImage.generate_idx_grid(sp_arr, sp_arr)
|
|
189
|
+
sp_outxys = self._inpix2world2outpix(sp_inxys).T.reshape(2, sp_res + 1, sp_res + 1)
|
|
190
|
+
del sp_inxys
|
|
191
|
+
|
|
192
|
+
# limits for input pixel positions in output pixel coordinates
|
|
193
|
+
pix_lower = -self.blk.cfg.n2 - 0.5
|
|
194
|
+
pix_upper = self.blk.cfg.NsideP + self.blk.cfg.n2 - 0.5
|
|
195
|
+
|
|
196
|
+
self.is_relevant = False # whether the input image is relevant to the output block
|
|
197
|
+
relevant_matrix = np.zeros((sp_res, sp_res), dtype=bool)
|
|
198
|
+
for j in range(1, sp_res):
|
|
199
|
+
for i in range(1, sp_res):
|
|
200
|
+
if not (
|
|
201
|
+
pix_lower < sp_outxys[0, j, i] < pix_upper and pix_lower < sp_outxys[1, j, i] < pix_upper
|
|
202
|
+
):
|
|
203
|
+
continue
|
|
204
|
+
i_st = int((sp_outxys[0, j, i] - pix_lower) // self.blk.cfg.n2) # st stands for stamp
|
|
205
|
+
j_st = int((sp_outxys[1, j, i] - pix_lower) // self.blk.cfg.n2)
|
|
206
|
+
# assert i_st >= 0 and j_st >= 0, 'i_st < 0 or j_st < 0'
|
|
207
|
+
|
|
208
|
+
if np.any(
|
|
209
|
+
self.blk.use_instamps[
|
|
210
|
+
max(j_st - 2, 0) : min(j_st + 3, self.blk.cfg.n1P + 2),
|
|
211
|
+
max(i_st - 2, 0) : min(i_st + 3, self.blk.cfg.n1P + 2),
|
|
212
|
+
]
|
|
213
|
+
):
|
|
214
|
+
# at least some of the input pixels are relevant
|
|
215
|
+
self.is_relevant = True
|
|
216
|
+
# we will study all the adjacent input pixels
|
|
217
|
+
relevant_matrix[
|
|
218
|
+
max(j - 2, 0) : min(j + 3, sp_res), max(i - 2, 0) : min(i + 3, sp_res)
|
|
219
|
+
] = True
|
|
220
|
+
|
|
221
|
+
if visualize and self.is_relevant:
|
|
222
|
+
with mpl.rc_context(format_axis_pars):
|
|
223
|
+
fig, axs = plt.subplots(2, 2, figsize=(10.8, 9.6))
|
|
224
|
+
|
|
225
|
+
for i in range(2):
|
|
226
|
+
ax = axs[0, i]
|
|
227
|
+
im = ax.imshow(sp_outxys[i] / self.blk.cfg.n2, origin="lower")
|
|
228
|
+
plt.colorbar(im, ax=ax)
|
|
229
|
+
ax.contour(sp_outxys[i], levels=[pix_lower, pix_upper], colors="r")
|
|
230
|
+
|
|
231
|
+
im = axs[1, 0].imshow(relevant_matrix, origin="lower", cmap="YlGn")
|
|
232
|
+
plt.colorbar(im, ax=axs[1, 0])
|
|
233
|
+
|
|
234
|
+
for ax, title in zip(
|
|
235
|
+
[*axs[0], axs[1, 0]],
|
|
236
|
+
["stamp index $i$", "stamp index $j$", "relevant matrix"],
|
|
237
|
+
strict=False,
|
|
238
|
+
):
|
|
239
|
+
ax.set_xlabel("sparse grid $i$")
|
|
240
|
+
ax.set_ylabel("sparse grid $j$")
|
|
241
|
+
ax.set_title(title)
|
|
242
|
+
format_axis(ax, False)
|
|
243
|
+
|
|
244
|
+
del sp_outxys
|
|
245
|
+
|
|
246
|
+
if not self.is_relevant:
|
|
247
|
+
del sp_arr, relevant_matrix
|
|
248
|
+
return
|
|
249
|
+
print("input image", self.idsca, flush=True)
|
|
250
|
+
|
|
251
|
+
# maximum number of input pixels per postage stamp (from this InImage)
|
|
252
|
+
# relax_coef: the actual maximum may be larger due to distortions
|
|
253
|
+
npixmax = int(
|
|
254
|
+
(
|
|
255
|
+
(self.blk.cfg.n2 * self.blk.cfg.dtheta * u.degree.to("arcsec"))
|
|
256
|
+
/ (Stn.pixscale_native / Stn.arcsec)
|
|
257
|
+
+ 1
|
|
258
|
+
)
|
|
259
|
+
** 2
|
|
260
|
+
* relax_coef
|
|
261
|
+
) # default: about 160
|
|
262
|
+
|
|
263
|
+
# arrays for indices (in the input image grid),
|
|
264
|
+
self.y_idx = np.zeros((self.blk.cfg.n1P + 2, self.blk.cfg.n1P + 2, npixmax), dtype=np.uint16)
|
|
265
|
+
self.x_idx = np.zeros((self.blk.cfg.n1P + 2, self.blk.cfg.n1P + 2, npixmax), dtype=np.uint16)
|
|
266
|
+
# positions (in the output block coordinates),
|
|
267
|
+
self.y_val = np.zeros((self.blk.cfg.n1P + 2, self.blk.cfg.n1P + 2, npixmax), dtype=np.float64)
|
|
268
|
+
self.x_val = np.zeros((self.blk.cfg.n1P + 2, self.blk.cfg.n1P + 2, npixmax), dtype=np.float64)
|
|
269
|
+
# and number of pixels in each postage stamp (from this InImage)
|
|
270
|
+
self.pix_count = np.zeros((self.blk.cfg.n1P + 2, self.blk.cfg.n1P + 2), dtype=np.uint32)
|
|
271
|
+
|
|
272
|
+
# load masks here
|
|
273
|
+
if self.blk.pmask is not None:
|
|
274
|
+
mask = self.blk.pmask[self.idsca[1] - 1]
|
|
275
|
+
else:
|
|
276
|
+
mask = np.ones((Stn.sca_nside, Stn.sca_nside), dtype=bool)
|
|
277
|
+
|
|
278
|
+
get_all_data(self) # shape : (n_inframe, Stn.sca_nside, Stn.sca_nside)
|
|
279
|
+
|
|
280
|
+
cr_mask = Mask.load_cr_mask(self)
|
|
281
|
+
if cr_mask is not None:
|
|
282
|
+
mask = np.logical_and(mask, cr_mask)
|
|
283
|
+
del cr_mask
|
|
284
|
+
|
|
285
|
+
# extract mask from file
|
|
286
|
+
mask &= Mask.load_mask_from_maskfile(self.blk.cfg, self.blk.obsdata, self.idsca)
|
|
287
|
+
|
|
288
|
+
# if there's already a mask, *REPLACE* it here
|
|
289
|
+
# This is important in the iterative PSF approach because we may need to change the input
|
|
290
|
+
# layers at various stages of the calculation.
|
|
291
|
+
if bool(self.blk.cfg.inlayercache):
|
|
292
|
+
inlayer_mask_filepath = (
|
|
293
|
+
self.blk.cfg.inlayercache + f"_{self.idsca[0]:08d}_{self.idsca[1]:02d}_mask.fits"
|
|
294
|
+
)
|
|
295
|
+
inlayer_mask_lockpath = inlayer_mask_filepath + ".lock"
|
|
296
|
+
lock = FileLock(inlayer_mask_lockpath)
|
|
297
|
+
if exists(inlayer_mask_filepath):
|
|
298
|
+
try:
|
|
299
|
+
with lock.acquire(timeout=300):
|
|
300
|
+
print("loading input mask <<", inlayer_mask_filepath)
|
|
301
|
+
with fits.open(inlayer_mask_filepath) as f:
|
|
302
|
+
mask = f[0].data > 0
|
|
303
|
+
except Timeout:
|
|
304
|
+
raise Exception("timeout while waiting for file:", inlayer_mask_filepath) from None
|
|
305
|
+
else:
|
|
306
|
+
try:
|
|
307
|
+
with lock.acquire(timeout=300):
|
|
308
|
+
print("saving input mask >>", inlayer_mask_filepath)
|
|
309
|
+
fits.PrimaryHDU(np.where(mask, 1, 0).astype(np.uint8)).writeto(
|
|
310
|
+
inlayer_mask_filepath, overwrite=True
|
|
311
|
+
)
|
|
312
|
+
except Timeout:
|
|
313
|
+
raise Exception("timeout while waiting for file:", inlayer_mask_filepath) from None
|
|
314
|
+
sys.stdout.flush()
|
|
315
|
+
|
|
316
|
+
# now loop over the regions set by the sparse grid
|
|
317
|
+
for j_sp in range(sp_res):
|
|
318
|
+
for i_sp in range(sp_res):
|
|
319
|
+
if not relevant_matrix[j_sp, i_sp]:
|
|
320
|
+
continue
|
|
321
|
+
left, right = sp_arr[i_sp : i_sp + 2]
|
|
322
|
+
bottom, top = sp_arr[j_sp : j_sp + 2]
|
|
323
|
+
inxys = InImage.generate_idx_grid(np.arange(left, right), np.arange(bottom, top))
|
|
324
|
+
outxys = self._inpix2world2outpix(inxys).T.reshape(2, top - bottom, right - left)
|
|
325
|
+
|
|
326
|
+
for j in range(top - bottom):
|
|
327
|
+
for i in range(right - left):
|
|
328
|
+
my_x, my_y = outxys[:, j, i]
|
|
329
|
+
if not (pix_lower < my_x < pix_upper and pix_lower < my_y < pix_upper):
|
|
330
|
+
continue
|
|
331
|
+
if not mask[bottom + j, left + i]:
|
|
332
|
+
continue
|
|
333
|
+
|
|
334
|
+
i_st = int((my_x - pix_lower) // self.blk.cfg.n2) # st stands for stamp
|
|
335
|
+
j_st = int((my_y - pix_lower) // self.blk.cfg.n2)
|
|
336
|
+
if not self.blk.use_instamps[j_st, i_st]:
|
|
337
|
+
continue
|
|
338
|
+
|
|
339
|
+
my_idx = self.pix_count[j_st, i_st]
|
|
340
|
+
self.y_idx[j_st, i_st, my_idx] = bottom + j
|
|
341
|
+
self.x_idx[j_st, i_st, my_idx] = left + i
|
|
342
|
+
self.y_val[j_st, i_st, my_idx] = my_y
|
|
343
|
+
self.x_val[j_st, i_st, my_idx] = my_x
|
|
344
|
+
self.pix_count[j_st, i_st] += 1
|
|
345
|
+
|
|
346
|
+
del inxys, outxys
|
|
347
|
+
|
|
348
|
+
if visualize and self.is_relevant:
|
|
349
|
+
with mpl.rc_context(format_axis_pars):
|
|
350
|
+
ax = axs[1, 1]
|
|
351
|
+
im = ax.imshow(self.pix_count, origin="lower", cmap="plasma")
|
|
352
|
+
plt.colorbar(im, ax=ax)
|
|
353
|
+
|
|
354
|
+
ax.set_xlabel("stamp index $i$")
|
|
355
|
+
ax.set_ylabel("stamp index $j$")
|
|
356
|
+
ax.set_title("pixel count")
|
|
357
|
+
format_axis(ax, False)
|
|
358
|
+
|
|
359
|
+
plt.show()
|
|
360
|
+
|
|
361
|
+
if verbose:
|
|
362
|
+
print("-->", np.sum(self.pix_count), "pixels selected from idsca", self.idsca, end="; ")
|
|
363
|
+
self.max_count = np.max(self.pix_count)
|
|
364
|
+
if verbose:
|
|
365
|
+
print("the most populous stamp has", self.max_count, "pixels")
|
|
366
|
+
del sp_arr, relevant_matrix, mask
|
|
367
|
+
|
|
368
|
+
def extract_layers(self, verbose: bool = False) -> None:
|
|
369
|
+
"""
|
|
370
|
+
Extract input layers.
|
|
371
|
+
|
|
372
|
+
Returns
|
|
373
|
+
-------
|
|
374
|
+
None
|
|
375
|
+
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
assert self.exists_, "Error: input image and/or input psf do(es) not exist"
|
|
379
|
+
|
|
380
|
+
self.data = np.zeros(
|
|
381
|
+
(self.blk.cfg.n_inframe, self.blk.cfg.n1P + 2, self.blk.cfg.n1P + 2, self.max_count),
|
|
382
|
+
dtype=np.float32,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
for j_st in range(self.blk.cfg.n1P + 2):
|
|
386
|
+
for i_st in range(self.blk.cfg.n1P + 2):
|
|
387
|
+
n_pix = self.pix_count[j_st, i_st]
|
|
388
|
+
self.data[:, j_st, i_st, :n_pix] = self.indata[
|
|
389
|
+
:, self.y_idx[j_st, i_st, :n_pix], self.x_idx[j_st, i_st, :n_pix]
|
|
390
|
+
]
|
|
391
|
+
|
|
392
|
+
del self.indata, self.y_idx, self.x_idx
|
|
393
|
+
if verbose:
|
|
394
|
+
print(f"--> finished extracting layers for InImage {self.idsca}", "@", self.blk.timer(), "s")
|
|
395
|
+
|
|
396
|
+
def clear(self) -> None:
|
|
397
|
+
"""
|
|
398
|
+
Free up memory space.
|
|
399
|
+
|
|
400
|
+
Returns
|
|
401
|
+
-------
|
|
402
|
+
None.
|
|
403
|
+
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
if self.is_relevant:
|
|
407
|
+
del self.y_val, self.x_val, self.pix_count, self.data
|
|
408
|
+
|
|
409
|
+
if hasattr(self, "inpsf_arr"):
|
|
410
|
+
del self.inpsf_arr
|
|
411
|
+
if hasattr(self, "inpsf_cube"):
|
|
412
|
+
del self.inpsf_cube
|
|
413
|
+
|
|
414
|
+
@staticmethod
|
|
415
|
+
def smooth_and_pad(inArray: np.array, tophatwidth: float = 0.0, gaussiansigma: float = 0.0) -> np.array:
|
|
416
|
+
"""
|
|
417
|
+
Utility to smear a PSF with a tophat and a Gaussian.
|
|
418
|
+
|
|
419
|
+
Parameters
|
|
420
|
+
----------
|
|
421
|
+
inArray : np.array
|
|
422
|
+
Input PSF array to be smeared. Shape (ny, nx)
|
|
423
|
+
tophatwidth : float, optional
|
|
424
|
+
Width of the tophat in pixels. The default is 0.0.
|
|
425
|
+
gaussiansigma : float, optional
|
|
426
|
+
Both in units of the pixels given (not native pixel).
|
|
427
|
+
|
|
428
|
+
Returns
|
|
429
|
+
-------
|
|
430
|
+
outArray : np.array
|
|
431
|
+
Smeared input PSF array, shape (ny+npad*2, nx+npad*2)
|
|
432
|
+
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
npad = int(np.ceil(tophatwidth + 6 * gaussiansigma + 1))
|
|
436
|
+
npad += (4 - npad) % 4 # make a multiple of 4
|
|
437
|
+
(ny, nx) = np.shape(inArray)
|
|
438
|
+
nyy = ny + npad * 2
|
|
439
|
+
nxx = nx + npad * 2
|
|
440
|
+
outArray = np.zeros((nyy, nxx))
|
|
441
|
+
outArray[npad:-npad, npad:-npad] = inArray
|
|
442
|
+
outArrayFT = np.fft.fft2(outArray)
|
|
443
|
+
|
|
444
|
+
# convolution
|
|
445
|
+
uy = np.linspace(0, nyy - 1, nyy) / nyy
|
|
446
|
+
uy = np.where(uy > 0.5, uy - 1, uy)
|
|
447
|
+
ux = np.linspace(0, nxx - 1, nxx) / nxx
|
|
448
|
+
ux = np.where(ux > 0.5, ux - 1, ux)
|
|
449
|
+
outArrayFT *= (
|
|
450
|
+
np.sinc(ux[None, :] * tophatwidth)
|
|
451
|
+
* np.sinc(uy[:, None] * tophatwidth)
|
|
452
|
+
* np.exp(-2.0 * np.pi**2 * gaussiansigma**2 * (ux[None, :] ** 2 + uy[:, None] ** 2))
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
outArray = np.real(np.fft.ifft2(outArrayFT))
|
|
456
|
+
return outArray
|
|
457
|
+
|
|
458
|
+
@staticmethod
|
|
459
|
+
def LPolyArr(PORDER, u_, v_):
|
|
460
|
+
"""
|
|
461
|
+
Generates an array of the Legendre polynomials.
|
|
462
|
+
|
|
463
|
+
Parameters
|
|
464
|
+
----------
|
|
465
|
+
PORDER : int
|
|
466
|
+
>=0, order in each axis
|
|
467
|
+
u_ : float
|
|
468
|
+
x-position on chip scaled to -1..+1
|
|
469
|
+
v_ : float
|
|
470
|
+
y-position on chip scaled to -1..+1
|
|
471
|
+
|
|
472
|
+
Returns
|
|
473
|
+
-------
|
|
474
|
+
arr: np.array, shape : ((PORDER+1)**2)
|
|
475
|
+
the array of Legendre polynomial products, shape : ((PORDER+1)**2)
|
|
476
|
+
|
|
477
|
+
Notes
|
|
478
|
+
-----
|
|
479
|
+
The returned array has length (PORDER+1)**2.
|
|
480
|
+
The constant (1) is first, then increasing x-order, then increasing y-order:
|
|
481
|
+
for n=0..PORDER { for m=0..PORDER { coef P_m(u_) P_n(v_) }}.
|
|
482
|
+
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
ua = np.ones(PORDER + 1)
|
|
486
|
+
va = np.ones(PORDER + 1)
|
|
487
|
+
for m in range(1, PORDER + 1):
|
|
488
|
+
L = legendre(m)
|
|
489
|
+
ua[m] = L(u_)
|
|
490
|
+
va[m] = L(v_)
|
|
491
|
+
arr = np.outer(va, ua).flatten()
|
|
492
|
+
return arr
|
|
493
|
+
|
|
494
|
+
@staticmethod
|
|
495
|
+
def psf_filename(inpsf_format, obsid):
|
|
496
|
+
"""PSF file name broker.
|
|
497
|
+
|
|
498
|
+
Parameters
|
|
499
|
+
----------
|
|
500
|
+
inpsf_format : str
|
|
501
|
+
Format for input PSFs.
|
|
502
|
+
obsid : int
|
|
503
|
+
The observation ID number.
|
|
504
|
+
|
|
505
|
+
Returns
|
|
506
|
+
-------
|
|
507
|
+
str
|
|
508
|
+
File name.
|
|
509
|
+
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
if inpsf_format == "dc2_imsim":
|
|
513
|
+
return f"dc2_psf_{obsid:d}.fits"
|
|
514
|
+
if inpsf_format in ["anlsim", "L2_2506"]:
|
|
515
|
+
return f"psf_polyfit_{obsid:d}.fits"
|
|
516
|
+
|
|
517
|
+
raise AssertionError("psf_filename: should not get here")
|
|
518
|
+
|
|
519
|
+
def get_psf_pos(self, psf_compute_point: np.array, use_shortrange: bool = False) -> np.array:
|
|
520
|
+
"""
|
|
521
|
+
Get input PSF array at given position.
|
|
522
|
+
|
|
523
|
+
This is an interface for layer.get_all_data and psfutil.PSFGrp._build_inpsfgrp.
|
|
524
|
+
|
|
525
|
+
Parameters
|
|
526
|
+
----------
|
|
527
|
+
psf_compute_point : np.array
|
|
528
|
+
Length 2 array, point to compute PSF in RA and Dec.
|
|
529
|
+
use_shortrange : bool, optional
|
|
530
|
+
If True and PSFSPLIT is set in the configuration file, then pulls only the short-range PSF G^(S).
|
|
531
|
+
|
|
532
|
+
Returns
|
|
533
|
+
-------
|
|
534
|
+
np.array
|
|
535
|
+
Input PSF array at given position (see smooth_and_pad for the shape).
|
|
536
|
+
|
|
537
|
+
"""
|
|
538
|
+
|
|
539
|
+
# The tophat width: in use_shortrange, the psfsplit module has already included this,
|
|
540
|
+
# so we set it to 0 so as to not double-count this contribution.
|
|
541
|
+
tophatwidth_use = self.blk.cfg.inpsf_oversamp
|
|
542
|
+
if use_shortrange and self.blk.cfg.psfsplit:
|
|
543
|
+
tophatwidth_use = 0
|
|
544
|
+
|
|
545
|
+
# get the pixel location on the input image
|
|
546
|
+
# (moved this up since some PSF models need it)
|
|
547
|
+
# pixloc = self.inwcs.all_world2pix(np.array([[*psf_compute_point]]).astype(np.float64), 0)[0]
|
|
548
|
+
pixloc = self.inwcs.all_world2pix(psf_compute_point[0], psf_compute_point[1], 0)
|
|
549
|
+
|
|
550
|
+
if self.blk.cfg.inpsf_format == "dc2_imsim":
|
|
551
|
+
if not hasattr(self, "inpsf_arr"):
|
|
552
|
+
fname = (
|
|
553
|
+
self.blk.cfg.inpsf_path
|
|
554
|
+
+ "/"
|
|
555
|
+
+ InImage.psf_filename(self.blk.cfg.inpsf_format, self.idsca[0])
|
|
556
|
+
)
|
|
557
|
+
assert exists(fname), "Error: input psf does not exist"
|
|
558
|
+
with fitsio.FITS(fname) as fileh:
|
|
559
|
+
self.inpsf_arr = InImage.smooth_and_pad(
|
|
560
|
+
fileh[self.idsca[1]][:, :], tophatwidth=tophatwidth_use
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
this_psf = self.inpsf_arr
|
|
564
|
+
|
|
565
|
+
elif self.blk.cfg.inpsf_format == "anlsim" or self.blk.cfg.inpsf_format == "L2_2506":
|
|
566
|
+
if not hasattr(self, "inpsf_cube"):
|
|
567
|
+
fname = (
|
|
568
|
+
self.blk.cfg.inpsf_path
|
|
569
|
+
+ "/"
|
|
570
|
+
+ InImage.psf_filename(self.blk.cfg.inpsf_format, self.idsca[0])
|
|
571
|
+
)
|
|
572
|
+
sskip = 0
|
|
573
|
+
readskip = False
|
|
574
|
+
if use_shortrange and self.blk.cfg.psfsplit:
|
|
575
|
+
fname = self.blk.cfg.inlayercache + f".psf/psf_{self.idsca[0]:d}.fits"
|
|
576
|
+
readskip = True
|
|
577
|
+
assert exists(fname), "Error: input psf does not exist"
|
|
578
|
+
with fits.open(fname) as f:
|
|
579
|
+
if readskip:
|
|
580
|
+
sskip = int(f[0].header["GSSKIP"])
|
|
581
|
+
self.inpsf_cube = f[self.idsca[1] + sskip].data[:, :, :]
|
|
582
|
+
print(" <<", fname, sskip)
|
|
583
|
+
|
|
584
|
+
# Legendre polynomial order
|
|
585
|
+
lporder = int(np.round(np.sqrt(np.shape(self.inpsf_cube)[0]))) - 1
|
|
586
|
+
lpoly = InImage.LPolyArr(lporder, (pixloc[0] - 2043.5) / 2044.0, (pixloc[1] - 2043.5) / 2044.0)
|
|
587
|
+
# pixels are in C/Python convention since pixloc was set this way
|
|
588
|
+
if self.blk.cfg.inpsf_format == "anlsim":
|
|
589
|
+
this_psf = (
|
|
590
|
+
InImage.smooth_and_pad(
|
|
591
|
+
np.einsum("a,aij->ij", lpoly, self.inpsf_cube), tophatwidth=tophatwidth_use
|
|
592
|
+
)
|
|
593
|
+
/ 64
|
|
594
|
+
)
|
|
595
|
+
# divide by 64=8**2 since anlsim files are in fractional intensity per s_in**2 instead of
|
|
596
|
+
# per (s_in/8)**2
|
|
597
|
+
else:
|
|
598
|
+
this_psf = InImage.smooth_and_pad(
|
|
599
|
+
np.einsum("a,aij->ij", lpoly, self.inpsf_cube), tophatwidth=tophatwidth_use
|
|
600
|
+
)
|
|
601
|
+
# L2_2506 and later are per (s_in/ovsamp)**2
|
|
602
|
+
|
|
603
|
+
else:
|
|
604
|
+
raise RuntimeError("Error: input psf does not exist")
|
|
605
|
+
|
|
606
|
+
# test of the astrometry, if requested
|
|
607
|
+
# if np.hypot(pixloc[0]-200, pixloc[1]-3000)<150:
|
|
608
|
+
# print(':::', pixloc, psf_compute_point); sys.stdout.flush()
|
|
609
|
+
return this_psf
|
|
610
|
+
|
|
611
|
+
# when distort_matrice is not required
|
|
612
|
+
# if dWdp_out is None: return this_psf
|
|
613
|
+
|
|
614
|
+
# get the distortion matrices d[(X,Y)perfect]/d[(X,Y)native]
|
|
615
|
+
# Note that rotations and magnifications are included in the distortion matrix, as well as shear
|
|
616
|
+
# Also the distortion is relative to the output grid, not to the tangent plane to the celestial sphere
|
|
617
|
+
# (although we really don't want the difference to be large ...)
|
|
618
|
+
# distort_matrice = np.linalg.inv(dWdp_out) \
|
|
619
|
+
# @ wcs.utils.local_partial_pixel_derivatives(self.inwcs, pixloc[0], pixloc[1]) \
|
|
620
|
+
# * self.blk.cfg.dtheta*Stn.degree/Stn.pixscale_native
|
|
621
|
+
|
|
622
|
+
# print(pixloc, self.blk.cfg.inpsf_oversamp, np.shape(this_psf), np.sum(this_psf))
|
|
623
|
+
# return this_psf, distort_matrice
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
class InStamp:
|
|
627
|
+
"""
|
|
628
|
+
Data structure for input pixel positions and signals.
|
|
629
|
+
|
|
630
|
+
Parameters
|
|
631
|
+
----------
|
|
632
|
+
blk : Block
|
|
633
|
+
The Block instance to which this InStamp instance is attached.
|
|
634
|
+
j_st : int
|
|
635
|
+
InStamp vertical index.
|
|
636
|
+
i_st : int
|
|
637
|
+
InStamp horizontal index.
|
|
638
|
+
|
|
639
|
+
Methods
|
|
640
|
+
-------
|
|
641
|
+
__init__
|
|
642
|
+
Constructor.
|
|
643
|
+
make_selection
|
|
644
|
+
Return the indices of selected input pixels.
|
|
645
|
+
get_inpsfgrp
|
|
646
|
+
Get the input PSFGrp attached to this InStamp.
|
|
647
|
+
clear
|
|
648
|
+
Free up memory space.
|
|
649
|
+
|
|
650
|
+
"""
|
|
651
|
+
|
|
652
|
+
def __init__(self, blk: "Block", j_st: int, i_st: int) -> None:
|
|
653
|
+
self.blk = blk
|
|
654
|
+
self.j_st = j_st
|
|
655
|
+
self.i_st = i_st
|
|
656
|
+
|
|
657
|
+
# numbers of input pixels from input images and the cumulative sum
|
|
658
|
+
self.pix_count = np.array(
|
|
659
|
+
[inimage.pix_count[j_st, i_st] for inimage in blk.inimages], dtype=np.uint32
|
|
660
|
+
)
|
|
661
|
+
self.pix_cumsum = np.cumsum([0] + list(self.pix_count), dtype=np.uint32)
|
|
662
|
+
|
|
663
|
+
# input pixel positions and signals
|
|
664
|
+
self.y_val = np.empty((self.pix_cumsum[-1],), dtype=np.float64)
|
|
665
|
+
self.x_val = np.empty((self.pix_cumsum[-1],), dtype=np.float64)
|
|
666
|
+
self.data = np.empty((blk.cfg.n_inframe, self.pix_cumsum[-1]), dtype=np.float32)
|
|
667
|
+
|
|
668
|
+
for i_im, inimage in enumerate(blk.inimages):
|
|
669
|
+
self.y_val[self.pix_cumsum[i_im] : self.pix_cumsum[i_im + 1]] = inimage.y_val[
|
|
670
|
+
j_st, i_st, : self.pix_count[i_im]
|
|
671
|
+
]
|
|
672
|
+
self.x_val[self.pix_cumsum[i_im] : self.pix_cumsum[i_im + 1]] = inimage.x_val[
|
|
673
|
+
j_st, i_st, : self.pix_count[i_im]
|
|
674
|
+
]
|
|
675
|
+
self.data[:, self.pix_cumsum[i_im] : self.pix_cumsum[i_im + 1]] = inimage.data[
|
|
676
|
+
:, j_st, i_st, : self.pix_count[i_im]
|
|
677
|
+
]
|
|
678
|
+
|
|
679
|
+
if j_st % 2 == 0 and i_st % 2 == 0:
|
|
680
|
+
# get where to compute the PSF and the camera distortion matrix
|
|
681
|
+
# (the center of the 2x2 group of postage stamps)
|
|
682
|
+
self.psf_compute_point_pix = [i_st * blk.cfg.n2 - 0.5, j_st * blk.cfg.n2 - 0.5]
|
|
683
|
+
self.inpsfgrp = None
|
|
684
|
+
self.inpsfgrp_ref = 0
|
|
685
|
+
|
|
686
|
+
def make_selection(self, pivot: tuple[float, float] = (None, None), radius: float = None) -> np.array:
|
|
687
|
+
"""
|
|
688
|
+
Return the indices of selected input pixels.
|
|
689
|
+
|
|
690
|
+
This is an interface for OutStamp._process_input_stamps.
|
|
691
|
+
|
|
692
|
+
Parameters
|
|
693
|
+
----------
|
|
694
|
+
pivot : (float or None, float or None), optional
|
|
695
|
+
Pivot position in the output block coordinates.
|
|
696
|
+
If None in one direction, select input pixels according to the other;
|
|
697
|
+
if None in both directions, select all input pixels.
|
|
698
|
+
radius : float or None, optional
|
|
699
|
+
Select input pixels within this radius.
|
|
700
|
+
|
|
701
|
+
Returns
|
|
702
|
+
-------
|
|
703
|
+
np.array or None
|
|
704
|
+
Indices of selected input pixels (if there are any).
|
|
705
|
+
None if selecting all input pixels.
|
|
706
|
+
|
|
707
|
+
"""
|
|
708
|
+
|
|
709
|
+
if pivot == (None, None) or radius is None:
|
|
710
|
+
return None # select all pixels
|
|
711
|
+
|
|
712
|
+
dist_sq = np.zeros((self.pix_cumsum[-1],))
|
|
713
|
+
if pivot[0] is not None:
|
|
714
|
+
dist_sq += np.square(self.x_val - pivot[0])
|
|
715
|
+
if pivot[1] is not None:
|
|
716
|
+
dist_sq += np.square(self.y_val - pivot[1])
|
|
717
|
+
|
|
718
|
+
selection = np.array(np.where(dist_sq < radius**2)[0], dtype=np.uint32)
|
|
719
|
+
return selection if (selection.shape[0] < self.pix_cumsum[-1]) else None
|
|
720
|
+
|
|
721
|
+
def get_inpsfgrp(self, sim_mode: bool = False, visualize: bool = False) -> None:
|
|
722
|
+
"""
|
|
723
|
+
Get the input PSFGrp attached to this InStamp.
|
|
724
|
+
|
|
725
|
+
This is an interface for psfutil.SysMatA._compute_iisubmats
|
|
726
|
+
and psfutil.SysMatB.get_iosubmat.
|
|
727
|
+
|
|
728
|
+
Parameters
|
|
729
|
+
----------
|
|
730
|
+
sim_mode : bool, optional
|
|
731
|
+
Whether to count references without actually making inpsfgrp.
|
|
732
|
+
See the docstring of psfutil.SysMatA._compute_iisubmats.
|
|
733
|
+
visualize : bool, optional
|
|
734
|
+
Whether to visualize the PSF group.
|
|
735
|
+
|
|
736
|
+
Returns
|
|
737
|
+
-------
|
|
738
|
+
None
|
|
739
|
+
|
|
740
|
+
"""
|
|
741
|
+
|
|
742
|
+
if sim_mode: # count references, no actual inpsfgrp involved
|
|
743
|
+
self.inpsfgrp_ref += 1
|
|
744
|
+
return
|
|
745
|
+
|
|
746
|
+
if self.inpsfgrp is None:
|
|
747
|
+
self.inpsfgrp = PSFGrp(in_or_out=True, inst=self, visualize=visualize)
|
|
748
|
+
|
|
749
|
+
self.inpsfgrp_ref -= 1
|
|
750
|
+
if self.inpsfgrp_ref > 0:
|
|
751
|
+
return self.inpsfgrp
|
|
752
|
+
else:
|
|
753
|
+
inpsfgrp = self.inpsfgrp
|
|
754
|
+
del self.inpsfgrp
|
|
755
|
+
self.inpsfgrp = None
|
|
756
|
+
return inpsfgrp
|
|
757
|
+
|
|
758
|
+
def clear(self) -> None:
|
|
759
|
+
"""Free up memory space."""
|
|
760
|
+
|
|
761
|
+
del self.pix_count, self.pix_cumsum
|
|
762
|
+
del self.y_val, self.x_val, self.data
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
class OutStamp:
|
|
766
|
+
"""
|
|
767
|
+
Driver for postage stamp coaddition.
|
|
768
|
+
|
|
769
|
+
Parameters
|
|
770
|
+
----------
|
|
771
|
+
blk : Block
|
|
772
|
+
The Block instance to which this InStamp instance is attached.
|
|
773
|
+
j_st : int
|
|
774
|
+
OutStamp index, vertical direction.
|
|
775
|
+
i_st : int
|
|
776
|
+
OutStamp index, horizontal direction.
|
|
777
|
+
visualize : bool, optional
|
|
778
|
+
Whether to run visualizations.
|
|
779
|
+
|
|
780
|
+
Methods
|
|
781
|
+
-------
|
|
782
|
+
__init__
|
|
783
|
+
Constructor.
|
|
784
|
+
_process_input_stamps
|
|
785
|
+
Fetch and process input postage stamps.
|
|
786
|
+
__call__
|
|
787
|
+
Build system matrices and perform coaddition.
|
|
788
|
+
_build_system_matrices
|
|
789
|
+
Build system matrices and coaddition matrices.
|
|
790
|
+
_visualize_system_matrices
|
|
791
|
+
Visualize system matrices.
|
|
792
|
+
_visualize_coadd_matrices
|
|
793
|
+
Visualize coaddition matrices.
|
|
794
|
+
trapezoid
|
|
795
|
+
Apply a trapezoid filter to transition pixels (staticmethod).
|
|
796
|
+
_perform_coaddition
|
|
797
|
+
Perform the actual multiplication.
|
|
798
|
+
_visualize_weight_computations
|
|
799
|
+
Display weight computations.
|
|
800
|
+
_show_in_and_out_images
|
|
801
|
+
Display input and output images.
|
|
802
|
+
_study_individual_pixels
|
|
803
|
+
Study individual input and output pixels.
|
|
804
|
+
clear
|
|
805
|
+
Free up memory space.
|
|
806
|
+
|
|
807
|
+
"""
|
|
808
|
+
|
|
809
|
+
LAKERNEL = {
|
|
810
|
+
"Eigen": EigenKernel,
|
|
811
|
+
"Cholesky": CholKernel,
|
|
812
|
+
"Iterative": IterKernel,
|
|
813
|
+
"Empirical": EmpirKernel,
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
def __init__(self, blk: "Block", j_st: int, i_st: int, visualize: bool = False) -> None:
|
|
817
|
+
self.blk = blk
|
|
818
|
+
self.j_st = j_st
|
|
819
|
+
self.i_st = i_st
|
|
820
|
+
|
|
821
|
+
# list of indices of overlapping and adjacent input postage stamps
|
|
822
|
+
# the final _s indicates plural
|
|
823
|
+
self.ji_st_in_s = [(j_st + dj, i_st + di) for dj in range(-1, 2) for di in range(-1, 2)]
|
|
824
|
+
|
|
825
|
+
# no-quality control option of the empirical kernel
|
|
826
|
+
self.no_qlt_ctrl = False
|
|
827
|
+
if blk.cfg.linear_algebra == "Empirical":
|
|
828
|
+
self.no_qlt_ctrl = blk.cfg.no_qlt_ctrl
|
|
829
|
+
|
|
830
|
+
# count references to PSF overlaps and system submatrices
|
|
831
|
+
if not self.no_qlt_ctrl:
|
|
832
|
+
for ji_st_in in self.ji_st_in_s:
|
|
833
|
+
blk.sysmata.get_iisubmat(ji_st_in, ji_st_in, sim_mode=True, visualize=visualize)
|
|
834
|
+
blk.sysmatb.get_iosubmat(ji_st_in, (j_st, i_st), sim_mode=True, visualize=visualize)
|
|
835
|
+
|
|
836
|
+
for ji_st_pair in combinations(self.ji_st_in_s, 2):
|
|
837
|
+
blk.sysmata.get_iisubmat(*ji_st_pair, sim_mode=True, visualize=visualize)
|
|
838
|
+
|
|
839
|
+
# limit y and x positions of this output postage stamp, all integers
|
|
840
|
+
# not including the transition pixels (of which the number
|
|
841
|
+
# of columns or rows on each side is set by fade_kernel)
|
|
842
|
+
self.bottom = (j_st - 1) * blk.cfg.n2
|
|
843
|
+
self.top = self.bottom + blk.cfg.n2 - 1
|
|
844
|
+
self.left = (i_st - 1) * blk.cfg.n2
|
|
845
|
+
self.right = self.left + blk.cfg.n2 - 1
|
|
846
|
+
|
|
847
|
+
fade_kernel = blk.cfg.fade_kernel # shortcut
|
|
848
|
+
# output pixel positions, all integers
|
|
849
|
+
self.yx_val = np.mgrid[
|
|
850
|
+
self.bottom - fade_kernel : self.top + fade_kernel + 1,
|
|
851
|
+
self.left - fade_kernel : self.right + fade_kernel + 1,
|
|
852
|
+
]
|
|
853
|
+
|
|
854
|
+
self._process_input_stamps(visualize=visualize)
|
|
855
|
+
|
|
856
|
+
def _process_input_stamps(self, visualize: bool = False) -> None:
|
|
857
|
+
"""
|
|
858
|
+
Fetch and process input postage stamps.
|
|
859
|
+
|
|
860
|
+
Parameters
|
|
861
|
+
----------
|
|
862
|
+
visualize : bool, optional
|
|
863
|
+
Whether to visualize the process.
|
|
864
|
+
|
|
865
|
+
Returns
|
|
866
|
+
-------
|
|
867
|
+
None
|
|
868
|
+
|
|
869
|
+
Notes
|
|
870
|
+
-----
|
|
871
|
+
|
|
872
|
+
This method selects input pixels to form a region like this:
|
|
873
|
+
+-----+-----+-----+
|
|
874
|
+
| **|*****|** |
|
|
875
|
+
| ****|*****|**** |
|
|
876
|
+
+-----+-----+-----+
|
|
877
|
+
|*****|*****|*****|
|
|
878
|
+
|*****|*****|*****|
|
|
879
|
+
+-----+-----+-----+
|
|
880
|
+
| ****|*****|**** |
|
|
881
|
+
| **|*****|** |
|
|
882
|
+
+-----+-----+-----+
|
|
883
|
+
where the central postage stamp is the OutStamp we are coadding.
|
|
884
|
+
|
|
885
|
+
"""
|
|
886
|
+
|
|
887
|
+
# fetch instamps and select input pixels
|
|
888
|
+
self.instamps = [None for _ in range(9)]
|
|
889
|
+
self.selections = [None for _ in range(9)]
|
|
890
|
+
self.inpix_count = np.zeros((9,), dtype=np.uint32)
|
|
891
|
+
|
|
892
|
+
# acceptance radius in units of output pixels
|
|
893
|
+
rpix_search = (self.blk.cfg.instamp_pad / Stn.arcsec) / (self.blk.cfg.dtheta * u.degree.to("arcsec"))
|
|
894
|
+
|
|
895
|
+
# now select input pixels
|
|
896
|
+
for idx, ji_st_in in enumerate(self.ji_st_in_s):
|
|
897
|
+
self.instamps[idx] = self.blk.instamps[ji_st_in[0]][ji_st_in[1]]
|
|
898
|
+
|
|
899
|
+
x_pivot = [self.left - 0.5, None, self.right + 0.5][ji_st_in[1] - self.i_st + 1]
|
|
900
|
+
y_pivot = [self.bottom - 0.5, None, self.top + 0.5][ji_st_in[0] - self.j_st + 1]
|
|
901
|
+
self.selections[idx] = self.instamps[idx].make_selection((x_pivot, y_pivot), rpix_search)
|
|
902
|
+
if self.selections[idx] is None:
|
|
903
|
+
self.inpix_count[idx] = self.instamps[idx].pix_cumsum[-1]
|
|
904
|
+
else:
|
|
905
|
+
self.inpix_count[idx] = self.selections[idx].shape[0]
|
|
906
|
+
|
|
907
|
+
self.inpix_cumsum = np.cumsum([0] + list(self.inpix_count), dtype=np.uint32)
|
|
908
|
+
|
|
909
|
+
if visualize:
|
|
910
|
+
with mpl.rc_context(format_axis_pars):
|
|
911
|
+
fig, ax = plt.subplots(figsize=(4.8, 4.8))
|
|
912
|
+
|
|
913
|
+
for idx, instamp, selection in zip(range(9), self.instamps, self.selections, strict=False):
|
|
914
|
+
if selection is None:
|
|
915
|
+
plt.scatter(instamp.x_val, instamp.y_val, s=2, c=f"C{idx}")
|
|
916
|
+
ax.scatter(instamp.x_val[selection], instamp.y_val[selection], s=2, c=f"C{idx}")
|
|
917
|
+
ax.axis("equal")
|
|
918
|
+
|
|
919
|
+
ax.set_xlabel("output grid $i$")
|
|
920
|
+
ax.set_ylabel("output grid $j$")
|
|
921
|
+
format_axis(ax)
|
|
922
|
+
plt.show()
|
|
923
|
+
|
|
924
|
+
# read input pixel positions and signals
|
|
925
|
+
iny_val = []
|
|
926
|
+
inx_val = []
|
|
927
|
+
indata = []
|
|
928
|
+
|
|
929
|
+
for inst, selection in zip(self.instamps, self.selections, strict=False):
|
|
930
|
+
if selection is None:
|
|
931
|
+
iny_val.append(inst.y_val)
|
|
932
|
+
inx_val.append(inst.x_val)
|
|
933
|
+
indata.append(inst.data)
|
|
934
|
+
else:
|
|
935
|
+
iny_val.append(inst.y_val[selection])
|
|
936
|
+
inx_val.append(inst.x_val[selection])
|
|
937
|
+
indata.append(inst.data[:, selection])
|
|
938
|
+
|
|
939
|
+
self.iny_val = np.hstack(iny_val)
|
|
940
|
+
iny_val.clear()
|
|
941
|
+
del iny_val
|
|
942
|
+
self.inx_val = np.hstack(inx_val)
|
|
943
|
+
inx_val.clear()
|
|
944
|
+
del inx_val
|
|
945
|
+
self.indata = np.hstack(indata)
|
|
946
|
+
indata.clear()
|
|
947
|
+
del indata
|
|
948
|
+
|
|
949
|
+
def __call__(self, visualize: bool = False, save_abc: bool = False, save_t: bool = False) -> None:
|
|
950
|
+
"""
|
|
951
|
+
Build system matrices and perform coaddition.
|
|
952
|
+
|
|
953
|
+
Parameters
|
|
954
|
+
----------
|
|
955
|
+
visualize : bool, optional
|
|
956
|
+
Whether to visualize the process.
|
|
957
|
+
save_abc : bool, optional
|
|
958
|
+
Whether to save system matrices.
|
|
959
|
+
save_t : bool, optional
|
|
960
|
+
Whether to save coaddition matrices.
|
|
961
|
+
|
|
962
|
+
Returns
|
|
963
|
+
-------
|
|
964
|
+
None
|
|
965
|
+
|
|
966
|
+
"""
|
|
967
|
+
|
|
968
|
+
self._build_system_matrices(visualize, save_abc)
|
|
969
|
+
self._perform_coaddition(visualize, save_t)
|
|
970
|
+
print()
|
|
971
|
+
|
|
972
|
+
def _build_system_matrices(self, visualize: bool = False, save_abc: bool = False) -> None:
|
|
973
|
+
"""
|
|
974
|
+
Build system matrices and coaddition matrices.
|
|
975
|
+
|
|
976
|
+
Parameters
|
|
977
|
+
----------
|
|
978
|
+
visualize : bool, optional
|
|
979
|
+
Whether to visualize the process.
|
|
980
|
+
save_abc : bool, optional
|
|
981
|
+
Whether to save system matrices.
|
|
982
|
+
|
|
983
|
+
Returns
|
|
984
|
+
-------
|
|
985
|
+
None
|
|
986
|
+
|
|
987
|
+
"""
|
|
988
|
+
|
|
989
|
+
# no-quality control option of the empirical kernel
|
|
990
|
+
if self.no_qlt_ctrl:
|
|
991
|
+
lakernel = OutStamp.LAKERNEL[self.blk.cfg.linear_algebra](self)
|
|
992
|
+
lakernel()
|
|
993
|
+
del lakernel
|
|
994
|
+
# this produces: self.T, (n_out, n_outpix, n_inpix)
|
|
995
|
+
return
|
|
996
|
+
|
|
997
|
+
# the A-matrix first
|
|
998
|
+
self.sysmata = np.zeros((self.inpix_cumsum[-1], self.inpix_cumsum[-1])) # dtype=np.float64
|
|
999
|
+
use_virmem = bool(self.blk.cfg.tempfile)
|
|
1000
|
+
|
|
1001
|
+
for idx, ji_st_in, selection in zip(range(9), self.ji_st_in_s, self.selections, strict=False):
|
|
1002
|
+
iisubmat = self.blk.sysmata.get_iisubmat(
|
|
1003
|
+
ji_st_in, ji_st_in, ji_st_out=(self.j_st, self.i_st) if use_virmem else None
|
|
1004
|
+
)
|
|
1005
|
+
if selection is not None:
|
|
1006
|
+
iisubmat = iisubmat[np.ix_(selection, selection)]
|
|
1007
|
+
|
|
1008
|
+
self.sysmata[
|
|
1009
|
+
self.inpix_cumsum[idx] : self.inpix_cumsum[idx + 1],
|
|
1010
|
+
self.inpix_cumsum[idx] : self.inpix_cumsum[idx + 1],
|
|
1011
|
+
] = iisubmat
|
|
1012
|
+
|
|
1013
|
+
for idx_s, ji_st_pair, selections_ in zip(
|
|
1014
|
+
combinations(range(9), 2),
|
|
1015
|
+
combinations(self.ji_st_in_s, 2),
|
|
1016
|
+
combinations(self.selections, 2),
|
|
1017
|
+
strict=False,
|
|
1018
|
+
):
|
|
1019
|
+
iisubmat = self.blk.sysmata.get_iisubmat(
|
|
1020
|
+
*ji_st_pair, ji_st_out=(self.j_st, self.i_st) if use_virmem else None
|
|
1021
|
+
)
|
|
1022
|
+
if selections_[0] is not None:
|
|
1023
|
+
if selections_[1] is not None:
|
|
1024
|
+
iisubmat = iisubmat[np.ix_(selections_[0], selections_[1])]
|
|
1025
|
+
else:
|
|
1026
|
+
iisubmat = iisubmat[selections_[0], :]
|
|
1027
|
+
else:
|
|
1028
|
+
if selections_[1] is not None:
|
|
1029
|
+
iisubmat = iisubmat[:, selections_[1]]
|
|
1030
|
+
|
|
1031
|
+
self.sysmata[
|
|
1032
|
+
self.inpix_cumsum[idx_s[0]] : self.inpix_cumsum[idx_s[0] + 1],
|
|
1033
|
+
self.inpix_cumsum[idx_s[1]] : self.inpix_cumsum[idx_s[1] + 1],
|
|
1034
|
+
] = iisubmat
|
|
1035
|
+
self.sysmata[
|
|
1036
|
+
self.inpix_cumsum[idx_s[1]] : self.inpix_cumsum[idx_s[1] + 1],
|
|
1037
|
+
self.inpix_cumsum[idx_s[0]] : self.inpix_cumsum[idx_s[0] + 1],
|
|
1038
|
+
] = iisubmat.T
|
|
1039
|
+
del iisubmat
|
|
1040
|
+
|
|
1041
|
+
# force exact symmetry
|
|
1042
|
+
# A = (A+A.T)/2.
|
|
1043
|
+
|
|
1044
|
+
# now the mBhalf matrix
|
|
1045
|
+
self.mhalfb = np.zeros(
|
|
1046
|
+
(self.blk.outpsfgrp.n_psf, self.blk.cfg.n2f**2, self.inpix_cumsum[-1])
|
|
1047
|
+
) # dtype=np.float64
|
|
1048
|
+
|
|
1049
|
+
for idx, ji_st_in in zip(range(9), self.ji_st_in_s, strict=False):
|
|
1050
|
+
self.mhalfb[
|
|
1051
|
+
:, :, self.inpix_cumsum[idx] : self.inpix_cumsum[idx + 1]
|
|
1052
|
+
] = self.blk.sysmatb.get_iosubmat(ji_st_in, (self.j_st, self.i_st), visualize=visualize)
|
|
1053
|
+
|
|
1054
|
+
# and C
|
|
1055
|
+
self.outovlc = self.blk.outpsfovl.outovlc
|
|
1056
|
+
|
|
1057
|
+
if visualize:
|
|
1058
|
+
print()
|
|
1059
|
+
self._visualize_system_matrices()
|
|
1060
|
+
|
|
1061
|
+
lakernel = OutStamp.LAKERNEL[self.blk.cfg.linear_algebra](self)
|
|
1062
|
+
lakernel()
|
|
1063
|
+
del lakernel
|
|
1064
|
+
# this produces: self.T, self.UC, self.Sigma, self.kappa
|
|
1065
|
+
# T: (n_out, n_outpix, n_inpix); others: (n_out, n2f, n2f)
|
|
1066
|
+
|
|
1067
|
+
# now visualize (if requested) and save
|
|
1068
|
+
if visualize:
|
|
1069
|
+
print()
|
|
1070
|
+
self._visualize_coadd_matrices()
|
|
1071
|
+
if not save_abc:
|
|
1072
|
+
del self.sysmata, self.mhalfb, self.outovlc
|
|
1073
|
+
|
|
1074
|
+
if self.blk.cfg.linear_algebra == "Iterative":
|
|
1075
|
+
# these could be negative as the iterative kernel is not exact
|
|
1076
|
+
self.UC = np.maximum(self.UC, 1e-32)
|
|
1077
|
+
self.Sigma = np.maximum(self.Sigma, 1e-32)
|
|
1078
|
+
|
|
1079
|
+
print(" n input pix =", self.T.shape[-1])
|
|
1080
|
+
sumstats = " sqUC,sqSig %iles |"
|
|
1081
|
+
for i in [50, 90, 98, 99]:
|
|
1082
|
+
sumstats += (
|
|
1083
|
+
f" {i:2d}% {np.percentile(np.sqrt(self.UC), i):8.2E} "
|
|
1084
|
+
f"{np.percentile(np.sqrt(self.Sigma), i):8.2E} |"
|
|
1085
|
+
)
|
|
1086
|
+
print(sumstats, flush=True)
|
|
1087
|
+
|
|
1088
|
+
if self.blk.cfg.fade_kernel > 0:
|
|
1089
|
+
fade_kernel = self.blk.cfg.fade_kernel # shortcut
|
|
1090
|
+
OutStamp.trapezoid(self.kappa, fade_kernel)
|
|
1091
|
+
OutStamp.trapezoid(self.Sigma, fade_kernel)
|
|
1092
|
+
OutStamp.trapezoid(self.UC, fade_kernel)
|
|
1093
|
+
|
|
1094
|
+
def _visualize_system_matrices(self) -> None:
|
|
1095
|
+
"""Visualize system matrices."""
|
|
1096
|
+
|
|
1097
|
+
with mpl.rc_context(format_axis_pars):
|
|
1098
|
+
print("OutStamp._visualize_system_matrices")
|
|
1099
|
+
|
|
1100
|
+
# the A-matrix first
|
|
1101
|
+
print(f"{self.sysmata.shape=}") # (n_inpix, n_inpix)
|
|
1102
|
+
print(f"{np.all(self.sysmata == self.sysmata.T)=}")
|
|
1103
|
+
|
|
1104
|
+
fig, ax = plt.subplots(figsize=(12.8, 9.6))
|
|
1105
|
+
vmin = self.sysmata.max() / (self.mhalfb.max() / self.mhalfb.min()) ** 2
|
|
1106
|
+
im = ax.imshow(np.log10(np.clip(self.sysmata, a_min=vmin, a_max=None)), vmin=np.log10(vmin))
|
|
1107
|
+
plt.colorbar(im, ax=ax)
|
|
1108
|
+
|
|
1109
|
+
for xy in self.inpix_cumsum[1:-1]:
|
|
1110
|
+
ax.axvline(xy, c="r", ls="--", lw=1.5)
|
|
1111
|
+
ax.axhline(xy, c="r", ls="--", lw=1.5)
|
|
1112
|
+
|
|
1113
|
+
ax.set_xlabel("input pixel $i$")
|
|
1114
|
+
ax.set_ylabel("input pixel $j$")
|
|
1115
|
+
ax.set_title(r"$A$ matrix: $\log_{10} (A_{ij})$")
|
|
1116
|
+
format_axis(ax, False)
|
|
1117
|
+
plt.show()
|
|
1118
|
+
|
|
1119
|
+
# now the mBhalf matrix
|
|
1120
|
+
print(f"{self.mhalfb.shape=}") # (n_out, n_outpix, n_inpix)
|
|
1121
|
+
n_out, n_outpix, n_inpix = self.mhalfb.shape
|
|
1122
|
+
height = 9.6 / n_inpix * n_outpix
|
|
1123
|
+
|
|
1124
|
+
for mhalfb_ in self.mhalfb:
|
|
1125
|
+
fig, ax = plt.subplots(figsize=(12.8, height))
|
|
1126
|
+
im = ax.imshow(np.log10(mhalfb_))
|
|
1127
|
+
plt.colorbar(im, ax=ax)
|
|
1128
|
+
|
|
1129
|
+
for x in self.inpix_cumsum[1:-1]:
|
|
1130
|
+
ax.axvline(x, c="r", ls="--", lw=1.5)
|
|
1131
|
+
|
|
1132
|
+
ax.set_xlabel("input pixel $i$")
|
|
1133
|
+
ax.set_ylabel(r"output pixel $\alpha$")
|
|
1134
|
+
ax.set_title(r"$B$ matrix: $\log_{10} (-B_{\alpha i}/2)$")
|
|
1135
|
+
format_axis(ax, False)
|
|
1136
|
+
plt.show()
|
|
1137
|
+
|
|
1138
|
+
# and C
|
|
1139
|
+
print(f"{self.outovlc.shape=}") # (n_out,)
|
|
1140
|
+
|
|
1141
|
+
def _visualize_coadd_matrices(self) -> None:
|
|
1142
|
+
"""Visualize coaddition matrices."""
|
|
1143
|
+
with mpl.rc_context(format_axis_pars):
|
|
1144
|
+
print("OutStamp._visualize_coadd_matrices")
|
|
1145
|
+
fk = self.blk.cfg.fade_kernel # shortcut
|
|
1146
|
+
|
|
1147
|
+
for j_out, T_ in enumerate(self.T):
|
|
1148
|
+
print(f"output PSF: {j_out}")
|
|
1149
|
+
|
|
1150
|
+
fig, axs = plt.subplots(1, 3, figsize=(14.4, 3.6))
|
|
1151
|
+
for ax, map_, title in zip(
|
|
1152
|
+
axs,
|
|
1153
|
+
[self.UC, self.Sigma, self.kappa],
|
|
1154
|
+
[
|
|
1155
|
+
r"PSF leakage: $\log_{10} (U/C)$",
|
|
1156
|
+
r"Noise amplification: $\log_{10} \Sigma$",
|
|
1157
|
+
r"Lagrange multiplier: $\log_{10} \kappa$",
|
|
1158
|
+
],
|
|
1159
|
+
strict=False,
|
|
1160
|
+
):
|
|
1161
|
+
im = ax.imshow(
|
|
1162
|
+
np.log10(map_[j_out]),
|
|
1163
|
+
origin="lower",
|
|
1164
|
+
extent=[self.left - fk, self.right + fk, self.bottom - fk, self.top + fk],
|
|
1165
|
+
)
|
|
1166
|
+
plt.colorbar(im, ax=ax)
|
|
1167
|
+
|
|
1168
|
+
ax.set_title(title)
|
|
1169
|
+
ax.set_xlabel("output grid $i$")
|
|
1170
|
+
ax.set_ylabel("output grid $j$")
|
|
1171
|
+
format_axis(ax, False)
|
|
1172
|
+
plt.show()
|
|
1173
|
+
|
|
1174
|
+
vmin, vmax = np.percentile(T_.ravel(), [1, 99])
|
|
1175
|
+
n_out, n_outpix, n_inpix = self.mhalfb.shape
|
|
1176
|
+
height = 9.6 / n_inpix * n_outpix
|
|
1177
|
+
|
|
1178
|
+
fig, ax = plt.subplots(figsize=(12.8, height))
|
|
1179
|
+
im = ax.imshow(T_, vmin=vmin, vmax=vmax)
|
|
1180
|
+
plt.colorbar(im, ax=ax)
|
|
1181
|
+
|
|
1182
|
+
for x in self.inpix_cumsum[1:-1]:
|
|
1183
|
+
ax.axvline(x, c="r", ls="--", lw=1.5)
|
|
1184
|
+
|
|
1185
|
+
ax.set_xlabel("input pixel $i$")
|
|
1186
|
+
ax.set_ylabel(r"output pixel $\alpha$")
|
|
1187
|
+
ax.set_title(r"$T$ matrix: $T_{\alpha i}$")
|
|
1188
|
+
format_axis(ax, False)
|
|
1189
|
+
plt.show()
|
|
1190
|
+
|
|
1191
|
+
@staticmethod
|
|
1192
|
+
def trapezoid(
|
|
1193
|
+
arr: np.array,
|
|
1194
|
+
fade_kernel: int,
|
|
1195
|
+
recover_mode: bool = False,
|
|
1196
|
+
pad_widths: tuple[int, int, int, int] = (0, 0, 0, 0),
|
|
1197
|
+
do_sides: str = "BTLR",
|
|
1198
|
+
use_trunc_sinc: bool = True,
|
|
1199
|
+
) -> None:
|
|
1200
|
+
"""
|
|
1201
|
+
Apply a trapezoid filter of width 2*fade_kernel on each side.
|
|
1202
|
+
|
|
1203
|
+
Parameters
|
|
1204
|
+
----------
|
|
1205
|
+
arr : np.array
|
|
1206
|
+
The array to apply the trapezoid filter. Shape (..., ny, nx).
|
|
1207
|
+
fade_kernel : int
|
|
1208
|
+
Half the width of the trapezoid filter.
|
|
1209
|
+
recover_mode : bool, optional
|
|
1210
|
+
Whether to recover faded boundaries.
|
|
1211
|
+
pad_widths : (int, int, int, int), optional
|
|
1212
|
+
Padding width on each side (order: bottom, top, left, right).
|
|
1213
|
+
do_sides : str, optional
|
|
1214
|
+
Which sides to apply the trapezoid filter.
|
|
1215
|
+
use_trunc_sinc : bool, optional
|
|
1216
|
+
Whether to use the truncated sinc function.
|
|
1217
|
+
|
|
1218
|
+
Returns
|
|
1219
|
+
-------
|
|
1220
|
+
None
|
|
1221
|
+
|
|
1222
|
+
"""
|
|
1223
|
+
|
|
1224
|
+
fk2 = fade_kernel * 2
|
|
1225
|
+
if not fk2 > 0:
|
|
1226
|
+
return
|
|
1227
|
+
|
|
1228
|
+
ny, nx = arr.shape[-2:]
|
|
1229
|
+
# assert ny > fk2 and nx > fk2, 'Fatal error in OutStamp.trapezoid: ' \
|
|
1230
|
+
# f'insufficient patch size, {ny= } {nx= } {fade_kernel= }'
|
|
1231
|
+
|
|
1232
|
+
pb, pt, pl, pr = pad_widths
|
|
1233
|
+
# assert ny > pb and ny > pt, 'Fatal error in OutStamp.trapezoid: ' \
|
|
1234
|
+
# f'insufficient patch size, {ny= } {pb= } {pt= }'
|
|
1235
|
+
# assert nx > pl and nx > pr, 'Fatal error in OutStamp.trapezoid: ' \
|
|
1236
|
+
# f'insufficient patch size, {nx= } {pl= } {pr= }'
|
|
1237
|
+
it, ir = ny - pt - 1, nx - pr - 1 # starting indices on these two sides
|
|
1238
|
+
|
|
1239
|
+
s = np.arange(1, fk2 + 1, dtype=np.float64) / (fk2 + 1)
|
|
1240
|
+
if use_trunc_sinc:
|
|
1241
|
+
s -= np.sin(2 * np.pi * s) / (2 * np.pi)
|
|
1242
|
+
sT = s[None, :].T
|
|
1243
|
+
|
|
1244
|
+
if not recover_mode:
|
|
1245
|
+
if "B" in do_sides:
|
|
1246
|
+
arr[..., pb : pb + fk2, :] *= sT
|
|
1247
|
+
if "T" in do_sides:
|
|
1248
|
+
arr[..., it : it - fk2 : -1, :] *= sT
|
|
1249
|
+
if "L" in do_sides:
|
|
1250
|
+
arr[..., :, pl : pl + fk2] *= s
|
|
1251
|
+
if "R" in do_sides:
|
|
1252
|
+
arr[..., :, ir : ir - fk2 : -1] *= s
|
|
1253
|
+
|
|
1254
|
+
else: # recover block boundaries
|
|
1255
|
+
if "B" in do_sides:
|
|
1256
|
+
arr[..., pb : pb + fk2, :] /= sT
|
|
1257
|
+
if "T" in do_sides:
|
|
1258
|
+
arr[..., it : it - fk2 : -1, :] /= sT
|
|
1259
|
+
if "L" in do_sides:
|
|
1260
|
+
arr[..., :, pl : pl + fk2] /= s
|
|
1261
|
+
if "R" in do_sides:
|
|
1262
|
+
arr[..., :, ir : ir - fk2 : -1] /= s
|
|
1263
|
+
|
|
1264
|
+
def _perform_coaddition(
|
|
1265
|
+
self, visualize: bool = False, save_t: bool = False, use_trunc_sinc: bool = True
|
|
1266
|
+
) -> None:
|
|
1267
|
+
"""
|
|
1268
|
+
Perform the actual multiplication.
|
|
1269
|
+
|
|
1270
|
+
Parameters
|
|
1271
|
+
----------
|
|
1272
|
+
visualize : bool, optional
|
|
1273
|
+
Whether to visualize the process.
|
|
1274
|
+
save_t : bool, optional
|
|
1275
|
+
Whether to save coaddition matrices.
|
|
1276
|
+
use_trunc_sinc : bool, optional
|
|
1277
|
+
Argument for coadd_utils.trapezoid.
|
|
1278
|
+
|
|
1279
|
+
Returns
|
|
1280
|
+
-------
|
|
1281
|
+
None
|
|
1282
|
+
|
|
1283
|
+
"""
|
|
1284
|
+
|
|
1285
|
+
# shortcuts
|
|
1286
|
+
n_out = self.blk.outpsfgrp.n_psf
|
|
1287
|
+
n2f = self.blk.cfg.n2f
|
|
1288
|
+
fade_kernel = self.blk.cfg.fade_kernel
|
|
1289
|
+
|
|
1290
|
+
# multiplication by the trapezoid filter with transition width fade_kernel
|
|
1291
|
+
if fade_kernel > 0:
|
|
1292
|
+
# self.T: (n_out, n_outpix, n_inpix)
|
|
1293
|
+
T_view = np.moveaxis(self.T, 1, -1).reshape((n_out, self.inpix_cumsum[-1], n2f, n2f))
|
|
1294
|
+
OutStamp.trapezoid(T_view, fade_kernel)
|
|
1295
|
+
|
|
1296
|
+
# weight computations
|
|
1297
|
+
Tsum_image = np.zeros(self.T.shape[:2] + (self.blk.n_inimage,)) # (n_out, n_outpix, n_inimage)
|
|
1298
|
+
|
|
1299
|
+
for j_st, inst, selection in zip(range(9), self.instamps, self.selections, strict=False):
|
|
1300
|
+
if selection is None:
|
|
1301
|
+
my_cumsum = inst.pix_cumsum.copy()
|
|
1302
|
+
else:
|
|
1303
|
+
my_cumsum = np.searchsorted(selection, inst.pix_cumsum)
|
|
1304
|
+
my_cumsum += self.inpix_cumsum[j_st]
|
|
1305
|
+
|
|
1306
|
+
for i_im in range(self.blk.n_inimage):
|
|
1307
|
+
Tsum_image[:, :, i_im] += np.sum(self.T[:, :, my_cumsum[i_im] : my_cumsum[i_im + 1]], axis=2)
|
|
1308
|
+
|
|
1309
|
+
self.Tsum_stamp = np.sum(Tsum_image, axis=1) / self.blk.cfg.n2**2 # (n_out, n_inimage)
|
|
1310
|
+
self.Tsum_inpix = np.sum(Tsum_image, axis=2).reshape((n_out, n2f, n2f))
|
|
1311
|
+
Tsum_norm = Tsum_image / np.abs(Tsum_image).sum(axis=2)[:, :, None]
|
|
1312
|
+
self.Neff = 1.0 / np.sum(np.square(Tsum_norm), axis=2).reshape((n_out, n2f, n2f))
|
|
1313
|
+
if fade_kernel > 0:
|
|
1314
|
+
OutStamp.trapezoid(self.Neff, fade_kernel)
|
|
1315
|
+
|
|
1316
|
+
del Tsum_image, Tsum_norm
|
|
1317
|
+
if visualize:
|
|
1318
|
+
print()
|
|
1319
|
+
self._visualize_weight_computations()
|
|
1320
|
+
|
|
1321
|
+
# the actual multiplication
|
|
1322
|
+
self.outimage = np.einsum("oaj,ij->oia", self.T, self.indata).reshape(
|
|
1323
|
+
(n_out, self.blk.cfg.n_inframe, n2f, n2f)
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
if visualize:
|
|
1327
|
+
print()
|
|
1328
|
+
self._show_in_and_out_images()
|
|
1329
|
+
print()
|
|
1330
|
+
self._study_individual_pixels()
|
|
1331
|
+
del self.iny_val, self.inx_val, self.indata
|
|
1332
|
+
if not save_t:
|
|
1333
|
+
del self.T
|
|
1334
|
+
|
|
1335
|
+
def _visualize_weight_computations(self) -> None:
|
|
1336
|
+
"""Display weight computations."""
|
|
1337
|
+
with mpl.rc_context(format_axis_pars):
|
|
1338
|
+
print("OutStamp._visualize_weight_computations")
|
|
1339
|
+
fk = self.blk.cfg.fade_kernel # shortcut
|
|
1340
|
+
|
|
1341
|
+
for j_out in range(self.blk.outpsfgrp.n_psf):
|
|
1342
|
+
print(f"output PSF: {j_out}")
|
|
1343
|
+
|
|
1344
|
+
fig, axs = plt.subplots(1, 3, figsize=(14.4, 3.6))
|
|
1345
|
+
|
|
1346
|
+
ax = axs[0]
|
|
1347
|
+
ax.barh(
|
|
1348
|
+
[f"${inimage.idsca}$" for inimage in self.blk.inimages],
|
|
1349
|
+
self.Tsum_stamp[0],
|
|
1350
|
+
color=[f"C{i}" for i in range(self.blk.n_inimage)],
|
|
1351
|
+
)
|
|
1352
|
+
|
|
1353
|
+
ax.set_title("Total contribution")
|
|
1354
|
+
ax.set_xlabel(r"$\sum {}_\alpha \sum {}_{i \in \bar{i}} T_{\alpha i}$")
|
|
1355
|
+
ax.set_ylabel("input image")
|
|
1356
|
+
format_axis(ax, False)
|
|
1357
|
+
|
|
1358
|
+
for ax, map_, title in zip(
|
|
1359
|
+
axs[1:],
|
|
1360
|
+
[self.Tsum_inpix, self.Neff],
|
|
1361
|
+
[r"Total weight: $T_{\rm tot}$", r"Effective coverage: $\bar{n}_{\rm eff}$"],
|
|
1362
|
+
strict=False,
|
|
1363
|
+
):
|
|
1364
|
+
im = ax.imshow(
|
|
1365
|
+
map_[j_out],
|
|
1366
|
+
origin="lower",
|
|
1367
|
+
extent=[self.left - fk, self.right + fk, self.bottom - fk, self.top + fk],
|
|
1368
|
+
)
|
|
1369
|
+
plt.colorbar(im, ax=ax)
|
|
1370
|
+
|
|
1371
|
+
ax.set_title(title)
|
|
1372
|
+
ax.set_xlabel("output grid $i$")
|
|
1373
|
+
ax.set_ylabel("output grid $j$")
|
|
1374
|
+
format_axis(ax, False)
|
|
1375
|
+
plt.show()
|
|
1376
|
+
|
|
1377
|
+
def _show_in_and_out_images(self) -> None:
|
|
1378
|
+
"""Display input and output images."""
|
|
1379
|
+
with mpl.rc_context(format_axis_pars):
|
|
1380
|
+
print("OutStamp._show_in_and_out_images")
|
|
1381
|
+
fk = self.blk.cfg.fade_kernel # shortcut
|
|
1382
|
+
|
|
1383
|
+
for j_in in range(self.blk.cfg.n_inframe):
|
|
1384
|
+
n_out = self.blk.outpsfgrp.n_psf
|
|
1385
|
+
fig, axs = plt.subplots(1, 1 + n_out, figsize=(4.8 * (1 + n_out), 3.6))
|
|
1386
|
+
|
|
1387
|
+
ax = axs[0]
|
|
1388
|
+
im = ax.scatter(self.inx_val, self.iny_val, c=self.indata[j_in], cmap="viridis", s=5)
|
|
1389
|
+
plt.colorbar(im, ax=ax)
|
|
1390
|
+
for x in [self.left - 0.5, self.right + 0.5]:
|
|
1391
|
+
ax.axvline(x, c="r", ls="--")
|
|
1392
|
+
for y in [self.bottom - 0.5, self.top + 0.5]:
|
|
1393
|
+
ax.axhline(y, c="r", ls="--")
|
|
1394
|
+
|
|
1395
|
+
ax.axis("equal")
|
|
1396
|
+
ax.set_xlabel("output grid $i$")
|
|
1397
|
+
ax.set_ylabel("output grid $j$")
|
|
1398
|
+
ax.set_title("layer: " + ("SCI" if j_in == 0 else self.blk.cfg.extrainput[j_in]))
|
|
1399
|
+
format_axis(ax)
|
|
1400
|
+
|
|
1401
|
+
for j_out in range(n_out):
|
|
1402
|
+
ax = axs[1 + j_out]
|
|
1403
|
+
im = ax.imshow(
|
|
1404
|
+
self.outimage[j_out, j_in],
|
|
1405
|
+
origin="lower",
|
|
1406
|
+
extent=[self.left - fk, self.right + fk, self.bottom - fk, self.top + fk],
|
|
1407
|
+
)
|
|
1408
|
+
plt.colorbar(im, ax=ax)
|
|
1409
|
+
|
|
1410
|
+
ax.set_xlabel("output grid $i$")
|
|
1411
|
+
ax.set_ylabel("output grid $j$")
|
|
1412
|
+
ax.set_title(f"output PSF: {j_out}")
|
|
1413
|
+
format_axis(ax, False)
|
|
1414
|
+
|
|
1415
|
+
plt.show()
|
|
1416
|
+
|
|
1417
|
+
def _study_individual_pixels(self) -> None:
|
|
1418
|
+
"""Study individual input and output pixels."""
|
|
1419
|
+
with mpl.rc_context(format_axis_pars):
|
|
1420
|
+
print("OutStamp._study_individual_pixels")
|
|
1421
|
+
|
|
1422
|
+
accrad = np.arange(15, 140, 5) # acceptance radius in units of output pixels
|
|
1423
|
+
closest_inpix = [] # indices of input pixels closest to the corners and the center
|
|
1424
|
+
|
|
1425
|
+
fk = self.blk.cfg.fade_kernel
|
|
1426
|
+
fk2 = fk * 2
|
|
1427
|
+
n2f = self.blk.cfg.n2f # shortcuts
|
|
1428
|
+
for j_out, i_out in [
|
|
1429
|
+
(fk2, fk2),
|
|
1430
|
+
(fk2, n2f - 1 - fk2),
|
|
1431
|
+
(n2f - 1 - fk2, fk2),
|
|
1432
|
+
(n2f - 1 - fk2, n2f - 1 - fk2),
|
|
1433
|
+
((n2f - 1) // 2, (n2f - 1) // 2),
|
|
1434
|
+
]:
|
|
1435
|
+
out_idx = j_out * self.blk.cfg.n2f + i_out
|
|
1436
|
+
T_elems = self.T[0, out_idx, :]
|
|
1437
|
+
|
|
1438
|
+
plt.figure(figsize=(10.8, 3.6))
|
|
1439
|
+
ax0 = plt.subplot(1, 2, 1)
|
|
1440
|
+
im = ax0.scatter(self.inx_val, self.iny_val, c=T_elems, cmap="viridis", s=5)
|
|
1441
|
+
plt.colorbar(im, ax=ax0)
|
|
1442
|
+
|
|
1443
|
+
ax0.axis("equal")
|
|
1444
|
+
ax0.set_xlabel("output grid $i$")
|
|
1445
|
+
ax0.set_ylabel("output grid $j$")
|
|
1446
|
+
ax0.set_title(r"$T_{\alpha i}$")
|
|
1447
|
+
format_axis(ax0)
|
|
1448
|
+
|
|
1449
|
+
# print(j_out-fk + self.bottom, i_out-fk + self.left)
|
|
1450
|
+
dist = np.sqrt(
|
|
1451
|
+
np.square(j_out - fk + self.bottom - self.iny_val)
|
|
1452
|
+
+ np.square(i_out - fk + self.left - self.inx_val)
|
|
1453
|
+
)
|
|
1454
|
+
closest_inpix.append(np.argmin(dist))
|
|
1455
|
+
|
|
1456
|
+
ax1 = plt.subplot(2, 2, 2)
|
|
1457
|
+
ax1.scatter(dist, T_elems, c=T_elems, cmap="viridis", s=5)
|
|
1458
|
+
ax1.axhline(0, c="b", ls="--")
|
|
1459
|
+
ax1.axvline(self.blk.cfg.n2, c="r", ls="--")
|
|
1460
|
+
|
|
1461
|
+
ax1.xaxis.set_ticklabels([])
|
|
1462
|
+
ax1.set_ylabel(r"$T_{\alpha i}$")
|
|
1463
|
+
format_axis(ax1)
|
|
1464
|
+
|
|
1465
|
+
signal = np.empty_like(accrad, dtype=np.float64)
|
|
1466
|
+
for i in range(accrad.shape[0]):
|
|
1467
|
+
T_arr = np.where(dist <= accrad[i], T_elems, 0.0)
|
|
1468
|
+
signal[i] = T_arr @ self.indata[0]
|
|
1469
|
+
|
|
1470
|
+
ax2 = plt.subplot(2, 2, 4)
|
|
1471
|
+
ax2.plot(accrad, signal, "go-")
|
|
1472
|
+
ax2.axvline(self.blk.cfg.n2, c="r", ls="--")
|
|
1473
|
+
ax2.axhline(self.outimage[0, 0].ravel()[out_idx], c="b", ls="--")
|
|
1474
|
+
|
|
1475
|
+
ax2.set_xlim(ax1.get_xlim())
|
|
1476
|
+
ax2.set_xlabel("acceptance radius")
|
|
1477
|
+
ax2.set_ylabel("signal")
|
|
1478
|
+
format_axis(ax2)
|
|
1479
|
+
|
|
1480
|
+
plt.show()
|
|
1481
|
+
|
|
1482
|
+
for in_idx in closest_inpix:
|
|
1483
|
+
print(f"{(self.inx_val[in_idx], self.iny_val[in_idx])=}")
|
|
1484
|
+
fig, ax = plt.subplots(figsize=(4.2, 4.2))
|
|
1485
|
+
im = ax.imshow(
|
|
1486
|
+
self.T[0, :, in_idx].reshape(n2f, n2f),
|
|
1487
|
+
origin="lower",
|
|
1488
|
+
extent=[self.left - fk, self.right + fk, self.bottom - fk, self.top + fk],
|
|
1489
|
+
)
|
|
1490
|
+
plt.colorbar(im, ax=ax)
|
|
1491
|
+
ax.scatter(self.inx_val[in_idx] - self.left, self.iny_val[in_idx] - self.bottom, c="r", s=2)
|
|
1492
|
+
|
|
1493
|
+
ax.set_xlabel("output grid $i$")
|
|
1494
|
+
ax.set_ylabel("output grid $j$")
|
|
1495
|
+
ax.set_title(r"$T_{\alpha i}$")
|
|
1496
|
+
format_axis(ax, False)
|
|
1497
|
+
|
|
1498
|
+
plt.show()
|
|
1499
|
+
|
|
1500
|
+
def clear(self) -> None:
|
|
1501
|
+
"""Free up memory space."""
|
|
1502
|
+
|
|
1503
|
+
del self.inpix_count, self.inpix_cumsum
|
|
1504
|
+
self.selections.clear()
|
|
1505
|
+
del self.selections
|
|
1506
|
+
|
|
1507
|
+
if hasattr(self, "sysmata"):
|
|
1508
|
+
del self.sysmata, self.mhalfb, self.outovlc
|
|
1509
|
+
if hasattr(self, "T"):
|
|
1510
|
+
del self.T
|
|
1511
|
+
|
|
1512
|
+
del self.kappa, self.Sigma, self.UC
|
|
1513
|
+
del self.Tsum_stamp, self.Tsum_inpix, self.Neff
|
|
1514
|
+
del self.yx_val, self.outimage
|
|
1515
|
+
|
|
1516
|
+
|
|
1517
|
+
class Block:
|
|
1518
|
+
"""
|
|
1519
|
+
Driver for block coaddition.
|
|
1520
|
+
|
|
1521
|
+
Parameters
|
|
1522
|
+
----------
|
|
1523
|
+
cfg : Config, optional
|
|
1524
|
+
Configuration for this Block.
|
|
1525
|
+
this_sub : int, optional
|
|
1526
|
+
Number determining the location of this Block in the mosaic.
|
|
1527
|
+
run_coadd : bool, optional
|
|
1528
|
+
Whether to coadd this block.
|
|
1529
|
+
Turn this off if you want to perform the procedure manually.
|
|
1530
|
+
|
|
1531
|
+
Methods
|
|
1532
|
+
-------
|
|
1533
|
+
__init__
|
|
1534
|
+
Constructor.
|
|
1535
|
+
__call__
|
|
1536
|
+
Coadd this block.
|
|
1537
|
+
parse_config
|
|
1538
|
+
Parse the configuration.
|
|
1539
|
+
_get_obs_cover
|
|
1540
|
+
Get observations relevant to this Block.
|
|
1541
|
+
_build_use_instamps
|
|
1542
|
+
Build the Boolean array use_instamps.
|
|
1543
|
+
_handle_postage_pad
|
|
1544
|
+
Handle the postage padding.
|
|
1545
|
+
process_input_images
|
|
1546
|
+
Process input images.
|
|
1547
|
+
build_input_stamps
|
|
1548
|
+
Build input stamps from input images.
|
|
1549
|
+
_output_stamp_wrapper
|
|
1550
|
+
Wrapper for output stamp coaddition.
|
|
1551
|
+
coadd_output_stamps
|
|
1552
|
+
Coadd output stamps using input stamps.
|
|
1553
|
+
compress_map
|
|
1554
|
+
Compress float32 map to (u)int16 to save storage.
|
|
1555
|
+
build_output_file
|
|
1556
|
+
Build the output FITS file.
|
|
1557
|
+
clear_all
|
|
1558
|
+
Free up all memory spaces.
|
|
1559
|
+
|
|
1560
|
+
"""
|
|
1561
|
+
|
|
1562
|
+
def __init__(self, cfg: Config = None, this_sub: int = 0, run_coadd: bool = True) -> None:
|
|
1563
|
+
self.timer = Timer()
|
|
1564
|
+
cfg()
|
|
1565
|
+
self.cfg = cfg
|
|
1566
|
+
if cfg is None:
|
|
1567
|
+
self.cfg = Config() # use the default config
|
|
1568
|
+
|
|
1569
|
+
if hasattr(cfg, "psf_interp") and cfg.psf_interp.upper() == "G4460":
|
|
1570
|
+
print("Setting PSF interpolation to use G4460.")
|
|
1571
|
+
PSFInterpolator.set_G4460()
|
|
1572
|
+
PSFGrp.setup(
|
|
1573
|
+
npixpsf=cfg.npixpsf, oversamp=cfg.inpsf_oversamp, dtheta=cfg.dtheta, psfsplit=bool(cfg.psfsplit)
|
|
1574
|
+
)
|
|
1575
|
+
PSFOvl.setup(flat_penalty=cfg.flat_penalty)
|
|
1576
|
+
self.this_sub = this_sub
|
|
1577
|
+
if run_coadd:
|
|
1578
|
+
self()
|
|
1579
|
+
|
|
1580
|
+
def __call__(self) -> None:
|
|
1581
|
+
"""Coadd this block."""
|
|
1582
|
+
|
|
1583
|
+
self.parse_config()
|
|
1584
|
+
# this produces: obsdata, outwcs, outpsfgrp, outpsfovl
|
|
1585
|
+
self.process_input_images()
|
|
1586
|
+
# this produces: obslist, inimages (1D list), n_inimage
|
|
1587
|
+
self.build_input_stamps()
|
|
1588
|
+
# this produces: instamps (2D list)
|
|
1589
|
+
|
|
1590
|
+
self.coadd_output_stamps(sim_mode=True)
|
|
1591
|
+
# this produces: sysmata (object), sysmatb (object), outstamps (2D list)
|
|
1592
|
+
self.coadd_output_stamps(sim_mode=False)
|
|
1593
|
+
# this produces: out_map, T_weightmap, and additional output maps
|
|
1594
|
+
|
|
1595
|
+
self.build_output_file(is_final=True)
|
|
1596
|
+
self.clear_all()
|
|
1597
|
+
print(f"finished at t = {self.timer():.2f} s")
|
|
1598
|
+
|
|
1599
|
+
def parse_config(self) -> None:
|
|
1600
|
+
"""Parse the configuration."""
|
|
1601
|
+
|
|
1602
|
+
print("General input information:")
|
|
1603
|
+
print("number of input frames = ", self.cfg.n_inframe, "type =", self.cfg.extrainput)
|
|
1604
|
+
# search radius for input pixels
|
|
1605
|
+
rpix_search_ = self.cfg.instamp_pad / Stn.arcsec
|
|
1606
|
+
dtheta_ = self.cfg.dtheta * u.degree.to("arcsec")
|
|
1607
|
+
print(
|
|
1608
|
+
f"acceptance radius --> {rpix_search_:.6f} arcsec or {rpix_search_ / dtheta_:.6f} output pixels"
|
|
1609
|
+
)
|
|
1610
|
+
print()
|
|
1611
|
+
|
|
1612
|
+
# Get observation table
|
|
1613
|
+
assert self.cfg.obsfile is not None, "Error: no obsfile found"
|
|
1614
|
+
print(f"Getting observations from {self.cfg.obsfile:s}")
|
|
1615
|
+
with fits.open(self.cfg.obsfile) as myf:
|
|
1616
|
+
# if the filter column is a string, replace with the number
|
|
1617
|
+
if myf[1].columns["filter"].format[-1] == "A":
|
|
1618
|
+
print("converting filter from names to integers:", myf[1].data["filter"])
|
|
1619
|
+
n_obs_tot = len(myf[1].data.field(0))
|
|
1620
|
+
mytable = Table(myf[1].data)
|
|
1621
|
+
fdata = np.zeros(n_obs_tot, dtype=np.uint16)
|
|
1622
|
+
for j in range(len(Stn.RomanFilters)):
|
|
1623
|
+
s = Stn.RomanFilters[j]
|
|
1624
|
+
for i in range(n_obs_tot):
|
|
1625
|
+
if myf[1].data["filter"][i] == s:
|
|
1626
|
+
fdata[i] = j
|
|
1627
|
+
mytable.replace_column("filter", fdata)
|
|
1628
|
+
myf[1] = fits.BinTableHDU(mytable)
|
|
1629
|
+
|
|
1630
|
+
self.obsdata = myf[1].data
|
|
1631
|
+
obscols = myf[1].columns
|
|
1632
|
+
n_obs_tot = len(self.obsdata.field(0))
|
|
1633
|
+
print("Retrieved columns:", obscols.names, f" {n_obs_tot:d} rows")
|
|
1634
|
+
|
|
1635
|
+
# display output information
|
|
1636
|
+
print(
|
|
1637
|
+
f"Output information: ctr at RA={self.cfg.ra:10.6f},DEC={self.cfg.dec:10.6f} "
|
|
1638
|
+
f"LONPOLE={self.cfg.lonpole:10.6f}"
|
|
1639
|
+
)
|
|
1640
|
+
print(
|
|
1641
|
+
"pixel scale={:8.6f} arcsec or {:11.5E} degree".format(
|
|
1642
|
+
self.cfg.dtheta * u.degree.to("arcsec"), self.cfg.dtheta
|
|
1643
|
+
)
|
|
1644
|
+
)
|
|
1645
|
+
print(f"output array size = {self.cfg.NsideP:d} ({self.cfg.n1P:d} postage stamps of {self.cfg.n2:d})")
|
|
1646
|
+
print()
|
|
1647
|
+
|
|
1648
|
+
# block information
|
|
1649
|
+
ibx, iby = divmod(self.this_sub, self.cfg.nblock)
|
|
1650
|
+
self.ibx = ibx # save this information
|
|
1651
|
+
self.iby = iby
|
|
1652
|
+
print(
|
|
1653
|
+
f"sub-block {self.this_sub:4d} <{ibx:2d},{iby:2d}> of "
|
|
1654
|
+
f"{self.cfg.nblock:2d}x{self.cfg.nblock:2d}={self.cfg.nblock**2:2d}"
|
|
1655
|
+
)
|
|
1656
|
+
self.outstem = self.cfg.outstem + f"_{ibx:02d}_{iby:02d}"
|
|
1657
|
+
print("outputs directed to -->", self.outstem)
|
|
1658
|
+
if self.cfg.tempfile is not None:
|
|
1659
|
+
self.cache_dir = Path(
|
|
1660
|
+
self.cfg.tempfile
|
|
1661
|
+
+ "_{:04d}_{:s}_cache".format(
|
|
1662
|
+
self.this_sub, datetime.datetime.now(pytz.timezone("UTC")).strftime("%Y%m%d%H%M%S%f")
|
|
1663
|
+
)
|
|
1664
|
+
)
|
|
1665
|
+
self.cache_dir.mkdir(exist_ok=True)
|
|
1666
|
+
print("temporary storage directed to -->", self.cache_dir)
|
|
1667
|
+
|
|
1668
|
+
# make the WCS
|
|
1669
|
+
self.outwcs = wcs.WCS(naxis=2)
|
|
1670
|
+
self.outwcs.wcs.crpix = [
|
|
1671
|
+
(self.cfg.NsideP + 1) / 2.0 - self.cfg.Nside * (ibx - (self.cfg.nblock - 1) / 2.0),
|
|
1672
|
+
(self.cfg.NsideP + 1) / 2.0 - self.cfg.Nside * (iby - (self.cfg.nblock - 1) / 2.0),
|
|
1673
|
+
]
|
|
1674
|
+
self.outwcs.wcs.cdelt = [-self.cfg.dtheta, self.cfg.dtheta]
|
|
1675
|
+
self.outwcs.wcs.ctype = ["RA---STG", "DEC--STG"]
|
|
1676
|
+
self.outwcs.wcs.crval = [self.cfg.ra, self.cfg.dec]
|
|
1677
|
+
self.outwcs.wcs.lonpole = self.cfg.lonpole
|
|
1678
|
+
|
|
1679
|
+
# print the corners of the square and the center, ordering:
|
|
1680
|
+
# 2 3
|
|
1681
|
+
# 4
|
|
1682
|
+
# 0 1
|
|
1683
|
+
cornerx = [-0.5, self.cfg.NsideP - 0.5, -0.5, self.cfg.NsideP - 0.5, (self.cfg.NsideP - 1) / 2.0]
|
|
1684
|
+
cornery = [-0.5, -0.5, self.cfg.NsideP - 0.5, self.cfg.NsideP - 0.5, (self.cfg.NsideP - 1) / 2.0]
|
|
1685
|
+
for i in range(5):
|
|
1686
|
+
print(i, self.outwcs.all_pix2world(np.array([[cornerx[i], cornery[i]]]), 0))
|
|
1687
|
+
self.centerpos = self.outwcs.all_pix2world(np.array([[cornerx[-1], cornery[-1]]]), 0)[
|
|
1688
|
+
0
|
|
1689
|
+
] # [ra,dec] array in degrees
|
|
1690
|
+
|
|
1691
|
+
print("kappa/C array", self.cfg.kappaC_arr)
|
|
1692
|
+
|
|
1693
|
+
# and the output PSFs
|
|
1694
|
+
self.outpsfgrp = PSFGrp(in_or_out=False, blk=self)
|
|
1695
|
+
self.outpsfovl = PSFOvl(self.outpsfgrp, None)
|
|
1696
|
+
print("computed overlap, C=", self.outpsfovl.outovlc)
|
|
1697
|
+
print()
|
|
1698
|
+
|
|
1699
|
+
def _get_obs_cover(self, radius: float) -> None:
|
|
1700
|
+
"""
|
|
1701
|
+
Get observations relevant to this Block.
|
|
1702
|
+
|
|
1703
|
+
Parameters
|
|
1704
|
+
----------
|
|
1705
|
+
radius : float
|
|
1706
|
+
Search for input images within this radius.
|
|
1707
|
+
|
|
1708
|
+
Returns
|
|
1709
|
+
-------
|
|
1710
|
+
None
|
|
1711
|
+
|
|
1712
|
+
Notes
|
|
1713
|
+
-----
|
|
1714
|
+
This uses the observation table (obsdata), center position (ra, dec), and filter information
|
|
1715
|
+
stored internally to the Block.
|
|
1716
|
+
|
|
1717
|
+
"""
|
|
1718
|
+
|
|
1719
|
+
self.obslist = []
|
|
1720
|
+
n_obs_tot = len(self.obsdata.field(0))
|
|
1721
|
+
|
|
1722
|
+
# rotate this observation to the (X,Y) of the local FoV for each observation
|
|
1723
|
+
# first rotate the RA direction
|
|
1724
|
+
x1 = np.cos(self.centerpos[1] * Stn.degree) * np.cos(
|
|
1725
|
+
(self.centerpos[0] - self.obsdata["ra"]) * Stn.degree
|
|
1726
|
+
)
|
|
1727
|
+
y1 = np.cos(self.centerpos[1] * Stn.degree) * np.sin(
|
|
1728
|
+
(self.centerpos[0] - self.obsdata["ra"]) * Stn.degree
|
|
1729
|
+
)
|
|
1730
|
+
z1 = np.sin(self.centerpos[1] * Stn.degree) * np.ones((n_obs_tot,))
|
|
1731
|
+
# then rotate the Dec direction
|
|
1732
|
+
x2 = np.sin(self.obsdata["dec"] * Stn.degree) * x1 - np.cos(self.obsdata["dec"] * Stn.degree) * z1
|
|
1733
|
+
y2 = y1
|
|
1734
|
+
z2 = np.cos(self.obsdata["dec"] * Stn.degree) * x1 + np.sin(self.obsdata["dec"] * Stn.degree) * z1
|
|
1735
|
+
# and finally the PA direction
|
|
1736
|
+
X = (
|
|
1737
|
+
-np.sin(self.obsdata["pa"] * Stn.degree) * x2 - np.cos(self.obsdata["pa"] * Stn.degree) * y2
|
|
1738
|
+
) / Stn.degree
|
|
1739
|
+
Y = (
|
|
1740
|
+
-np.cos(self.obsdata["pa"] * Stn.degree) * x2 + np.sin(self.obsdata["pa"] * Stn.degree) * y2
|
|
1741
|
+
) / Stn.degree
|
|
1742
|
+
#
|
|
1743
|
+
# throw away points in wrong hemisphere -- important since in orthographic projection,
|
|
1744
|
+
# can have (X,Y)=0 for antipodal point
|
|
1745
|
+
X = np.where(z2 > 0, X, 1e49)
|
|
1746
|
+
|
|
1747
|
+
for isca in range(18):
|
|
1748
|
+
obsgood = np.where(
|
|
1749
|
+
np.logical_and(
|
|
1750
|
+
np.sqrt((X - Stn.SCAFov[isca][0]) ** 2 + (Y - Stn.SCAFov[isca][1]) ** 2) < radius,
|
|
1751
|
+
self.obsdata["filter"] == self.cfg.use_filter,
|
|
1752
|
+
)
|
|
1753
|
+
)
|
|
1754
|
+
for k in range(len(obsgood[0])):
|
|
1755
|
+
self.obslist.append((obsgood[0][k], isca + 1))
|
|
1756
|
+
|
|
1757
|
+
self.obslist.sort()
|
|
1758
|
+
|
|
1759
|
+
def _build_use_instamps(self) -> None:
|
|
1760
|
+
"""Build use_instamps, Boolean array indicating whether to use each input postage stamp."""
|
|
1761
|
+
|
|
1762
|
+
self.use_instamps = np.zeros((self.cfg.n1P + 2, self.cfg.n1P + 2), dtype=bool)
|
|
1763
|
+
|
|
1764
|
+
n_coadded = 0 # number of output postage stamps to be coadded
|
|
1765
|
+
for j_st in range(self.j_st_min, self.j_st_max + 1, 2):
|
|
1766
|
+
for i_st in range(self.i_st_min, self.i_st_max + 1, 2):
|
|
1767
|
+
for dj, di in product(range(2), range(2)):
|
|
1768
|
+
self.use_instamps[j_st + dj - 1 : j_st + dj + 2, i_st + di - 1 : i_st + di + 2] = True
|
|
1769
|
+
|
|
1770
|
+
n_coadded += 1
|
|
1771
|
+
if n_coadded == self.nrun:
|
|
1772
|
+
return
|
|
1773
|
+
|
|
1774
|
+
def _handle_postage_pad(self) -> None:
|
|
1775
|
+
"""Handle the postage padding."""
|
|
1776
|
+
|
|
1777
|
+
postage_pad = self.cfg.postage_pad # shortcut
|
|
1778
|
+
self.j_st_min = self.i_st_min = postage_pad + 1 # 3 by default
|
|
1779
|
+
self.j_st_max = self.i_st_max = self.j_st_min + self.cfg.n1 - 1 # 50 by default
|
|
1780
|
+
self.pad_sides = "" # will also be used in build_output_file
|
|
1781
|
+
|
|
1782
|
+
# adjust these based on which side(s) to pad on
|
|
1783
|
+
if self.cfg.pad_sides == "all": # pad on all sides
|
|
1784
|
+
self.pad_sides = "BTLR"
|
|
1785
|
+
|
|
1786
|
+
elif self.cfg.pad_sides == "auto": # pad on mosaic boundaries only
|
|
1787
|
+
nblock = self.cfg.nblock # shortcut
|
|
1788
|
+
ibx, iby = divmod(self.this_sub, self.cfg.nblock)
|
|
1789
|
+
if iby == 0:
|
|
1790
|
+
self.pad_sides += "B"
|
|
1791
|
+
elif iby == nblock - 1:
|
|
1792
|
+
self.pad_sides += "T"
|
|
1793
|
+
if ibx == 0:
|
|
1794
|
+
self.pad_sides += "L"
|
|
1795
|
+
elif ibx == nblock - 1:
|
|
1796
|
+
self.pad_sides += "R"
|
|
1797
|
+
|
|
1798
|
+
elif self.cfg.pad_sides != "none": # pad on sides specified by the user
|
|
1799
|
+
self.pad_sides = self.cfg.pad_sides
|
|
1800
|
+
|
|
1801
|
+
if "B" in self.pad_sides:
|
|
1802
|
+
self.j_st_min -= postage_pad
|
|
1803
|
+
if "T" in self.pad_sides:
|
|
1804
|
+
self.j_st_max += postage_pad
|
|
1805
|
+
if "L" in self.pad_sides:
|
|
1806
|
+
self.i_st_min -= postage_pad
|
|
1807
|
+
if "R" in self.pad_sides:
|
|
1808
|
+
self.i_st_max += postage_pad
|
|
1809
|
+
|
|
1810
|
+
self.nrun = (self.j_st_max - self.j_st_min + 1) * (self.i_st_max - self.i_st_min + 1)
|
|
1811
|
+
if self.cfg.stoptile:
|
|
1812
|
+
self.nrun = self.cfg.stoptile
|
|
1813
|
+
self._build_use_instamps()
|
|
1814
|
+
|
|
1815
|
+
def process_input_images(self, visualize: bool = False) -> None:
|
|
1816
|
+
"""Process input images."""
|
|
1817
|
+
|
|
1818
|
+
### Now figure out which observations we need ###
|
|
1819
|
+
|
|
1820
|
+
search_radius = Stn.sca_sidelength / np.sqrt(
|
|
1821
|
+
2.0
|
|
1822
|
+
) / Stn.degree + self.cfg.NsideP * self.cfg.dtheta / np.sqrt(2.0)
|
|
1823
|
+
self._get_obs_cover(search_radius)
|
|
1824
|
+
print(
|
|
1825
|
+
len(self.obslist),
|
|
1826
|
+
f"observations within range ({search_radius:7.5f} deg)",
|
|
1827
|
+
"filter =",
|
|
1828
|
+
self.cfg.use_filter,
|
|
1829
|
+
f"({Stn.RomanFilters[self.cfg.use_filter]:s})",
|
|
1830
|
+
)
|
|
1831
|
+
|
|
1832
|
+
self.inimages = [InImage(self, idsca) for idsca in self.obslist]
|
|
1833
|
+
any_exists = False
|
|
1834
|
+
print("The observations -->")
|
|
1835
|
+
print(" OBSID SCA RAWFI DECWFI PA RASCA DECSCA FILE (x=missing)")
|
|
1836
|
+
for idsca, inimage in zip(self.obslist, self.inimages, strict=False):
|
|
1837
|
+
cpos = " "
|
|
1838
|
+
if inimage.exists_:
|
|
1839
|
+
any_exists = True
|
|
1840
|
+
cpos_coord = inimage.inwcs.all_pix2world([[Stn.sca_ctrpix, Stn.sca_ctrpix]], 0)[0]
|
|
1841
|
+
cpos = f"{cpos_coord[0]:8.4f} {cpos_coord[1]:8.4f}"
|
|
1842
|
+
print(
|
|
1843
|
+
"{:7d} {:2d} {:8.4f} {:8.4f} {:6.2f} {:s} {:s} {:s}".format(
|
|
1844
|
+
idsca[0],
|
|
1845
|
+
idsca[1],
|
|
1846
|
+
self.obsdata["ra"][idsca[0]],
|
|
1847
|
+
self.obsdata["dec"][idsca[0]],
|
|
1848
|
+
self.obsdata["pa"][idsca[0]],
|
|
1849
|
+
cpos,
|
|
1850
|
+
" " if inimage.exists_ else "x",
|
|
1851
|
+
inimage.infile,
|
|
1852
|
+
)
|
|
1853
|
+
)
|
|
1854
|
+
print()
|
|
1855
|
+
assert any_exists, "No candidate observations found to stack. Exiting now."
|
|
1856
|
+
|
|
1857
|
+
print("Reading input data ... ")
|
|
1858
|
+
self.pmask = Mask.load_permanent_mask(self)
|
|
1859
|
+
print()
|
|
1860
|
+
|
|
1861
|
+
self._handle_postage_pad()
|
|
1862
|
+
for inimage in self.inimages:
|
|
1863
|
+
if not inimage.exists_:
|
|
1864
|
+
inimage.is_relevant = False
|
|
1865
|
+
continue
|
|
1866
|
+
inimage.partition_pixels(visualize=visualize)
|
|
1867
|
+
# visualize = False # For now, visualize at most one partitioning process.
|
|
1868
|
+
if not inimage.is_relevant:
|
|
1869
|
+
continue
|
|
1870
|
+
inimage.extract_layers()
|
|
1871
|
+
print()
|
|
1872
|
+
del self.pmask
|
|
1873
|
+
|
|
1874
|
+
# remove irrelevant input images
|
|
1875
|
+
self.obslist = [self.obslist[i] for i, inimage in enumerate(self.inimages) if inimage.is_relevant]
|
|
1876
|
+
self.inimages = [inimage for inimage in self.inimages if inimage.is_relevant]
|
|
1877
|
+
self.n_inimage = len(self.inimages)
|
|
1878
|
+
|
|
1879
|
+
def build_input_stamps(self) -> None:
|
|
1880
|
+
"""Build input stamps from input images."""
|
|
1881
|
+
|
|
1882
|
+
n1P = self.cfg.n1P # shortcuts
|
|
1883
|
+
pad = self.cfg.postage_pad
|
|
1884
|
+
|
|
1885
|
+
# current version only works when acceptance radius <= postage stamp size
|
|
1886
|
+
self.instamps = [[None for i_st in range(n1P + 2)] for j_st in range(n1P + 2)] # st stands for stamp
|
|
1887
|
+
|
|
1888
|
+
n_inpix_out = 0 # number of input pixels in output region (not including padding)
|
|
1889
|
+
n_inpix_pad = 0 # number of input pixels in padding region
|
|
1890
|
+
|
|
1891
|
+
for j_st in range(n1P + 2):
|
|
1892
|
+
for i_st in range(n1P + 2):
|
|
1893
|
+
if self.use_instamps[j_st, i_st]:
|
|
1894
|
+
self.instamps[j_st][i_st] = InStamp(self, j_st, i_st)
|
|
1895
|
+
|
|
1896
|
+
if (pad < j_st <= n1P - pad) and (pad < i_st <= n1P - pad):
|
|
1897
|
+
n_inpix_out += self.instamps[j_st][i_st].pix_cumsum[-1]
|
|
1898
|
+
elif (0 < j_st <= n1P) and (0 < i_st <= n1P):
|
|
1899
|
+
n_inpix_pad += self.instamps[j_st][i_st].pix_cumsum[-1]
|
|
1900
|
+
|
|
1901
|
+
print(f"number of input pixels in output region: {n_inpix_out = }")
|
|
1902
|
+
print(f"number of input pixels in padding region: {n_inpix_pad = }")
|
|
1903
|
+
print()
|
|
1904
|
+
|
|
1905
|
+
del self.use_instamps
|
|
1906
|
+
for inimage in self.inimages:
|
|
1907
|
+
inimage.clear()
|
|
1908
|
+
|
|
1909
|
+
def _output_stamp_wrapper(self, i_st, j_st, n_coadded, sim_mode: bool = False, visualize: bool = False):
|
|
1910
|
+
"""
|
|
1911
|
+
Wrapper for output stamp coaddition.
|
|
1912
|
+
|
|
1913
|
+
Parameters
|
|
1914
|
+
----------
|
|
1915
|
+
j_st : int
|
|
1916
|
+
Vertical OutStamp index.
|
|
1917
|
+
i_st : int
|
|
1918
|
+
Horizontal OutStamp index.
|
|
1919
|
+
n_coadded : int
|
|
1920
|
+
Number of postage stamps to be coadded.
|
|
1921
|
+
sim_mode : bool, optional
|
|
1922
|
+
Whether to count references without actually making inpsfgrp.
|
|
1923
|
+
See the docstring of psfutil.SysMatA._compute_iisubmats.
|
|
1924
|
+
visualize : bool, optional
|
|
1925
|
+
Perform visualizations? (Usually just for testing.)
|
|
1926
|
+
|
|
1927
|
+
Returns
|
|
1928
|
+
-------
|
|
1929
|
+
None
|
|
1930
|
+
|
|
1931
|
+
"""
|
|
1932
|
+
|
|
1933
|
+
assert 1 <= i_st <= self.cfg.n1P and 1 <= j_st <= self.cfg.n1P, "outstamp out of boundary"
|
|
1934
|
+
|
|
1935
|
+
if sim_mode: # count references to PSF overlaps and system submatrices
|
|
1936
|
+
self.outstamps[j_st][i_st] = OutStamp(self, j_st, i_st, visualize=visualize)
|
|
1937
|
+
else:
|
|
1938
|
+
print(
|
|
1939
|
+
f"postage stamp {i_st:2d},{j_st:2d} {100 * n_coadded / self.nrun:6.3f}% "
|
|
1940
|
+
f"t= {self.timer():9.2f} s",
|
|
1941
|
+
flush=True,
|
|
1942
|
+
)
|
|
1943
|
+
outst = self.outstamps[j_st][i_st]
|
|
1944
|
+
outst(visualize=visualize)
|
|
1945
|
+
|
|
1946
|
+
bottom = (j_st - 1) * self.cfg.n2
|
|
1947
|
+
top = j_st * self.cfg.n2 + self.cfg.fade_kernel * 2
|
|
1948
|
+
left = (i_st - 1) * self.cfg.n2
|
|
1949
|
+
right = i_st * self.cfg.n2 + self.cfg.fade_kernel * 2
|
|
1950
|
+
|
|
1951
|
+
self.out_map[:, :, bottom:top, left:right] += outst.outimage
|
|
1952
|
+
self.T_weightmap[:, :, j_st - 1, i_st - 1] = outst.Tsum_stamp # weight computations
|
|
1953
|
+
|
|
1954
|
+
outmaps = self.cfg.outmaps # shortcut
|
|
1955
|
+
if "U" in outmaps:
|
|
1956
|
+
self.UC_map[:, bottom:top, left:right] += outst.UC # fidelity map
|
|
1957
|
+
if "S" in outmaps:
|
|
1958
|
+
self.Sigma_map[:, bottom:top, left:right] += outst.Sigma # noise map
|
|
1959
|
+
if "K" in outmaps:
|
|
1960
|
+
self.kappa_map[:, bottom:top, left:right] += outst.kappa # kappa map
|
|
1961
|
+
if "T" in outmaps:
|
|
1962
|
+
self.Tsum_map[:, bottom:top, left:right] += outst.Tsum_inpix # total weight
|
|
1963
|
+
if "N" in outmaps:
|
|
1964
|
+
self.Neff_map[:, bottom:top, left:right] += outst.Neff # effective coverage
|
|
1965
|
+
|
|
1966
|
+
outst.clear()
|
|
1967
|
+
self.outstamps[j_st][i_st] = None
|
|
1968
|
+
# coadding the OutStamp above is the last show of the InStamp below
|
|
1969
|
+
inst = self.instamps[j_st - 1][i_st - 1]
|
|
1970
|
+
inst.clear()
|
|
1971
|
+
self.instamps[j_st - 1][i_st - 1] = None
|
|
1972
|
+
|
|
1973
|
+
def coadd_output_stamps(self, sim_mode: bool = False, visualize: bool = False) -> None:
|
|
1974
|
+
"""
|
|
1975
|
+
Coadd output stamps using input stamps.
|
|
1976
|
+
|
|
1977
|
+
Parameters
|
|
1978
|
+
----------
|
|
1979
|
+
sim_mode : bool, optional
|
|
1980
|
+
Whether to count references without actually making inpsfgrp.
|
|
1981
|
+
See the docstring of psfutil.SysMatA._compute_iisubmats.
|
|
1982
|
+
visualize : bool, optional
|
|
1983
|
+
Perform visualizations? (Usually just for testing.)
|
|
1984
|
+
|
|
1985
|
+
Returns
|
|
1986
|
+
-------
|
|
1987
|
+
None
|
|
1988
|
+
|
|
1989
|
+
"""
|
|
1990
|
+
|
|
1991
|
+
if sim_mode: # count references to PSF overlaps and system submatrices
|
|
1992
|
+
self.sysmata = SysMatA(self)
|
|
1993
|
+
self.sysmatb = SysMatB(self)
|
|
1994
|
+
self.outstamps = [[None for i_st in range(self.cfg.n1P + 2)] for j_st in range(self.cfg.n1P + 2)]
|
|
1995
|
+
|
|
1996
|
+
else:
|
|
1997
|
+
n_out = self.outpsfgrp.n_psf
|
|
1998
|
+
NsidePf = self.cfg.NsideP + self.cfg.fade_kernel * 2
|
|
1999
|
+
|
|
2000
|
+
# make basic output array (including transition pixels on the boundaries)
|
|
2001
|
+
self.out_map = np.zeros((n_out, self.cfg.n_inframe, NsidePf, NsidePf), dtype=np.float32)
|
|
2002
|
+
# allocate ancillary arrays
|
|
2003
|
+
self.T_weightmap = np.zeros((n_out, self.n_inimage, self.cfg.n1P, self.cfg.n1P), dtype=np.float32)
|
|
2004
|
+
|
|
2005
|
+
# additional information (will convert to integer)
|
|
2006
|
+
outmaps = self.cfg.outmaps # shortcut
|
|
2007
|
+
shape = (n_out, NsidePf, NsidePf)
|
|
2008
|
+
if "U" in outmaps:
|
|
2009
|
+
self.UC_map = np.zeros(shape, dtype=np.float32) # fidelity map
|
|
2010
|
+
if "S" in outmaps:
|
|
2011
|
+
self.Sigma_map = np.zeros(shape, dtype=np.float32) # noise map
|
|
2012
|
+
if "K" in outmaps:
|
|
2013
|
+
self.kappa_map = np.zeros(shape, dtype=np.float32) # kappa map
|
|
2014
|
+
if "T" in outmaps:
|
|
2015
|
+
self.Tsum_map = np.zeros(shape, dtype=np.float32) # total weight
|
|
2016
|
+
if "N" in outmaps:
|
|
2017
|
+
self.Neff_map = np.zeros(shape, dtype=np.float32) # effective coverage
|
|
2018
|
+
|
|
2019
|
+
### Begin loop over all the postage stamps we want to create ###
|
|
2020
|
+
|
|
2021
|
+
n_coadded = 0 # number of coadded postage stamps
|
|
2022
|
+
if self.j_st_max + 1 - self.j_st_min % 2 == 1 or self.i_st_max + 1 - self.i_st_min % 2 == 1:
|
|
2023
|
+
raise ValueError(
|
|
2024
|
+
f"Size must be even: y={self.j_st_min}..{self.j_st_max}, x={self.i_st_min}..{self.i_st_max}"
|
|
2025
|
+
)
|
|
2026
|
+
for j_st in range(self.j_st_min, self.j_st_max + 1, 2):
|
|
2027
|
+
for i_st in range(self.i_st_min, self.i_st_max + 1, 2):
|
|
2028
|
+
for dj, di in product(range(2), range(2)):
|
|
2029
|
+
self._output_stamp_wrapper(i_st + di, j_st + dj, n_coadded, sim_mode, visualize)
|
|
2030
|
+
n_coadded += 1
|
|
2031
|
+
|
|
2032
|
+
if n_coadded == self.nrun:
|
|
2033
|
+
if sim_mode:
|
|
2034
|
+
self.sysmata.iisubmats.clear()
|
|
2035
|
+
self.sysmatb.iopsfovls.clear()
|
|
2036
|
+
else:
|
|
2037
|
+
assert len(self.sysmata.iisubmats) == 0, "self.sysmata.iisubmats is not empty"
|
|
2038
|
+
assert len(self.sysmatb.iopsfovls) == 0, "self.sysmatb.iopsfovls is not empty"
|
|
2039
|
+
return
|
|
2040
|
+
|
|
2041
|
+
if not sim_mode:
|
|
2042
|
+
gc.collect() # force a garbage collection
|
|
2043
|
+
|
|
2044
|
+
if not sim_mode:
|
|
2045
|
+
for i_st in range(self.i_st_min, self.i_st_max + 1, 2):
|
|
2046
|
+
for dj in range(-1, 1):
|
|
2047
|
+
inst = self.instamps[j_st + dj][i_st]
|
|
2048
|
+
if inst is not None:
|
|
2049
|
+
inst.clear()
|
|
2050
|
+
self.instamps[j_st + dj][i_st] = None
|
|
2051
|
+
gc.collect() # force a garbage collection
|
|
2052
|
+
|
|
2053
|
+
# print(" --> intermediate output -->\n")
|
|
2054
|
+
# self.build_output_file(is_final=False)
|
|
2055
|
+
|
|
2056
|
+
@staticmethod
|
|
2057
|
+
def compress_map(
|
|
2058
|
+
map_: np.array,
|
|
2059
|
+
coef: int,
|
|
2060
|
+
dtype: type,
|
|
2061
|
+
header: fits.Header = None,
|
|
2062
|
+
EXTNAME: str = None,
|
|
2063
|
+
UNIT: (str, str) = None,
|
|
2064
|
+
) -> fits.ImageHDU:
|
|
2065
|
+
"""
|
|
2066
|
+
Compress float32 map to (u)int16 to save storage.
|
|
2067
|
+
|
|
2068
|
+
Parameters
|
|
2069
|
+
----------
|
|
2070
|
+
map_ : np.array
|
|
2071
|
+
Map to be compressed. Shape is usually (NsideP, NsideP).
|
|
2072
|
+
coef : int
|
|
2073
|
+
Coefficient for log10 values.
|
|
2074
|
+
dtype : type
|
|
2075
|
+
Data type of the compressed map, np.(u)int16.
|
|
2076
|
+
header : fits.Header
|
|
2077
|
+
Template header of the HDU.
|
|
2078
|
+
If None, return the compressed map instead of an HDU.
|
|
2079
|
+
EXTNAME : str
|
|
2080
|
+
EXTNAME keyword of the header.
|
|
2081
|
+
If None, return the compressed map instead of an HDU.
|
|
2082
|
+
UNIT : (str, str)
|
|
2083
|
+
UNIT keyword of the header.
|
|
2084
|
+
If None, return the compressed map instead of an HDU.
|
|
2085
|
+
|
|
2086
|
+
Returns
|
|
2087
|
+
-------
|
|
2088
|
+
fits.ImageHDU
|
|
2089
|
+
HDU containing the compressed array.
|
|
2090
|
+
|
|
2091
|
+
"""
|
|
2092
|
+
|
|
2093
|
+
if dtype == np.uint16:
|
|
2094
|
+
a_min, a_max = 0, 65535
|
|
2095
|
+
elif dtype == np.int16:
|
|
2096
|
+
a_min, a_max = -32768, 32767
|
|
2097
|
+
|
|
2098
|
+
my_map = np.clip(np.floor(coef * np.log10(np.clip(map_, 1e-32, None)) + 0.5), a_min, a_max).astype(
|
|
2099
|
+
dtype
|
|
2100
|
+
)
|
|
2101
|
+
if header is None or EXTNAME is None or UNIT is None:
|
|
2102
|
+
return my_map
|
|
2103
|
+
|
|
2104
|
+
my_hdu = fits.ImageHDU(my_map, header=header)
|
|
2105
|
+
del my_map
|
|
2106
|
+
my_hdu.header["EXTNAME"] = EXTNAME
|
|
2107
|
+
my_hdu.header["UNIT"] = UNIT
|
|
2108
|
+
return my_hdu
|
|
2109
|
+
|
|
2110
|
+
def build_output_file(self, is_final: bool = False) -> None:
|
|
2111
|
+
"""
|
|
2112
|
+
Build the output FITS file.
|
|
2113
|
+
|
|
2114
|
+
The kappa maps have been unified and merged into the main FITS file.
|
|
2115
|
+
|
|
2116
|
+
Parameters
|
|
2117
|
+
----------
|
|
2118
|
+
is_final : bool, optional
|
|
2119
|
+
Whether this is the final (i.e., not intermediate) output.
|
|
2120
|
+
If so, recover the faded block boundaries.
|
|
2121
|
+
|
|
2122
|
+
Returns
|
|
2123
|
+
-------
|
|
2124
|
+
None
|
|
2125
|
+
|
|
2126
|
+
"""
|
|
2127
|
+
|
|
2128
|
+
# shortcuts
|
|
2129
|
+
fk = self.cfg.fade_kernel
|
|
2130
|
+
NsidePf = self.cfg.NsideP + fk * 2
|
|
2131
|
+
outmaps = self.cfg.outmaps
|
|
2132
|
+
|
|
2133
|
+
if is_final: # recover block boundaries
|
|
2134
|
+
OutStamp.trapezoid(self.out_map, fk, recover_mode=True)
|
|
2135
|
+
width = self.cfg.postage_pad * self.cfg.n2 # width of padding region
|
|
2136
|
+
pad_widths = (
|
|
2137
|
+
width * ("B" not in self.pad_sides),
|
|
2138
|
+
width * ("T" not in self.pad_sides),
|
|
2139
|
+
width * ("L" not in self.pad_sides),
|
|
2140
|
+
width * ("R" not in self.pad_sides),
|
|
2141
|
+
)
|
|
2142
|
+
if "U" in outmaps:
|
|
2143
|
+
OutStamp.trapezoid(self.UC_map, fk, True, pad_widths)
|
|
2144
|
+
if "S" in outmaps:
|
|
2145
|
+
OutStamp.trapezoid(self.Sigma_map, fk, True, pad_widths)
|
|
2146
|
+
if "K" in outmaps:
|
|
2147
|
+
OutStamp.trapezoid(self.kappa_map, fk, True, pad_widths)
|
|
2148
|
+
if "T" in outmaps:
|
|
2149
|
+
OutStamp.trapezoid(self.Tsum_map, fk, True, pad_widths)
|
|
2150
|
+
if "N" in outmaps:
|
|
2151
|
+
OutStamp.trapezoid(self.Neff_map, fk, True, pad_widths)
|
|
2152
|
+
|
|
2153
|
+
my_header = self.outwcs.to_header()
|
|
2154
|
+
maphdu = fits.PrimaryHDU(self.out_map[:, :, fk : NsidePf - fk, fk : NsidePf - fk], header=my_header)
|
|
2155
|
+
config_hdu = fits.TableHDU.from_columns(
|
|
2156
|
+
[fits.Column(name="text", array=self.cfg.to_file(None).splitlines(), format="A512", ascii=True)]
|
|
2157
|
+
)
|
|
2158
|
+
config_hdu.header["EXTNAME"] = "CONFIG"
|
|
2159
|
+
config_hdu.header["TILESCHM"] = (self.cfg.tileschm, "Tiling scheme name")
|
|
2160
|
+
config_hdu.header["RERUN"] = (self.cfg.rerun, "Rerun name")
|
|
2161
|
+
config_hdu.header["MOSAIC"] = (self.cfg.mosaic, "Mosaic number")
|
|
2162
|
+
config_hdu.header["FILTER"] = (Stn.RomanFilters[self.cfg.use_filter], "Filter code")
|
|
2163
|
+
config_hdu.header["BLOCKX"] = self.ibx
|
|
2164
|
+
config_hdu.header["BLOCKY"] = self.iby
|
|
2165
|
+
if is_final:
|
|
2166
|
+
for package in ["numpy", "scipy", "astropy", "fitsio", "asdf", "pyimcom", "furry_parakeet"]:
|
|
2167
|
+
keyword = "V" + package.upper()[:7]
|
|
2168
|
+
pkgname = package
|
|
2169
|
+
if package == "numpy":
|
|
2170
|
+
pkgname = "np"
|
|
2171
|
+
try:
|
|
2172
|
+
config_hdu.header[keyword] = (
|
|
2173
|
+
str(globals()[pkgname].__version__),
|
|
2174
|
+
f"Current version of {package}",
|
|
2175
|
+
)
|
|
2176
|
+
except (KeyError, AttributeError):
|
|
2177
|
+
config_hdu.header[keyword] = ("N/A", f"{package} had no version number")
|
|
2178
|
+
inlist_hdu = fits.BinTableHDU.from_columns(
|
|
2179
|
+
[
|
|
2180
|
+
fits.Column(name="obsid", array=np.array([obs[0] for obs in self.obslist]), format="J"),
|
|
2181
|
+
fits.Column(name="sca", array=np.array([obs[1] for obs in self.obslist]), format="I"),
|
|
2182
|
+
fits.Column(
|
|
2183
|
+
name="ra",
|
|
2184
|
+
array=np.array([self.obsdata["ra"][obs[0]] for obs in self.obslist]),
|
|
2185
|
+
format="D",
|
|
2186
|
+
unit="degree",
|
|
2187
|
+
),
|
|
2188
|
+
fits.Column(
|
|
2189
|
+
name="dec",
|
|
2190
|
+
array=np.array([self.obsdata["dec"][obs[0]] for obs in self.obslist]),
|
|
2191
|
+
format="D",
|
|
2192
|
+
unit="degree",
|
|
2193
|
+
),
|
|
2194
|
+
fits.Column(
|
|
2195
|
+
name="pa",
|
|
2196
|
+
array=np.array([self.obsdata["pa"][obs[0]] for obs in self.obslist]),
|
|
2197
|
+
format="D",
|
|
2198
|
+
unit="degree",
|
|
2199
|
+
),
|
|
2200
|
+
fits.Column(
|
|
2201
|
+
name="valid", array=np.array([inimage.exists_ for inimage in self.inimages]), format="L"
|
|
2202
|
+
),
|
|
2203
|
+
]
|
|
2204
|
+
)
|
|
2205
|
+
inlist_hdu.header["EXTNAME"] = "INDATA"
|
|
2206
|
+
T_hdu = fits.ImageHDU(self.T_weightmap)
|
|
2207
|
+
T_hdu.header["EXTNAME"] = "INWEIGHT"
|
|
2208
|
+
T_hdu2 = fits.ImageHDU(
|
|
2209
|
+
np.transpose(self.T_weightmap, axes=(0, 2, 1, 3)).reshape(
|
|
2210
|
+
(self.outpsfgrp.n_psf * self.cfg.n1P, self.n_inimage * self.cfg.n1P)
|
|
2211
|
+
)
|
|
2212
|
+
)
|
|
2213
|
+
T_hdu2.header["EXTNAME"] = "INWTFLAT"
|
|
2214
|
+
|
|
2215
|
+
hdulist = [maphdu, config_hdu, inlist_hdu, T_hdu, T_hdu2]
|
|
2216
|
+
|
|
2217
|
+
if "U" in outmaps:
|
|
2218
|
+
hdulist.append(
|
|
2219
|
+
Block.compress_map(
|
|
2220
|
+
self.UC_map[:, fk : NsidePf - fk, fk : NsidePf - fk],
|
|
2221
|
+
-5000,
|
|
2222
|
+
np.uint16,
|
|
2223
|
+
my_header,
|
|
2224
|
+
"FIDELITY",
|
|
2225
|
+
("-0.2mB", "-5000*log10(U/C)"),
|
|
2226
|
+
)
|
|
2227
|
+
)
|
|
2228
|
+
|
|
2229
|
+
if "S" in outmaps:
|
|
2230
|
+
hdulist.append(
|
|
2231
|
+
Block.compress_map(
|
|
2232
|
+
self.Sigma_map[:, fk : NsidePf - fk, fk : NsidePf - fk],
|
|
2233
|
+
-10000,
|
|
2234
|
+
np.int16,
|
|
2235
|
+
my_header,
|
|
2236
|
+
"SIGMA",
|
|
2237
|
+
("-0.1mB", "-10000*log10(Sigma)"),
|
|
2238
|
+
)
|
|
2239
|
+
)
|
|
2240
|
+
|
|
2241
|
+
if "K" in outmaps:
|
|
2242
|
+
hdulist.append(
|
|
2243
|
+
Block.compress_map(
|
|
2244
|
+
self.kappa_map[:, fk : NsidePf - fk, fk : NsidePf - fk],
|
|
2245
|
+
-5000,
|
|
2246
|
+
np.uint16,
|
|
2247
|
+
my_header,
|
|
2248
|
+
"KAPPA",
|
|
2249
|
+
("-0.2mB", "-5000*log10(kappa)"),
|
|
2250
|
+
)
|
|
2251
|
+
)
|
|
2252
|
+
|
|
2253
|
+
if "T" in outmaps:
|
|
2254
|
+
hdulist.append(
|
|
2255
|
+
Block.compress_map(
|
|
2256
|
+
self.Tsum_map[:, fk : NsidePf - fk, fk : NsidePf - fk],
|
|
2257
|
+
200000,
|
|
2258
|
+
np.int16,
|
|
2259
|
+
my_header,
|
|
2260
|
+
"INWTSUM",
|
|
2261
|
+
("5uB", "200000*log10(Tsum)"),
|
|
2262
|
+
)
|
|
2263
|
+
)
|
|
2264
|
+
|
|
2265
|
+
if "N" in outmaps:
|
|
2266
|
+
hdulist.append(
|
|
2267
|
+
Block.compress_map(
|
|
2268
|
+
self.Neff_map[:, fk : NsidePf - fk, fk : NsidePf - fk],
|
|
2269
|
+
50000,
|
|
2270
|
+
np.uint16,
|
|
2271
|
+
my_header,
|
|
2272
|
+
"EFFCOVER",
|
|
2273
|
+
("20uB", "50000*log10(Neff)"),
|
|
2274
|
+
)
|
|
2275
|
+
)
|
|
2276
|
+
|
|
2277
|
+
# splitpsf information
|
|
2278
|
+
if self.cfg.psfsplit:
|
|
2279
|
+
text = ""
|
|
2280
|
+
iter = 0
|
|
2281
|
+
iterfile = self.cfg.inlayercache + "_iter.txt"
|
|
2282
|
+
oldcfgfile = self.cfg.inlayercache + "_oldcfg.json"
|
|
2283
|
+
if exists(iterfile):
|
|
2284
|
+
with open(iterfile, "r") as f:
|
|
2285
|
+
iter = int(f.read().split()[0])
|
|
2286
|
+
if exists(oldcfgfile):
|
|
2287
|
+
with open(oldcfgfile, "r") as fcfg:
|
|
2288
|
+
text = fcfg.read()
|
|
2289
|
+
prevconfig_hdu = fits.TableHDU.from_columns(
|
|
2290
|
+
[fits.Column(name="text", array=text.split(), format="A512", ascii=True)]
|
|
2291
|
+
)
|
|
2292
|
+
prevconfig_hdu.header["EXTNAME"] = "OLDCFG"
|
|
2293
|
+
prevconfig_hdu.header["IMSBITER"] = (iter, "Number of iterations of PSFSPLIT")
|
|
2294
|
+
prevconfig_hdu.header["COMMENT"] = "Configuration files from previous iterations."
|
|
2295
|
+
hdulist.append(prevconfig_hdu)
|
|
2296
|
+
|
|
2297
|
+
hdu_list = fits.HDUList(hdulist)
|
|
2298
|
+
hdu_list.writeto(self.outstem + ".fits", overwrite=True)
|
|
2299
|
+
|
|
2300
|
+
def clear_all(self) -> None:
|
|
2301
|
+
"""Free up all memory spaces."""
|
|
2302
|
+
|
|
2303
|
+
if self.cfg.tempfile is not None:
|
|
2304
|
+
self.cache_dir.rmdir()
|
|
2305
|
+
|
|
2306
|
+
del self.obsdata, self.obslist, self.outwcs
|
|
2307
|
+
del self.outpsfgrp, self.outpsfovl
|
|
2308
|
+
self.sysmata.clear()
|
|
2309
|
+
del self.sysmata
|
|
2310
|
+
self.sysmatb.clear()
|
|
2311
|
+
del self.sysmatb
|
|
2312
|
+
del self.out_map, self.T_weightmap
|
|
2313
|
+
|
|
2314
|
+
outmaps = self.cfg.outmaps # shortcut
|
|
2315
|
+
if "U" in outmaps:
|
|
2316
|
+
del self.UC_map
|
|
2317
|
+
if "S" in outmaps:
|
|
2318
|
+
del self.Sigma_map
|
|
2319
|
+
if "K" in outmaps:
|
|
2320
|
+
del self.kappa_map
|
|
2321
|
+
if "T" in outmaps:
|
|
2322
|
+
del self.Tsum_map
|
|
2323
|
+
if "N" in outmaps:
|
|
2324
|
+
del self.Neff_map
|
|
2325
|
+
|
|
2326
|
+
for j_st in range(self.cfg.n1P + 2):
|
|
2327
|
+
for i_st in range(self.cfg.n1P + 2):
|
|
2328
|
+
inst = self.instamps[j_st][i_st]
|
|
2329
|
+
if inst is not None:
|
|
2330
|
+
inst.clear()
|
|
2331
|
+
self.instamps[j_st][i_st] = None
|