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
|
File without changes
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Compression tools for PyIMCOM outputs.
|
|
3
|
+
|
|
4
|
+
Classes
|
|
5
|
+
-------
|
|
6
|
+
CompressedOutput
|
|
7
|
+
Main class for compresseing a FITS file.
|
|
8
|
+
|
|
9
|
+
Functions
|
|
10
|
+
---------
|
|
11
|
+
_parser
|
|
12
|
+
File name parser; only needed for file names with regular expressions.
|
|
13
|
+
ReadFile
|
|
14
|
+
Stand-alone function to read a compressed FITS file.
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
import re
|
|
18
|
+
from urllib.parse import urlparse
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
from astropy.io import fits
|
|
22
|
+
|
|
23
|
+
from ..config import Config
|
|
24
|
+
|
|
25
|
+
# specific compression tools we need
|
|
26
|
+
from .i24 import i24compress, i24decompress
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CompressedOutput:
|
|
30
|
+
"""Class for compressing pyimcom output files.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
fname : str
|
|
35
|
+
File name for uncompressed file.
|
|
36
|
+
format : str or None, optional
|
|
37
|
+
Compression format.
|
|
38
|
+
layers : list, optional
|
|
39
|
+
Which layers to de-compress (if given; otherwise de-compresses everything).
|
|
40
|
+
Use only for reading (don't write an instance initialized with `layers` to a file).
|
|
41
|
+
extraargs : dict, optional
|
|
42
|
+
Extra arguments for astropy.io.fits.
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
Attributes
|
|
46
|
+
----------
|
|
47
|
+
gzip : bool
|
|
48
|
+
Is gzipped?
|
|
49
|
+
ftype : str
|
|
50
|
+
File type. Currently the only option is 'fits'.
|
|
51
|
+
cprstype : str
|
|
52
|
+
Compression type (for a full file, not currently used).
|
|
53
|
+
origfile : str
|
|
54
|
+
Original file name (when opened)
|
|
55
|
+
hdul : astropy.io.fits.HDUList
|
|
56
|
+
HDU List of information (initially a deep copy of the input file).
|
|
57
|
+
cfg : pyimcom.config.Config
|
|
58
|
+
Configuration file used to generate this image.
|
|
59
|
+
|
|
60
|
+
Methods
|
|
61
|
+
-------
|
|
62
|
+
__init__
|
|
63
|
+
Constructor.
|
|
64
|
+
compress_2d_image
|
|
65
|
+
Wrapper for 2d image compression (staticmethod).
|
|
66
|
+
get_compression_dict
|
|
67
|
+
Extract compression scheme for a PyIMCOM layer as a dictionary.
|
|
68
|
+
decompress_2d_image
|
|
69
|
+
Wrapper for 2d image decompression (staticmethod).
|
|
70
|
+
compress_layer
|
|
71
|
+
Compresses a layer.
|
|
72
|
+
decompress
|
|
73
|
+
Decompresses the whole file.
|
|
74
|
+
recompress
|
|
75
|
+
Recompress previously compressed layers.
|
|
76
|
+
to_file
|
|
77
|
+
Saves to a file.
|
|
78
|
+
close
|
|
79
|
+
Close associated file.
|
|
80
|
+
|
|
81
|
+
Notes
|
|
82
|
+
-----
|
|
83
|
+
At some point, we may add file formats other than FITS, but not yet.
|
|
84
|
+
|
|
85
|
+
This object is big since it stores a deep copy of the whole file.
|
|
86
|
+
Therefore, it is not a good idea to open lots of compressed files at once.
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, fname, format=None, layers=None, extraargs={}):
|
|
91
|
+
self.origfile = fname
|
|
92
|
+
|
|
93
|
+
# figure out what type of file it is, and if it is gzipped
|
|
94
|
+
self.gzip = False
|
|
95
|
+
|
|
96
|
+
# determines which layers to decompress
|
|
97
|
+
self.decompress_layers = layers
|
|
98
|
+
|
|
99
|
+
if fname[-3:] == ".gz":
|
|
100
|
+
self.gzip = True
|
|
101
|
+
if format is None:
|
|
102
|
+
pref = fname[:-3] if self.gzip else fname
|
|
103
|
+
|
|
104
|
+
# right now supports fits files
|
|
105
|
+
if pref[-5:] == ".fits":
|
|
106
|
+
self.ftype = "fits"
|
|
107
|
+
self.hdul = fits.open(fname, mode="readonly", decompress_in_memory=True, **extraargs)
|
|
108
|
+
if "CPRSTYPE" in self.hdul[0].header:
|
|
109
|
+
self.cprstype = self.hdul[0].header["CPRSTYPE"]
|
|
110
|
+
else:
|
|
111
|
+
self.cprstype = ""
|
|
112
|
+
self.hdul[0].header["CPRSTYPE"] = ""
|
|
113
|
+
self.cfg = Config(fname, inmode="block")
|
|
114
|
+
|
|
115
|
+
else:
|
|
116
|
+
raise Exception("unrecognized file type")
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def compress_2d_image(im, scheme, pars):
|
|
120
|
+
"""Wrapper to compress a 2D image.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
im : np.array
|
|
125
|
+
2D image to be compressed.
|
|
126
|
+
scheme : str
|
|
127
|
+
Name of the compression scheme.
|
|
128
|
+
pars : dict
|
|
129
|
+
Parameters to be passed to the compression algorithm.
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
imout : np.array
|
|
134
|
+
The compressed 2D image, same shape as `im`.
|
|
135
|
+
ovflow : astropy.io.fits.BinTableHDU
|
|
136
|
+
A table of values that overflowed the quantization range for the
|
|
137
|
+
compression scheme. Returns None if not used.
|
|
138
|
+
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
if scheme[:3] == "I24":
|
|
142
|
+
imout, ovflow = i24compress(im, scheme, pars)
|
|
143
|
+
return imout, ovflow
|
|
144
|
+
|
|
145
|
+
# unrecognized scheme or NULL
|
|
146
|
+
return np.copy(im), None
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def get_compression_dict(cls, hdulist, ilayer):
|
|
150
|
+
"""Extract compression scheme for a PyIMCOM layer as a dictionary.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
hdulist : astropy.io.fits.HDUList
|
|
155
|
+
The FITS file as an HDUList.
|
|
156
|
+
ilayer : int
|
|
157
|
+
The layer number to extract.
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
dict
|
|
162
|
+
The compression scheme parameter dictionary. Note that all values are
|
|
163
|
+
returned as strings, the calling routine must typecast them as appropriate.
|
|
164
|
+
|
|
165
|
+
If the layer was not
|
|
166
|
+
compressed, returns an empty list, ``{}``.
|
|
167
|
+
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
# extract the compression data if that HDU is present
|
|
171
|
+
try:
|
|
172
|
+
cprs = [x.strip().split(":") for x in hdulist["CPRESS"].data["text"]]
|
|
173
|
+
except KeyError:
|
|
174
|
+
return {} # stop here for uncompressed files
|
|
175
|
+
|
|
176
|
+
cprs_thislayer = [item for item in cprs if int(item[0], 16) == ilayer]
|
|
177
|
+
return dict(
|
|
178
|
+
zip(
|
|
179
|
+
[item[1].strip() for item in cprs_thislayer],
|
|
180
|
+
[item[2].strip() for item in cprs_thislayer],
|
|
181
|
+
strict=False,
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def decompress_2d_image(im, scheme, pars, overflow=None):
|
|
187
|
+
"""Wrapper to decompress a 2D image; overflow table used for some formats.
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
im : np.array
|
|
192
|
+
The compressed image.
|
|
193
|
+
scheme : str
|
|
194
|
+
Name of the compression scheme.
|
|
195
|
+
pars : dict
|
|
196
|
+
Parameters to be passed to the compression algorithm.
|
|
197
|
+
overflow : astropy.io.fits.BinTableHDU or None, optional
|
|
198
|
+
(If used.) A table of values that overflowed the quantization range for the
|
|
199
|
+
compression scheme. Returns None if not used.
|
|
200
|
+
|
|
201
|
+
Returns
|
|
202
|
+
-------
|
|
203
|
+
imout : np.array
|
|
204
|
+
The de-compressed 2D image, same shape as `im`.
|
|
205
|
+
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
if scheme[:3] == "I24":
|
|
209
|
+
return i24decompress(im, scheme, pars, overflow=overflow)
|
|
210
|
+
|
|
211
|
+
# unrecognized scheme or NULL
|
|
212
|
+
return np.copy(im)
|
|
213
|
+
|
|
214
|
+
def compress_layer(self, layerid, scheme=None, pars={}):
|
|
215
|
+
"""Compresses the given layerid with the indicated scheme.
|
|
216
|
+
|
|
217
|
+
The pars is a dictionary of parameters that go with that scheme.
|
|
218
|
+
It must be in a format that supports a FITS header.
|
|
219
|
+
If scheme is None, then the algorithm will re-compress the layer in the same way was
|
|
220
|
+
done previously (if it was compressed before), otherwise it will do the NULL compression.
|
|
221
|
+
|
|
222
|
+
Parameters
|
|
223
|
+
----------
|
|
224
|
+
layerid : int
|
|
225
|
+
Number of the layer to be compressed.
|
|
226
|
+
scheme : str or None, optional
|
|
227
|
+
Name of the compression scheme. Defaults to None (no compression).
|
|
228
|
+
pars : dict, optional
|
|
229
|
+
Parameters to be passed to the compression algorithm.
|
|
230
|
+
|
|
231
|
+
Returns
|
|
232
|
+
-------
|
|
233
|
+
None
|
|
234
|
+
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
# refuse to compress the science layer
|
|
238
|
+
if layerid == 0:
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
# this failure to write the EXTNAME shouldn't happen, but just in case
|
|
242
|
+
if layerid >= 16**4:
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
# make a blank table if there isn't compression information yet
|
|
246
|
+
if "CPRESS" not in [hdu.name for hdu in self.hdul]:
|
|
247
|
+
hdu = fits.TableHDU.from_columns([fits.Column(name="text", format="A512", array=[], ascii=True)])
|
|
248
|
+
hdu.name = "CPRESS"
|
|
249
|
+
self.hdul.append(hdu)
|
|
250
|
+
# now get compression information
|
|
251
|
+
rows = self.hdul["CPRESS"].data["text"].tolist()
|
|
252
|
+
|
|
253
|
+
# None means we should check if this data has been compressed
|
|
254
|
+
# before and use that method.
|
|
255
|
+
if scheme is None:
|
|
256
|
+
compressiondict = {}
|
|
257
|
+
for kv in rows:
|
|
258
|
+
layer_, key_, val_ = kv.strip().split(":")
|
|
259
|
+
key_ = key_.strip()
|
|
260
|
+
val_ = val_.strip()
|
|
261
|
+
if int(layer_, 0x10) == layerid:
|
|
262
|
+
compressiondict[key_] = val_
|
|
263
|
+
if "SCHEME" in compressiondict:
|
|
264
|
+
# this was done before
|
|
265
|
+
# re-compress without adding new keywords
|
|
266
|
+
cd_data, cd_overflow = CompressedOutput.compress_2d_image(
|
|
267
|
+
self.hdul[0].data[0, layerid, :, :], compressiondict["SCHEME"], compressiondict
|
|
268
|
+
)
|
|
269
|
+
self.hdul[0].data[0, layerid, :, :] = 0
|
|
270
|
+
newhdu = fits.ImageHDU(cd_data)
|
|
271
|
+
for p in compressiondict:
|
|
272
|
+
newhdu.header[p] = compressiondict[p]
|
|
273
|
+
newhdu.name = f"HSHX{layerid:04X}"
|
|
274
|
+
self.hdul.append(newhdu)
|
|
275
|
+
cd_overflow.name = f"HSHV{layerid:04X}"
|
|
276
|
+
self.hdul.append(cd_overflow)
|
|
277
|
+
|
|
278
|
+
# print('re-compressed', compressiondict)
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
# we will do the null compression in this case
|
|
282
|
+
scheme = "NULL"
|
|
283
|
+
|
|
284
|
+
cd_data, cd_overflow = CompressedOutput.compress_2d_image(
|
|
285
|
+
self.hdul[0].data[0, layerid, :, :], scheme, pars
|
|
286
|
+
)
|
|
287
|
+
self.hdul[0].data[0, layerid, :, :] = 0
|
|
288
|
+
newhdu = fits.ImageHDU(cd_data)
|
|
289
|
+
for p in pars:
|
|
290
|
+
newhdu.header[p] = pars[p]
|
|
291
|
+
rows += [f"{layerid:04X}:{p:8s}:{str(pars[p]):s}"]
|
|
292
|
+
newhdu.header["SCHEME"] = scheme
|
|
293
|
+
rows += ["{:04X}:{:8s}:{:s}".format(layerid, "SCHEME", scheme)]
|
|
294
|
+
newhdu.name = f"HSHX{layerid:04X}"
|
|
295
|
+
self.hdul.append(newhdu)
|
|
296
|
+
cd_overflow.name = f"HSHV{layerid:04X}"
|
|
297
|
+
self.hdul.append(cd_overflow)
|
|
298
|
+
|
|
299
|
+
# which HDU has the compression data?
|
|
300
|
+
j_ = -1
|
|
301
|
+
for j in range(len(self.hdul)):
|
|
302
|
+
if self.hdul[j].name == "CPRESS":
|
|
303
|
+
j_ = j
|
|
304
|
+
if j_ == -1:
|
|
305
|
+
raise Exception("Can't find CPRESS: this shouldn't happen.")
|
|
306
|
+
# now overwrite that one
|
|
307
|
+
hdu = fits.TableHDU.from_columns([fits.Column(name="text", format="A512", array=rows, ascii=True)])
|
|
308
|
+
hdu.name = "CPRESS"
|
|
309
|
+
self.hdul[j_] = hdu
|
|
310
|
+
|
|
311
|
+
def decompress(self):
|
|
312
|
+
"""Decompresses all the layers that were compressed by compress_layer."""
|
|
313
|
+
|
|
314
|
+
j = 0
|
|
315
|
+
while j < len(self.hdul):
|
|
316
|
+
if self.hdul[j].name[:4] == "HSHX":
|
|
317
|
+
layer_target = int(self.hdul[j].name[-4:], 0x10) # this is a layer number
|
|
318
|
+
|
|
319
|
+
if (self.decompress_layers is not None) and (layer_target not in self.decompress_layers):
|
|
320
|
+
j = j + 1
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
self.hdul[0].data[0, layer_target, :, :] = CompressedOutput.decompress_2d_image(
|
|
324
|
+
self.hdul[j].data,
|
|
325
|
+
self.hdul[j].header["SCHEME"],
|
|
326
|
+
self.hdul[j].header,
|
|
327
|
+
overflow=self.hdul["HSHV" + self.hdul[j].name[-4:]],
|
|
328
|
+
)
|
|
329
|
+
del self.hdul[j]
|
|
330
|
+
else:
|
|
331
|
+
# we only increment j if the we didn't remove anything, since if we removed an HDU then
|
|
332
|
+
# what was hdul[j+1] is now hdul[j]
|
|
333
|
+
j = j + 1
|
|
334
|
+
|
|
335
|
+
# remove the overflow tables
|
|
336
|
+
j = 0
|
|
337
|
+
while j < len(self.hdul):
|
|
338
|
+
if self.hdul[j].name[:4] == "HSHV":
|
|
339
|
+
del self.hdul[j]
|
|
340
|
+
else:
|
|
341
|
+
j = j + 1
|
|
342
|
+
|
|
343
|
+
def recompress(self):
|
|
344
|
+
"""Recompresses all the layers that were previously compressed by compress_layer."""
|
|
345
|
+
|
|
346
|
+
# if this wasn't compressed before, nothing to do
|
|
347
|
+
if "CPRESS" not in [hdu.name for hdu in self.hdul]:
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
# figure out which layers were previously compressed
|
|
351
|
+
nlayer = np.shape(self.hdul[0].data)[-3]
|
|
352
|
+
wascompressed = np.zeros(nlayer, dtype=bool)
|
|
353
|
+
for compressnote in self.hdul["CPRESS"].data["text"].tolist():
|
|
354
|
+
ilayer = int(compressnote.split(":")[0], 16) # which layer this referred to
|
|
355
|
+
wascompressed[ilayer] = True
|
|
356
|
+
# print(wascompressed)
|
|
357
|
+
for ilayer in range(nlayer):
|
|
358
|
+
if wascompressed[ilayer]:
|
|
359
|
+
self.compress_layer(ilayer)
|
|
360
|
+
|
|
361
|
+
def to_file(self, fname, overwrite=False):
|
|
362
|
+
"""
|
|
363
|
+
Saves to a file.
|
|
364
|
+
|
|
365
|
+
Parameters
|
|
366
|
+
----------
|
|
367
|
+
fname : str
|
|
368
|
+
File name.
|
|
369
|
+
overwrite : bool, optional
|
|
370
|
+
Whether to overwrite the file.
|
|
371
|
+
|
|
372
|
+
Returns
|
|
373
|
+
-------
|
|
374
|
+
None
|
|
375
|
+
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
self.hdul.writeto(fname, overwrite=overwrite)
|
|
379
|
+
|
|
380
|
+
def close(self):
|
|
381
|
+
"""Closes the associated file."""
|
|
382
|
+
self.hdul.close()
|
|
383
|
+
|
|
384
|
+
def __enter__(self):
|
|
385
|
+
"""Entry method."""
|
|
386
|
+
return self
|
|
387
|
+
|
|
388
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
389
|
+
"""Has context manager automatically close."""
|
|
390
|
+
self.close()
|
|
391
|
+
return False # do not suppress exception
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _parser(fname):
|
|
395
|
+
"""
|
|
396
|
+
Re-formats a file name containing a regular expression.
|
|
397
|
+
|
|
398
|
+
Regular expressions are separated with the ^ character and contain a row and column index,
|
|
399
|
+
followed by the suffix. For example::
|
|
400
|
+
|
|
401
|
+
>>> _parser('hello_world/Q_02_31.fits') # no ^, regular file name
|
|
402
|
+
'hello_world/Q_02_31.fits'
|
|
403
|
+
>>> _parser('hello_world/Row{1:2d}/Q_{0:02d}_{1:02d}^_02_31.fits') # FITS file
|
|
404
|
+
'hello_world/Row31/Q_02_31.fits'
|
|
405
|
+
>>> _parser('hello_world/Row{1:2d}/Q_{0:02d}_{1:02d}^_02_12.fits.gz') # gzipped; suffix is copied over
|
|
406
|
+
'hello_world/Row12/Q_02_12.fits.gz'
|
|
407
|
+
|
|
408
|
+
This is useful if the files are not all in the same directory.
|
|
409
|
+
|
|
410
|
+
Parameters
|
|
411
|
+
----------
|
|
412
|
+
fname : str or str-like
|
|
413
|
+
Regular file name (not including ^) or regular expression.
|
|
414
|
+
|
|
415
|
+
Returns
|
|
416
|
+
-------
|
|
417
|
+
str
|
|
418
|
+
The formatted file name.
|
|
419
|
+
|
|
420
|
+
"""
|
|
421
|
+
|
|
422
|
+
fname = str(fname) # converts other objects, e.g., pathlib.Path, to a string
|
|
423
|
+
|
|
424
|
+
# normal file name: nothing to be done
|
|
425
|
+
if "^" not in fname:
|
|
426
|
+
return fname
|
|
427
|
+
|
|
428
|
+
# pattern match
|
|
429
|
+
parts = fname.split("^")
|
|
430
|
+
sub = parts[1].split(".")
|
|
431
|
+
coordstring = sub[0]
|
|
432
|
+
m = re.match(r"_(\d+)_(\d+)(\D*)", coordstring)
|
|
433
|
+
if m is not None:
|
|
434
|
+
ix = int(m.group(1))
|
|
435
|
+
iy = int(m.group(2))
|
|
436
|
+
term = m.group(3)
|
|
437
|
+
suffix = term + "." + ".".join(sub[1:])
|
|
438
|
+
outname = "^".join(parts[:-1])
|
|
439
|
+
outname = outname.format(ix, iy) + suffix
|
|
440
|
+
return outname
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def ReadFile(fname, layers=None):
|
|
444
|
+
"""Wrapper to read a compressed file.
|
|
445
|
+
|
|
446
|
+
This should read a file just like astropy.io.fits.open(fname, mode='readonly'),
|
|
447
|
+
but works even if the file is compressed using compressutils.
|
|
448
|
+
|
|
449
|
+
Parameters
|
|
450
|
+
----------
|
|
451
|
+
fname : str or str-like
|
|
452
|
+
File name to read.
|
|
453
|
+
layers : list, optional
|
|
454
|
+
Which layers to de-compress (if given; otherwise de-compresses everything).
|
|
455
|
+
Use only for reading (don't write an instance initialized with `layers` to a file).
|
|
456
|
+
|
|
457
|
+
Notes
|
|
458
|
+
-----
|
|
459
|
+
This can also be used with the Python context manager, e.g.::
|
|
460
|
+
|
|
461
|
+
with ReadFile('my.fits.gz') as f:
|
|
462
|
+
...
|
|
463
|
+
|
|
464
|
+
File names with sepcific types of regular expressions are allowed, and unpacked by the ``_parser``
|
|
465
|
+
function. Regular expressions are separated with the ^ character and contain a row and column index,
|
|
466
|
+
followed by the suffix. For example::
|
|
467
|
+
|
|
468
|
+
>>> _parser('hello_world/Q_02_31.fits') # no ^, regular file name
|
|
469
|
+
'hello_world/Q_02_31.fits'
|
|
470
|
+
>>> _parser('hello_world/Row{1:2d}/Q_{0:02d}_{1:02d}^_02_31.fits') # FITS file
|
|
471
|
+
'hello_world/Row31/Q_02_31.fits'
|
|
472
|
+
>>> _parser('hello_world/Row{1:2d}/Q_{0:02d}_{1:02d}^_02_12.fits.gz') # gzipped; suffix is copied over
|
|
473
|
+
'hello_world/Row12/Q_02_12.fits.gz'
|
|
474
|
+
|
|
475
|
+
"""
|
|
476
|
+
|
|
477
|
+
fname = _parser(fname) # if the file name is a regular expression.
|
|
478
|
+
|
|
479
|
+
_o = urlparse(fname)
|
|
480
|
+
|
|
481
|
+
extraargs = {}
|
|
482
|
+
match _o.scheme:
|
|
483
|
+
case "" | "file":
|
|
484
|
+
pass
|
|
485
|
+
case "http" | "https":
|
|
486
|
+
extraargs["use_fsspec"] = True
|
|
487
|
+
case "s3":
|
|
488
|
+
extraargs["use_fsspec"] = True
|
|
489
|
+
extraargs["fsspec_kwargs"] = {"anon": True}
|
|
490
|
+
# extraargs["fsspec_kwargs"] = {"key": "YOUR-SECRET-KEY-ID", "secret": "YOUR-SECRET-KEY"}
|
|
491
|
+
case _:
|
|
492
|
+
raise ValueError(f"Scheme {_o.scheme} not supported")
|
|
493
|
+
|
|
494
|
+
_url = _o.geturl()
|
|
495
|
+
|
|
496
|
+
f = fits.open(_url, **extraargs)
|
|
497
|
+
|
|
498
|
+
if "CPRESS" not in [hdu.name for hdu in f]:
|
|
499
|
+
return f
|
|
500
|
+
else:
|
|
501
|
+
f.close()
|
|
502
|
+
|
|
503
|
+
# otherwise, make a decompressed version
|
|
504
|
+
x = CompressedOutput(fname, layers=layers, extraargs=extraargs)
|
|
505
|
+
x.decompress()
|
|
506
|
+
return fits.HDUList(x.hdul)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import multiprocessing as mp
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
6
|
+
|
|
7
|
+
from .compressutils import CompressedOutput
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def compress_one_block(cfg, layer_pars_dic, ibx, iby):
|
|
11
|
+
"""Helper function to compress one block of the IMCOM output.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
cfg : pyimcom.config.Config
|
|
16
|
+
The IMCOM configuration.
|
|
17
|
+
layer_pars_dic : dict
|
|
18
|
+
A dictionary of layer parameters, where the keys are the layer names and the values are
|
|
19
|
+
dictionaries of parameters for that layer.
|
|
20
|
+
ibx : int
|
|
21
|
+
Block x index.
|
|
22
|
+
iby : int
|
|
23
|
+
Block y index.
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
fout : str
|
|
28
|
+
The file name of the compressed output file for this block.
|
|
29
|
+
(None if the file was missing.)
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# Get the input file
|
|
34
|
+
fname = cfg.outstem + f"_{ibx:02d}_{iby:02d}.fits"
|
|
35
|
+
if not os.path.exists(fname):
|
|
36
|
+
return None
|
|
37
|
+
# ... and the output file name
|
|
38
|
+
fout = cfg.outstem + f"_{ibx:02d}_{iby:02d}.cpr.fits.gz"
|
|
39
|
+
print(fname, "-->", fout)
|
|
40
|
+
sys.stdout.flush()
|
|
41
|
+
|
|
42
|
+
with CompressedOutput(fname) as f:
|
|
43
|
+
# Get types of each layer (e.g., 'gsstar', 'cstar', etc.)
|
|
44
|
+
# These match the keys in config.yaml and will set the pars
|
|
45
|
+
# The science layer is special because its type is None.
|
|
46
|
+
layers_types = [""] + [re.sub(r"\d+$", "", item.split(",")[0]) for item in f.cfg.extrainput[1:]]
|
|
47
|
+
|
|
48
|
+
# Loop over layers, check if each is to be compressed
|
|
49
|
+
for j in range(1, len(f.cfg.extrainput)):
|
|
50
|
+
if layers_types[j] in layer_pars_dic:
|
|
51
|
+
pardict = layer_pars_dic[layers_types[j]]
|
|
52
|
+
f.compress_layer(j, scheme=pardict.get("SCHEME", "I24B"), pars=pardict)
|
|
53
|
+
|
|
54
|
+
f.to_file(fout, overwrite=True)
|
|
55
|
+
|
|
56
|
+
return fout
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def compress_all_blocks(cfg, layer_pars_dic, workers, require_all=False):
|
|
60
|
+
"""
|
|
61
|
+
Helper function to compress all blocks of the IMCOM output in parallel.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
cfg : pyimcom.config.Config
|
|
66
|
+
The IMCOM configuration.
|
|
67
|
+
layer_pars_dic : dict
|
|
68
|
+
A dictionary of layer parameters, where the keys are the layer names and the values are
|
|
69
|
+
dictionaries of parameters for that layer.
|
|
70
|
+
workers : int
|
|
71
|
+
The number of parallel workers to use for compression.
|
|
72
|
+
require_all : boolean, optional
|
|
73
|
+
If True, raises an exception for missing files (default of False just moves on).
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
None
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
nblock = cfg.nblock
|
|
81
|
+
nblock2 = cfg.nblock**2
|
|
82
|
+
|
|
83
|
+
start_method = "forkserver" if os.name.lower() == "posix" else "spawn"
|
|
84
|
+
ctx = mp.get_context(start_method)
|
|
85
|
+
nfail = 0 # number of failures
|
|
86
|
+
nmissing = 0 # number of missing files
|
|
87
|
+
|
|
88
|
+
with ProcessPoolExecutor(max_workers=workers, mp_context=ctx) as executor:
|
|
89
|
+
futures = []
|
|
90
|
+
for i in range(nblock2):
|
|
91
|
+
ibx = i % nblock
|
|
92
|
+
iby = i // nblock
|
|
93
|
+
futures.append(executor.submit(compress_one_block, cfg, layer_pars_dic, ibx, iby))
|
|
94
|
+
|
|
95
|
+
for future in as_completed(futures):
|
|
96
|
+
# Check for existence of output file to confirm completion
|
|
97
|
+
try:
|
|
98
|
+
fout = future.result()
|
|
99
|
+
if fout is None:
|
|
100
|
+
nmissing += 1
|
|
101
|
+
else:
|
|
102
|
+
if not os.path.exists(fout):
|
|
103
|
+
print(f"Error: {fout} was not created.")
|
|
104
|
+
nfail += 1
|
|
105
|
+
except Exception as e:
|
|
106
|
+
nfail += 1
|
|
107
|
+
print(f"Worker failed with exception {e}", flush=True)
|
|
108
|
+
|
|
109
|
+
if nfail > 0:
|
|
110
|
+
raise Exception(f"{nfail:d} instances of compress_one_block failed.")
|
|
111
|
+
|
|
112
|
+
if nmissing > 0:
|
|
113
|
+
if require_all:
|
|
114
|
+
raise Exception(f"{nmissing:d} instances of compress_one_block missing.")
|
|
115
|
+
else:
|
|
116
|
+
print(f"{nmissing:d} blocks missing.")
|