lime-stable 2.0.dev7__tar.gz → 2.0.dev8__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 (51) hide show
  1. {lime_stable-2.0.dev7/src/lime_stable.egg-info → lime_stable-2.0.dev8}/PKG-INFO +1 -1
  2. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/pyproject.toml +1 -1
  3. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/archives/read_fits.py +44 -38
  4. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/fitting/lines.py +2 -15
  5. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/io.py +4 -1
  6. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/lime.toml +1 -1
  7. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/observations.py +12 -5
  8. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/plotting/plots.py +4 -40
  9. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/plotting/plots_interactive.py +1 -1
  10. lime_stable-2.0.dev8/src/lime/retrieve/line_bands.py +226 -0
  11. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/transitions.py +67 -60
  12. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/workflow.py +9 -300
  13. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8/src/lime_stable.egg-info}/PKG-INFO +1 -1
  14. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime_stable.egg-info/SOURCES.txt +1 -0
  15. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/tests/test_model.py +1 -1
  16. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/tests/test_read_fits.py +33 -0
  17. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/tests/test_spectrum.py +13 -6
  18. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/tests/test_tools.py +4 -4
  19. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/LICENSE.rst +0 -0
  20. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/MANIFEST.in +0 -0
  21. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/README.rst +0 -0
  22. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/setup.cfg +0 -0
  23. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/__init__.py +0 -0
  24. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/archives/__init__.py +0 -0
  25. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/archives/tables.py +0 -0
  26. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/changelog.txt +0 -0
  27. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/fitting/__init__.py +0 -0
  28. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/fitting/redshift.py +0 -0
  29. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/inference/detection.py +0 -0
  30. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/inference/intensity_threshold.py +0 -0
  31. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/plotting/__init__.py +0 -0
  32. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/plotting/bokeh_plots.py +0 -0
  33. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/plotting/format.py +0 -0
  34. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/plotting/theme_lime.toml +0 -0
  35. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/plotting/utils.py +0 -0
  36. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/resources/__init__.py +0 -0
  37. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/resources/lines_database_formatting.py +0 -0
  38. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/resources/lines_database_v2.0.0.txt +0 -0
  39. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/resources/logo.py +0 -0
  40. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/resources/types_params.txt +0 -0
  41. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/retrieve/__init__.py +0 -0
  42. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/retrieve/peaks.py +0 -0
  43. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime/tools.py +0 -0
  44. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime_stable.egg-info/dependency_links.txt +0 -0
  45. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime_stable.egg-info/requires.txt +0 -0
  46. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/src/lime_stable.egg-info/top_level.txt +0 -0
  47. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/tests/test_astro.py +0 -0
  48. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/tests/test_cube.py +0 -0
  49. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/tests/test_io.py +0 -0
  50. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/tests/test_line.py +0 -0
  51. {lime_stable-2.0.dev7 → lime_stable-2.0.dev8}/tests/test_sample.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lime-stable
3
- Version: 2.0.dev7
3
+ Version: 2.0.dev8
4
4
  Summary: Line measuring algorithm for astronomical spectra
5
5
  Author-email: Vital Fernández <vgf@umich.edu>
6
6
  License-Expression: GPL-3.0-or-later
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lime-stable"
7
- version = "2.0.dev7"
7
+ version = "2.0.dev8"
8
8
  readme = "README.rst"
9
9
  requires-python = ">=3.11"
10
10
  license = "GPL-3.0-or-later"
@@ -257,35 +257,44 @@ def check_fits_instructions(fits_source, online_provider=False):
257
257
  else:
258
258
  fits_reader = None
259
259
 
260
- # # Check for url location for surveys function
261
- # if online_provider:
262
- # if hasattr(UrlFitsSurvey, fits_source):
263
- # url_locator = getattr(UrlFitsSurvey, fits_source)
264
- # else:
265
- # raise LiMe_Error(f'Input {fits_source} does not have a url manager for LiMe could not be created.')
266
- # else:
267
- # url_locator = None
268
-
269
260
  return fits_reader
270
261
 
271
- def load_txt(text_address):
262
+ def load_txt(text_address, **kwargs):
272
263
 
273
264
  # Columns
274
- out_array = np.loadtxt(text_address)
265
+ out_array = np.loadtxt(text_address, **kwargs)
275
266
 
276
- # Transform foot comments as dictionary data
277
- params_dict = {}
278
- with open(text_address, "r") as f:
267
+ # File address
268
+ if not type(text_address).__name__ == "UploadedFile":
269
+ with open(text_address, "r") as f:
270
+ lines = f.readlines()
279
271
 
280
- # Reverse loop while the lines start by a "#"
281
- for line in reversed(f.readlines()):
282
- line = line.strip()
283
- if not line.startswith("#") or (line.startswith("# LiMe")):
284
- break
272
+ # Uploaded file
273
+ else:
274
+ lines = text_address.getvalue().decode("utf-8").splitlines()
285
275
 
286
- # Extract key-value pairs
287
- key, value = line[1:].split(":", 1) # Split at the first ':'
288
- params_dict[key.strip()] = value.strip()
276
+ # Reverse loop over the lines
277
+ params_dict = {}
278
+ for line in reversed(lines):
279
+ line = line.strip()
280
+ if not line.startswith("#") or line.startswith("# LiMe"):
281
+ break
282
+ key, value = line[1:].split(":", 1)
283
+ params_dict[key.strip()] = value.strip()
284
+
285
+ # # Transform foot comments as dictionary data
286
+ # params_dict = {}
287
+ # with open(text_address, "r") as f:
288
+ #
289
+ # # Reverse loop while the lines start by a "#"
290
+ # for line in reversed(f.readlines()):
291
+ # line = line.strip()
292
+ # if not line.startswith("#") or (line.startswith("# LiMe")):
293
+ # break
294
+ #
295
+ # # Extract key-value pairs
296
+ # key, value = line[1:].split(":", 1) # Split at the first ':'
297
+ # params_dict[key.strip()] = value.strip()
289
298
 
290
299
  return out_array, params_dict
291
300
 
@@ -362,10 +371,10 @@ class OpenFits:
362
371
 
363
372
  return
364
373
 
365
- def parse_data_from_file(self, file_address, pixel_mask=None):
374
+ def parse_data_from_file(self, file_address, pixel_mask, **kwargs):
366
375
 
367
376
  # Read the fits data
368
- wave_array, flux_array, err_array, header_list, fits_params = self.fits_reader(file_address)
377
+ wave_array, flux_array, err_array, header_list, fits_params = self.fits_reader(file_address, **kwargs)
369
378
  pixel_mask = pixel_mask if pixel_mask is not None else fits_params['pixel_mask']
370
379
 
371
380
  # Mask requested entries
@@ -441,10 +450,10 @@ class OpenFits:
441
450
  return fits_args
442
451
 
443
452
  @staticmethod
444
- def text(file_address):
453
+ def text(file_address, **kwargs):
445
454
 
446
455
  # Read text file dividing the columns into the spectrum axis and the comments as its parameters
447
- data_arr, params_dict = load_txt(file_address)
456
+ data_arr, params_dict = load_txt(file_address, **kwargs)
448
457
 
449
458
  # Unpack the columns into the spectrum axes
450
459
  wave_array, flux_array = data_arr[:, 0], data_arr[:, 1]
@@ -456,16 +465,13 @@ class OpenFits:
456
465
  # Convert strings to expected format
457
466
  params_dict['redshift'] = float(params_dict['redshift']) if 'redshift' in params_dict else None
458
467
  params_dict['norm_flux'] = float(params_dict['norm_flux']) if 'norm_flux' in params_dict else None
459
- params_dict['id_label'] = params_dict['id_label'] if 'norm_flux' in params_dict else None
468
+ params_dict['id_label'] = params_dict['id_label'] if 'id_label' in params_dict else None
460
469
  params_dict['pixel_mask'] = mask_array
461
470
 
462
- # metadata['units_wave'] = au.Unit(metadata['units_wave']) if 'units_wave' in metadata else au.Unit('AA')
463
- # metadata['units_flux'] = au.Unit(metadata['units_flux']) if 'units_flux' in metadata else au.Unit('FLAM')
464
-
465
471
  return wave_array, flux_array, err_array, None, params_dict
466
472
 
467
473
  @staticmethod
468
- def nirspec(fits_address, data_ext_list=1, hdr_ext_list=(0, 1), pixel_mask=None):
474
+ def nirspec(fits_address, data_ext_list=1, hdr_ext_list=(0, 1), **kwargs):
469
475
 
470
476
  """
471
477
 
@@ -503,7 +509,7 @@ class OpenFits:
503
509
  return wave_array, flux_array, err_array, header_list, params_dict
504
510
 
505
511
  @staticmethod
506
- def isis(fits_address, data_ext_list=0, hdr_ext_list=0, pixel_mask=None):
512
+ def isis(fits_address, data_ext_list=0, hdr_ext_list=0, **kwargs):
507
513
 
508
514
  """
509
515
 
@@ -548,7 +554,7 @@ class OpenFits:
548
554
  return wave_array, flux_array, err_array, header_list, params_dict
549
555
 
550
556
  @staticmethod
551
- def osiris(fits_address, data_ext_list=0, hdr_ext_list=0, pixel_mask=None):
557
+ def osiris(fits_address, data_ext_list=0, hdr_ext_list=0, **kwargs):
552
558
 
553
559
  """
554
560
 
@@ -593,7 +599,7 @@ class OpenFits:
593
599
  return wave_array, flux_array, err_array, header_list, params_dict
594
600
 
595
601
  @staticmethod
596
- def sdss(fits_address, data_ext_list=(1, 2), hdr_ext_list=(0), pixel_mask=None):
602
+ def sdss(fits_address, data_ext_list=(1, 2), hdr_ext_list=(0), **kwargs):
597
603
 
598
604
  """
599
605
 
@@ -645,7 +651,7 @@ class OpenFits:
645
651
  return wave_array, flux_array, err_array, header_list, params_dict
646
652
 
647
653
  @staticmethod
648
- def manga(fits_address, data_ext_list=('WAVE', 'FLUX', 'IVAR'), hdr_ext_list=('FLUX'), pixel_mask=None):
654
+ def manga(fits_address, data_ext_list=('WAVE', 'FLUX', 'IVAR'), hdr_ext_list=('FLUX'), **kwargs):
649
655
 
650
656
  """
651
657
 
@@ -692,7 +698,7 @@ class OpenFits:
692
698
  return wave_array, flux_cube, err_cube, header_list, fits_params
693
699
 
694
700
  @staticmethod
695
- def muse(fits_address, data_ext_list=(1, 2), hdr_ext_list=1, pixel_mask=None):
701
+ def muse(fits_address, data_ext_list=(1, 2), hdr_ext_list=1, **kwargs):
696
702
 
697
703
  """
698
704
 
@@ -737,7 +743,7 @@ class OpenFits:
737
743
  return wave_array, flux_cube, err_cube, header_list, fits_params
738
744
 
739
745
  @staticmethod
740
- def megara(fits_address, data_ext_list=0, hdr_ext_list=(0, 1), pixel_mask=None):
746
+ def megara(fits_address, data_ext_list=0, hdr_ext_list=(0, 1), **kwargs):
741
747
 
742
748
  """
743
749
 
@@ -781,7 +787,7 @@ class OpenFits:
781
787
  return wave_array, flux_cube, err_cube, header_list, fits_params
782
788
 
783
789
  @staticmethod
784
- def miri(fits_address, data_ext_list=(1,2), hdr_ext_list=(1), pixel_mask=None):
790
+ def miri(fits_address, data_ext_list=(1,2), hdr_ext_list=(1), **kwargs):
785
791
 
786
792
  """
787
793
 
@@ -608,7 +608,6 @@ class ProfileModelCompiler:
608
608
  line.eqw = np.full(self.n_comps, np.nan)
609
609
  line.eqw_err = np.full(self.n_comps, np.nan)
610
610
  line.FWHM_p = np.full(self.n_comps, np.nan)
611
- # line.sigma_thermal = np.full(self.n_comps, np.nan)
612
611
 
613
612
  # Check for negative -0.0 # TODO this needs a better place # FIXME -0.0 error
614
613
  if np.signbit(line.sigma_err[i]):
@@ -825,12 +824,6 @@ class LineFitting:
825
824
  line.intg_flux = areasArray.mean()
826
825
  line.intg_flux_err = areasArray.std()
827
826
 
828
- # # Compute the integrated signal to noise # TODO is this an issue for absorptions
829
- # amp_ref = line.peak_flux - line.cont
830
- # if emission_check:
831
- # if amp_ref < 0:
832
- # amp_ref = line.peak_flux
833
-
834
827
  # Compute SN_r
835
828
  line.snr_line = signal_to_noise_rola(line.peak_flux - line.cont, line.cont_err, line.n_pixels)
836
829
  line.snr_cont = line.cont/line.cont_err
@@ -842,20 +835,14 @@ class LineFitting:
842
835
  else:
843
836
  line._narrow_check = False
844
837
 
845
- # # Line width to the pixel below the continuum (or mask size if not happening) # TODO Lime2.0 skip all this if narrow
846
- # idx_0 = compute_FWHM0(peakIdx, emis_flux, -1, cont_arr, emission_check)
847
- # idx_f = compute_FWHM0(peakIdx, emis_flux, 1, cont_arr, emission_check)
848
- #
849
- # # Velocity calculations
850
- # velocArray = c_KMpS * (emis_wave[idx_0:idx_f] - line.peak_wave) / line.peak_wave
851
- # self.velocity_profile_calc(line, velocArray, emis_flux[idx_0:idx_f], cont_arr[idx_0:idx_f], emission_check)
838
+ # Velocity calculations
852
839
  if (line.n_pixels >= min_array_dim) and (line._narrow_check is False):
853
840
  self.velocity_profile_calc(line, peakIdx, emis_wave, emis_flux, cont_arr, emission_check, min_array_dim=min_array_dim)
854
841
 
855
842
  # Pixel velocity # TODO we are not using this one
856
843
  line.pixel_vel = c_KMpS * line.pixelWidth/line.peak_wave
857
844
 
858
- # Equivalent width computation (it must be an 1d array to avoid conflict in blended lines) # TODO Lime2.0 put all this on its function
845
+ # Equivalent width computation (it must be an 1d array to avoid conflict in blended lines)
859
846
  lineContinuumMatrix = cont_arr + normalNoise
860
847
  eqwMatrix = areasArray / lineContinuumMatrix.mean(axis=1)
861
848
 
@@ -569,10 +569,13 @@ def results_to_log(line, log, norm_flux):
569
569
 
570
570
  # Converting None entries to str (9 = group_label)
571
571
  if j == 9:
572
+
572
573
  if param_value is None:
573
574
  param_value = 'none'
574
575
 
575
- # print(comp, param, param_value)
576
+ if line.sub_comps[i] is not None:
577
+ param_value = line.sub_comps[i].group_label
578
+
576
579
  log.at[comp, param] = param_value
577
580
 
578
581
  return
@@ -1,3 +1,3 @@
1
1
  [metadata]
2
2
  name = 'lime-stable'
3
- version = "2.0.dev7"
3
+ version = "2.0.dev8"
@@ -411,7 +411,8 @@ class Spectrum:
411
411
  return spec
412
412
 
413
413
  @classmethod
414
- def from_file(cls, file_address, instrument, mask_flux_entries=None, **kwargs):
414
+ def from_file(cls, file_address, instrument, redshift=None, norm_flux=None, crop_waves=None, res_power=None,
415
+ units_wave=None, units_flux=None, pixel_mask=None, id_label=None, wcs=None, **kwargs):
415
416
 
416
417
  """
417
418
 
@@ -446,13 +447,19 @@ class Spectrum:
446
447
  cls._fitsMgr = OpenFits(file_address, instrument, cls.__name__)
447
448
 
448
449
  # Load the scientific data from the file
449
- fits_args = cls._fitsMgr.parse_data_from_file(cls._fitsMgr.file_address, mask_flux_entries)
450
+ fits_args = cls._fitsMgr.parse_data_from_file(cls._fitsMgr.file_address, pixel_mask, **kwargs)
450
451
 
451
- # Update the parameters file parameters with the user parameters
452
- obs_args = {**fits_args, **kwargs}
452
+ # Update the file parameters with the user parameters
453
+ input_args = dict(redshift=redshift, norm_flux=norm_flux, crop_waves=crop_waves, res_power=res_power,
454
+ units_wave=units_wave, units_flux=units_flux, id_label=id_label, wcs=wcs)
455
+
456
+ if cls._fitsMgr.spectrum_check:
457
+ input_args.pop('wcs')
458
+
459
+ input_args = {**fits_args, **{k: v for k, v in input_args.items() if v is not None}}
453
460
 
454
461
  # Create the LiMe object
455
- return cls(**obs_args)
462
+ return cls(**input_args)
456
463
 
457
464
  @classmethod
458
465
  def from_survey(cls, target_id, survey, mask_flux_entries=None, **kwargs):
@@ -657,9 +657,10 @@ def redshift_permu_evaluation(spectrum, z_infered, obs_wave_arr, theo_wave_arr,
657
657
  return
658
658
 
659
659
 
660
- def bands_filling_plot(axis, x, y, z_corr, idcs_mask, label, exclude_continua=False, color_dict=theme.colors, show_central=True):
660
+ def bands_filling_plot(axis, x, y, z_corr, idcs_mask, label, exclude_continua=True, color_dict=theme.colors, show_central=True):
661
661
 
662
662
  # Security check for low selection
663
+ # TODO check this error crashing
663
664
  if y[idcs_mask[2]:idcs_mask[3]].size > 1:
664
665
 
665
666
  # Lower limit for the filled region
@@ -768,8 +769,8 @@ class Plotter:
768
769
  def _line_matching_plot(self, axis, bands, x, y, z_corr, redshift):
769
770
 
770
771
  # Open the bands file the bands
771
- match_log = self._spec.retrieve.line_bands(ref_bands=bands, fit_cfg=None, instrumental_correction=False,
772
- adjust_central_band=False)
772
+ match_log = check_file_dataframe(bands)
773
+
773
774
  # Compute bands limits
774
775
  w3 = match_log.w3.values * (1 + redshift)
775
776
  w4 = match_log.w4.values * (1 + redshift)
@@ -1008,43 +1009,6 @@ class SpectrumFigures(Plotter):
1008
1009
  in_ax.fill_between(wave_plot/z_corr, low_limit*z_corr, high_limit*z_corr, alpha=0.2,
1009
1010
  color=theme.colors['fade_fg'])
1010
1011
 
1011
- # # Include the detection bands
1012
- # if detection_band is not None:
1013
- #
1014
- # detec_obj = getattr(self._spec.infer, detection_band)
1015
- #
1016
- # if detec_obj.confidence is not None:
1017
- #
1018
- # # Boundaries array for confidence intervals
1019
- # bounds = np.array([0.0, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
1020
- #
1021
- # # Adjust color map to match lower detection limit to fg color
1022
- # cmap = plt.get_cmap(theme.colors['mask_map'])
1023
- # cmaplist = [cmap(i) for i in range(cmap.N)]
1024
- # cmaplist[0] = theme.colors['fg']
1025
- # cmap = colors.LinearSegmentedColormap.from_list('mcm', cmaplist, bounds.size-1)
1026
- # norm = colors.BoundaryNorm(bounds * 100, cmap.N)
1027
- #
1028
- # # Iterate through the confidence intervals and plot the step spectrum
1029
- # for i in range(1, len(bounds)):
1030
- # if i > 1:
1031
- # idcs = detec_obj(bounds[i-1]*100, confidence_max=bounds[i]*100)
1032
- # wave_nan, flux_nan = np.full(wave_plot.size, np.nan), np.full(flux_plot.size, np.nan)
1033
- # wave_nan[idcs], flux_nan[idcs] = wave_plot[idcs] / z_corr, flux_plot[idcs] * z_corr
1034
- #
1035
- # in_ax.step(wave_nan, flux_nan, label=label, where='mid', color=cmap(i-1))
1036
- #
1037
- # # Color bar
1038
- # sm = cm.ScalarMappable(cmap=cmap, norm=norm)
1039
- # sm.set_array([])
1040
- # cbar = plt.colorbar(sm, ax=in_ax)
1041
- # cbar.set_label('Detection confidence %', rotation=270, labelpad=35)
1042
- #
1043
- #
1044
- # else:
1045
- # _logger.warning(f'The line detection bands confidence has not been calculated. They are not included'
1046
- # f' on plot.')
1047
-
1048
1012
  # Show components
1049
1013
  if show_categories and self._spec.infer.pred_arr is not None:
1050
1014
 
@@ -1536,7 +1536,7 @@ class CubeInspection:
1536
1536
  self._ax0.set_ylim(self.axlim_dict['image_ylim'])
1537
1537
  self._ax1.set_xlim(self.axlim_dict['spec_xlim'])
1538
1538
 
1539
- if not self.maintain_y_zoom:
1539
+ if self.maintain_y_zoom:
1540
1540
  self._ax1.set_ylim(self.axlim_dict['spec_ylim'])
1541
1541
 
1542
1542
  else:
@@ -0,0 +1,226 @@
1
+ import logging
2
+ import numpy as np
3
+ import pandas as pd
4
+ from lime.io import LiMe_Error
5
+ from lime.transitions import Line
6
+
7
+
8
+ _logger = logging.getLogger('LiMe')
9
+
10
+
11
+ def pars_bands_conf(spec, bands, fit_conf, composite_lines, automatic_grouping=True):
12
+
13
+ # Use the input groups
14
+ if automatic_grouping is False:
15
+
16
+ # Get the the grouped lines
17
+ groups_dict = {} if fit_conf is False else {comp: group_label
18
+ for comp, group_label in fit_conf.items()
19
+ if comp.endswith(('_b', '_m'))}
20
+
21
+ # Limit the selection to the user lines
22
+ if composite_lines is not None:
23
+ groups_dict = {line: comps for line, comps in groups_dict.items() if line in composite_lines}
24
+
25
+ # Automatic group review
26
+ else:
27
+
28
+ # Check the input dataframe is sorted
29
+ if not np.all(np.diff(bands.wavelength) >= 0):
30
+ _logger.warning(f'The input bands table is not sorted. This can cause issues in the bands generation:'
31
+ f'\n{bands["wavelength"]}')
32
+
33
+ # Get list all the line groups and their lines
34
+ if fit_conf:
35
+
36
+ line_list, group_lines = [], []
37
+ group_names, group_blended_check = [], []
38
+ for comp, group_label in fit_conf.items():
39
+ if comp.endswith(('_b', '_m')):
40
+
41
+ # Homogeneous group
42
+ if '_m' not in group_label:
43
+ lines_i = group_label.split('+')
44
+ groups_i = [True] * (len(lines_i) - 1) if comp[-2:] == '_b' else [False] * (len(lines_i) - 1)
45
+
46
+ # Mixed group (merged child line in the blended parent group)
47
+ else:
48
+ lines_i, groups_i = [], []
49
+ for i, line in enumerate(group_label.split('+')):
50
+ if line[-2:] != '_m': # Single line
51
+ lines_i.append(line)
52
+ groups_i.append(True)
53
+ else: # Merged line
54
+ sub_group_label = fit_conf.get(line)
55
+ if sub_group_label:
56
+ items = sub_group_label.split('+')
57
+ lines_i += items
58
+ groups_i += [False] * len(items)
59
+ else:
60
+ raise LiMe_Error(f'The merged line: "{line}" in grouped line: "{comp}={group_label}" '
61
+ f'is not specified.\nPlease define a "{line}=LineA+LineB" '
62
+ f'in your configuration file.')
63
+
64
+ # Convert the sub_group_type to the relation
65
+ groups_i = np.array(groups_i)
66
+ groups_i = groups_i[:-1] != groups_i[1:]
67
+
68
+ # Add the group is all lines (sorted) in current wavelength range
69
+ idcs_i = bands.index.get_indexer(lines_i)
70
+ if np.all(idcs_i > -1):
71
+ group_names.append(comp)
72
+ group_lines.append(bands.loc[bands.index.isin(lines_i)].index.to_numpy())
73
+ group_blended_check.append(groups_i)
74
+ line_list += lines_i
75
+
76
+ # Sort the input lines using line banbs table
77
+ line_list = bands.loc[bands.index.isin(line_list)].index
78
+ sub_bands = bands.loc[line_list]
79
+ lambda_arr = sub_bands['wavelength'].to_numpy()
80
+
81
+ # Array to keep track of lines which have been assigned:
82
+ assigned_lines = np.zeros(line_list.size).astype(bool)
83
+
84
+ # Compare the observed line groups
85
+ groups_dict = {}
86
+ if line_list.size > 1:
87
+
88
+ # Get limits of the bands on the spectrum wavelength range
89
+ w3_arr = np.searchsorted(spec.wave_rest.data, bands.loc[line_list, 'w3'].to_numpy())
90
+ w4_arr = np.searchsorted(spec.wave_rest.data, bands.loc[line_list, 'w4'].to_numpy())
91
+
92
+ # Generate binary matrix with the line bands location
93
+ wave_matrix = np.zeros((lambda_arr.size, spec.wave_rest.data.size))
94
+ cols = np.arange(wave_matrix.shape[1])
95
+ wave_matrix[(cols >= w3_arr[:, None]) & (cols <= w4_arr[:, None])] = 1
96
+
97
+ # Compute the decision matrix with the common pixels
98
+ decision_matrix = wave_matrix @ wave_matrix.T
99
+
100
+ # pixels_width = wave_matrix.sum(axis=1)
101
+ # blended_matrix = decision_matrix < np.ceil(pixels_width/3)[:, None]
102
+ # math_dict = dict(zip(line_arr, np.arange(line_arr.size)))
103
+
104
+ # Loop through the input groups to confirm the best match
105
+ for i, group in enumerate(group_names):
106
+
107
+ # Diagnostic to establish the relation between the lines
108
+ threshold = 2
109
+ w3_arr = np.searchsorted(spec.wave_rest.data, bands.loc[group_lines[i], 'w3'].to_numpy())
110
+ w4_arr = np.searchsorted(spec.wave_rest.data, bands.loc[group_lines[i], 'w4'].to_numpy())
111
+ mu = (w3_arr + w4_arr) / 2
112
+ sigma = (w4_arr - w3_arr) / 6
113
+ delta_mu = np.diff(mu)
114
+ sigma_avg = np.sqrt((sigma[:-1] ** 2 + sigma[1:] ** 2) / 2)
115
+ R = delta_mu / sigma_avg
116
+ resolvable = R > threshold
117
+
118
+ match_group = True if np.all(resolvable == group_blended_check[i]) else False
119
+
120
+ # Before saving group check that there are no more lines grouped in the observation
121
+ if match_group:
122
+ idcs_i = sub_bands.index.get_indexer(group_lines[i])
123
+ if np.all(np.sum(decision_matrix[idcs_i, :] > 0, axis=1) == idcs_i.size):
124
+ groups_dict[group] = fit_conf[group]
125
+ assigned_lines[idcs_i] = True
126
+
127
+ # Invalid
128
+ else:
129
+ _logger.warning(f'The user requested automatic_grouping for the line transitions but the "fit_conf" is empty')
130
+
131
+ # Applyt the requested group changes
132
+ rename_dict, exclude_list= {}, []
133
+ group_dict, w3_dict, w4_dict = {}, {}, {}
134
+ for new_label, group_label in groups_dict.items():
135
+
136
+ component_list = np.unique([Line(x).core for x in group_lines[group_names.index(new_label)] if '_k-' not in x])
137
+ old_label = component_list[0]
138
+
139
+ # Only apply corrections if components are present
140
+ idcs_comps = bands.index.isin(component_list)
141
+ if np.sum(idcs_comps) == component_list.size:
142
+
143
+ # Save the modifications
144
+ rename_dict[old_label] = new_label
145
+ exclude_list += list(component_list)
146
+ w3_dict[new_label] = bands.loc[idcs_comps, 'w3'].min()
147
+ w4_dict[new_label] = bands.loc[idcs_comps, 'w4'].max()
148
+ group_dict[new_label] = group_label
149
+
150
+ # Check the line or the same group is already there
151
+ else:
152
+ low, high = spec.wave_rest.compressed()[[0, -1]]
153
+ check_arr = np.array([(low < Line(label).wavelength < high)[0] for label in component_list])
154
+
155
+ # Lines outside wavelength range
156
+ if np.all(~check_arr):
157
+ continue
158
+
159
+ # Check if lines outside range
160
+ else:
161
+ if 'group_label' in bands.columns:
162
+ if not np.any(groups_dict[new_label] == bands.group_label):
163
+ _logger.info(f'Line component "{old_label}" for configuration entry: '
164
+ f'"{new_label}={groups_dict[new_label]}" not found in lines table')
165
+ else:
166
+ _logger.info(f'Missing line(s) "{np.setxor1d(bands.loc[idcs_comps].index.to_numpy(), component_list)}" '
167
+ f'for configuration entry: '
168
+ f'"{new_label}={groups_dict[new_label]}" in reference lines table')
169
+
170
+ # Warn in case some of the bands dont match the database:
171
+ if not set(exclude_list).issubset(bands.index):
172
+ _logger.info(f' The following blended or merged lines were not found on the input lines database:\n'
173
+ f' - {list(set(exclude_list) - set(bands.index))}\n'
174
+ f' - It is recommended that the merged/blended components follow the reference transitions labels.\n')
175
+
176
+ # Change the latex labels
177
+ for old_label, new_label in rename_dict.items():
178
+ line = Line(new_label, band=bands, fit_conf=groups_dict, update_latex=True)
179
+ bands.loc[old_label, 'latex_label'] = line.latex_label[0] if line.merged_check else '+'.join(line.latex_label)
180
+
181
+ # Change the indexes
182
+ bands.rename(index=dict(rename_dict), inplace=True)
183
+
184
+ # Remove components columns
185
+ bands.drop(exclude_list, errors='ignore', inplace=True)
186
+
187
+ # Add the group_label values
188
+ if 'group_label' not in bands.columns:
189
+ bands['group_label'] = 'none'
190
+ bands['group_label'] = pd.Series(bands.index.map(group_dict), index=bands.index).fillna(bands['group_label'])
191
+
192
+ # Change velocity limits
193
+ bands['w3'] = pd.Series(bands.index.map(w3_dict), index=bands.index).fillna(bands['w3'])
194
+ bands['w4'] = pd.Series(bands.index.map(w4_dict), index=bands.index).fillna(bands['w4'])
195
+
196
+ return
197
+
198
+ # if np.all(idcs_i > -1):
199
+
200
+ # # Logic for single, merged and blended lines
201
+ # shared_pixels = decision_matrix[idcs_i[:-1], idcs_i[1:]]
202
+ # if np.any(shared_pixels > 0):
203
+ # match_group =False
204
+ # # line_pixels = np.max(pixels_width[idcs_i])
205
+ # # diag_arr = resolvable
206
+ # #
207
+ # # obs_type = ['_b'] if np.all(diag_arr) else ['_m'] if np.all(~diag_arr) else None
208
+ # else:
209
+ # match_group = False
210
+
211
+ # Compare observed group versus user group
212
+
213
+ # else:
214
+ #
215
+ # # Lines not assigned before:
216
+ # if np.all(assigned_lines[idcs_i] == False):
217
+ #
218
+ # # Group consists in blended merged lines: Assigned single merged
219
+ # if (group_name[-2:] == '_m') and np.any(diag_arr[:-1] & diag_arr[1:]):
220
+ # output_groups[group_name] = group_label
221
+ # assigned_lines[idcs_i] = True
222
+ # # Get groups of common entries
223
+ # from scipy.sparse import csr_matrix, csgraph
224
+ # _, auto_labels = csgraph.connected_components(csgraph=csr_matrix(decision_matrix > 1), directed=False)
225
+ # for labels, group in zip(line_arr, auto_labels):
226
+ # print(f"{labels} {group}")