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/filter/utils.py ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env python
2
+
3
+ """Utility functions for the filter module."""
4
+
5
+ import logging
6
+ from typing import List, Optional
7
+
8
+ class InMemoryHandler(logging.Handler):
9
+ """A logging handler that stores log records in memory."""
10
+
11
+ def __init__(self, capacity: Optional[int] = None):
12
+ """Initialize the handler with optional capacity limit.
13
+
14
+ Parameters
15
+ ----------
16
+ capacity : Optional[int]
17
+ Maximum number of log records to store. If None, no limit is applied.
18
+ """
19
+ super().__init__()
20
+ self.capacity = capacity
21
+ self.logs: List[str] = []
22
+
23
+ def emit(self, record: logging.LogRecord) -> None:
24
+ """Store the log record in memory.
25
+
26
+ Parameters
27
+ ----------
28
+ record : logging.LogRecord
29
+ The log record to store
30
+ """
31
+ log_entry = self.format(record)
32
+ self.logs.append(log_entry)
33
+ if self.capacity and len(self.logs) > self.capacity:
34
+ self.logs.pop(0)
35
+
36
+ def get_logs(self, log_type: str = 'all') -> List[str]:
37
+ """Get all stored log records.
38
+
39
+ Parameters
40
+ ----------
41
+ log_type : str
42
+ log type to get
43
+ possible values are 'all', 'info', 'debug', 'warning', 'error'
44
+ (default: 'all')
45
+
46
+ Returns
47
+ -------
48
+ List[str]
49
+ List of formatted log records
50
+ """
51
+ if log_type == 'all':
52
+ return self.logs.copy()
53
+ elif log_type == 'info':
54
+ return [log for log in self.logs if log[22:].startswith('INFO')]
55
+ elif log_type == 'debug':
56
+ return [log for log in self.logs if log[22:].startswith('DEBUG')]
57
+ elif log_type == 'warning':
58
+ return [log for log in self.logs if log[22:].startswith('WARNING')]
59
+ elif log_type == 'error':
60
+ return [log for log in self.logs if log[22:].startswith('ERROR')]
61
+ else:
62
+ raise ValueError(f"Invalid log type: {log_type}")
63
+
64
+ def clear(self) -> None:
65
+ """Clear all stored log records."""
66
+ self.logs.clear()
67
+
68
+ SVO_BASE_URL = 'http://svo2.cab.inta-csic.es/theory/fps/fps.php?'
69
+
70
+ SVO_FILTER_URL = SVO_BASE_URL + 'ID='
71
+ SVO_META_URL = SVO_BASE_URL + 'FORMAT=metadata'
sedlib/helper.py ADDED
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env python
2
+
3
+ """
4
+ Helper functions for sedlib
5
+ """
6
+
7
+ __all__ = [
8
+ 'get_pattern', '_init_simbad', 'query_gaia_parameters',
9
+ 'find_nearest', 'dumps_quantities', 'get_tmag', 'select_preferred_filters'
10
+ ]
11
+
12
+ import json
13
+ from typing import Any
14
+
15
+ import numpy as np
16
+
17
+ from astropy import units as u
18
+ from astropy.units import Quantity
19
+ from astroquery.simbad import Simbad
20
+ from astroquery.gaia import Gaia
21
+ from astroquery.vizier import Vizier
22
+
23
+
24
+ VIZIER_PHOTOMETRY_API_URL = 'https://vizier.cds.unistra.fr/viz-bin/sed'
25
+
26
+ FILTER_SYSTEM_MAPPINGS = {
27
+ 'HIP': {
28
+ 'Hp': 'Hipparcos/Hipparcos.Hp',
29
+ 'BT': 'TYCHO/TYCHO.B',
30
+ 'VT': 'TYCHO/TYCHO.B'
31
+ },
32
+ 'SDSS': lambda fn: f'SLOAN/SDSS.{fn[0]}prime_filter' if fn[-1] == "\'" else f'SLOAN/SDSS.{fn}',
33
+ 'TYCHO': lambda fn: f'TYCHO/{fn}',
34
+ 'Gaia': lambda fn: f'GAIA/GAIA3.{fn}',
35
+ 'GAIA/GAIA2': lambda fn: f'GAIA/GAIA2.{fn}',
36
+ 'GAIA/GAIA3': lambda fn: f'GAIA/GAIA3.{fn}',
37
+ 'IRAS': lambda fn: f'IRAS/IRAS.{fn}mu',
38
+ 'DIRBE': lambda fn: f'COBE/DIRBE.{fn.replace(".", "p")}m',
39
+ 'Cousins': lambda fn: f'Generic/Cousins.{fn}' if fn in ['R', 'I'] else f'Generic/Johnson_UBVRIJHKL.{fn}',
40
+ # 'Johnson': lambda fn: f'Generic/Johnson_UBVRIJHKL.{fn if fn not in ("L\'", "L", "L\'\'") else "LI" if fn in ("L\'", "L") else "LII"}'
41
+ # 'Johnson': lambda fn: f'Generic/Johnson_UBVRIJHKL.{fn}' if fn not in ("L'", "L", "L''") else f'Generic/Johnson_UBVRIJHKL.{"LI" if fn in ("L'", "L") else "LII"}'
42
+ 'Johnson': lambda fn: f'Generic/Johnson_UBVRIJHKL.{fn}' if fn not in ("L'", "L", "L''") else (
43
+ f'Generic/Johnson_UBVRIJHKL.LI' if fn in ("L'", "L") else f'Generic/Johnson_UBVRIJHKL.LII'
44
+ )
45
+ }
46
+
47
+
48
+ def get_pattern(system, filter_name):
49
+ if system in FILTER_SYSTEM_MAPPINGS:
50
+ mapping = FILTER_SYSTEM_MAPPINGS[system]
51
+ if callable(mapping):
52
+ return mapping(filter_name)
53
+ elif filter_name in mapping:
54
+ return mapping[filter_name]
55
+ return f'*{system.strip().lower()}*{filter_name.strip().lower()}*'
56
+
57
+
58
+ def find_nearest(array, target):
59
+ """
60
+ Find the nearest value in a NumPy array to a given target.
61
+
62
+ Parameters
63
+ ----------
64
+ array : numpy.ndarray
65
+ Input array to search within.
66
+ target : int or float
67
+ The target value to find the nearest to.
68
+
69
+ Returns
70
+ -------
71
+ nearest_value : float
72
+ The value in the array closest to the target.
73
+ nearest_index : tuple
74
+ The index of the nearest value (supports multi-dimensional arrays).
75
+
76
+ Raises
77
+ ------
78
+ TypeError
79
+ If `array` is not a numpy.ndarray or if `target` is not int or float.
80
+
81
+ Examples
82
+ --------
83
+ >>> array = np.array([10, 15, 20, 25, 30])
84
+ >>> find_nearest(array, 18)
85
+ (20, 2)
86
+
87
+ >>> array_2d = np.array([[10, 15], [20, 25]])
88
+ >>> find_nearest(array_2d, 18)
89
+ (20, (1, 0))
90
+ """
91
+ # Check if input is a NumPy array
92
+ if not isinstance(array, np.ndarray):
93
+ raise TypeError("Input array must be a NumPy ndarray.")
94
+
95
+ # Check if target is of type int or float
96
+ if not isinstance(target, (int, float)):
97
+ raise TypeError("Target value must be of type int or float.")
98
+
99
+ # Flatten the array to handle both 1D and multi-dimensional cases
100
+ flat_index = np.abs(array - target).ravel().argmin()
101
+
102
+ # Get the nearest value
103
+ nearest_value = array.ravel()[flat_index]
104
+
105
+ # Convert flat index to multi-dimensional index if necessary
106
+ nearest_index = np.unravel_index(flat_index, array.shape)
107
+
108
+ return nearest_value, nearest_index
109
+
110
+
111
+ def _init_simbad():
112
+ # Simbad.reset_votable_fields()
113
+ # Simbad.remove_votable_fields('coordinates')
114
+ # Simbad.add_votable_fields(
115
+ # 'ra', 'dec', 'parallax', 'otype', 'otypes', 'sptype', 'distance'
116
+ # )
117
+ Simbad.add_votable_fields(
118
+ 'parallax',
119
+ )
120
+
121
+
122
+ def query_gaia_parameters(name, search_simbad=True):
123
+ """Query Gaia DR3 to get stellar parameters for an object.
124
+
125
+ This function first searches for the object's Gaia DR3 source ID using SIMBAD,
126
+ then queries the Gaia archive for parallax, distance, temperature and radius information.
127
+
128
+ Parameters
129
+ ----------
130
+ name : str
131
+ Name of the astronomical object to query
132
+
133
+ search_simbad : bool, optional
134
+ If True, search for the object's Gaia DR3 source ID using SIMBAD.
135
+ If False, use the provided name as the Gaia DR3 source ID.
136
+
137
+ Returns
138
+ -------
139
+ dict or None
140
+ Dictionary containing source_id, parallax, parallax_error, distance_pc,
141
+ distance_error_pc, teff (effective temperature) and radius. Returns None
142
+ if no SIMBAD match is found.
143
+ """
144
+ gaia_id = name
145
+
146
+ # Query SIMBAD for object IDs
147
+ if search_simbad:
148
+ simbad_results = Simbad.query_objectids(name)
149
+ if len(simbad_results) == 0:
150
+ return None
151
+
152
+ # Find Gaia DR3 ID
153
+ gaia_id = None
154
+ for id_entry in simbad_results['id']:
155
+ if 'Gaia DR3' in id_entry:
156
+ # gaia_id = id_entry.strip().split()[-1]
157
+ gaia_id = id_entry
158
+ break
159
+
160
+ if not gaia_id:
161
+ return None
162
+
163
+ gaia_id = gaia_id.strip().split()[-1]
164
+
165
+ # Construct and execute Gaia query
166
+ query = f"""
167
+ SELECT
168
+ dr3.source_id,
169
+ dr3.ra,
170
+ dr3.dec,
171
+ dr3.parallax,
172
+ dr3.parallax_error,
173
+ 1000.0 / dr3.parallax AS distance_pc,
174
+ (1000.0 / dr3.parallax) * (dr3.parallax_error / dr3.parallax) AS distance_error_pc,
175
+ dr3.teff_gspphot AS teff,
176
+ dr3.teff_gspphot_lower AS teff_lower,
177
+ dr3.teff_gspphot_upper AS teff_upper,
178
+ ap.radius_gspphot AS radius,
179
+ ap.radius_gspphot_lower AS radius_lower,
180
+ ap.radius_gspphot_upper AS radius_upper
181
+ FROM gaiadr3.gaia_source AS dr3
182
+ INNER JOIN gaiadr3.astrophysical_parameters AS ap
183
+ ON dr3.source_id = ap.source_id
184
+ WHERE dr3.source_id = {gaia_id}
185
+ """
186
+
187
+ job = Gaia.launch_job(query)
188
+ t = job.get_results()
189
+
190
+ # Convert table row to dictionary
191
+ result = {
192
+ 'source_id': t['source_id'][0],
193
+ 'ra': t['ra'][0],
194
+ 'dec': t['dec'][0],
195
+ 'parallax': t['parallax'][0],
196
+ 'parallax_error': t['parallax_error'][0],
197
+ 'distance_pc': t['distance_pc'][0],
198
+ 'distance_error_pc': t['distance_error_pc'][0],
199
+ 'teff': t['teff'][0],
200
+ 'teff_lower': t['teff_lower'][0],
201
+ 'teff_upper': t['teff_upper'][0],
202
+ 'radius': t['radius'][0],
203
+ 'radius_lower': t['radius_lower'][0],
204
+ 'radius_upper': t['radius_upper'][0]
205
+ }
206
+
207
+ return result
208
+
209
+
210
+ def dumps_quantities(obj: Any, **json_kwargs) -> str:
211
+ """
212
+ Serialize `obj` to JSON, converting any astropy.units.Quantity into its raw value.
213
+
214
+ Parameters
215
+ ----------
216
+ obj
217
+ Any Python object (e.g. dict, list, nested structures) possibly
218
+ containing Quantity instances.
219
+ json_kwargs
220
+ Extra keyword arguments to pass through to json.dumps()
221
+ (e.g. indent=2, sort_keys=True).
222
+
223
+ Returns
224
+ -------
225
+ str
226
+ The JSON string with all Quantity instances replaced by their .value.
227
+ """
228
+ class _QuantityEncoder(json.JSONEncoder):
229
+ def default(self, o: Any) -> Any:
230
+ if isinstance(o, Quantity):
231
+ return o.value
232
+ if isinstance(o, np.ndarray):
233
+ return o.tolist()
234
+ # Handle LbfgsInvHessProduct type from scipy.optimize
235
+ if o.__class__.__name__ == 'LbfgsInvHessProduct':
236
+ return "<LbfgsInvHessProduct object>"
237
+ # Try to convert other numpy types
238
+ if isinstance(o, (np.integer, np.floating, np.bool_)):
239
+ return o.item()
240
+ return super().default(o)
241
+
242
+ return json.dumps(obj, cls=_QuantityEncoder, **json_kwargs)
243
+
244
+
245
+ def get_tmag(source_id, release="dr3"):
246
+ """
247
+ Retrieve Tmag and its uncertainty (e_Tmag) from the TESS Input Catalog (TIC 8.2, IV/39/tic82),
248
+ given either a Gaia DR2 or DR3 source_id.
249
+
250
+ Parameters
251
+ ----------
252
+ source_id : int or str
253
+ The Gaia source identifier (DR2 or DR3), depending on `release`.
254
+ release : str, optional
255
+ Indicates which Gaia release `source_id` refers to: "dr2" or "dr3".
256
+ Default is "dr3".
257
+
258
+ Returns
259
+ -------
260
+ tuple (Tmag, e_Tmag) as floats, or None if no match is found.
261
+ """
262
+ # Vizier.ROW_LIMIT = 1
263
+
264
+ catalog_id = "IV/39/tic82"
265
+ source_id = str(source_id).strip()
266
+
267
+ def _query_tic_by_dr2(dr2_id):
268
+ """Query TIC for Tmag given a DR2 source_id string."""
269
+ # 1) Try a direct GAIA‐constraint on TIC
270
+ viz = Vizier(columns=["Tmag", "e_Tmag", "GAIA"], catalog=catalog_id)
271
+ result = viz.query_constraints(GAIA=dr2_id)
272
+
273
+ if result:
274
+ t = result[0]
275
+ return float(t['Tmag'][0]), float(t['e_Tmag'][0])
276
+
277
+ # 2) Fallback: cone‐search around "Gaia DR2 <dr2_id>"
278
+ viz_fallback = Vizier(columns=["Tmag", "e_Tmag"], catalog=catalog_id)
279
+ name = f"Gaia DR2 {dr2_id}"
280
+ fallback = viz_fallback.query_object(name, radius=2 * u.arcsec)
281
+ if fallback and catalog_id in fallback and len(fallback[catalog_id]) > 0:
282
+ row = fallback[catalog_id][0]
283
+ return float(row["Tmag"]), float(row["e_Tmag"])
284
+
285
+ return None
286
+
287
+ if release.lower() == "dr2":
288
+ return _query_tic_by_dr2(source_id)
289
+
290
+ adql = f"""
291
+ SELECT TOP 1 x.dr2_source_id
292
+ FROM gaiadr3.gaia_source AS dr3
293
+ JOIN gaiadr3.dr2_neighbourhood AS x
294
+ ON dr3.source_id = x.dr3_source_id
295
+ WHERE dr3.source_id = {source_id}
296
+ """
297
+ job = Gaia.launch_job(adql)
298
+ rows = job.get_results()
299
+
300
+ if len(rows) == 0:
301
+ return None
302
+
303
+ dr2_id = str(rows["dr2_source_id"][0])
304
+ return _query_tic_by_dr2(dr2_id)
305
+
306
+
307
+ def select_preferred_filters(sed, column, column_error) -> dict:
308
+ """
309
+ The allowed filters and their priority are defined as follows:
310
+
311
+ - Johnson:B
312
+ - Johnson:V
313
+ - GAIA/GAIA3:G
314
+ - GAIA/GAIA3:Gbp
315
+ - GAIA/GAIA3:Grp
316
+ - GAIA/GAIA2:G
317
+ - GAIA/GAIA2:Gbp
318
+ - GAIA/GAIA2:Grp
319
+ - Gaia:G
320
+ - TESS/TESS:Red
321
+
322
+ Returns
323
+ -------
324
+ dict
325
+ Dictionary where keys are the selected filter names and values are
326
+ tuples (abs_mag, abs_mag_err).
327
+ """
328
+ # allowed filters in order
329
+ allowed_filters = [
330
+ 'Johnson:B',
331
+ 'Johnson:V',
332
+ 'GAIA/GAIA3:G',
333
+ 'GAIA/GAIA3:Grp',
334
+ 'GAIA/GAIA3:Gbp',
335
+ 'GAIA/GAIA2:G',
336
+ 'GAIA/GAIA2:Grp',
337
+ 'GAIA/GAIA2:Gbp',
338
+ 'Gaia:G',
339
+ 'TESS/TESS:Red'
340
+ ]
341
+
342
+ result = {}
343
+ chosen_bands = {}
344
+
345
+ for filt in allowed_filters:
346
+ mask = sed.catalog.table['vizier_filter'] == filt
347
+ if not mask.any():
348
+ continue
349
+
350
+ row = sed.catalog.table[mask][0]
351
+ mag = row[column]
352
+ mag_err = row[column_error]
353
+
354
+ band = filt.split(":")[-1]
355
+ if band not in chosen_bands:
356
+ if band == 'Red':
357
+ band = 'TESS'
358
+ result[band.upper()] = (mag, mag_err)
359
+ chosen_bands[band] = filt
360
+
361
+ return result