exotools 0.0.1__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.
- exotools/__init__.py +47 -0
- exotools/datasets/__init__.py +13 -0
- exotools/datasets/_exoplanet_dataset_reducer.py +96 -0
- exotools/datasets/candidate_exoplanets.py +44 -0
- exotools/datasets/gaia_parameters.py +68 -0
- exotools/datasets/known_exoplanets.py +124 -0
- exotools/datasets/lightcurves.py +58 -0
- exotools/datasets/tess.py +91 -0
- exotools/db/__init__.py +21 -0
- exotools/db/base_db.py +74 -0
- exotools/db/exo_db.py +113 -0
- exotools/db/gaia_db.py +62 -0
- exotools/db/lightcurve_db.py +112 -0
- exotools/db/lightcurve_plus.py +196 -0
- exotools/db/star_system/__init__.py +14 -0
- exotools/db/star_system/planet.py +90 -0
- exotools/db/star_system/star.py +26 -0
- exotools/db/star_system/star_system.py +87 -0
- exotools/db/star_system/uncertain_data.py +29 -0
- exotools/db/starsystem_db.py +38 -0
- exotools/db/tic_db.py +23 -0
- exotools/db/toi_db.py +13 -0
- exotools/db/urls_db.py +36 -0
- exotools/downloaders/__init__.py +22 -0
- exotools/downloaders/_utils.py +22 -0
- exotools/downloaders/dataset_downloader.py +98 -0
- exotools/downloaders/exoplanets_downloader.py +103 -0
- exotools/downloaders/gaia_downloader.py +135 -0
- exotools/downloaders/lightcurve_downloader.py +107 -0
- exotools/downloaders/tap_service.py +84 -0
- exotools/downloaders/tess_catalog_downloader.py +155 -0
- exotools/downloaders/tess_observations_downloader.py +61 -0
- exotools/downloaders/toi_exoplanets_downloader.py +52 -0
- exotools/io/__init__.py +11 -0
- exotools/io/base_storage.py +33 -0
- exotools/io/fs_storage.py +158 -0
- exotools/io/hdf5_storage.py +164 -0
- exotools/py.typed +0 -0
- exotools/utils/__init__.py +6 -0
- exotools/utils/download.py +7 -0
- exotools/utils/observations_fix.py +57 -0
- exotools/utils/qtable_utils.py +55 -0
- exotools/utils/unit_mapper.py +67 -0
- exotools-0.0.1.dist-info/METADATA +125 -0
- exotools-0.0.1.dist-info/RECORD +48 -0
- exotools-0.0.1.dist-info/WHEEL +5 -0
- exotools-0.0.1.dist-info/licenses/LICENSE +21 -0
- exotools-0.0.1.dist-info/top_level.txt +1 -0
exotools/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""ExoTools - Tools for working with exoplanet data."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .db.star_system import Star, Planet, StarSystem, UncertainValue, UncertainDataSource
|
|
6
|
+
from exotools.datasets.known_exoplanets import KnownExoplanetsDataset
|
|
7
|
+
from exotools.datasets.candidate_exoplanets import CandidateExoplanetsDataset
|
|
8
|
+
from exotools.datasets.tess import TessDataset
|
|
9
|
+
from exotools.datasets.lightcurves import LightcurveDataset
|
|
10
|
+
|
|
11
|
+
from .db import (
|
|
12
|
+
CandidateDB,
|
|
13
|
+
ExoDB,
|
|
14
|
+
GaiaDB,
|
|
15
|
+
StarSystemDB,
|
|
16
|
+
LightcurveDB,
|
|
17
|
+
LightCurvePlus,
|
|
18
|
+
TessMetaDB,
|
|
19
|
+
TicDB,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .utils.download import DownloadParams
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
# Main dataset classes
|
|
26
|
+
"KnownExoplanetsDataset",
|
|
27
|
+
"CandidateExoplanetsDataset",
|
|
28
|
+
"TessDataset",
|
|
29
|
+
"LightcurveDataset",
|
|
30
|
+
# Database classes
|
|
31
|
+
"CandidateDB",
|
|
32
|
+
"ExoDB",
|
|
33
|
+
"GaiaDB",
|
|
34
|
+
"StarSystemDB",
|
|
35
|
+
"LightcurveDB",
|
|
36
|
+
"LightCurvePlus",
|
|
37
|
+
"TessMetaDB",
|
|
38
|
+
"TicDB",
|
|
39
|
+
# Star system types
|
|
40
|
+
"Star",
|
|
41
|
+
"Planet",
|
|
42
|
+
"StarSystem",
|
|
43
|
+
"UncertainValue",
|
|
44
|
+
"UncertainDataSource",
|
|
45
|
+
# Utility types
|
|
46
|
+
"DownloadParams",
|
|
47
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from exotools.datasets.candidate_exoplanets import CandidateExoplanetsDataset
|
|
2
|
+
from exotools.datasets.known_exoplanets import KnownExoplanetsDataset
|
|
3
|
+
from exotools.datasets.tess import TessDataset
|
|
4
|
+
from exotools.datasets.lightcurves import LightcurveDataset
|
|
5
|
+
from exotools.datasets.gaia_parameters import GaiaParametersDataset
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"CandidateExoplanetsDataset",
|
|
9
|
+
"KnownExoplanetsDataset",
|
|
10
|
+
"TessDataset",
|
|
11
|
+
"LightcurveDataset",
|
|
12
|
+
"GaiaParametersDataset",
|
|
13
|
+
]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from astropy.table import QTable
|
|
4
|
+
|
|
5
|
+
from exotools.db import ExoDB
|
|
6
|
+
from exotools.utils.qtable_utils import get_empty_table_header, TableColumnInfo, QTableHeader
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get_subset_df(table: QTable) -> pd.DataFrame:
|
|
10
|
+
"""Select a subset of data from the main exoplanet dataset, including upper and lower bounds"""
|
|
11
|
+
err_cols = []
|
|
12
|
+
dataset_columns = ["tic_id", "gaia_id", "disc_telescope", "rowupdate"]
|
|
13
|
+
star_columns = ["hostname", "hostname_lowercase", "st_rad", "st_rad_gaia", "st_mass"]
|
|
14
|
+
planet_columns = [
|
|
15
|
+
"pl_name",
|
|
16
|
+
"pl_rade",
|
|
17
|
+
"pl_masse",
|
|
18
|
+
"pl_dens",
|
|
19
|
+
"pl_orbeccen",
|
|
20
|
+
"pl_orbper",
|
|
21
|
+
"pl_orblper",
|
|
22
|
+
"pl_orbincl",
|
|
23
|
+
"pl_orbsmax",
|
|
24
|
+
"pl_tranmid",
|
|
25
|
+
"pl_trandur",
|
|
26
|
+
"pl_trandep",
|
|
27
|
+
"pl_imppar",
|
|
28
|
+
"pl_ratror",
|
|
29
|
+
"pl_ratdor",
|
|
30
|
+
]
|
|
31
|
+
fields = dataset_columns + star_columns + planet_columns
|
|
32
|
+
for p in fields:
|
|
33
|
+
if f"{p}_lower" in table.colnames:
|
|
34
|
+
err_cols.extend([f"{p}_lower", f"{p}_upper"])
|
|
35
|
+
return table[fields + err_cols].to_pandas()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _reduce_group(group: pd.DataFrame) -> pd.Series:
|
|
39
|
+
"""
|
|
40
|
+
For each column, select the first element that is not null.
|
|
41
|
+
Parameters:
|
|
42
|
+
- group: a dataframe grouped by planet name, and sorted by "rowupdate" in descending order
|
|
43
|
+
"""
|
|
44
|
+
first_non_null = group.apply(lambda col: col.dropna().iloc[0] if not col.dropna().empty else np.nan)
|
|
45
|
+
return first_non_null
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _reduce_df(table: QTable) -> pd.DataFrame:
|
|
49
|
+
"""
|
|
50
|
+
Reduce all the planets having multiple rows to only one single row, taking the most updated not-null value
|
|
51
|
+
available in the dataset.
|
|
52
|
+
"""
|
|
53
|
+
df = _get_subset_df(table)
|
|
54
|
+
# Sort by update date
|
|
55
|
+
sorted_df = df.sort_values("rowupdate", ascending=False)
|
|
56
|
+
|
|
57
|
+
# Group by 'pl_name' and apply reduce_group
|
|
58
|
+
grouped = sorted_df.groupby("pl_name", as_index=True)
|
|
59
|
+
reduced_groups = grouped.apply(_reduce_group, include_groups=False).reset_index(drop=False)
|
|
60
|
+
return reduced_groups
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _flag_invalid_planets(dataset: pd.DataFrame):
|
|
64
|
+
"""
|
|
65
|
+
Adds a pl_valid_flag to the planets which have all the required parameters
|
|
66
|
+
"""
|
|
67
|
+
# Without these we can't fit the transits
|
|
68
|
+
mandatory_fields = ["pl_rade", "pl_trandur", "pl_tranmid", "pl_orbsmax"]
|
|
69
|
+
|
|
70
|
+
# Add the validation flag to the dataset
|
|
71
|
+
dataset["pl_valid_flag"] = ~dataset[mandatory_fields].isna().any(axis=1)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def reduce_exoplanet_dataset(exo_db: ExoDB) -> tuple[QTable, QTableHeader]:
|
|
75
|
+
"""
|
|
76
|
+
Post-processes the Known exoplanets dataset to select only the transiting planets, and reducing multiple
|
|
77
|
+
entries for each planet to only one. Additionally, impute some missing values using GAIA data.
|
|
78
|
+
"""
|
|
79
|
+
# Load datasets, limiting to only Tess and Kepler transiting planets
|
|
80
|
+
exo_db = exo_db.get_transiting_planets(kepler_or_tess_only=False)
|
|
81
|
+
|
|
82
|
+
# Reduce exoplanet dataset
|
|
83
|
+
reduced_df = _reduce_df(exo_db.dataset_copy)
|
|
84
|
+
_flag_invalid_planets(reduced_df)
|
|
85
|
+
|
|
86
|
+
# Assign units and convert to QTable
|
|
87
|
+
units_map = {c: exo_db.view[c].unit for c in reduced_df.columns if c in exo_db.view.colnames}
|
|
88
|
+
reduced_table = QTable.from_pandas(reduced_df, units=units_map)
|
|
89
|
+
|
|
90
|
+
# Assign units to reduced dataset and convert to QTable
|
|
91
|
+
reduced_header = get_empty_table_header(reduced_table)
|
|
92
|
+
reduced_header["pl_valid_flag"] = TableColumnInfo(
|
|
93
|
+
unit=None, description="True if the planet has all the " "parameters to determine transit events"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return reduced_table, reduced_header
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from astropy.table import QTable
|
|
4
|
+
|
|
5
|
+
from exotools.db import ExoDB, CandidateDB
|
|
6
|
+
from exotools.downloaders import CandidateExoplanetsDownloader
|
|
7
|
+
from exotools.io import BaseStorage
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CandidateExoplanetsDataset:
|
|
11
|
+
_DATASET_NAME_CANDIDATES = "candidate_exoplanets"
|
|
12
|
+
|
|
13
|
+
def __init__(self, storage: BaseStorage):
|
|
14
|
+
self._storage = storage
|
|
15
|
+
|
|
16
|
+
def load_candidate_exoplanets_dataset(self) -> Optional[CandidateDB]:
|
|
17
|
+
try:
|
|
18
|
+
candidate_qtable = self._storage.read_qtable(table_name=self._DATASET_NAME_CANDIDATES)
|
|
19
|
+
except ValueError:
|
|
20
|
+
print(
|
|
21
|
+
"Candidate Exoplanets dataset not found. "
|
|
22
|
+
"You need to download it first by calling download_candidate_exoplanets(store=True)."
|
|
23
|
+
)
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
return _create_candidate_db(candidate_dataset=candidate_qtable)
|
|
27
|
+
|
|
28
|
+
def download_candidate_exoplanets(self, limit: Optional[int] = None, store: bool = True) -> CandidateDB:
|
|
29
|
+
print("Preparing to download candidate exoplanets dataset...")
|
|
30
|
+
candidate_qtable, candidate_header = CandidateExoplanetsDownloader().download(limit=limit)
|
|
31
|
+
|
|
32
|
+
if store:
|
|
33
|
+
self._storage.write_qtable(
|
|
34
|
+
table=candidate_qtable,
|
|
35
|
+
header=candidate_header,
|
|
36
|
+
table_name=self._DATASET_NAME_CANDIDATES,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return _create_candidate_db(candidate_qtable)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _create_candidate_db(candidate_dataset: QTable) -> CandidateDB:
|
|
43
|
+
ExoDB.compute_bounds(candidate_dataset)
|
|
44
|
+
return CandidateDB(candidate_dataset)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from typing import Optional, Sequence
|
|
2
|
+
|
|
3
|
+
from astropy.table import QTable
|
|
4
|
+
|
|
5
|
+
from exotools.db import GaiaDB
|
|
6
|
+
from exotools.downloaders import GaiaDownloader
|
|
7
|
+
from exotools.io import BaseStorage
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GaiaParametersDataset:
|
|
11
|
+
_DATASET_GAIA = "known_gaia_astro_parameters"
|
|
12
|
+
|
|
13
|
+
def __init__(self, storage: BaseStorage):
|
|
14
|
+
self._storage = storage
|
|
15
|
+
|
|
16
|
+
def load_gaia_parameters_dataset(self) -> Optional[GaiaDB]:
|
|
17
|
+
"""
|
|
18
|
+
Load Gaia parameters dataset from storage.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
GaiaDB: Database containing Gaia parameters, or None if not found.
|
|
22
|
+
"""
|
|
23
|
+
try:
|
|
24
|
+
gaia_qtable = self._storage.read_qtable(table_name=self._DATASET_GAIA)
|
|
25
|
+
return self._create_gaia_db(gaia_qtable)
|
|
26
|
+
except ValueError:
|
|
27
|
+
print("Gaia dataset not found. You need to download it first by " "calling download_gaia_parameters().")
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
def download_gaia_parameters(self, gaia_ids: Sequence[int], store: bool = True) -> GaiaDB:
|
|
31
|
+
"""
|
|
32
|
+
Download Gaia DR3 data for the given Gaia IDs.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
gaia_ids: Sequence of Gaia IDs to download data for.
|
|
36
|
+
store: Whether to store the downloaded data.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
GaiaDB: Database containing the downloaded Gaia parameters.
|
|
40
|
+
"""
|
|
41
|
+
print(f"Preparing to download Gaia DR3 data for {len(gaia_ids)} stars...")
|
|
42
|
+
|
|
43
|
+
gaia_qtable, gaia_header = GaiaDownloader().download_by_id(ids=gaia_ids)
|
|
44
|
+
if store:
|
|
45
|
+
self._storage.write_qtable(
|
|
46
|
+
table=gaia_qtable,
|
|
47
|
+
header=gaia_header,
|
|
48
|
+
table_name=self._DATASET_GAIA,
|
|
49
|
+
override=True,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return self._create_gaia_db(gaia_qtable)
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def _create_gaia_db(gaia_dataset: QTable) -> GaiaDB:
|
|
56
|
+
"""
|
|
57
|
+
Create a GaiaDB instance from a QTable dataset.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
gaia_dataset: QTable containing Gaia data.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
GaiaDB: Database containing processed Gaia parameters.
|
|
64
|
+
"""
|
|
65
|
+
GaiaDB.impute_radius(gaia_dataset)
|
|
66
|
+
GaiaDB.compute_mean_temperature(gaia_dataset)
|
|
67
|
+
GaiaDB.compute_habitable_zone(gaia_dataset)
|
|
68
|
+
return GaiaDB(gaia_dataset)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from astropy.table import QTable
|
|
5
|
+
|
|
6
|
+
from exotools.datasets.gaia_parameters import GaiaParametersDataset
|
|
7
|
+
from exotools.db import ExoDB, GaiaDB, StarSystemDB
|
|
8
|
+
from exotools.downloaders import KnownExoplanetsDownloader
|
|
9
|
+
from exotools.io import BaseStorage
|
|
10
|
+
from ._exoplanet_dataset_reducer import reduce_exoplanet_dataset
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class KnownExoplanetsDataset:
|
|
14
|
+
_DATASET_EXO = "known_exoplanets"
|
|
15
|
+
_DATASET_EXO_REDUCED = "known_exoplanets_reduced"
|
|
16
|
+
|
|
17
|
+
def __init__(self, storage: BaseStorage):
|
|
18
|
+
self._storage = storage
|
|
19
|
+
self._gaia_dataset = GaiaParametersDataset(storage)
|
|
20
|
+
|
|
21
|
+
def load_known_exoplanets_dataset(self, with_gaia_star_data: bool = True) -> Optional[ExoDB]:
|
|
22
|
+
gaia_db = None
|
|
23
|
+
if with_gaia_star_data:
|
|
24
|
+
gaia_db = self._gaia_dataset.load_gaia_parameters_dataset()
|
|
25
|
+
if gaia_db is None:
|
|
26
|
+
print(
|
|
27
|
+
"Gaia dataset not found. You need to download it first by "
|
|
28
|
+
"calling download_known_exoplanets(with_gaia_star_data=True, store=True)."
|
|
29
|
+
)
|
|
30
|
+
return None
|
|
31
|
+
try:
|
|
32
|
+
exo_qtable = self._storage.read_qtable(table_name=self._DATASET_EXO)
|
|
33
|
+
except ValueError:
|
|
34
|
+
print(
|
|
35
|
+
"Known Exoplanets dataset not found. "
|
|
36
|
+
"You need to download it first by calling download_known_exoplanets(store=True)."
|
|
37
|
+
)
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
return _create_exo_db(exo_dataset=exo_qtable, gaia_db=gaia_db)
|
|
41
|
+
|
|
42
|
+
def load_star_system_dataset(self) -> Optional[StarSystemDB]:
|
|
43
|
+
try:
|
|
44
|
+
# Try to load reduced dataset
|
|
45
|
+
reduced_exo_dataset = self._storage.read_qtable(table_name=self._DATASET_EXO_REDUCED)
|
|
46
|
+
return _create_star_system_db(reduced_exo_dataset)
|
|
47
|
+
except ValueError:
|
|
48
|
+
# If it doesn't exist, compute it from the full datasets
|
|
49
|
+
gaia_db = self._gaia_dataset.load_gaia_parameters_dataset()
|
|
50
|
+
if gaia_db is None:
|
|
51
|
+
print(
|
|
52
|
+
"Gaia dataset not found. You need to download it first by "
|
|
53
|
+
"calling download_known_exoplanets(with_gaia_star_data=True, store=True)."
|
|
54
|
+
)
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
exo_qtable = self._storage.read_qtable(table_name=self._DATASET_EXO)
|
|
59
|
+
except ValueError:
|
|
60
|
+
print(
|
|
61
|
+
"Known Exoplanets dataset not found. "
|
|
62
|
+
"You need to download it first by calling download_known_exoplanets(store=True)."
|
|
63
|
+
)
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
return self._create_star_system_db_from_scratch(exo_dataset=exo_qtable, gaia_db=gaia_db)
|
|
67
|
+
|
|
68
|
+
def download_known_exoplanets(
|
|
69
|
+
self,
|
|
70
|
+
limit: Optional[int] = None,
|
|
71
|
+
with_gaia_star_data: bool = False,
|
|
72
|
+
store: bool = True,
|
|
73
|
+
) -> ExoDB:
|
|
74
|
+
print("Preparing to download known exoplanets dataset...")
|
|
75
|
+
exo_qtable, exo_header = KnownExoplanetsDownloader().download(limit=limit)
|
|
76
|
+
|
|
77
|
+
if store:
|
|
78
|
+
self._storage.write_qtable(table=exo_qtable, header=exo_header, table_name=self._DATASET_EXO, override=True)
|
|
79
|
+
|
|
80
|
+
if with_gaia_star_data:
|
|
81
|
+
gaia_ids = np.unique(exo_qtable["gaia_id"].value).tolist()
|
|
82
|
+
gaia_db = self._gaia_dataset.download_gaia_parameters(gaia_ids=gaia_ids, store=store)
|
|
83
|
+
else:
|
|
84
|
+
gaia_db = None
|
|
85
|
+
|
|
86
|
+
return _create_exo_db(exo_qtable, gaia_db)
|
|
87
|
+
|
|
88
|
+
def _create_star_system_db_from_scratch(self, exo_dataset: QTable, gaia_db: GaiaDB) -> StarSystemDB:
|
|
89
|
+
# Disable parsing Time columns; we need them as Quantities to copy units to the transiting qtable.
|
|
90
|
+
exo_db = _create_exo_db(exo_dataset=exo_dataset, gaia_db=gaia_db, convert_time_columns=False)
|
|
91
|
+
|
|
92
|
+
# Reduce exoplanet dataset to a compact representation
|
|
93
|
+
reduced_exo_dataset, header = reduce_exoplanet_dataset(exo_db=exo_db)
|
|
94
|
+
|
|
95
|
+
# Store the reduced dataset for future use
|
|
96
|
+
self._storage.write_qtable(
|
|
97
|
+
table=reduced_exo_dataset,
|
|
98
|
+
header=header,
|
|
99
|
+
table_name=self._DATASET_EXO_REDUCED,
|
|
100
|
+
override=True,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return _create_star_system_db(reduced_exo_dataset)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _create_exo_db(exo_dataset: QTable, gaia_db: Optional[GaiaDB] = None, convert_time_columns: bool = True) -> ExoDB:
|
|
107
|
+
ExoDB.preprocess_dataset(exo_dataset)
|
|
108
|
+
ExoDB.compute_bounds(exo_dataset)
|
|
109
|
+
|
|
110
|
+
# It's useful to disable parsing Time columns if we need them as Quantities,
|
|
111
|
+
# for example to copy units to another qtable.
|
|
112
|
+
if convert_time_columns:
|
|
113
|
+
ExoDB.convert_time_columns(exo_dataset)
|
|
114
|
+
|
|
115
|
+
if gaia_db:
|
|
116
|
+
ExoDB.impute_stellar_parameters(exo_dataset, gaia_db.view)
|
|
117
|
+
return ExoDB(exo_dataset)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _create_star_system_db(reduced_exo_dataset: QTable) -> StarSystemDB:
|
|
121
|
+
# Now it's safe to parse Time columns
|
|
122
|
+
ExoDB.convert_time_columns(reduced_exo_dataset)
|
|
123
|
+
reduced_exo_dataset = StarSystemDB.preprocess_dataset(reduced_exo_dataset)
|
|
124
|
+
return StarSystemDB(reduced_exo_dataset)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from exotools.db import TessMetaDB, LightcurveDB
|
|
5
|
+
from exotools.downloaders import LightcurveDownloader
|
|
6
|
+
from exotools.io import BaseStorage
|
|
7
|
+
from exotools.utils.download import DownloadParams
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LightcurveDataset:
|
|
11
|
+
_DATASET_LIGHTCURVES = "lightcurves"
|
|
12
|
+
|
|
13
|
+
def __init__(self, storage: BaseStorage, override_existing: bool = False, verbose: bool = False):
|
|
14
|
+
self._folder_path = storage.root_path / self._DATASET_LIGHTCURVES
|
|
15
|
+
self._downloader = LightcurveDownloader(override_existing=override_existing, verbose=verbose)
|
|
16
|
+
|
|
17
|
+
def download_lightcurves_from_tess_db(self, tess_db: TessMetaDB) -> Optional[LightcurveDB]:
|
|
18
|
+
download_params = [
|
|
19
|
+
DownloadParams(
|
|
20
|
+
url=row["dataURL"],
|
|
21
|
+
download_path=str(self._folder_path / str(row["tic_id"]) / f"{row['obs_id']}.fits"),
|
|
22
|
+
)
|
|
23
|
+
for row in tess_db.view
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
print(f"Downloading {len(download_params)} lightcurves")
|
|
27
|
+
downloaded_paths = self._downloader.download_fits_parallel(download_params)
|
|
28
|
+
print(f"Downloaded {len(downloaded_paths)} lightcurves")
|
|
29
|
+
|
|
30
|
+
return self.load_lightcurve_dataset()
|
|
31
|
+
|
|
32
|
+
def load_lightcurve_dataset(self) -> Optional[LightcurveDB]:
|
|
33
|
+
downloaded_paths = _get_file_paths_in_subfolder(self._folder_path, file_extension="fits")
|
|
34
|
+
if len(downloaded_paths) == 0:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
dataset = LightcurveDB.path_map_to_qtable(downloaded_paths)
|
|
38
|
+
return LightcurveDB(dataset)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_file_paths_in_subfolder(
|
|
42
|
+
parent_path: Path,
|
|
43
|
+
file_extension: Optional[str] = None,
|
|
44
|
+
match_name: Optional[str] = None,
|
|
45
|
+
) -> dict[int, list[Path]]:
|
|
46
|
+
subfolder_dict = {}
|
|
47
|
+
if not file_extension and not match_name:
|
|
48
|
+
raise ValueError("At least one between file_extension and match_name should be given")
|
|
49
|
+
pattern = match_name if match_name else f"*.{file_extension}"
|
|
50
|
+
|
|
51
|
+
# Iterate over each subfolder
|
|
52
|
+
for subfolder in parent_path.iterdir():
|
|
53
|
+
if subfolder.is_dir():
|
|
54
|
+
fits_files = list(subfolder.glob(pattern))
|
|
55
|
+
if fits_files:
|
|
56
|
+
subfolder_dict[int(subfolder.name)] = [Path(file) for file in fits_files]
|
|
57
|
+
|
|
58
|
+
return subfolder_dict
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from typing import Sequence, Optional
|
|
2
|
+
|
|
3
|
+
from exotools.db import TicDB, TessMetaDB
|
|
4
|
+
from exotools.downloaders import TessCatalogDownloader, TessObservationsDownloader
|
|
5
|
+
from exotools.io import BaseStorage
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TessDataset:
|
|
9
|
+
_OBSERVATIONS_NAME = "tess_observations"
|
|
10
|
+
_TIC_NAME = "tess_tic"
|
|
11
|
+
_TIC_BY_ID_NAME = "tess_tic_by_id"
|
|
12
|
+
|
|
13
|
+
def __init__(self, storage: BaseStorage, username: Optional[str] = None, password: Optional[str] = None):
|
|
14
|
+
self._storage = storage
|
|
15
|
+
self._catalog_downloader = TessCatalogDownloader(username, password) if username and password else None
|
|
16
|
+
|
|
17
|
+
def download_observation_metadata(self, targets_tic_id: Sequence[int], store: bool = True) -> TessMetaDB:
|
|
18
|
+
print(f"Preparing to download TESS observation list for {len(targets_tic_id)} objects...")
|
|
19
|
+
meta_qtable, meta_header = TessObservationsDownloader().download_by_id(targets_tic_id)
|
|
20
|
+
|
|
21
|
+
if store:
|
|
22
|
+
self._storage.write_qtable(meta_qtable, meta_header, self._OBSERVATIONS_NAME, override=True)
|
|
23
|
+
|
|
24
|
+
return TessMetaDB(meta_dataset=meta_qtable)
|
|
25
|
+
|
|
26
|
+
def search_tic_targets(
|
|
27
|
+
self,
|
|
28
|
+
limit: Optional[int] = None,
|
|
29
|
+
star_mass_range: Optional[tuple[float, float]] = None,
|
|
30
|
+
priority_threshold: Optional[float] = None,
|
|
31
|
+
store: bool = False,
|
|
32
|
+
) -> TicDB:
|
|
33
|
+
if self._catalog_downloader is None:
|
|
34
|
+
raise ValueError("You need to provide a username and password to download the TIC dataset.")
|
|
35
|
+
if star_mass_range is not None:
|
|
36
|
+
self._catalog_downloader.star_mass_range = star_mass_range
|
|
37
|
+
if priority_threshold is not None:
|
|
38
|
+
self._catalog_downloader.priority_threshold = priority_threshold
|
|
39
|
+
|
|
40
|
+
catalog_qtable, catalog_header = self._catalog_downloader.download(limit=limit)
|
|
41
|
+
|
|
42
|
+
if store:
|
|
43
|
+
self._storage.write_qtable(catalog_qtable, catalog_header, self._TIC_NAME, override=True)
|
|
44
|
+
|
|
45
|
+
return TicDB(dataset=catalog_qtable)
|
|
46
|
+
|
|
47
|
+
def download_tic_targets_by_ids(self, tic_ids: Sequence[int], store: bool = False) -> TicDB:
|
|
48
|
+
if self._catalog_downloader is None:
|
|
49
|
+
raise ValueError("You need to provide a username and password to download the TIC dataset.")
|
|
50
|
+
catalog_qtable, catalog_header = self._catalog_downloader.download_by_id(tic_ids)
|
|
51
|
+
|
|
52
|
+
if store:
|
|
53
|
+
self._storage.write_qtable(catalog_qtable, catalog_header, self._TIC_BY_ID_NAME, override=True)
|
|
54
|
+
|
|
55
|
+
return TicDB(dataset=catalog_qtable)
|
|
56
|
+
|
|
57
|
+
def load_observation_metadata(self) -> Optional[TessMetaDB]:
|
|
58
|
+
try:
|
|
59
|
+
meta_qtable = self._storage.read_qtable(table_name=self._OBSERVATIONS_NAME)
|
|
60
|
+
except ValueError:
|
|
61
|
+
print(
|
|
62
|
+
"Stored TIC dataset not found. You need to download it first by "
|
|
63
|
+
"calling download_tic_targets(store=True)."
|
|
64
|
+
)
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
return TessMetaDB(meta_dataset=meta_qtable)
|
|
68
|
+
|
|
69
|
+
def load_tic_target_dataset(self) -> Optional[TicDB]:
|
|
70
|
+
try:
|
|
71
|
+
tic_qtable = self._storage.read_qtable(table_name=self._TIC_NAME)
|
|
72
|
+
except ValueError:
|
|
73
|
+
print(
|
|
74
|
+
"Stored TIC dataset not found. You need to download it first by "
|
|
75
|
+
"calling download_tic_targets(store=True)."
|
|
76
|
+
)
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
return TicDB(dataset=tic_qtable)
|
|
80
|
+
|
|
81
|
+
def load_tic_target_dataset_by_id(self) -> Optional[TicDB]:
|
|
82
|
+
try:
|
|
83
|
+
tic_qtable = self._storage.read_qtable(table_name=self._TIC_BY_ID_NAME)
|
|
84
|
+
except ValueError:
|
|
85
|
+
print(
|
|
86
|
+
"Stored TIC dataset not found. You need to download it first by "
|
|
87
|
+
"calling download_tic_targets(store=True)."
|
|
88
|
+
)
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
return TicDB(dataset=tic_qtable)
|
exotools/db/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Database classes for exotools."""
|
|
2
|
+
|
|
3
|
+
from .lightcurve_plus import LightCurvePlus
|
|
4
|
+
from .toi_db import CandidateDB
|
|
5
|
+
from .exo_db import ExoDB
|
|
6
|
+
from .gaia_db import GaiaDB
|
|
7
|
+
from .starsystem_db import StarSystemDB
|
|
8
|
+
from .lightcurve_db import LightcurveDB
|
|
9
|
+
from .urls_db import TessMetaDB
|
|
10
|
+
from .tic_db import TicDB
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"CandidateDB",
|
|
14
|
+
"ExoDB",
|
|
15
|
+
"GaiaDB",
|
|
16
|
+
"StarSystemDB",
|
|
17
|
+
"LightcurveDB",
|
|
18
|
+
"LightCurvePlus",
|
|
19
|
+
"TessMetaDB",
|
|
20
|
+
"TicDB",
|
|
21
|
+
]
|
exotools/db/base_db.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from astropy.table import QTable
|
|
6
|
+
|
|
7
|
+
NAN_VALUE = -1
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseDB(ABC):
|
|
11
|
+
def __init__(self, dataset: QTable, id_field: str):
|
|
12
|
+
dataset.add_index(id_field)
|
|
13
|
+
self._ds = dataset
|
|
14
|
+
self._id_column = id_field
|
|
15
|
+
self._id_mask = self._ds[self._id_column] != NAN_VALUE
|
|
16
|
+
self._masked_ds = self._ds[self._id_mask]
|
|
17
|
+
|
|
18
|
+
def __len__(self):
|
|
19
|
+
return len(self.view)
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def _factory(self, dataset: QTable) -> "BaseDB":
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def view(self) -> QTable:
|
|
27
|
+
return self._ds
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def dataset_copy(self) -> QTable:
|
|
31
|
+
return self._ds.copy()
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def ids(self) -> np.ndarray:
|
|
35
|
+
return self._masked_ds[self._id_column].value
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def unique_ids(self) -> np.ndarray:
|
|
39
|
+
return np.unique(self.ids)
|
|
40
|
+
|
|
41
|
+
def match_ids(self, other_ids: np.ndarray) -> np.ndarray:
|
|
42
|
+
"""
|
|
43
|
+
Get matching IDs from another id set
|
|
44
|
+
"""
|
|
45
|
+
return np.isin(self.ids, other_ids)
|
|
46
|
+
|
|
47
|
+
def match_field(self, field_name: str, other_values: np.ndarray) -> np.ndarray:
|
|
48
|
+
"""
|
|
49
|
+
Get matching IDs from another id set
|
|
50
|
+
"""
|
|
51
|
+
return np.isin(self.view[field_name], other_values)
|
|
52
|
+
|
|
53
|
+
def select_by_id(self, other_ids: np.ndarray) -> "BaseDB":
|
|
54
|
+
"""
|
|
55
|
+
Match ids and returns data
|
|
56
|
+
"""
|
|
57
|
+
matching_ids = self.match_ids(other_ids)
|
|
58
|
+
return self._factory(self._masked_ds[matching_ids])
|
|
59
|
+
|
|
60
|
+
def select_by_mask(self, bit_mask: np.ndarray) -> "BaseDB":
|
|
61
|
+
"""
|
|
62
|
+
Returns data that matches the mask
|
|
63
|
+
"""
|
|
64
|
+
return self._factory(self._ds[bit_mask])
|
|
65
|
+
|
|
66
|
+
def select_random_sample(self, n: int, unique_ids: bool = True) -> "BaseDB":
|
|
67
|
+
if unique_ids:
|
|
68
|
+
return self.select_by_id(np.random.choice(self.unique_ids, size=n, replace=False))
|
|
69
|
+
|
|
70
|
+
random_indices = np.random.choice(len(self.view), size=n, replace=False)
|
|
71
|
+
return self._factory(self.view[random_indices])
|
|
72
|
+
|
|
73
|
+
def to_pandas(self) -> pd.DataFrame:
|
|
74
|
+
return self.view.to_pandas().reset_index()
|