sedlib 1.0.0__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.
sedlib/__init__.py ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env python
2
+
3
+ """
4
+ sedlib: A Python library for Spectral Energy Distribution (SED) analysis
5
+
6
+ This module provides tools for managing photometric data, performing SED analysis,
7
+ and modeling stellar energy distributions with interstellar extinction correction.
8
+
9
+ Features:
10
+ - Catalog class for organizing photometric data
11
+ - Filter class for managing photometric filters
12
+ - SED class for SED analysis and fitting
13
+ - Integration with astronomical libraries like astropy and dust_extinction
14
+ - Advanced optimization tools for interstellar extinction correction
15
+ - BolometricCorrection class for computing bolometric corrections and radii
16
+
17
+ Author:
18
+ Oğuzhan OKUYAN
19
+ ookuyan@gmail.com, oguzhan.okuyan@tubitak.gov.tr
20
+
21
+ Version:
22
+ 1.0.0
23
+
24
+ License:
25
+ Apache License 2.0
26
+ """
27
+
28
+ __author__ = 'Oğuzhan OKUYAN'
29
+ __license__ = 'Apache License 2.0'
30
+ __version__ = '1.0.0'
31
+ __maintainer__ = 'Oğuzhan OKUYAN'
32
+ __email__ = 'ookuyan@gmail.com, oguzhan.okuyan@tubitak.gov.tr'
33
+ __description__ = "A Python library for Spectral Energy Distribution analysis"
34
+ __url__ = "https://github.com/ookuyan/sedlib"
35
+
36
+
37
+ __all__ = [
38
+ 'Filter',
39
+ 'Catalog',
40
+ 'SED',
41
+ 'BolometricCorrection'
42
+ ]
43
+
44
+ from .filter import Filter
45
+ from .catalog import Catalog
46
+ from .core import SED
47
+ from .bol2rad import BolometricCorrection
sedlib/bol2rad.py ADDED
@@ -0,0 +1,552 @@
1
+ #!/usr/bin/env python
2
+
3
+ __all__ = ['BolometricCorrection']
4
+
5
+ import math
6
+ from typing import Optional, Tuple
7
+ from importlib import resources
8
+
9
+ import yaml
10
+
11
+ import numpy as np
12
+
13
+ import astropy.units as u
14
+
15
+
16
+ class BolometricCorrection(object):
17
+ """
18
+ Class for computing bolometric corrections (BC) for stars based on their
19
+ effective temperature and for inferring stellar properties such as the weighted
20
+ bolometric magnitude and the stellar radius (expressed in solar units).
21
+
22
+ This class performs a full analysis pipeline that includes the following steps:
23
+ 1. **Loading Coefficients:** Reads temperature-to-bolometric correction
24
+ polynomial coefficients from a YAML file. These coefficients (with their
25
+ fixed uncertainties) allow the calculation of bolometric corrections using
26
+ a 4th-degree polynomial.
27
+ 2. **Filter Selection:** From the available photometric filters in the star's
28
+ SED, the class selects the preferred filters (e.g., Johnson B, V; GAIA G,
29
+ GBP, GRP, etc.) based on a pre-defined priority order.
30
+ 3. **Bolometric Correction Computation:** For each selected filter, it
31
+ computes the bolometric correction for the target star's effective
32
+ temperature.
33
+ 4. **Absolute Bolometric Magnitude Determination:** The computed corrections
34
+ are applied to the observed absolute magnitudes, and a weighted average is
35
+ obtained using inverse-variance weighting. This yields the star's absolute
36
+ bolometric magnitude and its associated uncertainty.
37
+ 5. **Stellar Radius Estimation:** Using the relation between luminosity,
38
+ radius, and effective temperature (i.e., L = 4πR²σT⁴) along with the
39
+ bolometric magnitude definition (M_Bol = -2.5 log L + constant), the
40
+ stellar radius is determined relative to the Sun. The target star's radius
41
+ (in R_sun) is computed via:
42
+ R_star / R_sun = 10^((M_bol,☉ - M_bol,star)/5) * (T_☉ / T_star)²,
43
+ and the uncertainty is propagated accordingly.
44
+
45
+ The full analysis pipeline can be executed in one go using the `run` method.
46
+
47
+ Example
48
+ -------
49
+ >>> from sedlib import SED, BolometricCorrection
50
+ >>> from astropy import units as u
51
+ >>>
52
+ >>> sed = SED(name='Vega')
53
+ >>> sed.teff = 10070 * u.K
54
+ >>> sed.radius = 2.766 * u.Rsun
55
+ >>> sed.distance = 7.68 * u.pc
56
+ >>>
57
+ >>> sed.estimate_ebv()
58
+ >>> sed.compute_A_lambda()
59
+ >>> sed.compute_absolute_magnitudes()
60
+ >>>
61
+ >>> bc = BolometricCorrection(sed=sed)
62
+ >>> bc.run()
63
+ >>> print(f"Stellar radius: {bc.radius:.2f} ± {bc.radius_error:.2f} R_sun")
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ sed: Optional[object] = None,
69
+ coeff_file: Optional[str] = None,
70
+ accept_radius: bool = False
71
+ ):
72
+ """
73
+ Initialize the BolometricCorrection instance.
74
+
75
+ Parameters
76
+ ----------
77
+ sed : sedlib.SED, optional
78
+ SED object containing catalog and stellar parameters. Must have
79
+ computed absolute magnitudes and effective temperature.
80
+ coeff_file : str, optional
81
+ Path to the YAML file containing the coefficient data. If None, the
82
+ default 'temp_to_bc_coefficients.yaml' file is used.
83
+ accept_radius : bool, optional
84
+ If True, the computed radius will be stored in sed.radius and
85
+ sed.radius_error. Default is False.
86
+
87
+ Raises
88
+ ------
89
+ ValueError
90
+ If sed object is provided but lacks required attributes.
91
+ FileNotFoundError
92
+ If coefficient file cannot be found.
93
+ yaml.YAMLError
94
+ If coefficient file cannot be parsed.
95
+
96
+ Notes
97
+ -----
98
+ The bolometric correction analysis requires:
99
+ - Effective temperature (sed.teff)
100
+ - Absolute magnitudes for preferred filters
101
+ - Distance information for magnitude calculations
102
+
103
+ The preferred filters are selected automatically based on availability
104
+ and priority: Johnson B,V; GAIA G, GBP, GRP; etc.
105
+ """
106
+ self._sed = sed
107
+ self._coefficients = {}
108
+ self._bolometric_corrections = {}
109
+ self._bolometric_mags = {}
110
+ self._abs_mags = {}
111
+
112
+ self._abs_bol_mag = None
113
+ self._abs_bol_mag_err = None
114
+
115
+ self._sun_bol_mag = 4.74
116
+ self._sun_teff = 5772 * u.K
117
+
118
+ self._radius = None
119
+ self._radius_error = None
120
+
121
+ self._accept_radius = accept_radius
122
+
123
+ if sed is not None:
124
+ self._sed = sed
125
+ self._abs_mags = self._select_preferred_filters()
126
+
127
+ self._load_coefficients(coeff_file)
128
+
129
+ @property
130
+ def abs_bol_mag(self) -> float:
131
+ return self._abs_bol_mag
132
+
133
+ @property
134
+ def abs_bol_mag_err(self) -> float:
135
+ return self._abs_bol_mag_err
136
+
137
+ @property
138
+ def radius(self) -> u.Quantity:
139
+ return self._radius
140
+
141
+ @property
142
+ def radius_error(self) -> u.Quantity:
143
+ return self._radius_error
144
+
145
+ def _load_coefficients(self, coeff_file: Optional[str] = None) -> dict:
146
+ """
147
+ Internal method to load polynomial coefficients from a YAML file and store
148
+ each as a tuple (value, error). Additionally, if a filter block contains
149
+ an "RMS" key, that value is removed and stored as the fixed error for
150
+ that filter.
151
+
152
+ Parameters
153
+ ----------
154
+ coeff_file : str, optional
155
+ Path to the YAML file containing coefficient data.
156
+ """
157
+ self._coefficients = {}
158
+
159
+ try:
160
+ if coeff_file is None:
161
+ file_obj = resources.open_text(
162
+ 'sedlib.data',
163
+ 'temp_to_bc_coefficients.yaml'
164
+ )
165
+ else:
166
+ file_obj = open(coeff_file, 'r')
167
+ with file_obj as file:
168
+ self._coefficients = yaml.safe_load(file)
169
+ except Exception as e:
170
+ source = ("package resources" if coeff_file is None
171
+ else f"file '{coeff_file}'")
172
+ raise IOError(f"Error loading coefficients from {source}: {e}")
173
+
174
+ def _select_preferred_filters(self) -> dict:
175
+ """
176
+ The allowed filters and their priority are defined as follows:
177
+
178
+ - Johnson:B
179
+ - Johnson:V
180
+ - GAIA/GAIA3:G
181
+ - GAIA/GAIA3:Gbp
182
+ - GAIA/GAIA3:Grp
183
+ - GAIA/GAIA2:G
184
+ - GAIA/GAIA2:Gbp
185
+ - GAIA/GAIA2:Grp
186
+ - Gaia:G
187
+ - TESS/TESS:Red
188
+
189
+ Returns
190
+ -------
191
+ dict
192
+ Dictionary where keys are the selected filter names and values are
193
+ tuples (abs_mag, abs_mag_err).
194
+ """
195
+ # allowed filters in order
196
+ allowed_filters = [
197
+ 'Johnson:B',
198
+ 'Johnson:V',
199
+ 'GAIA/GAIA3:G',
200
+ 'GAIA/GAIA3:Grp',
201
+ 'GAIA/GAIA3:Gbp',
202
+ 'GAIA/GAIA2:G',
203
+ 'GAIA/GAIA2:Grp',
204
+ 'GAIA/GAIA2:Gbp',
205
+ 'Gaia:G',
206
+ 'TESS/TESS:Red',
207
+ ]
208
+
209
+ result = {}
210
+ chosen_bands = {}
211
+
212
+ for filt in allowed_filters:
213
+ mask = self._sed.catalog.table['vizier_filter'] == filt
214
+ if not mask.any():
215
+ continue
216
+
217
+ row = self._sed.catalog.table[mask][0]
218
+ mag = row['abs_mag']
219
+ mag_err = row['abs_mag_err']
220
+
221
+ band = filt.split(":")[-1]
222
+ if band not in chosen_bands:
223
+ if band == 'Red':
224
+ band = 'TESS'
225
+ result[band.upper()] = (mag, mag_err)
226
+ chosen_bands[band] = filt
227
+
228
+ return result
229
+
230
+ @staticmethod
231
+ def _compute_bc_and_error(
232
+ T: float,
233
+ T_err: float,
234
+ coeffs: dict
235
+ ) -> Tuple[float, float]:
236
+ """
237
+ Internal method to compute the bolometric correction (BC) for a given
238
+ effective temperature (in Kelvin) using the 4th-degree polynomial. (The
239
+ propagated uncertainty computed here will later be replaced by the fixed
240
+ value from the YAML file.)
241
+
242
+ Parameters
243
+ ----------
244
+ T : float
245
+ Effective temperature in Kelvin (nominal value).
246
+ T_err : float
247
+ Effective temperature error in Kelvin.
248
+ coeffs : dict
249
+ Dictionary of coefficients for a specific filter with keys 'a', 'b',
250
+ 'c', 'd', 'e'. Each coefficient is a tuple: (value, error).
251
+
252
+ Returns
253
+ -------
254
+ tuple of float
255
+ (BC, sigma_BC) where sigma_BC is computed but will later be replaced
256
+ by a fixed value.
257
+ """
258
+ # Compute L = log10(T)
259
+ L = math.log10(T)
260
+ # Propagate error from T to L: σ_L = T_err / (T ln(10))
261
+ sigma_L = T_err / (T * math.log(10))
262
+
263
+ # Unpack coefficients
264
+ a, sigma_a = coeffs["a"]
265
+ b, sigma_b = coeffs["b"]
266
+ c, sigma_c = coeffs["c"]
267
+ d, sigma_d = coeffs["d"]
268
+ e, sigma_e = coeffs["e"]
269
+
270
+ # Compute nominal BC:
271
+ BC = a + L * b + (L**2) * c + (L**3) * d + (L**4) * e
272
+
273
+ # Compute propagated uncertainty (to be replaced)
274
+ dfdL = b + 2 * L * c + 3 * (L**2) * d + 4 * (L**3) * e
275
+ sigma_BC = np.sqrt(
276
+ sigma_a**2 +
277
+ (L * sigma_b)**2 +
278
+ ((L**2) * sigma_c)**2 +
279
+ ((L**3) * sigma_d)**2 +
280
+ ((L**4) * sigma_e)**2 +
281
+ (dfdL * sigma_L)**2
282
+ )
283
+ return BC, sigma_BC
284
+
285
+ def compute_bolometric_corrections(
286
+ self,
287
+ filter_name: Optional[str] = None,
288
+ return_results: bool = False
289
+ ) -> dict:
290
+ """
291
+ Compute bolometric corrections (BC) for either a single filter or all
292
+ available filters.
293
+
294
+ Parameters
295
+ ----------
296
+ filter_name : str, optional
297
+ Filter name (e.g., 'B', 'V', 'G', 'GBP', 'GRP', 'TESS').
298
+ If None, computes BCs for all available filters.
299
+ return_results : bool, optional
300
+ If True, returns the computed BCs as a dictionary.
301
+ If False, updates the instance variables and returns None.
302
+
303
+ Returns
304
+ -------
305
+ dict
306
+ Dictionary mapping filter names to tuples of (BC, fixed_error).
307
+ For a single filter, returns a dictionary with one entry.
308
+
309
+ Raises
310
+ ------
311
+ ValueError
312
+ If specified filter_name is not valid.
313
+ """
314
+ results = {}
315
+
316
+ if filter_name is not None:
317
+ # Single filter case
318
+ if filter_name not in self._coefficients:
319
+ raise ValueError(
320
+ "Invalid filter name. Choose from: " +
321
+ ", ".join(self._coefficients.keys())
322
+ )
323
+ filters_to_process = [filter_name]
324
+ else:
325
+ # All filters case
326
+ filters_to_process = self._coefficients.keys()
327
+
328
+ T = self._sed.teff.to(u.K).value
329
+ T_err = self._sed.teff_error.to(u.K).value
330
+
331
+ for filt in filters_to_process:
332
+ coeffs = self._coefficients[filt]
333
+
334
+ bc_result = self._compute_bc_and_error(T, T_err, coeffs)
335
+ fixed_err = coeffs['RMS']
336
+
337
+ results[filt] = (bc_result[0], fixed_err)
338
+
339
+ self._bolometric_corrections = results
340
+
341
+ return self._bolometric_corrections if return_results else None
342
+
343
+ def apply_correction(self):
344
+ """
345
+ Apply the bolometric correction to a set of absolute magnitudes.
346
+ """
347
+ for filt in self._abs_mags.keys():
348
+ mag, mag_err = self._abs_mags[filt]
349
+ bc, bc_err = self._bolometric_corrections[filt]
350
+
351
+ self._bolometric_mags[filt] = (
352
+ mag + bc,
353
+ np.sqrt(mag_err**2 + bc_err**2)
354
+ )
355
+
356
+ def compute_weighted_abs_bol_mag(
357
+ self,
358
+ return_results: bool = False
359
+ ) -> Tuple[float, float]:
360
+ """
361
+ Compute a weighted average of the bolometric magnitudes from different
362
+ filters.
363
+
364
+ The weighted average is computed using inverse-variance weighting:
365
+
366
+ weighted_mag = sum(m_i / sigma_i^2) / sum(1 / sigma_i^2)
367
+ weighted_error = sqrt(1 / sum(1 / sigma_i^2))
368
+
369
+ Parameters
370
+ ----------
371
+ return_results : bool, optional
372
+ If True, returns the computed weighted average as a tuple.
373
+ If False, updates the instance variables and returns None.
374
+
375
+ Returns
376
+ -------
377
+ tuple of float
378
+ (weighted_bolometric_magnitude, weighted_error)
379
+
380
+ Raises
381
+ ------
382
+ ValueError
383
+ If no bolometric magnitudes have been computed.
384
+ """
385
+ if not self._bolometric_mags:
386
+ raise ValueError(
387
+ "No bolometric magnitudes available. "
388
+ "Run apply_correction() first."
389
+ )
390
+
391
+ mags = []
392
+ errors = []
393
+ for filt in self._bolometric_mags:
394
+ mag, err = self._bolometric_mags[filt]
395
+ mags.append(float(mag))
396
+ errors.append(err)
397
+
398
+ mags = np.array(mags)
399
+ errors = np.array(errors)
400
+ weights = 1 / errors**2
401
+
402
+ self._abs_bol_mag = np.sum(mags * weights) / np.sum(weights)
403
+ self._abs_bol_mag_err = np.sqrt(1 / np.sum(weights))
404
+
405
+ if return_results:
406
+ return (self._abs_bol_mag, self._abs_bol_mag_err)
407
+ return None
408
+
409
+ def compute_normalized_radius(
410
+ self,
411
+ return_results: bool = False
412
+ ) -> Tuple[u.Quantity, u.Quantity]:
413
+ """
414
+ The radius ratio is calculated as:
415
+ R_star / R_sun = 10^((M_bol,sun - M_bol,star)/5) * (T_sun / T_star)^2
416
+
417
+ Uncertainty propagation (via logarithmic differentiation):
418
+ (delta R_star / R_star)^2 = ( (ln10/5 * delta M_bol,star)^2 +
419
+ (2 * delta T_star / T_star)^2 )
420
+
421
+ Parameters
422
+ ----------
423
+ return_results : bool, optional
424
+ If True, return the radius ratio and its error as a tuple.
425
+ If False, update the instance variables and return None.
426
+
427
+ Returns
428
+ -------
429
+ tuple of astropy.units.Quantity
430
+ A tuple (radius_ratio, radius_ratio_err) where:
431
+ - radius_ratio is the target star's radius in units of the Sun's
432
+ radius.
433
+ - radius_ratio_err is the corresponding uncertainty.
434
+
435
+ Raises
436
+ ------
437
+ ValueError
438
+ If the target star's bolometric magnitude (or its error) has not been
439
+ computed, or if the Sun's parameters are not defined.
440
+ """
441
+ # Ensure that the target star's bolometric magnitude and error are
442
+ # available.
443
+ if self._abs_bol_mag is None or self._abs_bol_mag_err is None:
444
+ raise ValueError(
445
+ "Target star's bolometric magnitude and/or its error have not "
446
+ "been computed. Please run compute_weighted_abs_bol_mag() first."
447
+ )
448
+
449
+ # Retrieve target star parameters.
450
+ target_mbol = self._abs_bol_mag
451
+ target_mbol_err = self._abs_bol_mag_err
452
+ target_teff = self._sed.teff.to(u.K).value
453
+ target_teff_err = self._sed.teff_error.to(u.K).value
454
+
455
+ # Retrieve Sun parameters.
456
+ sun_bol_mag = self._sun_bol_mag
457
+ sun_teff = self._sun_teff.value
458
+
459
+ # Compute the radius ratio using the relation:
460
+ # R_star / R_sun = 10^((M_bol,sun - M_bol,star)/5) * (T_sun / T_star)^2
461
+ radius_ratio = (10 ** ((sun_bol_mag - target_mbol) / 5.0) *
462
+ (sun_teff / target_teff) ** 2)
463
+
464
+ # Propagate uncertainties.
465
+ # The logarithmic derivative of R_ratio is:
466
+ # d(ln(R_ratio)) = -(ln10/5)*dM_bol,star - 2*(dT_star/T_star)
467
+ # Thus, the relative uncertainty is:
468
+ # (delta R_ratio / R_ratio)^2 = ( (ln10/5 * delta M_bol,star)^2 +
469
+ # (2 * delta T_star/T_star)^2 )
470
+ rel_error = np.sqrt(
471
+ (np.log(10) / 5 * target_mbol_err) ** 2 +
472
+ (2 * target_teff_err / target_teff) ** 2
473
+ )
474
+ radius_ratio_err = radius_ratio * rel_error
475
+
476
+ self._radius = radius_ratio * u.R_sun
477
+ self._radius_error = radius_ratio_err * u.R_sun
478
+
479
+ if return_results:
480
+ return self._radius, self._radius_error
481
+ return None
482
+
483
+ def run(self, verbose: bool = False) -> None:
484
+ """
485
+ Execute the complete bolometric correction analysis pipeline.
486
+
487
+ The pipeline performs the following steps:
488
+ 1. Compute bolometric corrections for available filters.
489
+ 2. Apply the bolometric corrections to the target star's absolute
490
+ magnitudes.
491
+ 3. Compute a weighted average of the corrected bolometric magnitudes.
492
+ 4. Compute the target star's radius in units of the Sun's radius by
493
+ comparing the target's bolometric magnitude and effective temperature
494
+ with solar values.
495
+
496
+ Parameters
497
+ ----------
498
+ verbose : bool, optional
499
+ If True, print verbose output.
500
+
501
+ Returns
502
+ -------
503
+ tuple of astropy.units.Quantity
504
+ A tuple (radius, radius_error)
505
+
506
+ Example
507
+ -------
508
+ >>> from sedlib import SED, BolometricCorrection
509
+ >>> from astropy import units as u
510
+ >>>
511
+ >>> sed = SED(name='Vega')
512
+ >>> sed.teff = 10070 * u.K
513
+ >>> sed.radius = 2.766 * u.Rsun
514
+ >>> sed.distance = 7.68 * u.pc
515
+ >>>
516
+ >>> sed.estimate_ebv()
517
+ >>> sed.compute_A_lambda()
518
+ >>> sed.compute_absolute_magnitudes()
519
+ >>>
520
+ >>> bc = BolometricCorrection(sed=sed)
521
+ >>> bc.run()
522
+ >>> print(
523
+ ... f"Stellar radius: {bc.radius:.2f} ± {bc.radius_error:.2f} R_sun"
524
+ ... )
525
+ """
526
+ # Execute pipeline sequentially
527
+ self.compute_bolometric_corrections()
528
+ self.apply_correction()
529
+ self.compute_weighted_abs_bol_mag()
530
+ self.compute_normalized_radius()
531
+
532
+ if verbose:
533
+ print(
534
+ f"Stellar radius: {self._radius:.2f} ± "
535
+ f"{self._radius_error:.2f} R_sun"
536
+ )
537
+
538
+ if self._accept_radius:
539
+ self._sed._radius = self._radius
540
+ self._sed._radius_error = self._radius_error
541
+
542
+ self._result = {
543
+ 'abs_mags': self._abs_mags,
544
+ 'bolometric_corrections': self._bolometric_corrections,
545
+ 'bol_mags': self._bolometric_mags,
546
+ 'abs_bol_mag': self._abs_bol_mag,
547
+ 'abs_bol_mag_err': self._abs_bol_mag_err,
548
+ 'radius': self._radius,
549
+ 'radius_error': self._radius_error
550
+ }
551
+
552
+ return self._radius, self._radius_error