pyimcom 1.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyimcom/__init__.py +1 -0
- pyimcom/_version.py +24 -0
- pyimcom/analysis.py +1480 -0
- pyimcom/coadd.py +2331 -0
- pyimcom/compress/__init__.py +0 -0
- pyimcom/compress/compressutils.py +506 -0
- pyimcom/compress/compressutils_wrapper.py +116 -0
- pyimcom/compress/i24.py +514 -0
- pyimcom/config.py +1245 -0
- pyimcom/diagnostics/__init__.py +0 -0
- pyimcom/diagnostics/context_figure.py +58 -0
- pyimcom/diagnostics/dynrange.py +274 -0
- pyimcom/diagnostics/layer_diagnostics.py +208 -0
- pyimcom/diagnostics/mosaicimage.py +80 -0
- pyimcom/diagnostics/noise/stability.py +126 -0
- pyimcom/diagnostics/noise_diagnostics.py +709 -0
- pyimcom/diagnostics/outimage_utils/__init__.py +0 -0
- pyimcom/diagnostics/outimage_utils/helper.py +82 -0
- pyimcom/diagnostics/report.py +366 -0
- pyimcom/diagnostics/run.py +64 -0
- pyimcom/diagnostics/starcube_nonoise.py +264 -0
- pyimcom/diagnostics/starcube_nonoise_coldescr.txt +24 -0
- pyimcom/diagnostics/stars.py +469 -0
- pyimcom/imdestripe.py +2454 -0
- pyimcom/lakernel.py +805 -0
- pyimcom/layer.py +1439 -0
- pyimcom/layer_wrapper.py +96 -0
- pyimcom/meta/__init__.py +0 -0
- pyimcom/meta/distortimage.py +748 -0
- pyimcom/meta/ginterp.py +340 -0
- pyimcom/pictures/__init__.py +0 -0
- pyimcom/pictures/genpic.py +229 -0
- pyimcom/psfutil.py +2199 -0
- pyimcom/routine.py +588 -0
- pyimcom/splitpsf/__init__.py +0 -0
- pyimcom/splitpsf/imsubtract.py +793 -0
- pyimcom/splitpsf/imsubtract_wrapper.py +107 -0
- pyimcom/splitpsf/splitpsf.py +497 -0
- pyimcom/splitpsf/splitpsf_wrapper.py +161 -0
- pyimcom/splitpsf/update_cube.py +136 -0
- pyimcom/truthcats.py +396 -0
- pyimcom/utils/__init__.py +0 -0
- pyimcom/utils/compareutils.py +207 -0
- pyimcom/utils/piffutils.py +223 -0
- pyimcom/wcsutil.py +839 -0
- pyimcom-1.2.1.dist-info/METADATA +67 -0
- pyimcom-1.2.1.dist-info/RECORD +52 -0
- pyimcom-1.2.1.dist-info/WHEEL +5 -0
- pyimcom-1.2.1.dist-info/licenses/LICENSE +21 -0
- pyimcom-1.2.1.dist-info/scm_file_list.json +285 -0
- pyimcom-1.2.1.dist-info/scm_version.json +8 -0
- pyimcom-1.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
4
|
+
|
|
5
|
+
# import from furry_parakeet
|
|
6
|
+
from ..config import Config
|
|
7
|
+
|
|
8
|
+
# local imports
|
|
9
|
+
from .imsubtract import run_imsubtract_single
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_imsubtract_all(
|
|
13
|
+
cfg_file, workers=4, fft_workers=None, max_imgs=None, display=None, local_output=False, mmap=None
|
|
14
|
+
):
|
|
15
|
+
"""
|
|
16
|
+
Main routine to run imsubtract on all images in the cache.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
cfg_file: str
|
|
21
|
+
Path to the config file.
|
|
22
|
+
workers: int, optional
|
|
23
|
+
Number of workers to use for parallel processing. Default is 4.
|
|
24
|
+
fft_workers: int, optional
|
|
25
|
+
Number of workers to use for FFT parallelism (if requested).
|
|
26
|
+
max_imgs: int, optional
|
|
27
|
+
If provided, does computations for a maximum number of SCAs. Most users will
|
|
28
|
+
want the default of None; this is provided mainly for testing.
|
|
29
|
+
display: str or None, optional
|
|
30
|
+
Display location for intermediate steps. Default is None.
|
|
31
|
+
local_output: bool, optional
|
|
32
|
+
Whether to direct the file to local output instead of the cache directory.
|
|
33
|
+
mmap : str or str-like, optional
|
|
34
|
+
Directory to put temporary mmap files.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
# Additional imports
|
|
38
|
+
import multiprocessing as mp
|
|
39
|
+
import traceback
|
|
40
|
+
|
|
41
|
+
# load the file using Config and get information
|
|
42
|
+
cfgdata = Config(cfg_file)
|
|
43
|
+
|
|
44
|
+
cacheinfo = cfgdata.inlayercache
|
|
45
|
+
|
|
46
|
+
# separate the path from the inlayercache info
|
|
47
|
+
m = re.search(r"^(.*)\/(.*)", cacheinfo)
|
|
48
|
+
if m:
|
|
49
|
+
path = m.group(1)
|
|
50
|
+
stem = m.group(2)
|
|
51
|
+
|
|
52
|
+
# create empty list of exposures
|
|
53
|
+
exps = []
|
|
54
|
+
|
|
55
|
+
# find all the fits files and add them to the list
|
|
56
|
+
for _, _, files in os.walk(path):
|
|
57
|
+
for file in files:
|
|
58
|
+
if file.startswith(stem) and file.endswith(".fits") and file[-6].isdigit():
|
|
59
|
+
exps.append(file)
|
|
60
|
+
|
|
61
|
+
# print("List of exposures:", exps)
|
|
62
|
+
|
|
63
|
+
# Run imsubtract on each exposure in parallel using ProcessPoolExecutor
|
|
64
|
+
count = 0
|
|
65
|
+
start_method = "forkserver" if os.name.lower() == "posix" else "spawn"
|
|
66
|
+
ctx = mp.get_context(start_method)
|
|
67
|
+
nfail = 0
|
|
68
|
+
|
|
69
|
+
with ProcessPoolExecutor(max_workers=workers, mp_context=ctx) as executor:
|
|
70
|
+
futures = []
|
|
71
|
+
for exp in exps:
|
|
72
|
+
if max_imgs is not None and count > max_imgs:
|
|
73
|
+
break
|
|
74
|
+
m2 = re.search(r"(\w*)_0*(\d*)_(\d*).fits", exp)
|
|
75
|
+
if m2:
|
|
76
|
+
obsid = int(m2.group(2))
|
|
77
|
+
scaid = int(m2.group(3))
|
|
78
|
+
futures.append(
|
|
79
|
+
executor.submit(
|
|
80
|
+
run_imsubtract_single,
|
|
81
|
+
cfgdata,
|
|
82
|
+
scaid,
|
|
83
|
+
obsid,
|
|
84
|
+
path,
|
|
85
|
+
exp,
|
|
86
|
+
display=display,
|
|
87
|
+
fft_workers=fft_workers,
|
|
88
|
+
local_output=local_output,
|
|
89
|
+
max_layers=max_imgs,
|
|
90
|
+
mmap=mmap,
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
count += 1
|
|
94
|
+
|
|
95
|
+
# Wait for all futures to complete
|
|
96
|
+
for future in as_completed(futures):
|
|
97
|
+
try:
|
|
98
|
+
future.result()
|
|
99
|
+
print(f"Completed {count}/{len(futures)}", flush=True)
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
nfail += 1
|
|
103
|
+
print(f"Worker failed with exception {e}", flush=True)
|
|
104
|
+
traceback.print_exc()
|
|
105
|
+
|
|
106
|
+
if nfail > 0:
|
|
107
|
+
print(f"{nfail}/{len(futures)} instances of run_imsubtract_single failed.", flush=True)
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import asdf
|
|
6
|
+
import numpy as np
|
|
7
|
+
import scipy
|
|
8
|
+
import scipy.signal
|
|
9
|
+
from astropy.io import fits
|
|
10
|
+
from scipy.special import eval_legendre, roots_legendre
|
|
11
|
+
|
|
12
|
+
from ..coadd import InImage
|
|
13
|
+
from ..config import Settings
|
|
14
|
+
from ..layer import _get_sca_imagefile
|
|
15
|
+
from ..wcsutil import PyIMCOM_WCS, local_partial_pixel_derivatives2
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SplitPSF:
|
|
19
|
+
"""
|
|
20
|
+
Class for splitting the PSF into short- and long-range parts.
|
|
21
|
+
|
|
22
|
+
Methods
|
|
23
|
+
-------
|
|
24
|
+
Window_integratedBlackman
|
|
25
|
+
Integrated Blackman window.
|
|
26
|
+
Window_2D_integratedBlackman
|
|
27
|
+
2D version of integrated Blackman.
|
|
28
|
+
Truncate_2D_integratedBlackman
|
|
29
|
+
2D version of integrated Blackman.
|
|
30
|
+
tophatfilter
|
|
31
|
+
Smooth 3D array in each of the last 2 planes with a tophat of the given width.
|
|
32
|
+
gauss_deconv
|
|
33
|
+
Deconvolve a Gaussian, matrix C is 2x2 covariance.
|
|
34
|
+
gauss_stamp
|
|
35
|
+
Makes nxn array of a Gaussian with given covariance, centered at the image center.
|
|
36
|
+
__init__
|
|
37
|
+
Constructor.
|
|
38
|
+
build
|
|
39
|
+
Builds the short/long range decomposition for this SplitPSF.
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def Window_integratedBlackman(x):
|
|
45
|
+
"""Integrated Blackman window.
|
|
46
|
+
|
|
47
|
+
Returns 1 if x>1; 0 if x<-1; interpolates between these.
|
|
48
|
+
|
|
49
|
+
x is a numpy array.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
alpha = 0.08
|
|
53
|
+
return np.where(
|
|
54
|
+
x >= 1,
|
|
55
|
+
1.0,
|
|
56
|
+
np.where(
|
|
57
|
+
x <= -1,
|
|
58
|
+
0.0,
|
|
59
|
+
0.5 * (x + 1)
|
|
60
|
+
+ (0.5 * np.sin(np.pi * x) + alpha / 4 * np.sin(2 * np.pi * x)) / ((1 - alpha) * np.pi),
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def Window_2D_integratedBlackman(n, r1, r2):
|
|
66
|
+
"""2D version of integrated Blackman.
|
|
67
|
+
|
|
68
|
+
Inputs:
|
|
69
|
+
n = side length of array
|
|
70
|
+
r1 = inner radius of filter (pixels)
|
|
71
|
+
r2 = outer radius of filter (pixels)
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
arr = 2D image of filter, center at ((n-1)/2,(n-1)/2)
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
X_ = np.linspace((1 - n) / 2.0, (n - 1) / 2.0, n)
|
|
78
|
+
xx, yy = np.meshgrid(X_, X_)
|
|
79
|
+
r = np.sqrt(xx**2 + yy**2)
|
|
80
|
+
arr = SplitPSF.Window_integratedBlackman(-1.0 + 2.0 / (r2 - r1) * (r2 - r))
|
|
81
|
+
return arr
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def Truncate_2D_integratedBlackman(n, m):
|
|
85
|
+
"""2D version of integrated Blackman.
|
|
86
|
+
|
|
87
|
+
Inputs:
|
|
88
|
+
n = side length of array
|
|
89
|
+
m = number of pixels to truncate at the side
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
arr = 2D image of filter, center at ((n-1)/2,(n-1)/2)
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
if m == 0:
|
|
96
|
+
return np.ones((n, n)) # special case
|
|
97
|
+
|
|
98
|
+
X_ = np.ones((n,))
|
|
99
|
+
X_[:m] = SplitPSF.Window_integratedBlackman(np.linspace(-1.0, 1.0, m + 2))[1:-1]
|
|
100
|
+
X_[-m:] = X_[m - 1 :: -1]
|
|
101
|
+
return np.outer(X_, X_)
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def tophatfilter(inArray, tophatwidth):
|
|
105
|
+
"""Smooth 3D array in each of the last 2 planes with a tophat of the given width"""
|
|
106
|
+
|
|
107
|
+
npad = int(np.ceil(tophatwidth))
|
|
108
|
+
npad += (4 - npad) % 4 # make a multiple of 4
|
|
109
|
+
(nplane, ny, nx) = np.shape(inArray)
|
|
110
|
+
nyy = ny + npad * 2
|
|
111
|
+
nxx = nx + npad * 2
|
|
112
|
+
outArray = np.zeros((nplane, nyy, nxx))
|
|
113
|
+
outArray[:, npad:-npad, npad:-npad] = inArray
|
|
114
|
+
outArrayFT = np.fft.fft2(outArray)
|
|
115
|
+
|
|
116
|
+
# convolution
|
|
117
|
+
uy = np.linspace(0, nyy - 1, nyy) / nyy
|
|
118
|
+
uy = np.where(uy > 0.5, uy - 1, uy)
|
|
119
|
+
ux = np.linspace(0, nxx - 1, nxx) / nxx
|
|
120
|
+
ux = np.where(ux > 0.5, ux - 1, ux)
|
|
121
|
+
s = np.sinc(ux[None, :] * tophatwidth) * np.sinc(uy[:, None] * tophatwidth)
|
|
122
|
+
outArrayFT = outArrayFT * s[None, :, :]
|
|
123
|
+
|
|
124
|
+
outArray = np.real(np.fft.ifft2(outArrayFT))
|
|
125
|
+
if npad > 0:
|
|
126
|
+
outArray = outArray[:, npad:-npad, npad:-npad]
|
|
127
|
+
return outArray
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def gauss_deconv(arr, C, eps=1e-3):
|
|
131
|
+
"""Deconvolve a Gaussian, matrix C is 2x2 covariance (in pixel units), epsilon=cutoff"""
|
|
132
|
+
|
|
133
|
+
n = np.shape(arr)[1]
|
|
134
|
+
arr_double = np.zeros((2 * n, 2 * n), dtype=arr.dtype)
|
|
135
|
+
arr_double[:n, :n] = arr
|
|
136
|
+
ft = np.fft.fft2(arr_double.astype(np.complex128))
|
|
137
|
+
u_ = np.linspace(0, 2 * n - 1, 2 * n) / (2 * n)
|
|
138
|
+
u_[n:] = u_[n:] - 1
|
|
139
|
+
u, v = np.meshgrid(u_, u_)
|
|
140
|
+
GaussWin = np.exp(-2 * np.pi**2 * (C[0, 0] * u**2 + C[1, 1] * v**2 + 2 * C[0, 1] * u * v))
|
|
141
|
+
ft = ft * GaussWin / (GaussWin**2 + eps**2)
|
|
142
|
+
W = np.fft.ifft2(ft).real.astype(arr.dtype)
|
|
143
|
+
return W[:n, :n]
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def gauss_stamp(n, C):
|
|
147
|
+
"""Makes nxn array of a Gaussian with given covariance, centered at the image center.
|
|
148
|
+
|
|
149
|
+
n should be even. Covariance is in pixel units.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
X_ = np.linspace((1 - n) / 2.0, (n - 1) / 2.0, n)
|
|
153
|
+
xx, yy = np.meshgrid(X_, X_)
|
|
154
|
+
detC = C[0, 0] * C[1, 1] - C[0, 1] ** 2
|
|
155
|
+
iC = np.array([[C[1, 1], -C[0, 1]], [-C[0, 1], C[0, 0]]]) / detC
|
|
156
|
+
return np.exp(-0.5 * (iC[0, 0] * xx**2 + iC[1, 1] * yy**2) - iC[0, 1] * xx * yy) / (
|
|
157
|
+
2 * np.pi * np.sqrt(detC)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def __init__(self, psfcube, wcs_, pars):
|
|
161
|
+
"""Class constructor to generate a split PSF from a Legendre cube file.
|
|
162
|
+
|
|
163
|
+
Inputs:
|
|
164
|
+
psfcube = a PSF data cube in Legendre polynomial format. Each slice is a PSF image,
|
|
165
|
+
that should be multiplied by P_m(u_) P_n(v_) where flatten((n,m)) = range((order+1)**2)
|
|
166
|
+
where (u_, v_) are the coordinates on the SCA
|
|
167
|
+
|
|
168
|
+
wcs_ = WCS associated with the image. if None, then no distortion
|
|
169
|
+
|
|
170
|
+
pars = dictionary of parameters. The choices (and defaults if not specified) are:
|
|
171
|
+
ref_pixscale = 0.11 (arcsec) -> reference native pixel scale (without distortion)
|
|
172
|
+
oversamp = 8 -> oversampling of PSF relative to native scale
|
|
173
|
+
tophat_in = False -> pixel tophat already included
|
|
174
|
+
smallstamp_size = [side length of psfcube] -> size of small PSF postage stamp, in units of the
|
|
175
|
+
oversampled pixels
|
|
176
|
+
nside = 4088 -> SCA side length
|
|
177
|
+
r_in = 4.0 -> inner cut radius in native pixels
|
|
178
|
+
r_out = 9.0 -> inner cut radius in native pixels
|
|
179
|
+
sigmaGamma = 1.0 -> 1 sigma scale length of the desired output PSF, in reference input pixels
|
|
180
|
+
eps = 0.02 -> regularization parameter in Gaussian deconvolution of the PSF wings
|
|
181
|
+
m_trunc = 0 -> truncation window width at edge of PSF postage stamp
|
|
182
|
+
(in units of oversampled pixels)
|
|
183
|
+
|
|
184
|
+
A constraint is that PSF input and output sizes are even numbers of oversampled pixels, with (0,0)
|
|
185
|
+
at the center of the array.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
self.ref_pixscale = 0.11
|
|
189
|
+
if "ref_pixscale" in pars:
|
|
190
|
+
self.ref_pixscale = pars["ref_pixscale"]
|
|
191
|
+
self.oversamp = 8
|
|
192
|
+
if "oversamp" in pars:
|
|
193
|
+
self.oversamp = pars["oversamp"]
|
|
194
|
+
self.tophat_in = False
|
|
195
|
+
if "tophat_in" in pars:
|
|
196
|
+
self.tophat_in = pars["tophat_in"]
|
|
197
|
+
self.smallstamp_size = self.largestamp_size = np.shape(psfcube)[1]
|
|
198
|
+
if "smallstamp_size" in pars:
|
|
199
|
+
self.smallstamp_size = pars["smallstamp_size"]
|
|
200
|
+
self.nside = 4088
|
|
201
|
+
if "nside" in pars:
|
|
202
|
+
self.nside = pars["nside"]
|
|
203
|
+
self.r_in = 4.0
|
|
204
|
+
if "r_in" in pars:
|
|
205
|
+
self.r_in = pars["r_in"]
|
|
206
|
+
self.r_out = 9.0
|
|
207
|
+
if "r_out" in pars:
|
|
208
|
+
self.r_out = pars["r_out"]
|
|
209
|
+
self.sigmaGamma = 1.0
|
|
210
|
+
if "sigmaGamma" in pars:
|
|
211
|
+
self.sigmaGamma = pars["sigmaGamma"]
|
|
212
|
+
self.eps = 0.02
|
|
213
|
+
if "eps" in pars:
|
|
214
|
+
self.eps = pars["eps"]
|
|
215
|
+
self.m_trunc = 0
|
|
216
|
+
if "m_trunc" in pars:
|
|
217
|
+
self.m_trunc = pars["m_trunc"]
|
|
218
|
+
|
|
219
|
+
if self.tophat_in:
|
|
220
|
+
self.psfcube = np.copy(psfcube) # copy ensures the same reference behavior in both casees
|
|
221
|
+
else:
|
|
222
|
+
self.psfcube = SplitPSF.tophatfilter(psfcube, self.oversamp)
|
|
223
|
+
|
|
224
|
+
self.wcs_ = wcs_
|
|
225
|
+
|
|
226
|
+
# Get Legendre order
|
|
227
|
+
self.npoly = np.shape(psfcube)[0]
|
|
228
|
+
self.lorder = 0
|
|
229
|
+
while (self.lorder + 1) ** 2 < self.npoly:
|
|
230
|
+
self.lorder += 1
|
|
231
|
+
|
|
232
|
+
# Checks
|
|
233
|
+
if self.smallstamp_size % 2 != 0 or self.largestamp_size % 2 != 0:
|
|
234
|
+
raise ValueError("SplitPSF requires even dimension")
|
|
235
|
+
if (self.lorder + 1) ** 2 != self.npoly:
|
|
236
|
+
raise ValueError("SplitPSF Legendre polynomial dimension error")
|
|
237
|
+
|
|
238
|
+
def build(self):
|
|
239
|
+
"""Builds the short/long range decomposition for this SplitPSF."""
|
|
240
|
+
|
|
241
|
+
# Long/short range split
|
|
242
|
+
W = SplitPSF.Window_2D_integratedBlackman(
|
|
243
|
+
self.largestamp_size, self.oversamp * self.r_in, self.oversamp * self.r_out
|
|
244
|
+
)
|
|
245
|
+
ntrim = (self.largestamp_size - self.smallstamp_size) // 2
|
|
246
|
+
self.smallpsf = W[None, :, :] * self.psfcube
|
|
247
|
+
if ntrim > 0:
|
|
248
|
+
self.smallpsf = self.smallpsf[:, ntrim:-ntrim, ntrim:-ntrim]
|
|
249
|
+
resid = (
|
|
250
|
+
self.psfcube
|
|
251
|
+
* (1 - W)[None, :, :]
|
|
252
|
+
* SplitPSF.Truncate_2D_integratedBlackman(self.largestamp_size, self.m_trunc)[None, :, :]
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# select grid points for the conversion.
|
|
256
|
+
# we want int_{-1}^{+1} int_{-1}^{+1} dx dy f(x,y) approx sum_i w_i f(x_i,y_i)
|
|
257
|
+
# wg, xg, yg are numpy arrays of length self.npoly
|
|
258
|
+
(xLegendre, wLegendre) = roots_legendre(self.lorder + 1)
|
|
259
|
+
xg, yg = np.meshgrid(xLegendre, xLegendre)
|
|
260
|
+
xg = xg.flatten()
|
|
261
|
+
yg = yg.flatten()
|
|
262
|
+
wg = np.outer(wLegendre, wLegendre).flatten()
|
|
263
|
+
|
|
264
|
+
# The covariance matrix of the desired Gaussian Gamma (can be done outside the for loop)
|
|
265
|
+
var_ref = (self.oversamp * self.sigmaGamma) ** 2
|
|
266
|
+
|
|
267
|
+
# Now do the de-projection in each grid cell.
|
|
268
|
+
self.K_Legendre = np.zeros((self.npoly, self.largestamp_size, self.largestamp_size))
|
|
269
|
+
self.K_real = np.zeros((self.npoly, self.largestamp_size, self.largestamp_size))
|
|
270
|
+
self.zeta_real = np.zeros((self.npoly, self.largestamp_size, self.largestamp_size))
|
|
271
|
+
self.Cov = np.zeros((self.npoly, 2, 2))
|
|
272
|
+
for i in range(self.npoly):
|
|
273
|
+
if self.wcs_ is None:
|
|
274
|
+
self.Cov[i, :, :] = var_ref * np.identity(2)
|
|
275
|
+
else:
|
|
276
|
+
compute_point_pix = [self.nside / 2.0 * (1 + xg[i]), self.nside / 2.0 * (1 + yg[i])]
|
|
277
|
+
# globalpos = self.wcs_.all_pix2world(np.array([compute_point_pix]), 0)[0]
|
|
278
|
+
jac = local_partial_pixel_derivatives2(self.wcs_, *compute_point_pix)
|
|
279
|
+
self.Cov[i, :, :] = var_ref * np.linalg.inv(jac.T @ jac) * (self.ref_pixscale / 3600) ** 2
|
|
280
|
+
|
|
281
|
+
# get Legendre polynomial weights for this point (length self.npoly)
|
|
282
|
+
lpw = np.outer(
|
|
283
|
+
eval_legendre(range(self.lorder + 1), yg[i]), eval_legendre(range(self.lorder + 1), xg[i])
|
|
284
|
+
).flatten()
|
|
285
|
+
|
|
286
|
+
locLRP = np.einsum("a,aij->ij", lpw, resid)
|
|
287
|
+
self.K_real[i, :, :] = SplitPSF.gauss_deconv(locLRP, self.Cov[i, :, :], eps=self.eps)
|
|
288
|
+
self.zeta_real[i, :, :] = locLRP - scipy.signal.convolve(
|
|
289
|
+
self.K_real[i, :, :],
|
|
290
|
+
SplitPSF.gauss_stamp(self.largestamp_size, self.Cov[i, :, :]),
|
|
291
|
+
mode="same",
|
|
292
|
+
method="fft",
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Convert back to Legendre space --- do this with the current coefficients
|
|
296
|
+
self.K_Legendre += wg[i] * np.tensordot(lpw, self.K_real[i, :, :], axes=0)
|
|
297
|
+
|
|
298
|
+
# end for i
|
|
299
|
+
|
|
300
|
+
# normalize the Legendre coefficients, i.e. multiply by (2l_x+1)/2 * (2l_y+1)/2
|
|
301
|
+
l_ = np.array(range(self.lorder)) + 0.5
|
|
302
|
+
lnorm = np.outer(l_, l_).flatten()
|
|
303
|
+
self.K_Legendre = self.K_Legendre * lnorm[:, None, None]
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def split_psf_to_fits(psf_file, wcs_format, pars, outfile):
|
|
307
|
+
"""Computes split PSFs from an input PSF file.
|
|
308
|
+
|
|
309
|
+
Inputs:
|
|
310
|
+
psf_file = the PSF Legendre polynomial file as input (FITS file, primary and then 1 HDU per SCA)
|
|
311
|
+
wcs_format = WCS file format. should be able to generate a file with the SCA header in the path
|
|
312
|
+
wcs_format.format(sca) (sca = 1..18, inclusive)
|
|
313
|
+
missing files will have 'None' WCS (ignore distortion)
|
|
314
|
+
pars = PSF splitting parameters
|
|
315
|
+
outfile = output file for the PSF.
|
|
316
|
+
|
|
317
|
+
The format of the file written is as follows:
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
psf_hdulist = fits.open(psf_file)
|
|
321
|
+
|
|
322
|
+
# Generate the primary HDU
|
|
323
|
+
prim = fits.PrimaryHDU()
|
|
324
|
+
prim.header["FROMFILE"] = psf_file
|
|
325
|
+
for copykeys in ["CFORMAT", "PORDER", "ABSCISSA", "NCOEF", "SEQ", "OBSID", "NSCA", "OVSAMP", "SIMRUN"]:
|
|
326
|
+
if copykeys in psf_hdulist[0].header:
|
|
327
|
+
prim.header[copykeys] = psf_hdulist[0].header[copykeys]
|
|
328
|
+
prim.header.comments[copykeys] = psf_hdulist[0].header.comments[copykeys]
|
|
329
|
+
if "NSCA" in psf_hdulist[0].header:
|
|
330
|
+
nsca = int(psf_hdulist[0].header["NSCA"])
|
|
331
|
+
else:
|
|
332
|
+
nsca = len(psf_hdulist) - 1
|
|
333
|
+
prim.header["NSCA"] = (nsca, "from input file")
|
|
334
|
+
prim.header["GSSKIP"] = (nsca, "number of HDUs to skip for short range PSF")
|
|
335
|
+
prim.header["KERSKIP"] = (2 * nsca, "number of HDUs to skip for Kernel")
|
|
336
|
+
savezeta = False
|
|
337
|
+
if "SAVEZETA" in pars and pars["SAVEZETA"]:
|
|
338
|
+
prim.header["ZETASKIP"] = (3 * nsca, "number of HDUs to skip for zeta")
|
|
339
|
+
savezeta = True
|
|
340
|
+
prim.header["COMMENT"] = f"SplitPSF file. Original PSF in HDUs {1:d}..{nsca:d}"
|
|
341
|
+
prim.header["COMMENT"] = f"Short range PSF in HDUs {nsca + 1:d}..{2 * nsca:d}"
|
|
342
|
+
prim.header["COMMENT"] = f"Long-range kernel in HDUs {2 * nsca + 1:d}..{3 * nsca:d}"
|
|
343
|
+
|
|
344
|
+
# build the HDUs for each SCA
|
|
345
|
+
shortrangepsfs = []
|
|
346
|
+
kernels = []
|
|
347
|
+
zetas = []
|
|
348
|
+
zetamax = np.zeros((nsca,))
|
|
349
|
+
truewcs = np.zeros((nsca,), dtype=np.bool_)
|
|
350
|
+
Kint = np.zeros((nsca,))
|
|
351
|
+
K2int = np.zeros((nsca,))
|
|
352
|
+
for isca in range(1, nsca + 1):
|
|
353
|
+
try:
|
|
354
|
+
if wcs_format.format(isca)[-5:] == ".fits":
|
|
355
|
+
with fits.open(wcs_format.format(isca)) as f:
|
|
356
|
+
this_wcs_ = PyIMCOM_WCS(f["SCI"].header)
|
|
357
|
+
if wcs_format.format(isca)[-5:] == ".asdf":
|
|
358
|
+
with asdf.open(wcs_format.format(isca)) as f:
|
|
359
|
+
this_wcs_ = PyIMCOM_WCS(f["roman"]["meta"]["wcs"])
|
|
360
|
+
prim.header[f"INWCS{isca:02d}"] = wcs_format.format(isca)
|
|
361
|
+
except (RuntimeError, FileNotFoundError):
|
|
362
|
+
prim.header[f"INWCS{isca:02d}"] = "/dev/null"
|
|
363
|
+
this_wcs_ = None
|
|
364
|
+
|
|
365
|
+
sp = SplitPSF(psf_hdulist[isca].data.astype(np.float64), this_wcs_, pars)
|
|
366
|
+
sp.build()
|
|
367
|
+
|
|
368
|
+
# make the 'short range' image HDU
|
|
369
|
+
x = fits.ImageHDU(sp.smallpsf.astype(np.float32))
|
|
370
|
+
x.header["IMTYPE"] = "Short range PSF"
|
|
371
|
+
x.header["SCA"] = isca
|
|
372
|
+
shortrangepsfs += [x]
|
|
373
|
+
|
|
374
|
+
# make the 'kernel' HDU
|
|
375
|
+
y = fits.ImageHDU(sp.K_Legendre.astype(np.float32))
|
|
376
|
+
y.header["IMTYPE"] = "Kernel K"
|
|
377
|
+
y.header["SCA"] = isca
|
|
378
|
+
if this_wcs_ is None:
|
|
379
|
+
y.header["TRUEWCS"] = (False, "No WCS available, ignored distortion")
|
|
380
|
+
truewcs[isca - 1] = False
|
|
381
|
+
else:
|
|
382
|
+
y.header["TRUEWCS"] = (True, "Used WCS from file")
|
|
383
|
+
truewcs[isca - 1] = True
|
|
384
|
+
zetamax[isca - 1] = np.amax(np.abs(sp.zeta_real))
|
|
385
|
+
y.header["MAXZETA"] = (zetamax[isca - 1], "maximum error |zeta|")
|
|
386
|
+
Kint[isca - 1] = np.sum(sp.K_Legendre[0, :, :]) / sp.oversamp**2
|
|
387
|
+
K2int[isca - 1] = np.sum(sp.K_Legendre[0, :, :] ** 2) / sp.oversamp**2
|
|
388
|
+
y.header["KINT"] = (Kint[isca - 1], "integral of K kernel")
|
|
389
|
+
y.header["K2INT"] = (K2int[isca - 1], "integral of K^2 (native pix^-2)")
|
|
390
|
+
kernels += [y]
|
|
391
|
+
|
|
392
|
+
# the 'zeta' HDU (not currently written)
|
|
393
|
+
z = fits.ImageHDU(sp.zeta_real.astype(np.float32))
|
|
394
|
+
zetas += [z]
|
|
395
|
+
|
|
396
|
+
del sp
|
|
397
|
+
|
|
398
|
+
# report global worst zeta
|
|
399
|
+
prim.header["MAXZETA"] = np.amax(zetamax)
|
|
400
|
+
|
|
401
|
+
if savezeta:
|
|
402
|
+
prim.header["SAVEZETA"] = True
|
|
403
|
+
else:
|
|
404
|
+
prim.header["SAVEZETA"] = False
|
|
405
|
+
zetas = []
|
|
406
|
+
|
|
407
|
+
hdulist = fits.HDUList([prim] + psf_hdulist[1 : nsca + 1] + shortrangepsfs + kernels + zetas)
|
|
408
|
+
hdulist.writeto(outfile, overwrite=True)
|
|
409
|
+
|
|
410
|
+
psf_hdulist.close()
|
|
411
|
+
|
|
412
|
+
# tell the user which T/F values there were in the WCS
|
|
413
|
+
print("WCS:", truewcs)
|
|
414
|
+
print("zetamax:", zetamax)
|
|
415
|
+
print("Kint:", Kint)
|
|
416
|
+
print("K2int:", K2int)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def main(cfgfile):
|
|
420
|
+
"""Drives splitpsf from a configuration file."""
|
|
421
|
+
|
|
422
|
+
# Extract the information we need from the config file
|
|
423
|
+
with open(cfgfile) as f:
|
|
424
|
+
cfg_dict = json.load(f)
|
|
425
|
+
|
|
426
|
+
if "INLAYERCACHE" not in cfg_dict:
|
|
427
|
+
raise KeyError("Couldn't find INLAYERCACHE.")
|
|
428
|
+
|
|
429
|
+
# get target PSF properties
|
|
430
|
+
if cfg_dict["OUTPSF"] != "GAUSSIAN":
|
|
431
|
+
raise ValueError("SplitPSF currently only works for Gaussians.")
|
|
432
|
+
sigma = float(cfg_dict["EXTRASMOOTH"])
|
|
433
|
+
|
|
434
|
+
# get number of rows
|
|
435
|
+
with fits.open(cfg_dict["OBSFILE"]) as f:
|
|
436
|
+
Nobs = f[1].header["NAXIS2"]
|
|
437
|
+
filters_obs = f[1].data["filter"]
|
|
438
|
+
|
|
439
|
+
# extract oversampling factor
|
|
440
|
+
ovsamp = int(cfg_dict["INPSF"][2])
|
|
441
|
+
|
|
442
|
+
# extract PSF splitting parameters
|
|
443
|
+
r1 = float(cfg_dict["PSFSPLIT"][0])
|
|
444
|
+
r2 = float(cfg_dict["PSFSPLIT"][1])
|
|
445
|
+
epsilon = float(cfg_dict["PSFSPLIT"][2])
|
|
446
|
+
|
|
447
|
+
# decide on stamp size; multiple of 8, must include r2 radius
|
|
448
|
+
smallstampsize = int(np.ceil(r2 * ovsamp * 2 + 4))
|
|
449
|
+
smallstampsize += 8 - smallstampsize % 8
|
|
450
|
+
|
|
451
|
+
# where to put the files
|
|
452
|
+
targetdir = cfg_dict["INLAYERCACHE"] + ".psf"
|
|
453
|
+
try:
|
|
454
|
+
os.mkdir(targetdir)
|
|
455
|
+
print("made directory -->", targetdir)
|
|
456
|
+
except OSError as error:
|
|
457
|
+
print("Couldn't make directory", targetdir, ":", error)
|
|
458
|
+
|
|
459
|
+
use_filter = Settings.RomanFilters[int(cfg_dict["FILTER"])]
|
|
460
|
+
|
|
461
|
+
count = 0
|
|
462
|
+
for iobs in range(Nobs):
|
|
463
|
+
# different file name options depending on the simulation type
|
|
464
|
+
psf_file = cfg_dict["INPSF"][0] + "/" + InImage.psf_filename(cfg_dict["INPSF"][1], iobs)
|
|
465
|
+
sci_filename = _get_sca_imagefile(
|
|
466
|
+
cfg_dict["INDATA"][0], (iobs, -1), filters_obs[iobs], cfg_dict["INPSF"][1]
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
if os.path.exists(psf_file) and filters_obs[iobs] == use_filter:
|
|
470
|
+
# Need to transfer this file
|
|
471
|
+
outfile = targetdir + f"/psf_{iobs:d}.fits"
|
|
472
|
+
print(f"{iobs:8d}/{Nobs:8d} found, file is at " + psf_file, "-->", outfile)
|
|
473
|
+
split_psf_to_fits(
|
|
474
|
+
psf_file,
|
|
475
|
+
sci_filename,
|
|
476
|
+
{
|
|
477
|
+
"smallstamp_size": smallstampsize,
|
|
478
|
+
"sigmaGamma": sigma,
|
|
479
|
+
"r_in": r1,
|
|
480
|
+
"r_out": r2,
|
|
481
|
+
"eps": epsilon,
|
|
482
|
+
"SAVEZETA": False,
|
|
483
|
+
"oversamp": ovsamp,
|
|
484
|
+
},
|
|
485
|
+
outfile,
|
|
486
|
+
)
|
|
487
|
+
# <-- 'SAVEZETA': True is for diagnostics/figures only. The zeta HDUs are not actually needed for
|
|
488
|
+
# the calculation, and you might want to keep it off to save space.
|
|
489
|
+
|
|
490
|
+
sys.stdout.flush()
|
|
491
|
+
count = count + 1
|
|
492
|
+
# if count==1: exit() # <-- for testing: exit after one file
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
if __name__ == "__main__":
|
|
496
|
+
# Call with python3 -m pyimcom.splitpsf [config_file]
|
|
497
|
+
main(sys.argv[1])
|