visualastro 0.0.2__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.
@@ -0,0 +1,15 @@
1
+ # Core Classes
2
+ from .visual_classes import DataCube, ExtractedSpectrum, FitsFile
3
+
4
+ # Submodules
5
+ from .data_cube import *
6
+ from .data_cube_utils import *
7
+ from .io import *
8
+ from .numerical_utils import *
9
+ from .photometry import *
10
+ from .plotting import *
11
+ from .plot_utils import *
12
+ from .reduction import *
13
+ from .spectra import *
14
+ from .spectra_utils import *
15
+ from .visual_plots import *
@@ -0,0 +1,494 @@
1
+ import glob
2
+ import warnings
3
+ from astropy.io import fits
4
+ import astropy.units as u
5
+ from astropy.utils.exceptions import AstropyWarning
6
+ from matplotlib.patches import Ellipse
7
+ import numpy as np
8
+ from regions import PixCoord, EllipsePixelRegion, EllipseAnnulusPixelRegion
9
+ from spectral_cube import SpectralCube
10
+ from tqdm import tqdm
11
+ from .data_cube_utils import compute_line, get_spectral_slice_value, slice_cube
12
+ from .io import get_dtype, get_errors
13
+ from .numerical_utils import (
14
+ check_units_consistency, convert_units,
15
+ get_data, shift_by_radial_vel
16
+ )
17
+ from .plot_utils import (
18
+ add_colorbar, plot_ellipses, plot_interactive_ellipse,
19
+ return_imshow_norm, set_unit_labels, set_vmin_vmax,
20
+ )
21
+ from .visual_classes import DataCube, FitsFile
22
+
23
+ warnings.filterwarnings('ignore', category=AstropyWarning)
24
+
25
+
26
+ def load_data_cube(filepath, error=True, hdu=0, dtype=None,
27
+ print_info=True, transpose=False):
28
+ '''
29
+ Load a sequence of FITS files into a 3D data cube.
30
+ This function searches for all FITS files matching a given path pattern,
31
+ loads them into a NumPy array of shape (T, M, N), and bundles the data
32
+ and headers into a `DataCube` object.
33
+ Parameters
34
+ ––––––––––
35
+ filepath : str
36
+ Path pattern to FITS files. Wildcards are supported.
37
+ Example: 'Spectro-Module/raw/HARPS*.fits'
38
+ dtype : numpy.dtype, optional, default=None
39
+ Data type for the loaded FITS data. If None, will use
40
+ the dtype of the provided data, promoting integer or
41
+ unsigned to `np.float64`.
42
+ print_info : bool, optional, default=True
43
+ If True, print summary information about the loaded cube.
44
+ transpose : bool, optional, default=False
45
+ If True, transpose each 2D image before stacking into the cube.
46
+ Returns
47
+ –––––––
48
+ cube : DataCube
49
+ A DataCube object containing:
50
+ - 'cube.data' : np.ndarray of shape (T, M, N)
51
+ - 'cube.headers' : list of astropy.io.fits.Header objects
52
+ Example
53
+ –––––––
54
+ Search for all fits files starting with 'HARPS' with .fits extention and load them.
55
+ filepath = 'Spectro-Module/raw/HARPS.*.fits'
56
+ '''
57
+ # searches for all files within a directory
58
+ fits_files = sorted(glob.glob(filepath))
59
+ # allocate ixMxN data cube array and header array
60
+ n_files = len(fits_files)
61
+
62
+ # load first file to determine shape, dtype, and check for errors
63
+ with fits.open(fits_files[0]) as hdul:
64
+ if print_info:
65
+ hdul.info()
66
+
67
+ data = hdul[hdu].data
68
+ header = hdul[hdu].header
69
+ err = get_errors(hdul, dtype)
70
+
71
+ dt = get_dtype(data, dtype)
72
+
73
+ if transpose:
74
+ data = data.T
75
+ if err is not None:
76
+ err = err.T
77
+
78
+ # Preallocate data cube and headers
79
+ datacube = np.zeros((n_files, data.shape[0], data.shape[1]), dtype=dt)
80
+ datacube[0] = data.astype(dt)
81
+ headers = [None] * n_files
82
+ headers[0] = header
83
+ # preallocate error array if needed and error exists
84
+ error_array = None
85
+ if error and err is not None:
86
+ error_array = np.zeros_like(datacube, dtype=dt)
87
+ error_array[0] = err.astype(dt)
88
+
89
+ # loop through remaining files
90
+ for i, file in enumerate(tqdm(fits_files[1:], desc="Loading FITS")):
91
+ with fits.open(file) as hdul:
92
+ data = hdul[hdu].data
93
+ headers[i+1] = hdul[hdu].header
94
+ err = get_errors(hdul, dt)
95
+ if transpose:
96
+ data = data.T
97
+ if err is not None:
98
+ err = err.T
99
+ datacube[i+1] = data.astype(dt)
100
+ if error_array is not None and err is not None:
101
+ error_array[i+1] = err.astype(dt)
102
+
103
+ return DataCube(datacube, headers, error_array)
104
+
105
+
106
+ def load_spectral_cube(filepath, hdu, error=True, header=True, dtype=None, print_info=False):
107
+ '''
108
+ Load a spectral cube from a FITS file, optionally including errors and header.
109
+ Parameters
110
+ ––––––––––
111
+ filepath : str
112
+ Path to the FITS file to read.
113
+ hdu : int or str
114
+ HDU index or name to read from the FITS file.
115
+ error : bool, optional, default=True
116
+ If True, load the associated error array using `get_errors`.
117
+ header : bool, optional, default=True
118
+ If True, load the HDU header.
119
+ dtype : data-type, optional, default=None
120
+ Desired NumPy dtype for the error array. If None, inferred
121
+ from FITS data, promoting integer and unsigned to `np.float64`.
122
+ print_info : bool, optional, default=False
123
+ If True, print FITS file info to the console.
124
+ Returns
125
+ –––––––
126
+ DataCube
127
+ A `DataCube` object containing:
128
+ - data : SpectralCube
129
+ Fits file data loaded as SpectralCube object.
130
+ - header : astropy.io.fits.Header
131
+ Fits file header.
132
+ - error : np.ndarray
133
+ Fits file error array.
134
+ - value : np.ndarray
135
+ Fits file data as np.ndarray.
136
+ Ex:
137
+ data = cube.data
138
+ '''
139
+ # load SpectralCube from filepath
140
+ spectral_cube = SpectralCube.read(filepath, hdu=hdu)
141
+ # initialize error and header objects
142
+ error_array = None
143
+ hdr = None
144
+ # open fits file
145
+ with fits.open(filepath) as hdul:
146
+ # print fits info
147
+ if print_info:
148
+ hdul.info()
149
+ # load error array
150
+ if error:
151
+ error_array = get_errors(hdul, dtype)
152
+ # load header
153
+ if header:
154
+ hdr = hdul[hdu].header
155
+
156
+ return DataCube(spectral_cube, headers=hdr, errors=error_array)
157
+
158
+ def plot_spectral_cube(cubes, idx, ax, vmin=None, vmax=None, percentile=[3,99.5],
159
+ norm='asinh', radial_vel=None, unit=None, cmap='turbo', **kwargs):
160
+ '''
161
+ Plot a single spectral slice from one or more spectral cubes.
162
+ Parameters
163
+ ––––––––––
164
+ cubes : DataCube, SpectralCube, or list of such
165
+ One or more spectral cubes to plot. All cubes should have consistent units.
166
+ idx : int
167
+ Index along the spectral axis corresponding to the slice to plot.
168
+ ax : matplotlib.axes.Axes
169
+ The axes on which to draw the slice.
170
+ vmin, vmax : float, optional, default=None
171
+ Minimum and maximum values for image scaling. Overrides percentile if provided.
172
+ percentile : list of two floats, default=[3, 99.5]
173
+ Percentile values for automatic scaling if vmin/vmax are not specified.
174
+ norm : str or None, default='asinh'
175
+ Normalization type for `imshow`. Use None for linear scaling.
176
+ radial_vel : float or astropy.units.Quantity, optional, default=None
177
+ Radial velocity to shift spectral axis to the rest frame.
178
+ unit : astropy.units.Unit or str, optional, default=None
179
+ Desired spectral axis unit for labeling.
180
+ cmap : str, list, or tuple, default='turbo'
181
+ Colormap(s) to use for plotting.
182
+
183
+ **kwargs : dict, optional
184
+ Additional plotting parameters.
185
+
186
+ Supported keywords:
187
+
188
+ - `title` : bool, default=False
189
+ If True, display spectral slice label as plot title.
190
+ - `emission_line` : str or None, default=None
191
+ Optional emission line label to display instead of slice value.
192
+ - `text_loc` : list of float, default=[0.03, 0.03]
193
+ Relative axes coordinates for overlay text placement.
194
+ - `text_color` : str, default='k'
195
+ Color of overlay text.
196
+ - `colorbar` : bool, default=True
197
+ Whether to add a colorbar.
198
+ - `cbar_width` : float, default=0.03
199
+ Width of the colorbar.
200
+ - `cbar_pad` : float, default=0.015
201
+ Padding between axes and colorbar.
202
+ - `clabel` : str, bool, or None, default=True
203
+ Label for colorbar. If True, automatically generate from cube unit.
204
+ - `xlabel`, `ylabel` : str, default='Right Ascension', 'Declination'
205
+ Axes labels.
206
+ - `spectral_label` : bool, default=True
207
+ Whether to draw spectral slice value as a label.
208
+ - `highlight` : bool, default=True
209
+ Whether to highlight interactive ellipse if plotted.
210
+ - `ellipses` : list or None, default=None
211
+ Ellipse objects to overlay on the image.
212
+ - `plot_ellipse` : bool, default=False
213
+ If True, plot a default or interactive ellipse.
214
+ - `center` : list of two ints, default=[Nx//2, Ny//2]
215
+ Center of default ellipse.
216
+ - `w`, `h` : float, default=X//5, Y//5
217
+ Width and height of default ellipse.
218
+ - `angle` : float or None, default=None
219
+ Angle of ellipse in degrees.
220
+ Notes
221
+ –––––
222
+ - If multiple cubes are provided, they are overplotted in sequence.
223
+ '''
224
+ # check cube units match and ensure cubes is iterable
225
+ cubes = check_units_consistency(cubes)
226
+ # –––– Kwargs ––––
227
+ # labels
228
+ title = kwargs.get('title', False)
229
+ emission_line = kwargs.get('emission_line', None)
230
+ text_loc = kwargs.get('text_loc', [0.03, 0.03])
231
+ text_color = kwargs.get('text_color', 'k')
232
+ colorbar = kwargs.get('colorbar', True)
233
+ cbar_width = kwargs.get('cbar_width', 0.03)
234
+ cbar_pad = kwargs.get('cbar_pad', 0.015)
235
+ clabel = kwargs.get('clabel', True)
236
+ xlabel = kwargs.get('xlabel', 'Right Ascension')
237
+ ylabel = kwargs.get('ylabel', 'Declination')
238
+ draw_spectral_label = kwargs.get('spectral_label', True)
239
+ highlight = kwargs.get('highlight', True)
240
+ # plot ellipse
241
+ ellipses = kwargs.get('ellipses', None)
242
+ plot_ellipse = (
243
+ True if ellipses is not None else kwargs.get('plot_ellipse', False)
244
+ )
245
+ _, X, Y = get_data(cubes[0]).shape
246
+ center = kwargs.get('center', [X//2, Y//2])
247
+ w = kwargs.get('w', X//5)
248
+ h = kwargs.get('h', Y//5)
249
+ angle = kwargs.get('angle', None)
250
+
251
+ for cube in cubes:
252
+ # extract data component
253
+ cube = get_data(cube)
254
+
255
+ # return data cube slices
256
+ cube_slice = slice_cube(cube, idx)
257
+ data = cube_slice.value
258
+
259
+ # compute imshow stretch
260
+ vmin, vmax = set_vmin_vmax(data, percentile, vmin, vmax)
261
+ cube_norm = return_imshow_norm(vmin, vmax, norm)
262
+
263
+ # imshow data
264
+ if norm is None:
265
+ im = ax.imshow(data, origin='lower', vmin=vmin, vmax=vmax, cmap=cmap)
266
+ else:
267
+ im = ax.imshow(data, origin='lower', cmap=cmap, norm=cube_norm)
268
+
269
+ # determine unit of colorbar
270
+ cbar_unit = set_unit_labels(cube.unit)
271
+ # set colorbar label
272
+ if clabel is True:
273
+ clabel = f'${cbar_unit}$' if cbar_unit is not None else None
274
+ # set colorbar
275
+ if colorbar:
276
+ add_colorbar(im, ax, cbar_width, cbar_pad, clabel)
277
+
278
+ # plot ellipses
279
+ if plot_ellipse:
280
+ # plot Ellipse objects
281
+ if ellipses is not None:
282
+ plot_ellipses(ellipses, ax)
283
+ # plot ellipse with angle
284
+ elif angle is not None:
285
+ e = Ellipse(xy=(center[0], center[1]), width=w, height=h, angle=angle, fill=False)
286
+ ax.add_patch(e)
287
+ # plot default/interactive ellipse
288
+ else:
289
+ plot_interactive_ellipse(center, w, h, ax, text_loc, text_color, highlight)
290
+ draw_spectral_label = False
291
+
292
+ # plot wavelength/frequency of current spectral slice, and emission line
293
+ if draw_spectral_label:
294
+ # compute spectral axis value of slice for label
295
+ spectral_axis = convert_units(cube.spectral_axis, unit)
296
+ spectral_axis = shift_by_radial_vel(spectral_axis, radial_vel)
297
+ spectral_value = get_spectral_slice_value(spectral_axis, idx)
298
+ unit_label = set_unit_labels(spectral_axis.unit)
299
+
300
+ # lambda for wavelength, f for frequency
301
+ spectral_type = r'\lambda = ' if spectral_axis.unit.physical_type == 'length' else r'f = '
302
+ # replace spectral type with emission line if provided
303
+ if emission_line is None:
304
+ slice_label = fr'${spectral_type}{spectral_value:0.2f}\,\mathrm{{{unit_label}}}$'
305
+ else:
306
+ # replace spaces with latex format
307
+ emission_line = emission_line.replace(' ', r'\ ')
308
+ slice_label = fr'$\mathrm{{{emission_line}}}\,{spectral_value:0.2f}\,\mathrm{{{unit_label}}}$'
309
+ # display label as either a title or text in figure
310
+ if title:
311
+ ax.set_title(slice_label, color=text_color, loc='center')
312
+ else:
313
+ ax.text(text_loc[0], text_loc[1], slice_label,
314
+ transform=ax.transAxes, color=text_color)
315
+
316
+ # set axes labels
317
+ ax.set_xlabel(xlabel)
318
+ ax.set_ylabel(ylabel)
319
+ ax.coords['dec'].set_ticklabel(rotation=90)
320
+
321
+
322
+ def mask_image(image, ellipse_region=None, region=None,
323
+ line_points=None, invert_region=False, above_line=True,
324
+ preserve_shape=True, existing_mask=None, **kwargs):
325
+ '''
326
+ Mask an image with modular filters.
327
+ Supports applying an elliptical or annular region mask, an optional
328
+ line cut (upper or lower half-plane), and combining with an existing mask.
329
+ Parameters
330
+ ––––––––––
331
+ image : array-like, DataCube, FitsFile, or SpectralCube
332
+ Input image or cube. If higher-dimensional, the mask is applied
333
+ to the last two axes.
334
+ ellipse_region : `EllipsePixelRegion` or `EllipseAnnulusPixelRegion`, optional, default=None
335
+ Region object specifying an ellipse or annulus.
336
+ region : str {'annulus', 'ellipse'}, optional, default=None
337
+ Type of region to apply. Ignored if `ellipse_region` is provided.
338
+ line_points : array-like, shape (2, 2), optional, default=None
339
+ Two (x, y) points defining a line for masking above/below.
340
+ Ex: [[0,2], [20,10]]
341
+ invert_region : bool, default=False
342
+ If True, invert the region mask.
343
+ above_line : bool, default=True
344
+ If True, keep the region above the line. If False, keep below.
345
+ preserve_shape : bool, default=True
346
+ If True, return an array of the same shape with masked values set to NaN.
347
+ If False, return only the unmasked pixels.
348
+ existing_mask : ndarray of bool, optional, default=None
349
+ An existing mask to combine (union) with the new mask.
350
+ **kwargs : dict, optional
351
+ Additional plotting parameters.
352
+
353
+ Supported keywords:
354
+
355
+ - center : tuple of float, optional, default=None
356
+ Center coordinates (x, y).
357
+ - w : float, optional, default=None
358
+ Width of ellipse.
359
+ - h : float, optional, default=None
360
+ Height of ellipse.
361
+ - angle : float, optional, default=0
362
+ Rotation angle in degrees.
363
+ - tolerance : float, optional, default=2
364
+ Tolerance for annulus inner/outer radii
365
+ Returns
366
+ –––––––
367
+ masked_image : ndarray or SpectralCube
368
+ Image with mask applied. Type matches input.
369
+ masks : ndarray of bool or list
370
+ If multiple masks are combined, returns a list containing the
371
+ master mask followed by individual masks. Otherwise returns a single mask.
372
+ '''
373
+ # –––– Kwargs ––––
374
+ center = kwargs.get('center', None)
375
+ w = kwargs.get('w', None)
376
+ h = kwargs.get('h', None)
377
+ angle = kwargs.get('angle', 0)
378
+ tolerance = kwargs.get('tolerance', 2)
379
+
380
+ # ensure working with array
381
+ if isinstance(image, (DataCube, FitsFile)):
382
+ image = image.data
383
+ elif isinstance(image, (list, tuple)):
384
+ image = np.asarray(image)
385
+
386
+ # determine image shape
387
+ N, M = image.shape[-2:]
388
+ y, x = np.indices((N, M))
389
+ # empty list to hold all masks
390
+ masks = []
391
+
392
+ # early return if just applying an existing mask
393
+ if ellipse_region is None and region is None and line_points is None and existing_mask is not None:
394
+ if existing_mask.shape != image.shape[-2:]:
395
+ raise ValueError("existing_mask must have same shape as image")
396
+
397
+ if isinstance(image, np.ndarray):
398
+ if preserve_shape:
399
+ masked_image = np.full_like(image, np.nan, dtype=float)
400
+ masked_image[..., existing_mask] = image[..., existing_mask]
401
+ else:
402
+ masked_image = image[..., existing_mask]
403
+ else:
404
+ # if spectral cube or similar object
405
+ masked_image = image.with_mask(existing_mask)
406
+
407
+ return masked_image
408
+
409
+ # –––– Region Mask ––––
410
+ # if ellipse region is passed in use those values
411
+ if ellipse_region is not None:
412
+ center = ellipse_region.center
413
+ a = ellipse_region.width / 2
414
+ b = ellipse_region.height / 2
415
+ angle = ellipse_region.angle if ellipse_region.angle is not None else 0
416
+ # accept user defined center, w, and h values if used
417
+ elif None not in (center, w, h):
418
+ a = w / 2
419
+ b = h / 2
420
+ # stop program if attempting to plot a region without necessary data
421
+ elif region is not None:
422
+ raise ValueError("Either 'ellipse_region' or 'center', 'w', 'h' must be provided.")
423
+
424
+ # construct region
425
+ if region is not None:
426
+ if region.lower() == 'annulus':
427
+ region_obj = EllipseAnnulusPixelRegion(
428
+ center=PixCoord(center[0], center[1]),
429
+ inner_width=2*(a - tolerance),
430
+ inner_height=2*(b - tolerance),
431
+ outer_width=2*(a + tolerance),
432
+ outer_height=2*(b + tolerance),
433
+ angle=angle * u.deg
434
+ )
435
+ elif region.lower() == 'ellipse':
436
+ region_obj = EllipsePixelRegion(
437
+ center=PixCoord(center[0], center[1]),
438
+ width=2*a,
439
+ height=2*b,
440
+ angle=angle * u.deg
441
+ )
442
+ else:
443
+ raise ValueError("region must be 'annulus' or 'ellipse'")
444
+
445
+ # filter by region mask
446
+ region_mask = region_obj.to_mask(mode='center').to_image((N, M)).astype(bool)
447
+ if invert_region:
448
+ region_mask = ~region_mask
449
+ masks.append(region_mask.copy())
450
+ else:
451
+ # empty mask if no region
452
+ region_mask = np.ones((N, M), dtype=bool)
453
+
454
+ # –––– Line Mask ––––
455
+ if line_points is not None:
456
+ # start from previous mask
457
+ line_mask = region_mask.copy()
458
+ # compute slope and intercept of line
459
+ m, b_line = compute_line(line_points)
460
+ # filter out points above/below line
461
+ line_mask &= (y >= m*x + b_line) if above_line else (y <= m*x + b_line)
462
+ # add line region to mask array
463
+ masks.append(line_mask.copy())
464
+ else:
465
+ # empty mask if no region
466
+ line_mask = region_mask.copy()
467
+
468
+ # –––– Combine Masks ––––
469
+ # start master mask with line_mask (or region if no line)
470
+ mask = line_mask.copy()
471
+
472
+ # union with existing mask if provided
473
+ if existing_mask is not None:
474
+ if existing_mask.shape != mask.shape:
475
+ raise ValueError("existing_mask must have the same shape as the image")
476
+ mask |= existing_mask
477
+
478
+ # –––– Apply Mask ––––
479
+ # if numpy array:
480
+ if isinstance(image, np.ndarray):
481
+ if preserve_shape:
482
+ masked_image = np.full_like(image, np.nan, dtype=float)
483
+ masked_image[..., mask] = image[..., mask]
484
+ else:
485
+ masked_image = image[..., mask]
486
+ # if spectral cube object
487
+ else:
488
+ masked_image = image.with_mask(mask)
489
+
490
+ # ––––– Final Mask List –––––
491
+ # Return master mask as first element
492
+ masks = [mask] + masks if len(masks) > 1 else mask
493
+
494
+ return masked_image, masks
@@ -0,0 +1,127 @@
1
+ import astropy.units as u
2
+ from astropy.units import spectral
3
+ import numpy as np
4
+ from .numerical_utils import get_data
5
+
6
+
7
+ # Cube Manipulation Functions
8
+ # –––––––––––––––––––––––––––
9
+ def extract_spectral_axis(cube, unit=None):
10
+ '''
11
+ Extract the spectral axis from a data cube and optionally
12
+ convert it to a specified unit.
13
+ Parameters
14
+ ––––––––––
15
+ cube : SpectralCube
16
+ The input spectral data cube.
17
+ unit : astropy.units.Unit, optional
18
+ Desired unit for the spectral axis. If None, the axis
19
+ is returned in its native units.
20
+ Returns
21
+ –––––––
22
+ spectral_axis : Quantity
23
+ The spectral axis of the cube, optionally converted
24
+ to the specified unit.
25
+ '''
26
+ axis = cube.spectral_axis
27
+ # return axis if unit is None
28
+ if unit is None:
29
+ return axis
30
+ # if a unit is specified, attempt to
31
+ # convert axis to those units
32
+ try:
33
+ return axis.to(unit, equivalencies=spectral())
34
+ except u.UnitConversionError:
35
+ raise ValueError(f"Cannot convert spectral axis from {axis.unit} to {unit}")
36
+
37
+
38
+ def slice_cube(cube, idx):
39
+ '''
40
+ Return a slice of a data cube along the first axis.
41
+ Parameters
42
+ ––––––––––
43
+ cube : np.ndarray
44
+ Input data cube, typically with shape (T, N, ...) where T is the first axis.
45
+ idx : int or list of int
46
+ Index or indices specifying the slice along the first axis:
47
+ - i -> returns 'cube[i]'
48
+ - [i] -> returns 'cube[i]'
49
+ - [i, j] -> returns 'cube[i:j+1].sum(axis=0)'
50
+ Returns
51
+ –––––––
52
+ cube : np.ndarray
53
+ Sliced cube with shape (N, ...).
54
+ '''
55
+ cube = get_data(cube)
56
+ # if index is integer
57
+ if isinstance(idx, int):
58
+ return cube[idx]
59
+ # if index is list of integers
60
+ elif isinstance(idx, list):
61
+ # list of len 1
62
+ if len(idx) == 1:
63
+ return cube[idx[0]]
64
+ # list of len 2
65
+ elif len(idx) == 2:
66
+ start, end = idx
67
+ return cube[start:end+1].sum(axis=0)
68
+
69
+ raise ValueError("'idx' must be an int or a list of one or two integers")
70
+
71
+
72
+ def get_spectral_slice_value(spectral_axis, idx):
73
+ '''
74
+ Return a representative value from a spectral axis
75
+ given an index or index range.
76
+ Parameters
77
+ ––––––––––
78
+ spectral_axis : Quantity
79
+ The spectral axis (e.g., wavelength, frequency, or
80
+ velocity) as an 'astropy.units.Quantity' array.
81
+ idx : int or list of int
82
+ Index or indices specifying the slice along the first axis:
83
+ - i -> returns 'spectral_axis[i]'
84
+ - [i] -> returns 'spectral_axis[i]'
85
+ - [i, j] -> returns '(spectral_axis[i] + spectral_axis[j+1])/2'
86
+ Returns
87
+ –––––––
88
+ spectral_value : float
89
+ The spectral value at the specified index or index
90
+ range, in the units of 'spectral_axis'.
91
+ '''
92
+ if isinstance(idx, int):
93
+ return spectral_axis[idx].value
94
+ elif isinstance(idx, list):
95
+ if len(idx) == 1:
96
+ return spectral_axis[idx[0]].value
97
+ elif len(idx) == 2:
98
+ return (spectral_axis[idx[0]].value + spectral_axis[idx[1]+1].value)/2
99
+
100
+ raise ValueError("'idx' must be an int or a list of one or two integers")
101
+
102
+
103
+ # Cube Masking Functions
104
+ # ––––––––––––––––––––––
105
+ def compute_line(points):
106
+ '''
107
+ Compute the slope and intercept of a line passing through two points.
108
+ Parameters
109
+ ––––––––––
110
+ points : list or tuple of tuples
111
+ A sequence containing exactly two points, each as (x, y), e.g.,
112
+ [(x0, y0), (x1, y1)].
113
+ Returns
114
+ –––––––
115
+ m : float
116
+ Slope of the line.
117
+ b : float
118
+ Intercept of the line (y = m*x + b).
119
+ Notes
120
+ –––––
121
+ - The function assumes the two points have different x-coordinates.
122
+ - If the x-coordinates are equal, a ZeroDivisionError will be raised.
123
+ '''
124
+ m = (points[0][1] - points[1][1]) / (points[0][0] - points[1][0])
125
+ b = points[0][1] - m*points[0][0]
126
+
127
+ return m, b