antares-devkit 0.6.3__tar.gz → 0.6.5__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.
Files changed (66) hide show
  1. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/PKG-INFO +1 -1
  2. antares_devkit-0.6.5/antares_devkit/filters/lsst_transient_quality_scimma/__init__.py +92 -0
  3. antares_devkit-0.6.5/antares_devkit/filters/microlensing/__init__.py +505 -0
  4. antares_devkit-0.6.5/antares_devkit/filters/shadow_virgo/__init__.py +50 -0
  5. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/young_rubin_transients_soraisam/__init__.py +27 -23
  6. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/utils.py +4 -1
  7. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/pyproject.toml +1 -1
  8. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/LICENSE.txt +0 -0
  9. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/README.md +0 -0
  10. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/__init__.py +0 -0
  11. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/bad_detection/__init__.py +0 -0
  12. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/bad_seeing/__init__.py +0 -0
  13. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/crash_filter/__init__.py +0 -0
  14. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/desi_target/__init__.py +0 -0
  15. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/dwarf_nova_outburst/__init__.py +0 -0
  16. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/ecl_transient_1/__init__.py +0 -0
  17. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/ecl_transient_2/__init__.py +0 -0
  18. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/extragalactic/__init__.py +0 -0
  19. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/highamp/__init__.py +0 -0
  20. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/highfluxchange/__init__.py +0 -0
  21. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/highsnr/__init__.py +0 -0
  22. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/if_anomaly_detection/__init__.py +0 -0
  23. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/if_anomaly_detection/data/__init__.py +0 -0
  24. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/if_anomaly_detection/data/paleo2_106IFmodel_472727objs_nest=1000_cont=0.001_transf.pkl +0 -0
  25. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/imbh/__init__.py +0 -0
  26. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/inm31/__init__.py +0 -0
  27. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/laiss_rfc_ad_filter/__init__.py +0 -0
  28. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/laiss_rfc_ad_filter/data/__init__.py +0 -0
  29. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/laiss_rfc_ad_filter/data/cls=binary_n_estimators=100_max_depth=35_rs=11_max_feats=35_cw=balanced.pkl +0 -0
  30. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/lsst_ddf/__init__.py +0 -0
  31. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/malanchev_anomaly_transient_dmdt/__init__.py +0 -0
  32. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/malanchev_anomaly_transient_dmdt/data/__init__.py +0 -0
  33. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/malanchev_anomaly_transient_dmdt/data/malanchev_ssiforest_v1.pickle +0 -0
  34. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/malanchev_features_v1/__init__.py +0 -0
  35. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/novtest/__init__.py +0 -0
  36. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/nuclear_transient/__init__.py +0 -0
  37. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/nuttela_tao/__init__.py +0 -0
  38. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/persistent_dimmers/__init__.py +0 -0
  39. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/random_tagger_filter/__init__.py +0 -0
  40. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/recent_reddening/__init__.py +0 -0
  41. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/refitt_newsources_snrcut/__init__.py +0 -0
  42. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/rubin_NovTest_soraisam/__init__.py +0 -0
  43. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/sample_filter/__init__.py +0 -0
  44. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/siena_mag_coord_cut/__init__.py +0 -0
  45. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/sso/__init__.py +0 -0
  46. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/sub_lum/__init__.py +0 -0
  47. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/superphot_plus_v1/__init__.py +0 -0
  48. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/superphot_plus_ztf_v3/__init__.py +0 -0
  49. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/test_dev_all/__init__.py +0 -0
  50. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/test_dev_lsst/__init__.py +0 -0
  51. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/test_dev_ztf/__init__.py +0 -0
  52. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/vpdf/__init__.py +0 -0
  53. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/vpdf/data/__init__.py +0 -0
  54. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/vpdf/data/matheson_percent_grid_v1 +0 -0
  55. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/filters/young_extragalactic_candidate/__init__.py +0 -0
  56. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/antares_devkit/models.py +0 -0
  57. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/test/Dockerfile +0 -0
  58. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/test/__init__.py +0 -0
  59. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/test/conftest.py +0 -0
  60. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/test/data/large_locus.py +0 -0
  61. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/test/data/small_locus.py +0 -0
  62. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/test/unit/antares_devkit/filters/test_test_dev_all.py +0 -0
  63. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/test/unit/antares_devkit/filters/test_test_dev_lsst.py +0 -0
  64. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/test/unit/antares_devkit/filters/test_test_dev_ztf.py +0 -0
  65. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/test/unit/antares_devkit/test_models.py +0 -0
  66. {antares_devkit-0.6.3 → antares_devkit-0.6.5}/test/unit/test_filters.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: antares-devkit
3
- Version: 0.6.3
3
+ Version: 0.6.5
4
4
  Summary: Provides tools and utility methods to write and debug ANTARES filters with Python.
5
5
  Keywords: antares,devkit,filter development
6
6
  Author-Email: NSF NOIRLab ANTARES Team <antares@noirlab.edu>
@@ -0,0 +1,92 @@
1
+ """
2
+ ANTARES filter for selecting high-quality LSST transient candidates
3
+ for cross matching against Gaia and other catalogs using LSDB
4
+ running in SCiMMA infrastructure. See https://scimma.org/
5
+
6
+ This filter runs inside ANTARES infrastructure. It evaluates each incoming
7
+ LSST alert against quality criteria and tags passing alerts for downstream
8
+ crossmatching against catalogs.
9
+ """
10
+
11
+ from antares_devkit.models import BaseFilter
12
+
13
+
14
+ class LsstTransientQualityFilterScimma(BaseFilter):
15
+ """
16
+ This filter finds loci where the first alert has a SNR > 10 and the
17
+ image is not saturated, near an image edge, not classified as a dipole,
18
+ and excludes known solar system objects.
19
+ """
20
+
21
+ SLACK_CHANNEL = "#filter-scimma-lsst-quality"
22
+
23
+ TRIGGERING_SURVEY = "lsst"
24
+
25
+ REQUIRED_LOCUS_PROPERTIES = []
26
+
27
+ REQUIRED_ALERT_PROPERTIES = [
28
+ "lsst_diaSource_snr",
29
+ "lsst_diaSource_ssObjectId",
30
+ "lsst_diaSource_psfFlux_flag",
31
+ "lsst_diaSource_centroid_flag",
32
+ "lsst_diaSource_shape_flag",
33
+ "lsst_diaSource_isDipole",
34
+ "lsst_diaSource_pixelFlags_saturated",
35
+ "lsst_diaSource_pixelFlags_edge",
36
+ "lsst_diaSource_pixelFlags_cr",
37
+ "lsst_diaSource_pixelFlags_streak",
38
+ ]
39
+
40
+ REQUIRED_TAGS = []
41
+
42
+ REQUIRES_FILES = []
43
+
44
+ OUTPUT_TAG = "lsst_scimma_quality_transient"
45
+
46
+ OUTPUT_LOCUS_PROPERTIES = []
47
+
48
+ OUTPUT_LOCUS_TAGS = [
49
+ {
50
+ "name": OUTPUT_TAG,
51
+ "description": (
52
+ "LSST alert passes quality cuts for transient candidate "
53
+ "crossmatching by SCiMMA: SNR > 10, no quality flags, "
54
+ "not a known solar system object."
55
+ ),
56
+ },
57
+ ]
58
+
59
+ def setup(self):
60
+ pass
61
+
62
+ def _run(self, locus):
63
+ props = locus.alerts[-1].properties
64
+
65
+ # SNR must be strictly greater than 10.
66
+ if not props["lsst_diaSource_snr"] > 10:
67
+ return
68
+
69
+ # Exclude known solar system objects. The type of ssObjectId is
70
+ # not yet confirmed, so check for integer 0, None, and string "0".
71
+ if props["lsst_diaSource_ssObjectId"] not in (0, None, "0"):
72
+ return
73
+
74
+ # Quality flags must all be falsy.
75
+ if props["lsst_diaSource_psfFlux_flag"]:
76
+ return
77
+ if props["lsst_diaSource_centroid_flag"]:
78
+ return
79
+ if props["lsst_diaSource_shape_flag"]:
80
+ return
81
+ if props["lsst_diaSource_isDipole"]:
82
+ return
83
+ if props["lsst_diaSource_pixelFlags_saturated"]:
84
+ return
85
+ if props["lsst_diaSource_pixelFlags_edge"]:
86
+ return
87
+ if props["lsst_diaSource_pixelFlags_cr"]:
88
+ return
89
+ if props["lsst_diaSource_pixelFlags_streak"]:
90
+ return
91
+
92
+ locus.tag(self.OUTPUT_TAG)
@@ -0,0 +1,505 @@
1
+ # Filter for microlensing events designed for the ANTARES broker
2
+
3
+ # Authors: Somayeh Khakpash, Natasha Abrams, Rachel Street, Atousa Kalantari
4
+
5
+ import antares_devkit as dk
6
+ from antares_devkit.models import DevKitLocus
7
+ from antares_devkit.models import BaseFilter
8
+ import warnings
9
+ import math
10
+
11
+
12
+ class microlensing(BaseFilter):
13
+ INPUT_LOCUS_PROPERTIES = [
14
+ 'ant_object_id',
15
+ ]
16
+
17
+ REQUIRED_TAGS = ['lc_feature_extractor']
18
+
19
+ OUTPUT_TAGS = [
20
+ {
21
+ 'name': 'microlensing_candidate',
22
+ 'description': 'Locus - a transient candidate - exhibits a microlensing-like variability',
23
+ }
24
+ ]
25
+
26
+ np = None
27
+ curve_fit = None
28
+ skew = None
29
+ sigma_clip = None
30
+
31
+ # Define a Paczyński microlensing model
32
+ def paczynski(self, t, t0, u0, tE, F_s):
33
+ """
34
+ Paczyński microlensing light curve model
35
+ t0 : peak time
36
+ u0 : impact parameter
37
+ tE : Einstein crossing time
38
+ F_s : source flux
39
+ F_b : blended flux
40
+ """
41
+ u = self.np.sqrt(u0 ** 2 + ((t - t0) / tE) ** 2)
42
+ A = (u ** 2 + 2) / (u * self.np.sqrt(u ** 2 + 4))
43
+ return F_s * (A) + (1 - F_s)
44
+
45
+ def fit_paczynski(self, times, mags, flxs, flx_errs):
46
+ """
47
+ Fit the Paczyński microlensing model to flux data.
48
+ Returns best-fit parameters and chi-squared value.
49
+ """
50
+ if len(times) < 4:
51
+ return None, None # Not enough data
52
+
53
+ # initial guesses
54
+ t0_guess = times[self.np.argmin(flxs)]
55
+ u0_guess = 1.0 / (self.np.max(flxs))
56
+
57
+ tE_guess = 20.0
58
+ F0_guess = 0.5
59
+
60
+ initial_guess = [t0_guess, u0_guess, tE_guess, F0_guess]
61
+
62
+ bounds = (
63
+ [times.min() - 50, 0, 1.0, 0.0],
64
+ [times.max() + 50, self.np.inf, 500.0, 1]
65
+ )
66
+
67
+ try:
68
+ popt, _ = self.curve_fit(
69
+ self.paczynski,
70
+ times, flxs,
71
+ p0=initial_guess,
72
+ sigma=flx_errs,
73
+ bounds=bounds,
74
+
75
+ maxfev=5000
76
+ )
77
+ chi2 = self.np.sum(((flxs - self.paczynski(times, *popt)) / flx_errs) ** 2) / len(times)
78
+ return popt, chi2
79
+ except Exception as e:
80
+ print(f" Paczynski fitting error: {e}")
81
+ return None, None
82
+
83
+ def mag_to_flux(self, mag, F0=1.0):
84
+ """
85
+ Convert magnitude to flux.
86
+
87
+ Parameters:
88
+ - mag : magnitude (float or array)
89
+ - F0 : reference flux (zeropoint), default=1.0 for relative flux
90
+
91
+ Returns:
92
+ - flux : flux corresponding to the magnitude
93
+ """
94
+ flux = F0 * 10 ** (-0.4 * mag)
95
+ flux = flux / self.np.min(flux)
96
+ return flux
97
+
98
+ def magerr_to_fluxerr(self, mag, mag_err, F0=1.0):
99
+ """
100
+ Convert magnitude uncertainty to flux uncertainty.
101
+
102
+ Parameters:
103
+ - mag : magnitude value or array
104
+ - mag_err : magnitude uncertainty value or array
105
+ - F0 : zeropoint flux (default=1.0 for relative flux)
106
+
107
+ Returns:
108
+ - flux_err : flux uncertainty
109
+ """
110
+ flux = self.mag_to_flux(mag, F0)
111
+ flux_err = 0.4 * self.np.log(10) * flux * mag_err
112
+ return flux_err
113
+
114
+ # Chi squared functions for BAGLE model
115
+ def calc_chi2_mean(self, times, mag, mag_err, verbose=False):
116
+ """
117
+ Parameters
118
+ ----------
119
+ params : str or dict, optional
120
+ model_params = 'best' will load up the best solution and calculate
121
+ the chi^2 based on those values. Alternatively, pass in a dictionary
122
+ with the model parameters to use.
123
+ """
124
+ # Get likelihoods.
125
+ lnL_phot = self.log_likely_photometry(times, mag, mag_err)
126
+
127
+ # Lists to store lnL, chi2, and constants for each filter.
128
+ chi2_phot_filts = []
129
+ lnL_const_phot_filts = []
130
+
131
+ # Calculate the lnL for just a single filter.
132
+ lnL_phot_nn = self.log_likely_photometry(times, mag, mag_err)
133
+
134
+ # Calculate the chi2 and constants for just a single filter.
135
+ lnL_const_phot_nn = -0.5 * self.np.log(2.0 * math.pi * mag_err ** 2)
136
+ lnL_const_phot_nn = lnL_const_phot_nn.sum()
137
+
138
+ chi2_phot_nn = (lnL_phot_nn - lnL_const_phot_nn) / -0.5
139
+
140
+ # Save to our lists
141
+ chi2_phot_filts.append(chi2_phot_nn)
142
+ lnL_const_phot_filts.append(lnL_const_phot_nn)
143
+
144
+ lnL_const_phot = sum(lnL_const_phot_filts)
145
+
146
+ # Calculate chi2.
147
+ chi2 = (lnL_phot - lnL_const_phot) / -0.5
148
+
149
+ if verbose:
150
+ fmt = '{0:13s} = {1:f} '
151
+ ff = 0
152
+ print(fmt.format('chi2_phot' + str(ff + 1), chi2_phot_filts[ff]))
153
+
154
+ print(fmt.format('chi2', chi2))
155
+
156
+ return chi2
157
+
158
+ def log_likely_photometry(self, times, mag, mag_err, verbose=False):
159
+
160
+ lnL_phot = 0.0
161
+
162
+ # commenting out weight stuff for now
163
+ # weight = self.weights[i]
164
+ lnL_phot_unwgt = self.log_likely_photometry_each(times, mag, mag_err)
165
+ lnL_phot_i = lnL_phot_unwgt # * weight
166
+ lnL_phot += lnL_phot_i
167
+
168
+ if verbose:
169
+ print(
170
+ f'lnL_phot: i = {i} L_unwgt = {lnL_phot_unwgt:15.1f}, L_wgt = {lnL_phot_i:15.1f}, weight = {weight:.1e}')
171
+
172
+ return lnL_phot
173
+
174
+ def log_likely_photometry_each(self, t_obs, mag_obs, mag_err_obs, filt_idx=0):
175
+ """
176
+ Get the natural log of the likelihood for the input photometric data in the
177
+ specified filter or data sets. Note, this function returns a list and it
178
+ is the full ln(likelihood), including the normalization constant.
179
+
180
+ Parameters
181
+ ----------
182
+ t_obs : array_like
183
+ List of times in MJD for the observations.
184
+ mag_obs : array_like
185
+ List of observed photometric measurements of the microlensing event in magnitudes.
186
+ Length must be the same as t_obs.
187
+ mag_obs_err : array_like
188
+ List of observed photometric uncertainties of the microlensing event in magnitudes.
189
+ Length must be the same as t_obs.
190
+ filt_idx : int, optional
191
+ Index of the photometric filter or data set.
192
+
193
+ Returns
194
+ -------
195
+ ln_L : array_like
196
+ List of ln(likelihood) for each photometric measurement.
197
+
198
+ """
199
+
200
+ chi2_m = self.get_chi2_photometry_mean(t_obs, mag_obs, mag_err_obs, filt_idx=filt_idx)
201
+
202
+ lnL_const_m = self.get_lnL_constant(mag_err_obs)
203
+
204
+ lnL = (-0.5 * chi2_m) + lnL_const_m
205
+
206
+ return lnL.sum()
207
+
208
+ def get_chi2_photometry_mean(self, t_obs, mag_obs, mag_err_obs, filt_idx=0):
209
+ """
210
+ Get chi^2 values for THE MEAN OF DATA and input photometric data in the
211
+ specified photometric filter or data set.
212
+
213
+ Parameters
214
+ ----------
215
+ t_obs : array_like
216
+ List of times in MJD for the observations.
217
+ mag_obs : array_like
218
+ List of observed photometric measurements of the microlensing event in magnitudes.
219
+ Length must be the same as t_obs.
220
+ mag_obs_err : array_like
221
+ List of observed photometric uncertainties of the microlensing event in magnitudes.
222
+ Length must be the same as t_obs.
223
+ filt_idx : int, optional
224
+ Index of the photometric filter or data set.
225
+
226
+ Returns
227
+ -------
228
+ chi2 : array_like
229
+ List of chi^2 values from the model and photometric data.
230
+
231
+ """
232
+ sigma_clip_mag = self.sigma_clip(mag_obs)
233
+ mag_model = self.np.mean(sigma_clip_mag)
234
+
235
+ chi2 = ((mag_obs - mag_model) / mag_err_obs) ** 2
236
+
237
+ return chi2
238
+
239
+ def get_lnL_constant(self, err_obs):
240
+ """
241
+ Get the natural log of the constant normalization terms of the likelihood.
242
+
243
+ .. math:: -0.5 * \ln{2 \pi \sigma_{obs}^2}
244
+
245
+ Parameters
246
+ ----------
247
+ err_obs : array_like
248
+ List of the uncertainties.
249
+
250
+ Returns
251
+ -------
252
+ List of ln(likelihood constants).
253
+
254
+ """
255
+ lnL_const = -0.5 * self.np.log(2.0 * math.pi * err_obs ** 2)
256
+
257
+ return lnL_const
258
+
259
+ def is_known_other_phenomenon(self, locus, locus_params):
260
+ """
261
+ Method to check the locus' pre-existing parameters indicated that it has
262
+ been identified or is likely to be a variable of a type other than microlensing
263
+
264
+ :param locus:
265
+ :param locus_params:
266
+ :return: boolean
267
+ """
268
+
269
+ # Default result is not a known variable
270
+ known_var = False
271
+
272
+ # Tunable detection thresholds.
273
+ # Ref: Sokolovsky et al. 2016: https://ui.adsabs.harvard.edu/abs/2017MNRAS.464..274S/abstract
274
+ period_peak_sn_threshold = 20.0 # Based on tests with ZTF alerts
275
+ stetson_k_threshold = 0.8 # The expected K-value for a constant lightcurve with Gaussian noise
276
+
277
+ # Check for periodicity
278
+ feature_period_s_to_n_0_magn_r_exists = 'feature_period_s_to_n_0_magn_r' in locus_params.keys()
279
+ if feature_period_s_to_n_0_magn_r_exists:
280
+ if locus_params['feature_period_s_to_n_0_magn_r'] >= period_peak_sn_threshold:
281
+ known_var = True
282
+
283
+ # Check Stetson-K index
284
+ feature_stetson_k_magn_r_exists = 'feature_stetson_k_magn_r' in locus_params.keys()
285
+ if feature_stetson_k_magn_r_exists:
286
+ if locus_params['feature_stetson_k_magn_r'] <= stetson_k_threshold:
287
+ known_var = True
288
+
289
+ # If the alert has parameters from JPL Horizons, then it is likely cause by
290
+ # a Solar System object
291
+ if 'horizons_targetname' in locus_params.keys():
292
+ known_var = True
293
+
294
+ # Check whether the ANTARES crossmatch against known galaxy catalogs threw up any matches
295
+ # The locus.catalog_objects attribute is a dictionary of lists of known objects for each
296
+ # catalogs. If a match has been found, then the key for the corresponding catalog will be
297
+ # in the list of keys. So we can use that to check for matches with galaxy catalogs.
298
+ # Of those available in the list the Gemini NIR survey of known quasars is the closest
299
+ if 'gnirs_dqs' in locus.catalog_objects.keys():
300
+ known_var = True
301
+
302
+ return known_var
303
+
304
+ def calculate_eta(self, mag):
305
+ """ Via puzle https://github.com/jluastro/puzle/blob/main/puzle/stats.py"""
306
+ delta = self.np.sum((self.np.diff(mag) * self.np.diff(mag)) / (len(mag) - 1))
307
+ variance = self.np.var(mag)
308
+ eta = delta / variance
309
+ return eta
310
+
311
+ def return_eta_residual_slope_offset(self):
312
+ """
313
+ Via puzle https://github.com/jluastro/puzle/blob/main/puzle/cands.py
314
+ TODO is 6 months and a year - calculate slope and intercept based on real Rubin data
315
+ """
316
+ slope = 3.8187919463087248
317
+ offset = -0.07718120805369133
318
+ return slope, offset
319
+
320
+ def make_bagle_data_dir(self, times, mags, errors):
321
+ data = {}
322
+ data['target'] = 'single_event'
323
+ data['phot_data'] = 'alert'
324
+ data['phot_files'] = ['locus']
325
+ data['ast_data'] = 'None'
326
+ data['ast_files'] = []
327
+
328
+ data['t_phot1'] = times
329
+ data['mag1'] = mags
330
+ data['mag_err1'] = errors
331
+
332
+ return data
333
+
334
+ def is_microlensing_candidate(self, locus, times, mags, errors, verbose):
335
+ """
336
+ Example of a set of Microlensing detection criteria
337
+ """
338
+ if len(times) < 10: # Too few data points
339
+ if verbose == True:
340
+ print('Too Few Datapoints')
341
+ return False
342
+
343
+ # Extract the full parameter set from the locus and the alert
344
+ locus_params = locus.properties
345
+
346
+ # Use the pre-calculated properties of the locus to eliminate those
347
+ # which show signs of variability, e.g. in their periodicity signature or
348
+ # the Stetson-K index
349
+ known_var = self.is_known_other_phenomenon(locus, locus_params)
350
+ if known_var:
351
+ if verbose == True:
352
+ print('Other known phenomenon')
353
+ return False
354
+
355
+ # Sort data by time
356
+ sorted_idx = self.np.argsort(times)
357
+ times, mags, errors = times[sorted_idx], mags[sorted_idx], errors[sorted_idx]
358
+ npts = len(times)
359
+
360
+ # 1. Check for smoothness (low skewness means symmetric light curve)
361
+ # TODO: Check for threshold with parallax and maybe remove or lower threshold
362
+ if abs(self.skew(mags)) > 1:
363
+ if verbose == True:
364
+ print('Too skewed')
365
+ return False
366
+
367
+ # TODO is 6 months and a year - calculate this based on percentile of real data
368
+ eta_thresh = 1.255 # Avg from ZTF level 2 (low eta)
369
+ # Do check for existance since if there's only one band of data, only one will exist
370
+ eta_r_exists = 'feature_eta_e_magn_r' in locus_params.keys()
371
+ eta_g_exists = 'feature_eta_e_magn_g' in locus_params.keys()
372
+ if eta_r_exists:
373
+ eta_r = locus_params['feature_eta_e_magn_r']
374
+ if eta_g_exists:
375
+ eta_g = locus_params['feature_eta_e_magn_g']
376
+ if eta_r_exists and eta_g_exists:
377
+ if eta_r >= eta_thresh and eta_g >= eta_thresh:
378
+ if verbose == True:
379
+ print('Failed von Neumann threshold')
380
+ return False
381
+ elif eta_r_exists:
382
+ if eta_r >= eta_thresh:
383
+ if verbose == True:
384
+ print('Failed von Neumann threshold')
385
+ return False
386
+ elif eta_g_exists:
387
+ if eta_g >= eta_thresh:
388
+ if verbose == True:
389
+ print('Failed von Neumann threshold')
390
+ return False
391
+
392
+ # 2. Check variability (microlensing should have a clear peak)
393
+ # Decrease threshold with longer baseline
394
+ # Q for broker - 365 days or full lightcurve?
395
+ if self.np.ptp(mags) < 0.5: # Peak-to-peak magnitude difference
396
+ if verbose == True:
397
+ print('Too small magnitude change')
398
+ return False
399
+
400
+ flxs = self.mag_to_flux(mags)
401
+ flx_errs = self.magerr_to_fluxerr(mags, errors)
402
+
403
+ # 3. Perform a lightweight template fit (Paczyński model)
404
+
405
+ popt, chi2_paczynski = self.fit_paczynski(times, mags, flxs, flx_errs)
406
+ resid = flxs - self.paczynski(times, *popt)
407
+ chi2_val = self.np.sum((resid / flx_errs) ** 2) / npts
408
+
409
+ # 4. Apply a simple chi2 threshold
410
+ if chi2_val > 2: # Poor-fit light curves fails
411
+ if verbose == True:
412
+ print('Failed simple fit chi^2 threshold', chi2_val)
413
+ return False
414
+ # except RuntimeError:
415
+ # return False # Fit failed
416
+
417
+ # 5. Check that the residual isn't correlated in all avaliable bands
418
+ eta_resid = self.calculate_eta(resid)
419
+ eta_slope, eta_offset = self.return_eta_residual_slope_offset()
420
+ if eta_r_exists and eta_g_exists:
421
+ if (eta_resid < eta_r * eta_slope + eta_offset) and (eta_resid < eta_g * eta_slope + eta_offset):
422
+ if verbose == True:
423
+ print('Failed eta residual threshold')
424
+ return False
425
+ elif eta_r_exists:
426
+ if (eta_resid < eta_r * eta_slope + eta_offset):
427
+ if verbose == True:
428
+ print('Failed eta residual threshold')
429
+ return False
430
+ elif eta_g_exists:
431
+ if (eta_resid < eta_g * eta_slope + eta_offset):
432
+ if verbose == True:
433
+ print('Failed eta residual threshold')
434
+ return False
435
+
436
+ # outbase = 'microlens_fit_'
437
+ # data = self.make_bagle_data_dir(times, mags, errors)
438
+ # fitter = model_fitter.PSPL_Solver(data,
439
+ # model.PSPL_Phot_noPar_Param2,
440
+ # importance_nested_sampling=False,
441
+ # n_live_points=200,
442
+ # outputfiles_basename=outbase)
443
+
444
+ # fitter.priors['tE'] = model_fitter.make_gen(1, 400)
445
+ # #1 year before start and 1 years after end of data
446
+ # fitter.priors['t0'] = model_fitter.make_gen(self.np.min(times) - 365.25, self.np.max(times) + 365.25)
447
+ # fitter.priors['b_sff1'] = model_fitter.make_gen(0.001, 1.25)
448
+
449
+ # fitter.solve()
450
+
451
+ # fit_vals = fitter.get_best_fit(def_best='map')
452
+ # chi2_red_bagle = fitter.calc_chi2(params=fit_vals) / (npts - len(fit_vals))
453
+ # chi2_red_flat = self.calc_chi2_mean(times, mags, errors)
454
+
455
+ # # Delta chi^2 threshold
456
+ # delta_chi2_threshold = 100
457
+ # if self.np.abs(chi2_red_bagle - chi2_red_flat) < delta_chi2_threshold:
458
+ # if verbose == True:
459
+ # print('Failed BAGLE fit chi^2 threshold', 'delta chi^2 = ', self.np.abs(chi2_red_bagle - chi2_red_flat))
460
+ # return False
461
+
462
+ # fit_vals['b_sff'] = fit_vals.pop('b_sff1')
463
+ # fit_vals['mag_base'] = fit_vals.pop('mag_base1')
464
+ # for key in fit_vals.keys():
465
+ # locus.set_property('feature_microlensing_' + key, fit_vals[key])
466
+ # locus.set_property('feature_microlensing_chi2_red', chi2_red_bagle)
467
+
468
+ return True
469
+
470
+ def _run(self, locus):
471
+ import numpy as np
472
+ import astropy
473
+ from scipy.optimize import curve_fit
474
+ from scipy.stats import skew
475
+ from astropy.stats import sigma_clip
476
+
477
+ self.np = np
478
+ self.curve_fit = curve_fit
479
+ self.skew = skew
480
+ self.sigma_clip = sigma_clip
481
+
482
+
483
+ print('Processing Locus:', locus.locus_id)
484
+
485
+ with warnings.catch_warnings():
486
+ # The cast of locus.timeseries: astropy.table.Table to a pandas
487
+ # dataframe results in the conversion of some integer-valued
488
+ # columns to floating point represntation. This can result in a
489
+ # number of noisy warning so we will catch & ignore them for the
490
+ # next couple of lines.
491
+ warnings.simplefilter("ignore", astropy.table.TableReplaceWarning)
492
+ df = locus.timeseries().to_pandas()
493
+
494
+ data = df[['ant_mjd', 'ant_passband', 'ant_mag', 'ant_magerr']].dropna()
495
+
496
+ # Split into g-band and i-band
497
+ band_list = np.unique(data['ant_passband'])
498
+ for band in band_list: # 1 = g-band, 2 = i-band
499
+ band_data = data[data['ant_passband'] == band]
500
+ times, mags, errors = band_data['ant_mjd'].values, band_data['ant_mag'].values, band_data[
501
+ 'ant_magerr'].values
502
+
503
+ if self.is_microlensing_candidate(locus, times, mags, errors, verbose=False):
504
+ print(f'Locus {locus.locus_id} is a microlensing candidate in band {band}')
505
+ locus.tag('microlensing_candidate')
@@ -0,0 +1,50 @@
1
+ """Description
2
+ This filter finds locus located within a radius of 1.1 deg from the center of the Shadow Project's Virgo Pointings.
3
+ """
4
+ from antares_devkit.models import BaseFilter
5
+
6
+ class InShadowVirgo(BaseFilter):
7
+ NAME = 'in_shadow_virgo' #for this default filter, name is same as its tag
8
+
9
+ SLACK_CHANNEL = "#filter-in-shadow-virgo"
10
+
11
+ ## bare absolute minimum alert properties proving it's good to go
12
+ REQUIRED_ALERT_PROPERTIES = [
13
+ 'ant_ra',
14
+ 'ant_dec',
15
+ 'ant_survey'
16
+ ]
17
+
18
+ OUTPUT_TAGS = [
19
+ {
20
+ 'name': 'in_virgo_shadow',
21
+ 'description': 'Locus is along the line of sight to the Virgo fields covered by the DECam Shadow Project',
22
+ }
23
+ ]
24
+
25
+ def _run(self, locus):
26
+ import numpy as np
27
+ from astropy import units as u
28
+ from astropy.coordinates import SkyCoord
29
+
30
+ if locus.alert.properties['ant_survey'] == 2: #be sure we don't work with upper limits
31
+ return
32
+
33
+ ra = locus.alert.properties['ant_ra']
34
+ dec = locus.alert.properties['ant_dec']
35
+ #https://github.com/lsstshadow/shadowpointings/tree/main
36
+ shadow_virgo_ra = np.array([187.7014, 186.0754, 187.9486, 189.4263, 189.1219, 187.4805,
37
+ 185.9601, 187.6007, 186.0131, 184.8111, 184.3502, 183.9883,
38
+ 184.4658, 186.3005, 191.0718, 190.5631, 189.1516, 188.7788,
39
+ 186.6354, 185.6507, 182.4151, 193.2665, 189.5427, 190.6638,
40
+ 193.4287, 188.2634, 185.2524, 188.0598, 193.0875, 191.0943])
41
+ shadow_virgo_dec = np.array([12.3963, 13.4965, 14.307, 13.3041, 11.295, 10.4477,
42
+ 11.6469, 8.4389, 7.1655, 5.7578, 12.8014, 7.6415,
43
+ 14.6628, 15.364, 12.0456, 10.0351, 8.9448, 6.8992,
44
+ 5.3375, 8.9489, 13.7313, 8.2527, 4.8363, 2.8801,
45
+ 1.8786, 2.356, 3.7762, 0.1955, -1.1488, -0.0399])
46
+ field_centers = SkyCoord(ra=shadow_virgo_ra*u.deg, dec=shadow_virgo_dec*u.deg)
47
+ c = SkyCoord(ra*u.deg, dec*u.deg, frame='icrs')
48
+ sep = c.separation(field_centers)
49
+ if (sep.deg < 1.1).any(): #DECam FOV = 2.2 deg diameter
50
+ locus.tag('in_shadow_virgo')
@@ -1,10 +1,8 @@
1
- ## v2
1
+ ## production version
2
2
  from antares_devkit.models import BaseFilter
3
3
 
4
-
5
4
  class young_rubin_transients_soraisam(BaseFilter):
6
- ## MS: this is mainly for the NOIRLab end-to-end demonstration
7
-
5
+
8
6
  TRIGGERING_SURVEY = "lsst"
9
7
 
10
8
  SLACK_CHANNEL = "#filter-young-rubin-transients"
@@ -76,15 +74,19 @@ class young_rubin_transients_soraisam(BaseFilter):
76
74
  if 'lsst' in locus.properties['survey'].keys():
77
75
  if len(locus.properties['survey']['lsst']['dia_object_id']) > 1:
78
76
  C0 = True
77
+ # print ("C0", C0)
79
78
  if C0:
80
79
  return
80
+
81
81
 
82
-
83
- C1 = (locus.alert.properties['lsst_diaSource_reliability'] < 0.9)
82
+ C1 = (locus.alert.properties['lsst_diaSource_reliability'] < 0.8)
83
+ # print ("C1", C1, locus.alert.properties['lsst_diaSource_reliability'])
84
84
  if C1:
85
85
  return
86
+
86
87
 
87
88
  C2 = (locus.alert.properties['lsst_diaSource_ssObjectId'] != 0)
89
+ # print ("C2", C2)
88
90
  if C2:
89
91
  return
90
92
 
@@ -95,28 +97,30 @@ class young_rubin_transients_soraisam(BaseFilter):
95
97
  current_pb = locus.alert.properties['ant_passband']
96
98
 
97
99
  # ## confirm locus is extragalactic
100
+ C3 = True
98
101
  astro_objects = locus.catalog_objects
99
- # matching_catalog_names = astro_objects.keys()
100
- # # Below is a battle-tested set of extended source catalogs (Antares-based names)
101
- # xsc_cats = [
102
- # "2mass_xsc",
103
- # "ned",
104
- # "nyu_valueadded_gals",
105
- # "sdss_gals",
106
- # "veron_agn_qso",
107
- # "RC3",
108
- # ]
109
- # C3 = set(matching_catalog_names) & set(xsc_cats)
110
- # if not C3:
111
- # # print ("No match with extragalactic catalog")
112
- # return
113
-
102
+ matching_catalog_names = astro_objects.keys()
103
+ ps_cats = [
104
+ "bright_guide_star_cat", ## Classification = 0 star
105
+ #"gaia_dr3_gaia_source",
106
+ "sdss_stars", ## type = 6 star
107
+ "2mass_psc",
108
+ ]
109
+ if "2mass_psc" in matching_catalog_names:
110
+ C3 = False
111
+ elif "bright_guide_star_cat" in matching_catalog_names and astro_objects['bright_guide_star_cat'][0]['classification']==0:
112
+ C3 = False
113
+ elif "sdss_stars" in matching_catalog_names and astro_objects['sdss_stars'][0]['type']==6:
114
+ C3 = False
115
+ # print ("C3", C3)
116
+ if not C3:
117
+ return
114
118
 
115
119
 
116
- TS_all = locus.timeseries('all')
117
120
  TS_lsst = locus.timeseries('lsst')
118
121
 
119
- C4 = (len(TS_lsst) > 2) and (max(TS_lsst["ant_mjd"].data) - min(TS_lsst["ant_mjd"].data) < 30.0)
122
+ ## without upper limits info in alert packet, using a less strict time baseline
123
+ C4 = (len(TS_lsst) > 2) and (max(TS_lsst["ant_mjd"].data) - min(TS_lsst["ant_mjd"].data) < 15.0)
120
124
  if not C4:
121
125
  return
122
126
 
@@ -23,11 +23,12 @@ def get_filters_path():
23
23
  "HighSNR": "antares_devkit.filters.highsnr",
24
24
  "IF_anomaly_detection": "antares_devkit.filters.if_anomaly_detection", # ["numpy", "pandas", "sklearn", "astropy"]
25
25
  "IMBH": "antares_devkit.filters.imbh",
26
- "In_LSSTDDF": "antares_devkit.filters.lsst_ddf", #["astropy", "numpy"]
26
+ "In_LSSTDDF": "antares_devkit.filters.lsst_ddf", # ["astropy", "numpy"]
27
27
  "InM31": "antares_devkit.filters.inm31",
28
28
  "LAISS_RFC_AD_filter": "antares_devkit.filters.laiss_rfc_ad_filter", # ["astropy", "timeout_decorator", "alerce", "numpy", "pandas", "matplotlib", "astro_ghost", "sklearn", "scipy"]
29
29
  "AnomalyTransientDmDt": "antares_devkit.filters.malanchev_anomaly_transient_dmdt", # ["numpy", "astropy", "light_curve"]
30
30
  "Features": "antares_devkit.filters.malanchev_features_v1", # ["numpy", "astropy", "light_curve"]
31
+ "microlensing": "antares_devkit.filters.microlensing",
31
32
  "NovTest": "antares_devkit.filters.novtest",
32
33
  "NuclearTransient": "antares_devkit.filters.nuclear_transient",
33
34
  "NUTTelA_TAO": "antares_devkit.filters.nuttela_tao",
@@ -36,6 +37,7 @@ def get_filters_path():
36
37
  "RecentReddening": "antares_devkit.filters.recent_reddening",
37
38
  "refitt_newsources_snrcut": "antares_devkit.filters.refitt_newsources_snrcut",
38
39
  "SampleFilter": "antares_devkit.filters.sample_filter",
40
+ "InShadowVirgo": "antares_devkit.filters.shadow_virgo", # ["astropy", "numpy"]
39
41
  "siena_mag_coord_cut": "antares_devkit.filters.siena_mag_coord_cut",
40
42
  "SSOConfirmed": "antares_devkit.filters.sso",
41
43
  "sub_lum": "antares_devkit.filters.sub_lum",
@@ -48,6 +50,7 @@ def get_filters_path():
48
50
  "YoungExtragalacticCandidate": "antares_devkit.filters.young_extragalactic_candidate",
49
51
  "rubin_NovTest_soraisam": "antares_devkit.filters.rubin_NovTest_soraisam",
50
52
  "young_rubin_transients_soraisam": "antares_devkit.filters.young_rubin_transients_soraisam",
53
+ "LsstTransientQualityFilterScimma": "antares_devkit.filters.lsst_transient_quality_scimma",
51
54
  }
52
55
 
53
56
 
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
6
6
 
7
7
  [project]
8
8
  name = "antares-devkit"
9
- version = "0.6.3"
9
+ version = "0.6.5"
10
10
  description = "Provides tools and utility methods to write and debug ANTARES filters with Python."
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.9"
File without changes