timewise 1.0.0a2__py3-none-any.whl → 1.0.0a6__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.
@@ -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
@@ -0,0 +1,10 @@
1
+ unit:
2
+ - ampel.timewise.alert.load.TimewiseFileLoader
3
+ - ampel.timewise.alert.TimewiseAlertSupplier
4
+ - ampel.timewise.ingest.TiDataPointShaper
5
+ - ampel.timewise.ingest.TiMongoMuxer
6
+ - ampel.timewise.ingest.TiCompilerOptions
7
+ - ampel.timewise.t2.T2StackVisits
8
+ - ampel.timewise.t1.T1HDBSCAN
9
+ - ampel.timewise.util.AuxDiagnosticPlotter
10
+ - ampel.timewise.t1.TimewiseFilter
timewise/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.0.0a2"
1
+ __version__ = "1.0.0a6"
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
- app = typer.Typer(help="Timewsie CLI")
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
- @app.command(help="Prepares the AMPEL job file so AMPEL can be run manually")
57
- def prepare_ampel(
58
- config_path: config_path_type,
59
- ):
60
- cfg = TimewiseConfig.from_yaml(config_path)
61
- ampel_interface = cfg.build_ampel_interface()
62
- p = ampel_interface.prepare(config_path)
63
- typer.echo(f"AMPEL job file: {p}")
64
-
65
-
66
- @app.command(help="Processes the lightcurves using AMPEL")
67
- def process(
68
- config_path: config_path_type,
69
- ampel_config_path: ampel_config_path_type,
70
- ):
71
- cfg = TimewiseConfig.from_yaml(config_path)
72
- ampel_interface = cfg.build_ampel_interface()
73
- ampel_interface.run(config_path, ampel_config_path)
74
-
75
-
76
- @app.command(help="Write stacked lightcurves to disk")
77
- def export(
78
- config_path: config_path_type,
79
- output_directory: Annotated[Path, typer.Argument(help="output directory")],
80
- indices: Annotated[
81
- list[int] | None,
82
- typer.Option(
83
- "-i", "--indices", help="Indices to export, defaults to all indices"
84
- ),
85
- ] = None,
86
- ):
87
- TimewiseConfig.from_yaml(config_path).build_ampel_interface().export_many(
88
- output_directory, indices
89
- )
90
-
91
-
92
- @app.command(help="Run download, process and export")
93
- def run_chain(
94
- config_path: config_path_type,
95
- ampel_config_path: ampel_config_path_type,
96
- output_directory: Annotated[Path, typer.Argument(help="output directory")],
97
- indices: Annotated[
98
- list[int] | None,
99
- typer.Option(
100
- "-i", "--indices", help="Indices to export, defaults to all indices"
101
- ),
102
- ] = None,
103
- ):
104
- download(config_path)
105
- process(config_path, ampel_config_path)
106
- export(config_path, output_directory, indices)
107
-
108
-
109
- @app.command(help="Make diagnostic plots")
110
- def plot(
111
- config_path: config_path_type,
112
- indices: Annotated[
113
- List[int],
114
- typer.Argument(help="Identifiers of the objects for which to create plots"),
115
- ],
116
- output_directory: Annotated[Path, typer.Argument(help="Output directory")],
117
- cutout: Annotated[
118
- Literal["sdss", "panstarrs"],
119
- typer.Option("-c", "--cutout", help="Which survey to use for cutouts"),
120
- ] = "panstarrs",
121
- ):
122
- make_plot(
123
- config_path, indices=indices, cutout=cutout, output_directory=output_directory
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
- meta["status"] = status
242
- with self.job_lock:
243
- self.jobs[task] = meta
244
- backend.save_meta(task, meta)
245
- else:
246
- with self.job_lock:
247
- self.jobs[task]["status"] = status
248
- snapshot = self.jobs[task]
249
- backend.save_meta(task, snapshot)
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