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
pyimcom/config.py ADDED
@@ -0,0 +1,1245 @@
1
+ """
2
+ Encapsulation of pyimcom background settings and configuration.
3
+
4
+ Classes
5
+ -------
6
+ Timer
7
+ All-purpose timer.
8
+ Settings
9
+ pyimcom background settings.
10
+ fpaCoords
11
+ some basic data on the Roman FPA coordinates, needed for some of the tests.
12
+ Config
13
+ pyimcom configuration, with JSON file interface.
14
+
15
+ Functions
16
+ ---------
17
+ format_axis
18
+ Format a panel (an axis) of a figure.
19
+
20
+ """
21
+
22
+ import json
23
+ import os
24
+ from importlib.resources import files
25
+ from time import perf_counter
26
+
27
+ import numpy as np
28
+ from astropy import units as u
29
+ from astropy.io import fits
30
+
31
+ JWST = os.environ.get("INSTRUMENT", "WFI") == "NIRCAM"
32
+ # This will be True if the environment variable INSTRUMENT is set to "NIRCAM", and False otherwise.
33
+
34
+
35
+ class Timer:
36
+ """
37
+ All-purpose timer.
38
+
39
+ Methods
40
+ -------
41
+ __init__
42
+ Constructor.
43
+ __call__
44
+ Return the time elapsed since tstart in seconds.
45
+
46
+ """
47
+
48
+ def __init__(self) -> None:
49
+ self.tstart = perf_counter()
50
+
51
+ def __call__(self, reset: bool = False) -> float:
52
+ """
53
+ Return the time elapsed since tstart in seconds.
54
+
55
+ Parameters
56
+ ----------
57
+ reset : bool, optional
58
+ Whether to reset tstart.
59
+
60
+ Returns
61
+ -------
62
+ float
63
+ Time elapsed since tstart in seconds.
64
+
65
+ """
66
+
67
+ tnow = perf_counter()
68
+ tstart = self.tstart
69
+ if reset:
70
+ self.tstart = tnow
71
+ return tnow - tstart
72
+
73
+
74
+ class Settings:
75
+ """
76
+ pyimcom background settings.
77
+
78
+ This class contains assorted Roman WFI data needed for the coadd code.
79
+
80
+ """
81
+
82
+ # which header in the input file contains the WCS information
83
+ hdu_with_wcs = "SCI"
84
+
85
+ degree = u.degree.to("rad") # == np.pi/180.0 == 0.017453292519943295
86
+ arcmin = u.arcmin.to("rad") # == degree/60.0 == 0.0002908882086657216
87
+ arcsec = u.arcsec.to("rad") # == arcmin/60.0 == 4.84813681109536e-06
88
+
89
+ # filter list
90
+ RomanFilters = ["W146", "F184", "H158", "J129", "Y106", "Z087", "R062", "PRSM", "DARK", "GRSM", "K213"]
91
+ QFilterNative = [1.155, 1.456, 1.250, 1.021, 0.834, 0.689, 0.491, 1.009, 0.000, 1.159, 1.685]
92
+
93
+ # linear obscuration of the telescope
94
+ obsc = 0.31
95
+
96
+ # SCA parameters
97
+ pixscale_native = 0.11 * arcsec
98
+ sca_nside = 4088 # excludes reference pixels
99
+ sca_ctrpix = (sca_nside - 1) / 2
100
+ sca_sidelength = sca_nside * pixscale_native
101
+
102
+ # SCA field of view centers
103
+ # SCAFov[i,j] = position of SCA #[i+1] (i=0..17) in j coordinate (j=0 for X, j=1 for Y)
104
+ # these are in 'WFI local' field angles, in degrees
105
+ # just for checking coverage since only 3 decimal places
106
+ SCAFov = np.asarray(
107
+ [
108
+ [-0.071, -0.037],
109
+ [-0.071, 0.109],
110
+ [-0.070, 0.240],
111
+ [-0.206, -0.064],
112
+ [-0.206, 0.083],
113
+ [-0.206, 0.213],
114
+ [-0.341, -0.129],
115
+ [-0.341, 0.018],
116
+ [-0.342, 0.147],
117
+ [0.071, -0.037],
118
+ [0.071, 0.109],
119
+ [0.070, 0.240],
120
+ [0.206, -0.064],
121
+ [0.206, 0.083],
122
+ [0.206, 0.213],
123
+ [0.341, -0.129],
124
+ [0.341, 0.018],
125
+ [0.342, 0.147],
126
+ ]
127
+ )
128
+
129
+ @classmethod
130
+ def jwst(cls):
131
+ """
132
+ Method to modify the Settings object to have JWST NIRCam parameters
133
+ instead of Roman WFI parameters.
134
+
135
+ Note: Currently only includes those attributes that are used in imdestripe.py,
136
+ but more can be added as needed.
137
+
138
+ """
139
+
140
+ cls.sca_nside = 2048
141
+ nircam_short_bands = [
142
+ "F070W",
143
+ "F090W",
144
+ "F115W",
145
+ "F140M",
146
+ "F150W",
147
+ "F150W2",
148
+ "F162M",
149
+ "F164N",
150
+ "F182M",
151
+ "F187N",
152
+ "F200W",
153
+ "F210M",
154
+ "F212N",
155
+ ]
156
+ nircam_long_bands = [
157
+ "F250M",
158
+ "F277W",
159
+ "F300M",
160
+ "F322W2",
161
+ "F323N",
162
+ "F335M",
163
+ "F356W",
164
+ "F360M",
165
+ "F405N",
166
+ "F410M",
167
+ "F430M",
168
+ "F444W",
169
+ "F460M",
170
+ "F466N",
171
+ "F470N",
172
+ "F480M",
173
+ ]
174
+ # KL Leaving "Roman" so we don't have to change as much in the code
175
+ cls.RomanFilters = nircam_short_bands + nircam_long_bands
176
+ cls.pixscale_short_native = 0.031 * cls.arcsec
177
+ cls.pixscale_long_native = 0.062 * cls.arcsec
178
+
179
+
180
+ class fpaCoords:
181
+ """This contains some static data on the FPA coordinate system.
182
+
183
+ It also has some associated static methods.
184
+ """
185
+
186
+ # focal plane coordinates of SCA centers, in mm
187
+ xfpa = np.array(
188
+ [
189
+ -22.14,
190
+ -22.29,
191
+ -22.44,
192
+ -66.42,
193
+ -66.92,
194
+ -67.42,
195
+ -110.70,
196
+ -111.48,
197
+ -112.64,
198
+ 22.14,
199
+ 22.29,
200
+ 22.44,
201
+ 66.42,
202
+ 66.92,
203
+ 67.42,
204
+ 110.70,
205
+ 111.48,
206
+ 112.64,
207
+ ]
208
+ )
209
+ yfpa = np.array(
210
+ [
211
+ 12.15,
212
+ -37.03,
213
+ -82.06,
214
+ 20.90,
215
+ -28.28,
216
+ -73.06,
217
+ 42.20,
218
+ -6.98,
219
+ -51.06,
220
+ 12.15,
221
+ -37.03,
222
+ -82.06,
223
+ 20.90,
224
+ -28.28,
225
+ -73.06,
226
+ 42.20,
227
+ -6.98,
228
+ -51.06,
229
+ ]
230
+ )
231
+ # radius to circumscribe FPAs
232
+ Rfpa = 151.07129575137697
233
+
234
+ # orientation of SCAs
235
+ # -1 orient means SCA +x pointed along FPA -x, SCA +y pointed along FPA -y
236
+ # +1 orient means SCA +x pointed along FPA +x, SCA +y pointed along FPA +y (SCA #3,6,9,12,15,18)
237
+ sca_orient = np.array([-1, -1, 1, -1, -1, 1, -1, -1, 1, -1, -1, 1, -1, -1, 1, -1, -1, 1]).astype(np.int16)
238
+
239
+ pixsize = 0.01 # in mm
240
+ nside = 4088 # number of active pixels on a side
241
+
242
+ @classmethod
243
+ def pix2fpa(cls, sca, x, y):
244
+ """Method to convert pixel (x,y) on a given sca to focal plane coordinates.
245
+
246
+ Inputs:
247
+ sca (in form 1..18)
248
+ x and y (in pixels)
249
+ sca, x, y may be scalars or arrays
250
+
251
+ Outputs:
252
+ xfpa, yfpa (in mm)
253
+ """
254
+
255
+ if np.amin(sca) < 1 or np.amax(sca) > 18:
256
+ raise ValueError(f"Invalid SCA in fpadata.pix2fpa, range={np.amin(sca):d},{np.amax(sca):d}")
257
+
258
+ return (
259
+ cls.xfpa[sca - 1] + cls.pixsize * (x - (cls.nside - 1) / 2.0) * cls.sca_orient[sca - 1],
260
+ cls.yfpa[sca - 1] + cls.pixsize * (y - (cls.nside - 1) / 2.0) * cls.sca_orient[sca - 1],
261
+ )
262
+
263
+
264
+ class Config:
265
+ """
266
+ pyimcom configuration, with JSON file interface.
267
+
268
+ Parameters
269
+ ----------
270
+ cfg_file : str or None, optional
271
+ File path to or text content of a JSON configuration file.
272
+ The default is ''. This uses pyimcom/configs/default_config.json.
273
+ Set cfg_file=None to build a configuration from scratch.
274
+ inmode : str or None, optional
275
+ Directives for special behavior. Right now the only one supported
276
+ is 'block' (meaning the configuration is read from a block output).
277
+
278
+ Methods
279
+ -------
280
+ __init__
281
+ Constructor.
282
+ __call__
283
+ Calculate or update derived quantities.
284
+ _from_dict
285
+ Build a configuration from a dictionary.
286
+ _get_attrs_wrapper
287
+ Wrapper for getting an attribute or a set of attributes.
288
+ _build_config
289
+ Terminal interface to build a configuration from scratch.
290
+ to_file
291
+ Save the configuration to a JSON file.
292
+ to_dict
293
+ Convert to a dictionary.
294
+
295
+ """
296
+
297
+ __slots__ = (
298
+ "cfg_file",
299
+ "NsideP",
300
+ "n1P",
301
+ "n2f", # __init__, __call__
302
+ "obsfile",
303
+ "inpath",
304
+ "informat",
305
+ "use_filter",
306
+ "inpsf_path",
307
+ "inpsf_format",
308
+ "inpsf_oversamp",
309
+ "psfsplit",
310
+ "psfsplit_r1",
311
+ "psfsplit_r2",
312
+ "psfsplit_epsilon", # SECTION I
313
+ "permanent_mask",
314
+ "cr_mask_rate",
315
+ "extrainput",
316
+ "n_inframe",
317
+ "labnoisethreshold", # SECTION II
318
+ "ra",
319
+ "dec",
320
+ "lonpole",
321
+ "nblock",
322
+ "n1",
323
+ "n2",
324
+ "dtheta",
325
+ "Nside", # SECTION III
326
+ "fade_kernel",
327
+ "postage_pad",
328
+ "pad_sides",
329
+ "stoptile", # SECTION IV
330
+ "outmaps",
331
+ "outstem",
332
+ "tempfile",
333
+ "inlayercache", # SECTION V
334
+ "n_out",
335
+ "outpsf",
336
+ "sigmatarget",
337
+ "outpsf_extra",
338
+ "sigmatarget_extra", # SECTION VI
339
+ "npixpsf",
340
+ "psf_circ",
341
+ "psf_norm",
342
+ "amp_penalty",
343
+ "flat_penalty",
344
+ "psf_interp",
345
+ "instamp_pad", # SECTION VII
346
+ "linear_algebra",
347
+ "iter_rtol",
348
+ "iter_max",
349
+ "no_qlt_ctrl",
350
+ "kappaC_arr",
351
+ "uctarget",
352
+ "sigmamax", # SECTION VIII
353
+ "ds_model",
354
+ "ds_rows",
355
+ "ds_outpath",
356
+ "ds_outstem",
357
+ "cg_model",
358
+ "cost_model",
359
+ "ds_obsfile",
360
+ "ds_noisefile",
361
+ "ds_restart",
362
+ "cost_prior",
363
+ "resid_model",
364
+ "hub_thresh",
365
+ "cg_maxiter",
366
+ "cg_tol",
367
+ "gaindir", # SECTION IX
368
+ "col_pars",
369
+ "amp_cols",
370
+ "col_boundary_const",
371
+ "tileschm",
372
+ "rerun",
373
+ "mosaic", # SECTION X
374
+ )
375
+
376
+ def __init__(self, cfg_file: str = "", inmode=None) -> None:
377
+ # option to load from a block output file
378
+ if inmode == "block":
379
+ with fits.open(cfg_file) as f:
380
+ c = f["CONFIG"].data["text"]
381
+ n = len(c)
382
+ cf = ""
383
+ for j in range(n):
384
+ cf += c[j] + "\n"
385
+ self._from_dict(json.loads(cf))
386
+ self()
387
+ return
388
+
389
+ self.cfg_file = cfg_file
390
+ if cfg_file is not None:
391
+ if cfg_file == "":
392
+ # print('> Using pyimcom/configs/default_config.json', flush=True)
393
+ self.cfg_file = files(__package__).joinpath("configs/default_config.json")
394
+
395
+ try:
396
+ with open(self.cfg_file) as f:
397
+ cfg_dict = json.load(f)
398
+ except (OSError, FileNotFoundError):
399
+ cfg_dict = json.loads(self.cfg_file)
400
+ self._from_dict(cfg_dict)
401
+
402
+ else:
403
+ self._build_config()
404
+
405
+ self()
406
+
407
+ def __call__(self) -> None:
408
+ """Calculate or update derived quantities."""
409
+
410
+ ### SECTION I: INPUT FILES ###
411
+ if self.psfsplit:
412
+ self.psfsplit_r1 = float(self.psfsplit[0])
413
+ self.psfsplit_r2 = float(self.psfsplit[1])
414
+ self.psfsplit_epsilon = float(self.psfsplit[2])
415
+
416
+ ### SECTION II: MASKS AND LAYERS ###
417
+ self.n_inframe = len(self.extrainput)
418
+
419
+ ### SECTION III: WHAT AREA TO COADD ###
420
+ self.Nside = self.n1 * self.n2
421
+ self.NsideP = self.Nside + self.postage_pad * self.n2 * 2
422
+ self.n1P = self.n1 + self.postage_pad * 2
423
+ self.n2f = self.n2 + self.fade_kernel * 2
424
+
425
+ ### SECTION VIII: SOLVING LINEAR SYSTEMS ###
426
+ if self.linear_algebra == "Empirical":
427
+ self.outmaps = self.outmaps.replace("T", "")
428
+ if self.no_qlt_ctrl:
429
+ self.outmaps = self.outmaps.replace("U", "").replace("S", "")
430
+ elif "U" not in self.outmaps and "S" not in self.outmaps:
431
+ self.no_qlt_ctrl = True
432
+
433
+ if self.linear_algebra == "Empirical" or self.kappaC_arr.size == 1:
434
+ self.outmaps = self.outmaps.replace("K", "")
435
+
436
+ ### SECTION IX: DESTRIPING PARAMS ###
437
+ if hasattr(self, "cost_model"):
438
+ if self.cost_model == "quadratic":
439
+ self.resid_model = "quad_prime"
440
+ elif self.cost_model == "absolute":
441
+ self.resid_model = "abs_prime"
442
+ elif self.cost_model == "huber_loss":
443
+ self.resid_model = "hub_prime"
444
+
445
+ def _from_dict(self, cfg_dict: dict) -> None:
446
+ """
447
+ Build a configuration from a dictionary.
448
+
449
+ Parameters
450
+ ----------
451
+ cfg_dict : dict
452
+ This is a dictionary, usually built from a JSON file.
453
+
454
+ Returns
455
+ -------
456
+ None
457
+
458
+ """
459
+
460
+ ### SECTION I: INPUT FILES ###
461
+ # input files
462
+ self.obsfile = cfg_dict["OBSFILE"]
463
+ self.inpath, self.informat = cfg_dict["INDATA"]
464
+ # which filter to make coadd
465
+ self.use_filter = cfg_dict["FILTER"]
466
+ # input PSF information
467
+ self.inpsf_path, self.inpsf_format, self.inpsf_oversamp = cfg_dict["INPSF"]
468
+ # if PSF splitting is used
469
+ self.psfsplit = cfg_dict.get("PSFSPLIT", "")
470
+
471
+ ### SECTION II: MASKS AND LAYERS ###
472
+ # permanent mask file
473
+ self.permanent_mask = cfg_dict.get("PMASK")
474
+ # CR hit probability for stochastic mask
475
+ self.cr_mask_rate = cfg_dict.get("CMASK", 0.0)
476
+ # input images to stack at once
477
+ self.extrainput = [None] + cfg_dict.get("EXTRAINPUT", [])
478
+ # threshold for masking lab noise data
479
+ self.labnoisethreshold = cfg_dict.get("LABNOISETHRESHOLD", 3.0)
480
+
481
+ ### SECTION III: WHAT AREA TO COADD ###
482
+ # tile center in degrees RA, DEC
483
+ self.ra, self.dec = cfg_dict["CTR"]
484
+ self.lonpole = float(cfg_dict.get("LONPOLE", 180.0))
485
+ # if we are doing a nblock x nblock array on the same projection
486
+ self.nblock = cfg_dict["BLOCK"]
487
+ # and output size: n1 (number of IMCOM postage stamps)
488
+ # n2 (size of single run), dtheta (arcsec)
489
+ # output array size will be (n1 x n2 x dtheta) on a side
490
+ # with padding, it is (n1 + 2*postage_pad) n2 x dtheta on a side
491
+ self.n1, self.n2, self.dtheta = cfg_dict["OUTSIZE"]
492
+ assert self.n1 % 2 == 0, "Error: n1 must be even since PSF computations are in 2x2 groups"
493
+ self.dtheta *= u.arcsec.to("degree")
494
+
495
+ ### SECTION IV: MORE ABOUT POSTAGE STAMPS ###
496
+ # fading kernel width
497
+ self.fade_kernel = cfg_dict.get("FADE", 3)
498
+ # pad this many IMCOM postage stamps around the edge
499
+ self.postage_pad = cfg_dict.get("PAD", 0)
500
+ # according to the strategy or on the sides specified by the user
501
+ self.pad_sides = cfg_dict.get("PADSIDES", "auto")
502
+ # stop bulding the tile after a certain number of postage stamps
503
+ self.stoptile = cfg_dict.get("STOP", 0)
504
+
505
+ ### SECTION V: WHAT AND WHERE TO OUTPUT ###
506
+ # choose which outputs to report
507
+ self.outmaps = cfg_dict.get("OUTMAPS", "USKTN")
508
+ # output stem
509
+ self.outstem = cfg_dict["OUT"]
510
+ # temporary storage
511
+ # virtual memory will be used if this is not empty str
512
+ self.tempfile = cfg_dict.get("TEMPFILE", "")
513
+ if not self.tempfile:
514
+ self.tempfile = None
515
+ # cache input layers here
516
+ self.inlayercache = cfg_dict.get("INLAYERCACHE", "")
517
+ if not self.inlayercache:
518
+ self.inlayercache = None
519
+
520
+ ### SECTION VI: TARGET OUTPUT PSF(S) ###
521
+ # number of target output PSF(s)
522
+ self.n_out = cfg_dict.get("NOUT", 1)
523
+ # target output PSF type
524
+ self.outpsf = cfg_dict.get("OUTPSF", "AIRYOBSC")
525
+ # target output PSF extra smearing
526
+ self.sigmatarget = cfg_dict.get("EXTRASMOOTH", 1.5 / 2.355)
527
+
528
+ if self.n_out > 1: # more than one target output PSF
529
+ self.outpsf_extra = []
530
+ self.sigmatarget_extra = []
531
+ for j_out in range(1, self.n_out):
532
+ self.outpsf_extra.append(cfg_dict.get(f"OUTPSF{j_out + 1}", "AIRYOBSC"))
533
+ self.sigmatarget_extra.append(cfg_dict.get(f"EXTRASMOOTH{j_out + 1}", 1.5 / 2.355))
534
+
535
+ ### SECTION VII: BUILDING LINEAR SYSTEMS ###
536
+ # width of PSF sampling/overlap arrays in native pixels
537
+ self.npixpsf = cfg_dict.get("NPIXPSF", 48)
538
+ # experimental feature to apply a circular cutout to PSFs
539
+ self.psf_circ = cfg_dict.get("PSFCIRC", False)
540
+ # experimental feature to normalize PSFs after sampling
541
+ self.psf_norm = cfg_dict.get("PSFNORM", False)
542
+
543
+ # experimental feature to change the weighting of Fourier modes
544
+ self.amp_penalty = cfg_dict.get("AMPPEN", (0.0, 0.0))
545
+ # amount by which to penalize having different contributions
546
+ # to the output from different input images
547
+ self.flat_penalty = cfg_dict.get("FLATPEN", 0.0)
548
+ # PSF interpolators
549
+ self.psf_interp = cfg_dict.get("PSFINTERP", "D5512")
550
+ # input stamp size padding (aka acceptance radius)
551
+ self.instamp_pad = cfg_dict.get("INPAD", 1.055) * Settings.arcsec
552
+
553
+ ### SECTION VIII: SOLVING LINEAR SYSTEMS ###
554
+ # kernel to solve linear systems
555
+ self.linear_algebra = cfg_dict.get("LAKERNEL", "Cholesky")
556
+ if self.linear_algebra == "Iterative":
557
+ # relative tolerance and maximum number of iterations
558
+ self.iter_rtol = cfg_dict.get("ITERRTOL", 1.5e-3)
559
+ self.iter_max = cfg_dict.get("ITERMAX", 30)
560
+ elif self.linear_algebra == "Empirical":
561
+ # no-quality control option
562
+ self.no_qlt_ctrl = cfg_dict.get("EMPIRNQC", False)
563
+
564
+ ### SECTION IX: DESTRIPING PARAMS ###
565
+ self.ds_model, self.ds_rows = cfg_dict.get("DSMODEL", [None, None])
566
+ self.ds_outpath, self.ds_outstem = cfg_dict.get("DSOUT", [None, None])
567
+ self.cg_model, self.cg_maxiter, self.cg_tol = cfg_dict.get("CGMODEL", [None, None, None])
568
+ self.cost_model, self.cost_prior, self.hub_thresh = cfg_dict.get("DSCOST", [None, None, None])
569
+ self.ds_obsfile = cfg_dict.get("DSOBSFILE")
570
+ self.ds_noisefile = cfg_dict.get("DSNOISEFILE", False)
571
+ self.ds_restart = cfg_dict.get("DSRESTART")
572
+ self.gaindir = cfg_dict.get("GAINDIR", False)
573
+ self.col_pars = cfg_dict.get("AMPCOLS", [None, 0.0])
574
+ self.amp_cols = self.col_pars[0]
575
+ self.col_boundary_const = self.col_pars[1]
576
+
577
+ # Lagrange multiplier (kappa) information
578
+ # list of kappa/C values, ascending order
579
+ self.kappaC_arr = np.array(cfg_dict.get("KAPPAC", [1e-5, 1e-4, 1e-3]))
580
+ # target (minimum) leakage
581
+ self.uctarget = cfg_dict.get("UCMIN", 1e-6)
582
+ # maximum allowed value of Sigma
583
+ self.sigmamax = cfg_dict.get("SMAX", 0.5)
584
+
585
+ ### SECTION IX: PASS THROUGHS ###
586
+ self.tileschm = cfg_dict.get("TILESCHM", "Not_specified")
587
+ self.rerun = cfg_dict.get("RERUN", "Not_specified")
588
+ self.mosaic = cfg_dict.get("MOSAIC", -1)
589
+
590
+ cfg_dict.clear()
591
+ del cfg_dict
592
+
593
+ def _get_attrs_wrapper(self, code: str, newline: bool = True) -> None:
594
+ """
595
+ Wrapper for getting an attribute or a set of attributes.
596
+
597
+ Parameters
598
+ ----------
599
+ code : str
600
+ Code segment to execute.
601
+ newline : bool, optional
602
+ Whether to print a blank line when finished.
603
+
604
+ Returns
605
+ -------
606
+ None
607
+
608
+ """
609
+
610
+ while True:
611
+ try:
612
+ exec(code)
613
+ except ValueError as error:
614
+ print(error)
615
+ print("# Invalid input, please try again.", flush=True)
616
+ else:
617
+ break
618
+ if newline:
619
+ print()
620
+
621
+ def _build_config(self) -> None:
622
+ """
623
+ Terminal interface to build a configuration from scratch.
624
+
625
+ The prompts are based on comments in old text configuration files.
626
+ assert statements for further validity checks to be added.
627
+
628
+ Returns
629
+ -------
630
+ None
631
+
632
+ """
633
+
634
+ print("### GENERAL NOTE: INPUT NOTHING TO USE DEFAULT ###\n", flush=True)
635
+
636
+ print("### SECTION I: INPUT FILES ###\n", flush=True)
637
+ # input files: OBSFILE, INDATA, FILTER, INPSF, PSFSPLIT
638
+
639
+ print("# input observation list", flush=True)
640
+ self._get_attrs_wrapper("self.obsfile = input('OBSFILE (str): ')")
641
+
642
+ print(
643
+ "# reference input file directory and naming convention\n# (including the WCS used for stacking)",
644
+ flush=True,
645
+ )
646
+ self._get_attrs_wrapper("self.inpath, self.informat = input('INDATA (str str): ').split(' ')")
647
+
648
+ print("# which filter", flush=True)
649
+ self._get_attrs_wrapper("self.use_filter = int(input('FILTER (int): '))")
650
+
651
+ print("# input PSF files & format & oversamp", flush=True)
652
+ self._get_attrs_wrapper(
653
+ "self.inpsf_path, self.inpsf_format, OVERSAMP = input('INPSF (str str int): ').split(' ')"
654
+ "\n"
655
+ "self.inpsf_oversamp = int(OVERSAMP)"
656
+ )
657
+
658
+ print("# PSF splitting", flush=True)
659
+ self._get_attrs_wrapper(
660
+ "self.psfsplit_r1, self.psfsplit_r2, self.psfsplit_epsilon = input('PSFSPLIT (float float float) "
661
+ "[default: no split]: ').split(' ')"
662
+ "\n"
663
+ "self.psfsplit = [self.psfsplit_r1, self.psfsplit_r2, self.psfsplit_epsilon] if self.psfsplit_r1 "
664
+ "else ''"
665
+ )
666
+
667
+ print("### SECTION II: MASKS AND LAYERS ###\n", flush=True)
668
+ # masks and layers: PMASK, CMASK, EXTRAINPUT, LABNOISETHRESHOLD
669
+
670
+ print(
671
+ "# mask options:"
672
+ "\n"
673
+ "# PMASK --> permanent mask (from file)"
674
+ "\n"
675
+ "# default: no permanent pixel mask"
676
+ "\n"
677
+ "# CMASK --> cosmic ray hit probability for stochastic mask",
678
+ flush=True,
679
+ )
680
+ self._get_attrs_wrapper(
681
+ "PMASK = input('PMASK (str) [default: None]: ')\nself.permanent_mask = PMASK if PMASK else None",
682
+ newline=False,
683
+ )
684
+ self._get_attrs_wrapper(
685
+ "CMASK = input('CMASK (float) [default: 0.0]: ')"
686
+ "\n"
687
+ "self.cr_mask_rate = float(CMASK) if CMASK else 0.0"
688
+ )
689
+
690
+ print(
691
+ "# extra inputs (input images to stack at once)"
692
+ "\n"
693
+ "# (use names for each one, space-delimited; meaning of names must be coded into"
694
+ "\n"
695
+ "# layer.get_all_data, with the meaning based on the naming convention in INDATA)",
696
+ flush=True,
697
+ )
698
+ self._get_attrs_wrapper(
699
+ "EXTRAINPUT = input('EXTRAINPUT (str str ...) [default: None]: ')"
700
+ "\n"
701
+ "self.extrainput = [None] + (EXTRAINPUT.split() if EXTRAINPUT else [])"
702
+ )
703
+
704
+ print(
705
+ "# mask out pixels with lab noise beyond this threshold"
706
+ "\n"
707
+ "# (ignored if labnoise is not in EXTRAINPUT or does not exist)",
708
+ flush=True,
709
+ )
710
+ self._get_attrs_wrapper(
711
+ "LABNOISETHRESHOLD = input('LABNOISETHRESHOLD (float) [default: 3.0]: ')"
712
+ "\n"
713
+ "self.labnoisethreshold = float(LABNOISETHRESHOLD) if LABNOISETHRESHOLD else 3.0"
714
+ )
715
+
716
+ print("### SECTION III: WHAT AREA TO COADD ###\n", flush=True)
717
+ # what area to coadd: CTR, BLOCK, OUTSIZE
718
+
719
+ print("# location of the output region to make", flush=True)
720
+ self._get_attrs_wrapper(
721
+ "self.ra, self.dec = map(float, input('CTR (float float): ').split(' '))", newline=False
722
+ )
723
+ self._get_attrs_wrapper(
724
+ "self.lonpole = map(float, input('LONPOLE (float): ').split(' '))", newline=False
725
+ )
726
+ self._get_attrs_wrapper("self.nblock = int(input('BLOCK (int): '))", newline=False)
727
+ self._get_attrs_wrapper(
728
+ "self.n1, self.n2, self.dtheta = map(eval, input('OUTSIZE (int int float): ').split(' '))"
729
+ "\n"
730
+ "assert self.n1 % 2 == 0, 'Error: n1 must be even since PSF computations are in 2x2 groups'"
731
+ "\n"
732
+ "self.dtheta *= u.arcsec.to('degree')"
733
+ )
734
+
735
+ print("### SECTION IV: MORE ABOUT POSTAGE STAMPS ###\n", flush=True)
736
+ # more about postage stamps: FADE, PAD, PADSIDES, STOP
737
+
738
+ print(
739
+ "# fading kernel width (number of rows or columns"
740
+ "\n"
741
+ "# of transition pixels on each side of a postage stamp)",
742
+ flush=True,
743
+ )
744
+ self._get_attrs_wrapper(
745
+ "FADE = input('FADE (int) [default: 3]: ')"
746
+ "\n"
747
+ "self.fade_kernel = int(FADE) if FADE else 3"
748
+ "\n"
749
+ "assert self.n2 > self.fade_kernel * 2, 'insufficient patch size'"
750
+ )
751
+
752
+ print("# number of IMCOM postage stamps to pad around each output region", flush=True)
753
+ self._get_attrs_wrapper(
754
+ "PAD = input('PAD (int) [default: 0]: ')\nself.postage_pad = int(PAD) if PAD else 0"
755
+ )
756
+
757
+ print(
758
+ "# on which side(s) to pad IMCOM postage stamps"
759
+ "\n"
760
+ '# "all": pad on all sides;'
761
+ "\n"
762
+ '# "auto": pad on mosaic boundaries only;'
763
+ "\n"
764
+ '# "none": pad on none of the sides;'
765
+ "\n"
766
+ "# otherwise, please specify which side(s) to pad on"
767
+ "\n"
768
+ "# using CAPITAL letters (the order does not matter)"
769
+ "\n"
770
+ '# "B" (bottom), "T" (top), "L" (left), and "R" (right)',
771
+ flush=True,
772
+ )
773
+ self._get_attrs_wrapper(
774
+ "PADSIDES = input('PADSIDES (str) [default: \"auto\"]: ')"
775
+ "\n"
776
+ "self.pad_sides = PADSIDES if PADSIDES else 'auto'"
777
+ )
778
+
779
+ print(
780
+ "# stop execution after a certain number of postage stamps"
781
+ "\n"
782
+ "# (for testing so we don't have to wait for all the postage stamps)"
783
+ "\n"
784
+ "# good choices: 16 to see if it runs; 624 to get a portion of the image without using lots of "
785
+ "time"
786
+ "\n"
787
+ "# default: don't stop until we get to the end",
788
+ flush=True,
789
+ )
790
+ self._get_attrs_wrapper(
791
+ "STOP = input('STOP (int) [default: 0]: ')\nself.stoptile = int(STOP) if STOP else 0"
792
+ )
793
+
794
+ print("### SECTION V: WHAT AND WHERE TO OUTPUT ###\n", flush=True)
795
+ # what and where to output: OUTMAPS, OUT, TEMPFILE, INLAYERCACHE
796
+
797
+ print(
798
+ "# choose which outputs to report"
799
+ "\n"
800
+ "# U = PSF leakage map (U_alpha/C)"
801
+ "\n"
802
+ "# S = noise map"
803
+ "\n"
804
+ "# K = kappa (Lagrange multiplier map)"
805
+ "\n"
806
+ "# T = total weight (sum over all input pixels)"
807
+ "\n"
808
+ "# N = effective coverage",
809
+ flush=True,
810
+ )
811
+ self._get_attrs_wrapper(
812
+ "OUTMAPS = input('OUTMAPS (str) [default: \"USKTN\"]: ')"
813
+ "\n"
814
+ "self.outmaps = OUTMAPS if OUTMAPS else 'USKTN'"
815
+ )
816
+
817
+ print("# output location:\n# set to something in your directory", flush=True)
818
+ self._get_attrs_wrapper("self.outstem = input('OUT (str): ')")
819
+
820
+ print("# temporary storage location (prefix):\n# not to use virtual memory", flush=True)
821
+ self._get_attrs_wrapper(
822
+ "TEMPFILE = input('TEMPFILE (str) [default: '']: ')\nself.tempfile = TEMPFILE",
823
+ newline=False,
824
+ )
825
+
826
+ print("# input layer cache (prefix):", flush=True)
827
+ self._get_attrs_wrapper(
828
+ "INLAYERCACHE = input('INLAYERCACHE (str) [default: '']: ')\nself.inlayercache = INLAYERCACHE",
829
+ newline=False,
830
+ )
831
+
832
+ print("### SECTION VI: TARGET OUTPUT PSF(S) ###\n", flush=True)
833
+ # target output PSF(s): NOUT, OUTPSF, EXTRASMOOTH
834
+ # (optional: OUTPSF2, EXTRASMOOTH2, etc.)
835
+
836
+ print("# number of target output PSF(s)", flush=True)
837
+ self._get_attrs_wrapper(
838
+ "NOUT = input('NOUT (int) [default: 1]: ')"
839
+ "\n"
840
+ "self.n_out = (int(NOUT) if NOUT else 1)"
841
+ "\n"
842
+ "assert self.n_out >= 1, 'NOUT should be at least 1'"
843
+ )
844
+
845
+ print(
846
+ "# target output PSF type, options include"
847
+ "\n"
848
+ '# "GAUSSIAN": simple Gaussian'
849
+ "\n"
850
+ '# "AIRYOBSC": obscured Airy disk convolved with Gaussian'
851
+ "\n"
852
+ '# "AIRYUNOBSC": unobscured Airy disk convolved with Gaussian',
853
+ flush=True,
854
+ )
855
+ self._get_attrs_wrapper(
856
+ "OUTPSF = input('OUTPSF (str) [default: \"AIRYOBSC\"]: ')"
857
+ "\n"
858
+ "assert OUTPSF in ['', 'GAUSSIAN', 'AIRYOBSC', 'AIRYUNOBSC'], 'unrecognized type'"
859
+ "\n"
860
+ "self.outpsf = OUTPSF if OUTPSF else 'AIRYOBSC'"
861
+ )
862
+
863
+ print(
864
+ "# smoothing of output PSF (units: input pixels, 1 sigma)"
865
+ "\n"
866
+ "# default: FWHM Gaussian smoothing divided by 2.355 to be a sigma",
867
+ flush=True,
868
+ )
869
+ self._get_attrs_wrapper(
870
+ "EXTRASMOOTH = input('EXTRASMOOTH (float) [default: 1.5 / 2.355]: ')"
871
+ "\n"
872
+ "self.sigmatarget = float(EXTRASMOOTH) if EXTRASMOOTH else (1.5 / 2.355)"
873
+ )
874
+
875
+ if self.n_out > 1: # more than one target output PSF
876
+ self.outpsf_extra = []
877
+ self.sigmatarget_extra = []
878
+ for j_out in range(1, self.n_out):
879
+ print(f"# now talking about target output PSF {j_out + 1}:\n# output PSF type", flush=True)
880
+ self._get_attrs_wrapper(
881
+ f"OUTPSF{j_out + 1} = input('OUTPSF{j_out + 1} (str) [default: \"AIRYOBSC\"]: ')"
882
+ "\n"
883
+ f"assert OUTPSF{j_out + 1} in ['', 'GAUSSIAN', 'AIRYOBSC', 'AIRYUNOBSC'], "
884
+ "'unrecognized type'"
885
+ "\n"
886
+ f"self.outpsf_extra.append(OUTPSF{j_out + 1} if OUTPSF{j_out + 1} else 'AIRYOBSC')"
887
+ )
888
+ print("# smoothing of output PSF", flush=True)
889
+ self._get_attrs_wrapper(
890
+ f"EXTRASMOOTH{j_out + 1} = input('EXTRASMOOTH{j_out + 1} (float) "
891
+ "[default: 1.5 / 2.355]: ')"
892
+ "\n"
893
+ f"self.sigmatarget_extra.append(float(EXTRASMOOTH{j_out + 1}) if EXTRASMOOTH{j_out + 1} "
894
+ "else (1.5 / 2.355))"
895
+ )
896
+
897
+ print("### SECTION VII: BUILDING LINEAR SYSTEMS ###\n", flush=True)
898
+ # building linear systems: NPIXPSF, AMPPEN, FLATPEN, INPAD
899
+
900
+ # print('# size of PSF postage stamp in native pixels')
901
+ print(
902
+ "# width of PSF sampling/overlap arrays in native pixels"
903
+ "\n"
904
+ "# preferably a nice number for FFT purposes",
905
+ flush=True,
906
+ )
907
+ self._get_attrs_wrapper(
908
+ "NPIXPSF = input('NPIXPSF (int) [default: 48]: ')"
909
+ "\n"
910
+ "self.npixpsf = (int(NPIXPSF) if NPIXPSF else 48)"
911
+ )
912
+
913
+ print("# whether to apply a circular cutout to PSFs", flush=True)
914
+ self._get_attrs_wrapper(
915
+ "PSFCIRC = input('PSFCIRC (bool) [default: False]: ')"
916
+ "\n"
917
+ "self.psf_circ = (bool(eval(PSFCIRC)) if PSFCIRC else False)"
918
+ )
919
+
920
+ print("# whether to normalize PSFs after sampling", flush=True)
921
+ self._get_attrs_wrapper(
922
+ "PSFNORM = input('PSFNORM (bool) [default: False]: ')"
923
+ "\n"
924
+ "self.psf_norm = (bool(eval(PSFNORM)) if PSFNORM else False)"
925
+ )
926
+
927
+ print(
928
+ "# experimental feature to change the weighting of Fourier modes"
929
+ "\n"
930
+ "# format: (amp, sig), where amp is the amplitude,"
931
+ "\n"
932
+ "# sig is the width in units of input pixels",
933
+ flush=True,
934
+ )
935
+ self._get_attrs_wrapper(
936
+ "AMPPEN = input('AMPPEN (float float) [default: (0.0, 0.0)]: ')"
937
+ "\n"
938
+ "self.amp_penalty = map(float, AMPPEN.split(' ')) if AMPPEN else (0.0, 0.0)"
939
+ )
940
+
941
+ print(
942
+ "# amount by which to penalize having different contributions"
943
+ "\n"
944
+ "# to the output from different input images",
945
+ flush=True,
946
+ )
947
+ self._get_attrs_wrapper(
948
+ "FLATPEN = input('FLATPEN (float) [default: 0.0]: ')"
949
+ "\n"
950
+ "self.flat_penalty = (float(FLATPEN) if FLATPEN else 0.0)"
951
+ )
952
+ self._get_attrs_wrapper(
953
+ "PSFINTERP = input('PSFINTERP (str) [default: \"D5512\"]: ')"
954
+ "\n"
955
+ "self.psf_interp = PSFINTERP if PSFINTERP else 'D5512'"
956
+ )
957
+
958
+ print("# input stamp size padding (aka acceptance radius) in arcsec", flush=True)
959
+ self._get_attrs_wrapper(
960
+ "INPAD = input('INPAD (float) [default: 1.055]: ')"
961
+ "\n"
962
+ "self.instamp_pad = (float(INPAD) if INPAD else 1.055) * Settings.arcsec"
963
+ )
964
+
965
+ print("### SECTION VIII: SOLVING LINEAR SYSTEMS ###\n", flush=True)
966
+ # solving linear systems: LAKERNEL, KAPPAC, UCMIN, SMAX
967
+
968
+ print(
969
+ "# kernel to solve linear systems, options include"
970
+ "\n"
971
+ '# "Eigen": kernel using eigendecomposition'
972
+ "\n"
973
+ '# "Cholesky": kernel using Cholesky decomposition'
974
+ "\n"
975
+ '# "Iterative": kernel using iterative method'
976
+ "\n"
977
+ '# "Empirical": kernel using empirical method',
978
+ flush=True,
979
+ )
980
+ self._get_attrs_wrapper(
981
+ "LAKERNEL = input('LAKERNEL (str) [default: \"Cholesky\"]: ')"
982
+ "\n"
983
+ "self.linear_algebra = LAKERNEL if LAKERNEL else 'Cholesky'"
984
+ "\n"
985
+ "assert self.linear_algebra in ['Eigen', 'Cholesky', 'Iterative', 'Empirical'], "
986
+ "'unrecognized kernel'"
987
+ )
988
+
989
+ if self.linear_algebra == "Iterative":
990
+ print("# Iterative kernel: relative tolerance", flush=True)
991
+ self._get_attrs_wrapper(
992
+ "ITERRTOL = input('ITERRTOL (float) [default: 1.5e-3]: ')"
993
+ "\n"
994
+ "self.iter_rtol = (float(ITERRTOL) if ITERRTOL else 1.5e-3)"
995
+ )
996
+ print("# Iterative kernel: maximum number of iterations", flush=True)
997
+ self._get_attrs_wrapper(
998
+ "ITERMAX = input('ITERMAX (int) [default: 30]: ')"
999
+ "\n"
1000
+ "self.iter_max = (int(ITERMAX) if ITERMAX else 30)"
1001
+ )
1002
+
1003
+ elif self.linear_algebra == "Empirical":
1004
+ print(
1005
+ "# Empirical kernel: no-quality control option"
1006
+ "\n"
1007
+ "# coadd images without computing system matrices A and B"
1008
+ "\n"
1009
+ '# "U" and "S" will be automatically removed from OUTMAPS;'
1010
+ "\n"
1011
+ '# automatically set to true if neither "U" nor "S" is in OUTMAPS',
1012
+ flush=True,
1013
+ )
1014
+ self._get_attrs_wrapper(
1015
+ "EMPIRNQC = input('EMPIRNQC (bool) [default: False]: ')"
1016
+ "\n"
1017
+ "self.no_qlt_ctrl = (bool(eval(EMPIRNQC)) if EMPIRNQC else False)"
1018
+ )
1019
+
1020
+ print(
1021
+ "# Lagrange multiplier (kappa) information"
1022
+ "\n"
1023
+ "# list of kappa/C values, ascending order"
1024
+ "\n"
1025
+ '# if LAKERNEL == "Empirical", only the first kappa/C value is used;'
1026
+ "\n"
1027
+ "# otherwise if single value: use this fixed kappa/C value;"
1028
+ "\n"
1029
+ '# if multiple values: "Eigen" performs a bisection search'
1030
+ "\n"
1031
+ '# between the first and last kappa/C values, while "Cholesky"'
1032
+ "\n"
1033
+ '# and "Iterative" kernels use these kappa/C values as nodes',
1034
+ flush=True,
1035
+ )
1036
+ self._get_attrs_wrapper(
1037
+ "KAPPAC = input('KAPPAC (float ...) [default: [1e-5, 1e-4, 1e-3]]: ')"
1038
+ "\n"
1039
+ "self.kappaC_arr = np.array(list(map(float, KAPPAC.split(' '))) if KAPPAC else "
1040
+ "[1e-5, 1e-4, 1e-3])"
1041
+ "\n"
1042
+ "assert np.all(np.diff(self.kappaC_arr) > 0.0), 'must be in ascending order'"
1043
+ )
1044
+
1045
+ print("# target (minimum) leakage", flush=True)
1046
+ self._get_attrs_wrapper(
1047
+ "UCMIN = input('UCMIN (float) [default: 1e-6]: ')"
1048
+ "\n"
1049
+ "self.uctarget = float(UCMIN) if UCMIN else 1e-6"
1050
+ )
1051
+
1052
+ print("# maximum allowed value of Sigma", flush=True)
1053
+ self._get_attrs_wrapper(
1054
+ "SMAX = input('SMAX (float) [default: 0.5]: ')\nself.sigmamax = float(SMAX) if SMAX else 0.5"
1055
+ )
1056
+
1057
+ print("### SECTION IX: PASS THROUGHS ###\n", flush=True)
1058
+ # pass throughs: TILESCHM, RERUN, MOSAIC
1059
+ print("# tiling scheme name", flush=True)
1060
+ self._get_attrs_wrapper(
1061
+ "TILESCHM = input('TILESCHM (str) [default: \"Not_specified\"]: ')"
1062
+ "\n"
1063
+ "self.tileschm = TILESCHM if TILESCHM else 'Not_specified'"
1064
+ )
1065
+
1066
+ print("# rerun name", flush=True)
1067
+ self._get_attrs_wrapper(
1068
+ "RERUN = input('RERUN (str) [default: \"Not_specified\"]: ')"
1069
+ "\n"
1070
+ "self.rerun = RERUN if RERUN else 'Not_specified'"
1071
+ )
1072
+
1073
+ print("# mosaic index", flush=True)
1074
+ self._get_attrs_wrapper(
1075
+ "MOSAIC = input('MOSAIC (int) [default: \"-1\"]: ')"
1076
+ "\n"
1077
+ "self.mosaic = int(MOSAIC) if MOSAIC else -1"
1078
+ )
1079
+
1080
+ print("# To save this configuration, call Config.to_file.\n", flush=True)
1081
+
1082
+ def to_file(self, fname: str = "") -> None:
1083
+ """
1084
+ Save the configuration to a JSON file.
1085
+
1086
+ Parameters
1087
+ ----------
1088
+ fname : str or None, optional
1089
+ JSON configuration file name to save to.
1090
+ The default is ''. This overwrites pyimcom/configs/default_config.json.
1091
+ Set fname=None to get a text version of the configuration.
1092
+
1093
+ Returns
1094
+ -------
1095
+ str or None
1096
+ Text version of the configuration.
1097
+
1098
+ """
1099
+
1100
+ cfg_dict = {}
1101
+
1102
+ ### SECTION I: INPUT FILES ###
1103
+ cfg_dict["OBSFILE"] = self.obsfile
1104
+ cfg_dict["INDATA"] = [self.inpath, self.informat]
1105
+ cfg_dict["FILTER"] = self.use_filter
1106
+ cfg_dict["INPSF"] = [self.inpsf_path, self.inpsf_format, self.inpsf_oversamp]
1107
+ if self.psfsplit:
1108
+ cfg_dict["PSFSPLIT"] = [self.psfsplit_r1, self.psfsplit_r2, self.psfsplit_epsilon]
1109
+
1110
+ ### SECTION II: MASKS AND LAYERS ###
1111
+ cfg_dict["PMASK"] = self.permanent_mask
1112
+ cfg_dict["CMASK"] = self.cr_mask_rate
1113
+ cfg_dict["EXTRAINPUT"] = self.extrainput[1:]
1114
+ cfg_dict["LABNOISETHRESHOLD"] = self.labnoisethreshold
1115
+
1116
+ ### SECTION III: WHAT AREA TO COADD ###
1117
+ cfg_dict["CTR"] = [self.ra, self.dec]
1118
+ cfg_dict["LONPOLE"] = self.lonpole
1119
+ cfg_dict["BLOCK"] = self.nblock
1120
+ cfg_dict["OUTSIZE"] = [self.n1, self.n2, self.dtheta * u.degree.to("arcsec")]
1121
+
1122
+ ### SECTION IV: MORE ABOUT POSTAGE STAMPS ###
1123
+ cfg_dict["FADE"] = self.fade_kernel
1124
+ cfg_dict["PAD"] = self.postage_pad
1125
+ cfg_dict["PADSIDES"] = self.pad_sides
1126
+ cfg_dict["STOP"] = self.stoptile
1127
+
1128
+ ### SECTION V: WHAT AND WHERE TO OUTPUT ###
1129
+ cfg_dict["OUTMAPS"] = self.outmaps
1130
+ cfg_dict["OUT"] = self.outstem
1131
+ cfg_dict["TEMPFILE"] = self.tempfile if self.tempfile else ""
1132
+ cfg_dict["INLAYERCACHE"] = self.inlayercache if self.inlayercache else ""
1133
+
1134
+ ### SECTION VI: TARGET OUTPUT PSF(S) ###
1135
+ cfg_dict["NOUT"] = self.n_out
1136
+ cfg_dict["OUTPSF"] = self.outpsf
1137
+ cfg_dict["EXTRASMOOTH"] = self.sigmatarget
1138
+ if self.n_out > 1: # more than one target output PSF
1139
+ for j_out in range(1, self.n_out):
1140
+ cfg_dict[f"OUTPSF{j_out + 1}"] = self.outpsf_extra[j_out - 1]
1141
+ cfg_dict[f"EXTRASMOOTH{j_out + 1}"] = self.sigmatarget_extra[j_out - 1]
1142
+
1143
+ ### SECTION VII: BUILDING LINEAR SYSTEMS ###
1144
+ cfg_dict["NPIXPSF"] = self.npixpsf
1145
+ cfg_dict["PSFCIRC"] = self.psf_circ
1146
+ cfg_dict["PSFNORM"] = self.psf_norm
1147
+
1148
+ cfg_dict["AMPPEN"] = self.amp_penalty
1149
+ cfg_dict["FLATPEN"] = self.flat_penalty
1150
+ cfg_dict["PSFINTERP"] = self.psf_interp
1151
+ cfg_dict["INPAD"] = self.instamp_pad / Settings.arcsec
1152
+
1153
+ ### SECTION VIII: SOLVING LINEAR SYSTEMS ###
1154
+ cfg_dict["LAKERNEL"] = self.linear_algebra
1155
+ if self.linear_algebra == "Iterative":
1156
+ cfg_dict["ITERRTOL"] = self.iter_rtol
1157
+ cfg_dict["ITERMAX"] = self.iter_max
1158
+ elif self.linear_algebra == "Empirical":
1159
+ cfg_dict["EMPIRNQC"] = self.no_qlt_ctrl
1160
+
1161
+ cfg_dict["KAPPAC"] = list(self.kappaC_arr)
1162
+ cfg_dict["UCMIN"] = self.uctarget
1163
+ cfg_dict["SMAX"] = self.sigmamax
1164
+
1165
+ ### SECTION IX: PASS THROUGHS ###
1166
+ cfg_dict["TILESCHM"] = self.tileschm
1167
+ cfg_dict["RERUN"] = self.rerun
1168
+ cfg_dict["MOSAIC"] = self.mosaic
1169
+
1170
+ if fname is not None:
1171
+ if fname == "":
1172
+ print("> Overwriting pyimcom/configs/default_config.json", flush=True)
1173
+ fname = files(__package__).joinpath("configs/default_config.json")
1174
+
1175
+ with open(fname, "w") as f:
1176
+ json.dump(cfg_dict, f, indent=4)
1177
+ cfg_dict.clear()
1178
+ del cfg_dict
1179
+
1180
+ else: # return text version of the configuration
1181
+ res = json.dumps(cfg_dict, indent=4)
1182
+ cfg_dict.clear()
1183
+ del cfg_dict
1184
+ return res
1185
+
1186
+ def to_dict(self):
1187
+ """
1188
+ Save the configuration as a dictionary.
1189
+
1190
+ Parameters
1191
+ ----------
1192
+ None
1193
+
1194
+ Returns
1195
+ -------
1196
+ dict
1197
+ Dictionary version of the configuration.
1198
+
1199
+ """
1200
+ return json.loads(self.to_file(None))
1201
+
1202
+
1203
+ # Parameters for format_axis.
1204
+ # Can be imported for context via
1205
+ # with mpl.rc_context(config.format_axis_pars):
1206
+ format_axis_pars = {
1207
+ "font.family": "serif",
1208
+ "mathtext.fontset": "dejavuserif",
1209
+ "font.size": 12,
1210
+ "text.latex.preamble": r"\usepackage{amsmath}",
1211
+ "xtick.major.pad": 2,
1212
+ "ytick.major.pad": 2,
1213
+ "xtick.major.size": 6,
1214
+ "ytick.major.size": 6,
1215
+ "xtick.minor.size": 3,
1216
+ "ytick.minor.size": 3,
1217
+ "axes.linewidth": 2,
1218
+ "axes.labelpad": 1,
1219
+ }
1220
+
1221
+
1222
+ def format_axis(ax, grid_on=True):
1223
+ """
1224
+ Format a panel (an axis) of a figure.
1225
+
1226
+ Parameters
1227
+ ----------
1228
+ ax : mpl.axes._axes.Axes
1229
+ Panel to be formatted.
1230
+ grid_on : bool, optional
1231
+ Whether to add grid to the panel.
1232
+
1233
+ Returns
1234
+ -------
1235
+ None
1236
+
1237
+ """
1238
+
1239
+ ax.minorticks_on()
1240
+ if grid_on:
1241
+ ax.grid(visible=True, which="major", linestyle=":")
1242
+ ax.tick_params(axis="both", which="both", direction="out")
1243
+ ax.xaxis.set_ticks_position("both")
1244
+ ax.yaxis.set_ticks_position("both")
1245
+ ax.patch.set_alpha(0.0)