timewise 1.0.0a2__py3-none-any.whl → 1.0.0a5__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.
- ampel/timewise/alert/TimewiseAlertSupplier.py +113 -0
- ampel/timewise/alert/load/TimewiseFileLoader.py +118 -0
- ampel/timewise/ingest/TiCompilerOptions.py +20 -0
- ampel/timewise/ingest/TiDataPointShaper.py +91 -0
- ampel/timewise/ingest/TiMongoMuxer.py +176 -0
- ampel/timewise/ingest/tags.py +15 -0
- ampel/timewise/t1/T1HDBSCAN.py +222 -0
- ampel/timewise/t1/TimewiseFilter.py +47 -0
- ampel/timewise/t2/T2StackVisits.py +56 -0
- ampel/timewise/util/AuxDiagnosticPlotter.py +47 -0
- ampel/timewise/util/pdutil.py +48 -0
- timewise/__init__.py +1 -1
- timewise/cli.py +76 -70
- timewise/io/download.py +11 -13
- timewise/process/interface.py +15 -3
- timewise/query/base.py +9 -2
- {timewise-1.0.0a2.dist-info → timewise-1.0.0a5.dist-info}/METADATA +49 -29
- {timewise-1.0.0a2.dist-info → timewise-1.0.0a5.dist-info}/RECORD +21 -10
- {timewise-1.0.0a2.dist-info → timewise-1.0.0a5.dist-info}/WHEEL +0 -0
- {timewise-1.0.0a2.dist-info → timewise-1.0.0a5.dist-info}/entry_points.txt +0 -0
- {timewise-1.0.0a2.dist-info → timewise-1.0.0a5.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# File: ampel/timewise/t1/T1HDBSCAN.py
|
|
4
|
+
# License: BSD-3-Clause
|
|
5
|
+
# Author: Jannis Necker <jannis.necker@gmail.com>
|
|
6
|
+
# Date: 24.09.2025
|
|
7
|
+
# Last Modified Date: 24.09.2025
|
|
8
|
+
# Last Modified By: Jannis Necker <jannis.necker@gmail.com>
|
|
9
|
+
from typing import Iterable, Sequence
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
from numpy import typing as npt
|
|
13
|
+
from ampel.base.AuxUnitRegister import AuxUnitRegister
|
|
14
|
+
from astropy.coordinates.angle_utilities import angular_separation, position_angle
|
|
15
|
+
from sklearn.cluster import HDBSCAN
|
|
16
|
+
from pymongo import MongoClient
|
|
17
|
+
|
|
18
|
+
from ampel.content.DataPoint import DataPoint
|
|
19
|
+
from ampel.struct.T1CombineResult import T1CombineResult
|
|
20
|
+
from ampel.types import DataPointId
|
|
21
|
+
from ampel.abstract.AbsT1CombineUnit import AbsT1CombineUnit
|
|
22
|
+
from ampel.model.UnitModel import UnitModel
|
|
23
|
+
|
|
24
|
+
from ampel.timewise.util.pdutil import datapoints_to_dataframe
|
|
25
|
+
from ampel.timewise.util.AuxDiagnosticPlotter import AuxDiagnosticPlotter
|
|
26
|
+
from timewise.process import keys
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class T1HDBSCAN(AbsT1CombineUnit):
|
|
30
|
+
input_mongo_db_name: str
|
|
31
|
+
original_id_key: str
|
|
32
|
+
whitelist_region_arcsec: float = 1
|
|
33
|
+
cluster_distance_arcsec: float = 0.5
|
|
34
|
+
|
|
35
|
+
plot: bool = False
|
|
36
|
+
plotter: UnitModel = UnitModel.model_validate(
|
|
37
|
+
{
|
|
38
|
+
"unit": "AuxDiagnosticPlotter",
|
|
39
|
+
"config": {
|
|
40
|
+
"plot_properties": {
|
|
41
|
+
"file_name": {
|
|
42
|
+
"format_str": "%s_hdbscan_selection.svg",
|
|
43
|
+
"arg_keys": ["stock"],
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def __init__(self, **kwargs):
|
|
51
|
+
super().__init__(**kwargs)
|
|
52
|
+
self._col = MongoClient()[self.input_mongo_db_name]["input"]
|
|
53
|
+
self._plotter = AuxUnitRegister.new_unit(
|
|
54
|
+
model=self.plotter, sub_type=AuxDiagnosticPlotter
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def combine(
|
|
58
|
+
self, datapoints: Iterable[DataPoint]
|
|
59
|
+
) -> Sequence[DataPointId] | T1CombineResult:
|
|
60
|
+
columns = ["ra", "dec", "mjd"]
|
|
61
|
+
if self.plot:
|
|
62
|
+
for i in range(1, 3):
|
|
63
|
+
columns += [
|
|
64
|
+
f"w{i}{self._plotter.lum_key}",
|
|
65
|
+
f"w{i}{keys.ERROR_EXT}{self._plotter.lum_key}",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
lightcurve, stock_ids = datapoints_to_dataframe(
|
|
69
|
+
datapoints, columns, check_tables=["allwise_p3as_mep"]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# make sure that the is one stock id that fits all dps
|
|
73
|
+
# this is a redundant check, the muxer should take care of it
|
|
74
|
+
unique_stocks = np.unique(np.array(stock_ids).flatten())
|
|
75
|
+
stock_in_all_dps = [
|
|
76
|
+
all([s in sids for sids in stock_ids]) for s in unique_stocks
|
|
77
|
+
]
|
|
78
|
+
# make sure only one stock is in all datapoints
|
|
79
|
+
assert sum(stock_in_all_dps) == 1
|
|
80
|
+
stock_id = unique_stocks[stock_in_all_dps][0].item()
|
|
81
|
+
self.logger.debug(f"stock: {stock_id}")
|
|
82
|
+
|
|
83
|
+
# query the database that holds the parent sample
|
|
84
|
+
d = self._col.find_one({self.original_id_key: stock_id})
|
|
85
|
+
source_ra = d["ra"]
|
|
86
|
+
source_dec = d["dec"]
|
|
87
|
+
|
|
88
|
+
lc_ra_rad = np.deg2rad(lightcurve.ra)
|
|
89
|
+
lc_dec_rad = np.deg2rad(lightcurve.dec)
|
|
90
|
+
source_ra_rad = np.deg2rad(source_ra)
|
|
91
|
+
source_dec_rad = np.deg2rad(source_dec)
|
|
92
|
+
|
|
93
|
+
# calculate separation and position angle
|
|
94
|
+
_angular_separation = angular_separation(
|
|
95
|
+
source_ra_rad, source_dec_rad, lc_ra_rad, lc_dec_rad
|
|
96
|
+
)
|
|
97
|
+
_position_angle = position_angle(
|
|
98
|
+
source_ra_rad, source_dec_rad, lc_ra_rad, lc_dec_rad
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# The AllWISE multiframe pipeline detects sources on the deep coadded atlas images and then measures the sources
|
|
102
|
+
# for all available single-exposure images in all bands simultaneously, while the NEOWISE magnitudes are
|
|
103
|
+
# obtained by PSF fit to individual exposures directly. Effect: all allwise data points that belong to the same
|
|
104
|
+
# object have the same position. We take only the closest one and treat it as one datapoint in the clustering.
|
|
105
|
+
allwise_time_mask = lightcurve.allwise_p3as_mep
|
|
106
|
+
if any(allwise_time_mask):
|
|
107
|
+
allwise_sep_min = np.min(_angular_separation[allwise_time_mask])
|
|
108
|
+
closest_allwise_mask = (
|
|
109
|
+
_angular_separation == allwise_sep_min
|
|
110
|
+
) & allwise_time_mask
|
|
111
|
+
closest_allwise_mask_first_entry = (
|
|
112
|
+
~closest_allwise_mask.duplicated() & closest_allwise_mask
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# the data we want to use is then the selected AllWISE datapoint and the NEOWISE-R data
|
|
116
|
+
data_mask = closest_allwise_mask_first_entry | ~allwise_time_mask
|
|
117
|
+
else:
|
|
118
|
+
closest_allwise_mask_first_entry = closest_allwise_mask = None
|
|
119
|
+
data_mask = np.ones_like(_angular_separation, dtype=bool)
|
|
120
|
+
|
|
121
|
+
# no matter which cluster they belong to, we want to keep all datapoints within 1 arcsec
|
|
122
|
+
one_arcsec_mask = _angular_separation < np.radians(
|
|
123
|
+
self.whitelist_region_arcsec / 3600
|
|
124
|
+
)
|
|
125
|
+
selected_indices = set(lightcurve.index[data_mask & one_arcsec_mask])
|
|
126
|
+
|
|
127
|
+
# if there are more than one datapoints, we use a clustering algorithm to potentially find a cluster with
|
|
128
|
+
# its center within 1 arcsec
|
|
129
|
+
if data_mask.sum() > 1:
|
|
130
|
+
# instead of the polar coordinates separation and position angle we use cartesian coordinates because the
|
|
131
|
+
# clustering algorithm works better with them
|
|
132
|
+
cartesian_full = np.array(
|
|
133
|
+
[
|
|
134
|
+
_angular_separation * np.cos(_position_angle),
|
|
135
|
+
_angular_separation * np.sin(_position_angle),
|
|
136
|
+
]
|
|
137
|
+
).T
|
|
138
|
+
cartesian = cartesian_full[data_mask]
|
|
139
|
+
|
|
140
|
+
# we are now ready to do the clustering
|
|
141
|
+
cluster_res = HDBSCAN(
|
|
142
|
+
store_centers="centroid",
|
|
143
|
+
min_cluster_size=max(min(20, len(cartesian)), 2),
|
|
144
|
+
allow_single_cluster=True,
|
|
145
|
+
cluster_selection_epsilon=np.radians(
|
|
146
|
+
self.cluster_distance_arcsec / 3600
|
|
147
|
+
),
|
|
148
|
+
).fit(cartesian)
|
|
149
|
+
centroids: npt.NDArray[np.float64] = cluster_res.__getattribute__(
|
|
150
|
+
"centroids_"
|
|
151
|
+
)
|
|
152
|
+
labels: npt.NDArray[np.int64] = cluster_res.__getattribute__("labels_")
|
|
153
|
+
|
|
154
|
+
# we select the closest cluster within 1 arcsec
|
|
155
|
+
cluster_separations = np.sqrt(np.sum(centroids**2, axis=1))
|
|
156
|
+
self.logger.debug(f"Found {len(cluster_separations)} clusters")
|
|
157
|
+
|
|
158
|
+
# if there is no cluster or no cluster within 1 arcsec,
|
|
159
|
+
# only the datapoints within 1 arcsec are selected as we did above
|
|
160
|
+
if len(cluster_separations) == 0:
|
|
161
|
+
self.logger.debug(
|
|
162
|
+
"No cluster found. Selecting all noise datapoints within 1 arcsec."
|
|
163
|
+
)
|
|
164
|
+
elif (_min := min(cluster_separations)) > np.radians(
|
|
165
|
+
self.whitelist_region_arcsec / 3600
|
|
166
|
+
):
|
|
167
|
+
self.logger.debug(
|
|
168
|
+
f"Closest cluster is at {np.degrees(_min) * 3600} arcsec"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# if there is a cluster within 1 arcsec, we select all datapoints belonging to that cluster
|
|
172
|
+
# in addition to the datapoints within 1 arcsec
|
|
173
|
+
else:
|
|
174
|
+
closest_label = cluster_separations.argmin()
|
|
175
|
+
selected_cluster_mask = labels == closest_label
|
|
176
|
+
|
|
177
|
+
# now we have to trace back the selected datapoints to the original lightcurve
|
|
178
|
+
selected_indices |= set(
|
|
179
|
+
lightcurve.index[data_mask][selected_cluster_mask]
|
|
180
|
+
)
|
|
181
|
+
self.logger.debug(f"Selected {len(selected_indices)} datapoints")
|
|
182
|
+
|
|
183
|
+
else:
|
|
184
|
+
# if there is only one datapoint we give him a label manually
|
|
185
|
+
labels = np.array([-1])
|
|
186
|
+
|
|
187
|
+
# if the closest allwise source is selected, we also select all other detections belonging to that
|
|
188
|
+
# source in the allwise period
|
|
189
|
+
if (
|
|
190
|
+
closest_allwise_mask_first_entry is not None
|
|
191
|
+
and lightcurve.index[closest_allwise_mask_first_entry][0]
|
|
192
|
+
in selected_indices
|
|
193
|
+
):
|
|
194
|
+
closest_allwise_mask_not_first = (
|
|
195
|
+
closest_allwise_mask & ~closest_allwise_mask_first_entry
|
|
196
|
+
)
|
|
197
|
+
closest_allwise_indices_not_first = lightcurve.index[
|
|
198
|
+
closest_allwise_mask_not_first
|
|
199
|
+
]
|
|
200
|
+
self.logger.debug(
|
|
201
|
+
f"Adding remaining {len(closest_allwise_indices_not_first)} from AllWISE period"
|
|
202
|
+
)
|
|
203
|
+
selected_indices |= set(closest_allwise_indices_not_first)
|
|
204
|
+
|
|
205
|
+
selected_indices_list = list(selected_indices)
|
|
206
|
+
res = T1CombineResult(dps=selected_indices_list)
|
|
207
|
+
|
|
208
|
+
if self.plot:
|
|
209
|
+
all_labels = np.array([-1] * len(lightcurve))
|
|
210
|
+
all_labels[data_mask] = labels
|
|
211
|
+
svg_rec = self._plotter.make_plot(
|
|
212
|
+
lightcurve,
|
|
213
|
+
None,
|
|
214
|
+
all_labels,
|
|
215
|
+
source_ra,
|
|
216
|
+
source_dec,
|
|
217
|
+
selected_indices_list,
|
|
218
|
+
highlight_radius=self.whitelist_region_arcsec,
|
|
219
|
+
)
|
|
220
|
+
res.add_meta("plot", svg_rec)
|
|
221
|
+
|
|
222
|
+
return res
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# File: timewise/ampel/timewise/t1/TimewiseFilter.py
|
|
4
|
+
# License: BSD-3-Clause
|
|
5
|
+
# Author: Jannis Necker <jannis.necker@gmail.com>
|
|
6
|
+
# Date: 16.09.2025
|
|
7
|
+
# Last Modified Date: 16.09.2025
|
|
8
|
+
# Last Modified By: Jannis Necker <jannis.necker@gmail.com>
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from ampel.abstract.AbsAlertFilter import AbsAlertFilter
|
|
12
|
+
from ampel.protocol.AmpelAlertProtocol import AmpelAlertProtocol
|
|
13
|
+
|
|
14
|
+
from timewise.util.visits import get_visit_map
|
|
15
|
+
from timewise.process import keys
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TimewiseFilter(AbsAlertFilter):
|
|
19
|
+
det_per_visit: int = 8
|
|
20
|
+
n_visits = 10
|
|
21
|
+
|
|
22
|
+
def process(self, alert: AmpelAlertProtocol) -> None | bool | int:
|
|
23
|
+
columns = [
|
|
24
|
+
"ra",
|
|
25
|
+
"dec",
|
|
26
|
+
"mjd",
|
|
27
|
+
]
|
|
28
|
+
for i in range(1, 3):
|
|
29
|
+
for key in [keys.MAG_EXT, keys.FLUX_EXT]:
|
|
30
|
+
columns.extend([f"w{i}{key}", f"w{i}{keys.ERROR_EXT}{key}"])
|
|
31
|
+
|
|
32
|
+
mjd = np.array([dp["mjd"] for dp in alert.datapoints])
|
|
33
|
+
|
|
34
|
+
# enough single detections per visit
|
|
35
|
+
visit_map = get_visit_map(mjd)
|
|
36
|
+
visits, counts = np.unique(visit_map, return_counts=True)
|
|
37
|
+
visit_passed = counts >= self.det_per_visit
|
|
38
|
+
if not all(visit_passed):
|
|
39
|
+
self.logger.info(None, extra={"min_det_per_visit": min(counts).item()})
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
# enough visits
|
|
43
|
+
if not len(visits) >= self.n_visits:
|
|
44
|
+
self.logger.info(None, extra={"n_visits": len(visits)})
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
return True
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# File: ampel/timewise/t2/T2StackVisits.py
|
|
4
|
+
# License: BSD-3-Clause
|
|
5
|
+
# Author: Jannis Necker <jannis.necker@gmail.com>
|
|
6
|
+
# Date: 24.09.2025
|
|
7
|
+
# Last Modified Date: 24.09.2025
|
|
8
|
+
# Last Modified By: Jannis Necker <jannis.necker@gmail.com>
|
|
9
|
+
from scipy import stats
|
|
10
|
+
|
|
11
|
+
from ampel.abstract.AbsLightCurveT2Unit import AbsLightCurveT2Unit
|
|
12
|
+
from ampel.struct.UnitResult import UnitResult
|
|
13
|
+
from ampel.types import UBson
|
|
14
|
+
from ampel.view.LightCurve import LightCurve
|
|
15
|
+
|
|
16
|
+
from ampel.timewise.util.pdutil import datapoints_to_dataframe
|
|
17
|
+
|
|
18
|
+
from timewise.process import keys
|
|
19
|
+
from timewise.process.stacking import stack_visits
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
SIGMA = float(stats.chi2.cdf(1, 1))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class T2StackVisits(AbsLightCurveT2Unit):
|
|
26
|
+
clean_outliers: bool = True
|
|
27
|
+
|
|
28
|
+
# spread quantile of each visit to estimate, defaults to
|
|
29
|
+
# ~68% which is the equivalent of 1 sigma for a normal distribution
|
|
30
|
+
outlier_quantile: float = SIGMA
|
|
31
|
+
|
|
32
|
+
# threshold above which to exclude outliers
|
|
33
|
+
outlier_threshold: float = 5
|
|
34
|
+
|
|
35
|
+
def process(self, light_curve: LightCurve) -> UBson | UnitResult:
|
|
36
|
+
columns = [
|
|
37
|
+
"ra",
|
|
38
|
+
"dec",
|
|
39
|
+
"mjd",
|
|
40
|
+
]
|
|
41
|
+
for i in range(1, 3):
|
|
42
|
+
for key in [keys.MAG_EXT, keys.FLUX_EXT]:
|
|
43
|
+
columns.extend([f"w{i}{key}", f"w{i}{keys.ERROR_EXT}{key}"])
|
|
44
|
+
|
|
45
|
+
photopoints = light_curve.get_photopoints()
|
|
46
|
+
if photopoints is None:
|
|
47
|
+
return {}
|
|
48
|
+
data, _ = datapoints_to_dataframe(photopoints, columns=columns)
|
|
49
|
+
if len(data) == 0:
|
|
50
|
+
return {}
|
|
51
|
+
return stack_visits(
|
|
52
|
+
data,
|
|
53
|
+
outlier_threshold=self.outlier_threshold,
|
|
54
|
+
outlier_quantile=self.outlier_quantile,
|
|
55
|
+
clean_outliers=self.clean_outliers,
|
|
56
|
+
).to_dict(orient="records")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from typing import Literal, Dict, Any
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from numpy import typing as npt
|
|
5
|
+
|
|
6
|
+
from ampel.plot.create import create_plot_record
|
|
7
|
+
from ampel.base.AmpelBaseModel import AmpelBaseModel
|
|
8
|
+
from ampel.model.PlotProperties import PlotProperties
|
|
9
|
+
from ampel.content.NewSVGRecord import NewSVGRecord
|
|
10
|
+
|
|
11
|
+
from timewise.plot.diagnostic import DiagnosticPlotter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AuxDiagnosticPlotter(AmpelBaseModel):
|
|
15
|
+
plot_properties: PlotProperties
|
|
16
|
+
cutout: Literal["sdss", "panstarrs"] = DiagnosticPlotter.model_fields[
|
|
17
|
+
"cutout"
|
|
18
|
+
].default
|
|
19
|
+
band_colors: Dict[str, str] = DiagnosticPlotter.model_fields["band_colors"].default
|
|
20
|
+
lum_key: str = DiagnosticPlotter.model_fields["lum_key"].default
|
|
21
|
+
|
|
22
|
+
def __init__(self, /, **data: Any):
|
|
23
|
+
super().__init__(**data)
|
|
24
|
+
self._plotter = DiagnosticPlotter.model_validate(
|
|
25
|
+
{k: self.__getattribute__(k) for k in ["cutout", "band_colors", "lum_key"]}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def make_plot(
|
|
29
|
+
self,
|
|
30
|
+
raw_lightcurve: pd.DataFrame,
|
|
31
|
+
stacked_lightcurve: pd.DataFrame | None,
|
|
32
|
+
labels: npt.ArrayLike,
|
|
33
|
+
source_ra: float,
|
|
34
|
+
source_dec: float,
|
|
35
|
+
selected_indices: list[Any],
|
|
36
|
+
highlight_radius: float | None = None,
|
|
37
|
+
) -> NewSVGRecord:
|
|
38
|
+
fig, axs = self._plotter.make_plot(
|
|
39
|
+
raw_lightcurve=raw_lightcurve,
|
|
40
|
+
stacked_lightcurve=stacked_lightcurve,
|
|
41
|
+
labels=labels,
|
|
42
|
+
source_ra=source_ra,
|
|
43
|
+
source_dec=source_dec,
|
|
44
|
+
selected_indices=selected_indices,
|
|
45
|
+
highlight_radius=highlight_radius,
|
|
46
|
+
)
|
|
47
|
+
return create_plot_record(fig, self.plot_properties)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Iterable, List
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
from ampel.types import StockId
|
|
5
|
+
from ampel.content.DataPoint import DataPoint
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def datapoints_to_dataframe(
|
|
9
|
+
datapoints: Iterable[DataPoint],
|
|
10
|
+
columns: list[str],
|
|
11
|
+
check_tables: list[str] | None = None,
|
|
12
|
+
) -> tuple[pd.DataFrame, List[List[StockId]]]:
|
|
13
|
+
"""
|
|
14
|
+
Convert a list of Ampel DataPoints into a pandas DataFrame.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
datapoints : list of dict
|
|
19
|
+
List of DataPoints (each must have a "body" dict).
|
|
20
|
+
columns : list of str
|
|
21
|
+
Keys from datapoint["body"] to include as DataFrame columns.
|
|
22
|
+
check_tables: list of str
|
|
23
|
+
check if the tables are in the tags
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
pd.DataFrame
|
|
28
|
+
DataFrame with one row per DataPoint and one column per requested key.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
records = []
|
|
32
|
+
ids = []
|
|
33
|
+
stock_ids = []
|
|
34
|
+
for dp in datapoints:
|
|
35
|
+
body = dp.get("body", {})
|
|
36
|
+
# Build one row with only requested keys
|
|
37
|
+
row = {col: body[col] for col in columns}
|
|
38
|
+
# check if the tables are in tags
|
|
39
|
+
if check_tables is not None:
|
|
40
|
+
for table in check_tables:
|
|
41
|
+
row[table] = table in dp["tag"]
|
|
42
|
+
# build the index from datapoint ids
|
|
43
|
+
ids.append(dp["id"])
|
|
44
|
+
records.append(row)
|
|
45
|
+
stock_ids.append(np.atleast_1d(dp["stock"]).tolist())
|
|
46
|
+
|
|
47
|
+
colnames = columns if check_tables is None else columns + check_tables
|
|
48
|
+
return pd.DataFrame.from_records(records, columns=colnames, index=ids), stock_ids
|
timewise/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.0.
|
|
1
|
+
__version__ = "1.0.0a5"
|
timewise/cli.py
CHANGED
|
@@ -6,11 +6,15 @@ import typer
|
|
|
6
6
|
|
|
7
7
|
from .config import TimewiseConfig
|
|
8
8
|
from .plot.diagnostic import make_plot
|
|
9
|
+
from .process.interface import AMPEL_EXISTS
|
|
9
10
|
|
|
10
11
|
from rich.logging import RichHandler
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
ampel_missing_msg = (
|
|
15
|
+
" (download only because AMPEL is not installed)" if not AMPEL_EXISTS else ""
|
|
16
|
+
)
|
|
17
|
+
app = typer.Typer(help="Timewsie CLI" + ampel_missing_msg)
|
|
14
18
|
|
|
15
19
|
config_path_type = Annotated[
|
|
16
20
|
Path, typer.Argument(help="Pipeline config file (YAML/JSON)")
|
|
@@ -53,72 +57,74 @@ def download(
|
|
|
53
57
|
TimewiseConfig.from_yaml(config_path).download.build_downloader().run()
|
|
54
58
|
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
@app.command(help="Run download, process and export")
|
|
93
|
-
def run_chain(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
):
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
60
|
+
# the following commands will only be added if ampel is installed
|
|
61
|
+
if AMPEL_EXISTS:
|
|
62
|
+
|
|
63
|
+
@app.command(help="Prepares the AMPEL job file so AMPEL can be run manually")
|
|
64
|
+
def prepare_ampel(
|
|
65
|
+
config_path: config_path_type,
|
|
66
|
+
):
|
|
67
|
+
cfg = TimewiseConfig.from_yaml(config_path)
|
|
68
|
+
ampel_interface = cfg.build_ampel_interface()
|
|
69
|
+
p = ampel_interface.prepare(config_path)
|
|
70
|
+
typer.echo(f"AMPEL job file: {p}")
|
|
71
|
+
|
|
72
|
+
@app.command(help="Processes the lightcurves using AMPEL")
|
|
73
|
+
def process(
|
|
74
|
+
config_path: config_path_type,
|
|
75
|
+
ampel_config_path: ampel_config_path_type,
|
|
76
|
+
):
|
|
77
|
+
cfg = TimewiseConfig.from_yaml(config_path)
|
|
78
|
+
ampel_interface = cfg.build_ampel_interface()
|
|
79
|
+
ampel_interface.run(config_path, ampel_config_path)
|
|
80
|
+
|
|
81
|
+
@app.command(help="Write stacked lightcurves to disk")
|
|
82
|
+
def export(
|
|
83
|
+
config_path: config_path_type,
|
|
84
|
+
output_directory: Annotated[Path, typer.Argument(help="output directory")],
|
|
85
|
+
indices: Annotated[
|
|
86
|
+
list[int] | None,
|
|
87
|
+
typer.Option(
|
|
88
|
+
"-i", "--indices", help="Indices to export, defaults to all indices"
|
|
89
|
+
),
|
|
90
|
+
] = None,
|
|
91
|
+
):
|
|
92
|
+
TimewiseConfig.from_yaml(config_path).build_ampel_interface().export_many(
|
|
93
|
+
output_directory, indices
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@app.command(help="Run download, process and export")
|
|
97
|
+
def run_chain(
|
|
98
|
+
config_path: config_path_type,
|
|
99
|
+
ampel_config_path: ampel_config_path_type,
|
|
100
|
+
output_directory: Annotated[Path, typer.Argument(help="output directory")],
|
|
101
|
+
indices: Annotated[
|
|
102
|
+
list[int] | None,
|
|
103
|
+
typer.Option(
|
|
104
|
+
"-i", "--indices", help="Indices to export, defaults to all indices"
|
|
105
|
+
),
|
|
106
|
+
] = None,
|
|
107
|
+
):
|
|
108
|
+
download(config_path)
|
|
109
|
+
process(config_path, ampel_config_path)
|
|
110
|
+
export(config_path, output_directory, indices)
|
|
111
|
+
|
|
112
|
+
@app.command(help="Make diagnostic plots")
|
|
113
|
+
def plot(
|
|
114
|
+
config_path: config_path_type,
|
|
115
|
+
indices: Annotated[
|
|
116
|
+
List[int],
|
|
117
|
+
typer.Argument(help="Identifiers of the objects for which to create plots"),
|
|
118
|
+
],
|
|
119
|
+
output_directory: Annotated[Path, typer.Argument(help="Output directory")],
|
|
120
|
+
cutout: Annotated[
|
|
121
|
+
Literal["sdss", "panstarrs"],
|
|
122
|
+
typer.Option("-c", "--cutout", help="Which survey to use for cutouts"),
|
|
123
|
+
] = "panstarrs",
|
|
124
|
+
):
|
|
125
|
+
make_plot(
|
|
126
|
+
config_path,
|
|
127
|
+
indices=indices,
|
|
128
|
+
cutout=cutout,
|
|
129
|
+
output_directory=output_directory,
|
|
130
|
+
)
|
timewise/io/download.py
CHANGED
|
@@ -232,27 +232,25 @@ class Downloader:
|
|
|
232
232
|
backend.save_data(task, payload_table)
|
|
233
233
|
meta["status"] = "COMPLETED"
|
|
234
234
|
meta["completed_at"] = str(datetime.now())
|
|
235
|
-
backend.save_meta(task, meta)
|
|
236
235
|
backend.mark_done(task)
|
|
237
|
-
with self.job_lock:
|
|
238
|
-
self.jobs[task] = meta
|
|
239
236
|
elif status in ("ERROR", "ABORTED"):
|
|
240
237
|
logger.warning(f"failed {task}: {status}")
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
238
|
+
elif not status:
|
|
239
|
+
logger.warning(
|
|
240
|
+
f"No job found under {meta['url']} for {task}! "
|
|
241
|
+
f"Probably took too long before downloading results."
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
meta["status"] = status
|
|
245
|
+
with self.job_lock:
|
|
246
|
+
self.jobs[task] = meta
|
|
247
|
+
backend.save_meta(task, meta)
|
|
250
248
|
|
|
251
249
|
if self.all_chunks_submitted:
|
|
252
250
|
with self.job_lock:
|
|
253
251
|
all_done = (
|
|
254
252
|
all(
|
|
255
|
-
j.get("status") in ("COMPLETED", "ERROR", "ABORTED")
|
|
253
|
+
j.get("status") in ("COMPLETED", "ERROR", "ABORTED", None)
|
|
256
254
|
for j in self.jobs.values()
|
|
257
255
|
)
|
|
258
256
|
if len(self.jobs) > 0
|
timewise/process/interface.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
import logging
|
|
3
3
|
from typing import Iterable, List, cast
|
|
4
|
+
from importlib.util import find_spec
|
|
4
5
|
|
|
5
6
|
import numpy as np
|
|
6
7
|
from numpy import typing as npt
|
|
@@ -8,12 +9,19 @@ import pandas as pd
|
|
|
8
9
|
from pymongo import MongoClient, ASCENDING
|
|
9
10
|
from pymongo.collection import Collection
|
|
10
11
|
from pymongo.database import Database
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
if find_spec("ampel.core"):
|
|
14
|
+
AMPEL_EXISTS = True
|
|
15
|
+
from ampel.cli.JobCommand import JobCommand
|
|
16
|
+
else:
|
|
17
|
+
AMPEL_EXISTS = False
|
|
13
18
|
|
|
14
19
|
|
|
15
20
|
logger = logging.getLogger(__name__)
|
|
16
21
|
|
|
22
|
+
# copy from ampel.types
|
|
23
|
+
StockId = int | bytes | str
|
|
24
|
+
|
|
17
25
|
|
|
18
26
|
class AmpelInterface:
|
|
19
27
|
def __init__(
|
|
@@ -71,6 +79,10 @@ class AmpelInterface:
|
|
|
71
79
|
return self.make_ampel_job_file(cfg_path)
|
|
72
80
|
|
|
73
81
|
def run(self, timewise_cfg_path: Path, ampel_config_path: Path):
|
|
82
|
+
if not AMPEL_EXISTS:
|
|
83
|
+
raise ModuleNotFoundError(
|
|
84
|
+
"You are trying to run ampel but it is not installed!"
|
|
85
|
+
)
|
|
74
86
|
ampel_job_path = self.prepare(timewise_cfg_path)
|
|
75
87
|
cmd = JobCommand()
|
|
76
88
|
parser = cmd.get_parser()
|
|
@@ -123,7 +135,7 @@ class AmpelInterface:
|
|
|
123
135
|
index.append(ic["id"])
|
|
124
136
|
return pd.DataFrame(records, index=index)
|
|
125
137
|
|
|
126
|
-
def extract_selected_datapoint_ids(self, stock_id: StockId) -> List[
|
|
138
|
+
def extract_selected_datapoint_ids(self, stock_id: StockId) -> List[int]:
|
|
127
139
|
d = self.t1.find_one({"stock": stock_id})
|
|
128
140
|
if d is None:
|
|
129
141
|
return []
|