blastwave 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- blastwave/__init__.py +7 -0
- blastwave/errors.py +9 -0
- blastwave/models/__init__.py +8 -0
- blastwave/models/constants.py +0 -0
- blastwave/models/observation.py +61 -0
- blastwave/models/parquet.py +55 -0
- blastwave/models/query.py +38 -0
- blastwave/models/source.py +432 -0
- blastwave/projections/__init__.py +6 -0
- blastwave/projections/lsst.py +27 -0
- blastwave/projections/ztf.py +34 -0
- blastwave/query/__init__.py +8 -0
- blastwave/query/alert.py +252 -0
- blastwave/query/boom.py +339 -0
- blastwave/query/lsst.py +16 -0
- blastwave/query/timeout.py +32 -0
- blastwave/query/ztf.py +16 -0
- blastwave/utils/__init__.py +12 -0
- blastwave/utils/cache.py +100 -0
- blastwave/utils/combine.py +35 -0
- blastwave/utils/photometry.py +108 -0
- blastwave/utils/plot.py +42 -0
- blastwave-1.0.0.dist-info/METADATA +87 -0
- blastwave-1.0.0.dist-info/RECORD +27 -0
- blastwave-1.0.0.dist-info/WHEEL +5 -0
- blastwave-1.0.0.dist-info/licenses/LICENSE +21 -0
- blastwave-1.0.0.dist-info/top_level.txt +1 -0
blastwave/__init__.py
ADDED
blastwave/errors.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for various models
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from blastwave.models.observation import Observation
|
|
6
|
+
from blastwave.models.parquet import pydantic_to_arrow_schema
|
|
7
|
+
from blastwave.models.query import BOOMQuery, CatalogQuery, FilterQuery
|
|
8
|
+
from blastwave.models.source import Source
|
|
File without changes
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Observation model
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pyarrow as pa
|
|
10
|
+
from pydantic import BaseModel, computed_field
|
|
11
|
+
|
|
12
|
+
from blastwave.models.parquet import pydantic_to_arrow_schema
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
ObsClass = Literal["alert", "fp", "ul"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Observation(BaseModel):
|
|
20
|
+
"""
|
|
21
|
+
Model for an observation
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
jd: float
|
|
25
|
+
magpsf: float
|
|
26
|
+
sigmapsf: float
|
|
27
|
+
diffmaglim: float
|
|
28
|
+
ra: float | None
|
|
29
|
+
dec: float | None
|
|
30
|
+
snr: float
|
|
31
|
+
band: str
|
|
32
|
+
survey: str
|
|
33
|
+
det_type: ObsClass = "alert"
|
|
34
|
+
isdiffpos: bool
|
|
35
|
+
|
|
36
|
+
@computed_field()
|
|
37
|
+
@property
|
|
38
|
+
def estdiffmaglim(self) -> float:
|
|
39
|
+
"""
|
|
40
|
+
Estimate the difference magnitude limit based on snr and magpsf
|
|
41
|
+
|
|
42
|
+
:return:
|
|
43
|
+
"""
|
|
44
|
+
return self.magpsf - 2.5 * float(np.log10(5.0 / self.snr))
|
|
45
|
+
|
|
46
|
+
@computed_field()
|
|
47
|
+
@property
|
|
48
|
+
def mjd(self) -> float:
|
|
49
|
+
"""
|
|
50
|
+
Convert JD to MJD
|
|
51
|
+
"""
|
|
52
|
+
return self.jd - 2400000.5
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def get_arrow_schema(cls) -> pa.Schema:
|
|
56
|
+
"""
|
|
57
|
+
Get arrow schema from pydantic model
|
|
58
|
+
|
|
59
|
+
:return: Arrow schema
|
|
60
|
+
"""
|
|
61
|
+
return pydantic_to_arrow_schema(cls)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Script for converting Pydantic models to PyArrow schemas for Parquet serialization
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pyarrow as pa
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
PYDANTIC_TO_ARROW = {
|
|
9
|
+
float: pa.float64(),
|
|
10
|
+
int: pa.int64(),
|
|
11
|
+
str: pa.string(),
|
|
12
|
+
bool: pa.bool_(),
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def pydantic_to_arrow_schema(
|
|
17
|
+
model: type[BaseModel], include_computed: bool = False
|
|
18
|
+
) -> pa.Schema:
|
|
19
|
+
"""
|
|
20
|
+
Convert a Pydantic model to an Arrow schema
|
|
21
|
+
|
|
22
|
+
:param model: BaseModel
|
|
23
|
+
:param include_computed: bool indicating whether to include computed fields
|
|
24
|
+
:return: PyArrow schema
|
|
25
|
+
"""
|
|
26
|
+
fields = []
|
|
27
|
+
|
|
28
|
+
for name, field in model.model_fields.items():
|
|
29
|
+
annotation = field.annotation
|
|
30
|
+
if annotation is None:
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
args = getattr(annotation, "__args__", None)
|
|
34
|
+
if args:
|
|
35
|
+
annotation = next(a for a in args if not isinstance(a, type(None)))
|
|
36
|
+
|
|
37
|
+
arrow_type = PYDANTIC_TO_ARROW.get(annotation, pa.string())
|
|
38
|
+
fields.append(pa.field(name, arrow_type, nullable=True))
|
|
39
|
+
|
|
40
|
+
if include_computed:
|
|
41
|
+
for name, computed_field in model.model_computed_fields.items():
|
|
42
|
+
annotation = computed_field.return_type
|
|
43
|
+
|
|
44
|
+
if annotation not in PYDANTIC_TO_ARROW:
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
args = getattr(annotation, "__args__", None)
|
|
48
|
+
if args:
|
|
49
|
+
annotation = next(a for a in args if not isinstance(a, type(None)))
|
|
50
|
+
# Skip derived lists etc
|
|
51
|
+
if annotation not in PYDANTIC_TO_ARROW:
|
|
52
|
+
continue
|
|
53
|
+
fields.append(pa.field(name, PYDANTIC_TO_ARROW[annotation], nullable=True))
|
|
54
|
+
|
|
55
|
+
return pa.schema(fields)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Model for BOOM queries
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CatalogQuery(BaseModel):
|
|
12
|
+
"""
|
|
13
|
+
Model for BOOM queries using only catalog name
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
catalog_name: str
|
|
17
|
+
|
|
18
|
+
model_config = {"from_attributes": True}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FilterQuery(CatalogQuery):
|
|
22
|
+
"""
|
|
23
|
+
Model for BOOM queries using catalog name and a filter
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
filter: Mapping[str, Any] | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BOOMQuery(FilterQuery):
|
|
30
|
+
"""
|
|
31
|
+
Model for a full BOOM query
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
limit: int | None = None
|
|
35
|
+
max_time_ms: int | None = None
|
|
36
|
+
projection: Mapping[str, Any] | None = None
|
|
37
|
+
skip: int | None = None
|
|
38
|
+
sort: Mapping[str, Any] | None = None
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Model for a Source
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
import pyarrow as pa
|
|
12
|
+
import pyarrow.parquet as pq
|
|
13
|
+
from matplotlib import pyplot as plt
|
|
14
|
+
from pydantic import BaseModel, computed_field
|
|
15
|
+
|
|
16
|
+
from blastwave.models.observation import Observation
|
|
17
|
+
from blastwave.models.parquet import pydantic_to_arrow_schema
|
|
18
|
+
from blastwave.utils import (
|
|
19
|
+
get_crossmatch_path,
|
|
20
|
+
get_photometry_path,
|
|
21
|
+
get_source_path,
|
|
22
|
+
plot_lightcurve,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
FID_MAPPING = {
|
|
28
|
+
"g": "1",
|
|
29
|
+
"r": "2",
|
|
30
|
+
"i": "3",
|
|
31
|
+
"u": "-1",
|
|
32
|
+
"z": "-2",
|
|
33
|
+
"y": "-3",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Source(BaseModel):
|
|
38
|
+
"""
|
|
39
|
+
Model for a Source
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
objectid: str
|
|
43
|
+
jd: float
|
|
44
|
+
ztfid: str | None = None
|
|
45
|
+
lsstid: str | None = None
|
|
46
|
+
tns_name: str | None = None
|
|
47
|
+
ra: float
|
|
48
|
+
dec: float
|
|
49
|
+
offset: float | None
|
|
50
|
+
host_origin: str | None
|
|
51
|
+
redshift: float | None = None
|
|
52
|
+
redshift_error: float | None = None
|
|
53
|
+
redshift_origin: str | None = None
|
|
54
|
+
photometry: list[Observation]
|
|
55
|
+
crossmatches: dict[str, list[dict]] | None = None
|
|
56
|
+
|
|
57
|
+
@computed_field
|
|
58
|
+
@property
|
|
59
|
+
def ndethist(self) -> int:
|
|
60
|
+
"""
|
|
61
|
+
Get number of positive detections
|
|
62
|
+
|
|
63
|
+
:return: Number of positive detections
|
|
64
|
+
"""
|
|
65
|
+
return len(self.get_detections())
|
|
66
|
+
|
|
67
|
+
@computed_field
|
|
68
|
+
@property
|
|
69
|
+
def filters(self) -> list[str]:
|
|
70
|
+
"""
|
|
71
|
+
Get list of unique filters in detections
|
|
72
|
+
|
|
73
|
+
:return: List of unique filters
|
|
74
|
+
"""
|
|
75
|
+
return list(set(self.get_detections()["band"]))
|
|
76
|
+
|
|
77
|
+
@computed_field
|
|
78
|
+
@property
|
|
79
|
+
def ndetfilters(self) -> int:
|
|
80
|
+
"""
|
|
81
|
+
Get number of unique filters in detections
|
|
82
|
+
|
|
83
|
+
:return: Number of unique filters
|
|
84
|
+
"""
|
|
85
|
+
return len(self.filters)
|
|
86
|
+
|
|
87
|
+
@computed_field
|
|
88
|
+
@property
|
|
89
|
+
def jdstarthist(self) -> float:
|
|
90
|
+
"""
|
|
91
|
+
Get JD of first positive detection
|
|
92
|
+
|
|
93
|
+
:return: JD of first positive detection
|
|
94
|
+
"""
|
|
95
|
+
return float(self.get_detections()["jd"].min())
|
|
96
|
+
|
|
97
|
+
@computed_field
|
|
98
|
+
@property
|
|
99
|
+
def jdendhist(self) -> float:
|
|
100
|
+
"""
|
|
101
|
+
Get JD of last positive detection
|
|
102
|
+
|
|
103
|
+
:return: JD of last positive detection
|
|
104
|
+
"""
|
|
105
|
+
return float(self.get_detections()["jd"].max())
|
|
106
|
+
|
|
107
|
+
@computed_field
|
|
108
|
+
@property
|
|
109
|
+
def age(self) -> float:
|
|
110
|
+
"""
|
|
111
|
+
Get age of source in days (time between first and last positive detection)
|
|
112
|
+
|
|
113
|
+
:return: Age of source in days
|
|
114
|
+
"""
|
|
115
|
+
return self.jdendhist - self.jdstarthist
|
|
116
|
+
|
|
117
|
+
@computed_field
|
|
118
|
+
@property
|
|
119
|
+
def peak_mag(self) -> float:
|
|
120
|
+
"""
|
|
121
|
+
Get peak magnitude of source (brightest positive detection)
|
|
122
|
+
|
|
123
|
+
:return: Peak magnitude of source
|
|
124
|
+
"""
|
|
125
|
+
return float(self.get_detections()["magpsf"].min())
|
|
126
|
+
|
|
127
|
+
def get_photometry(self) -> pd.DataFrame:
|
|
128
|
+
"""
|
|
129
|
+
Get photometry as DataFrame
|
|
130
|
+
|
|
131
|
+
:return: DataFrame of photometry
|
|
132
|
+
"""
|
|
133
|
+
return pd.DataFrame([x.model_dump() for x in self.photometry])
|
|
134
|
+
|
|
135
|
+
def get_crossmatches(self) -> dict[str, list[dict]]:
|
|
136
|
+
"""
|
|
137
|
+
Get crossmatches as dict
|
|
138
|
+
|
|
139
|
+
:return: Dictionary of crossmatches
|
|
140
|
+
"""
|
|
141
|
+
return self.crossmatches if self.crossmatches else {}
|
|
142
|
+
|
|
143
|
+
def get_detections(
|
|
144
|
+
self,
|
|
145
|
+
min_snr: float = 3.0,
|
|
146
|
+
include_lsst_fp: bool = False,
|
|
147
|
+
) -> pd.DataFrame:
|
|
148
|
+
"""
|
|
149
|
+
Get positive detections from photometry
|
|
150
|
+
|
|
151
|
+
:param min_snr: Minimum SNR of positive detections
|
|
152
|
+
:param include_lsst_fp: Include LSST FP detections
|
|
153
|
+
:return: DataFrame of positive detections
|
|
154
|
+
"""
|
|
155
|
+
df = self.get_photometry()
|
|
156
|
+
if len(df) > 0:
|
|
157
|
+
positive_det_mask = (
|
|
158
|
+
df["isdiffpos"].astype(bool)
|
|
159
|
+
& (pd.notnull(df["magpsf"]))
|
|
160
|
+
& (df["snr"] > min_snr)
|
|
161
|
+
& ~(df["magpsf"] > (df["diffmaglim"] + 1.0))
|
|
162
|
+
)
|
|
163
|
+
if not include_lsst_fp:
|
|
164
|
+
lsst_mask = (df["det_type"] == "fp") & (df["survey"] == "LSST")
|
|
165
|
+
positive_det_mask &= ~lsst_mask
|
|
166
|
+
df = df[positive_det_mask].reset_index(drop=True)
|
|
167
|
+
return df
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def get_arrow_schema(cls, include_computed: bool = False) -> pa.Schema:
|
|
171
|
+
"""
|
|
172
|
+
Function to return Arrow schema
|
|
173
|
+
|
|
174
|
+
:param include_computed: Include computed fields from pydantic
|
|
175
|
+
:return: Schema
|
|
176
|
+
"""
|
|
177
|
+
return pydantic_to_arrow_schema(cls, include_computed=include_computed)
|
|
178
|
+
|
|
179
|
+
@classmethod
|
|
180
|
+
def from_lsst(cls, full_data: dict, photometry_df: pd.DataFrame) -> "Source":
|
|
181
|
+
"""
|
|
182
|
+
Create Source object from raw ZTF alert
|
|
183
|
+
|
|
184
|
+
:param full_data: Full alert data
|
|
185
|
+
:param photometry_df: Photometry data
|
|
186
|
+
:return: Source object
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
object_id = full_data["objectId"]
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
ztf_id = full_data["aliases"]["ZTF"][0]
|
|
193
|
+
except (IndexError, KeyError):
|
|
194
|
+
ztf_id = None
|
|
195
|
+
|
|
196
|
+
photometry = [
|
|
197
|
+
Observation(**row) for row in photometry_df.to_dict(orient="records")
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
# Add in something
|
|
201
|
+
offset = None
|
|
202
|
+
offset_origin = None
|
|
203
|
+
redshift = None
|
|
204
|
+
redshift_error = None
|
|
205
|
+
redshift_origin = None
|
|
206
|
+
|
|
207
|
+
crossmatches = full_data["cross_matches"]
|
|
208
|
+
for key in ["NED", "LSPSC", "PS1_DR1"]:
|
|
209
|
+
if key in crossmatches:
|
|
210
|
+
if len(crossmatches[key]) > 0:
|
|
211
|
+
match = crossmatches[key][0]
|
|
212
|
+
offset = match["distance_arcsec"]
|
|
213
|
+
offset_origin = key
|
|
214
|
+
if "z" in match:
|
|
215
|
+
redshift = match["z"]
|
|
216
|
+
redshift_error = match.get("z_unc", None)
|
|
217
|
+
redshift_origin = f"{key}_{match['z_tech']}"
|
|
218
|
+
break
|
|
219
|
+
|
|
220
|
+
return cls(
|
|
221
|
+
objectid=object_id,
|
|
222
|
+
lsstid=object_id,
|
|
223
|
+
ztfid=ztf_id,
|
|
224
|
+
photometry=photometry,
|
|
225
|
+
offset=offset,
|
|
226
|
+
host_origin=offset_origin,
|
|
227
|
+
ra=full_data["candidate"]["ra"],
|
|
228
|
+
dec=full_data["candidate"]["dec"],
|
|
229
|
+
jd=full_data["candidate"]["jd"],
|
|
230
|
+
crossmatches=crossmatches,
|
|
231
|
+
redshift=redshift,
|
|
232
|
+
redshift_error=redshift_error,
|
|
233
|
+
redshift_origin=redshift_origin,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
@classmethod
|
|
237
|
+
def from_ztf(cls, full_data: dict, photometry_df: pd.DataFrame) -> "Source":
|
|
238
|
+
"""
|
|
239
|
+
Create Source object from raw ZTF alert
|
|
240
|
+
|
|
241
|
+
:param full_data: Full alert data
|
|
242
|
+
:param photometry_df: Photometry data
|
|
243
|
+
:return: Source object
|
|
244
|
+
"""
|
|
245
|
+
object_id = full_data["objectId"]
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
lsst_id = full_data["aliases"]["LSST"][0]
|
|
249
|
+
except (IndexError, KeyError):
|
|
250
|
+
lsst_id = None
|
|
251
|
+
|
|
252
|
+
photometry = [
|
|
253
|
+
Observation(**row) for row in photometry_df.to_dict(orient="records")
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
return cls(
|
|
257
|
+
objectid=object_id,
|
|
258
|
+
lsstid=lsst_id,
|
|
259
|
+
ztfid=object_id,
|
|
260
|
+
photometry=photometry,
|
|
261
|
+
offset=full_data["candidate"]["distpsnr1"],
|
|
262
|
+
host_origin="PS1",
|
|
263
|
+
ra=full_data["candidate"]["ra"],
|
|
264
|
+
dec=full_data["candidate"]["dec"],
|
|
265
|
+
jd=full_data["candidate"]["jd"],
|
|
266
|
+
crossmatches=full_data["cross_matches"],
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def show_lightcurve(self, min_snr: float = 3.0) -> plt.Figure:
|
|
270
|
+
"""
|
|
271
|
+
Helper function for plotting lightcurve of source.
|
|
272
|
+
|
|
273
|
+
:param min_snr: Minimum SNR of positive detections
|
|
274
|
+
:return: Figure of light curve
|
|
275
|
+
"""
|
|
276
|
+
df = self.get_detections(min_snr=min_snr)
|
|
277
|
+
return plot_lightcurve(df)
|
|
278
|
+
|
|
279
|
+
def get_trimmed_photometry(self, min_snr: float = 3.0) -> list[Observation]:
|
|
280
|
+
"""
|
|
281
|
+
Trim photometry from source
|
|
282
|
+
|
|
283
|
+
:param min_snr: Minimum SNR of positive detections
|
|
284
|
+
:return: list of trimmed Observations
|
|
285
|
+
"""
|
|
286
|
+
photometry_df = self.get_detections(min_snr=min_snr)
|
|
287
|
+
return [Observation(**row) for row in photometry_df.to_dict(orient="records")]
|
|
288
|
+
|
|
289
|
+
def to_json(self, trim_photometry: bool = True) -> str:
|
|
290
|
+
"""
|
|
291
|
+
Convert to JSON
|
|
292
|
+
|
|
293
|
+
:return: JSON representation of Source
|
|
294
|
+
"""
|
|
295
|
+
data = self.model_dump(
|
|
296
|
+
mode="json", exclude_defaults=True, exclude_computed_fields=True
|
|
297
|
+
)
|
|
298
|
+
df = (
|
|
299
|
+
self.get_detections(min_snr=3.0)
|
|
300
|
+
if trim_photometry
|
|
301
|
+
else self.get_photometry()
|
|
302
|
+
)
|
|
303
|
+
df = df[list(Observation.model_fields.keys())]
|
|
304
|
+
data["photometry"] = df.to_dict(orient="list")
|
|
305
|
+
return json.dumps(data)
|
|
306
|
+
|
|
307
|
+
@classmethod
|
|
308
|
+
def from_json(cls, data: str) -> "Source":
|
|
309
|
+
"""
|
|
310
|
+
Create Source object from JSON
|
|
311
|
+
|
|
312
|
+
:param data: JSON representation of Source
|
|
313
|
+
:return: Source object
|
|
314
|
+
"""
|
|
315
|
+
d = json.loads(data)
|
|
316
|
+
photometry_df = pd.DataFrame(d.pop("photometry"))
|
|
317
|
+
photometry = [
|
|
318
|
+
Observation(**row) for row in photometry_df.to_dict(orient="records")
|
|
319
|
+
]
|
|
320
|
+
return cls(**d, photometry=photometry)
|
|
321
|
+
|
|
322
|
+
def to_parquet(
|
|
323
|
+
self, base_path: Path | str | None = None, trim_photometry: bool = True
|
|
324
|
+
) -> None:
|
|
325
|
+
"""
|
|
326
|
+
Export photometry as parquet file
|
|
327
|
+
|
|
328
|
+
:param base_path: Base directory
|
|
329
|
+
:param trim_photometry: Store only positive detections
|
|
330
|
+
:return: None
|
|
331
|
+
"""
|
|
332
|
+
source_path = get_source_path(self.objectid, base_path)
|
|
333
|
+
crossmatch_path = get_crossmatch_path(self.objectid, base_path)
|
|
334
|
+
photometry_path = get_photometry_path(self.objectid, base_path)
|
|
335
|
+
|
|
336
|
+
source_path.parent.mkdir(parents=True, exist_ok=True)
|
|
337
|
+
crossmatch_path.parent.mkdir(parents=True, exist_ok=True)
|
|
338
|
+
photometry_path.parent.mkdir(parents=True, exist_ok=True)
|
|
339
|
+
|
|
340
|
+
# Split out photometry and crossmatches
|
|
341
|
+
del_cols = ["photometry", "crossmatches"]
|
|
342
|
+
|
|
343
|
+
# source metadata row
|
|
344
|
+
meta = pd.DataFrame(
|
|
345
|
+
[{k: v for k, v in self.model_dump().items() if k not in del_cols}]
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
schema = Source.get_arrow_schema(include_computed=True)
|
|
349
|
+
for field in del_cols:
|
|
350
|
+
schema = schema.remove(schema.get_field_index(field))
|
|
351
|
+
# meta = meta[[x for x in schema.names]]
|
|
352
|
+
table = pa.Table.from_pandas(meta, schema=schema)
|
|
353
|
+
pq.write_table(table, source_path, compression="zstd")
|
|
354
|
+
|
|
355
|
+
# Dump crossmatches to json
|
|
356
|
+
crossmatch_path.write_text(json.dumps(self.crossmatches))
|
|
357
|
+
|
|
358
|
+
# photometry
|
|
359
|
+
df = (
|
|
360
|
+
self.get_detections(min_snr=3.0)
|
|
361
|
+
if trim_photometry
|
|
362
|
+
else self.get_photometry()
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# Clip derived columns
|
|
366
|
+
df = df[list(Observation.model_fields.keys())]
|
|
367
|
+
|
|
368
|
+
table = pa.Table.from_pandas(df, schema=Observation.get_arrow_schema())
|
|
369
|
+
pq.write_table(table, photometry_path, compression="zstd")
|
|
370
|
+
|
|
371
|
+
@classmethod
|
|
372
|
+
def from_parquet(
|
|
373
|
+
cls, object_id: str, base_path: Path | str | None = None
|
|
374
|
+
) -> "Source":
|
|
375
|
+
"""
|
|
376
|
+
Load a Source object from parquet files.
|
|
377
|
+
|
|
378
|
+
:param object_id: Object ID
|
|
379
|
+
:param base_path: Base path to parquet files
|
|
380
|
+
:return: Source object
|
|
381
|
+
"""
|
|
382
|
+
meta = pd.read_parquet(get_source_path(object_id, base_path))
|
|
383
|
+
|
|
384
|
+
meta = meta.replace({np.nan: None})
|
|
385
|
+
|
|
386
|
+
photometry_df = pd.read_parquet(get_photometry_path(object_id, base_path))
|
|
387
|
+
crossmatches = json.loads(get_crossmatch_path(object_id, base_path).read_text())
|
|
388
|
+
|
|
389
|
+
photometry = [
|
|
390
|
+
Observation(**row) for row in photometry_df.to_dict(orient="records")
|
|
391
|
+
]
|
|
392
|
+
|
|
393
|
+
return cls(
|
|
394
|
+
**meta.iloc[0].to_dict(), photometry=photometry, crossmatches=crossmatches
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# def to_archive(self):
|
|
398
|
+
# """
|
|
399
|
+
# Dump Source object to dictionary in archive format,
|
|
400
|
+
# with photometry as list of dictionaries
|
|
401
|
+
#
|
|
402
|
+
# :return: Dictionary in archive format
|
|
403
|
+
# """
|
|
404
|
+
#
|
|
405
|
+
# output_path = babamul_cache_dir / f"{self.objectid}.json"
|
|
406
|
+
# res = self.model_dump()
|
|
407
|
+
# with open(output_path, "w") as f:
|
|
408
|
+
# json.dump(res, f, indent=4)
|
|
409
|
+
#
|
|
410
|
+
def convert_to_ztfstyle(self) -> dict:
|
|
411
|
+
"""
|
|
412
|
+
Convert Source object to ZTF-style dictionary,
|
|
413
|
+
with candidate and previous candidates
|
|
414
|
+
|
|
415
|
+
:return: ZTF-style dictionary (like Kowalski)
|
|
416
|
+
"""
|
|
417
|
+
res = self.model_dump()
|
|
418
|
+
cand = {key: val for key, val in res.items() if key not in ["photometry"]}
|
|
419
|
+
res["candidate"] = cand
|
|
420
|
+
res["objectId"] = self.objectid
|
|
421
|
+
|
|
422
|
+
df = self.get_detections()
|
|
423
|
+
|
|
424
|
+
df["fid"] = df["band"].map(FID_MAPPING).astype("Int8")
|
|
425
|
+
df["filter"] = df["band"]
|
|
426
|
+
|
|
427
|
+
match = df.iloc[-1]
|
|
428
|
+
|
|
429
|
+
res["candidate"].update(match.to_dict())
|
|
430
|
+
res["prv_candidates"] = df[:-1].to_dict(orient="records")
|
|
431
|
+
|
|
432
|
+
return res
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LSST alert projections
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# pylint: disable=duplicate-code
|
|
6
|
+
|
|
7
|
+
prefixes = ["prv_candidates", "fp_hists"]
|
|
8
|
+
fields = [
|
|
9
|
+
"jd",
|
|
10
|
+
"magpsf",
|
|
11
|
+
"sigmapsf",
|
|
12
|
+
"diffmaglim",
|
|
13
|
+
"ra",
|
|
14
|
+
"dec",
|
|
15
|
+
"snr",
|
|
16
|
+
"band",
|
|
17
|
+
"isdiffpos",
|
|
18
|
+
"psfFlux",
|
|
19
|
+
"psfFluxErr",
|
|
20
|
+
"reliability",
|
|
21
|
+
"isDipole",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
lsst_aux_projection = {
|
|
25
|
+
f"{prefix}.{field}": 1 for prefix in prefixes for field in fields
|
|
26
|
+
}
|
|
27
|
+
lsst_aux_projection["aliases"] = 1
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Minimal projections for ZTF
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# pylint: disable=duplicate-code
|
|
6
|
+
|
|
7
|
+
ztf_alert_projection = {
|
|
8
|
+
"objectId": 1,
|
|
9
|
+
"candidate.distpsnr1": 1,
|
|
10
|
+
"candidate.jd": 1,
|
|
11
|
+
"candidate.fid": 1,
|
|
12
|
+
"candidate.magpsf": 1,
|
|
13
|
+
"candidate.sigmapsf": 1,
|
|
14
|
+
"candidate.ra": 1,
|
|
15
|
+
"candidate.dec": 1,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
prefixes = ["prv_candidates", "fp_hists", "prv_nondetections"]
|
|
19
|
+
fields = [
|
|
20
|
+
"jd",
|
|
21
|
+
"magpsf",
|
|
22
|
+
"sigmapsf",
|
|
23
|
+
"diffmaglim",
|
|
24
|
+
"ra",
|
|
25
|
+
"dec",
|
|
26
|
+
"band",
|
|
27
|
+
"isdiffpos",
|
|
28
|
+
"psfFlux",
|
|
29
|
+
"psfFluxErr",
|
|
30
|
+
"snr_psf",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
ztf_aux_projection = {f"{prefix}.{field}": 1 for prefix in prefixes for field in fields}
|
|
34
|
+
ztf_aux_projection["aliases"] = 1
|