teareduce 0.3.6__py3-none-any.whl → 0.4.0__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.
- teareduce/__init__.py +5 -1
- teareduce/elapsed_time.py +17 -1
- teareduce/imshow.py +42 -7
- teareduce/simulateccdexposure.py +639 -0
- teareduce/sliceregion.py +144 -29
- teareduce/version.py +1 -1
- teareduce/write_array_to_fits.py +28 -0
- {teareduce-0.3.6.dist-info → teareduce-0.4.0.dist-info}/METADATA +5 -5
- {teareduce-0.3.6.dist-info → teareduce-0.4.0.dist-info}/RECORD +12 -10
- {teareduce-0.3.6.dist-info → teareduce-0.4.0.dist-info}/WHEEL +1 -1
- {teareduce-0.3.6.dist-info → teareduce-0.4.0.dist-info}/LICENSE.txt +0 -0
- {teareduce-0.3.6.dist-info → teareduce-0.4.0.dist-info}/top_level.txt +0 -0
teareduce/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright 2023-
|
|
2
|
+
# Copyright 2023-2025 Universidad Complutense de Madrid
|
|
3
3
|
#
|
|
4
4
|
# This file is part of teareduce
|
|
5
5
|
#
|
|
@@ -13,7 +13,9 @@ from .cosmicrays import cr2images, apply_cr2images_ccddata, crmedian
|
|
|
13
13
|
from .ctext import ctext
|
|
14
14
|
from .draw_rectangle import draw_rectangle
|
|
15
15
|
from .elapsed_time import elapsed_time
|
|
16
|
+
from .elapsed_time import elapsed_time_since
|
|
16
17
|
from .imshow import imshow
|
|
18
|
+
from .imshow import imshowme
|
|
17
19
|
from .numsplines import AdaptiveLSQUnivariateSpline
|
|
18
20
|
from .peaks_spectrum import find_peaks_spectrum, refine_peaks_spectrum
|
|
19
21
|
from .polfit import polfit_residuals, polfit_residuals_with_sigma_rejection
|
|
@@ -21,8 +23,10 @@ from .robust_std import robust_std
|
|
|
21
23
|
from .sdistortion import fit_sdistortion
|
|
22
24
|
from .sliceregion import SliceRegion1D, SliceRegion2D
|
|
23
25
|
from .statsummary import ifc_statsummary, statsummary
|
|
26
|
+
from .simulateccdexposure import SimulateCCDExposure
|
|
24
27
|
from .version import version
|
|
25
28
|
from .wavecal import TeaWaveCalibration, apply_wavecal_ccddata
|
|
29
|
+
from .write_array_to_fits import write_array_to_fits
|
|
26
30
|
from .zscale import zscale
|
|
27
31
|
|
|
28
32
|
__version__ = version
|
teareduce/elapsed_time.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
#
|
|
3
|
-
# Copyright 2024 Universidad Complutense de Madrid
|
|
3
|
+
# Copyright 2024-2025 Universidad Complutense de Madrid
|
|
4
4
|
#
|
|
5
5
|
# This file is part of teareduce
|
|
6
6
|
#
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
# License-Filename: LICENSE.txt
|
|
9
9
|
#
|
|
10
10
|
|
|
11
|
+
from datetime import datetime
|
|
11
12
|
import platform
|
|
12
13
|
import sys
|
|
13
14
|
from .version import version
|
|
@@ -39,3 +40,18 @@ def elapsed_time(time_ini, time_end, osinfo=True):
|
|
|
39
40
|
print(f"Initial time.....: {time_ini}")
|
|
40
41
|
print(f"Final time.......: {time_end}")
|
|
41
42
|
print(f"Elapsed time.....: {time_end - time_ini}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def elapsed_time_since(time_ini, osinfo=True):
|
|
46
|
+
"""Display elapsed time and OS info since a given time.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
time_ini : datetime instance
|
|
51
|
+
Initial time.
|
|
52
|
+
osinfo : bool
|
|
53
|
+
If True, display OS info.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
time_end = datetime.now()
|
|
57
|
+
elapsed_time(time_ini, time_end)
|
teareduce/imshow.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright 2022-
|
|
2
|
+
# Copyright 2022-2025 Universidad Complutense de Madrid
|
|
3
3
|
#
|
|
4
4
|
# This file is part of teareduce
|
|
5
5
|
#
|
|
@@ -7,12 +7,37 @@
|
|
|
7
7
|
# License-Filename: LICENSE.txt
|
|
8
8
|
#
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
from astropy.units import Unit
|
|
11
|
+
from matplotlib import pyplot as plt
|
|
12
|
+
from matplotlib.figure import Figure
|
|
13
|
+
from matplotlib.axes import Axes
|
|
11
14
|
from mpl_toolkits.axes_grid1 import make_axes_locatable
|
|
12
15
|
|
|
13
16
|
|
|
17
|
+
def imshowme(data, **kwargs):
|
|
18
|
+
"""Simple execution of teareduce.imshow.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
data : numpy.ndarray
|
|
23
|
+
2D array to be displayed
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
fig : matplotlib.figure.Figure
|
|
28
|
+
Instance of Figure.
|
|
29
|
+
ax : matplotlib.axes.Axes
|
|
30
|
+
Instance of Axes.
|
|
31
|
+
img : matplotlib AxesImage
|
|
32
|
+
Instance returned by ax.imshow()
|
|
33
|
+
"""
|
|
34
|
+
fig, ax = plt.subplots()
|
|
35
|
+
img = imshow(fig=fig, ax=ax, data=data, **kwargs)
|
|
36
|
+
return fig, ax, img
|
|
37
|
+
|
|
38
|
+
|
|
14
39
|
def imshow(fig=None, ax=None, data=None,
|
|
15
|
-
crpix1=1, crval1=None, cdelt1=None, cunit1=None, cunitx=
|
|
40
|
+
crpix1=1, crval1=None, cdelt1=None, cunit1=None, cunitx=Unit('Angstrom'),
|
|
16
41
|
xlabel=None, ylabel=None, title=None,
|
|
17
42
|
colorbar=True, cblabel='Number of counts',
|
|
18
43
|
**kwargs):
|
|
@@ -25,9 +50,9 @@ def imshow(fig=None, ax=None, data=None,
|
|
|
25
50
|
Parameters
|
|
26
51
|
----------
|
|
27
52
|
fig : matplotlib.figure.Figure
|
|
28
|
-
Figure
|
|
53
|
+
Instance of Figure.
|
|
29
54
|
ax : matplotlib.axes.Axes
|
|
30
|
-
Axes
|
|
55
|
+
Instance of Axes.
|
|
31
56
|
data : numpy array
|
|
32
57
|
2D array to be displayed.
|
|
33
58
|
crpix1 : astropy.units.Quantity
|
|
@@ -63,6 +88,13 @@ def imshow(fig=None, ax=None, data=None,
|
|
|
63
88
|
|
|
64
89
|
"""
|
|
65
90
|
|
|
91
|
+
# protections
|
|
92
|
+
if not isinstance(fig, Figure):
|
|
93
|
+
raise ValueError("Unexpected 'fig' argument")
|
|
94
|
+
if not isinstance(ax, Axes):
|
|
95
|
+
raise ValueError("Unexpected 'ax' argument")
|
|
96
|
+
|
|
97
|
+
# default labels
|
|
66
98
|
if xlabel is None:
|
|
67
99
|
xlabel = 'X axis (array index)'
|
|
68
100
|
|
|
@@ -77,8 +109,9 @@ def imshow(fig=None, ax=None, data=None,
|
|
|
77
109
|
naxis2, naxis1 = data.shape
|
|
78
110
|
xmin, xmax = -0.5, naxis1 - 0.5
|
|
79
111
|
ymin, ymax = -0.5, naxis2 - 0.5
|
|
80
|
-
|
|
81
|
-
|
|
112
|
+
u_pixel = Unit('pixel')
|
|
113
|
+
xminwv = crval1 + (xmin * u_pixel - crpix1 + 1 * u_pixel) * cdelt1
|
|
114
|
+
xmaxwv = crval1 + (xmax * u_pixel - crpix1 + 1 * u_pixel) * cdelt1
|
|
82
115
|
extent = [xminwv.to(cunitx).value, xmaxwv.to(cunitx).value, ymin, ymax]
|
|
83
116
|
xlabel = f'Wavelength ({cunitx})'
|
|
84
117
|
aspect = 'auto'
|
|
@@ -114,3 +147,5 @@ def imshow(fig=None, ax=None, data=None,
|
|
|
114
147
|
divider = make_axes_locatable(ax)
|
|
115
148
|
cax = divider.append_axes("right", size="5%", pad=0.05)
|
|
116
149
|
fig.colorbar(img, cax=cax, label=cblabel)
|
|
150
|
+
|
|
151
|
+
return img
|
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 Universidad Complutense de Madrid
|
|
3
|
+
#
|
|
4
|
+
# This file is part of teareduce
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: GPL-3.0+
|
|
7
|
+
# License-Filename: LICENSE.txt
|
|
8
|
+
#
|
|
9
|
+
from astropy.io import fits
|
|
10
|
+
from astropy.units import Unit
|
|
11
|
+
from astropy.units import Quantity
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from .sliceregion import SliceRegion2D
|
|
15
|
+
from .imshow import imshowme
|
|
16
|
+
|
|
17
|
+
VALID_BITPIX = {8: np.uint8, 16: np.uint16, 32: np.uint32, 64: np.uint64}
|
|
18
|
+
VALID_PARAMETERS = ["bias", "gain", "readout_noise", "dark", "flatfield", "data_model"]
|
|
19
|
+
VALID_IMAGE_TYPES = ["bias", "dark", "object"]
|
|
20
|
+
VALID_METHODS = ["poisson", "gaussian"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ImageParameter:
|
|
24
|
+
"""Auxiliary class to hold image parameters.
|
|
25
|
+
|
|
26
|
+
Attributes
|
|
27
|
+
----------
|
|
28
|
+
name : str
|
|
29
|
+
Parameter name.
|
|
30
|
+
value : int or float
|
|
31
|
+
Value of the parameter (without units).
|
|
32
|
+
unit : astropy.units.Unit or None
|
|
33
|
+
Expected unit of the parameter.
|
|
34
|
+
dtype : type
|
|
35
|
+
Expected type of the parameter value.
|
|
36
|
+
"""
|
|
37
|
+
def __init__(self, name, quantity, expected_unit, expected_type):
|
|
38
|
+
"""Initialize the class attributes.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
name : str
|
|
43
|
+
Parameter name.
|
|
44
|
+
quantity : astropy.units.Quantity or float
|
|
45
|
+
Parameter value (with units when required)
|
|
46
|
+
expected_unit : astropy.units.Unit or None
|
|
47
|
+
Expected unit of the parameter.
|
|
48
|
+
expected_type : type
|
|
49
|
+
Expected type of the parameter value.
|
|
50
|
+
"""
|
|
51
|
+
if expected_unit is not None:
|
|
52
|
+
if not isinstance(quantity, Quantity):
|
|
53
|
+
raise TypeError(f"{name} must be a Quantity: {expected_unit=}")
|
|
54
|
+
if quantity.unit != expected_unit:
|
|
55
|
+
raise ValueError(f"{quantity.unit=} != {expected_unit=}")
|
|
56
|
+
self.value = quantity.value
|
|
57
|
+
else:
|
|
58
|
+
if isinstance(quantity, Quantity):
|
|
59
|
+
raise TypeError(f"{quantity=} should not be a Quantity but a float or a numpy array")
|
|
60
|
+
self.value = quantity
|
|
61
|
+
self.name = name
|
|
62
|
+
self.unit = expected_unit
|
|
63
|
+
self.dtype = expected_type
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SimulatedCCDResult:
|
|
67
|
+
"""Result of executing SimulateCCDExposure.run().
|
|
68
|
+
|
|
69
|
+
Auxiliary class to store the result and all the relevant
|
|
70
|
+
parameters employed when generating the simulated CCD image.
|
|
71
|
+
|
|
72
|
+
Attributes
|
|
73
|
+
----------
|
|
74
|
+
data : numpy.ndarray or None
|
|
75
|
+
Data array with the result of the simulated CCD exposure.
|
|
76
|
+
unit : astropy.units.Unit
|
|
77
|
+
Units of the simulated CCD exposure.
|
|
78
|
+
imgtype : str
|
|
79
|
+
Type of image to be generated. It must be one of
|
|
80
|
+
VALID_IMAGE_TYPES.
|
|
81
|
+
method : str
|
|
82
|
+
Method used to generate the simulated CCD image.
|
|
83
|
+
It must be one of VALID_METHODS.
|
|
84
|
+
origin : SimulateCCDExposure
|
|
85
|
+
Instance of SimulateCCDExposure employed to generate
|
|
86
|
+
the simulated CCD image.
|
|
87
|
+
|
|
88
|
+
Methods
|
|
89
|
+
-------
|
|
90
|
+
imshow(**kwargs)
|
|
91
|
+
Display simulated CCD image using tea.imshow().
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, data, unit, imgtype, method, origin, seed):
|
|
95
|
+
"""
|
|
96
|
+
Initialize the class attributes.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
data : numpy.ndarray or None
|
|
101
|
+
Data array with the result of the simulated CCD exposure.
|
|
102
|
+
unit : astropy.units.Unit
|
|
103
|
+
Units of the simulated CCD exposure.
|
|
104
|
+
imgtype : str
|
|
105
|
+
Type of image to be generated. It must be one of
|
|
106
|
+
VALID_IMAGE_TYPES.
|
|
107
|
+
method : str
|
|
108
|
+
Method used to generate the simulated CCD image.
|
|
109
|
+
It must be one of VALID_METHODS.
|
|
110
|
+
origin : SimulateCCDExposure
|
|
111
|
+
Instance of SimulateCCDExposure employed to generate
|
|
112
|
+
the simulated CCD image.
|
|
113
|
+
seed : int or None
|
|
114
|
+
Random number generator seed.
|
|
115
|
+
"""
|
|
116
|
+
self.data = data
|
|
117
|
+
self.unit = unit
|
|
118
|
+
self.imgtype = imgtype
|
|
119
|
+
self.method = method
|
|
120
|
+
self.origin = origin
|
|
121
|
+
self.seed = seed
|
|
122
|
+
|
|
123
|
+
def __repr__(self):
|
|
124
|
+
output = f'{self.__class__.__name__}(\n'
|
|
125
|
+
output += f' data={self.data!r},\n'
|
|
126
|
+
output += f' unit={self.unit!r},\n'
|
|
127
|
+
output += f' imgtype={self.imgtype!r},\n'
|
|
128
|
+
output += f' method={self.method!r},\n'
|
|
129
|
+
output += f' origin={self.origin!r},\n'
|
|
130
|
+
output += f' seed={self.seed!r},\n'
|
|
131
|
+
output += ')'
|
|
132
|
+
return output
|
|
133
|
+
|
|
134
|
+
def imshowme(self, **kwargs):
|
|
135
|
+
"""Plot simulated CCD image
|
|
136
|
+
|
|
137
|
+
This function simplys call teareduce.imshowme()
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
**kwargs : dict
|
|
142
|
+
Additional keyword arguments to be passed to
|
|
143
|
+
teareduce.imshowme().
|
|
144
|
+
"""
|
|
145
|
+
return imshowme(self.data, **kwargs)
|
|
146
|
+
|
|
147
|
+
def writeto(self, filename, overwrite=False, save_params=False):
|
|
148
|
+
"""Write simulated result to FITS file.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
filename : path-like or file-like
|
|
153
|
+
File to write to.
|
|
154
|
+
overwrite : bool, optional
|
|
155
|
+
If True, overwrite the output file if it exists. Raises an
|
|
156
|
+
OSError if False and the output file exists.
|
|
157
|
+
save_params : bool, optional
|
|
158
|
+
If True, save the simulated CCD parameters to a file.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
hdu_data = fits.PrimaryHDU(self.data)
|
|
162
|
+
hdu_data.header["UNIT"] = str(self.unit)
|
|
163
|
+
hdu_data.header["IMGTYPE"] = self.imgtype
|
|
164
|
+
hdu_data.header["METHOD"] = self.method
|
|
165
|
+
|
|
166
|
+
if save_params:
|
|
167
|
+
hdu_bias = fits.ImageHDU(self.origin.bias.data, name='BIAS')
|
|
168
|
+
hdu_bias.header["UNIT"] = str(self.origin.bias.unit)
|
|
169
|
+
|
|
170
|
+
hdu_gain = fits.ImageHDU(self.origin.gain.data, name='GAIN')
|
|
171
|
+
hdu_gain.header["UNIT"] = str(self.origin.gain.unit)
|
|
172
|
+
|
|
173
|
+
hdu_readout_noise = fits.ImageHDU(self.origin.readout_noise.data, name='RNOISE')
|
|
174
|
+
hdu_readout_noise.header["UNIT"] = str(self.origin.readout_noise.unit)
|
|
175
|
+
|
|
176
|
+
hdu_dark = fits.ImageHDU(self.origin.dark.data, name='DARK')
|
|
177
|
+
hdu_dark.header["UNIT"] = str(self.origin.dark.unit)
|
|
178
|
+
|
|
179
|
+
hdu_flatfield = fits.ImageHDU(self.origin.flatfield.data, name='FLATFIELD')
|
|
180
|
+
|
|
181
|
+
hdu_data_model = fits.ImageHDU(self.origin.data_model.data, name='DATAMODEL')
|
|
182
|
+
hdu_data_model.header["UNIT"] = str(self.origin.data_model.unit)
|
|
183
|
+
|
|
184
|
+
hdul = fits.HDUList([hdu_data, hdu_bias, hdu_gain, hdu_readout_noise,
|
|
185
|
+
hdu_dark, hdu_flatfield, hdu_data_model])
|
|
186
|
+
else:
|
|
187
|
+
hdul = fits.HDUList([hdu_data])
|
|
188
|
+
|
|
189
|
+
hdul.writeto(filename, overwrite=overwrite)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class SimulateCCDExposure:
|
|
193
|
+
"""Simulated image generator from first principles.
|
|
194
|
+
|
|
195
|
+
CCD exposures are simulated making use of basic CCD parameters,
|
|
196
|
+
such as gain, readout noise, bias, dark and flat field.
|
|
197
|
+
A data model can also be employed to simulate more realising
|
|
198
|
+
CCD exposures.
|
|
199
|
+
|
|
200
|
+
The saturated pixels in 'data_model' are returned as 2**bitpix - 1
|
|
201
|
+
in the simulated image (for instance, 65535 when bitpix=16).
|
|
202
|
+
|
|
203
|
+
By initializing the seed of the random number generator
|
|
204
|
+
when instantiating this class, there is in principle no need to
|
|
205
|
+
use another seed for the run() method. This is useful for
|
|
206
|
+
generating reproducible sets of consecutive exposures. In any case,
|
|
207
|
+
it is also possible to provide a particular seed to the run()
|
|
208
|
+
method in order to initialize the execution of single exposures.
|
|
209
|
+
|
|
210
|
+
Attributes
|
|
211
|
+
----------
|
|
212
|
+
naxis1 : int
|
|
213
|
+
NAXIS1 value.
|
|
214
|
+
naxis2 : int
|
|
215
|
+
NAXIS2 value.
|
|
216
|
+
bitpix : int
|
|
217
|
+
BITPIX value.
|
|
218
|
+
bias : Quantity
|
|
219
|
+
Numpy array with the detector bias level (ADU).
|
|
220
|
+
gain : Quantity
|
|
221
|
+
Numpy array with the detector gain (electrons/ADU).
|
|
222
|
+
readout_noise : Quantity
|
|
223
|
+
Numpy array with the readout noise (ADU).
|
|
224
|
+
dark : Quantity
|
|
225
|
+
Numpy array with the total dark current (ADU) for each pixel.
|
|
226
|
+
These numbers should not be the dark current rate (ADU/s).
|
|
227
|
+
The provided numbers must correspond to the total dark current
|
|
228
|
+
since the exposure time is not defined.
|
|
229
|
+
flatfield : numpy.ndarray
|
|
230
|
+
Numpy array with the pixel to pixel sensitivity (without units).
|
|
231
|
+
data_model : Quantity
|
|
232
|
+
Numpy array with the model of the source to be simulated (ADU).
|
|
233
|
+
seed : int or None
|
|
234
|
+
Random number generator seed.
|
|
235
|
+
_rng : np.random.RandomState
|
|
236
|
+
Random number generator.
|
|
237
|
+
|
|
238
|
+
Methods
|
|
239
|
+
-------
|
|
240
|
+
set_constant(parameter, value, region)
|
|
241
|
+
Set the value of a particular CCD parameter to a constant
|
|
242
|
+
value. The parameter can be any of VALID_PARAMETERS. The
|
|
243
|
+
constant value can be employed for all pixels or in a specific
|
|
244
|
+
region.
|
|
245
|
+
set_array2d(parameter, value, region)
|
|
246
|
+
Set the value of a particular CCD parameter to a 2D array.
|
|
247
|
+
The parameter can be any of VALID_PARAMETERS. The 2D array
|
|
248
|
+
may correspond to the whole simulated array or to a specific
|
|
249
|
+
region.
|
|
250
|
+
run(imgtype, seed, method)
|
|
251
|
+
Execute the generation of the simulated CCD exposure of
|
|
252
|
+
type 'imgtype', where 'imgtype' is one of VALID_IMAGE_TYPES.
|
|
253
|
+
The signal can be generated using either method: Poisson
|
|
254
|
+
or Gaussian. It is possible to set the seed in order to
|
|
255
|
+
initialize the random number generator.
|
|
256
|
+
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
def __init__(self,
|
|
260
|
+
naxis1=None,
|
|
261
|
+
naxis2=None,
|
|
262
|
+
bitpix=None,
|
|
263
|
+
bias=np.nan * Unit('adu'),
|
|
264
|
+
gain=np.nan * Unit('electron') / Unit('adu'),
|
|
265
|
+
readout_noise=np.nan * Unit('adu'),
|
|
266
|
+
dark=np.nan * Unit('adu'),
|
|
267
|
+
flatfield=np.nan,
|
|
268
|
+
data_model=np.nan * Unit('adu'),
|
|
269
|
+
seed=None,):
|
|
270
|
+
"""Initialize the class attributes.
|
|
271
|
+
|
|
272
|
+
The simulated array dimensions are mandatory. If any additional
|
|
273
|
+
parameter is not provided, all the pixel values are set to NaN.
|
|
274
|
+
The values of each parameter can be subsequently modified using
|
|
275
|
+
methods that allow these values to be changed in specific regions
|
|
276
|
+
of the CCD.
|
|
277
|
+
|
|
278
|
+
The input parameters must be a Quantity whose value is either
|
|
279
|
+
a single number, which is expanded to fill the numpy.array,
|
|
280
|
+
or a numpy.array with the expected shape (NAXIS2, NAXIS1).
|
|
281
|
+
|
|
282
|
+
Parameters
|
|
283
|
+
----------
|
|
284
|
+
naxis1 : int
|
|
285
|
+
NAXIS1 value.
|
|
286
|
+
naxis2 : int
|
|
287
|
+
NAXIS2 value.
|
|
288
|
+
bitpix : int
|
|
289
|
+
BITPIX value.
|
|
290
|
+
bias : Quantity
|
|
291
|
+
Detector bias level (ADU).
|
|
292
|
+
gain : Quantity
|
|
293
|
+
Detector gain (electrons/ADU).
|
|
294
|
+
readout_noise : Quantity
|
|
295
|
+
Readout noise (ADU).
|
|
296
|
+
dark : Quantity
|
|
297
|
+
Total dark current (ADU). This number should not be the
|
|
298
|
+
dark current rate (ADU/s). The provided number must
|
|
299
|
+
be the total dark current since the exposure time is not
|
|
300
|
+
defined.
|
|
301
|
+
flatfield : float or numpy.ndarray
|
|
302
|
+
Pixel to pixel sensitivity (without units).
|
|
303
|
+
data_model : Quantity
|
|
304
|
+
Model of the source to be simulated (ADU).
|
|
305
|
+
seed : int or None
|
|
306
|
+
Random number generator seed.
|
|
307
|
+
"""
|
|
308
|
+
# protections
|
|
309
|
+
if naxis1 is None or naxis2 is None:
|
|
310
|
+
raise RuntimeError("Basic image parameters (naxis1, naxis2) must be provided")
|
|
311
|
+
if not isinstance(naxis1, int):
|
|
312
|
+
raise ValueError(f"{naxis1=} must be an integer")
|
|
313
|
+
if not isinstance(naxis2, int):
|
|
314
|
+
raise ValueError(f"{naxis2=} must be an integer")
|
|
315
|
+
if naxis1 < 0 or naxis2 < 0:
|
|
316
|
+
raise ValueError(f"Both {naxis1=} and {naxis2=} must be positive")
|
|
317
|
+
if bitpix is None:
|
|
318
|
+
raise ValueError(f"{bitpix=} must be provided")
|
|
319
|
+
if not isinstance(bitpix, int):
|
|
320
|
+
raise ValueError(f"{bitpix=} must be an integer")
|
|
321
|
+
if bitpix not in VALID_BITPIX.keys():
|
|
322
|
+
raise ValueError(f"BITPIX {bitpix} must be {VALID_BITPIX.keys()=}")
|
|
323
|
+
|
|
324
|
+
# image shape
|
|
325
|
+
self.naxis1 = naxis1
|
|
326
|
+
self.naxis2 = naxis2
|
|
327
|
+
self.bitpix = bitpix
|
|
328
|
+
self.bias = None
|
|
329
|
+
self.gain = None
|
|
330
|
+
self.readout_noise = None
|
|
331
|
+
self.dark = None
|
|
332
|
+
self.flatfield = None
|
|
333
|
+
self.data_model = None
|
|
334
|
+
self.seed = seed
|
|
335
|
+
self._rng = np.random.default_rng(seed)
|
|
336
|
+
|
|
337
|
+
# check that each input parameter is a Quantity with the expected units,
|
|
338
|
+
parameter_list = [
|
|
339
|
+
ImageParameter(
|
|
340
|
+
name="bias",
|
|
341
|
+
quantity=bias,
|
|
342
|
+
expected_unit=Unit('adu'),
|
|
343
|
+
expected_type=int),
|
|
344
|
+
ImageParameter(
|
|
345
|
+
name="gain",
|
|
346
|
+
quantity=gain,
|
|
347
|
+
expected_unit=Unit('electron') / Unit('adu'),
|
|
348
|
+
expected_type=float),
|
|
349
|
+
ImageParameter(
|
|
350
|
+
name="readout_noise",
|
|
351
|
+
quantity=readout_noise,
|
|
352
|
+
expected_unit=Unit('adu'),
|
|
353
|
+
expected_type=float),
|
|
354
|
+
ImageParameter(
|
|
355
|
+
name="dark",
|
|
356
|
+
quantity=dark,
|
|
357
|
+
expected_unit=Unit('adu'),
|
|
358
|
+
expected_type=float),
|
|
359
|
+
ImageParameter(
|
|
360
|
+
name="flatfield",
|
|
361
|
+
quantity=flatfield,
|
|
362
|
+
expected_unit=None,
|
|
363
|
+
expected_type=float),
|
|
364
|
+
ImageParameter(
|
|
365
|
+
name="data_model",
|
|
366
|
+
quantity=data_model,
|
|
367
|
+
expected_unit=Unit('adu'),
|
|
368
|
+
expected_type=float),
|
|
369
|
+
]
|
|
370
|
+
# check that the parameter values are defined either as a
|
|
371
|
+
# single number (integer or float) or as a numpy.array with
|
|
372
|
+
# the expected shape
|
|
373
|
+
for p in parameter_list:
|
|
374
|
+
if isinstance(p.value, (int, float)):
|
|
375
|
+
# constant value for the full array
|
|
376
|
+
if p.unit is None:
|
|
377
|
+
setattr(self, p.name, np.full(shape=(naxis2, naxis1), fill_value=float(p.value)))
|
|
378
|
+
else:
|
|
379
|
+
setattr(self, p.name, np.full(shape=(naxis2, naxis1), fill_value=p.value) * p.unit)
|
|
380
|
+
elif isinstance(p.value, np.ndarray):
|
|
381
|
+
if p.value.ndim != 2:
|
|
382
|
+
raise ValueError(f"Unexpected number of dimensions {p.value.ndim} for {p.name}")
|
|
383
|
+
if not isinstance(p.value[0, 0], (int, float)):
|
|
384
|
+
raise ValueError(f"Unexpected value {p.value[0, 0]} for {p.name}: it should be an int or float")
|
|
385
|
+
naxis2_, naxis1_ = p.value.shape
|
|
386
|
+
if naxis1_ == naxis1 and naxis2_ == naxis2:
|
|
387
|
+
# array of the expected shape
|
|
388
|
+
if p.unit is None:
|
|
389
|
+
setattr(self, p.name, p.value)
|
|
390
|
+
else:
|
|
391
|
+
setattr(self, p.name, p.value * p.unit)
|
|
392
|
+
else:
|
|
393
|
+
msg = (f"Parameter {p.name}: NAXIS1={naxis1_}, NAXIS2={naxis2_} "
|
|
394
|
+
f"are not compatible with expected values NAXIS1={naxis1}, NAXIS2={naxis2}")
|
|
395
|
+
raise ValueError(msg)
|
|
396
|
+
else:
|
|
397
|
+
raise ValueError(f"Unexpected {p.name=} with {type(p.value)=}")
|
|
398
|
+
|
|
399
|
+
def __repr__(self):
|
|
400
|
+
output = f'{self.__class__.__name__}(\n'
|
|
401
|
+
output += f' naxis1={self.naxis1},\n'
|
|
402
|
+
output += f' naxis2={self.naxis2},\n'
|
|
403
|
+
output += f' bitpix={self.bitpix},\n'
|
|
404
|
+
for parameter in VALID_PARAMETERS:
|
|
405
|
+
output += f' {parameter}={getattr(self, parameter)!r},\n'
|
|
406
|
+
output += f' seed={self.seed},\n'
|
|
407
|
+
output += ')'
|
|
408
|
+
return output
|
|
409
|
+
|
|
410
|
+
def _precheck_set_function(self, parameter, quantity, region):
|
|
411
|
+
"""Auxiliary function to check function inputs.
|
|
412
|
+
|
|
413
|
+
This function checks whether the parameters provided to
|
|
414
|
+
the functions in charge of defining pixels values are correct.
|
|
415
|
+
|
|
416
|
+
Parameters
|
|
417
|
+
----------
|
|
418
|
+
parameter : str
|
|
419
|
+
CCD parameter to set. It must be any of VALID_PARAMETERS.
|
|
420
|
+
quantity : Quantity
|
|
421
|
+
Float or numpy.array with units (except for flatfield).
|
|
422
|
+
region : SliceRegion2D or None
|
|
423
|
+
Region in which to define de parameter. When it is None, it
|
|
424
|
+
indicates that 'value' should be set for all pixels.
|
|
425
|
+
|
|
426
|
+
Returns
|
|
427
|
+
-------
|
|
428
|
+
region : SliceRegion2D
|
|
429
|
+
Updated region in which to define de parameter. When the input
|
|
430
|
+
value is None, the returned region will be the full frame.
|
|
431
|
+
"""
|
|
432
|
+
full_frame = SliceRegion2D(f"[1:{self.naxis1}, 1:{self.naxis2}]", mode='fits')
|
|
433
|
+
# protections
|
|
434
|
+
if parameter not in VALID_PARAMETERS:
|
|
435
|
+
raise RuntimeError(f"Invalid {parameter=}\n{VALID_PARAMETERS=}")
|
|
436
|
+
if region is None:
|
|
437
|
+
region = full_frame
|
|
438
|
+
else:
|
|
439
|
+
if isinstance(region, SliceRegion2D):
|
|
440
|
+
# check region is within NAXIS1, NAXIS2 rectangle
|
|
441
|
+
if not region.within(full_frame):
|
|
442
|
+
raise RuntimeError(f"Region {region=} outside of frame {full_frame=}")
|
|
443
|
+
else:
|
|
444
|
+
raise TypeError(f"The parameter {region=} must be an instance of SliceRegion2D")
|
|
445
|
+
|
|
446
|
+
if parameter == "gain":
|
|
447
|
+
expected_units = Unit('electron') / Unit('adu')
|
|
448
|
+
elif parameter == "flatfield":
|
|
449
|
+
expected_units = None
|
|
450
|
+
else:
|
|
451
|
+
expected_units = Unit('adu')
|
|
452
|
+
if expected_units is not None:
|
|
453
|
+
if not isinstance(quantity, Quantity):
|
|
454
|
+
raise TypeError(f"{parameter} must be a Quantity: {expected_units=}")
|
|
455
|
+
if quantity.unit != expected_units:
|
|
456
|
+
raise ValueError(f"{quantity.unit=} != {expected_units=}")
|
|
457
|
+
|
|
458
|
+
return region
|
|
459
|
+
|
|
460
|
+
def set_constant(self, parameter, constant, region=None):
|
|
461
|
+
"""
|
|
462
|
+
Set the value of a particular parameter to a constant value.
|
|
463
|
+
|
|
464
|
+
The parameter can be any of VALID_PARAMETERS. The constant
|
|
465
|
+
value can be employed for all pixels or in a specific region.
|
|
466
|
+
|
|
467
|
+
Parameters
|
|
468
|
+
----------
|
|
469
|
+
parameter : str
|
|
470
|
+
CCD parameter to set. It must be any of VALID_PARAMETERS.
|
|
471
|
+
constant : Quantity or float
|
|
472
|
+
Constant value for the parameter.
|
|
473
|
+
region : SliceRegion2D or None
|
|
474
|
+
Region in which to define de parameter. When it is None, it
|
|
475
|
+
indicates that 'value' should be set for all pixels.
|
|
476
|
+
"""
|
|
477
|
+
# protections
|
|
478
|
+
parameter = parameter.lower()
|
|
479
|
+
region = self._precheck_set_function(parameter, constant, region)
|
|
480
|
+
if parameter == "flatfield":
|
|
481
|
+
value = constant
|
|
482
|
+
else:
|
|
483
|
+
value = constant.value
|
|
484
|
+
|
|
485
|
+
if not isinstance(value, (int, float)) or isinstance(value, np.ndarray):
|
|
486
|
+
raise TypeError("The parameter 'quantity' must be a single number")
|
|
487
|
+
|
|
488
|
+
# set parameter
|
|
489
|
+
try:
|
|
490
|
+
if isinstance(constant, Quantity):
|
|
491
|
+
getattr(self, parameter)[region.python] = constant
|
|
492
|
+
else:
|
|
493
|
+
getattr(self, parameter)[region.python] = float(constant)
|
|
494
|
+
except AttributeError:
|
|
495
|
+
raise RuntimeError(f"Unexpected parameter '{parameter}' for SimulateCCDExposure")
|
|
496
|
+
|
|
497
|
+
def set_array2d(self, parameter, array2d, region=None):
|
|
498
|
+
"""
|
|
499
|
+
Set the value of a particular parameter to a 2D array.
|
|
500
|
+
|
|
501
|
+
The parameter can be any of VALID_PARAMETERS. The 2D array
|
|
502
|
+
may correspond to the whole simulated array or to a specific
|
|
503
|
+
region.
|
|
504
|
+
|
|
505
|
+
Parameters
|
|
506
|
+
----------
|
|
507
|
+
parameter : str
|
|
508
|
+
CCD parameter to set. It must be any of VALID_PARAMETERS.
|
|
509
|
+
array2d : Quantity
|
|
510
|
+
Array of values to be used to define 'parameter'.
|
|
511
|
+
region : SliceRegion2D
|
|
512
|
+
Region in which to define de parameter. When it is None, it
|
|
513
|
+
indicates that 'quantity' has the same shape as the simulated
|
|
514
|
+
image.
|
|
515
|
+
"""
|
|
516
|
+
# protections
|
|
517
|
+
parameter = parameter.lower()
|
|
518
|
+
region = self._precheck_set_function(parameter, array2d, region)
|
|
519
|
+
if parameter == "flatfield":
|
|
520
|
+
value = array2d.astype(float)
|
|
521
|
+
else:
|
|
522
|
+
value = array2d.value
|
|
523
|
+
|
|
524
|
+
if not isinstance(value, np.ndarray):
|
|
525
|
+
raise TypeError("The parameter 'quantity' must be a numpy array of quantities")
|
|
526
|
+
naxis2_, naxis1_ = value.shape
|
|
527
|
+
if self.naxis1 != naxis1_ or self.naxis2 != naxis2_:
|
|
528
|
+
print(f"{value.shape=}")
|
|
529
|
+
raise ValueError(f"The parameter 'quantity' must have shape ({self.naxis1=}, {self.naxis2=})")
|
|
530
|
+
|
|
531
|
+
# set parameter
|
|
532
|
+
try:
|
|
533
|
+
getattr(self, parameter)[region.python] = array2d[region.python]
|
|
534
|
+
except AttributeError:
|
|
535
|
+
raise RuntimeError(f"Unexpected parameter '{parameter}' for SimulateCCDExposure")
|
|
536
|
+
|
|
537
|
+
def run(self, imgtype, method="Poisson", seed=None):
|
|
538
|
+
"""
|
|
539
|
+
Execute the generation of the simulated CCD exposure.
|
|
540
|
+
|
|
541
|
+
This function generates an image of type 'imgtype', which must
|
|
542
|
+
be one of VALID_IMAGE_TYPES. The signal can be generated using
|
|
543
|
+
either method: Poisson or Gaussian. It is possible to set the
|
|
544
|
+
seed in order to initialize the random number generator.
|
|
545
|
+
|
|
546
|
+
Parameters
|
|
547
|
+
----------
|
|
548
|
+
imgtype : str
|
|
549
|
+
Type of image to be generated. It must be one of
|
|
550
|
+
VALID_IMAGE_TYPES.
|
|
551
|
+
method : str
|
|
552
|
+
Method to generate the simulated data. It must be one of
|
|
553
|
+
VALID_METHODS.
|
|
554
|
+
seed : int, optional
|
|
555
|
+
Seed for the random number generator. The default is None.
|
|
556
|
+
|
|
557
|
+
Returns
|
|
558
|
+
-------
|
|
559
|
+
result : SimulatedCCDResult
|
|
560
|
+
Instance of SimulatedCCDResult to store the simulated image
|
|
561
|
+
and the associated parameters employed to define how to
|
|
562
|
+
generate the simulated CCD exposure.
|
|
563
|
+
"""
|
|
564
|
+
# protections
|
|
565
|
+
imgtype = imgtype.lower()
|
|
566
|
+
method = method.lower()
|
|
567
|
+
if imgtype not in VALID_IMAGE_TYPES:
|
|
568
|
+
raise ValueError(f'Unexpected {imgtype=}.\nValid image types: {VALID_IMAGE_TYPES}')
|
|
569
|
+
if method not in VALID_METHODS:
|
|
570
|
+
raise ValueError(f'Unexpected {method=}.\nValid methods: {VALID_METHODS}')
|
|
571
|
+
|
|
572
|
+
if seed is not None:
|
|
573
|
+
self._rng = np.random.default_rng(seed)
|
|
574
|
+
|
|
575
|
+
# initialize result instance
|
|
576
|
+
result = SimulatedCCDResult(
|
|
577
|
+
data=None,
|
|
578
|
+
unit=Unit('adu'),
|
|
579
|
+
imgtype=imgtype,
|
|
580
|
+
method=method,
|
|
581
|
+
origin=self,
|
|
582
|
+
seed=seed
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
# BIAS and Readout Noise
|
|
586
|
+
if np.isnan(self.bias.value).any():
|
|
587
|
+
raise ValueError(f"The parameter 'bias' contains NaN")
|
|
588
|
+
if np.isnan(self.readout_noise.value).any():
|
|
589
|
+
raise ValueError(f"The parameter 'readout_noise' contains NaN")
|
|
590
|
+
image2d = self._rng.normal(
|
|
591
|
+
loc=self.bias.value,
|
|
592
|
+
scale=self.readout_noise.value
|
|
593
|
+
)
|
|
594
|
+
if imgtype == "bias":
|
|
595
|
+
result.data = image2d
|
|
596
|
+
return result
|
|
597
|
+
|
|
598
|
+
# DARK
|
|
599
|
+
if np.isnan(self.dark.value).any():
|
|
600
|
+
raise ValueError(f"The parameter 'dark' contains NaN")
|
|
601
|
+
image2d += self.dark.value
|
|
602
|
+
if imgtype == "dark":
|
|
603
|
+
result.data = image2d
|
|
604
|
+
return result
|
|
605
|
+
|
|
606
|
+
# OBJECT
|
|
607
|
+
if np.isnan(self.flatfield).any():
|
|
608
|
+
raise ValueError(f"The parameter 'flatfield' contains NaN")
|
|
609
|
+
if np.isnan(self.data_model.value).any():
|
|
610
|
+
raise ValueError(f"The parameter 'data_model' contains NaN")
|
|
611
|
+
if np.isnan(self.gain.value).any():
|
|
612
|
+
raise ValueError(f"The parameter 'gain' contains NaN")
|
|
613
|
+
if method.lower() == "poisson":
|
|
614
|
+
# transform data_model from ADU to electrons,
|
|
615
|
+
# generate Poisson distribution
|
|
616
|
+
# and transform back from electrons to ADU
|
|
617
|
+
image2d += self.flatfield * self._rng.poisson(
|
|
618
|
+
self.data_model.value * self.gain.value
|
|
619
|
+
) / self.gain.value
|
|
620
|
+
elif method.lower() == "gaussian":
|
|
621
|
+
image2d += self.flatfield * self._rng.normal(
|
|
622
|
+
loc=self.data_model.value,
|
|
623
|
+
scale=np.sqrt(self.data_model.value/self.gain.value)
|
|
624
|
+
)
|
|
625
|
+
else:
|
|
626
|
+
raise RuntimeError(f"Unknown method: {method}")
|
|
627
|
+
|
|
628
|
+
# round to integer
|
|
629
|
+
image2d = np.round(image2d)
|
|
630
|
+
# saturated pixels
|
|
631
|
+
if self.bitpix in [8, 16, 32, 64]:
|
|
632
|
+
saturation = 2**self.bitpix - 1
|
|
633
|
+
image2d[self.data_model.value >= saturation] = saturation
|
|
634
|
+
image2d[image2d >= saturation] = saturation
|
|
635
|
+
else:
|
|
636
|
+
raise ValueError(f"The parameter 'bitpix' must be one of {VALID_BITPIX.keys()}")
|
|
637
|
+
|
|
638
|
+
result.data = image2d.astype(VALID_BITPIX[self.bitpix])
|
|
639
|
+
return result
|
teareduce/sliceregion.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright 2022-
|
|
2
|
+
# Copyright 2022-2025 Universidad Complutense de Madrid
|
|
3
3
|
#
|
|
4
4
|
# This file is part of teareduce
|
|
5
5
|
#
|
|
@@ -7,18 +7,60 @@
|
|
|
7
7
|
# License-Filename: LICENSE.txt
|
|
8
8
|
#
|
|
9
9
|
|
|
10
|
+
import numpy as np
|
|
11
|
+
import re
|
|
10
12
|
|
|
11
13
|
class SliceRegion1D:
|
|
12
14
|
"""Store indices for slicing of 1D regions.
|
|
13
15
|
|
|
14
16
|
The attributes .python and .fits provide the indices following
|
|
15
17
|
the Python and the FITS convention, respectively.
|
|
16
|
-
|
|
18
|
+
|
|
19
|
+
Attributes
|
|
20
|
+
----------
|
|
21
|
+
fits : slice
|
|
22
|
+
1D slice following the FITS convention.
|
|
23
|
+
python : slice
|
|
24
|
+
1D slice following the Python convention.
|
|
25
|
+
mode : str
|
|
26
|
+
Convention mode employed to define the slice.
|
|
27
|
+
The two possible modes are 'fits' and 'python'.
|
|
28
|
+
fits_section : str
|
|
29
|
+
Resulting slice section in FITS convention: '[num1:num2]'.
|
|
30
|
+
This string is defined after successfully initializing
|
|
31
|
+
the SliceRegion1D instance.
|
|
32
|
+
|
|
33
|
+
Methods
|
|
34
|
+
-------
|
|
35
|
+
within(other)
|
|
36
|
+
Check if slice 'other' is within the parent slice.
|
|
17
37
|
"""
|
|
18
38
|
|
|
19
39
|
def __init__(self, region, mode=None):
|
|
40
|
+
"""Initialize SliceRegion1D.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
region : slice or str
|
|
45
|
+
Slice region. It can be provided as np.s_[num1:num2],
|
|
46
|
+
as slice(num1, num2) or as a string '[num1:num2]'
|
|
47
|
+
mode : str
|
|
48
|
+
Convention mode employed to define the slice.
|
|
49
|
+
The two possible modes are 'fits' and 'python'.
|
|
50
|
+
"""
|
|
51
|
+
if isinstance(region, str):
|
|
52
|
+
pattern = r'^\s*\[\s*\d+\s*:\s*\d+\s*\]\s*$'
|
|
53
|
+
if not re.match(pattern, region):
|
|
54
|
+
raise ValueError(f"Invalid {region!r}. It must match '[num:num]'")
|
|
55
|
+
# extract numbers and generate np.s_[num:num]
|
|
56
|
+
numbers_str = re.findall(r'\d+', region)
|
|
57
|
+
numbers_int = list(map(int, numbers_str))
|
|
58
|
+
region = np.s_[numbers_int[0]:numbers_int[1]]
|
|
59
|
+
|
|
20
60
|
if isinstance(region, slice):
|
|
21
|
-
|
|
61
|
+
for number in [slice.start, slice.stop]:
|
|
62
|
+
if number is None:
|
|
63
|
+
raise ValueError(f'Invalid {slice!r}: you must specify start:stop in slice by number')
|
|
22
64
|
else:
|
|
23
65
|
raise ValueError(f'Object {region} of type {type(region)} is not a slice')
|
|
24
66
|
|
|
@@ -44,6 +86,9 @@ class SliceRegion1D:
|
|
|
44
86
|
else:
|
|
45
87
|
raise ValueError(errmsg)
|
|
46
88
|
|
|
89
|
+
s = self.fits
|
|
90
|
+
self.fits_section = f'[{s.start}:{s.stop}]'
|
|
91
|
+
|
|
47
92
|
def __eq__(self, other):
|
|
48
93
|
return self.fits == other.fits and self.python == other.python
|
|
49
94
|
|
|
@@ -56,17 +101,34 @@ class SliceRegion1D:
|
|
|
56
101
|
f'{self.python!r}, mode="python")')
|
|
57
102
|
|
|
58
103
|
def within(self, other):
|
|
104
|
+
"""Determine if slice 'other' is within the parent slice.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
other : SliceRegion1D
|
|
109
|
+
New instance for which we want to determine
|
|
110
|
+
if it is within the parent SliceRegion1D instance.
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
result : bool
|
|
115
|
+
Return True if 'other' is within the parent slice.
|
|
116
|
+
False otherwise.
|
|
117
|
+
"""
|
|
59
118
|
if isinstance(other, self.__class__):
|
|
60
119
|
pass
|
|
61
120
|
else:
|
|
62
121
|
raise ValueError(f'Object {other} of type {type(other)} is not a {self.__class__.__name__}')
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if
|
|
68
|
-
return
|
|
69
|
-
|
|
122
|
+
|
|
123
|
+
s = self.python
|
|
124
|
+
s_other = other.python
|
|
125
|
+
result = False
|
|
126
|
+
if s.start < s_other.start:
|
|
127
|
+
return result
|
|
128
|
+
if s.stop > s_other.stop:
|
|
129
|
+
return result
|
|
130
|
+
result = True
|
|
131
|
+
return result
|
|
70
132
|
|
|
71
133
|
|
|
72
134
|
class SliceRegion2D:
|
|
@@ -75,23 +137,60 @@ class SliceRegion2D:
|
|
|
75
137
|
The attributes .python and .fits provide the indices following
|
|
76
138
|
the Python and the FITS convention, respectively.
|
|
77
139
|
|
|
78
|
-
|
|
140
|
+
Attributes
|
|
141
|
+
----------
|
|
142
|
+
fits : slice
|
|
143
|
+
1D slice following the FITS convention.
|
|
144
|
+
python : slice
|
|
145
|
+
1D slice following the Python convention.
|
|
146
|
+
mode : str
|
|
147
|
+
Convention mode employed to define the slice.
|
|
148
|
+
The two possible modes are 'fits' and 'python'.
|
|
149
|
+
fits_section : str
|
|
150
|
+
Resulting slice section in FITS convention:
|
|
151
|
+
'[num1:num2,num3:num4]'. This string is defined after
|
|
152
|
+
successfully initializing the SliceRegion2D instance.
|
|
153
|
+
|
|
154
|
+
Methods
|
|
155
|
+
-------
|
|
156
|
+
within(other)
|
|
157
|
+
Check if slice 'other' is within the parent slice."""
|
|
79
158
|
|
|
80
159
|
def __init__(self, region, mode=None):
|
|
81
|
-
|
|
82
|
-
raise ValueError(f'This class {self.__class__.__name__} '
|
|
83
|
-
'only handles 2D regions')
|
|
160
|
+
"""Initialize SliceRegion1D.
|
|
84
161
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
162
|
+
Parameters
|
|
163
|
+
----------
|
|
164
|
+
region : slice or str
|
|
165
|
+
Slice region. It can be provided as np.s_[num1:num2, num3:num4],
|
|
166
|
+
as a tuple (slice(num1, num2), slice(num3, num4)),
|
|
167
|
+
or as a string '[num1:num2, num3:num4]'
|
|
168
|
+
mode : str
|
|
169
|
+
Convention mode employed to define the slice.
|
|
170
|
+
The two possible modes are 'fits' and 'python'.
|
|
171
|
+
"""
|
|
172
|
+
if isinstance(region, str):
|
|
173
|
+
pattern = r'^\s*\[\s*\d+\s*:\s*\d+\s*,\s*\d+\s*:\s*\d+\s*\]\s*$'
|
|
174
|
+
if not re.match(pattern, region):
|
|
175
|
+
raise ValueError(f"Invalid {region!r}. It must match '[num:num, num:num]'")
|
|
176
|
+
# extract numbers and generate np.s_[num:num, num:num]
|
|
177
|
+
numbers_str = re.findall(r'\d+', region)
|
|
178
|
+
numbers_int = list(map(int, numbers_str))
|
|
179
|
+
region = np.s_[numbers_int[0]:numbers_int[1], numbers_int[2]:numbers_int[3]]
|
|
91
180
|
|
|
92
|
-
if
|
|
93
|
-
|
|
94
|
-
|
|
181
|
+
if isinstance(region, tuple) and len(region) == 2:
|
|
182
|
+
s1, s2 = region
|
|
183
|
+
for item in [s1, s2]:
|
|
184
|
+
if isinstance(item, slice):
|
|
185
|
+
for number in [s1.start, s1.stop, s2.start, s2.stop]:
|
|
186
|
+
if number is None:
|
|
187
|
+
raise ValueError(f'Invalid {item!r}: you must specify start:stop in slice by number')
|
|
188
|
+
else:
|
|
189
|
+
raise ValueError(f'Object {item} of type {type(item)} is not a slice')
|
|
190
|
+
if s1.step not in [1, None] or s2.step not in [1, None]:
|
|
191
|
+
raise ValueError(f'This class {self.__class__.__name__} does not handle step != 1')
|
|
192
|
+
else:
|
|
193
|
+
raise ValueError(f'This class {self.__class__.__name__} only handles 2D regions')
|
|
95
194
|
|
|
96
195
|
errmsg = f'Invalid mode={mode}. Only "FITS" or "Python" (case insensitive) are valid'
|
|
97
196
|
if mode is None:
|
|
@@ -116,7 +215,7 @@ class SliceRegion2D:
|
|
|
116
215
|
raise ValueError(errmsg)
|
|
117
216
|
|
|
118
217
|
s1, s2 = self.fits
|
|
119
|
-
self.fits_section = f'[{s1.start}:{s1.stop},
|
|
218
|
+
self.fits_section = f'[{s1.start}:{s1.stop},{s2.start}:{s2.stop}]'
|
|
120
219
|
|
|
121
220
|
def __eq__(self, other):
|
|
122
221
|
return self.fits == other.fits and self.python == other.python
|
|
@@ -130,6 +229,20 @@ class SliceRegion2D:
|
|
|
130
229
|
f'{self.python!r}, mode="python")')
|
|
131
230
|
|
|
132
231
|
def within(self, other):
|
|
232
|
+
"""Determine if slice 'other' is within the parent slice.
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
other : SliceRegion2D
|
|
237
|
+
New instance for which we want to determine
|
|
238
|
+
if it is within the parent SliceRegion2D instance.
|
|
239
|
+
|
|
240
|
+
Returns
|
|
241
|
+
-------
|
|
242
|
+
result : bool
|
|
243
|
+
Return True if 'other' is within the parent slice.
|
|
244
|
+
False otherwise.
|
|
245
|
+
"""
|
|
133
246
|
if isinstance(other, self.__class__):
|
|
134
247
|
pass
|
|
135
248
|
else:
|
|
@@ -137,12 +250,14 @@ class SliceRegion2D:
|
|
|
137
250
|
|
|
138
251
|
s1, s2 = self.python
|
|
139
252
|
s1_other, s2_other = other.python
|
|
253
|
+
result = False
|
|
140
254
|
if s1.start < s1_other.start:
|
|
141
|
-
return
|
|
255
|
+
return result
|
|
142
256
|
if s1.stop > s1_other.stop:
|
|
143
|
-
return
|
|
257
|
+
return result
|
|
144
258
|
if s2.start < s2_other.start:
|
|
145
|
-
return
|
|
259
|
+
return result
|
|
146
260
|
if s2.stop > s2_other.stop:
|
|
147
|
-
return
|
|
148
|
-
|
|
261
|
+
return result
|
|
262
|
+
result = True
|
|
263
|
+
return result
|
teareduce/version.py
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 Universidad Complutense de Madrid
|
|
3
|
+
#
|
|
4
|
+
# This file is part of teareduce
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: GPL-3.0+
|
|
7
|
+
# License-Filename: LICENSE.txt
|
|
8
|
+
#
|
|
9
|
+
|
|
10
|
+
from astropy.io import fits
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def write_array_to_fits(data, filename, overwrite):
|
|
14
|
+
"""Write a single array to a FITS file.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
data : numpy.ndarray
|
|
19
|
+
Array to write.
|
|
20
|
+
filename : str
|
|
21
|
+
Path to the FITS file.
|
|
22
|
+
overwrite : bool
|
|
23
|
+
Whether to overwrite the existing FITS file.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
hdu = fits.PrimaryHDU(data)
|
|
27
|
+
hdulist = fits.HDUList([hdu])
|
|
28
|
+
hdulist.writeto(filename, overwrite=overwrite)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: teareduce
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Utilities for astronomical data reduction
|
|
5
5
|
Author-email: Nicolás Cardiel <cardiel@ucm.es>
|
|
6
6
|
License: GPL-3.0-or-later
|
|
@@ -21,14 +21,14 @@ Requires-Python: >=3.8
|
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE.txt
|
|
23
23
|
Requires-Dist: astropy
|
|
24
|
-
Requires-Dist:
|
|
24
|
+
Requires-Dist: importlib_resources
|
|
25
25
|
Requires-Dist: lmfit
|
|
26
26
|
Requires-Dist: matplotlib
|
|
27
|
-
Requires-Dist: numpy
|
|
27
|
+
Requires-Dist: numpy>=1.20
|
|
28
28
|
Requires-Dist: scipy
|
|
29
29
|
Requires-Dist: tqdm
|
|
30
30
|
Provides-Extra: test
|
|
31
|
-
Requires-Dist: pytest
|
|
31
|
+
Requires-Dist: pytest; extra == "test"
|
|
32
32
|
|
|
33
33
|
# teareduce
|
|
34
34
|
Utilities for astronomical data reduction
|
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
teareduce/__init__.py,sha256=
|
|
1
|
+
teareduce/__init__.py,sha256=MLlKpaDRjG33kfn52OHonENfsHYZqlGBmQFhk7_CUR0,1214
|
|
2
2
|
teareduce/avoid_astropy_warnings.py,sha256=2YgQ47pxsKYWDxUtzyEOfh3Is3aAHHmjJkuOa1JCDN4,648
|
|
3
3
|
teareduce/correct_pincushion_distortion.py,sha256=Xpt03jtmJMyqik4ta95zMRE3Z6dVfzzHI2z5IDbtnMk,1685
|
|
4
4
|
teareduce/cosmicrays.py,sha256=y8f5KLUsiOeOkArGnZVi_q11chsgnYlwtxbSD_a9EAQ,24209
|
|
5
5
|
teareduce/ctext.py,sha256=8QP_KW7ueJ34IUyduVpy7nk-x0If5eilawf87icDMJA,2084
|
|
6
6
|
teareduce/draw_rectangle.py,sha256=xlwcKIkl7e0U6sa9zWZr8t_WuWAte_UKIqCwZQ41x4Q,1922
|
|
7
|
-
teareduce/elapsed_time.py,sha256=
|
|
8
|
-
teareduce/imshow.py,sha256=
|
|
7
|
+
teareduce/elapsed_time.py,sha256=Nv77LjxvfuL3EECU7F0nMzEjD-qkdQq0C5IoRGzO-5k,1403
|
|
8
|
+
teareduce/imshow.py,sha256=hDefLOL2R_pXkyHvruzaXrj9T9_SL7y7qjgYDmUuV7w,4861
|
|
9
9
|
teareduce/numsplines.py,sha256=1PpG-frdc9Qz3VRbC7XyZFWKmhus05ID4REtFnWDmUo,8049
|
|
10
10
|
teareduce/peaks_spectrum.py,sha256=YPCJz8skJmIjWYqT7ZhBJGhnqPayFwy5xb7I9OHlUZI,9890
|
|
11
11
|
teareduce/polfit.py,sha256=CGsrRsz_Du2aKxOcgXi36lpAZO04JyqCCUaxhC0C-Mk,14281
|
|
12
12
|
teareduce/robust_std.py,sha256=dk1G3VgiplZzmSfL7qWniEZ-5c3novHnBpRPCM77u84,1084
|
|
13
13
|
teareduce/sdistortion.py,sha256=5ZsZn4vD5Sw2aoqO8-NIOH7H89Zmh7ZDkow6YbAotHU,5916
|
|
14
|
-
teareduce/
|
|
14
|
+
teareduce/simulateccdexposure.py,sha256=3SNPBOP09vnwoGqdf97f9s21WmTIw6skafZ6qRkJUrw,24642
|
|
15
|
+
teareduce/sliceregion.py,sha256=XVyUuY9P1ZuTiE5PARTmA51KA4bfHVFWy_Wt_blqd80,9450
|
|
15
16
|
teareduce/statsummary.py,sha256=mtaM21d5aHvtLjCt_SSDMvD_fjI5nK21ZqxuDtcvldI,5426
|
|
16
|
-
teareduce/version.py,sha256=
|
|
17
|
+
teareduce/version.py,sha256=8qbLEf4-_Nt3i3QYFA-69LhL_9dbKjM11e4aC50LJ8U,303
|
|
17
18
|
teareduce/wavecal.py,sha256=iiKG_RPW2CllwZxG5fTsyckE0Ec_IeZ6v7v2cQt6OeU,68706
|
|
19
|
+
teareduce/write_array_to_fits.py,sha256=kWDrEH9coJ1yIu56oQJpWtDqJL4c8HGmssE9jle4e94,617
|
|
18
20
|
teareduce/zscale.py,sha256=HuPYagTW55D7RtjPGc7HcibQlCx5oqLYHKoM6WEHG2g,1161
|
|
19
|
-
teareduce-0.
|
|
20
|
-
teareduce-0.
|
|
21
|
-
teareduce-0.
|
|
22
|
-
teareduce-0.
|
|
23
|
-
teareduce-0.
|
|
21
|
+
teareduce-0.4.0.dist-info/LICENSE.txt,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
22
|
+
teareduce-0.4.0.dist-info/METADATA,sha256=iFb3sEiauuYWrXW7ecoTT3P7DCJSBNrrR513NZaWjno,2302
|
|
23
|
+
teareduce-0.4.0.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
|
24
|
+
teareduce-0.4.0.dist-info/top_level.txt,sha256=7OkwtX9zNRkGJ7ACgjk4ESgC74qUYcS5O2qcO0v-Si4,10
|
|
25
|
+
teareduce-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|