lime-stable 2.2.dev2__py3-none-any.whl → 2.2.dev5__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.
@@ -7,6 +7,7 @@ from astropy.wcs import WCS
7
7
 
8
8
  from lime.io import LiMe_Error, lime_cfg
9
9
  from urllib.parse import urlparse
10
+ from io import BytesIO
10
11
 
11
12
  try:
12
13
  import requests
@@ -265,7 +266,6 @@ def check_fits_instructions(fits_source, online_provider=False):
265
266
  fits_reader = getattr(fits_manager, fits_source)
266
267
  else:
267
268
  source_type = 'instrument' if online_provider is False else 'survey'
268
- # TODO show instruments supported
269
269
  raise LiMe_Error(f'Input {source_type} "{fits_source}" is not recognized. LiMe observation cannot be created.')
270
270
 
271
271
  else:
@@ -280,14 +280,12 @@ def load_txt(text_address, **kwargs):
280
280
  out_array = np.loadtxt(text_address, **kwargs)
281
281
 
282
282
  # File address
283
- if not type(text_address).__name__ == "UploadedFile":
283
+ if isinstance(text_address, BytesIO) or (type(text_address).__name__ == "UploadedFile"):
284
+ lines = text_address.getvalue().decode("utf-8").splitlines()
285
+ else:
284
286
  with open(text_address, "r") as f:
285
287
  lines = f.readlines()
286
288
 
287
- # Uploaded file
288
- else:
289
- lines = text_address.getvalue().decode("utf-8").splitlines()
290
-
291
289
  # Reverse loop over the lines
292
290
  params_dict = {}
293
291
  for line in reversed(lines):
@@ -297,19 +295,6 @@ def load_txt(text_address, **kwargs):
297
295
  key, value = line[1:].split(":", 1)
298
296
  params_dict[key.strip()] = value.strip()
299
297
 
300
- # # Transform foot comments as dictionary data
301
- # params_dict = {}
302
- # with open(text_address, "r") as f:
303
- #
304
- # # Reverse loop while the lines start by a "#"
305
- # for line in reversed(f.readlines()):
306
- # line = line.strip()
307
- # if not line.startswith("#") or (line.startswith("# LiMe")):
308
- # break
309
- #
310
- # # Extract key-value pairs
311
- # key, value = line[1:].split(":", 1) # Split at the first ':'
312
- # params_dict[key.strip()] = value.strip()
313
298
 
314
299
  return out_array, params_dict
315
300
 
lime/fitting/lines.py CHANGED
@@ -232,7 +232,7 @@ def voigt_area(line, idx, n_steps):
232
232
  sigma = np.random.normal(line.sigma[idx], line.sigma_err[idx], n_steps)
233
233
  gamma = np.random.normal(line.gamma[idx], line.gamma_err[idx], n_steps)
234
234
 
235
- return gaussian_area(amp, sigma) + lorentz_area(amp, gamma)
235
+ return gaussian_area(amp, sigma, n_steps) + lorentz_area(amp, gamma, n_steps)
236
236
 
237
237
 
238
238
  def pseudo_voigt_area(line, idx, n_steps):
@@ -268,7 +268,7 @@ def pseudo_power_area(line, idx, n_steps):
268
268
 
269
269
  def velocity_to_wavelength_band(n_sigma, band_velocity_sigma, lambda_obs, delta_instr):
270
270
 
271
- return n_sigma * ((band_velocity_sigma / c_KMpS) * lambda_obs + delta_instr)
271
+ return n_sigma * np.sqrt(np.square((band_velocity_sigma / c_KMpS) * lambda_obs) + np.square(delta_instr))
272
272
 
273
273
 
274
274
  ALL_PARAMS = np.array(['m_cont', 'n_cont', 'amp', 'center', 'sigma', 'gamma', 'alpha', 'frac', 'a', 'b', 'c'])
lime/lime.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = 'lime-stable'
3
- version = "2.2.dev2"
3
+ version = "2.2.dev5"
4
4
 
5
5
  # =====================
6
6
  # Spectrum / Long-slit
lime/observations.py CHANGED
@@ -340,6 +340,10 @@ class Spectrum:
340
340
  internally for fitting and removed in reported measurements.
341
341
  crop_waves : tuple or numpy.ndarray, optional
342
342
  Two-element ``(min, max)`` wavelength range used to crop the input arrays.
343
+ crop_flux : tuple, optional
344
+ Two-element ``(min_percentile, max_percentile)`` range used to clip the flux array
345
+ by percentile. Defaults to ``None``, equivalent to ``(1, 100)`` (i.e. the 1st to
346
+ 100th percentile).
343
347
  res_power : float or numpy.ndarray, optional
344
348
  Instrument resolving power :math:`R = \\lambda/\\Delta\\lambda`. If provided,
345
349
  it can be used to compute and apply an instrumental broadening correction
@@ -923,14 +927,18 @@ class Spectrum:
923
927
 
924
928
  # Confirm the lines in the log match the one of the spectrum
925
929
  if line_0.units_wave != self.units_wave:
926
- _logger.warning(f'Different units in the spectrum dispersion ({self.units_wave}) axis and the lines log'
927
- f' in {line_0.units_wave[0]}')
928
-
929
- # Confirm all the log lines have the same units
930
- au_str = 'A' if line_0.units_wave == 'Angstrom' else str(line_0.units_wave)
931
- same_units_check = np.flatnonzero(np.core.defchararray.find(line_list.astype(str), au_str) != -1).size == line_list.size
932
- if not same_units_check:
933
- _logger.warning(f'The log has lines with different units')
930
+ _logger.critical(f'Different units in the spectrum dispersion ({self.units_wave}) axis and the lines log'
931
+ f' in {line_0.units_wave[0]}')
932
+
933
+ # # Confirm all the log lines have the same units This fails if blended or merged in table
934
+ # au_str = 'A' if line_0.units_wave == 'Angstrom' else str(line_0.units_wave)
935
+ # same_units_check = np.flatnonzero(np.core.defchararray.find(line_list.astype(str), au_str) != -1).size == line_list.size
936
+ # same_units_check = np.char.find(line_list.astype(str), au_str)
937
+ # (np.char.find(line_list.astype(str), au_str) != -1).all()
938
+ # np.array([str(l) for l in line_list]).astype(str)
939
+ #
940
+ # if not same_units_check:
941
+ # _logger.warning(f'The log has lines with different units')
934
942
 
935
943
  # Assign the log
936
944
  self.frame = log_df
@@ -957,7 +965,7 @@ class Spectrum:
957
965
  if frame is not None:
958
966
  if line_label in frame.index:
959
967
  bands_limits = frame.loc[line_label, 'w1':'w6']
960
- idcs_bands = np.searchsorted(self._spec.wave.data, bands_limits * (1 + self._spec.redshift))
968
+ idcs_bands = np.searchsorted(self.wave.data, bands_limits * (1 + self.redshift))
961
969
  idcs = (idcs_bands[0], idcs_bands[5])
962
970
  line_measured = True
963
971
  else:
@@ -973,9 +981,9 @@ class Spectrum:
973
981
  line_list = line.list_comps
974
982
 
975
983
  # 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]])
984
+ gaussian_arr = profiles_computation(line_list, frame, 1 + self.redshift, line.profile,
985
+ x_array=self.wave.data[idcs[0]: idcs[1]])
986
+ linear_arr = linear_continuum_computation(line_list, frame, 1 + self.redshift, x_array=self.wave.data[idcs[0]: idcs[1]])
979
987
 
980
988
  # Determine which component you want to extract:
981
989
  if split_components is False:
@@ -1145,13 +1153,14 @@ class Spectrum:
1145
1153
  >>> spec.frame.shape
1146
1154
  (0, 10)
1147
1155
  """
1156
+ if self.frame is not None:
1148
1157
 
1149
- if line_data:
1150
- self.frame = self.frame[0:0]
1158
+ if line_data:
1159
+ self.frame = self.frame[0:0]
1151
1160
 
1152
- if cont_data:
1153
- self.cont = None
1154
- self.cont_std = None
1161
+ if cont_data:
1162
+ self.cont = None
1163
+ self.cont_std = None
1155
1164
 
1156
1165
  return
1157
1166
 
@@ -39,6 +39,10 @@ category_conf_styles = {0: 'dotted',
39
39
  1: 'dashed',
40
40
  2: 'solid'}
41
41
 
42
+ def ensure_list(x):
43
+ return x if isinstance(x, list) else [x]
44
+
45
+
42
46
  def update_bokeh_figure(figure_obj, config_dict):
43
47
 
44
48
  # Set general figure properties
@@ -48,30 +52,40 @@ def update_bokeh_figure(figure_obj, config_dict):
48
52
  if isinstance(value, dict):
49
53
  match key:
50
54
  case "xaxis":
51
- for axis in figure_obj.xaxis: # Update all x-axes
52
- for attr, val in value.items():
53
- setattr(axis, attr, val)
55
+ for item_obj in ensure_list(figure_obj):
56
+ if item_obj is not None:
57
+ for axis in item_obj.xaxis: # Update all x-axes
58
+ for attr, val in value.items():
59
+ setattr(axis, attr, val)
54
60
 
55
61
  case "yaxis":
56
- for axis in figure_obj.yaxis: # Update all y-axes
57
- for attr, val in value.items():
58
- setattr(axis, attr, val)
62
+ for item_obj in ensure_list(figure_obj):
63
+ if item_obj is not None:
64
+ for axis in item_obj.yaxis: # Update all y-axes
65
+ for attr, val in value.items():
66
+ setattr(axis, attr, val)
59
67
 
60
68
  case "title":
61
69
  for attr, val in value.items():
62
- setattr(figure_obj.title, attr, val)
70
+ for item_obj in ensure_list(figure_obj):
71
+ if item_obj is not None:
72
+ setattr(item_obj.title, attr, val)
63
73
 
64
74
  case "xgrid":
65
- for grid in figure_obj.xgrid: # Update all x-grids
66
- for attr, val in value.items():
67
- val = None if val == 'None' else val
68
- setattr(grid, attr, val)
75
+ for item_obj in ensure_list(figure_obj):
76
+ if item_obj is not None:
77
+ for grid in item_obj.xgrid: # Update all x-grids
78
+ for attr, val in value.items():
79
+ val = None if val == 'None' else val
80
+ setattr(grid, attr, val)
69
81
 
70
82
  case "ygrid":
71
- for grid in figure_obj.ygrid: # Update all y-grids
72
- for attr, val in value.items():
73
- val = None if val == 'None' else val
74
- setattr(grid, attr, val)
83
+ for item_obj in ensure_list(figure_obj):
84
+ if item_obj is not None:
85
+ for grid in item_obj.ygrid: # Update all y-grids
86
+ for attr, val in value.items():
87
+ val = None if val == 'None' else val
88
+ setattr(grid, attr, val)
75
89
 
76
90
  # Single value entries
77
91
  else:
@@ -86,7 +100,12 @@ def update_bokeh_figure(figure_obj, config_dict):
86
100
  case 'active_tap':
87
101
  figure_obj.toolbar.active_tap = figure_obj.select_one(getattr(models, value))
88
102
  case _ :
89
- setattr(figure_obj, key, value)
103
+ if isinstance(figure_obj, list):
104
+ for ax in figure_obj:
105
+ if ax is not None:
106
+ setattr(ax, key, value)
107
+ else:
108
+ setattr(figure_obj, key, value)
90
109
 
91
110
  # # Set zoom and pan as active
92
111
  # figure_obj.toolbar.active_scroll = figure_obj.select_one(WheelZoomTool) # Activate zoom wheel
@@ -129,6 +148,7 @@ def save_close_fig_swicth(file_path, fig_obj, display_check):
129
148
 
130
149
  return
131
150
 
151
+
132
152
  def bokeh_bands(fig, bands, x, y, z_corr, redshift):
133
153
 
134
154
  # Open the bands file the bands
@@ -314,6 +334,61 @@ def profile_bokeh(fig, line, z_cor, log, redshift, norm_flux):
314
334
  return line_single
315
335
 
316
336
 
337
+ def redshift_fit_evaluation_bokeh(spectrum, z_infered, data_mask, gauss_arr, z_arr, flux_sum_arr, rest_frame=True):
338
+
339
+ # gauss_arr_max = compute_z_key(z_infer, theo_lambda, wave_matrix, 1, sigma_arr)
340
+
341
+ wave_plot, flux_plot, z_corr, idcs_mask = frame_mask_switch(spectrum.wave, spectrum.flux, z_infered, rest_frame)
342
+ wave_corr = wave_plot / z_corr
343
+ flux_corr = flux_plot * z_corr
344
+
345
+ # Masked flux (red overlay)
346
+ y_mask = np.full(flux_plot.size, np.nan)
347
+ y_mask[data_mask] = flux_corr[data_mask]
348
+
349
+ # --- Top panel: spectrum ---
350
+ p1 = figure(width=800, height=300,
351
+ x_axis_label=str(spectrum.units_wave),
352
+ y_axis_label=str(spectrum.units_flux),
353
+ tools="xpan,xwheel_zoom,reset,save")
354
+
355
+ # Full spectrum
356
+ p1.step(wave_corr, flux_corr, color='white', line_width=1, mode='center', legend_label='Spectrum')
357
+
358
+ # Masked region
359
+ p1.step(wave_corr, y_mask, color='red', line_width=1, mode='center', legend_label='Mask')
360
+
361
+ p1.legend.location = "top_right"
362
+ p1.legend.click_policy = "hide"
363
+
364
+ # --- Twin axis equivalent: gauss_arr on secondary y ---
365
+ # Bokeh doesn't have true twinx, so we normalize gauss_arr to flux scale for overlay
366
+ flux_max = np.nanmax(flux_corr)
367
+ gauss_scaled = gauss_arr * flux_max # scale to flux range
368
+
369
+ p1.step(wave_corr, gauss_scaled, color='yellow', line_width=1, mode='center', legend_label='Bands')
370
+
371
+ # --- Bottom panel: redshift search ---
372
+ title = f'z_prediction = {z_infered:.3f}'
373
+ p2 = figure(width=800, height=200, title=title,
374
+ x_axis_label='Redshift range',
375
+ y_axis_label='F_sum / max(F_sum)',
376
+ tools="xpan,xwheel_zoom,reset,save")
377
+
378
+ flux_sum_norm = flux_sum_arr / np.max(flux_sum_arr)
379
+
380
+ p2.step(z_arr, flux_sum_norm, color='white', line_width=1, mode='center')
381
+
382
+ # Peak marker
383
+ p2.scatter([z_infered], [1], marker='circle', color='red', size=10)
384
+
385
+ # y ticks
386
+ p2.yaxis.ticker = [0, 1]
387
+
388
+ # Link x ranges if rest_frame shares a common axis (optional)
389
+ # p2.x_range = p1.x_range # uncomment if x axes are the same
390
+
391
+ # streamlit_bokeh(column(p1, p2), use_container_width=True)
317
392
 
318
393
 
319
394
  # Sentinel object for non input figures
lime/plotting/format.py CHANGED
@@ -201,8 +201,6 @@ class Themer:
201
201
  # Figure colors for bokeh
202
202
  colors_bokeh = nested_dict(deepcopy(self.conf['bokeh']['colors']), self.colors)
203
203
  self.active_conf['bokeh'].update(colors_bokeh)
204
- # for key, value in self.conf['bokeh']['colors'].items():
205
- # self.active_conf['bokeh'][key] = self.colors.get(value, value)
206
204
 
207
205
  # Set the size
208
206
  if self.scale[0] in self.conf['matplotlib']['size']:
@@ -211,7 +209,6 @@ class Themer:
211
209
  if self.scale[0] in self.conf['bokeh']['size']:
212
210
  self.bokeh = self.conf['bokeh']['size'][self.scale[0]]
213
211
 
214
-
215
212
  return
216
213
 
217
214
 
@@ -58,7 +58,7 @@ default."label_lines" = 10
58
58
  [bokeh]
59
59
  default."width" = 600
60
60
  default."height" = 300
61
- default."tools" = 'fullscreen,pan,wheel_zoom,box_zoom,xzoom_in,yzoom_in,reset,save'
61
+ default."tools" = 'fullscreen,pan,wheel_zoom,box_zoom,xwheel_zoom,reset,save'
62
62
  default.'active_drag' = 'PanTool'
63
63
  default.'active_scroll' = 'WheelZoomTool'
64
64
 
@@ -18,6 +18,63 @@ def deblend_criteria(mu_arr, sigma_arr, Rayleigh_threshold):
18
18
  return resolvable
19
19
 
20
20
 
21
+ def find_sharing_groups(lines: np.ndarray, matrix: np.ndarray) -> list[np.ndarray]:
22
+ superdiag = np.diag(matrix, k=1).astype(bool)
23
+ breaks = np.where(np.diff(superdiag.astype(int), prepend=0, append=0) != 0)[0]
24
+ starts, ends = breaks[0::2], breaks[1::2]
25
+ return [lines[s:e + 1] for s, e in zip(starts, ends) if superdiag[s]]
26
+
27
+
28
+ def blend_merge_dict(line_list: np.ndarray, relation_list: np.ndarray) -> dict:
29
+ """
30
+ Build a blend/merge dictionary from a list of lines and their relations.
31
+
32
+ Parameters
33
+ ----------
34
+ line_list : np.ndarray
35
+ Ordered array of line names.
36
+ relation_list : np.ndarray of bool
37
+ Connections between consecutive lines. True=blended, False=merged.
38
+ Length must be len(line_list) - 1.
39
+
40
+ Returns
41
+ -------
42
+ dict
43
+ Keys/values describing blend and merge groupings.
44
+ """
45
+ # All True: single blended group
46
+ if relation_list.all():
47
+ return {f"{line_list[0]}_b": "+".join(line_list)}
48
+
49
+ # All False: single merge group
50
+ if not relation_list.any():
51
+ return {f"{line_list[0]}_m": "+".join(line_list)}
52
+
53
+ # Mixed: find contiguous False entries
54
+ false_mask = ~relation_list
55
+ edges = np.diff(false_mask.astype(int), prepend=0, append=0)
56
+ starts = np.where(edges == 1)[0]
57
+ ends = np.where(edges == -1)[0]
58
+
59
+ # Build merge entries: each False run covers lines[start : end+1]
60
+ merge_entries = {f"{line_list[s]}_m": "+".join(line_list[s:e + 1]) for s, e in zip(starts, ends)}
61
+
62
+ # Build blend parts: replace merged subgroups with their key (Mark hidden in a merge subgroup)
63
+ hidden = np.zeros(len(line_list), dtype=bool)
64
+ for s, e in zip(starts, ends):
65
+ hidden[s + 1:e + 1] = True # keep the first line as anchor, absorb the rest
66
+
67
+ # For non-hidden lines: use line name, but swap merge-subgroup anchors for their key
68
+ is_anchor = np.zeros(len(line_list), dtype=bool)
69
+ is_anchor[starts] = True
70
+
71
+ blend_parts = np.where(is_anchor, np.array([f"{l}_m" for l in line_list]), line_list)
72
+ blend_value = "+".join(blend_parts[~hidden])
73
+
74
+ blend_key = f"{line_list[0]}_b"
75
+ return {blend_key: blend_value, **merge_entries}
76
+
77
+
21
78
  def determine_line_groups(spec, bands, fit_conf, composite_lines, automatic_grouping, n_sigma, Rayleigh_threshold):
22
79
 
23
80
  # Check if the input configuration has a list of lines
@@ -42,7 +99,7 @@ def determine_line_groups(spec, bands, fit_conf, composite_lines, automatic_grou
42
99
  line_list, group_lines = [], []
43
100
  group_names, group_blended_check = [], []
44
101
  for comp, group_label in fit_conf.items():
45
- if comp.endswith(('_b', '_m')):
102
+ if comp.endswith(('_b', '_m')) and groups_dict.get(comp) is None:
46
103
 
47
104
  # No kinematic groups
48
105
 
@@ -162,6 +219,47 @@ def determine_line_groups(spec, bands, fit_conf, composite_lines, automatic_grou
162
219
  return groups_dict
163
220
 
164
221
 
222
+ def get_spectrum_line_groups(wave_rest, bands, n_sigma=4, Rayleigh_threshold=2):
223
+
224
+ # Get limits of the bands on the spectrum wavelength range
225
+ idx3_arr = np.searchsorted(wave_rest, bands.w3.to_numpy())
226
+ idx4_arr = np.searchsorted(wave_rest, bands.w4.to_numpy())
227
+
228
+ # Generate binary matrix with the line bands location
229
+ wave_matrix = np.zeros((bands.index.size, wave_rest.size))
230
+ cols = np.arange(wave_matrix.shape[1])
231
+ wave_matrix[(cols >= idx3_arr[:, None]) & (cols <= idx4_arr[:, None])] = 1
232
+
233
+ # Compute the decision matrix with the common pixels
234
+ decision_matrix = wave_matrix @ wave_matrix.T
235
+
236
+ (np.diagonal(decision_matrix, offset=1) > 0).sum() + 1
237
+
238
+ grouped_lines = find_sharing_groups(bands.index.to_numpy(), decision_matrix)
239
+
240
+ idx3_arr = np.searchsorted(wave_rest, bands.w3.to_numpy())
241
+ idx4_arr = np.searchsorted(wave_rest, bands.w4.to_numpy())
242
+
243
+ relation_list = []
244
+ for group in grouped_lines:
245
+ idcs = bands.index.isin(group)
246
+ w3_arr = idx3_arr[idcs]
247
+ w4_arr = idx4_arr[idcs]
248
+ mu_arr = np.searchsorted(wave_rest, bands.loc[group, 'wavelength'].to_numpy())
249
+ obs_group_blend_chek = deblend_criteria(mu_arr=mu_arr,
250
+ sigma_arr=(w4_arr - w3_arr) / (n_sigma * 2),
251
+ Rayleigh_threshold=Rayleigh_threshold)
252
+ relation_list.append(obs_group_blend_chek)
253
+
254
+ group_dict = {}
255
+ for line_list, relation_list in zip(grouped_lines, relation_list):
256
+ group_dict.update(blend_merge_dict(line_list, relation_list))
257
+
258
+ group_dict = None if len(group_dict) == 0 else group_dict
259
+
260
+ return group_dict
261
+
262
+
165
263
  def groupify_lines_df(bands, fit_cfg, groups_dict, spec, save_group_label=False):
166
264
 
167
265
  # Containers for the requested changes
@@ -181,8 +279,11 @@ def groupify_lines_df(bands, fit_cfg, groups_dict, spec, save_group_label=False)
181
279
  # Extract the single components ignoring extra kinmatics
182
280
  unique_comp_list = []
183
281
  for trans in line.list_comps:
184
- if trans.kinem == 0:
185
- unique_comp_list.append(trans.label)
282
+ if trans.group == 'm':
283
+ unique_comp_list += [sub_comp.label for sub_comp in trans.list_comps]
284
+ else:
285
+ if trans.kinem == 0:
286
+ unique_comp_list.append(trans.label)
186
287
  unique_comp_list = np.unique(unique_comp_list)
187
288
 
188
289
  # Only apply corrections if components are present
lime/tools.py CHANGED
@@ -224,6 +224,7 @@ def normalize_fluxes(log, line_list=None, norm_list=None, flux_column='profile_f
224
224
  raise LiMe_Error(f'Input log is missing "{flux_column}" or "{flux_column}_err" columns')
225
225
 
226
226
  # Check the normalization for the lines
227
+ line_list = log.index.to_numpy() if line_list is None else line_list
227
228
  line_array, norm_array = check_lines_normalization(line_list, norm_list, log)
228
229
 
229
230
  # Add new columns if necessary
lime/workflow.py CHANGED
@@ -228,10 +228,54 @@ def continuum_model_fit(x_array, y_array, idcs, degree):
228
228
 
229
229
  def res_power_approx(wavelength_arr):
230
230
 
231
+ """
232
+ Estimate the spectral resolving power R = λ / Δλ approximation for a wavelength array.
233
+
234
+ The dispersion per pixel (Δλ/pixel) is computed from the finite differences
235
+ of the wavelength array. The resolution element is assumed to be Nyquist-sampled
236
+ by 2 pixels, so the FWHM resolution element is 2 * (Δλ/pixel), giving:
237
+
238
+ R ≈ λ / (2 * Δλ_pixel)
239
+
240
+ Note: This is an approximation. The true R depends on the slit width,
241
+ detector sampling, and optical quality of the spectrograph. For precise
242
+ instrumental broadening estimates, an empirical LSF from arc/sky lines
243
+ is preferred.
244
+
245
+ Parameters
246
+ ----------
247
+ wavelength_arr : np.ndarray
248
+ 1D array of wavelengths, assumed to be in a consistent unit (e.g. Å).
249
+ Must be monotonically increasing and uniformly or smoothly sampled.
250
+
251
+ Returns
252
+ -------
253
+ res_power : np.ndarray
254
+ 1D array of resolving power R at each pixel, same shape as wavelength_arr.
255
+ Dimensionless.
256
+
257
+ Notes
258
+ -----
259
+ - The last pixel is extrapolated by repeating the second-to-last dispersion
260
+ value, since np.ediff1d produces N-1 differences for an N-element array.
261
+ - If the wavelength array has non-uniform sampling (e.g. from a non-linear
262
+ dispersion solution), R will vary across the array accordingly.
263
+ - Assumes 2 pixels per resolution element (Nyquist sampling). If your
264
+ spectrograph samples the LSF with a different number of pixels, replace
265
+ the factor of 2 with the appropriate value.
266
+
267
+ Examples
268
+ --------
269
+ >>> wave = np.linspace(4000, 7000, 3000) # 1 Å/pixel
270
+ >>> R = res_power_approx(wave)
271
+ >>> print(R[0]) # expect ~2000 at 4000 Å with 1 Å/pixel dispersion
272
+ 2000.0
273
+
274
+ """
275
+
231
276
  delta_lambda = np.ediff1d(wavelength_arr, to_end=0)
232
277
  delta_lambda[-1] = delta_lambda[-2]
233
-
234
- return wavelength_arr/delta_lambda
278
+ return wavelength_arr / (2 * delta_lambda)
235
279
 
236
280
 
237
281
 
@@ -250,6 +294,120 @@ class SpecRetriever:
250
294
  update_labels=False, update_latex=False, rejected_lines=None, Rayleigh_threshold=2, lines_redshift=None,
251
295
  map_origin=None, components=None, save_group_label=False):
252
296
 
297
+ """
298
+ Return a bands dataframe with the spectral lines within the spectrum wavelength range.
299
+
300
+ If the user does not provide a ``ref_bands`` This method queries the `LiMe bands database <https://lime-stable.readthedocs.io/en/latest/inputs/n_inputs3_line_bands.html>`_
301
+ and returns a :class:`pandas.DataFrame` of transitions visible within the spectrum's observed
302
+ wavelength interval, taking into account the spectrum redshift, units, and pixel mask.
303
+
304
+ The central bands (``w3``–``w4``) are optionally adjusted to match the expected line width,
305
+ accounting for the emitting and/or absorbing medium velocity dispersion (``band_vsigma``) and instrumental broadening
306
+ (``instrumental_correction``). Lines whose central band falls entirely within a masked
307
+ pixel region can be excluded via ``exclude_bands_masked``.
308
+
309
+ If a fitting configuration (``fit_cfg``) is provided, blended or merged line groups are
310
+ resolved and the bands table is updated accordingly.
311
+
312
+ Parameters
313
+ ----------
314
+ band_vsigma : float, optional
315
+ Velocity sigma in km/s used to set the half-width of the central band (``w3``–``w4``).
316
+ Default is ``70``.
317
+ n_sigma : int, optional
318
+ Number of sigma used to compute the band half-width from ``band_vsigma``. Default is ``4``.
319
+ adjust_central_band : bool, optional
320
+ If ``True`` (default), recompute ``w3`` and ``w4`` from ``band_vsigma``, ``n_sigma``,
321
+ and the instrumental broadening.
322
+ instrumental_correction : bool, optional
323
+ If ``True`` (default), include the instrumental broadening (derived from ``res_power``)
324
+ when adjusting the central band width.
325
+ exclude_bands_masked : bool, optional
326
+ If ``True`` (default), remove lines whose central band pixels are entirely masked.
327
+ map_band_vsigma : dict, optional
328
+ Per-line overrides for ``band_vsigma``, keyed by line label. Lines not present in the
329
+ dict use the global ``band_vsigma`` value.
330
+ grouped_lines : dict, optional
331
+ Explicit line grouping definitions. If ``None``, grouping is read from ``fit_cfg`` if
332
+ available.
333
+ automatic_grouping : bool, optional
334
+ If ``True``, automatically decide the blended or merged line groups which match the observation. Default is ``False``.
335
+ fit_cfg : dict or str or pathlib.Path, optional
336
+ Fitting configuration. Can be a dictionary or a path to a configuration file. When
337
+ provided, grouped lines and rejected lines are read from this configuration unless
338
+ explicitly overridden.
339
+ default_cfg_prefix : str, optional
340
+ Prefix for default parameter entries in ``fit_cfg``. Default is ``"default"``.
341
+ obj_cfg_prefix : str, optional
342
+ Prefix for object-specific parameter entries in ``fit_cfg``. Default is ``None``.
343
+ update_default : bool, optional
344
+ If ``True`` (default), object-specific configuration entries override default entries.
345
+ line_list : list or numpy.ndarray, optional
346
+ Restrict the output to these line labels. Must follow
347
+ `LiMe notation <https://lime-stable.readthedocs.io/en/latest/inputs/n_inputs2_line_labels.html>`_.
348
+ particle_list : list or numpy.ndarray, optional
349
+ Restrict the output to transitions from these ionic species (e.g. ``["H1", "O3"]``).
350
+ sig_digits : int, optional
351
+ Number of decimal figures in the line labels. Default is ``4``.
352
+ ref_bands : pandas.DataFrame, str, or pathlib.Path, optional
353
+ Alternative reference bands database. Defaults to the internal LiMe database.
354
+ vacuum_waves : bool, optional
355
+ If ``True``, convert wavelengths and band limits to vacuum values. Default is ``False``.
356
+ update_labels : bool, optional
357
+ If ``True``, recompute line labels from the transition data. Default is ``False``.
358
+ update_latex : bool, optional
359
+ If ``True``, recompute the ``latex_label`` column. Default is ``False``.
360
+ rejected_lines : list, optional
361
+ Line labels to exclude from the output. If ``None``, falls back to the value in
362
+ ``fit_cfg`` if present.
363
+ Rayleigh_threshold : float, optional
364
+ Minimum wavelength separation (in units of ``band_vsigma``) below which two lines
365
+ are considered blended via Rayleigh's criterion. Default is ``2``.
366
+ lines_redshift : float, optional
367
+ Redshift applied to the transition wavelengths when adjusting the central band, if
368
+ no per-line ``z_line`` column is present in the bands table. Falls back to the
369
+ spectrum redshift if ``None``.
370
+ map_origin : dict, optional
371
+ Mapping of origin labels to redshifts for multi-origin line queries.
372
+ components : list, optional
373
+ List of spectral shape components (e.g. ``["emission", "absorption"]``) used to
374
+ filter lines by the predicted profile type from the feature detection algorithm.
375
+ Requires ``spec.infer.pred_arr`` to have been computed beforehand.
376
+ save_group_label : bool, optional
377
+ If ``True``, store the group label in the bands table for grouped lines. Default is
378
+ ``False``.
379
+
380
+ Returns
381
+ -------
382
+ pandas.DataFrame
383
+ Bands dataframe with one row per transition, indexed by line label, containing
384
+ wavelength, band limits (``w1``–``w6``), and metadata columns.
385
+
386
+ Notes
387
+ -----
388
+ - If ``res_power`` is not set on the spectrum, an approximate resolving power is
389
+ computed from the wavelength array when ``instrumental_correction=True`` or
390
+ ``fit_cfg`` is provided.
391
+ - ``components`` filtering requires the aspect package and a prior call to the
392
+ component detection algorithm; a :exc:`LiMe_Error` is raised otherwise.
393
+ - Per-line redshifts in a ``z_line`` column (added by ``map_origin``) take precedence
394
+ over ``lines_redshift`` and the spectrum redshift when adjusting central bands.
395
+
396
+ Examples
397
+ --------
398
+ Get all lines in the spectrum wavelength range:
399
+
400
+ >>> bands = spec.fit.lines_frame()
401
+
402
+ Restrict to hydrogen and oxygen transitions with a wider velocity band:
403
+
404
+ >>> bands = spec.fit.lines_frame(particle_list=["H1", "O3"], band_vsigma=120)
405
+
406
+ Use a fitting configuration to resolve blended lines:
407
+
408
+ >>> bands = spec.fit.lines_frame(fit_cfg="my_cfg.toml")
409
+ """
410
+
253
411
 
254
412
  # Remove the mask from the wavelength array if necessary
255
413
  wave_intvl = self._spec.wave.compressed()
@@ -258,6 +416,7 @@ class SpecRetriever:
258
416
  in_cfg = check_fit_conf(fit_cfg, default_cfg_prefix, obj_cfg_prefix, update_default) if fit_cfg else None
259
417
 
260
418
  # Generate the table of single lines taking into account possible origins
419
+ rejected_lines = rejected_lines if rejected_lines is not None else (in_cfg or {}).get('rejected_lines')
261
420
  bands = multi_origin_lines_frame(map_origin, line_list, self._spec.redshift, wave_intvl=wave_intvl,
262
421
  particle_list=particle_list, units_wave=self._spec.units_wave,
263
422
  sig_digits=sig_digits, ref_bands=ref_bands, vacuum_waves=vacuum_waves,
@@ -302,7 +461,6 @@ class SpecRetriever:
302
461
  delta_lambda_inst = 0
303
462
 
304
463
  # Use unique or specific velocity sigma for the bands
305
- # map_band_vsigma = in_cfg['map_band_vsigma'] if in_cfg and ('map_band_vsigma' in in_cfg) else map_band_vsigma
306
464
  map_band_vsigma = map_band_vsigma if map_band_vsigma else (in_cfg or {}).get('map_band_vsigma')
307
465
 
308
466
  if map_band_vsigma is not None:
@@ -363,7 +521,6 @@ class SpecRetriever:
363
521
 
364
522
  return bands
365
523
 
366
-
367
524
  def spectrum(self, redshift=None, norm_flux=None, crop_waves=None, crop_flux=None, pixel_mask=None, mask_intvls=None,
368
525
  obj_redshift=False,):
369
526
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lime-stable
3
- Version: 2.2.dev2
3
+ Version: 2.2.dev5
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.7.dev1; extra == "full"
21
+ Requires-Dist: aspect-stable~=0.7.dev2; 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"
@@ -1,26 +1,26 @@
1
1
  lime/__init__.py,sha256=oTUSuAwbROsVVLsPKzeUEIVYhJyTN4Rgqqz_G13qFnk,1099
2
2
  lime/changelog.txt,sha256=XUYFC9rr23PVBkFAICOG3aljHS2Dh9-6zgF_hSNIbdE,10615
3
3
  lime/io.py,sha256=xLYMDfdFR2cjfcWwqwnTldH3NoIvsQnAcwhjpjbfWTY,36199
4
- lime/lime.toml,sha256=tq4a2HKg71VUbkoGbBovO_16jdOTPPJaDp9P03FcuWM,2309
5
- lime/observations.py,sha256=k6a3CNklAXn-UO5zLQSfGC_v0KjwsnTkQgUQdrNg4MA,98114
4
+ lime/lime.toml,sha256=QYPXHds95MbDPyDtNfgIHNgSUw0xml0-8cTYqQhxriQ,2309
5
+ lime/observations.py,sha256=o0P97ZYPUL-ht6BYqrEdPSz9wNifE1CZw40bda92Y_U,98652
6
6
  lime/rsrc_manager.py,sha256=lTy6byVCoV-Dc43tq5stmocufvPr8gp5cpLmD25FzR8,70
7
- lime/tools.py,sha256=PXgspRmRiF6z4n3Ii0XIxNJir_D5JLnwITQR00DnqOM,39574
7
+ lime/tools.py,sha256=7DCI-97iueiR4mkB30aFk_XU6AjbKL1AGO8Cx3pWKbk,39647
8
8
  lime/transitions.py,sha256=RSkvpWbuA4k5eUZV5RV7J2wsjl5HrQDN14C9h_Ipfik,81433
9
- lime/workflow.py,sha256=fhOsC1BmWh9NCxiChjHQ35ZVeonzmDmlP4bKSkanj08,71208
9
+ lime/workflow.py,sha256=DQSK18AX5gBp_tRmaFyw8oCC1EHUcUsJEPGUCNv9ZdM,79624
10
10
  lime/archives/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- lime/archives/read_fits.py,sha256=C6xvZBJd7hKlw0CmZs021vFocGcxTNEgQPV7RBx_beo,50164
11
+ lime/archives/read_fits.py,sha256=rPSym_ooPFqxHpvWApTUEk8gOZXZI-aI3JSpbE92maI,49622
12
12
  lime/archives/tables.py,sha256=j8axa1FvxOCZYaJE69JJVTOOp4OXlEKdx-m3GhOMFM0,14254
13
13
  lime/fitting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- lime/fitting/lines.py,sha256=ZVJtYRiCLUgynU0OEC9JzxDKZcjfYg_Px_X9HOTMnmg,49597
14
+ lime/fitting/lines.py,sha256=NxbkLOoduX_nqGzC7ovQcj5ElSfVTSAyaNVZB7OF2h4,49644
15
15
  lime/fitting/redshift.py,sha256=mzo81MOZScQBsk-XSkVo4Y7lLjU9sV10PAaw9IMBRJo,12366
16
16
  lime/inference/detection.py,sha256=evcDl9GfHYo6Zfa0c3L9WA47i-0TEE2JRzH-UZp8zqg,841
17
17
  lime/inference/intensity_threshold.py,sha256=Qnax2rimu6yga-4rAgB_AE6TFxqoRp7vChaYM83GQGQ,5308
18
18
  lime/plotting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- lime/plotting/bokeh_plots.py,sha256=EwqyHArAuX43bQK3ggZosWGNSsAD9B4n6g6KBofXUAE,29853
20
- lime/plotting/format.py,sha256=40RL7s8yVt91ZhjDgKH-v-cYC2meE5uRTIwhgOBzKOU,7574
19
+ lime/plotting/bokeh_plots.py,sha256=HUYjMvLr90vpV5d4zeHgzvcG0Uv1NzAa36XayyK8_Z8,33019
20
+ lime/plotting/format.py,sha256=Y85t6ah-MVWW4cJIsxsT2RUANlZ9QLal-Bk9SofdipM,7414
21
21
  lime/plotting/plots.py,sha256=L_iy_M2g5E9NgbozhQkKDN9zaKIn4Q8BoNIjAoKkjuE,109861
22
22
  lime/plotting/plots_interactive.py,sha256=H1NQpaAjoIAgpbPYrRGYvo-mpKHB8MQ8VF8vvWNtcjA,53873
23
- lime/plotting/theme_lime.toml,sha256=X6j8M2d42xDkjXK8iODrFBHuCRIQJpJ995t6FVs0XfY,3939
23
+ lime/plotting/theme_lime.toml,sha256=tuc2nG4vazYXF56-YBizhgJCz1GEUHOgb21WUb_gMAs,3933
24
24
  lime/plotting/utils.py,sha256=n8NYheQ_E__aJOxlgm-4Yn6BzJBlfCanBqW17dHiIHw,2203
25
25
  lime/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  lime/resources/generator_db.py,sha256=JZnVx_BEWsxldKnB7y79jGiQILg7M89Wyy5PktvBqvo,5939
@@ -29,9 +29,9 @@ lime/resources/lines_database_v2.0.0.txt,sha256=kcE9lxsl4NEh_Gs0Jo65r9UW1k9JlfYD
29
29
  lime/resources/lines_database_v2.0.6.txt,sha256=iEQEjQSd0LFtpnb_4bmH2A0BzcTPLXsmNZbVlWt3o-U,71831
30
30
  lime/resources/types_params.txt,sha256=X2-KQhRQLKLnO4kP3olndfLEUEi5gn7dFA73BcM1mls,10237
31
31
  lime/retrieve/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
- lime/retrieve/line_bands.py,sha256=-1XZuYgtj6UrnD-rJW5GwcYYvADwMg_LDD8gVHoSyPU,13014
33
- lime_stable-2.2.dev2.dist-info/licenses/LICENSE.rst,sha256=seOLF7CWBhO2ouy8K_seWP3undpeEXppbpb7lYgH5vg,35413
34
- lime_stable-2.2.dev2.dist-info/METADATA,sha256=aKOtU4E-C--WYuqRPD24pW0M56DRdQQxorzwNJgj7Io,3830
35
- lime_stable-2.2.dev2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
36
- lime_stable-2.2.dev2.dist-info/top_level.txt,sha256=F6pWR5Cgjf9EkXNBZlUSKFKcPG8vPzM08QwYFfwpsZc,5
37
- lime_stable-2.2.dev2.dist-info/RECORD,,
32
+ lime/retrieve/line_bands.py,sha256=s4QfruLbYiliVl0jT2d_PIBcBDjg7skh5w-1lV1HdKM,17046
33
+ lime_stable-2.2.dev5.dist-info/licenses/LICENSE.rst,sha256=seOLF7CWBhO2ouy8K_seWP3undpeEXppbpb7lYgH5vg,35413
34
+ lime_stable-2.2.dev5.dist-info/METADATA,sha256=YaOfuSxRwG0AdlU1tk6MDQOGWcfi2-hmoEusE2aE7Ek,3830
35
+ lime_stable-2.2.dev5.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
36
+ lime_stable-2.2.dev5.dist-info/top_level.txt,sha256=F6pWR5Cgjf9EkXNBZlUSKFKcPG8vPzM08QwYFfwpsZc,5
37
+ lime_stable-2.2.dev5.dist-info/RECORD,,