pyimcom 1.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. pyimcom/__init__.py +1 -0
  2. pyimcom/_version.py +24 -0
  3. pyimcom/analysis.py +1480 -0
  4. pyimcom/coadd.py +2331 -0
  5. pyimcom/compress/__init__.py +0 -0
  6. pyimcom/compress/compressutils.py +506 -0
  7. pyimcom/compress/compressutils_wrapper.py +116 -0
  8. pyimcom/compress/i24.py +514 -0
  9. pyimcom/config.py +1245 -0
  10. pyimcom/diagnostics/__init__.py +0 -0
  11. pyimcom/diagnostics/context_figure.py +58 -0
  12. pyimcom/diagnostics/dynrange.py +274 -0
  13. pyimcom/diagnostics/layer_diagnostics.py +208 -0
  14. pyimcom/diagnostics/mosaicimage.py +80 -0
  15. pyimcom/diagnostics/noise/stability.py +126 -0
  16. pyimcom/diagnostics/noise_diagnostics.py +709 -0
  17. pyimcom/diagnostics/outimage_utils/__init__.py +0 -0
  18. pyimcom/diagnostics/outimage_utils/helper.py +82 -0
  19. pyimcom/diagnostics/report.py +366 -0
  20. pyimcom/diagnostics/run.py +64 -0
  21. pyimcom/diagnostics/starcube_nonoise.py +264 -0
  22. pyimcom/diagnostics/starcube_nonoise_coldescr.txt +24 -0
  23. pyimcom/diagnostics/stars.py +469 -0
  24. pyimcom/imdestripe.py +2454 -0
  25. pyimcom/lakernel.py +805 -0
  26. pyimcom/layer.py +1439 -0
  27. pyimcom/layer_wrapper.py +96 -0
  28. pyimcom/meta/__init__.py +0 -0
  29. pyimcom/meta/distortimage.py +748 -0
  30. pyimcom/meta/ginterp.py +340 -0
  31. pyimcom/pictures/__init__.py +0 -0
  32. pyimcom/pictures/genpic.py +229 -0
  33. pyimcom/psfutil.py +2199 -0
  34. pyimcom/routine.py +588 -0
  35. pyimcom/splitpsf/__init__.py +0 -0
  36. pyimcom/splitpsf/imsubtract.py +793 -0
  37. pyimcom/splitpsf/imsubtract_wrapper.py +107 -0
  38. pyimcom/splitpsf/splitpsf.py +497 -0
  39. pyimcom/splitpsf/splitpsf_wrapper.py +161 -0
  40. pyimcom/splitpsf/update_cube.py +136 -0
  41. pyimcom/truthcats.py +396 -0
  42. pyimcom/utils/__init__.py +0 -0
  43. pyimcom/utils/compareutils.py +207 -0
  44. pyimcom/utils/piffutils.py +223 -0
  45. pyimcom/wcsutil.py +839 -0
  46. pyimcom-1.2.1.dist-info/METADATA +67 -0
  47. pyimcom-1.2.1.dist-info/RECORD +52 -0
  48. pyimcom-1.2.1.dist-info/WHEEL +5 -0
  49. pyimcom-1.2.1.dist-info/licenses/LICENSE +21 -0
  50. pyimcom-1.2.1.dist-info/scm_file_list.json +285 -0
  51. pyimcom-1.2.1.dist-info/scm_version.json +8 -0
  52. pyimcom-1.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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])