teareduce 0.4.1__py3-none-any.whl → 0.4.3__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 CHANGED
@@ -14,6 +14,8 @@ from .ctext import ctext
14
14
  from .draw_rectangle import draw_rectangle
15
15
  from .elapsed_time import elapsed_time
16
16
  from .elapsed_time import elapsed_time_since
17
+ from .histogram1d import hist_step
18
+ from .histogram1d import plot_hist_step
17
19
  from .imshow import imshow
18
20
  from .imshow import imshowme
19
21
  from .numsplines import AdaptiveLSQUnivariateSpline
@@ -21,12 +23,12 @@ from .peaks_spectrum import find_peaks_spectrum, refine_peaks_spectrum
21
23
  from .polfit import polfit_residuals, polfit_residuals_with_sigma_rejection
22
24
  from .robust_std import robust_std
23
25
  from .sdistortion import fit_sdistortion
24
- from .sliceregion import SliceRegion1D, SliceRegion2D
25
- from .statsummary import ifc_statsummary, statsummary
26
26
  from .simulateccdexposure import SimulateCCDExposure
27
- from .version import version
27
+ from .sliceregion import SliceRegion1D, SliceRegion2D, SliceRegion3D
28
+ from .statsummary import ifc_statsummary, statsummary
29
+ from .version import VERSION
28
30
  from .wavecal import TeaWaveCalibration, apply_wavecal_ccddata
29
31
  from .write_array_to_fits import write_array_to_fits
30
32
  from .zscale import zscale
31
33
 
32
- __version__ = version
34
+ __version__ = VERSION
teareduce/cosmicrays.py CHANGED
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2022-2024 Universidad Complutense de Madrid
2
+ # Copyright 2022-2025 Universidad Complutense de Madrid
3
3
  #
4
4
  # This file is part of teareduce
5
5
  #
@@ -20,14 +20,17 @@ from .robust_std import robust_std
20
20
  from .sliceregion import SliceRegion2D
21
21
 
22
22
 
23
- def cr2images(data1, data2=None, ioffx=0, ioffy=0,
23
+ def cr2images(data1, data2=None,
24
+ single_mode=False,
25
+ ioffx=0, ioffy=0,
24
26
  tsigma_peak=10, tsigma_tail=3, maxsize=None,
25
27
  list_skipped_regions=None,
26
28
  image_region=None,
27
29
  median_size=None,
28
30
  return_masks=False,
29
31
  debug_level=0,
30
- zoom_region_imshow=None):
32
+ zoom_region_imshow=None,
33
+ aspect='equal'):
31
34
  """Remove cosmic rays from differences between 2 images.
32
35
 
33
36
  The input images must have the same shape. If only 1 image
@@ -42,6 +45,10 @@ def cr2images(data1, data2=None, ioffx=0, ioffy=0,
42
45
  Second image. If None, a median filtered version of 'data1'
43
46
  is employed. In this case, the parameter 'median_size' must
44
47
  be properly set.
48
+ single_mode : bool
49
+ If True, the function is used in single mode, i.e., only the
50
+ first image is cleaned (default=False). When the second image
51
+ is None, this parameter is automatically set to True.
45
52
  ioffx : int
46
53
  Integer offset (pixels) to place the second image on top of
47
54
  the first image in the horizontal direction (axis=1) in the
@@ -55,9 +62,13 @@ def cr2images(data1, data2=None, ioffx=0, ioffy=0,
55
62
  tsigma_tail : float
56
63
  Times sigma to detect additional pixels affected by cosmic
57
64
  rays.
58
- maxsize : int
59
- Maximum number of pixels affected by a single cosmic ray.
60
- Above this number the detection is ignored.
65
+ maxsize : int or None
66
+ If not None, this parameter sets the maximum number of pixels
67
+ affected by cosmic rays in a single region. If the number of
68
+ pixels affected by a single cosmic ray is larger
69
+ than this value, the region is not cleaned. If None, all
70
+ regions are cleaned regardless of the number of pixels
71
+ affected by single cosmic rays.
61
72
  list_skipped_regions : list or None
62
73
  List of SliceRegion2D instances indicating image regions where
63
74
  detected cosmic rays will not be removed. The indices refer
@@ -80,6 +91,8 @@ def cr2images(data1, data2=None, ioffx=0, ioffy=0,
80
91
  zoom_region_imshow : SliceRegion2D instance or None
81
92
  If not None, display intermediate images, zooming in the
82
93
  indicated region.
94
+ aspect : str
95
+ Aspect ratio of the displayed images. Default is 'equal'.
83
96
 
84
97
  Returns
85
98
  -------
@@ -103,11 +116,9 @@ def cr2images(data1, data2=None, ioffx=0, ioffy=0,
103
116
  if (ioffx != 0) or (ioffy != 0):
104
117
  raise ValueError(f'ERROR: ioffx={ioffx} and ioffy={ioffy} must be zero!')
105
118
  if median_size is None:
106
- raise ValueError(f'ERROR: you must specify median_size when only one image is available')
119
+ raise ValueError('ERROR: you must specify median_size when only one image is available')
107
120
  data2 = ndimage.median_filter(data1, size=median_size)
108
121
  single_mode = True
109
- else:
110
- single_mode = False
111
122
 
112
123
  if list_skipped_regions is not None and image_region is not None:
113
124
  raise ValueError('list_skipped_regions and useful_region are incompatible')
@@ -183,9 +194,6 @@ def cr2images(data1, data2=None, ioffx=0, ioffy=0,
183
194
  if shape1 != shape2:
184
195
  raise ValueError('ERROR: overlapping regions have different shape')
185
196
 
186
- if maxsize is None:
187
- maxsize = shape1[0] * shape1[1]
188
-
189
197
  # difference between the two overlapping regions
190
198
  diff = subdata1 - subdata2
191
199
  if debug_level > 0:
@@ -200,8 +208,20 @@ def cr2images(data1, data2=None, ioffx=0, ioffy=0,
200
208
  print(f'>>> Robust_std: {std:.3f}')
201
209
 
202
210
  # search for positive peaks (CR in data1)
211
+ if debug_level > 0:
212
+ print(f'>>> Searching for positive peaks (CR in data1) with tsigma_peak={tsigma_peak}')
203
213
  labels_pos_peak, no_cr_pos_peak = ndimage.label(diff > median + tsigma_peak * std)
214
+ if debug_level > 0:
215
+ print(f'>>> Found {no_cr_pos_peak} positive peaks')
216
+ # search for additional pixels affected by cosmic rays (tail)
217
+ if debug_level > 0:
218
+ print(f'>>> Searching for positive tails (CR in data1) with tsigma_tail={tsigma_tail}')
204
219
  labels_pos_tail, no_cr_pos_tail = ndimage.label(diff > median + tsigma_tail * std)
220
+ if debug_level > 0:
221
+ print(f'>>> Found {no_cr_pos_tail} positive tails')
222
+ # merge positive peaks and tails
223
+ if debug_level > 0:
224
+ print('>>> Merging positive peaks and tails')
205
225
  # set all CR peak pixels to 1
206
226
  mask_pos_peak = np.zeros_like(labels_pos_peak)
207
227
  mask_pos_peak[labels_pos_peak > 0] = 1
@@ -211,13 +231,21 @@ def cr2images(data1, data2=None, ioffx=0, ioffy=0,
211
231
  mask_pos_clean = np.zeros_like(labels_pos_peak)
212
232
  for icr in np.unique(labels_pos_tail_in_peak):
213
233
  if icr > 0:
214
- npix_affected = np.sum(labels_pos_tail == icr)
215
- if npix_affected <= maxsize:
234
+ if maxsize is None:
216
235
  mask_pos_clean[labels_pos_tail == icr] = 1
236
+ else:
237
+ npix_affected = np.sum(labels_pos_tail == icr)
238
+ if npix_affected <= maxsize:
239
+ mask_pos_clean[labels_pos_tail == icr] = 1
240
+
217
241
  # replace pixels affected by cosmic rays
242
+ if debug_level > 0:
243
+ print(f'>>> Replacing {np.sum(mask_pos_clean)} pixels affected by cosmic rays in data1')
218
244
  data1c = data1.copy()
219
245
  for item in np.argwhere(mask_pos_clean):
220
246
  data1c[item[0] + i1, item[1] + j1] = data2[item[0] + ii1, item[1] + jj1] + median
247
+ if debug_level > 0:
248
+ print('>>> Finished replacing pixels affected by cosmic rays in data1')
221
249
 
222
250
  if single_mode:
223
251
  data2c = data2.copy()
@@ -229,8 +257,20 @@ def cr2images(data1, data2=None, ioffx=0, ioffy=0,
229
257
  no_cr_neg_tail = 0
230
258
  else:
231
259
  # search for negative peaks (CR in data2)
260
+ if debug_level > 0:
261
+ print(f'>>> Searching for negative peaks (CR in data2) with tsigma_peak={tsigma_peak}')
232
262
  labels_neg_peak, no_cr_neg_peak = ndimage.label(diff < median - tsigma_peak * std)
263
+ if debug_level > 0:
264
+ print(f'>>> Found {no_cr_neg_peak} negative peaks')
265
+ # search for additional pixels affected by cosmic rays (tail)
266
+ if debug_level > 0:
267
+ print(f'>>> Searching for negative tails (CR in data2) with tsigma_tail={tsigma_tail}')
233
268
  labels_neg_tail, no_cr_neg_tail = ndimage.label(diff < median - tsigma_tail * std)
269
+ if debug_level > 0:
270
+ print(f'>>> Found {no_cr_neg_tail} negative tails')
271
+ # merge negative peaks and tails
272
+ if debug_level > 0:
273
+ print('>>> Merging negative peaks and tails')
234
274
  # set all CR peak pixels to 1
235
275
  mask_neg_peak = np.zeros_like(labels_neg_peak)
236
276
  mask_neg_peak[labels_neg_peak > 0] = 1
@@ -240,13 +280,20 @@ def cr2images(data1, data2=None, ioffx=0, ioffy=0,
240
280
  mask_neg_clean = np.zeros_like(labels_neg_peak)
241
281
  for icr in np.unique(labels_neg_tail_in_peak):
242
282
  if icr > 0:
243
- npix_affected = np.sum(labels_neg_tail == icr)
244
- if npix_affected <= maxsize:
283
+ if maxsize is None:
245
284
  mask_neg_clean[labels_neg_tail == icr] = 1
285
+ else:
286
+ npix_affected = np.sum(labels_neg_tail == icr)
287
+ if npix_affected <= maxsize:
288
+ mask_neg_clean[labels_neg_tail == icr] = 1
246
289
  # replace pixels affected by cosmic rays
290
+ if debug_level > 0:
291
+ print(f'>>> Replacing {np.sum(mask_neg_clean)} pixels affected by cosmic rays in data2')
247
292
  data2c = data2.copy()
248
293
  for item in np.argwhere(mask_neg_clean):
249
294
  data2c[item[0] + ii1, item[1] + jj1] = data1[item[0] + i1, item[1] + j1] - median
295
+ if debug_level > 0:
296
+ print('>>> Finished replacing pixels affected by cosmic rays in data2')
250
297
 
251
298
  # insert result in arrays with the original data shape
252
299
  mask_data1c = np.zeros((naxis2, naxis1), dtype=int)
@@ -269,7 +316,7 @@ def cr2images(data1, data2=None, ioffx=0, ioffy=0,
269
316
  hmin = max(min(diff.flatten()), median - 10*tsigma_peak*std)
270
317
  hmax = min(max(diff.flatten()), median + 10*tsigma_peak*std)
271
318
  bins = np.linspace(hmin, hmax, 100)
272
- fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(12, 6))
319
+ fig, ax = plt.subplots(ncols=1, nrows=1) # figsize=(12, 6)
273
320
  ax.hist(diff[zoom_region_imshow.python].flatten(), bins=bins)
274
321
  ax.set_xlabel('ADU')
275
322
  ax.set_ylabel('Number of pixels')
@@ -277,10 +324,10 @@ def cr2images(data1, data2=None, ioffx=0, ioffy=0,
277
324
  ax.set_yscale('log')
278
325
  plt.show()
279
326
  # display diff
280
- fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(15, 15*naxis2/naxis1))
327
+ fig, ax = plt.subplots(ncols=1, nrows=1) # figsize=(15, 15*naxis2/naxis1)
281
328
  vmin = median - tsigma_peak * std
282
329
  vmax = median + tsigma_peak * std
283
- imshow(fig, ax, diff, vmin=vmin, vmax=vmax, cmap='seismic')
330
+ imshow(fig, ax, diff, vmin=vmin, vmax=vmax, cmap='seismic', aspect=aspect)
284
331
  ax.set_xlim([zoom_region_imshow.python[1].start, zoom_region_imshow.python[1].stop])
285
332
  ax.set_ylim([zoom_region_imshow.python[0].start, zoom_region_imshow.python[0].stop])
286
333
  ax.set_title('diff: overlapping data1 - data2')
@@ -291,7 +338,7 @@ def cr2images(data1, data2=None, ioffx=0, ioffy=0,
291
338
  data1, labels_pos_peak, labels_pos_tail, labels_pos_tail_in_peak, data1, data1c
292
339
  ]
293
340
  title_list1 = [
294
- 'data1', 'labels_pos_peak', 'labels_pos_tail', 'labels_pos_tail_in_peak',
341
+ 'data1', 'labels_pos_peak', 'labels_pos_tail', 'labels_pos_tail_in_peak',
295
342
  'data1 with C.R.', 'data1c'
296
343
  ]
297
344
  if debug_level == 1:
@@ -305,7 +352,7 @@ def cr2images(data1, data2=None, ioffx=0, ioffy=0,
305
352
  data2, labels_neg_peak, labels_neg_tail, labels_neg_tail_in_peak, data2, data2c
306
353
  ]
307
354
  title_list2 = [
308
- 'data2', 'labels_neg_peak', 'labels_neg_tail', 'labels_neg_tail_in_peak',
355
+ 'data2', 'labels_neg_peak', 'labels_neg_tail', 'labels_neg_tail_in_peak',
309
356
  'data2 with C.R.', 'data2c'
310
357
  ]
311
358
  if debug_level == 1:
@@ -329,7 +376,7 @@ def cr2images(data1, data2=None, ioffx=0, ioffy=0,
329
376
  for iplot, (image, title) in enumerate(zip(image_list, title_list)):
330
377
  imgplot = image[zoom_region_imshow.python]
331
378
  naxis2_, naxis1_ = imgplot.shape
332
- fig, ax = plt.subplots(ncols=1, nrows=1, figsize=(15, 15*naxis2_/naxis1_))
379
+ fig, ax = plt.subplots(ncols=1, nrows=1) # figsize=(15, 15*naxis2_/naxis1_)
333
380
  median_ = np.median(imgplot)
334
381
  std_ = robust_std(imgplot)
335
382
  if std_ == 0:
@@ -340,7 +387,7 @@ def cr2images(data1, data2=None, ioffx=0, ioffy=0,
340
387
  vmin = median_ - 2 * std_
341
388
  vmax = median_ + 5 * std_
342
389
  cmap = 'gray'
343
- imshow(fig, ax, image, vmin=vmin, vmax=vmax, cmap=cmap)
390
+ imshow(fig, ax, image, vmin=vmin, vmax=vmax, cmap=cmap, aspect=aspect)
344
391
  ax.set_xlim([zoom_region_imshow.python[1].start, zoom_region_imshow.python[1].stop - 1])
345
392
  ax.set_ylim([zoom_region_imshow.python[0].start, zoom_region_imshow.python[0].stop - 1])
346
393
  ax.set_title(title)
@@ -385,7 +432,7 @@ def cr2images(data1, data2=None, ioffx=0, ioffy=0,
385
432
  def apply_cr2images_ccddata(infile1, infile2=None, outfile1=None, outfile2=None,
386
433
  ioffx=0, ioffy=0, tsigma_peak=10, tsigma_tail=3,
387
434
  list_skipped_regions=None, image_region=None,
388
- median_size=None, debug_level=0, zoom_region_imshow=None):
435
+ median_size=None, debug_level=0, zoom_region_imshow=None, aspect='equal'):
389
436
  """Apply cr2images() to FITS files storing CCDData.
390
437
 
391
438
  The FITS file must contain:
@@ -444,6 +491,8 @@ def apply_cr2images_ccddata(infile1, infile2=None, outfile1=None, outfile2=None,
444
491
  zoom_region_imshow : SliceRegion2D instance or None
445
492
  If not None, display intermediate images, zooming in the
446
493
  indicated region.
494
+ aspect : str
495
+ Aspect ratio of the displayed images. Default is 'equal'.
447
496
 
448
497
  """
449
498
 
@@ -451,7 +500,7 @@ def apply_cr2images_ccddata(infile1, infile2=None, outfile1=None, outfile2=None,
451
500
  if (ioffx != 0) or (ioffy != 0):
452
501
  raise ValueError(f'ERROR: ioffx={ioffx} and ioffy={ioffy} must be zero!')
453
502
  if median_size is None:
454
- raise ValueError(f'ERROR: you must specify median_size when only one image is available')
503
+ raise ValueError('ERROR: you must specify median_size when only one image is available')
455
504
 
456
505
  history_list = ['using cr2images:']
457
506
 
@@ -472,7 +521,8 @@ def apply_cr2images_ccddata(infile1, infile2=None, outfile1=None, outfile2=None,
472
521
  image_region=image_region,
473
522
  return_masks=True,
474
523
  debug_level=debug_level,
475
- zoom_region_imshow=zoom_region_imshow
524
+ zoom_region_imshow=zoom_region_imshow,
525
+ aspect=aspect
476
526
  )
477
527
  ccdimage1_clean.mask[mask_data1c.astype(bool)] = True
478
528
  ccdimage2_clean.mask[mask_data2c.astype(bool)] = True
@@ -495,7 +545,8 @@ def apply_cr2images_ccddata(infile1, infile2=None, outfile1=None, outfile2=None,
495
545
  image_region=image_region,
496
546
  return_masks=True,
497
547
  debug_level=debug_level,
498
- zoom_region_imshow=zoom_region_imshow
548
+ zoom_region_imshow=zoom_region_imshow,
549
+ aspect=aspect
499
550
  )
500
551
  ccdimage1_clean.mask[mask_data1c.astype(bool)] = True
501
552
  ccdimage2_uncertainty_array = ndimage.median_filter(ccdimage1.uncertainty.array, size=median_size)
teareduce/elapsed_time.py CHANGED
@@ -11,7 +11,7 @@
11
11
  from datetime import datetime
12
12
  import platform
13
13
  import sys
14
- from .version import version
14
+ from .version import VERSION
15
15
 
16
16
 
17
17
  def elapsed_time(time_ini, time_end, osinfo=True):
@@ -36,7 +36,7 @@ def elapsed_time(time_ini, time_end, osinfo=True):
36
36
  print(f'node.............: {result.node}')
37
37
  print(f'Python executable: {sys.executable}')
38
38
 
39
- print(f"teareduce version: {version}")
39
+ print(f"teareduce version: {VERSION}")
40
40
  print(f"Initial time.....: {time_ini}")
41
41
  print(f"Final time.......: {time_end}")
42
42
  print(f"Elapsed time.....: {time_end - time_ini}")
@@ -54,4 +54,4 @@ def elapsed_time_since(time_ini, osinfo=True):
54
54
  """
55
55
 
56
56
  time_end = datetime.now()
57
- elapsed_time(time_ini, time_end)
57
+ elapsed_time(time_ini, time_end, osinfo=osinfo)
@@ -0,0 +1,85 @@
1
+ #
2
+ # Copyright 2025 Universidad Complutense de Madrid
3
+ #
4
+ # This file is part of teareduce
5
+ #
6
+ # SPDX-License-Identifier: GPL-3.0-or-later
7
+ # License-Filename: LICENSE.txt
8
+ #
9
+
10
+ """Auxiliary function to display 1D histograms computed with numpy"""
11
+
12
+ import numpy as np
13
+
14
+
15
+ def plot_hist_step(ax, bins, h, color='C0', alpha=1.0, fill_color=None, fill_alpha=0.4):
16
+ """Plot histogram already computed.
17
+
18
+ Parameters
19
+ ----------
20
+ ax : matplotlib.axes.Axes
21
+ Axes to plot on.
22
+ bins : np.ndarray
23
+ Array of bin edges.
24
+ h : np.ndarray
25
+ Array of histogram values.
26
+ color : str, optional
27
+ Color of the histogram line, by default 'C0'.
28
+ alpha : float, optional
29
+ Transparency of the histogram line, by default 1.0.
30
+ fill_color : str, optional
31
+ Color to fill the histogram area, by default None (no fill).
32
+ fill_alpha : float, optional
33
+ Transparency of the filled area, by default 0.4.
34
+ """
35
+ # bin centers
36
+ xdum = (bins[:-1] + bins[1:]) / 2
37
+ ax.step(xdum, h, where='mid')
38
+ # draw vertical lines at the edges
39
+ ax.plot([bins[0], bins[0], xdum[0]], [0, h[0], h[0]], alpha=alpha, color=f'{color}', linestyle='-')
40
+ ax.plot([xdum[-1], bins[-1], bins[-1]], [h[-1], h[-1], 0], alpha=alpha, color=f'{color}', linestyle='-')
41
+ # fill area under the histogram
42
+ if fill_color is not None:
43
+ ax.fill_between(np.concatenate((np.array([bins[0]]), xdum, np.array([bins[-1]]))),
44
+ np.concatenate((np.array([h[0]]), h, np.array([h[-1]]))),
45
+ step='mid', alpha=fill_alpha, color=f'{fill_color}')
46
+
47
+
48
+ def hist_step(ax, data, bins, color='C0', alpha=1.0, fill_color=None, fill_alpha=0.4):
49
+ """Compute and plot histogram of data.
50
+
51
+ Parameters
52
+ ----------
53
+ ax : matplotlib.axes.Axes
54
+ Axes to plot on.
55
+ data : np.ndarray
56
+ Data to compute the histogram from.
57
+ bins : int or np.ndarray
58
+ Number of bins or array of bin edges.
59
+ color : str, optional
60
+ Color of the histogram line, by default 'C0'.
61
+ alpha : float, optional
62
+ Transparency of the histogram line, by default 1.0.
63
+ fill_color : str, optional
64
+ Color to fill the histogram area, by default None (no fill).
65
+ fill_alpha : float, optional
66
+ Transparency of the filled area, by default 0.4.
67
+
68
+ Returns
69
+ -------
70
+ h : np.ndarray
71
+ Histogram values.
72
+ edges : np.ndarray
73
+ Bin edges of the histogram.
74
+ """
75
+
76
+ if isinstance(bins, int):
77
+ bins = np.linspace(np.min(data), np.max(data), bins + 1)
78
+ elif isinstance(bins, np.ndarray):
79
+ pass
80
+ else:
81
+ raise ValueError(f'Unexpected {bins=}')
82
+ h, edges = np.histogram(data, bins=bins)
83
+ plot_hist_step(ax, bins, h, color=color, alpha=alpha, fill_color=fill_color, fill_alpha=fill_alpha)
84
+
85
+ return h, edges
@@ -584,9 +584,9 @@ class SimulateCCDExposure:
584
584
 
585
585
  # BIAS and Readout Noise
586
586
  if np.isnan(self.bias.value).any():
587
- raise ValueError(f"The parameter 'bias' contains NaN")
587
+ raise ValueError("The parameter 'bias' contains NaN")
588
588
  if np.isnan(self.readout_noise.value).any():
589
- raise ValueError(f"The parameter 'readout_noise' contains NaN")
589
+ raise ValueError("The parameter 'readout_noise' contains NaN")
590
590
  image2d = self._rng.normal(
591
591
  loc=self.bias.value,
592
592
  scale=self.readout_noise.value
@@ -597,7 +597,7 @@ class SimulateCCDExposure:
597
597
 
598
598
  # DARK
599
599
  if np.isnan(self.dark.value).any():
600
- raise ValueError(f"The parameter 'dark' contains NaN")
600
+ raise ValueError("The parameter 'dark' contains NaN")
601
601
  image2d += self.dark.value
602
602
  if imgtype == "dark":
603
603
  result.data = image2d
@@ -605,11 +605,11 @@ class SimulateCCDExposure:
605
605
 
606
606
  # OBJECT
607
607
  if np.isnan(self.flatfield).any():
608
- raise ValueError(f"The parameter 'flatfield' contains NaN")
608
+ raise ValueError("The parameter 'flatfield' contains NaN")
609
609
  if np.isnan(self.data_model.value).any():
610
- raise ValueError(f"The parameter 'data_model' contains NaN")
610
+ raise ValueError("The parameter 'data_model' contains NaN")
611
611
  if np.isnan(self.gain.value).any():
612
- raise ValueError(f"The parameter 'gain' contains NaN")
612
+ raise ValueError("The parameter 'gain' contains NaN")
613
613
  if method.lower() == "poisson":
614
614
  # transform data_model from ADU to electrons,
615
615
  # generate Poisson distribution
teareduce/sliceregion.py CHANGED
@@ -6,10 +6,16 @@
6
6
  # SPDX-License-Identifier: GPL-3.0+
7
7
  # License-Filename: LICENSE.txt
8
8
  #
9
+ """Auxiliary classes to handle slicing regions in 1D, 2D, and 3D.
10
+
11
+ These classes provide a way to define and manipulate slices in a
12
+ consistent manner, following both FITS and Python conventions.
13
+ """
9
14
 
10
- import numpy as np
11
15
  import re
12
16
 
17
+ import numpy as np
18
+
13
19
  class SliceRegion1D:
14
20
  """Store indices for slicing of 1D regions.
15
21
 
@@ -35,7 +41,7 @@ class SliceRegion1D:
35
41
  within(other)
36
42
  Check if slice 'other' is within the parent slice.
37
43
  """
38
-
44
+
39
45
  def __init__(self, region, mode=None):
40
46
  """Initialize SliceRegion1D.
41
47
 
@@ -58,21 +64,21 @@ class SliceRegion1D:
58
64
  region = np.s_[numbers_int[0]:numbers_int[1]]
59
65
 
60
66
  if isinstance(region, slice):
61
- for number in [slice.start, slice.stop]:
67
+ for number in [region.start, region.stop]:
62
68
  if number is None:
63
- raise ValueError(f'Invalid {slice!r}: you must specify start:stop in slice by number')
69
+ raise ValueError(f'Invalid {region!r}: you must specify start:stop in slice by number')
64
70
  else:
65
- raise ValueError(f'Object {region} of type {type(region)} is not a slice')
66
-
71
+ raise ValueError(f'Object {region} of type {type(region)} is not a slice')
72
+
67
73
  if region.step not in [1, None]:
68
74
  raise ValueError(f'This class {self.__class__.__name__} '
69
75
  'does not handle step != 1')
70
-
76
+
71
77
  errmsg = f'Invalid mode={mode}. Only "FITS" or "Python" (case insensitive) are valid'
72
78
  if mode is None:
73
79
  raise ValueError(errmsg)
74
80
  self.mode = mode.lower()
75
-
81
+
76
82
  if self.mode == 'fits':
77
83
  if region.stop < region.start:
78
84
  raise ValueError(f'Invalid {region!r}')
@@ -129,7 +135,7 @@ class SliceRegion1D:
129
135
  return result
130
136
  result = True
131
137
  return result
132
-
138
+
133
139
 
134
140
  class SliceRegion2D:
135
141
  """Store indices for slicing of 2D regions.
@@ -140,9 +146,9 @@ class SliceRegion2D:
140
146
  Attributes
141
147
  ----------
142
148
  fits : slice
143
- 1D slice following the FITS convention.
149
+ 2D slice following the FITS convention.
144
150
  python : slice
145
- 1D slice following the Python convention.
151
+ 2D slice following the Python convention.
146
152
  mode : str
147
153
  Convention mode employed to define the slice.
148
154
  The two possible modes are 'fits' and 'python'.
@@ -157,7 +163,7 @@ class SliceRegion2D:
157
163
  Check if slice 'other' is within the parent slice."""
158
164
 
159
165
  def __init__(self, region, mode=None):
160
- """Initialize SliceRegion1D.
166
+ """Initialize SliceRegion2D.
161
167
 
162
168
  Parameters
163
169
  ----------
@@ -182,13 +188,13 @@ class SliceRegion2D:
182
188
  s1, s2 = region
183
189
  for item in [s1, s2]:
184
190
  if isinstance(item, slice):
185
- for number in [s1.start, s1.stop, s2.start, s2.stop]:
191
+ for number in [item.start, item.stop]:
186
192
  if number is None:
187
193
  raise ValueError(f'Invalid {item!r}: you must specify start:stop in slice by number')
194
+ if item.step not in [1, None]:
195
+ raise ValueError(f'This class {self.__class__.__name__} does not handle step != 1')
188
196
  else:
189
197
  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
198
  else:
193
199
  raise ValueError(f'This class {self.__class__.__name__} only handles 2D regions')
194
200
 
@@ -261,3 +267,143 @@ class SliceRegion2D:
261
267
  return result
262
268
  result = True
263
269
  return result
270
+
271
+
272
+ class SliceRegion3D:
273
+ """Store indices for slicing of 3D regions.
274
+
275
+ The attributes .python and .fits provide the indices following
276
+ the Python and the FITS convention, respectively.
277
+
278
+ Attributes
279
+ ----------
280
+ fits : slice
281
+ 3D slice following the FITS convention.
282
+ python : slice
283
+ 3D slice following the Python convention.
284
+ mode : str
285
+ Convention mode employed to define the slice.
286
+ The two possible modes are 'fits' and 'python'.
287
+ fits_section : str
288
+ Resulting slice section in FITS convention:
289
+ '[num1:num2,num3:num4,num5:num6]'. This string is defined after
290
+ successfully initializing the SliceRegion3D instance.
291
+
292
+ Methods
293
+ -------
294
+ within(other)
295
+ Check if slice 'other' is within the parent slice."""
296
+
297
+ def __init__(self, region, mode=None):
298
+ """Initialize SliceRegion3D.
299
+
300
+ Parameters
301
+ ----------
302
+ region : slice or str
303
+ Slice region. It can be provided as np.s_[num1:num2, num3:num4, num5:num6],
304
+ as a tuple (slice(num1, num2), slice(num3, num4), slice(num5, num6)),
305
+ or as a string '[num1:num2, num3:num4, num5:num6]'
306
+ mode : str
307
+ Convention mode employed to define the slice.
308
+ The two possible modes are 'fits' and 'python'.
309
+ """
310
+ if isinstance(region, str):
311
+ pattern = r'^\s*\[\s*\d+\s*:\s*\d+\s*,\s*\d+\s*:\s*\d+\s*,\s*\d+\s*:\s*\d+\s*\]\s*$'
312
+ if not re.match(pattern, region):
313
+ raise ValueError(f"Invalid {region!r}. It must match '[num:num, num:num, num:num]'")
314
+ # extract numbers and generate np.s_[num:num, num:num, num:num]
315
+ numbers_str = re.findall(r'\d+', region)
316
+ numbers_int = list(map(int, numbers_str))
317
+ region = np.s_[numbers_int[0]:numbers_int[1], numbers_int[2]:numbers_int[3], numbers_int[4]:numbers_int[5]]
318
+
319
+ if isinstance(region, tuple) and len(region) == 3:
320
+ s1, s2, s3 = region
321
+ for item in [s1, s2, s3]:
322
+ if isinstance(item, slice):
323
+ for number in [item.start, item.stop]:
324
+ if number is None:
325
+ raise ValueError(f'Invalid {item!r}: you must specify start:stop in slice by number')
326
+ if item.step not in [1, None]:
327
+ raise ValueError(f'This class {self.__class__.__name__} does not handle step != 1')
328
+ else:
329
+ raise ValueError(f'Object {item} of type {type(item)} is not a slice')
330
+ else:
331
+ raise ValueError(f'This class {self.__class__.__name__} only handles 3D regions')
332
+
333
+ errmsg = f'Invalid mode={mode}. Only "FITS" or "Python" (case insensitive) are valid'
334
+ if mode is None:
335
+ raise ValueError(errmsg)
336
+ self.mode = mode.lower()
337
+
338
+ if self.mode == 'fits':
339
+ if s1.stop < s1.start:
340
+ raise ValueError(f'Invalid {s1!r}')
341
+ if s2.stop < s2.start:
342
+ raise ValueError(f'Invalid {s2!r}')
343
+ if s3.stop < s3.start:
344
+ raise ValueError(f'Invalid {s3!r}')
345
+ self.fits = region
346
+ self.python = slice(s3.start-1, s3.stop), slice(s2.start-1, s2.stop), slice(s1.start-1, s1.stop)
347
+ elif self.mode == 'python':
348
+ if s1.stop <= s1.start:
349
+ raise ValueError(f'Invalid {s1!r}')
350
+ if s2.stop <= s2.start:
351
+ raise ValueError(f'Invalid {s2!r}')
352
+ if s3.stop <= s3.start:
353
+ raise ValueError(f'Invalid {s3!r}')
354
+ self.fits = slice(s3.start+1, s3.stop), slice(s2.start+1, s2.stop), slice(s1.start+1, s1.stop)
355
+ self.python = region
356
+ else:
357
+ raise ValueError(errmsg)
358
+
359
+ s1, s2, s3 = self.fits
360
+ self.fits_section = f'[{s1.start}:{s1.stop},{s2.start}:{s2.stop},{s3.start}:{s3.stop}]'
361
+
362
+ def __eq__(self, other):
363
+ return self.fits == other.fits and self.python == other.python
364
+
365
+ def __repr__(self):
366
+ if self.mode == 'fits':
367
+ return (f'{self.__class__.__name__}('
368
+ f'{self.fits!r}, mode="fits")')
369
+ else:
370
+ return (f'{self.__class__.__name__}('
371
+ f'{self.python!r}, mode="python")')
372
+
373
+ def within(self, other):
374
+ """Determine if slice 'other' is within the parent slice.
375
+
376
+ Parameters
377
+ ----------
378
+ other : SliceRegion3D
379
+ New instance for which we want to determine
380
+ if it is within the parent SliceRegion3D instance.
381
+
382
+ Returns
383
+ -------
384
+ result : bool
385
+ Return True if 'other' is within the parent slice.
386
+ False otherwise.
387
+ """
388
+ if isinstance(other, self.__class__):
389
+ pass
390
+ else:
391
+ raise ValueError(f'Object {other} of type {type(other)} is not a {self.__class__.__name__}')
392
+
393
+ s1, s2, s3 = self.python
394
+ s1_other, s2_other, s3_other = other.python
395
+ result = False
396
+ if s1.start < s1_other.start:
397
+ return result
398
+ if s1.stop > s1_other.stop:
399
+ return result
400
+ if s2.start < s2_other.start:
401
+ return result
402
+ if s2.stop > s2_other.stop:
403
+ return result
404
+ if s3.start < s3_other.start:
405
+ return result
406
+ if s3.stop > s3_other.stop:
407
+ return result
408
+ result = True
409
+ return result
File without changes
@@ -0,0 +1,49 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2025 Universidad Complutense de Madrid
4
+ #
5
+ # This file is part of teareduce
6
+ #
7
+ # SPDX-License-Identifier: GPL-3.0-or-later
8
+ # License-Filename: LICENSE.txt
9
+ #
10
+ """Tests for the SliceRegion class."""
11
+
12
+ import numpy as np
13
+
14
+ from ..sliceregion import SliceRegion1D, SliceRegion2D, SliceRegion3D
15
+
16
+
17
+ def test_slice_region_creation():
18
+ """Test the creation of a SliceRegion."""
19
+
20
+ region1d = SliceRegion1D(np.s_[1:10], mode='python')
21
+ assert region1d.fits == slice(2, 10, None)
22
+ assert region1d.python == slice(1, 10, None)
23
+ assert region1d.fits_section == '[2:10]'
24
+
25
+ region2d = SliceRegion2D(np.s_[1:10, 2:20], mode='python')
26
+ assert region2d.fits == (slice(3, 20, None), slice(2, 10, None))
27
+ assert region2d.python == (slice(1, 10, None), slice(2, 20, None))
28
+ assert region2d.fits_section == '[3:20,2:10]'
29
+
30
+ region3d = SliceRegion3D(np.s_[1:10, 2:20, 3:30], mode='python')
31
+ assert region3d.fits == (slice(4, 30, None), slice(3, 20, None), slice(2, 10, None))
32
+ assert region3d.python == (slice(1, 10, None), slice(2, 20, None), slice(3, 30, None))
33
+ assert region3d.fits_section == '[4:30,3:20,2:10]'
34
+
35
+ def test_slice_values():
36
+ """Test the values of the slices in different modes."""
37
+
38
+ array1d = np.arange(10)
39
+
40
+ region1d = SliceRegion1D(np.s_[1:3], mode='python')
41
+ assert np.all(array1d[region1d.python] == np.array([1, 2]))
42
+
43
+ array2d = np.arange(12).reshape(3, 4)
44
+ region2d = SliceRegion2D(np.s_[1:3, 2:3], mode='python')
45
+ assert np.all(array2d[region2d.python] == np.array([[6], [10]]))
46
+
47
+ array3d = np.arange(24).reshape(3, 4, 2)
48
+ region3d = SliceRegion3D(np.s_[1:3, 2:4, 1:2], mode='python')
49
+ assert np.all(array3d[region3d.python] == np.array([[[13], [15]], [[21], [23]]]))
teareduce/version.py CHANGED
@@ -1,18 +1,20 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  #
3
- # Copyright 2023-2024 Universidad Complutense de Madrid
3
+ # Copyright 2023-2025 Universidad Complutense de Madrid
4
4
  #
5
5
  # This file is part of teareduce
6
6
  #
7
7
  # SPDX-License-Identifier: GPL-3.0-or-later
8
8
  # License-Filename: LICENSE.txt
9
9
  #
10
+ """Module to define the version of the teareduce package."""
10
11
 
11
- version = '0.4.1'
12
+ VERSION = '0.4.3'
12
13
 
13
14
 
14
15
  def main():
15
- print('Version: ' + version)
16
+ """Prints the version of the teareduce package."""
17
+ print('Version: ' + VERSION)
16
18
 
17
19
 
18
20
  if __name__ == "__main__":
@@ -1,34 +1,33 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: teareduce
3
- Version: 0.4.1
3
+ Version: 0.4.3
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
7
7
  Project-URL: Homepage, https://github.com/nicocardiel/teareduce
8
8
  Project-URL: Repository, https://github.com/nicocardiel/teareduce.git
9
- Classifier: Programming Language :: Python :: 3.8
10
- Classifier: Programming Language :: Python :: 3.9
11
9
  Classifier: Programming Language :: Python :: 3.10
12
10
  Classifier: Programming Language :: Python :: 3.11
13
11
  Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
14
13
  Classifier: Development Status :: 3 - Alpha
15
14
  Classifier: Environment :: Console
16
15
  Classifier: Intended Audience :: Science/Research
17
- Classifier: License :: OSI Approved :: GNU General Public License (GPL)
16
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
18
17
  Classifier: Operating System :: OS Independent
19
18
  Classifier: Topic :: Scientific/Engineering :: Astronomy
20
- Requires-Python: >=3.8
19
+ Requires-Python: >=3.10
21
20
  Description-Content-Type: text/markdown
22
21
  License-File: LICENSE.txt
23
22
  Requires-Dist: astropy
24
- Requires-Dist: importlib_resources
25
23
  Requires-Dist: lmfit
26
24
  Requires-Dist: matplotlib
27
- Requires-Dist: numpy>=1.20
25
+ Requires-Dist: numpy>=1.22
28
26
  Requires-Dist: scipy
29
27
  Requires-Dist: tqdm
30
28
  Provides-Extra: test
31
29
  Requires-Dist: pytest; extra == "test"
30
+ Dynamic: license-file
32
31
 
33
32
  # teareduce
34
33
  Utilities for astronomical data reduction
@@ -1,25 +1,28 @@
1
- teareduce/__init__.py,sha256=MLlKpaDRjG33kfn52OHonENfsHYZqlGBmQFhk7_CUR0,1214
1
+ teareduce/__init__.py,sha256=u-6eZ9oz2reoSWxm1kf-9uj2VB4yERhh7fuE5gd4-RA,1304
2
2
  teareduce/avoid_astropy_warnings.py,sha256=2YgQ47pxsKYWDxUtzyEOfh3Is3aAHHmjJkuOa1JCDN4,648
3
3
  teareduce/correct_pincushion_distortion.py,sha256=Xpt03jtmJMyqik4ta95zMRE3Z6dVfzzHI2z5IDbtnMk,1685
4
- teareduce/cosmicrays.py,sha256=y8f5KLUsiOeOkArGnZVi_q11chsgnYlwtxbSD_a9EAQ,24209
4
+ teareduce/cosmicrays.py,sha256=gLHgq9LdfNHmZ5n_FYxvxcGI2245TXGV-rFHSIogxPE,26877
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=Nv77LjxvfuL3EECU7F0nMzEjD-qkdQq0C5IoRGzO-5k,1403
7
+ teareduce/elapsed_time.py,sha256=QWakPeiOUA__WpjjFREnodKqW1FZzVGWstab0N3Ro6k,1418
8
+ teareduce/histogram1d.py,sha256=3hlvcI8XPRmZ27H_sFmyL26gIcbJozk07ELBKWtngQk,2835
8
9
  teareduce/imshow.py,sha256=5j5RFoZOWBkF8CzUSLTR8-fNuuZHOFCMgH2CMOaDu50,4878
9
10
  teareduce/numsplines.py,sha256=1PpG-frdc9Qz3VRbC7XyZFWKmhus05ID4REtFnWDmUo,8049
10
11
  teareduce/peaks_spectrum.py,sha256=YPCJz8skJmIjWYqT7ZhBJGhnqPayFwy5xb7I9OHlUZI,9890
11
12
  teareduce/polfit.py,sha256=CGsrRsz_Du2aKxOcgXi36lpAZO04JyqCCUaxhC0C-Mk,14281
12
13
  teareduce/robust_std.py,sha256=dk1G3VgiplZzmSfL7qWniEZ-5c3novHnBpRPCM77u84,1084
13
14
  teareduce/sdistortion.py,sha256=5ZsZn4vD5Sw2aoqO8-NIOH7H89Zmh7ZDkow6YbAotHU,5916
14
- teareduce/simulateccdexposure.py,sha256=3SNPBOP09vnwoGqdf97f9s21WmTIw6skafZ6qRkJUrw,24642
15
- teareduce/sliceregion.py,sha256=XVyUuY9P1ZuTiE5PARTmA51KA4bfHVFWy_Wt_blqd80,9450
15
+ teareduce/simulateccdexposure.py,sha256=cdbpca6GVuM3d7R1LGzlIZZvjTq_jzrlkk_Cli7aouQ,24636
16
+ teareduce/sliceregion.py,sha256=0h1xYcNG4mkJhR6bdaDe9pF2_MCuGeLJC7WVbbMZy8E,14977
16
17
  teareduce/statsummary.py,sha256=mtaM21d5aHvtLjCt_SSDMvD_fjI5nK21ZqxuDtcvldI,5426
17
- teareduce/version.py,sha256=ElQYBbcanPuGZfEvdh_7EEDNElio30JdXqoXBR8Wut4,303
18
+ teareduce/version.py,sha256=SH9n_ZVdY2JuLjfax6Is4ri1SsZNjB9mWwd__OBJx0Y,419
18
19
  teareduce/wavecal.py,sha256=iiKG_RPW2CllwZxG5fTsyckE0Ec_IeZ6v7v2cQt6OeU,68706
19
20
  teareduce/write_array_to_fits.py,sha256=kWDrEH9coJ1yIu56oQJpWtDqJL4c8HGmssE9jle4e94,617
20
21
  teareduce/zscale.py,sha256=HuPYagTW55D7RtjPGc7HcibQlCx5oqLYHKoM6WEHG2g,1161
21
- teareduce-0.4.1.dist-info/LICENSE.txt,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
22
- teareduce-0.4.1.dist-info/METADATA,sha256=8H7vYJvwXV1vamdBa4jYkxCBlSJKn2B9BESH7g33jhc,2302
23
- teareduce-0.4.1.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
24
- teareduce-0.4.1.dist-info/top_level.txt,sha256=7OkwtX9zNRkGJ7ACgjk4ESgC74qUYcS5O2qcO0v-Si4,10
25
- teareduce-0.4.1.dist-info/RECORD,,
22
+ teareduce/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ teareduce/tests/test_sliceregion.py,sha256=25dhS7yJ1z_3tgFLw21uOkNKuugyVA-8L1ehafSQjFg,1769
24
+ teareduce-0.4.3.dist-info/licenses/LICENSE.txt,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
25
+ teareduce-0.4.3.dist-info/METADATA,sha256=xj4Kdw08405olw1sdCcZ1ybcK65YPlOA9qxk6ou9sTk,2246
26
+ teareduce-0.4.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
+ teareduce-0.4.3.dist-info/top_level.txt,sha256=7OkwtX9zNRkGJ7ACgjk4ESgC74qUYcS5O2qcO0v-Si4,10
28
+ teareduce-0.4.3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (76.0.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5