timewise 1.0.0a8__tar.gz → 1.0.0a9__tar.gz
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.
- {timewise-1.0.0a8 → timewise-1.0.0a9}/PKG-INFO +4 -3
- {timewise-1.0.0a8 → timewise-1.0.0a9}/README.md +2 -2
- {timewise-1.0.0a8 → timewise-1.0.0a9}/ampel/timewise/alert/TimewiseAlertSupplier.py +2 -2
- {timewise-1.0.0a8 → timewise-1.0.0a9}/ampel/timewise/alert/load/TimewiseFileLoader.py +11 -1
- {timewise-1.0.0a8 → timewise-1.0.0a9}/ampel/timewise/ingest/TiMongoMuxer.py +126 -10
- {timewise-1.0.0a8 → timewise-1.0.0a9}/pyproject.toml +2 -1
- timewise-1.0.0a9/timewise/__init__.py +1 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/backend/base.py +2 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/backend/filesystem.py +3 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/cli.py +9 -1
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/io/config.py +14 -10
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/io/download.py +36 -15
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/io/stable_tap.py +10 -7
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/plot/sdss.py +1 -3
- timewise-1.0.0a9/timewise/query/__init__.py +11 -0
- timewise-1.0.0a9/timewise/query/by_allwise_cntr_and_position.py +49 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/query/positional.py +0 -1
- timewise-1.0.0a9/timewise/tables/__init__.py +11 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/tables/allwise_p3as_mep.py +3 -1
- timewise-1.0.0a9/timewise/tables/allwise_p3as_psd.py +24 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/tables/neowiser_p1bs_psd.py +3 -1
- timewise-1.0.0a8/timewise/__init__.py +0 -1
- timewise-1.0.0a8/timewise/query/__init__.py +0 -6
- timewise-1.0.0a8/timewise/tables/__init__.py +0 -10
- timewise-1.0.0a8/timewise/util/backoff.py +0 -12
- {timewise-1.0.0a8 → timewise-1.0.0a9}/LICENSE +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/ampel/timewise/ingest/TiCompilerOptions.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/ampel/timewise/ingest/TiDataPointShaper.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/ampel/timewise/ingest/tags.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/ampel/timewise/t1/T1HDBSCAN.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/ampel/timewise/t1/TimewiseFilter.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/ampel/timewise/t2/T2StackVisits.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/ampel/timewise/util/AuxDiagnosticPlotter.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/ampel/timewise/util/pdutil.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/backend/__init__.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/chunking.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/config.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/io/__init__.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/plot/__init__.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/plot/diagnostic.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/plot/lightcurve.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/plot/panstarrs.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/process/__init__.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/process/config.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/process/interface.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/process/keys.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/process/stacking.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/process/template.yml +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/query/base.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/tables/base.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/types.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/util/csv_utils.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/util/error_threading.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/util/path.py +0 -0
- {timewise-1.0.0a8 → timewise-1.0.0a9}/timewise/util/visits.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: timewise
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0a9
|
|
4
4
|
Summary: Download WISE infrared data for many objects and process them with AMPEL
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -27,6 +27,7 @@ Requires-Dist: coveralls (>=3.3.1,<4.0.0) ; extra == "dev"
|
|
|
27
27
|
Requires-Dist: jupyter[jupyter] (>=1.0.0,<2.0.0)
|
|
28
28
|
Requires-Dist: jupyterlab[jupyter] (>=4.0.6,<5.0.0)
|
|
29
29
|
Requires-Dist: matplotlib (>=3.5.3,<4.0.0)
|
|
30
|
+
Requires-Dist: mongomock (>=4.3.0,<5.0.0) ; extra == "dev"
|
|
30
31
|
Requires-Dist: mypy (>=1.18.2,<2.0.0) ; extra == "dev"
|
|
31
32
|
Requires-Dist: myst-parser (>=1,<3) ; extra == "docs"
|
|
32
33
|
Requires-Dist: numpy (>=1.23.2,<2.0.0)
|
|
@@ -77,12 +78,12 @@ to get the MongoDB community edition. </sub>
|
|
|
77
78
|
### If you use timewise only for downloading
|
|
78
79
|
The package can be installed via `pip` (but make sure to install the v1 pre-release):
|
|
79
80
|
```bash
|
|
80
|
-
pip install --pre timewise==1.0.
|
|
81
|
+
pip install --pre timewise==1.0.0a9
|
|
81
82
|
```
|
|
82
83
|
### If you use timewise also for stacking individual exposures
|
|
83
84
|
You must install with the `ampel` extra:
|
|
84
85
|
```bash
|
|
85
|
-
pip install --pre 'timewise[ampel]==1.0.
|
|
86
|
+
pip install --pre 'timewise[ampel]==1.0.0a9'
|
|
86
87
|
```
|
|
87
88
|
To tell AMPEL which modules, aka units, to use, build the corresponding configuration file:
|
|
88
89
|
```bash
|
|
@@ -24,12 +24,12 @@ to get the MongoDB community edition. </sub>
|
|
|
24
24
|
### If you use timewise only for downloading
|
|
25
25
|
The package can be installed via `pip` (but make sure to install the v1 pre-release):
|
|
26
26
|
```bash
|
|
27
|
-
pip install --pre timewise==1.0.
|
|
27
|
+
pip install --pre timewise==1.0.0a9
|
|
28
28
|
```
|
|
29
29
|
### If you use timewise also for stacking individual exposures
|
|
30
30
|
You must install with the `ampel` extra:
|
|
31
31
|
```bash
|
|
32
|
-
pip install --pre 'timewise[ampel]==1.0.
|
|
32
|
+
pip install --pre 'timewise[ampel]==1.0.0a9'
|
|
33
33
|
```
|
|
34
34
|
To tell AMPEL which modules, aka units, to use, build the corresponding configuration file:
|
|
35
35
|
```bash
|
|
@@ -71,8 +71,8 @@ class TimewiseAlertSupplier(BaseAlertSupplier, AmpelABC):
|
|
|
71
71
|
|
|
72
72
|
move = {
|
|
73
73
|
c: c.replace("_ep", "")
|
|
74
|
-
for c in
|
|
75
|
-
if c.replace("_ep", "") in table.columns
|
|
74
|
+
for c in table.columns
|
|
75
|
+
if (c.replace("_ep", "") in table.columns) and (c.endswith("_ep"))
|
|
76
76
|
}
|
|
77
77
|
if move:
|
|
78
78
|
# In this case, the columns already exists because the neowise data is present
|
|
@@ -32,6 +32,9 @@ class TimewiseFileLoader(AbsAlertLoader[Dict], AmpelABC):
|
|
|
32
32
|
|
|
33
33
|
chunks: list[int] | None = None
|
|
34
34
|
|
|
35
|
+
# optionally skip files that are missing
|
|
36
|
+
skip_missing_files: bool = False
|
|
37
|
+
|
|
35
38
|
def __init__(self, **kwargs) -> None:
|
|
36
39
|
super().__init__(**kwargs)
|
|
37
40
|
|
|
@@ -81,7 +84,14 @@ class TimewiseFileLoader(AbsAlertLoader[Dict], AmpelABC):
|
|
|
81
84
|
data = []
|
|
82
85
|
for task in tasks:
|
|
83
86
|
self.logger.debug(f"reading {task}")
|
|
84
|
-
|
|
87
|
+
try:
|
|
88
|
+
idata = backend.load_data(task)
|
|
89
|
+
except FileNotFoundError as e:
|
|
90
|
+
if self.skip_missing_files:
|
|
91
|
+
self.logger.warn(f"file for task {task} not found, skipping...")
|
|
92
|
+
continue
|
|
93
|
+
else:
|
|
94
|
+
raise e
|
|
85
95
|
|
|
86
96
|
# add table name
|
|
87
97
|
idata["table_name"] = (
|
|
@@ -8,13 +8,20 @@
|
|
|
8
8
|
|
|
9
9
|
from bisect import bisect_right
|
|
10
10
|
from contextlib import suppress
|
|
11
|
-
from typing import Any
|
|
12
|
-
|
|
11
|
+
from typing import Any, Sequence
|
|
13
12
|
|
|
14
13
|
from ampel.abstract.AbsT0Muxer import AbsT0Muxer
|
|
15
14
|
from ampel.content.DataPoint import DataPoint
|
|
16
|
-
from ampel.
|
|
15
|
+
from ampel.model.operator.AllOf import AllOf
|
|
16
|
+
from ampel.model.operator.AnyOf import AnyOf
|
|
17
|
+
from ampel.types import ChannelId, DataPointId, StockId
|
|
17
18
|
from ampel.util.mappings import unflatten_dict
|
|
19
|
+
from astropy.table import Table
|
|
20
|
+
from pydantic import TypeAdapter
|
|
21
|
+
from timewise.io.stable_tap import StableTAPService
|
|
22
|
+
from timewise.query import QueryType
|
|
23
|
+
from timewise.tables.allwise_p3as_mep import allwise_p3as_mep
|
|
24
|
+
from timewise.types import TYPE_MAP
|
|
18
25
|
|
|
19
26
|
|
|
20
27
|
class ConcurrentUpdateError(Exception):
|
|
@@ -51,8 +58,13 @@ class TiMongoMuxer(AbsT0Muxer):
|
|
|
51
58
|
"body.dec": 1,
|
|
52
59
|
}
|
|
53
60
|
|
|
61
|
+
channel: None | ChannelId | AnyOf[ChannelId] | AllOf[ChannelId] = None
|
|
62
|
+
|
|
54
63
|
unique_key: list[str] = ["mjd", "ra", "dec"]
|
|
55
64
|
|
|
65
|
+
# URL of tap service for query of AllWISE Source Table
|
|
66
|
+
tap_service_url: str = "https://irsa.ipac.caltech.edu/TAP"
|
|
67
|
+
|
|
56
68
|
def __init__(self, **kwargs) -> None:
|
|
57
69
|
super().__init__(**kwargs)
|
|
58
70
|
|
|
@@ -60,6 +72,11 @@ class TiMongoMuxer(AbsT0Muxer):
|
|
|
60
72
|
self._photo_col = self.context.db.get_collection("t0")
|
|
61
73
|
self._projection_spec = unflatten_dict(self.projection)
|
|
62
74
|
|
|
75
|
+
self._tap_service = StableTAPService(self.tap_service_url)
|
|
76
|
+
|
|
77
|
+
self._allwise_source_cntr: list[str] = []
|
|
78
|
+
self._not_allwise_source_cntr: list[str] = []
|
|
79
|
+
|
|
63
80
|
def process(
|
|
64
81
|
self, dps: list[DataPoint], stock_id: None | StockId = None
|
|
65
82
|
) -> tuple[None | list[DataPoint], None | list[DataPoint]]:
|
|
@@ -81,7 +98,76 @@ class TiMongoMuxer(AbsT0Muxer):
|
|
|
81
98
|
|
|
82
99
|
# NB: this 1-liner is a separate method to provide a patch point for race condition testing
|
|
83
100
|
def _get_dps(self, stock_id: None | StockId) -> list[DataPoint]:
|
|
84
|
-
|
|
101
|
+
if self.channel is not None:
|
|
102
|
+
if isinstance(self.channel, ChannelId):
|
|
103
|
+
channel_query: (
|
|
104
|
+
ChannelId | dict[str, Sequence[ChannelId | AllOf[ChannelId]]]
|
|
105
|
+
) = self.channel
|
|
106
|
+
elif isinstance(self.channel, AnyOf):
|
|
107
|
+
channel_query = {"$in": self.channel.any_of}
|
|
108
|
+
elif isinstance(self.channel, AllOf):
|
|
109
|
+
channel_query = {"$all": self.channel.all_of}
|
|
110
|
+
else:
|
|
111
|
+
# should not happen
|
|
112
|
+
raise TypeError()
|
|
113
|
+
_channel = {"channel": channel_query}
|
|
114
|
+
else:
|
|
115
|
+
_channel = {}
|
|
116
|
+
query = {"stock": stock_id, **_channel}
|
|
117
|
+
return list(self._photo_col.find(query, self.projection))
|
|
118
|
+
|
|
119
|
+
def _check_cntrs(self, dps: Sequence[DataPoint]) -> None:
|
|
120
|
+
# assemble query
|
|
121
|
+
query_config = {
|
|
122
|
+
"type": "by_allwise_cntr_and_position",
|
|
123
|
+
"radius_arcsec": 10,
|
|
124
|
+
"columns": ["cntr"],
|
|
125
|
+
"constraints": [],
|
|
126
|
+
"table": {"name": "allwise_p3as_psd"},
|
|
127
|
+
}
|
|
128
|
+
query: QueryType = TypeAdapter(QueryType).validate_python(query_config)
|
|
129
|
+
|
|
130
|
+
# load datapoints into astropy table
|
|
131
|
+
upload = Table([dp["body"] for dp in dps])
|
|
132
|
+
upload["allwise_cntr"] = upload[allwise_p3as_mep.allwise_cntr_column]
|
|
133
|
+
upload[query.original_id_key] = [dp["id"] for dp in dps]
|
|
134
|
+
for key, dtype in query.input_columns.items():
|
|
135
|
+
upload[key] = upload[key].astype(TYPE_MAP[dtype])
|
|
136
|
+
for key in upload.colnames:
|
|
137
|
+
if key not in query.input_columns:
|
|
138
|
+
upload.remove_column(key)
|
|
139
|
+
|
|
140
|
+
# run query
|
|
141
|
+
self.logger.info("Querying AllWISE Source Table for MEP CNTRs ...")
|
|
142
|
+
res = self._tap_service.run_sync(
|
|
143
|
+
query.adql, uploads={query.upload_name: upload}
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# update internal state
|
|
147
|
+
res_cntr = res.to_table()["cntr"].astype(str)
|
|
148
|
+
self._allwise_source_cntr.extend(list(res_cntr))
|
|
149
|
+
self._not_allwise_source_cntr.extend(
|
|
150
|
+
list(set(upload["allwise_cntr"].astype(str)) - set(res_cntr))
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def _check_mep_allwise_sources(self, dps: Sequence[DataPoint]) -> list[DataPointId]:
|
|
154
|
+
dps_with_unchecked_cntr = [
|
|
155
|
+
dp
|
|
156
|
+
for dp in dps
|
|
157
|
+
if str(dp["body"][allwise_p3as_mep.allwise_cntr_column])
|
|
158
|
+
not in self._allwise_source_cntr + self._not_allwise_source_cntr
|
|
159
|
+
]
|
|
160
|
+
if len(dps_with_unchecked_cntr) > 0:
|
|
161
|
+
self._check_cntrs(dps_with_unchecked_cntr)
|
|
162
|
+
|
|
163
|
+
# compile list of invalid datapoint ids
|
|
164
|
+
invalid_dp_ids = []
|
|
165
|
+
for dp in dps:
|
|
166
|
+
cntr = str(dp["body"][allwise_p3as_mep.allwise_cntr_column])
|
|
167
|
+
if cntr in self._not_allwise_source_cntr:
|
|
168
|
+
invalid_dp_ids.append(dp["id"])
|
|
169
|
+
|
|
170
|
+
return invalid_dp_ids
|
|
85
171
|
|
|
86
172
|
def _process(
|
|
87
173
|
self, dps: list[DataPoint], stock_id: None | StockId = None
|
|
@@ -128,7 +214,10 @@ class TiMongoMuxer(AbsT0Muxer):
|
|
|
128
214
|
else:
|
|
129
215
|
unique_dps_ids[key] = [dp["id"]]
|
|
130
216
|
|
|
131
|
-
#
|
|
217
|
+
# Part 2: Check that there are no duplicates and handle redundant AllWISE MEP data
|
|
218
|
+
##################################################################################
|
|
219
|
+
|
|
220
|
+
invalid_dp_ids = []
|
|
132
221
|
for key, simultaneous_dps in unique_dps_ids.items():
|
|
133
222
|
dps_db_wrong = [dp for dp in dps_db if dp["id"] in simultaneous_dps]
|
|
134
223
|
dps_wrong = [dp for dp in dps if dp["id"] in simultaneous_dps]
|
|
@@ -136,20 +225,47 @@ class TiMongoMuxer(AbsT0Muxer):
|
|
|
136
225
|
f"stockID {str(stock_id)}: Duplicate photopoints at {key}!\nDPS from DB:"
|
|
137
226
|
f"\n{dps_db_wrong}\nNew DPS:\n{dps_wrong}"
|
|
138
227
|
)
|
|
139
|
-
assert len(simultaneous_dps) == 1, msg
|
|
140
228
|
|
|
141
|
-
|
|
142
|
-
|
|
229
|
+
all_wrong_dps = dps_db_wrong + dps_wrong
|
|
230
|
+
if len(simultaneous_dps) > 1:
|
|
231
|
+
# if these datapoints come from the AllWISE MEP database, downloaded by timewise
|
|
232
|
+
# there can be duplicates. Only the AllWISE CNTR can tell us which datapoints
|
|
233
|
+
# should be used: the CNTR that appears in the AllWISE source catalog.
|
|
234
|
+
if all(
|
|
235
|
+
[
|
|
236
|
+
("TIMEWISE" in dp["tag"]) and ("allwise_p3as_mep" in dp["tag"])
|
|
237
|
+
for dp in all_wrong_dps
|
|
238
|
+
]
|
|
239
|
+
):
|
|
240
|
+
self.logger.info(
|
|
241
|
+
f"{len(all_wrong_dps)} duplicate MEP datapoints found. Checking ..."
|
|
242
|
+
)
|
|
243
|
+
i_invalid_dp_ids = self._check_mep_allwise_sources(
|
|
244
|
+
dps_db_wrong + dps_wrong
|
|
245
|
+
)
|
|
246
|
+
self.logger.info(
|
|
247
|
+
f"Found {len(i_invalid_dp_ids)} invalid MEP datapoints."
|
|
248
|
+
)
|
|
249
|
+
invalid_dp_ids.extend(i_invalid_dp_ids)
|
|
250
|
+
|
|
251
|
+
else:
|
|
252
|
+
raise RuntimeError(msg)
|
|
253
|
+
|
|
254
|
+
# Part 3: Compile final lists of datapoints to insert and combine
|
|
255
|
+
#################################################################
|
|
143
256
|
|
|
144
257
|
# Difference between candids from the alert and candids present in DB
|
|
145
|
-
ids_dps_to_insert = ids_dps_alert - ids_dps_db
|
|
258
|
+
ids_dps_to_insert = ids_dps_alert - ids_dps_db - set(invalid_dp_ids)
|
|
146
259
|
dps_to_insert = [dp for dp in dps if dp["id"] in ids_dps_to_insert]
|
|
147
260
|
dps_to_combine = [
|
|
148
|
-
dp
|
|
261
|
+
dp
|
|
262
|
+
for dp in dps + dps_db
|
|
263
|
+
if dp["id"] in ((ids_dps_alert | ids_dps_db) - set(invalid_dp_ids))
|
|
149
264
|
]
|
|
150
265
|
self.logger.debug(
|
|
151
266
|
f"Got {len(ids_dps_alert)} datapoints from alerts, "
|
|
152
267
|
f"found {len(dps_db)} in DB, "
|
|
268
|
+
f"{len(invalid_dp_ids)} invalid datapoints, "
|
|
153
269
|
f"inserting {len(dps_to_insert)} datapoints, "
|
|
154
270
|
f"combining {len(dps_to_combine)} datapoints"
|
|
155
271
|
)
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "timewise"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.0a9"
|
|
8
8
|
description = "Download WISE infrared data for many objects and process them with AMPEL"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Jannis Necker", email = "jannis.necker@gmail.com" },
|
|
@@ -54,6 +54,7 @@ dev = [
|
|
|
54
54
|
"scipy-stubs (>=1.16.2.0,<2.0.0.0)",
|
|
55
55
|
"types-pyyaml (>=6.0.12.20250915,<7.0.0.0)",
|
|
56
56
|
"types-requests (>=2.32.4.20250913,<3.0.0.0)",
|
|
57
|
+
"mongomock (>=4.3.0,<5.0.0)",
|
|
57
58
|
]
|
|
58
59
|
docs = [
|
|
59
60
|
"myst-parser>=1,<3",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0a9"
|
|
@@ -20,6 +20,8 @@ class Backend(abc.ABC, BaseModel):
|
|
|
20
20
|
def save_meta(self, task: TaskID, meta: dict[str, Any]) -> None: ...
|
|
21
21
|
@abc.abstractmethod
|
|
22
22
|
def load_meta(self, task: TaskID) -> dict[str, Any] | None: ...
|
|
23
|
+
@abc.abstractmethod
|
|
24
|
+
def drop_meta(self, task: TaskID) -> None: ...
|
|
23
25
|
|
|
24
26
|
# --- Markers ---
|
|
25
27
|
@abc.abstractmethod
|
|
@@ -52,6 +52,9 @@ class FileSystemBackend(Backend):
|
|
|
52
52
|
def meta_exists(self, task: TaskID) -> bool:
|
|
53
53
|
return self._meta_path(task).exists()
|
|
54
54
|
|
|
55
|
+
def drop_meta(self, task: TaskID) -> None:
|
|
56
|
+
self._meta_path(task).unlink()
|
|
57
|
+
|
|
55
58
|
# ----------------------------
|
|
56
59
|
# Markers
|
|
57
60
|
# ----------------------------
|
|
@@ -53,8 +53,16 @@ def main(
|
|
|
53
53
|
@app.command(help="Download WISE photometry from IRSA")
|
|
54
54
|
def download(
|
|
55
55
|
config_path: config_path_type,
|
|
56
|
+
resubmit_failed: Annotated[
|
|
57
|
+
bool,
|
|
58
|
+
typer.Option(
|
|
59
|
+
help="Re-submit jobs when failed due to connection issues",
|
|
60
|
+
),
|
|
61
|
+
] = False,
|
|
56
62
|
):
|
|
57
|
-
TimewiseConfig.from_yaml(config_path).download.build_downloader(
|
|
63
|
+
TimewiseConfig.from_yaml(config_path).download.build_downloader(
|
|
64
|
+
resubmit_failed=resubmit_failed
|
|
65
|
+
).run()
|
|
58
66
|
|
|
59
67
|
|
|
60
68
|
# the following commands will only be added if ampel is installed
|
|
@@ -17,6 +17,7 @@ class DownloadConfig(BaseModel):
|
|
|
17
17
|
poll_interval: float = 10.0
|
|
18
18
|
queries: List[QueryType] = Field(..., description="One or more queries per chunk")
|
|
19
19
|
backend: BackendType = Field(..., discriminator="type")
|
|
20
|
+
resubmit_failed: bool = False
|
|
20
21
|
|
|
21
22
|
service_url: str = "https://irsa.ipac.caltech.edu/TAP"
|
|
22
23
|
|
|
@@ -57,13 +58,16 @@ class DownloadConfig(BaseModel):
|
|
|
57
58
|
|
|
58
59
|
return self
|
|
59
60
|
|
|
60
|
-
def build_downloader(self) -> Downloader:
|
|
61
|
-
|
|
62
|
-
service_url
|
|
63
|
-
input_csv
|
|
64
|
-
chunk_size
|
|
65
|
-
backend
|
|
66
|
-
queries
|
|
67
|
-
max_concurrent_jobs
|
|
68
|
-
poll_interval
|
|
69
|
-
|
|
61
|
+
def build_downloader(self, **overwrite) -> Downloader:
|
|
62
|
+
default = {
|
|
63
|
+
"service_url": self.service_url,
|
|
64
|
+
"input_csv": self.expanded_input_csv,
|
|
65
|
+
"chunk_size": self.chunk_size,
|
|
66
|
+
"backend": self.backend,
|
|
67
|
+
"queries": self.queries,
|
|
68
|
+
"max_concurrent_jobs": self.max_concurrent_jobs,
|
|
69
|
+
"poll_interval": self.poll_interval,
|
|
70
|
+
"resubmit_failed": self.resubmit_failed,
|
|
71
|
+
}
|
|
72
|
+
default.update(overwrite)
|
|
73
|
+
return Downloader(**default) # type: ignore
|
|
@@ -1,25 +1,22 @@
|
|
|
1
|
-
import time
|
|
2
|
-
import threading
|
|
3
1
|
import logging
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from datetime import datetime, timedelta
|
|
6
5
|
from itertools import product
|
|
7
6
|
from pathlib import Path
|
|
8
|
-
from
|
|
7
|
+
from queue import Empty
|
|
8
|
+
from typing import Dict, Iterator
|
|
9
9
|
|
|
10
|
-
import pandas as pd
|
|
11
10
|
import numpy as np
|
|
12
11
|
from astropy.table import Table
|
|
13
12
|
from pyvo.utils.http import create_session
|
|
14
13
|
|
|
15
|
-
from .stable_tap import StableTAPService
|
|
16
14
|
from ..backend import BackendType
|
|
17
|
-
from ..
|
|
15
|
+
from ..chunking import Chunk, Chunker
|
|
18
16
|
from ..query import QueryType
|
|
19
|
-
from ..
|
|
17
|
+
from ..types import TYPE_MAP, TAPJobMeta, TaskID
|
|
20
18
|
from ..util.error_threading import ErrorQueue, ExceptionSafeThread
|
|
21
|
-
from
|
|
22
|
-
|
|
19
|
+
from .stable_tap import StableTAPService
|
|
23
20
|
|
|
24
21
|
logger = logging.getLogger(__name__)
|
|
25
22
|
|
|
@@ -34,6 +31,7 @@ class Downloader:
|
|
|
34
31
|
queries: list[QueryType],
|
|
35
32
|
max_concurrent_jobs: int,
|
|
36
33
|
poll_interval: float,
|
|
34
|
+
resubmit_failed: bool,
|
|
37
35
|
):
|
|
38
36
|
self.backend = backend
|
|
39
37
|
self.queries = queries
|
|
@@ -67,6 +65,7 @@ class Downloader:
|
|
|
67
65
|
self.service: StableTAPService = StableTAPService(
|
|
68
66
|
service_url, session=self.session
|
|
69
67
|
)
|
|
68
|
+
self.resubmit_failed = resubmit_failed
|
|
70
69
|
|
|
71
70
|
self.chunker = Chunker(input_csv=input_csv, chunk_size=chunk_size)
|
|
72
71
|
|
|
@@ -74,7 +73,7 @@ class Downloader:
|
|
|
74
73
|
# helpers
|
|
75
74
|
# ----------------------------
|
|
76
75
|
@staticmethod
|
|
77
|
-
def get_task_id(chunk: Chunk, query:
|
|
76
|
+
def get_task_id(chunk: Chunk, query: QueryType) -> TaskID:
|
|
78
77
|
return TaskID(
|
|
79
78
|
namespace="download", key=f"chunk{chunk.chunk_id:04d}_{query.hash}"
|
|
80
79
|
)
|
|
@@ -107,7 +106,7 @@ class Downloader:
|
|
|
107
106
|
# TAP submission and download
|
|
108
107
|
# ----------------------------
|
|
109
108
|
|
|
110
|
-
def submit_tap_job(self, query:
|
|
109
|
+
def submit_tap_job(self, query: QueryType, chunk: Chunk) -> TAPJobMeta:
|
|
111
110
|
adql = query.adql
|
|
112
111
|
chunk_df = chunk.data
|
|
113
112
|
|
|
@@ -133,7 +132,6 @@ class Downloader:
|
|
|
133
132
|
logger.debug(f"uploading {len(upload)} objects.")
|
|
134
133
|
job = self.service.submit_job(adql, uploads={query.upload_name: upload})
|
|
135
134
|
job.run()
|
|
136
|
-
logger.debug(job.url)
|
|
137
135
|
|
|
138
136
|
return TAPJobMeta(
|
|
139
137
|
url=job.url,
|
|
@@ -163,7 +161,7 @@ class Downloader:
|
|
|
163
161
|
def _submission_worker(self):
|
|
164
162
|
while not self.stop_event.is_set():
|
|
165
163
|
try:
|
|
166
|
-
chunk, query = self.submit_queue.get(timeout=1.0) # type: Chunk,
|
|
164
|
+
chunk, query = self.submit_queue.get(timeout=1.0) # type: Chunk, QueryType
|
|
167
165
|
except Empty:
|
|
168
166
|
if self.all_chunks_queued:
|
|
169
167
|
self.all_chunks_submitted = True
|
|
@@ -194,6 +192,26 @@ class Downloader:
|
|
|
194
192
|
# ----------------------------
|
|
195
193
|
# Polling thread
|
|
196
194
|
# ----------------------------
|
|
195
|
+
|
|
196
|
+
def resubmit(self, resubmit_task: TaskID):
|
|
197
|
+
logger.info(f"resubmitting {resubmit_task}")
|
|
198
|
+
submit = None
|
|
199
|
+
for chunk, q in product(self.chunker, self.queries):
|
|
200
|
+
task = self.get_task_id(chunk, q)
|
|
201
|
+
if task == resubmit_task:
|
|
202
|
+
submit = chunk, q
|
|
203
|
+
break
|
|
204
|
+
if submit is None:
|
|
205
|
+
raise RuntimeError(f"resubmit task {resubmit_task} not found!")
|
|
206
|
+
|
|
207
|
+
# remove current info, so the job won't be re-submitted over and over again
|
|
208
|
+
self.backend.drop_meta(resubmit_task)
|
|
209
|
+
with self.job_lock:
|
|
210
|
+
self.jobs.pop(resubmit_task)
|
|
211
|
+
|
|
212
|
+
# put task back in resubmit queue
|
|
213
|
+
self.submit_queue.put(submit)
|
|
214
|
+
|
|
197
215
|
def _polling_worker(self):
|
|
198
216
|
logger.debug("starting polling worker")
|
|
199
217
|
backend = self.backend
|
|
@@ -225,6 +243,9 @@ class Downloader:
|
|
|
225
243
|
f"No job found under {meta['url']} for {task}! "
|
|
226
244
|
f"Probably took too long before downloading results."
|
|
227
245
|
)
|
|
246
|
+
if self.resubmit_failed:
|
|
247
|
+
self.resubmit(task)
|
|
248
|
+
continue
|
|
228
249
|
|
|
229
250
|
meta["status"] = status
|
|
230
251
|
with self.job_lock:
|
|
@@ -5,9 +5,6 @@ from xml.etree import ElementTree
|
|
|
5
5
|
|
|
6
6
|
import requests
|
|
7
7
|
|
|
8
|
-
from timewise.util.backoff import backoff_hndlr
|
|
9
|
-
|
|
10
|
-
|
|
11
8
|
logger = logging.getLogger(__name__)
|
|
12
9
|
|
|
13
10
|
|
|
@@ -26,7 +23,6 @@ class StableAsyncTAPJob(vo.dal.AsyncTAPJob):
|
|
|
26
23
|
backoff.expo,
|
|
27
24
|
requests.exceptions.HTTPError,
|
|
28
25
|
max_tries=5,
|
|
29
|
-
on_backoff=backoff_hndlr,
|
|
30
26
|
)
|
|
31
27
|
def create(
|
|
32
28
|
cls,
|
|
@@ -92,7 +88,6 @@ class StableAsyncTAPJob(vo.dal.AsyncTAPJob):
|
|
|
92
88
|
backoff.expo,
|
|
93
89
|
(vo.dal.DALServiceError, AttributeError),
|
|
94
90
|
max_tries=50,
|
|
95
|
-
on_backoff=backoff_hndlr,
|
|
96
91
|
)
|
|
97
92
|
def phase(self):
|
|
98
93
|
return super(StableAsyncTAPJob, self).phase
|
|
@@ -101,7 +96,6 @@ class StableAsyncTAPJob(vo.dal.AsyncTAPJob):
|
|
|
101
96
|
backoff.expo,
|
|
102
97
|
vo.dal.DALServiceError,
|
|
103
98
|
max_tries=50,
|
|
104
|
-
on_backoff=backoff_hndlr,
|
|
105
99
|
)
|
|
106
100
|
def _update(self, *args, **kwargs):
|
|
107
101
|
return super(StableAsyncTAPJob, self)._update(*args, **kwargs)
|
|
@@ -116,7 +110,6 @@ class StableTAPService(vo.dal.TAPService):
|
|
|
116
110
|
backoff.expo,
|
|
117
111
|
(vo.dal.DALServiceError, AttributeError, AssertionError),
|
|
118
112
|
max_tries=5,
|
|
119
|
-
on_backoff=backoff_hndlr,
|
|
120
113
|
)
|
|
121
114
|
def submit_job(
|
|
122
115
|
self, query, *, language="ADQL", maxrec=None, uploads=None, **keywords
|
|
@@ -136,3 +129,13 @@ class StableTAPService(vo.dal.TAPService):
|
|
|
136
129
|
|
|
137
130
|
def get_job_from_url(self, url):
|
|
138
131
|
return StableAsyncTAPJob(url, session=self._session)
|
|
132
|
+
|
|
133
|
+
@backoff.on_exception(
|
|
134
|
+
backoff.expo,
|
|
135
|
+
(vo.dal.DALServiceError, vo.dal.DALFormatError),
|
|
136
|
+
max_tries=5,
|
|
137
|
+
)
|
|
138
|
+
def run_sync(
|
|
139
|
+
self, query, *, language="ADQL", maxrec=None, uploads=None,
|
|
140
|
+
**keywords):
|
|
141
|
+
return super().run_sync(query, language=language, maxrec=maxrec, uploads=uploads, **keywords)
|
|
@@ -5,8 +5,6 @@ import logging
|
|
|
5
5
|
import matplotlib.pyplot as plt
|
|
6
6
|
import backoff
|
|
7
7
|
|
|
8
|
-
from ..util.backoff import backoff_hndlr
|
|
9
|
-
|
|
10
8
|
|
|
11
9
|
logger = logging.getLogger(__name__)
|
|
12
10
|
|
|
@@ -34,7 +32,7 @@ def login_to_sciserver():
|
|
|
34
32
|
|
|
35
33
|
|
|
36
34
|
@backoff.on_exception(
|
|
37
|
-
backoff.expo, requests.RequestException, max_tries=50
|
|
35
|
+
backoff.expo, requests.RequestException, max_tries=50
|
|
38
36
|
)
|
|
39
37
|
def get_cutout(*args, **kwargs):
|
|
40
38
|
login_to_sciserver()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from typing import Annotated, TypeAlias, Union
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from .by_allwise_cntr_and_position import AllWISECntrQuery
|
|
6
|
+
from .positional import PositionalQuery
|
|
7
|
+
|
|
8
|
+
# Discriminated union of all query types
|
|
9
|
+
QueryType: TypeAlias = Annotated[
|
|
10
|
+
Union[PositionalQuery, AllWISECntrQuery], Field(discriminator="type")
|
|
11
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Dict, Literal
|
|
3
|
+
|
|
4
|
+
from .base import Query
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AllWISECntrQuery(Query):
|
|
10
|
+
type: Literal["by_allwise_cntr_and_position"] = "by_allwise_cntr_and_position"
|
|
11
|
+
radius_arcsec: float
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def input_columns(self) -> Dict[str, str]:
|
|
15
|
+
return {
|
|
16
|
+
"allwise_cntr": "int",
|
|
17
|
+
"ra": "float",
|
|
18
|
+
"dec": "float",
|
|
19
|
+
self.original_id_key: "int",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
def build(self) -> str:
|
|
23
|
+
logger.debug(f"constructing query by AllWISE cntr for {self.table.name}")
|
|
24
|
+
|
|
25
|
+
q = "SELECT \n\t"
|
|
26
|
+
for k in self.columns:
|
|
27
|
+
q += f"{self.table.name}.{k}, "
|
|
28
|
+
q += f"\n\tmine.{self.original_id_key} \n"
|
|
29
|
+
q += f"FROM\n\tTAP_UPLOAD.{self.upload_name} AS mine \n"
|
|
30
|
+
q += f"RIGHT JOIN\n\t{self.table.name} \n"
|
|
31
|
+
q += "WHERE \n"
|
|
32
|
+
q += (
|
|
33
|
+
f"\tCONTAINS(POINT('J2000',{self.table.name}.{self.table.ra_column},{self.table.name}.{self.table.dec_column}),"
|
|
34
|
+
f"CIRCLE('J2000',mine.ra,mine.dec,{self.radius_arcsec / 3600:.18f}))=1 "
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
constraints = self.constraints + [
|
|
38
|
+
f"{self.table.allwise_cntr_column} = {self.upload_name}.allwise_cntr"
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
if len(constraints) > 0:
|
|
42
|
+
q += " AND (\n"
|
|
43
|
+
for c in constraints:
|
|
44
|
+
q += f"\t{self.table.name}.{c} AND \n"
|
|
45
|
+
q = q.strip(" AND \n")
|
|
46
|
+
q += "\t)"
|
|
47
|
+
|
|
48
|
+
logger.debug(f"\n{q}")
|
|
49
|
+
return q
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from typing import Annotated, Union
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from .allwise_p3as_mep import allwise_p3as_mep
|
|
6
|
+
from .allwise_p3as_psd import allwise_p3as_psd
|
|
7
|
+
from .neowiser_p1bs_psd import neowiser_p1bs_psd
|
|
8
|
+
|
|
9
|
+
TableType = Annotated[
|
|
10
|
+
Union[allwise_p3as_mep, neowiser_p1bs_psd, allwise_p3as_psd], Field(discriminator="name")
|
|
11
|
+
]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import ClassVar, Dict, Literal, Type
|
|
2
|
+
|
|
2
3
|
from .base import TableConfig
|
|
3
4
|
|
|
4
5
|
|
|
@@ -20,3 +21,4 @@ class allwise_p3as_mep(TableConfig):
|
|
|
20
21
|
}
|
|
21
22
|
ra_column: ClassVar[str] = "ra"
|
|
22
23
|
dec_column: ClassVar[str] = "dec"
|
|
24
|
+
allwise_cntr_column: ClassVar[str] = "cntr_mf"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import ClassVar, Dict, Literal, Type
|
|
2
|
+
|
|
3
|
+
from .base import TableConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class allwise_p3as_psd(TableConfig):
|
|
7
|
+
name: Literal["allwise_p3as_psd"] = "allwise_p3as_psd"
|
|
8
|
+
columns_dtypes: ClassVar[Dict[str, Type]] = {
|
|
9
|
+
"ra": float,
|
|
10
|
+
"dec": float,
|
|
11
|
+
"mjd": float,
|
|
12
|
+
"cntr": str,
|
|
13
|
+
"w1mpro": float,
|
|
14
|
+
"w1sigmpro": float,
|
|
15
|
+
"w2mpro": float,
|
|
16
|
+
"w2sigmpro": float,
|
|
17
|
+
"w1flux": float,
|
|
18
|
+
"w1sigflux": float,
|
|
19
|
+
"w2flux": float,
|
|
20
|
+
"w2sigflux": float,
|
|
21
|
+
}
|
|
22
|
+
ra_column: ClassVar[str] = "ra"
|
|
23
|
+
dec_column: ClassVar[str] = "dec"
|
|
24
|
+
allwise_cntr_column: ClassVar[str] = "cntr"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import ClassVar, Dict, Literal, Type
|
|
2
|
+
|
|
2
3
|
from .base import TableConfig
|
|
3
4
|
|
|
4
5
|
|
|
@@ -20,3 +21,4 @@ class neowiser_p1bs_psd(TableConfig):
|
|
|
20
21
|
}
|
|
21
22
|
ra_column: ClassVar[str] = "ra"
|
|
22
23
|
dec_column: ClassVar[str] = "dec"
|
|
24
|
+
allwise_cntr_column: ClassVar[str] = "allwise_cntr"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.0.0a8"
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
from pydantic import Field
|
|
2
|
-
from typing import Union, Annotated
|
|
3
|
-
|
|
4
|
-
from .allwise_p3as_mep import allwise_p3as_mep
|
|
5
|
-
from .neowiser_p1bs_psd import neowiser_p1bs_psd
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
TableType = Annotated[
|
|
9
|
-
Union[allwise_p3as_mep, neowiser_p1bs_psd], Field(discriminator="name")
|
|
10
|
-
]
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
logger = logging.getLogger(__name__)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def backoff_hndlr(details):
|
|
8
|
-
logger.info(
|
|
9
|
-
"Backing off {wait:0.1f} seconds after {tries} tries "
|
|
10
|
-
"calling function {target} with args {args} and kwargs "
|
|
11
|
-
"{kwargs}".format(**details)
|
|
12
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|