lime-stable 2.0.5__tar.gz → 2.2.dev2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/MANIFEST.in +1 -1
  2. {lime_stable-2.0.5/src/lime_stable.egg-info → lime_stable-2.2.dev2}/PKG-INFO +2 -2
  3. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/pyproject.toml +2 -2
  4. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/archives/read_fits.py +64 -17
  5. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/fitting/redshift.py +1 -6
  6. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/io.py +27 -6
  7. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/lime.toml +1 -1
  8. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/observations.py +165 -30
  9. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/plotting/bokeh_plots.py +153 -44
  10. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/plotting/plots.py +98 -23
  11. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/plotting/plots_interactive.py +5 -5
  12. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/plotting/theme_lime.toml +18 -2
  13. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/resources/generator_db.py +32 -8
  14. lime_stable-2.2.dev2/src/lime/resources/lines_database_v2.0.6.txt +292 -0
  15. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/retrieve/line_bands.py +33 -7
  16. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/tools.py +9 -2
  17. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/transitions.py +250 -67
  18. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/workflow.py +138 -143
  19. {lime_stable-2.0.5 → lime_stable-2.2.dev2/src/lime_stable.egg-info}/PKG-INFO +2 -2
  20. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime_stable.egg-info/SOURCES.txt +1 -0
  21. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime_stable.egg-info/requires.txt +1 -1
  22. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/LICENSE.rst +0 -0
  23. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/README.md +0 -0
  24. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/setup.cfg +0 -0
  25. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/__init__.py +0 -0
  26. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/archives/__init__.py +0 -0
  27. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/archives/tables.py +0 -0
  28. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/changelog.txt +0 -0
  29. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/fitting/__init__.py +0 -0
  30. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/fitting/lines.py +0 -0
  31. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/inference/detection.py +0 -0
  32. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/inference/intensity_threshold.py +0 -0
  33. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/plotting/__init__.py +0 -0
  34. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/plotting/format.py +0 -0
  35. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/plotting/utils.py +0 -0
  36. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/resources/__init__.py +0 -0
  37. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/resources/generator_logo.py +0 -0
  38. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/resources/lines_database_v2.0.0.txt +0 -0
  39. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/resources/types_params.txt +0 -0
  40. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/retrieve/__init__.py +0 -0
  41. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime/rsrc_manager.py +0 -0
  42. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime_stable.egg-info/dependency_links.txt +0 -0
  43. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/src/lime_stable.egg-info/top_level.txt +0 -0
  44. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/tests/test_astro.py +0 -0
  45. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/tests/test_cube.py +0 -0
  46. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/tests/test_io.py +0 -0
  47. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/tests/test_line.py +0 -0
  48. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/tests/test_model.py +0 -0
  49. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/tests/test_plots.py +0 -0
  50. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/tests/test_read_fits.py +0 -0
  51. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/tests/test_redshift.py +0 -0
  52. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/tests/test_resources.py +0 -0
  53. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/tests/test_sample.py +0 -0
  54. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/tests/test_spectrum.py +0 -0
  55. {lime_stable-2.0.5 → lime_stable-2.2.dev2}/tests/test_tools.py +0 -0
@@ -3,4 +3,4 @@ include src/lime/changelog.txt
3
3
  include src/lime/plotting/theme_lime.toml
4
4
  include src/lime/resources/parent_mask.txt
5
5
  include src/lime/resources/types_params.txt
6
- include src/lime/resources/lines_database_v2.0.0.txt
6
+ include src/lime/resources/lines_database_v2.0.6.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lime-stable
3
- Version: 2.0.5
3
+ Version: 2.2.dev2
4
4
  Summary: Line measuring algorithm for astronomical spectra
5
5
  Author-email: Vital Fernández <vgf@stsci.edu>
6
6
  License: GPL-3.0-or-later
@@ -18,7 +18,7 @@ Requires-Dist: scipy~=1.16
18
18
  Requires-Dist: tomli>=2.0.0; python_version < "3.11"
19
19
  Provides-Extra: full
20
20
  Requires-Dist: asdf~=4.1; extra == "full"
21
- Requires-Dist: aspect-stable~=0.5.1; extra == "full"
21
+ Requires-Dist: aspect-stable~=0.7.dev1; extra == "full"
22
22
  Requires-Dist: bokeh~=3.8; extra == "full"
23
23
  Requires-Dist: mplcursors~=0.6; extra == "full"
24
24
  Requires-Dist: openpyxl~=3.1; extra == "full"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lime-stable"
7
- version = "2.0.5"
7
+ version = "2.2.dev2"
8
8
  readme = { file = "README.md", content-type = "text/markdown" }
9
9
  requires-python = ">=3.11"
10
10
  license = {text = "GPL-3.0-or-later"}
@@ -24,7 +24,7 @@ dependencies = ["astropy~=7.1",
24
24
 
25
25
  [project.optional-dependencies]
26
26
  full = ["asdf~=4.1",
27
- "aspect-stable~=0.5.1",
27
+ "aspect-stable~=0.7.dev1",
28
28
  "bokeh~=3.8",
29
29
  "mplcursors~=0.6",
30
30
  "openpyxl~=3.1",
@@ -265,6 +265,7 @@ def check_fits_instructions(fits_source, online_provider=False):
265
265
  fits_reader = getattr(fits_manager, fits_source)
266
266
  else:
267
267
  source_type = 'instrument' if online_provider is False else 'survey'
268
+ # TODO show instruments supported
268
269
  raise LiMe_Error(f'Input {source_type} "{fits_source}" is not recognized. LiMe observation cannot be created.')
269
270
 
270
271
  else:
@@ -375,6 +376,40 @@ def load_fits(fits_address, data_ext_list=None, hdr_ext_list=None, url_check=Fal
375
376
  return data_list, header_list
376
377
 
377
378
 
379
+ def join_spectra_matrix(wave, flux, err=None):
380
+
381
+ # Make sure the wavelength array is increasing
382
+ # for i in range(wave.shape[0]):
383
+ # if wave[i, 0] > wave[i, -1]:
384
+ # wave[i] = wave[i, ::-1]
385
+ # flux[i] = flux[i, ::-1]
386
+ # err[i] = err[i, ::-1] if err is not None else None
387
+
388
+ # Make sure the passes are sorted:
389
+ key = np.nanmean(wave, axis=1)
390
+ order = np.argsort(key)
391
+ wave, flux, err = wave[order], flux[order], err[order] if err is not None else None
392
+
393
+ # Get dimensions
394
+ n_passes, n_pix = wave.shape
395
+
396
+ join_wl = 0.5 * (wave[:-1, -1] + wave[1:, 0])
397
+ cut_idx = np.sum(wave[:-1] <= join_wl[:, None], axis=1)
398
+ cut_idx = np.append(cut_idx, n_pix)
399
+
400
+ pix = np.arange(n_pix)
401
+ mask = pix < cut_idx[:, None]
402
+
403
+ wave_1d = wave[mask]
404
+ flux_1d = flux[mask]
405
+ err_1d = err[mask] if err is not None else None
406
+
407
+ lengths = np.sum(mask, axis=1)
408
+ starts = np.concatenate(([0], np.cumsum(lengths[:-1])))
409
+
410
+ return wave_1d, flux_1d, err_1d
411
+
412
+
378
413
  class OpenFits:
379
414
 
380
415
  def __init__(self, file_address, file_source=None, load_function=None, lime_object=None):
@@ -785,30 +820,42 @@ class OpenFits:
785
820
 
786
821
  """
787
822
 
788
- # Check dimensions of array
823
+ # Read the array
789
824
  data_list, header_list = load_fits(fits_address, data_ext_list, hdr_ext_list, url_check=False)
825
+
826
+ # Warning in the case of empty observartion
827
+ if len(data_list[0]['WAVELENGTH'].squeeze()) == 0:
828
+ _logger.critical(f'Input COS observation does not have scientific data ({fits_address}).')
829
+
830
+ # One single array with the data
790
831
  if data_list[0]['WAVELENGTH'].squeeze().ndim == 1:
791
832
  wave_arr = data_list[0]['WAVELENGTH'].squeeze()
792
833
  flux_arr = data_list[0]['FLUX'].squeeze()
793
834
  err_arr = data_list[0]['ERROR'].squeeze()
794
835
 
836
+ # Multiple passes
795
837
  else:
796
- # Get common middle index for joining the spectra
797
- # wave_matrix = data_list[0]['WAVELENGTH'][::-1]
798
- idcs_common = np.nonzero(data_list[0]['WAVELENGTH'][1, :] > data_list[0]['WAVELENGTH'][0, 0])[0]
799
- center_idx = idcs_common.shape[0] // 2
800
-
801
- # Create empty containers
802
- wave_arr = np.empty(data_list[0]['WAVELENGTH'].size - center_idx, data_list[0]['WAVELENGTH'].dtype) # TODO check for additional extension to join the spectra
803
- flux_arr = np.empty(data_list[0]['FLUX'].size - center_idx, data_list[0]['FLUX'].dtype) # dtype=data_list[0]['FLUX'].dtype)
804
- err_arr = np.empty(data_list[0]['ERROR'].size - center_idx, data_list[0]['ERROR'].dtype) # dtype=data_list[0]['ERROR'].dtype)
805
-
806
- # Fill with the array data
807
- arr_size = data_list[0]['WAVELENGTH'].shape[1]
808
- for key_arr, cont_arr in zip(['WAVELENGTH', 'FLUX', 'ERROR'], [wave_arr, flux_arr, err_arr]):
809
- cont_arr[0:arr_size - center_idx] = data_list[0][key_arr][1][0:arr_size - center_idx]
810
- cont_arr[arr_size - center_idx:] = data_list[0][key_arr][0]
811
- # print(key_arr, np.any(np.isnan(cont_arr)))
838
+
839
+ wave_arr, flux_arr, err_arr = join_spectra_matrix(data_list[0]['WAVELENGTH'],
840
+ data_list[0]['FLUX'],
841
+ data_list[0]['ERROR'])
842
+
843
+ # # Get common middle index for joining the spectra
844
+ # # wave_matrix = data_list[0]['WAVELENGTH'][::-1]
845
+ # idcs_common = np.nonzero(data_list[0]['WAVELENGTH'][1, :] > data_list[0]['WAVELENGTH'][0, 0])[0]
846
+ # center_idx = idcs_common.shape[0] // 2
847
+ #
848
+ # # Create empty containers
849
+ # wave_arr = np.empty(data_list[0]['WAVELENGTH'].size - center_idx, data_list[0]['WAVELENGTH'].dtype) # TODO check for additional extension to join the spectra
850
+ # flux_arr = np.empty(data_list[0]['FLUX'].size - center_idx, data_list[0]['FLUX'].dtype) # dtype=data_list[0]['FLUX'].dtype)
851
+ # err_arr = np.empty(data_list[0]['ERROR'].size - center_idx, data_list[0]['ERROR'].dtype) # dtype=data_list[0]['ERROR'].dtype)
852
+ #
853
+ # # Fill with the array data
854
+ # arr_size = data_list[0]['WAVELENGTH'].shape[1]
855
+ # for key_arr, cont_arr in zip(['WAVELENGTH', 'FLUX', 'ERROR'], [wave_arr, flux_arr, err_arr]):
856
+ # cont_arr[0:arr_size - center_idx] = data_list[0][key_arr][1][0:arr_size - center_idx]
857
+ # cont_arr[arr_size - center_idx:] = data_list[0][key_arr][0]
858
+ # # print(key_arr, np.any(np.isnan(cont_arr)))
812
859
 
813
860
  # Spectrum properties
814
861
  params_dict = SPECTRUM_FITS_PARAMS['cos']
@@ -149,9 +149,6 @@ def redshift_key_method(spec, bands, z_min, z_max, delta_z, pred_arr, components
149
149
  if spec.res_power is not None:
150
150
  res_power = spec.res_power
151
151
  else:
152
- # wave_arr / np.r_[np.diff(wave_arr), np.diff(wave_arr)[-1]]
153
- # alpha_lambda = np.diff(wave_arr)
154
- # res_power = wave_arr / np.r_[alpha_lambda, alpha_lambda[-1]]
155
152
  delta_lambda = np.ediff1d(wave_arr, to_end=0)
156
153
  delta_lambda[-1] = delta_lambda[-2]
157
154
  res_power = wave_arr / delta_lambda
@@ -166,8 +163,6 @@ def redshift_key_method(spec, bands, z_min, z_max, delta_z, pred_arr, components
166
163
  z_arr = np.arange(z_min, z_max + 0.5 * delta_z, delta_z)
167
164
 
168
165
  # Parameters for the brute analysis
169
- # z_arr = np.linspace(z_min, z_max, z_nsteps)
170
- # z_arr = np.arange(z_min, z_max, step=0.005)
171
166
  wave_matrix = np.tile(wave_arr, (theo_lambda.size, 1))
172
167
  flux_sum = np.zeros(z_arr.size)
173
168
 
@@ -197,7 +192,7 @@ def redshift_key_method(spec, bands, z_min, z_max, delta_z, pred_arr, components
197
192
 
198
193
  if plot_results and (z_infer is not None):
199
194
  gauss_arr_max = compute_gaussian_ridges(z_infer, theo_lambda, wave_matrix, 1, band_vsigma, res_power)
200
- redshift_key_evaluation(spec, method, z_infer, mask, gauss_arr_max, z_arr, flux_sum)
195
+ redshift_key_evaluation(spec, method, z_infer, mask, gauss_arr_max, z_arr, flux_sum, theo_lambda)
201
196
 
202
197
  return z_infer
203
198
 
@@ -141,7 +141,6 @@ def parse_lime_cfg(toml_cfg, fit_cfg_suffix='_line_fitting'):
141
141
  return toml_cfg
142
142
 
143
143
 
144
- # Function to load configuration file
145
144
  def load_cfg(file_address, fit_cfg_suffix='_line_fitting'):
146
145
 
147
146
  """
@@ -206,7 +205,6 @@ def load_cfg(file_address, fit_cfg_suffix='_line_fitting'):
206
205
  return cfg_lime
207
206
 
208
207
 
209
- # Function to save SpecSyzer configuration file
210
208
  def save_cfg(output_file, param_dict, section_name=None, clear_section=False):
211
209
 
212
210
  """
@@ -221,9 +219,32 @@ def save_cfg(output_file, param_dict, section_name=None, clear_section=False):
221
219
 
222
220
  # TODO review convert numpy arrays and floats64
223
221
  if toml_check:
224
- toml_dict = param_dict if section_name is None else {section_name: param_dict}
225
- with open(output_file, "w") as f:
226
- toml.dump(toml_dict, f)
222
+
223
+ # Section dict or the default dictionary
224
+ output_data = param_dict if section_name is None else {section_name: param_dict}
225
+
226
+ # If the file does not exist create a new file
227
+ if not output_path.is_file():
228
+ with open(output_file, "w") as f:
229
+ toml.dump(output_data, f)
230
+
231
+ # Load the file and add the new section
232
+ else:
233
+ with open(output_path, 'r') as f:
234
+ full_config = toml.load(f)
235
+
236
+ # Update the section data
237
+ full_config.update(output_data)
238
+ # if section_name is not None:
239
+ # full_config.update(output_data)
240
+ #
241
+ # # Add the new data
242
+ # else:
243
+ # full_config.update(output_data)
244
+
245
+ # Save the new data
246
+ with open(output_file, "w") as f:
247
+ toml.dump(full_config, f)
227
248
 
228
249
  else:
229
250
  raise LiMe_Error(f'toml library is not installed. Toml files cannot be saved')
@@ -538,7 +559,7 @@ def save_frame(fname, dataframe, page='FRAME', parameters='all', header=None, co
538
559
 
539
560
  lineLogHDU = log_to_HDU(lines_log, ext_name=page, column_dtypes=column_dtypes, header_dict=header)
540
561
 
541
- if log_path.is_file(): # TODO this strategy is slow for many 2_guides
562
+ if log_path.is_file(): # TODO this strategy is slow for many configuration
542
563
  try:
543
564
  fits.update(log_path, data=lineLogHDU.data, header=lineLogHDU.header, extname=lineLogHDU.name, verify=True)
544
565
  except KeyError:
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = 'lime-stable'
3
- version = "2.0.5"
3
+ version = "2.2.dev2"
4
4
 
5
5
  # =====================
6
6
  # Spectrum / Long-slit
@@ -7,6 +7,7 @@ from pathlib import Path
7
7
  from astropy.io import fits
8
8
  from collections import UserDict
9
9
 
10
+ from lime.fitting.lines import profiles_computation, linear_continuum_computation
10
11
  from lime.tools import extract_fluxes, normalize_fluxes, ProgressBar, check_units, extract_wcs_header, \
11
12
  parse_unit_convertion
12
13
 
@@ -157,6 +158,7 @@ def check_spectra_arrays(observation):
157
158
 
158
159
  return
159
160
 
161
+
160
162
  def check_redshift_norm(redshift, norm_flux, flux_array, units_flux, norm_factor=1, min_flux_scale=0.001, max_flux_scale=1e50):
161
163
 
162
164
  if (redshift is None) or np.isnan(redshift) or np.isinf(redshift):
@@ -226,10 +228,10 @@ def check_sample_levels(levels, necessary_levels=("id", "file")):
226
228
  return
227
229
 
228
230
 
229
- def cropping_spectrum(crop_waves, input_wave, input_flux, input_err, pixel_mask):
231
+ def cropping_spectrum(crop_waves, crop_flux, input_wave, input_flux, input_err, pixel_mask):
230
232
 
231
233
  if crop_waves is not None:
232
-
234
+ # TODO warning message
233
235
  idx_min = np.searchsorted(input_wave, crop_waves[0]) if crop_waves[0] != 0 else 0
234
236
  idx_max = np.searchsorted(input_wave, crop_waves[1]) if crop_waves[1] != -1 else None
235
237
 
@@ -256,6 +258,23 @@ def cropping_spectrum(crop_waves, input_wave, input_flux, input_err, pixel_mask)
256
258
  if pixel_mask is not None:
257
259
  pixel_mask = pixel_mask[idcs_crop[0]:idcs_crop[1]]
258
260
 
261
+ if crop_flux is not None:
262
+
263
+ # Validate percentiles
264
+ for perctl, name in zip(crop_flux, ("Min percentile", "Max percentile")):
265
+ if not np.isscalar(perctl):
266
+ raise TypeError(f"{name} must be a scalar.")
267
+ if not (0.0 <= perctl <= 100):
268
+ raise ValueError(f"{name} must be in the range [0, 100].")
269
+
270
+ if crop_flux[0] >= crop_flux[1]:
271
+ raise ValueError(f"The lower percentile limit ({crop_flux[0]}) must be must be smaller than upper limit "
272
+ f"({crop_flux[1]}).")
273
+
274
+ # Clip the flux to the limits
275
+ lo, hi = np.percentile(input_flux[~pixel_mask], crop_flux)
276
+ input_flux[~pixel_mask] = np.clip(input_flux[~pixel_mask], lo, hi)
277
+
259
278
  return input_wave, input_flux, input_err, pixel_mask
260
279
 
261
280
 
@@ -416,7 +435,8 @@ class Spectrum:
416
435
  _fitsMgr = None
417
436
 
418
437
  def __init__(self, input_wave=None, input_flux=None, input_err=None, redshift=None, norm_flux=None, crop_waves=None,
419
- res_power=None, units_wave='AA', units_flux='FLAM', pixel_mask=None, id_label=None, review_inputs=True):
438
+ crop_flux=None, res_power=None, units_wave='AA', units_flux='FLAM', pixel_mask=None, id_label=None,
439
+ review_inputs=True):
420
440
 
421
441
  # Class attributes
422
442
  self.label = None
@@ -447,7 +467,7 @@ class Spectrum:
447
467
 
448
468
  # Review and assign the attibutes data
449
469
  if review_inputs:
450
- self._set_attributes(input_wave, input_flux, input_err, redshift, norm_flux, crop_waves, res_power,
470
+ self._set_attributes(input_wave, input_flux, input_err, redshift, norm_flux, crop_waves, crop_flux, res_power,
451
471
  units_wave, units_flux, pixel_mask, id_label)
452
472
 
453
473
  return
@@ -503,11 +523,6 @@ class Spectrum:
503
523
  # Class attributes
504
524
  spec.label = label
505
525
  spec.flux = cube.flux[:, idx_j, idx_i]
506
- # from matplotlib import pyplot as plt
507
- # fig, ax = plt.subplots()
508
- # ax.imshow(cube.err_flux[500, :, :].data)
509
- # plt.show()
510
-
511
526
  spec.err_flux = None if cube.err_flux is None else cube.err_flux[:, idx_j, idx_i]
512
527
  spec.norm_flux = cube.norm_flux
513
528
  spec.redshift = cube.redshift
@@ -526,7 +541,7 @@ class Spectrum:
526
541
  return spec
527
542
 
528
543
  @classmethod
529
- def from_file(cls, fname, instrument, redshift=None, norm_flux=None, crop_waves=None, res_power=None,
544
+ def from_file(cls, fname, instrument, redshift=None, norm_flux=None, crop_waves=None, crop_flux=None, res_power=None,
530
545
  units_wave=None, units_flux=None, pixel_mask=None, id_label=None, wcs=None, **kwargs):
531
546
 
532
547
  """
@@ -623,8 +638,8 @@ class Spectrum:
623
638
  fits_args = cls._fitsMgr.parse_data_from_file(cls._fitsMgr.file_address, pixel_mask, **kwargs)
624
639
 
625
640
  # Update the file parameters with the user parameters
626
- input_args = dict(redshift=redshift, norm_flux=norm_flux, crop_waves=crop_waves, res_power=res_power,
627
- units_wave=units_wave, units_flux=units_flux, id_label=id_label, wcs=wcs)
641
+ input_args = dict(redshift=redshift, norm_flux=norm_flux, crop_waves=crop_waves, crop_flux=crop_flux,
642
+ res_power=res_power, units_wave=units_wave, units_flux=units_flux, id_label=id_label, wcs=wcs)
628
643
 
629
644
  if cls._fitsMgr.spectrum_check:
630
645
  input_args.pop('wcs')
@@ -675,8 +690,8 @@ class Spectrum:
675
690
  # Create the LiMe object
676
691
  return cls(**fits_args)
677
692
 
678
- def _set_attributes(self, input_wave, input_flux, input_err, redshift, norm_flux, crop_waves, res_power, units_wave,
679
- units_flux, pixel_mask, label):
693
+ def _set_attributes(self, input_wave, input_flux, input_err, redshift, norm_flux, crop_waves, crop_flux, res_power,
694
+ units_wave, units_flux, pixel_mask, label):
680
695
 
681
696
  # Class attributes
682
697
  self.label = label
@@ -691,8 +706,8 @@ class Spectrum:
691
706
  self.redshift, self.norm_flux = check_redshift_norm(redshift, norm_flux, input_flux, self.units_flux)
692
707
 
693
708
  # Crop the input spectrum if necessary
694
- input_wave, input_flux, input_err, pixel_mask = cropping_spectrum(crop_waves, input_wave, input_flux, input_err,
695
- pixel_mask)
709
+ input_wave, input_flux, input_err, pixel_mask = cropping_spectrum(crop_waves, crop_flux, input_wave, input_flux,
710
+ input_err, pixel_mask)
696
711
 
697
712
  # Normalization and masking
698
713
  self.wave, self.wave_rest, self.flux, self.err_flux = spec_normalization_masking(input_wave, input_flux,
@@ -925,6 +940,116 @@ class Spectrum:
925
940
 
926
941
  return
927
942
 
943
+ def save_spectrum(self, fname=None, line_label=None, ref_frame=None, split_components=False, **kwargs):
944
+
945
+ # Headers for the default list
946
+ headers = np.array(["wave", "flux", "err_flux", "pixel_mask"])
947
+
948
+ # Use the observation frame if none is provided
949
+ frame = self.frame if ref_frame is None else ref_frame
950
+
951
+ # By default report complete spectrum
952
+ idcs = (0, None)
953
+
954
+ # If a line is provided get indexes for the bands limits
955
+ line_measured = False
956
+ if line_label is not None:
957
+ if frame is not None:
958
+ if line_label in frame.index:
959
+ bands_limits = frame.loc[line_label, 'w1':'w6']
960
+ idcs_bands = np.searchsorted(self._spec.wave.data, bands_limits * (1 + self._spec.redshift))
961
+ idcs = (idcs_bands[0], idcs_bands[5])
962
+ line_measured = True
963
+ else:
964
+ _logger.warning(f'Line {line_label} not found on observation frame')
965
+ else:
966
+ _logger.warning(f'No lines measured on object')
967
+
968
+ # Compute the bands
969
+ if line_measured:
970
+
971
+ # Declare line object and the components and its components from the frame
972
+ line = Line.from_transition(line_label, data_frame=frame)
973
+ line_list = line.list_comps
974
+
975
+ # Compute the linear components
976
+ gaussian_arr = profiles_computation(line_list, frame, 1 + self._spec.redshift, line.profile,
977
+ x_array=self._spec.wave.data[idcs[0]: idcs[1]])
978
+ linear_arr = linear_continuum_computation(line_list, frame, 1 + self._spec.redshift, x_array=self._spec.wave.data[idcs[0]: idcs[1]])
979
+
980
+ # Determine which component you want to extract:
981
+ if split_components is False:
982
+ gaussian_arr = gaussian_arr.sum(axis=1) + linear_arr[:, 0]
983
+ gaussian_arr = gaussian_arr.reshape(-1, 1)
984
+ line_hdrs = [line_label]
985
+ else:
986
+ gaussian_arr = gaussian_arr + linear_arr[:, 0][:, np.newaxis]
987
+ line_hdrs = line_list
988
+
989
+ # Add the line list to the headers
990
+ headers = np.append(headers, line_hdrs)
991
+
992
+ # Container for the data
993
+ out_arr = np.full((self.wave.data[idcs[0]: idcs[1]].size, len(headers)), np.nan)
994
+
995
+ # Fill the array:
996
+ out_arr[:, 0] = self.wave.data[idcs[0]: idcs[1]]
997
+ out_arr[:, 1] = self.flux.data[idcs[0]: idcs[1]] * self.norm_flux
998
+
999
+ # Err array if it exists
1000
+ if self.err_flux is not None:
1001
+ out_arr[:, 2] = self.err_flux[idcs[0]: idcs[1]].data * self.norm_flux
1002
+
1003
+ # Pixel mask if any is invalid
1004
+ if np.any(self.wave.mask):
1005
+ out_arr[:, 3] = self.wave[idcs[0]: idcs[1]].mask
1006
+
1007
+ # Add the components
1008
+ if line_measured:
1009
+ for i, line_comp in enumerate(line_hdrs):
1010
+ out_arr[:, 4 + i] = gaussian_arr[:, i]
1011
+
1012
+ # Crop array if some columns are missing
1013
+ nan_columns = np.zeros(out_arr.shape[1]).astype(bool)
1014
+ nan_columns[:4] = np.all(np.isnan(out_arr[:, :4]), axis=0)
1015
+ out_arr = out_arr[:, ~nan_columns]
1016
+
1017
+ # Headers
1018
+ headers = headers[~nan_columns]
1019
+
1020
+ # Formatting for the data
1021
+ spec_hdrs_list = ['%.18e', '%.18e', '%.18e', '%d']
1022
+ spec_hdrs_list = spec_hdrs_list + ['%.18e'] * len(line_hdrs) if line_measured else spec_hdrs_list
1023
+ array_fmt = np.array(spec_hdrs_list)
1024
+ array_fmt = list(array_fmt[~nan_columns])
1025
+
1026
+ # Update defaults with user-provided values
1027
+ default_kwargs = {"fmt": array_fmt, "delimiter": ' '}
1028
+ default_kwargs.update(kwargs)
1029
+
1030
+ # Create header
1031
+ if default_kwargs.get('header') is None:
1032
+ default_kwargs['header'] = default_kwargs['delimiter'].join(headers)
1033
+
1034
+ # Dictionary with parameters
1035
+ if 'footer' not in default_kwargs:
1036
+ footer_dict = {'LiMe': f"v{lime_cfg['metadata']['version']}",
1037
+ 'units_wave': self.units_wave, 'units_flux': self.units_flux,
1038
+ 'redshift': self.redshift, 'norm_flux': self.norm_flux, 'id_label': self.label}
1039
+ footer_str = "\n".join(f"{key}:{value}" for key, value in footer_dict.items())
1040
+ default_kwargs['footer'] = footer_str
1041
+
1042
+ # Return a recarray with the spectrum data
1043
+ if fname is None:
1044
+ output = np.core.records.fromarrays([out_arr[:, i] for i in range(out_arr.shape[1])], names=list(headers))
1045
+
1046
+ # Save to a file
1047
+ else:
1048
+ np.savetxt(fname, out_arr, **default_kwargs)
1049
+ output = None
1050
+
1051
+ return output
1052
+
928
1053
  def update_redshift(self, redshift):
929
1054
 
930
1055
  """
@@ -982,20 +1107,23 @@ class Spectrum:
982
1107
 
983
1108
  return
984
1109
 
985
- def line_detection(self, *args, **kwargs):
986
-
987
- raise LiMe_Error(f'The line_detection functionality has been moved an rebranded. Please use:\n'
988
- f'Spectrum.infer.peaks_troughs()')
989
-
990
- def clear_data(self):
1110
+ def clear_data(self, line_data=True, cont_data=True):
991
1111
 
992
1112
  """
993
- Clear the spectrum’s measurements frame.
1113
+ Clear the spectrum’s line measurements frame and fitted continuum.
994
1114
 
995
1115
  This method removes all entries from the internal ``frame`` attribute,
996
1116
  effectively resetting the stored measurements while preserving the
997
1117
  dataframe structure (columns and metadata).
998
1118
 
1119
+ Parameters
1120
+ ----------
1121
+ line_data : bool, optional
1122
+ Clear the spectrum’s line measurements frame. The default value is true.
1123
+
1124
+ cont_data : bool, optional
1125
+ Clear the spectrum’s fitted continuum. The default value is true.
1126
+
999
1127
  Returns
1000
1128
  -------
1001
1129
  None
@@ -1005,6 +1133,7 @@ class Spectrum:
1005
1133
  -----
1006
1134
  - The operation is equivalent to reassigning ``self.frame = self.frame[0:0]``,
1007
1135
  which clears all rows but keeps column definitions intact.
1136
+ - The Spectrum.cont and cont_std variables are set to None.
1008
1137
  - Use this method to reset the spectrum’s measurement results before
1009
1138
  reprocessing or refitting without recreating the object.
1010
1139
 
@@ -1017,7 +1146,12 @@ class Spectrum:
1017
1146
  (0, 10)
1018
1147
  """
1019
1148
 
1020
- self.frame = self.frame[0:0]
1149
+ if line_data:
1150
+ self.frame = self.frame[0:0]
1151
+
1152
+ if cont_data:
1153
+ self.cont = None
1154
+ self.cont_std = None
1021
1155
 
1022
1156
  return
1023
1157
 
@@ -1105,9 +1239,10 @@ class Cube:
1105
1239
  _fitsMgr = None
1106
1240
 
1107
1241
  def __init__(self, input_wave=None, input_flux=None, input_err=None, redshift=None, norm_flux=None, crop_waves=None,
1108
- res_power=None, units_wave='AA', units_flux='FLAM', pixel_mask=None, id_label=None, wcs=None):
1242
+ crop_flux=None, res_power=None, units_wave='AA', units_flux='FLAM', pixel_mask=None, id_label=None,
1243
+ wcs=None):
1109
1244
 
1110
- # Review the 2_guides
1245
+ # Review the inputs
1111
1246
  pixel_mask = check_inputs_arrays(input_wave, input_flux, input_err, pixel_mask, self)
1112
1247
 
1113
1248
  # Class attributes
@@ -1133,8 +1268,8 @@ class Cube:
1133
1268
  self.redshift, self.norm_flux = check_redshift_norm(redshift, norm_flux, input_flux, self.units_flux)
1134
1269
 
1135
1270
  # Start cropping the input spectrum if necessary
1136
- input_wave, input_flux, input_err, pixel_mask = cropping_spectrum(crop_waves, input_wave, input_flux, input_err,
1137
- pixel_mask)
1271
+ input_wave, input_flux, input_err, pixel_mask = cropping_spectrum(crop_waves, crop_flux, input_wave, input_flux,
1272
+ input_err, pixel_mask)
1138
1273
 
1139
1274
  # Spectrum normalization, redshift and mask calculation
1140
1275
  self.wave, self.wave_rest, self.flux, self.err_flux = spec_normalization_masking(input_wave, input_flux,
@@ -1336,7 +1471,7 @@ class Cube:
1336
1471
  >>> cube.spatial_masking("H1_4861A", param="SN_line", contour_pctls=[80, 90, 95])
1337
1472
  """
1338
1473
 
1339
- # Check the function 2_guides
1474
+ # Check the function inputs
1340
1475
  contour_pctls = np.atleast_1d(contour_pctls)
1341
1476
  if not np.all(np.diff(contour_pctls) > 0):
1342
1477
  raise LiMe_Error(f'The mask percentiles ({contour_pctls}) must be in increasing order')
@@ -1561,7 +1696,7 @@ class Cube:
1561
1696
  --------
1562
1697
  Extract a single spaxel spectrum from a cube:
1563
1698
 
1564
- >>> spec = cube.spectrum_from_indices(25, 30)
1699
+ >>> spec = cube.get_spectrum(25, 30)
1565
1700
 
1566
1701
  """
1567
1702