lime-stable 2.0.dev7__py3-none-any.whl → 2.0.dev8__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.
@@ -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
 
lime/fitting/lines.py CHANGED
@@ -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
 
lime/io.py CHANGED
@@ -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
lime/lime.toml CHANGED
@@ -1,3 +1,3 @@
1
1
  [metadata]
2
2
  name = 'lime-stable'
3
- version = "2.0.dev7"
3
+ version = "2.0.dev8"
lime/observations.py CHANGED
@@ -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):
lime/plotting/plots.py CHANGED
@@ -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}")
lime/transitions.py CHANGED
@@ -185,7 +185,7 @@ def air_to_vacuum_function(wave_array, units_wave='AA'):
185
185
  return wave_array / (1 + 1e-6 * (287.6155 + 1.62887 * sigma2 + 0.01360 * np.square(sigma2)))
186
186
 
187
187
 
188
- def check_units_from_wave(line, str_ion, str_wave, bands, ref_bands=None):
188
+ def check_units_from_wave(line, str_ion, str_wave, bands):
189
189
 
190
190
  # First the input database
191
191
  if bands is not None:
@@ -202,16 +202,14 @@ def check_units_from_wave(line, str_ion, str_wave, bands, ref_bands=None):
202
202
 
203
203
  # Convert to units
204
204
  units = au.Unit(units) if units is not None else units
205
- # units = None if (units is None or np.isnan(units)) else au.Unit(units)
206
205
 
207
206
  else:
208
207
  wave, units = None, None
209
208
 
210
209
  # Second the reference database
211
210
  if (units is None) or (wave is None):
212
- ref_bands = ref_bands if ref_bands is not None else _PARENT_BANDS
213
- wave = pd_get(ref_bands, line, 'wavelength', nan_to_none=True)
214
- units = pd_get(ref_bands, line, 'units_wave', nan_to_none=True)
211
+ wave = pd_get(_PARENT_BANDS, line, 'wavelength', nan_to_none=True)
212
+ units = pd_get(_PARENT_BANDS, line, 'units_wave', nan_to_none=True)
215
213
 
216
214
  # Convert to units
217
215
  units = au.Unit(units) if units is not None else units
@@ -229,10 +227,6 @@ def check_units_from_wave(line, str_ion, str_wave, bands, ref_bands=None):
229
227
  units = au_unit.bases[0]
230
228
  wave = au_unit.scale
231
229
 
232
- # # Give preferences to the tabel values
233
- # wave = wave_label if wave is None else wave
234
- # units = units_label if units is None else units
235
-
236
230
  return wave, units
237
231
 
238
232
 
@@ -407,7 +401,7 @@ def latex_from_label(label, particle=None, wave=None, units_wave=None, kinem=Non
407
401
  #
408
402
 
409
403
 
410
- def label_composition(line_list, bands=None, default_profile=None, ref_bands=None):
404
+ def label_composition(line_list, bands=None, default_profile=None):
411
405
 
412
406
  # Empty containers for the label componentes
413
407
  n_comps = len(line_list)
@@ -436,7 +430,7 @@ def label_composition(line_list, bands=None, default_profile=None, ref_bands=Non
436
430
  particle[i] = Particle.from_label(line_items[0])
437
431
 
438
432
  # Wavelength properties
439
- wavelength[i], units_wave[i] = check_units_from_wave(line, line_items[0], line_items[1], bands, ref_bands)
433
+ wavelength[i], units_wave[i] = check_units_from_wave(line, line_items[0], line_items[1], bands)
440
434
 
441
435
  # Split the optional components: "H1_1216A_t-rec_k-0_p-g" -> {'t': 'rec', 'k': '0', 'p': 'g'} # TODO better do that with optional_comps
442
436
  comp_conf = {optC[0]: optC[2:] for optC in line_items[2:]}
@@ -765,25 +759,26 @@ class Particle:
765
759
 
766
760
  class Line:
767
761
 
768
- def __init__(self, label, band=None, fit_conf=None, profile=None, cont_from_bands=True, z_line=None,
769
- update_latex=False, ref_bands=None, interpret=True):
762
+ def __init__(self, label, band=None, fit_conf=None, profile=None, update_latex=False):
770
763
 
771
- # Label attributes
764
+ # Core attributes
772
765
  self.label = label
773
766
  self.mask = None
774
767
  self.latex_label = None,
775
768
  self.group_label, self.list_comps = None, None
769
+ self.sub_comps = None
776
770
 
777
771
  self.particle = None
778
772
  self.wavelength, self.units_wave = None, None
779
773
  self.blended_check, self.merged_check = False, False
780
774
 
775
+ # Transition components
781
776
  self.kinem = None
782
- self.profile_comp = profile
783
- self.transition_comp = None
784
777
  self.core = None
785
-
786
778
  self._ref_idx = None
779
+
780
+ self.transition_comp = None
781
+ self.profile_comp = profile
787
782
  self._p_type = None
788
783
  self._p_shape = None
789
784
 
@@ -802,7 +797,7 @@ class Line:
802
797
  self.alpha, self.beta = None, None
803
798
  self.alpha_err, self.beta_err = None, None
804
799
  self.frac, self.frac_err = None, None
805
- self.z_line = z_line
800
+ self.z_line = None
806
801
  self.v_r, self.v_r_err = None, None
807
802
  self.pixel_vel = None
808
803
  self.sigma_vel, self.sigma_vel_err = None, None
@@ -822,71 +817,55 @@ class Line:
822
817
  self.pixelWidth = None
823
818
 
824
819
  # Extra checks
825
- self._cont_from_adjacent = cont_from_bands
826
- self._decimal_wave = False
820
+ # self._cont_from_adjacent = cont_from_bands
821
+ # self._decimal_wave = False
827
822
  self._narrow_check = False
828
823
 
829
- # Interpret the line from the user reference
830
- if interpret:
831
- self._from_label(label, band, fit_conf, update_latex, ref_bands)
824
+ # Recover the line data from the input databases
825
+ self._get_containers_data(label,
826
+ _PARENT_BANDS if band is None else check_file_dataframe(band, copy_input=False),
827
+ fit_conf,
828
+ update_latex)
832
829
 
833
830
  return
834
831
 
835
832
  def __str__(self):
836
-
837
833
  return self.label
838
834
 
839
835
  def __repr__(self):
840
-
841
836
  return self.label
842
837
 
843
- def _from_label(self, label, band=None, fit_conf=None, update_latex=False, ref_bands=None):
838
+ def __eq__(self, var):
839
+ return self.label == var if isinstance(var, str) else False
844
840
 
845
- # If band is not provided use default database
846
- if band is None:
847
- band = _PARENT_BANDS
848
- band = check_file_dataframe(band, copy_input=False)
841
+ def __hash__(self):
842
+ return hash(self.label)
843
+
844
+ def _get_containers_data(self, label, band=None, fit_conf=None, update_latex=False):
849
845
 
850
846
  # Infer label from log if input line is a wavelength
851
847
  self.label = check_line_in_log(label, band)
852
848
 
853
- # Get label components
854
- comps_list = self.label.split('_')
855
- n_comps = len(comps_list)
856
- if n_comps < 2:
857
- raise LiMe_Error(f'The {self.label} the line label format is not recognized. '
858
- f'Please use a "Particle_WavelengthUnits" format.')
859
-
860
- # Check the modularity (only 2 comps) (case b-6)
861
- modularity_comp = comps_list[-1] if (comps_list[-1] == 'b') or (comps_list[-1] == 'm') else None
862
- self._modularity_component(modularity_comp, fit_conf, band)
849
+ # Get the transitions involved on the line
850
+ self.line_group_review(fit_conf, band)
863
851
 
864
852
  # Review the components of the line
865
- items = label_composition(self.list_comps, bands=band if isinstance(band, DataFrame) else None,
866
- default_profile=self.profile_comp)
867
- self.particle, self.wavelength, self.units_wave, self.kinem, self.profile_comp, self.transition_comp = items
853
+ components = label_composition(self.list_comps,
854
+ band if isinstance(band, DataFrame) else None,
855
+ self.profile_comp)
856
+ self.particle, self.wavelength, self.units_wave, self.kinem, self.profile_comp, self.transition_comp = components
868
857
 
869
858
  # Quick elements for the line profile
870
859
  self._p_shape, self._p_type = label_profiling(self.profile_comp)
871
860
 
872
- # Index of the line closest to the one in label
873
- if self.blended_check or self.merged_check:
874
- wave_label, _label_units = check_units_from_wave(None, None, comps_list[1], None)
875
- self._ref_idx = argmin(self.wavelength - wave_label)
876
- else:
877
- self._ref_idx = 0
878
-
879
- # Core component definition
880
- self.core = f'{comps_list[0]}_{comps_list[1]}'
881
-
882
861
  # Provide a bands from the log if possible
883
862
  self.mask = label_mask_assigning(self.label, band, self.blended_check, self.merged_check, self.core)
884
863
 
885
864
  # Check if there are masked pixels in the line
886
865
  self.pixel_mask = 'no' if fit_conf is None else fit_conf.get(f'{self.label}_mask', 'no')
887
866
 
888
- # Check if the wavelength has decimal transition
889
- self._decimal_wave = True if '.' in self.label else False
867
+ # Check sub-line components
868
+ self.sub_transitions(fit_conf, band)
890
869
 
891
870
  # Compute latex entry if necessary
892
871
  self.latex_label = np.atleast_1d(self._review_science_notation(band if isinstance(band, DataFrame) else None,
@@ -898,13 +877,14 @@ class Line:
898
877
  def from_log(cls, label, log=None, norm_flux=1):
899
878
 
900
879
  # Confirm we are not introducing a blended line which has been blended
901
-
902
880
  if label in log.index:
903
881
  measured_check = True
882
+
904
883
  elif (label[-2:] == '_b') and (label[:-2] in log.index):
905
884
  _logger.warning(f'Blended line {label} not found in log, reading {label[:-2]}')
906
885
  label = label[:-2]
907
886
  measured_check = True
887
+
908
888
  else:
909
889
  _logger.warning(f'Input line {label} not found in log')
910
890
  measured_check = False
@@ -932,7 +912,17 @@ class Line:
932
912
 
933
913
  return inline
934
914
 
935
- def _modularity_component(self, modularity_label, fit_conf=None, bands_log=None):
915
+ def line_group_review(self, fit_conf=None, bands_log=None):
916
+
917
+ # Establish the components
918
+ comps_list = self.label.split('_')
919
+ n_comps = len(comps_list)
920
+ if n_comps < 2:
921
+ raise LiMe_Error(f'The {self.label} the line label format is not recognized. '
922
+ f'Please use a "Particle_WavelengthUnits" format.')
923
+
924
+ # Check the line type
925
+ modularity_label = comps_list[-1] if (comps_list[-1] == 'b') or (comps_list[-1] == 'm') else None
936
926
 
937
927
  # Not a single line
938
928
  if modularity_label is not None:
@@ -951,10 +941,8 @@ class Line:
951
941
  group_label_cfg = None if fit_conf is None else fit_conf.get(self.label, None)
952
942
  group_label_frame = None if not isinstance(bands_log, pd.DataFrame) else pd_get(bands_log, self.label,
953
943
  column='group_label', transform='none')
954
-
955
- self.group_label = group_label_cfg if group_label_cfg is not None else group_label_frame
956
-
957
944
  # Confirm blended or merged
945
+ self.group_label = group_label_cfg if group_label_cfg is not None else group_label_frame
958
946
  self.blended_check = True if (self.group_label is not None) and (self.merged_check is False) else False
959
947
 
960
948
  # Recover the profile components
@@ -982,6 +970,15 @@ class Line:
982
970
  else:
983
971
  self.list_comps = [self.label]
984
972
 
973
+ # Core component definition
974
+ self.core = f'{comps_list[0]}_{comps_list[1]}'
975
+
976
+ # Index of core component
977
+ if self.blended_check or self.merged_check:
978
+ self._ref_idx = self.list_comps.index(self.core) if self.core in self.list_comps else 0
979
+ else:
980
+ self._ref_idx = 0
981
+
985
982
  return
986
983
 
987
984
  def _review_science_notation(self, bands, update_latex=False, decimals=None):
@@ -1074,6 +1071,16 @@ class Line:
1074
1071
  else:
1075
1072
  return idcs_bands
1076
1073
 
1074
+ def sub_transitions(self, fit_cfg, bands):
1075
+
1076
+ self.sub_comps = [None] * len(self.list_comps)
1077
+ if self.blended_check and fit_cfg:
1078
+ for i, sub_stransition in enumerate(self.list_comps):
1079
+ if sub_stransition[-2:] == '_m':
1080
+ self.sub_comps[i] = Line(sub_stransition, bands, fit_cfg)
1081
+
1082
+ return
1083
+
1077
1084
  # def index_bands_orig(self, wavelength_array, redshift, merge_continua=True, just_band_edges=False):
1078
1085
  #
1079
1086
  # if self.mask is None:
lime/workflow.py CHANGED
@@ -10,10 +10,10 @@ from lmfit.models import PolynomialModel
10
10
  from lime.fitting.lines import LineFitting, signal_to_noise_rola, sigma_corrections, k_gFWHM, velocity_to_wavelength_band, profiles_computation, linear_continuum_computation
11
11
  from lime.tools import ProgressBar, join_fits_files, extract_wcs_header, pd_get, unit_conversion
12
12
  from lime.transitions import Line, air_to_vacuum_function, label_decomposition
13
+ from lime.retrieve.line_bands import pars_bands_conf
13
14
  from lime.io import check_file_dataframe, check_file_array_mask, log_to_HDU, results_to_log, load_frame, LiMe_Error, check_fit_conf, _PARENT_BANDS
14
15
  from lime.fitting.redshift import RedshiftFitting
15
16
  from lime import __version__
16
- from scipy.sparse import csr_matrix, csgraph
17
17
 
18
18
 
19
19
 
@@ -341,296 +341,6 @@ def res_power_approx(wavelength_arr):
341
341
  return wavelength_arr/delta_lambda
342
342
 
343
343
 
344
- def get_merged_blended_lines(spec, bands, in_cfg, composite_lines):
345
-
346
- # Get lines on the list
347
- comps_dict = {} if in_cfg is False else {comp: group_label
348
- for comp, group_label in in_cfg.items()
349
- if comp.endswith(('_b', '_m'))}
350
-
351
- # Limit the selection to the user lines
352
- if composite_lines is not None:
353
- comps_dict = {line: comps for line, comps in comps_dict.items() if line in composite_lines}
354
-
355
- # # Determine the grouped lines
356
- # key_cfg = f'{default_cfg_prefix}_line_fitting'
357
- # default_dict = {} if key_cfg not in in_cfg else {comp: group_label
358
- # for comp, group_label in in_cfg[key_cfg].items()
359
- # if comp.endswith(('_b', '_m'))}
360
- #
361
- # key_cfg = f'{obj_cfg_prefix}_line_fitting'
362
- # obj_dict = {} if key_cfg not in in_cfg else{comp: group_label
363
- # for comp, group_label in in_cfg[key_cfg].items()
364
- # if comp.endswith(('_b', '_m'))}
365
- #
366
- # if len(default_dict) == 0 and len(obj_dict) == 0:
367
- # output_dict = fit_conf.copy()
368
- # else:
369
- # output_dict = {**default_dict, **obj_dict}
370
-
371
- # b_m_arr = np.array(list(output_dict.keys()))
372
- # core_arr = np.array([s[:-2] for s in b_m_arr])
373
- # unique_elements, counts = np.unique(core_arr, return_counts=True)
374
- # repeated_lines = unique_elements[counts > 1]
375
- #
376
- # # Check each case individually
377
- # for line in repeated_lines:
378
- # line_blended, line_merged = line + '_b', line + '_m'
379
- # default_b, default_m = line_blended in default_dict, line_merged in default_dict
380
- # obj_b, obj_m = line_blended in obj_dict, line_merged in obj_dict
381
- #
382
- # # Object configuration has priority
383
- # if not (obj_b == obj_m):
384
- # output_dict.pop(line_blended if obj_b else line_merged)
385
- #
386
- # # Try default configuration
387
- # elif not (default_b == default_m):
388
- # output_dict.pop(line_blended if default_b else line_merged)
389
- #
390
- # # Solve the kinematics
391
- # else:
392
- #
393
- # # Get components
394
- # comps = output_dict[line_blended].split('+')
395
- #
396
- # # Check if more than one component is on the bands
397
- # df_comps = bands.loc[bands.index.isin(comps)]
398
- # if df_comps.index.size > 1:
399
- #
400
- # # Set minimum number of pixels for the model solution in the bands
401
- # central_band = bands.loc[bands.index.isin(comps), 'w3':'w4'].to_numpy() * (1 + spec.redshift)
402
- # idcs_central = np.searchsorted(spec.wave, (central_band[:, 0].min(), central_band[:, 1].max()))
403
- # blended_check = idcs_central[1] - idcs_central[0] > (3 * len(comps))
404
- #
405
- # # Print warning
406
- # _logger.info(f'The input configuration has merged and blended entries for {line}. '
407
- # f'Automatic assignment as {"blended" if blended_check else "Merged"} ({line_blended if blended_check else line_blended}'
408
- # f'={output_dict[line_blended if blended_check else line_blended]})')
409
- # output_dict.pop(line_blended if blended_check else line_blended)
410
-
411
- # # Get candidate lines for grouping
412
- # line_arr = [item for v in output_dict.values() for item in v.split('+')]
413
- # line_arr = bands.loc[bands.index.isin(line_arr)].index
414
- # lambda_arr = bands.loc[line_arr, 'wavelength'].to_numpy()
415
- #
416
- # # Generate the line - wavelength matrix with the line pixels width
417
- # wave_matrix = np.zeros((lambda_arr.size, spec.wave_rest.data.size))
418
- # w3_arr = np.searchsorted(spec.wave_rest.data, bands.loc[line_arr, 'w3'].to_numpy())
419
- # w4_arr = np.searchsorted(spec.wave_rest.data, bands.loc[line_arr, 'w4'].to_numpy())
420
- # cols = np.arange(wave_matrix.shape[1])
421
- # wave_matrix[(cols >= w3_arr[:, None]) & (cols <= w4_arr[:, None])] = 1
422
- # pixels_width = wave_matrix.sum(axis=1)
423
- #
424
- # # Get the decision matrix with the matching intervals
425
- # decision_matrix = wave_matrix @ wave_matrix.T
426
- # blended_matrix = decision_matrix < np.ceil(pixels_width/3)[:, None]
427
- # math_dict = dict(zip(line_arr, np.arange(line_arr.size)))
428
- #
429
- # results_dict = {}
430
- # for label, group_label in output_dict.items():
431
- # list_group = group_label.split('+')
432
- # if all(key in math_dict for key in list_group):
433
- # blended_group = True if label[-1] == 'b' else False
434
- # relations = np.array([blended_matrix[math_dict[i], math_dict[j]] for i, j in list(combinations(list_group, 2))])
435
- # if np.all(relations == blended_group):
436
- # print(f'Passess: {label}={group_label}')
437
- # results_dict[label] = group_label
438
-
439
- return comps_dict
440
-
441
-
442
-
443
-
444
-
445
- # upper_triangle < (np.ceil(pixels_width/3)[:, None]=
446
- #
447
- #
448
- # np.fill_diagonal(decision_matrix, 0) # optional
449
- #
450
- # # Step 2: Extract the upper triangle (excluding diagonal)
451
- # upper_triangle = np.ma.masked_array(np.triu(decision_matrix, k=1), mask=np.tril(np.ones_like(decision_matrix), k=0).astype(bool)).astype(int)
452
- #
453
- # # Step 3: Find (i, j) pairs with >1 shared steps
454
- # rows_i, rows_j = np.where(upper_triangle > 1)
455
- # edges = list(zip(line_arr[rows_i], line_arr[rows_j]))
456
- #
457
- # matrix < thresholds[:, None]
458
- #
459
- # # Step 3: Check if all values in each column satisfy the condition
460
- # idcs_type = upper_triangle < np.ceil(pixels_width/3)[:, None]
461
- # merged_idcs = np.all(idcs_type, axis=0)
462
- # blended_idcs = np.all(~idcs_type, axis=0)
463
- # #
464
- # math_dict = dict(zip(line_arr, np.arange(line_arr.size)))
465
- # for row in rows_j:
466
- # print(line_arr[row])
467
- #
468
- # for label, group_label in output_dict.keys():
469
- # list_group = group_label.split('+')
470
- # if len(list_group) == 2:
471
- # x_label, y_label = math_dict.get(list_group[0]), math_dict.get(list_group[1])
472
- # if (x_label is not None) and (y_label is not None):
473
- #
474
-
475
-
476
- # print(w3_arr)
477
- # print(w4_arr)
478
- #
479
- # wave_matrix[w3_arr:w4_arr, :]
480
- # # box_pixels =
481
- # #
482
- # # print(idcs_lines)
483
- # lambda_arr = bands.index.isin(line_arr)
484
- # wave_arr = spec.wave.data
485
-
486
- # Generate matrices array
487
-
488
- # b_m_arr = np.array(list(output_dict.keys()))
489
- # core_arr = np.array([s[:-2] for s in b_m_arr])
490
- # unique_elements, counts = np.unique(core_arr, return_counts=True)
491
- # repeated_lines = unique_elements[counts > 1]
492
- #
493
- # # Check each case individually
494
- # for line in repeated_lines:
495
- # line_blended, line_merged = line + '_b', line + '_m'
496
- # default_b, default_m = line_blended in default_dict, line_merged in default_dict
497
- # obj_b, obj_m = line_blended in obj_dict, line_merged in obj_dict
498
- #
499
- # # Object configuration has priority
500
- # if not (obj_b == obj_m):
501
- # output_dict.pop(line_blended if obj_b else line_merged)
502
- #
503
- # # Try default configuration
504
- # elif not (default_b == default_m):
505
- # output_dict.pop(line_blended if default_b else line_merged)
506
- #
507
- # # Solve the kinematics
508
- # else:
509
- #
510
- # # Get components
511
- # comps = output_dict[line_blended].split('+')
512
- #
513
- # # Check if more than one component is on the bands
514
- # df_comps = bands.loc[bands.index.isin(comps)]
515
- # if df_comps.index.size > 1:
516
- #
517
- # # Set minimum number of pixels for the model solution in the bands
518
- # central_band = bands.loc[bands.index.isin(comps), 'w3':'w4'].to_numpy() * (1 + spec.redshift)
519
- # idcs_central = np.searchsorted(spec.wave, (central_band[:, 0].min(), central_band[:, 1].max()))
520
- # blended_check = idcs_central[1] - idcs_central[0] > (3 * len(comps))
521
- #
522
- # # Print warning
523
- # _logger.info(f'The input configuration has merged and blended entries for {line}. '
524
- # f'Automatic assignment as {"blended" if blended_check else "Merged"} ({line_blended if blended_check else line_blended}'
525
- # f'={output_dict[line_blended if blended_check else line_blended]})')
526
- # output_dict.pop(line_blended if blended_check else line_blended)
527
- #
528
- #
529
- # return output_dict
530
-
531
- def pars_bands_conf(spec, bands, ref_bands, fit_conf, composite_lines, automatic_grouping=False):
532
-
533
- # Get the the grouped lines
534
- comps_dict = {} if fit_conf is False else {comp: group_label
535
- for comp, group_label in fit_conf.items()
536
- if comp.endswith(('_b', '_m'))}
537
-
538
- # Limit the selection to the user lines
539
- if composite_lines is not None:
540
- comps_dict = {line: comps for line, comps in comps_dict.items() if line in composite_lines}
541
-
542
- if automatic_grouping:
543
-
544
- # Get candidate lines for grouping
545
- line_arr = [item for v in comps_dict.values() for item in v.split('+')]
546
- line_arr = bands.loc[bands.index.isin(line_arr)].index
547
- lambda_arr = bands.loc[line_arr, 'wavelength'].to_numpy()
548
-
549
- # Generate the line - wavelength matrix with the line pixels width
550
- wave_matrix = np.zeros((lambda_arr.size, spec.wave_rest.data.size))
551
- w3_arr = np.searchsorted(spec.wave_rest.data, bands.loc[line_arr, 'w3'].to_numpy())
552
- w4_arr = np.searchsorted(spec.wave_rest.data, bands.loc[line_arr, 'w4'].to_numpy())
553
- cols = np.arange(wave_matrix.shape[1])
554
- wave_matrix[(cols >= w3_arr[:, None]) & (cols <= w4_arr[:, None])] = 1
555
- pixels_width = wave_matrix.sum(axis=1)
556
-
557
- # Get the decision matrix with the matching intervals
558
- decision_matrix = wave_matrix @ wave_matrix.T
559
- blended_matrix = decision_matrix < np.ceil(pixels_width/3)[:, None]
560
- math_dict = dict(zip(line_arr, np.arange(line_arr.size)))
561
-
562
- # Get groups of common entries
563
- _, auto_labels = csgraph.connected_components(csgraph=csr_matrix(decision_matrix > 1), directed=False)
564
- for labels, group in zip(line_arr, auto_labels):
565
- print(f"{labels} {group}")
566
-
567
- # Loop through the lines on the list and review which changes are necessary
568
- rename_dict, exclude_list= {}, []
569
- group_dict, w3_dict, w4_dict = {}, {}, {}
570
- for new_label, group_label in comps_dict.items():
571
-
572
- component_list = np.unique([Line(x).core for x in group_label.split('+') if '_k-' not in x])
573
- old_label = component_list[0]
574
-
575
- # Only apply corrections if components are present
576
- idcs_comps = bands.index.isin(component_list)
577
- if np.sum(idcs_comps) == component_list.size:
578
-
579
- # Save the modifications
580
- rename_dict[old_label] = new_label
581
- exclude_list += list(component_list)
582
- w3_dict[new_label] = bands.loc[idcs_comps, 'w3'].min()
583
- w4_dict[new_label] = bands.loc[idcs_comps, 'w4'].max()
584
- group_dict[new_label] = group_label
585
-
586
- # Check the line or the same group is already there
587
- else:
588
- low, high = spec.wave_rest.compressed()[[0, -1]]
589
- check_arr = np.array([(low < Line(label).wavelength < high)[0] for label in component_list])
590
-
591
- # Lines outside wavelength range
592
- if np.all(~check_arr):
593
- continue
594
-
595
- # Check if lines outside range
596
- else:
597
- if 'group_label' in bands.columns:
598
- if not np.any(comps_dict[new_label] == bands.group_label):
599
- _logger.info(f'Line component "{old_label}" for configuration entry: '
600
- f'"{new_label}={comps_dict[new_label]}" not found in lines table')
601
- else:
602
- _logger.info(f'Missing line(s) "{np.setxor1d(bands.loc[idcs_comps].index.to_numpy(), component_list)}" '
603
- f'for configuration entry: '
604
- f'"{new_label}={comps_dict[new_label]}" in reference lines table')
605
-
606
- # Warn in case some of the bands dont match the database:
607
- if not set(exclude_list).issubset(bands.index):
608
- _logger.info(f' The following blended or merged lines were not found on the input lines database:\n'
609
- f' - {list(set(exclude_list) - set(bands.index))}\n'
610
- f' - It is recommended that the merged/blended components follow the reference transitions labels.\n')
611
-
612
- # Change the latex labels
613
- for old_label, new_label in rename_dict.items():
614
- line = Line(new_label, band=bands, fit_conf=comps_dict, update_latex=True)
615
- bands.loc[old_label, 'latex_label'] = line.latex_label[0] if line.merged_check else '+'.join(line.latex_label)
616
-
617
- # Change the indexes
618
- bands.rename(index=dict(rename_dict), inplace=True)
619
-
620
- # Remove components columns
621
- bands.drop(exclude_list, errors='ignore', inplace=True)
622
-
623
- # Add the group_label values
624
- if 'group_label' not in bands.columns:
625
- bands['group_label'] = 'none'
626
- bands['group_label'] = pd.Series(bands.index.map(group_dict), index=bands.index).fillna(bands['group_label'])
627
-
628
- # Change velocity limits
629
- bands['w3'] = pd.Series(bands.index.map(w3_dict), index=bands.index).fillna(bands['w3'])
630
- bands['w4'] = pd.Series(bands.index.map(w4_dict), index=bands.index).fillna(bands['w4'])
631
-
632
- return
633
-
634
344
 
635
345
  class SpecRetriever:
636
346
 
@@ -641,7 +351,7 @@ class SpecRetriever:
641
351
  return
642
352
 
643
353
  def line_bands(self, band_vsigma=70, n_sigma=4, adjust_central_band=True, instrumental_correction=True, components_detection=False,
644
- composite_lines=None, fit_cfg=None, default_cfg_prefix='default', obj_cfg_prefix=None, update_default=True,
354
+ composite_lines=None, automatic_grouping=False, fit_cfg=None, default_cfg_prefix='default', obj_cfg_prefix=None, update_default=True,
645
355
  line_list=None, particle_list=None, decimals=None, vacuum_waves=False, ref_bands=None, update_labels=False,
646
356
  update_latex=False, vacuum_label=False):
647
357
 
@@ -685,10 +395,10 @@ class SpecRetriever:
685
395
  bands['w3'] = (lambda_obs - delta_lambda) / (1 + self._spec.redshift)
686
396
  bands['w4'] = (lambda_obs + delta_lambda) / (1 + self._spec.redshift)
687
397
 
688
- # Combine the blended/merged lines
398
+ # Combine the blended/merged lines in the bands table
689
399
  if fit_cfg is not None:
690
400
  in_cfg = check_fit_conf(fit_cfg, default_cfg_prefix, obj_cfg_prefix, update_default)
691
- pars_bands_conf(self._spec, bands, ref_bands, in_cfg, composite_lines)
401
+ pars_bands_conf(self._spec, bands, in_cfg, composite_lines, automatic_grouping)
692
402
 
693
403
  # Filter the table to match the line detections
694
404
  if components_detection:
@@ -725,7 +435,7 @@ class SpecRetriever:
725
435
  frame = self._spec.frame if ref_frame is None else ref_frame
726
436
 
727
437
  # By default report complete spectrum
728
- idcs = (0, -1)
438
+ idcs = (0, None)
729
439
 
730
440
  # If a line is provided get indexes for the bands limits
731
441
  line_measured = False
@@ -838,8 +548,7 @@ class SpecTreatment(LineFitting, RedshiftFitting):
838
548
  self._n_lines = 0
839
549
 
840
550
  def bands(self, label, bands=None, fit_cfg=None, min_method='least_squares', profile='g-emi', cont_from_bands=True,
841
- err_from_bands=None, temp=10000.0, default_cfg_prefix='default', obj_cfg_prefix=None, update_default=True,
842
- ref_bands=None):
551
+ err_from_bands=None, temp=10000.0, default_cfg_prefix='default', obj_cfg_prefix=None, update_default=True):
843
552
 
844
553
  """
845
554
 
@@ -911,7 +620,7 @@ class SpecTreatment(LineFitting, RedshiftFitting):
911
620
  cont_from_bands = True if cont_from_bands is None else cont_from_bands
912
621
 
913
622
  # Interpret the input line
914
- self.line = Line(label, bands, input_conf, profile, cont_from_bands, ref_bands=ref_bands)
623
+ self.line = Line(label, bands, input_conf, profile, cont_from_bands)
915
624
 
916
625
  # Check the line selection is valid
917
626
  idcs_selection = review_bands(self._spec, self.line, user_cont_from_bands=cont_from_bands, user_err_from_bands=err_from_bands)
@@ -940,7 +649,7 @@ class SpecTreatment(LineFitting, RedshiftFitting):
940
649
  err_arr= pixel_err_arr[idcs_fitting],
941
650
  user_conf=input_conf, fit_method=min_method)
942
651
 
943
- # Instrumental and thermal corrections for the lines # TODO make res_power array length of wave
652
+ # Instrumental and thermal corrections for the lines
944
653
  sigma_corrections(self.line, idcs_line, self._spec.wave[idcs_line], self._spec.res_power, temp)
945
654
 
946
655
  # Recalculate the SNR with the profile parameters
@@ -1081,7 +790,7 @@ class SpecTreatment(LineFitting, RedshiftFitting):
1081
790
  # Fit the lines
1082
791
  self.bands(line, bands, input_conf, min_method, profile,
1083
792
  cont_from_bands=cont_from_bands, err_from_bands=err_from_bands,
1084
- temp=temp, obj_cfg_prefix=None, default_cfg_prefix=None, ref_bands=ref_bands)
793
+ temp=temp, obj_cfg_prefix=None, default_cfg_prefix=None)
1085
794
 
1086
795
  if plot_fit:
1087
796
  self._spec.plot.bands()
@@ -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
@@ -1,24 +1,24 @@
1
1
  lime/__init__.py,sha256=i7iYJDoAYCsApb35OG33Y0repT6FpgLCsFMTk0Nu0rM,1279
2
2
  lime/changelog.txt,sha256=e02y8w_romtBvZLt7wrDxfL0Kry5ccTUkaajX_XwHbs,9657
3
- lime/io.py,sha256=9bH51m6DAYXJBLDXVQLmaOB0xw6_XoJiMoZlXDQ7Up4,32708
4
- lime/lime.toml,sha256=YZ4ISkDoWKzFX_o2XTfOzUGpZQvFYzH0ee1dkLaEcV4,52
5
- lime/observations.py,sha256=2XV1V7FsQYwi1plcGPHATNeLn-XccUhcKtzjds3D3YU,64552
3
+ lime/io.py,sha256=J3oGfyznx-dqZCIY2tlTWBmuQzQEEJiBgU9Jpu-Sc9E,32778
4
+ lime/lime.toml,sha256=oIJIPuAkwCzbGRKwY9gknhQRFHCyIcibg6BsC0yZd64,52
5
+ lime/observations.py,sha256=BjzcfGi0VbzWUHw3DpME4AK3hxWaqC4fjt9bT1o_oxk,65018
6
6
  lime/tools.py,sha256=5E7_YeXeLrzFo-2h5EAI8J-cPaK4i6g6OJ1uL2jXJ7o,34136
7
- lime/transitions.py,sha256=-TgVCqL9R0-i4o8jHQGkGllHrMyP-Bo9DqoWfVNxHe4,45634
8
- lime/workflow.py,sha256=x3es6Sgq9wI785WdTsIuOWnWvKdSYdnJHN_MSkIhcxM,65417
7
+ lime/transitions.py,sha256=9zxgUIgRRQIq1LFwiDSFYYdnMAppH-tENL6W2RIrw6Q,45639
8
+ lime/workflow.py,sha256=7FqULLns2KKAlAf8AFQ_iEjMjts3k-Y3L1PLwVHubvQ,51784
9
9
  lime/archives/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- lime/archives/read_fits.py,sha256=TIJtP8iGokjE9Nj1ALYb7MHE2lUxZGHuz6cx5SZBb5U,37944
10
+ lime/archives/read_fits.py,sha256=EifjLTsklZcrUAtCtuind5KWvDtEK_wMw_-5HA1P7l8,37928
11
11
  lime/archives/tables.py,sha256=sn3dINNorARuhEA_NsUKnhz8pT5d9u_sNBNKgv3EtuA,14242
12
12
  lime/fitting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- lime/fitting/lines.py,sha256=X1QI9ZqpLfxvfVsOMWMDmwLxXhR_gNvIYBdFA7A2GD8,41288
13
+ lime/fitting/lines.py,sha256=4WVw_auqLCCzftoxMJzhxYWJVqC1umcURWRq8kbScR8,40423
14
14
  lime/fitting/redshift.py,sha256=JBYQlESr63PrF3gRYVT-QLQr2yMi9YZKaeNxI8gFvgY,11639
15
15
  lime/inference/detection.py,sha256=m_Yz-53RGEvQQa6rgS_-nU2DwiRG45yuZIuneAvs0Dc,425
16
16
  lime/inference/intensity_threshold.py,sha256=9LqSjJQajfrM4SOHSqJWnverKFnrdrjnV472w56T79U,5047
17
17
  lime/plotting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  lime/plotting/bokeh_plots.py,sha256=xJlFS90NW-kQLSBDrfYd3tkjixyHK0DUs1vm5ExmjOQ,23444
19
19
  lime/plotting/format.py,sha256=KM2UjhfJ7j2iEOIRTn8JfpLNmSRSlUFTHqB5EKxTq80,7296
20
- lime/plotting/plots.py,sha256=LruFJvee0NcZ8wkMeb_-bMqonwLsgB6cF92aPqZRAOk,87496
21
- lime/plotting/plots_interactive.py,sha256=wX_ckpv_lM6YgZZFEfx36dfGt1DpTB5e-R3DyDkWgMY,73357
20
+ lime/plotting/plots.py,sha256=hWyFL82U0UaTe6nspkhsEzMJqrZmz-Myck55wXvlPKE,85352
21
+ lime/plotting/plots_interactive.py,sha256=9qj369eRBGI53qAvqAToUb4t1KqpSO4R_ZeGhovhwgg,73353
22
22
  lime/plotting/theme_lime.toml,sha256=moyNfRDx0x0Gma3azW_DNfLQoKd1qnBRiX0DAkioBbs,3396
23
23
  lime/plotting/utils.py,sha256=tPBXCvMlg7_onLWN9O2I7FoAurzkfF0Qe-JVYSLb1Xs,2069
24
24
  lime/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -27,9 +27,10 @@ lime/resources/lines_database_v2.0.0.txt,sha256=grhl32eDMtHHnXOw-LVmSCPbYjqUgccJ
27
27
  lime/resources/logo.py,sha256=buuJuwCjw0JFFtqvvIBpjb-LCuFgBwRFqnVOqCmSam8,3296
28
28
  lime/resources/types_params.txt,sha256=HgK-rr_cHnjNR3oLJomdYt6UvKwBn-FMWt40MkCR0wM,10111
29
29
  lime/retrieve/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
+ lime/retrieve/line_bands.py,sha256=QwcU5A2_4987q4JgIFF3p89r7pwQVlSlD4fSTZWwBpc,10595
30
31
  lime/retrieve/peaks.py,sha256=zERD2-GO-AtNrx8xwGqNbml0pB0nTCH4Oro-9fr69GU,8713
31
- lime_stable-2.0.dev7.dist-info/licenses/LICENSE.rst,sha256=B_kw459IXvKujLR27bhVUwXmTq9AM72DdCA1xGgBMKI,35608
32
- lime_stable-2.0.dev7.dist-info/METADATA,sha256=pvUxdCfhIskIQOvSpTUiTH4EQdn8ejTi_WiPfedg4ag,3931
33
- lime_stable-2.0.dev7.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
34
- lime_stable-2.0.dev7.dist-info/top_level.txt,sha256=F6pWR5Cgjf9EkXNBZlUSKFKcPG8vPzM08QwYFfwpsZc,5
35
- lime_stable-2.0.dev7.dist-info/RECORD,,
32
+ lime_stable-2.0.dev8.dist-info/licenses/LICENSE.rst,sha256=B_kw459IXvKujLR27bhVUwXmTq9AM72DdCA1xGgBMKI,35608
33
+ lime_stable-2.0.dev8.dist-info/METADATA,sha256=cJ-MeAUOkn4CEF6tIqp8Ihn2fldgXMwssvI-Gcy3jII,3931
34
+ lime_stable-2.0.dev8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
+ lime_stable-2.0.dev8.dist-info/top_level.txt,sha256=F6pWR5Cgjf9EkXNBZlUSKFKcPG8vPzM08QwYFfwpsZc,5
36
+ lime_stable-2.0.dev8.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.7.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5