rubin-sim 2.6.1.dev2__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.
Files changed (387) hide show
  1. batch/compile_prenight_metadata_cache.sh +34 -0
  2. batch/run_auxtel_prenight_sims.sh +163 -0
  3. batch/run_prenight_sims.sh +239 -0
  4. docs/.gitignore +3 -0
  5. docs/Makefile +32 -0
  6. docs/api.rst +25 -0
  7. docs/conf.py +8 -0
  8. docs/data-api.rst +12 -0
  9. docs/data-download.rst +70 -0
  10. docs/developer.rst +14 -0
  11. docs/documenteer.toml +11 -0
  12. docs/index.rst +22 -0
  13. docs/installation.rst +76 -0
  14. docs/introduction.rst +37 -0
  15. docs/maf-api-batches.rst +12 -0
  16. docs/maf-api-db.rst +12 -0
  17. docs/maf-api-maf-contrib.rst +12 -0
  18. docs/maf-api-maps.rst +12 -0
  19. docs/maf-api-metricbundles.rst +12 -0
  20. docs/maf-api-metrics.rst +12 -0
  21. docs/maf-api-plots.rst +12 -0
  22. docs/maf-api-run-comparison.rst +12 -0
  23. docs/maf-api-slicers.rst +12 -0
  24. docs/maf-api-stackers.rst +12 -0
  25. docs/maf-api-utils.rst +12 -0
  26. docs/maf-api.rst +22 -0
  27. docs/maf.rst +60 -0
  28. docs/metric_list.py +49 -0
  29. docs/moving-objects-api.rst +12 -0
  30. docs/moving-objects.rst +22 -0
  31. docs/phot-utils-api.rst +12 -0
  32. docs/phot-utils.rst +11 -0
  33. docs/prenight.rst +117 -0
  34. docs/protoarchive.rst +276 -0
  35. docs/satellite-constellations-api.rst +12 -0
  36. docs/satellite-constellations.rst +13 -0
  37. docs/selfcal-api.rst +12 -0
  38. docs/selfcal.rst +15 -0
  39. docs/sim-archive-api.rst +12 -0
  40. docs/skybrightness-api.rst +12 -0
  41. docs/skybrightness.rst +23 -0
  42. docs/user-guide.rst +28 -0
  43. docs/vseqarchive-architecture.rst +438 -0
  44. docs/vseqarchive-setup.rst +238 -0
  45. docs/vseqarchive.rst +225 -0
  46. rubin_sim/__init__.py +7 -0
  47. rubin_sim/data/__init__.py +1 -0
  48. rubin_sim/data/rs_download_data.py +155 -0
  49. rubin_sim/maf/__init__.py +34 -0
  50. rubin_sim/maf/batches/__init__.py +18 -0
  51. rubin_sim/maf/batches/altaz_batch.py +161 -0
  52. rubin_sim/maf/batches/col_map_dict.py +274 -0
  53. rubin_sim/maf/batches/common.py +388 -0
  54. rubin_sim/maf/batches/ddf_batch.py +728 -0
  55. rubin_sim/maf/batches/filterchange_batch.py +182 -0
  56. rubin_sim/maf/batches/glance_batch.py +591 -0
  57. rubin_sim/maf/batches/hourglass_batch.py +79 -0
  58. rubin_sim/maf/batches/info_batch.py +114 -0
  59. rubin_sim/maf/batches/metadata_batch.py +558 -0
  60. rubin_sim/maf/batches/moving_objects_batch.py +1587 -0
  61. rubin_sim/maf/batches/openshutter_batch.py +100 -0
  62. rubin_sim/maf/batches/radar_limited.py +1100 -0
  63. rubin_sim/maf/batches/science_radar_batch.py +1910 -0
  64. rubin_sim/maf/batches/skycoverage.py +155 -0
  65. rubin_sim/maf/batches/slew_batch.py +146 -0
  66. rubin_sim/maf/batches/srd_batch.py +637 -0
  67. rubin_sim/maf/batches/time_batch.py +757 -0
  68. rubin_sim/maf/batches/time_sci_batch.py +105 -0
  69. rubin_sim/maf/batches/visitdepth_batch.py +485 -0
  70. rubin_sim/maf/db/__init__.py +2 -0
  71. rubin_sim/maf/db/add_run.py +34 -0
  72. rubin_sim/maf/db/results_db.py +1074 -0
  73. rubin_sim/maf/db/tracking_db.py +332 -0
  74. rubin_sim/maf/ddf_dir.py +53 -0
  75. rubin_sim/maf/generate_ss.py +218 -0
  76. rubin_sim/maf/glance_dir.py +58 -0
  77. rubin_sim/maf/maf_contrib/__init__.py +25 -0
  78. rubin_sim/maf/maf_contrib/calculate_lsst_field_visibility_astropy.py +170 -0
  79. rubin_sim/maf/maf_contrib/depth_limited_num_gal_metric.py +109 -0
  80. rubin_sim/maf/maf_contrib/example_new_metrics.py +40 -0
  81. rubin_sim/maf/maf_contrib/filter_pair_t_gaps_metric.py +208 -0
  82. rubin_sim/maf/maf_contrib/grb_transient_metric.py +249 -0
  83. rubin_sim/maf/maf_contrib/gw170817_det_metric.py +72 -0
  84. rubin_sim/maf/maf_contrib/intervals_between_obs_metric.py +68 -0
  85. rubin_sim/maf/maf_contrib/kne_metrics.py +473 -0
  86. rubin_sim/maf/maf_contrib/lss_metrics.py +45 -0
  87. rubin_sim/maf/maf_contrib/lss_obs_strategy/__init__.py +3 -0
  88. rubin_sim/maf/maf_contrib/lss_obs_strategy/constants_for_pipeline.py +73 -0
  89. rubin_sim/maf/maf_contrib/lss_obs_strategy/galaxy_counts_metric_extended.py +205 -0
  90. rubin_sim/maf/maf_contrib/lss_obs_strategy/galaxy_counts_with_pixel_calibration.py +171 -0
  91. rubin_sim/maf/maf_contrib/lv_dwarfs/__init__.py +1 -0
  92. rubin_sim/maf/maf_contrib/lv_dwarfs/lv_dwarfs_metrics.py +388 -0
  93. rubin_sim/maf/maf_contrib/microlensing_metric.py +593 -0
  94. rubin_sim/maf/maf_contrib/num_obs_in_survey_time_overlap_metric.py +43 -0
  95. rubin_sim/maf/maf_contrib/periodic_metric.py +51 -0
  96. rubin_sim/maf/maf_contrib/periodic_star_metric.py +186 -0
  97. rubin_sim/maf/maf_contrib/periodic_star_modulation_metric.py +236 -0
  98. rubin_sim/maf/maf_contrib/presto_color_kne_pop_metric.py +441 -0
  99. rubin_sim/maf/maf_contrib/selfcal_uniformity_metric.py +171 -0
  100. rubin_sim/maf/maf_contrib/star_count_mass_metric.py +64 -0
  101. rubin_sim/maf/maf_contrib/star_count_metric.py +56 -0
  102. rubin_sim/maf/maf_contrib/star_counts/__init__.py +8 -0
  103. rubin_sim/maf/maf_contrib/star_counts/abs_mag.py +324 -0
  104. rubin_sim/maf/maf_contrib/star_counts/coords.py +99 -0
  105. rubin_sim/maf/maf_contrib/star_counts/readme.txt +1 -0
  106. rubin_sim/maf/maf_contrib/star_counts/spec_type.py +91 -0
  107. rubin_sim/maf/maf_contrib/star_counts/starcount.py +30 -0
  108. rubin_sim/maf/maf_contrib/star_counts/starcount_bymass.py +93 -0
  109. rubin_sim/maf/maf_contrib/star_counts/stellardensity.py +59 -0
  110. rubin_sim/maf/maf_contrib/static_probes_fom_summary_metric.py +386 -0
  111. rubin_sim/maf/maf_contrib/tdes_pop_metric.py +362 -0
  112. rubin_sim/maf/maf_contrib/transient_ascii_sed_metric.py +652 -0
  113. rubin_sim/maf/maf_contrib/triplet_metric.py +156 -0
  114. rubin_sim/maf/maf_contrib/var_depth_metric.py +157 -0
  115. rubin_sim/maf/maf_contrib/xrb_metrics.py +523 -0
  116. rubin_sim/maf/maf_contrib/young_stellar_objects_metric.py +200 -0
  117. rubin_sim/maf/maf_night_report.py +200 -0
  118. rubin_sim/maf/make_fbs_tracking_db.py +90 -0
  119. rubin_sim/maf/maps/__init__.py +8 -0
  120. rubin_sim/maf/maps/base_map.py +72 -0
  121. rubin_sim/maf/maps/create_gaia_density_map.py +158 -0
  122. rubin_sim/maf/maps/dust_map.py +66 -0
  123. rubin_sim/maf/maps/dust_map_3d.py +190 -0
  124. rubin_sim/maf/maps/ebv_3d_hp.py +170 -0
  125. rubin_sim/maf/maps/ebv_hp.py +64 -0
  126. rubin_sim/maf/maps/gal_coords_map.py +22 -0
  127. rubin_sim/maf/maps/galactic_plane_priority_maps.py +210 -0
  128. rubin_sim/maf/maps/stellar_density_map.py +73 -0
  129. rubin_sim/maf/maps/trilegal_map.py +82 -0
  130. rubin_sim/maf/metadata_dir.py +90 -0
  131. rubin_sim/maf/metric_bundles/__init__.py +3 -0
  132. rubin_sim/maf/metric_bundles/metric_bundle.py +816 -0
  133. rubin_sim/maf/metric_bundles/metric_bundle_group.py +834 -0
  134. rubin_sim/maf/metric_bundles/mo_metric_bundle.py +646 -0
  135. rubin_sim/maf/metrics/__init__.py +52 -0
  136. rubin_sim/maf/metrics/agn_time_lag_metric.py +118 -0
  137. rubin_sim/maf/metrics/agnstructure.py +141 -0
  138. rubin_sim/maf/metrics/area_summary_metrics.py +120 -0
  139. rubin_sim/maf/metrics/base_metric.py +215 -0
  140. rubin_sim/maf/metrics/brown_dwarf_metric.py +162 -0
  141. rubin_sim/maf/metrics/cadence_metrics.py +400 -0
  142. rubin_sim/maf/metrics/calibration_metrics.py +618 -0
  143. rubin_sim/maf/metrics/chip_vendor_metric.py +43 -0
  144. rubin_sim/maf/metrics/color_slope_metrics.py +189 -0
  145. rubin_sim/maf/metrics/cosmology_summary_metrics.py +1212 -0
  146. rubin_sim/maf/metrics/coverage_metric.py +48 -0
  147. rubin_sim/maf/metrics/crowding_metric.py +255 -0
  148. rubin_sim/maf/metrics/cumulative_metric.py +46 -0
  149. rubin_sim/maf/metrics/dcr_metric.py +93 -0
  150. rubin_sim/maf/metrics/exgal_m5.py +57 -0
  151. rubin_sim/maf/metrics/fft_metric.py +27 -0
  152. rubin_sim/maf/metrics/galactic_plane_metrics.py +375 -0
  153. rubin_sim/maf/metrics/galplane_time_sampling_metrics.py +236 -0
  154. rubin_sim/maf/metrics/hourglass_metric.py +130 -0
  155. rubin_sim/maf/metrics/incremental_template_metric.py +255 -0
  156. rubin_sim/maf/metrics/kuiper_metrics.py +26 -0
  157. rubin_sim/maf/metrics/mo_metrics.py +1496 -0
  158. rubin_sim/maf/metrics/mo_summary_metrics.py +464 -0
  159. rubin_sim/maf/metrics/night_pointing_metric.py +70 -0
  160. rubin_sim/maf/metrics/optimal_m5_metric.py +94 -0
  161. rubin_sim/maf/metrics/pair_metric.py +69 -0
  162. rubin_sim/maf/metrics/periodic_detect_metric.py +133 -0
  163. rubin_sim/maf/metrics/phase_gap_metric.py +172 -0
  164. rubin_sim/maf/metrics/qso_number_counts_metric.py +138 -0
  165. rubin_sim/maf/metrics/scaling_metrics.py +148 -0
  166. rubin_sim/maf/metrics/schedview_metrics.py +39 -0
  167. rubin_sim/maf/metrics/season_metrics.py +344 -0
  168. rubin_sim/maf/metrics/simple_metrics.py +546 -0
  169. rubin_sim/maf/metrics/sky_sat_metric.py +18 -0
  170. rubin_sim/maf/metrics/sn_cadence_metric.py +104 -0
  171. rubin_sim/maf/metrics/sn_n_sn_metric.py +1364 -0
  172. rubin_sim/maf/metrics/sn_sl_metric.py +216 -0
  173. rubin_sim/maf/metrics/sn_snr_metric.py +715 -0
  174. rubin_sim/maf/metrics/snr_weight.py +24 -0
  175. rubin_sim/maf/metrics/star_density.py +77 -0
  176. rubin_sim/maf/metrics/string_count_metric.py +95 -0
  177. rubin_sim/maf/metrics/summary_metrics.py +201 -0
  178. rubin_sim/maf/metrics/surfb_metric.py +175 -0
  179. rubin_sim/maf/metrics/technical_metrics.py +271 -0
  180. rubin_sim/maf/metrics/tgaps.py +412 -0
  181. rubin_sim/maf/metrics/tomography_models.py +2034 -0
  182. rubin_sim/maf/metrics/transient_metrics.py +268 -0
  183. rubin_sim/maf/metrics/uniformity_metrics.py +418 -0
  184. rubin_sim/maf/metrics/use_metrics.py +55 -0
  185. rubin_sim/maf/metrics/vector_metrics.py +190 -0
  186. rubin_sim/maf/metrics/visit_groups_metric.py +341 -0
  187. rubin_sim/maf/metrics/weak_lensing_systematics_metric.py +210 -0
  188. rubin_sim/maf/plots/__init__.py +14 -0
  189. rubin_sim/maf/plots/hg_plotters.py +1117 -0
  190. rubin_sim/maf/plots/hourglass_plotters.py +120 -0
  191. rubin_sim/maf/plots/mo_plotters.py +358 -0
  192. rubin_sim/maf/plots/nd_plotters.py +220 -0
  193. rubin_sim/maf/plots/neo_distance_plotter.py +140 -0
  194. rubin_sim/maf/plots/night_pointing_plotter.py +96 -0
  195. rubin_sim/maf/plots/oned_plotters.py +114 -0
  196. rubin_sim/maf/plots/perceptual_rainbow.py +30 -0
  197. rubin_sim/maf/plots/plot_handler.py +766 -0
  198. rubin_sim/maf/plots/skyproj_plotters.py +540 -0
  199. rubin_sim/maf/plots/spatial_plotters.py +1029 -0
  200. rubin_sim/maf/plots/special_plotters.py +256 -0
  201. rubin_sim/maf/plots/two_d_plotters.py +177 -0
  202. rubin_sim/maf/plots/xyplotter.py +37 -0
  203. rubin_sim/maf/run_comparison/__init__.py +5 -0
  204. rubin_sim/maf/run_comparison/archive.py +584 -0
  205. rubin_sim/maf/run_comparison/gather_summaries.py +125 -0
  206. rubin_sim/maf/run_comparison/microlensing_compare.py +514 -0
  207. rubin_sim/maf/run_comparison/radar_plot.py +202 -0
  208. rubin_sim/maf/run_comparison/summary_plots.py +710 -0
  209. rubin_sim/maf/run_moving_calc.py +239 -0
  210. rubin_sim/maf/run_moving_fractions.py +128 -0
  211. rubin_sim/maf/run_moving_join.py +103 -0
  212. rubin_sim/maf/run_selfcal_metric.py +92 -0
  213. rubin_sim/maf/scimaf_dir.py +87 -0
  214. rubin_sim/maf/show_maf.py +227 -0
  215. rubin_sim/maf/slicers/__init__.py +13 -0
  216. rubin_sim/maf/slicers/base_slicer.py +513 -0
  217. rubin_sim/maf/slicers/base_spatial_slicer.py +202 -0
  218. rubin_sim/maf/slicers/healpix_sdss_slicer.py +118 -0
  219. rubin_sim/maf/slicers/healpix_slicer.py +189 -0
  220. rubin_sim/maf/slicers/healpix_subset_slicer.py +293 -0
  221. rubin_sim/maf/slicers/hourglass_slicer.py +37 -0
  222. rubin_sim/maf/slicers/mo_slicer.py +171 -0
  223. rubin_sim/maf/slicers/movie_slicer.py +288 -0
  224. rubin_sim/maf/slicers/nd_slicer.py +147 -0
  225. rubin_sim/maf/slicers/one_d_slicer.py +210 -0
  226. rubin_sim/maf/slicers/time_interval_slicers.py +283 -0
  227. rubin_sim/maf/slicers/uni_slicer.py +57 -0
  228. rubin_sim/maf/slicers/user_points_slicer.py +141 -0
  229. rubin_sim/maf/stackers/__init__.py +13 -0
  230. rubin_sim/maf/stackers/base_stacker.py +221 -0
  231. rubin_sim/maf/stackers/coord_stackers.py +224 -0
  232. rubin_sim/maf/stackers/date_stackers.py +192 -0
  233. rubin_sim/maf/stackers/general_stackers.py +683 -0
  234. rubin_sim/maf/stackers/get_col_info.py +104 -0
  235. rubin_sim/maf/stackers/label_stackers.py +114 -0
  236. rubin_sim/maf/stackers/m5_optimal_stacker.py +126 -0
  237. rubin_sim/maf/stackers/mo_phase.py +265 -0
  238. rubin_sim/maf/stackers/mo_stackers.py +486 -0
  239. rubin_sim/maf/stackers/n_follow_stacker.py +187 -0
  240. rubin_sim/maf/stackers/neo_dist_stacker.py +126 -0
  241. rubin_sim/maf/stackers/sdss_stackers.py +30 -0
  242. rubin_sim/maf/stackers/sn_stacker.py +152 -0
  243. rubin_sim/maf/stackers/teff_stacker.py +154 -0
  244. rubin_sim/maf/utils/__init__.py +6 -0
  245. rubin_sim/maf/utils/astrometry_utils.py +93 -0
  246. rubin_sim/maf/utils/generate_fov_map.py +58 -0
  247. rubin_sim/maf/utils/get_date_version.py +28 -0
  248. rubin_sim/maf/utils/maf_utils.py +212 -0
  249. rubin_sim/maf/utils/opsim_utils.py +259 -0
  250. rubin_sim/maf/utils/output_utils.py +34 -0
  251. rubin_sim/maf/utils/sn_n_sn_utils.py +894 -0
  252. rubin_sim/maf/utils/sn_utils.py +369 -0
  253. rubin_sim/maf/utils/stellar_mags.py +197 -0
  254. rubin_sim/maf/web/__init__.py +2 -0
  255. rubin_sim/maf/web/favicon.ico +0 -0
  256. rubin_sim/maf/web/maf_run_results.py +645 -0
  257. rubin_sim/maf/web/maf_tracking.py +139 -0
  258. rubin_sim/maf/web/sorttable.js +496 -0
  259. rubin_sim/maf/web/templates/allmetricresults.html +72 -0
  260. rubin_sim/maf/web/templates/configs.html +28 -0
  261. rubin_sim/maf/web/templates/macros.html +243 -0
  262. rubin_sim/maf/web/templates/master.html +294 -0
  263. rubin_sim/maf/web/templates/metricselect.html +91 -0
  264. rubin_sim/maf/web/templates/multicolor.html +147 -0
  265. rubin_sim/maf/web/templates/results.html +82 -0
  266. rubin_sim/maf/web/templates/runselect.html +58 -0
  267. rubin_sim/maf/web/templates/stats.html +50 -0
  268. rubin_sim/moving_objects/__init__.py +8 -0
  269. rubin_sim/moving_objects/base_obs.py +468 -0
  270. rubin_sim/moving_objects/cheby_fits.py +689 -0
  271. rubin_sim/moving_objects/cheby_values.py +231 -0
  272. rubin_sim/moving_objects/chebyshev_utils.py +365 -0
  273. rubin_sim/moving_objects/direct_obs.py +376 -0
  274. rubin_sim/moving_objects/make_lsst_obs.py +282 -0
  275. rubin_sim/moving_objects/ooephemerides.py +643 -0
  276. rubin_sim/moving_objects/orbits.py +507 -0
  277. rubin_sim/moving_objects/pre_generate.py +37 -0
  278. rubin_sim/moving_objects/utils.py +61 -0
  279. rubin_sim/phot_utils/__init__.py +7 -0
  280. rubin_sim/phot_utils/bandpass.py +425 -0
  281. rubin_sim/phot_utils/photometric_parameters.py +449 -0
  282. rubin_sim/phot_utils/physical_parameters.py +50 -0
  283. rubin_sim/phot_utils/predicted_zeropoints.py +245 -0
  284. rubin_sim/phot_utils/sed.py +1773 -0
  285. rubin_sim/phot_utils/sed_utils.py +52 -0
  286. rubin_sim/phot_utils/signaltonoise.py +695 -0
  287. rubin_sim/phot_utils/spectral_resampling.py +161 -0
  288. rubin_sim/satellite_constellations/__init__.py +3 -0
  289. rubin_sim/satellite_constellations/basis_function.py +40 -0
  290. rubin_sim/satellite_constellations/model_observatory.py +157 -0
  291. rubin_sim/satellite_constellations/sat_utils.py +488 -0
  292. rubin_sim/selfcal/__init__.py +4 -0
  293. rubin_sim/selfcal/generate_catalog.py +188 -0
  294. rubin_sim/selfcal/offsets.py +119 -0
  295. rubin_sim/selfcal/solver.py +143 -0
  296. rubin_sim/selfcal/star_tools.py +42 -0
  297. rubin_sim/sim_archive/__init__.py +14 -0
  298. rubin_sim/sim_archive/conda_packages.py +65 -0
  299. rubin_sim/sim_archive/future_vsarch.py +480 -0
  300. rubin_sim/sim_archive/make_snapshot.py +255 -0
  301. rubin_sim/sim_archive/prenight.py +216 -0
  302. rubin_sim/sim_archive/prenightindex.py +229 -0
  303. rubin_sim/sim_archive/prototype.py +1029 -0
  304. rubin_sim/sim_archive/sim_archive.py +680 -0
  305. rubin_sim/sim_archive/tempdb.py +56 -0
  306. rubin_sim/sim_archive/util.py +146 -0
  307. rubin_sim/sim_archive/vseqarchive.py +965 -0
  308. rubin_sim/sim_archive/vseqmetadata.py +1721 -0
  309. rubin_sim/skybrightness/__init__.py +5 -0
  310. rubin_sim/skybrightness/allsky_db.py +93 -0
  311. rubin_sim/skybrightness/data/ESO_Spectra/eso_tools.py +505 -0
  312. rubin_sim/skybrightness/data/solarSpec/package.py +22 -0
  313. rubin_sim/skybrightness/generate_hdf5.py +243 -0
  314. rubin_sim/skybrightness/interp_components.py +798 -0
  315. rubin_sim/skybrightness/sky_model.py +771 -0
  316. rubin_sim/skybrightness/twilight_func.py +83 -0
  317. rubin_sim/skybrightness/utils.py +91 -0
  318. rubin_sim-2.6.1.dev2.dist-info/METADATA +95 -0
  319. rubin_sim-2.6.1.dev2.dist-info/RECORD +387 -0
  320. rubin_sim-2.6.1.dev2.dist-info/WHEEL +5 -0
  321. rubin_sim-2.6.1.dev2.dist-info/entry_points.txt +24 -0
  322. rubin_sim-2.6.1.dev2.dist-info/licenses/LICENSE +674 -0
  323. rubin_sim-2.6.1.dev2.dist-info/top_level.txt +5 -0
  324. sql/make_vseqmeta.sql +251 -0
  325. tests/data/test_data.py +26 -0
  326. tests/data/test_ddf_grid.py +17 -0
  327. tests/maf/test_3x2fom.py +68 -0
  328. tests/maf/test_archive.py +162 -0
  329. tests/maf/test_basemetrics.py +99 -0
  330. tests/maf/test_batchcommon.py +36 -0
  331. tests/maf/test_batches.py +159 -0
  332. tests/maf/test_cadencemetrics.py +388 -0
  333. tests/maf/test_calibrationmetrics.py +242 -0
  334. tests/maf/test_color_slopes.py +71 -0
  335. tests/maf/test_cosmology_summaries.py +20 -0
  336. tests/maf/test_gathersummaries.py +48 -0
  337. tests/maf/test_healpixslicer.py +439 -0
  338. tests/maf/test_healpixsubsetslicer.py +273 -0
  339. tests/maf/test_hgplotters.py +145 -0
  340. tests/maf/test_hourglassmetric.py +33 -0
  341. tests/maf/test_io.py +187 -0
  342. tests/maf/test_json.py +169 -0
  343. tests/maf/test_maps.py +199 -0
  344. tests/maf/test_metricbundle.py +69 -0
  345. tests/maf/test_mometrics.py +422 -0
  346. tests/maf/test_movieslicer.py +216 -0
  347. tests/maf/test_ndslicer.py +240 -0
  348. tests/maf/test_neodistanceplotter.py +41 -0
  349. tests/maf/test_onedslicer.py +245 -0
  350. tests/maf/test_opsimutils.py +80 -0
  351. tests/maf/test_plotters.py +134 -0
  352. tests/maf/test_resultsdb.py +145 -0
  353. tests/maf/test_schedviewmetrics.py +14 -0
  354. tests/maf/test_simplemetrics.py +200 -0
  355. tests/maf/test_snmetrics.py +335 -0
  356. tests/maf/test_snmetrics_nsn.py +123 -0
  357. tests/maf/test_stackers.py +423 -0
  358. tests/maf/test_stellarmags.py +50 -0
  359. tests/maf/test_stringcount.py +26 -0
  360. tests/maf/test_summary_plots.py +197 -0
  361. tests/maf/test_summarymetrics.py +82 -0
  362. tests/maf/test_technicalmetrics.py +267 -0
  363. tests/maf/test_templatemetrics.py +86 -0
  364. tests/maf/test_timeintervalslicers.py +97 -0
  365. tests/maf/test_trackingdb.py +88 -0
  366. tests/maf/test_unislicer.py +116 -0
  367. tests/maf/test_vectormetrics.py +225 -0
  368. tests/maf/test_visitgroupsmetric.py +181 -0
  369. tests/moving_objects/test_camera.py +39 -0
  370. tests/moving_objects/test_chebyfits.py +188 -0
  371. tests/moving_objects/test_chebyshevutils.py +76 -0
  372. tests/moving_objects/test_chebyvalues.py +245 -0
  373. tests/moving_objects/test_ephemerides.py +219 -0
  374. tests/moving_objects/test_orbits.py +170 -0
  375. tests/moving_objects/test_sed_data.py +61 -0
  376. tests/phot_utils/test_approximatebandpasses.py +91 -0
  377. tests/phot_utils/test_photometricparameters.py +268 -0
  378. tests/phot_utils/test_photometry.py +61 -0
  379. tests/phot_utils/test_predicted_zeropoints.py +53 -0
  380. tests/phot_utils/test_read_bandpasses.py +29 -0
  381. tests/phot_utils/test_sed.py +368 -0
  382. tests/phot_utils/test_snr.py +370 -0
  383. tests/satellite_constellations/test_satellites.py +36 -0
  384. tests/sim_archive/test_make_snapshot.py +24 -0
  385. tests/sim_archive/test_prototype_sim_archive.py +204 -0
  386. tests/sim_archive/test_vseqarchive.py +947 -0
  387. tests/skybrightness/test_skymodel.py +359 -0
@@ -0,0 +1,1496 @@
1
+ __all__ = (
2
+ "BaseMoMetric",
3
+ "NObsMetric",
4
+ "NObsNoSinglesMetric",
5
+ "NNightsMetric",
6
+ "ObsArcMetric",
7
+ "DiscoveryMetric",
8
+ "DiscoveryNChancesMetric",
9
+ "DiscoveryNObsMetric",
10
+ "DiscoveryTimeMetric",
11
+ "DiscoveryDistanceMetric",
12
+ "DiscoveryRadecMetric",
13
+ "DiscoveryEclonlatMetric",
14
+ "DiscoveryVelocityMetric",
15
+ "ActivityOverTimeMetric",
16
+ "ActivityOverPeriodMetric",
17
+ "MagicDiscoveryMetric",
18
+ "HighVelocityMetric",
19
+ "HighVelocityNightsMetric",
20
+ "LightcurveInversionAsteroidMetric",
21
+ "ColorAsteroidMetric",
22
+ "InstantaneousColorMetric",
23
+ "LightcurveColorOuterMetric",
24
+ "PeakVMagMetric",
25
+ "KnownObjectsMetric",
26
+ )
27
+
28
+ import numpy as np
29
+
30
+ from .base_metric import BaseMetric
31
+
32
+
33
+ def _set_vis(sso_obs, snr_limit, snr_col, vis_col):
34
+ if snr_limit is not None:
35
+ vis = np.where(sso_obs[snr_col] >= snr_limit)[0]
36
+ else:
37
+ vis = np.where(sso_obs[vis_col] > 0)[0]
38
+ return vis
39
+
40
+
41
+ class BaseMoMetric(BaseMetric):
42
+ """Base class for the moving object metrics.
43
+ Intended to be used with the Moving Object Slicer.
44
+
45
+ Parameters
46
+ ----------
47
+ cols : `list` [`str`] or None
48
+ List of the column names needed to run the metric.
49
+ These columns must be in the moving object data files.
50
+ metric_name : `str` or None
51
+ Name of the metric.
52
+ If None, a name is created based on the class name.
53
+ units : `str`, opt
54
+ Units for the resulting metric values.
55
+ badval : `float`, opt
56
+ Flag "bad" value returned if the metric cannot be calculated.
57
+ comment : `str` or None, opt
58
+ A default comment to use for the DisplayDict (display caption)
59
+ if no value is provided elsewhere.
60
+ child_metrics : `list` [`~BaseChildMetric`] or None, opt
61
+ A list of child metrics to run on the results of (this) metric.
62
+ Child metrics take the metric results from this metric and
63
+ add some additional processing or pull out a particular value.
64
+ The results of the child metric are passed to a new MoMetricBundle.
65
+ app_mag_col : `str`, opt
66
+ Name of the apparent magnitude column
67
+ in the object observations. Typically added by a stacker.
68
+ app_mag_v_col : `str`, opt
69
+ Name of the apparent magnitude V band column
70
+ in the objects observations.
71
+ m5_col : `str`, opt
72
+ Name of the m5 limiting magnitude column
73
+ in the objects observations.
74
+ night_col : `str`, opt
75
+ Name of the night column in the objects observations.
76
+ mjd_col : `str`, opt
77
+ Name of the MJD column in the objects observations.
78
+ snr_col : `str`, opt
79
+ Name of the column describing the SNR of this object in a given
80
+ observation, in the objects observations. Added by a stacker.
81
+ vis_col : `str`, opt
82
+ Name of the column describing the probability of detecting
83
+ this object in a given observation. Added by a stacker.
84
+ ra_col : `str`, opt
85
+ Name of the column describing the RA of this object
86
+ in the objects observations.
87
+ dec_col : `str`, opt
88
+ Name of the column describing the Declination of this object
89
+ in the objects observations.
90
+ seeing_col : `str`, opt
91
+ Name of the column describing the seeing to be used in
92
+ evaluations of this object, in the objects observations.
93
+ Tpyically this is the geometric seeing, for evaluating streak length.
94
+ exp_time_col : `str`, opt
95
+ Name of the exposure time column in the objects observations.
96
+ filter_col : `str`, opt
97
+ Name of the column describing the filter used for a given observation,
98
+ in the objects observations.
99
+ """
100
+
101
+ def __init__(
102
+ self,
103
+ cols=None,
104
+ metric_name=None,
105
+ units="#",
106
+ badval=0,
107
+ comment=None,
108
+ child_metrics=None,
109
+ app_mag_col="appMag",
110
+ app_mag_v_col="appMagV",
111
+ m5_col="fiveSigmaDepth",
112
+ night_col="night",
113
+ mjd_col="observationStartMJD",
114
+ snr_col="SNR",
115
+ vis_col="vis",
116
+ ra_col="ra",
117
+ dec_col="dec",
118
+ seeing_col="seeingFwhmGeom",
119
+ exp_time_col="visitExposureTime",
120
+ filter_col="band",
121
+ ):
122
+ # Set metric name.
123
+ self.name = metric_name
124
+ if self.name is None:
125
+ self.name = self.__class__.__name__.replace("Metric", "", 1)
126
+ # Set badval and units, leave space for 'comment'
127
+ # (tied to display_dict).
128
+ self.badval = badval
129
+ self.units = units
130
+ self.comment = comment
131
+ # Set some commonly used column names.
132
+ self.m5_col = m5_col
133
+ self.app_mag_col = app_mag_col
134
+ self.app_mag_v_col = app_mag_v_col
135
+ self.night_col = night_col
136
+ self.mjd_col = mjd_col
137
+ self.snr_col = snr_col
138
+ self.vis_col = vis_col
139
+ self.ra_col = ra_col
140
+ self.dec_col = dec_col
141
+ self.seeing_col = seeing_col
142
+ self.exp_time_col = exp_time_col
143
+ self.filter_col = filter_col
144
+ self.cols_req = [
145
+ self.app_mag_col,
146
+ self.m5_col,
147
+ self.night_col,
148
+ self.mjd_col,
149
+ self.snr_col,
150
+ self.vis_col,
151
+ ]
152
+ if cols is not None:
153
+ for col in cols:
154
+ self.cols_req.append(col)
155
+
156
+ if child_metrics is None:
157
+ try:
158
+ if not isinstance(self.child_metrics, dict):
159
+ raise ValueError("self.child_metrics must be a dictionary (possibly empty)")
160
+ except AttributeError:
161
+ self.child_metrics = {}
162
+ self.metric_dtype = "float"
163
+ else:
164
+ if not isinstance(child_metrics, dict):
165
+ raise ValueError("childmetrics must be provided as a dictionary.")
166
+ self.child_metrics = child_metrics
167
+ self.metric_dtype = "object"
168
+ self.shape = 1
169
+
170
+ def run(self, sso_obs, orb, hval):
171
+ """Calculate the metric value.
172
+
173
+ Parameters
174
+ ----------
175
+ sso_obs : `np.ndarray`, (N,)
176
+ The input data to the metric (same as the parent metric).
177
+ orb : `np.ndarray`, (N,)
178
+ The information about the orbit for which the metric is
179
+ being calculated.
180
+ hval : `float`
181
+ The H value for which the metric is being calculated.
182
+
183
+ Returns
184
+ -------
185
+ metric_val : `float` or `np.ndarray` or `dict`
186
+ """
187
+ raise NotImplementedError
188
+
189
+
190
+ class BaseChildMetric(BaseMoMetric):
191
+ """Base class for child metrics.
192
+
193
+ Parameters
194
+ ----------
195
+ parentDiscoveryMetric : `~BaseMoMetric`
196
+ The 'parent' metric which generated the metric data used
197
+ calculate this 'child' metric.
198
+ badval : `float`, opt
199
+ Value to return when metric cannot be calculated.
200
+ """
201
+
202
+ def __init__(self, parent_discovery_metric, badval=0, **kwargs):
203
+ super().__init__(badval=badval, **kwargs)
204
+ self.parent_metric = parent_discovery_metric
205
+ self.child_metrics = {}
206
+ if "metricDtype" in kwargs:
207
+ self.metric_dtype = kwargs["metricDtype"]
208
+ else:
209
+ self.metric_dtype = "float"
210
+
211
+ def run(self, sso_obs, orb, hval, metric_values):
212
+ """Calculate the child metric value.
213
+
214
+ Parameters
215
+ ----------
216
+ sso_obs : `np.ndarray`, (N,)
217
+ The input data to the metric (same as the parent metric).
218
+ orb : `np.ndarray`, (N,)
219
+ The information about the orbit for which the metric is
220
+ being calculated.
221
+ hval : `float`
222
+ The H value for which the metric is being calculated.
223
+ metric_values : `dict` or `np.ndarray`, (N,)
224
+ The return value from the parent metric.
225
+
226
+ Returns
227
+ -------
228
+ metric_val : `float`
229
+ """
230
+ raise NotImplementedError
231
+
232
+
233
+ class NObsMetric(BaseMoMetric):
234
+ """
235
+ Count the total number of observations where an SSobject was 'visible'.
236
+
237
+ Parameters
238
+ ----------
239
+ snr_limit : `float` or None
240
+ If the snr_limit is None, detection of the object in a visit is
241
+ determined using the _calcVis method (completeness calculation).
242
+ If not None, the snr is calculated and used as a flat cutoff instead.
243
+ """
244
+
245
+ def __init__(self, snr_limit=None, **kwargs):
246
+ super().__init__(**kwargs)
247
+ self.snr_limit = snr_limit
248
+
249
+ def run(self, sso_obs, orb, hval):
250
+ if self.snr_limit is not None:
251
+ vis = np.where(sso_obs[self.snr_col] >= self.snr_limit)[0]
252
+ return vis.size
253
+ else:
254
+ vis = np.where(sso_obs[self.vis_col] > 0)[0]
255
+ return vis.size
256
+
257
+
258
+ class NObsNoSinglesMetric(BaseMoMetric):
259
+ """Count the number of observations for an SSobject, without singles.
260
+ Don't include observations where it was a single observation on a night.
261
+
262
+ Parameters
263
+ ----------
264
+ snr_limit : `float` or None
265
+ If the snr_limit is None, detection of the object in a visit is
266
+ determined using the _calcVis method (completeness calculation).
267
+ If not None, the snr is calculated and used as a flat cutoff instead.
268
+ """
269
+
270
+ def __init__(self, snr_limit=None, **kwargs):
271
+ super().__init__(**kwargs)
272
+ self.snr_limit = snr_limit
273
+
274
+ def run(self, sso_obs, orb, hval):
275
+ vis = _set_vis(sso_obs, self.snr_limit, self.snr_col, self.vis_col)
276
+ if len(vis) == 0:
277
+ return 0
278
+ nights = sso_obs[self.night_col][vis]
279
+ nights = nights.astype("int")
280
+ ncounts = np.bincount(nights)
281
+ nobs = ncounts[np.where(ncounts > 1)].sum()
282
+ return nobs
283
+
284
+
285
+ class NNightsMetric(BaseMoMetric):
286
+ """Count the number of distinct nights an SSobject is observed.
287
+
288
+ Parameters
289
+ ----------
290
+ snr_limit : `float` or None
291
+ If the snr_limit is None, detection of the object in a visit is
292
+ determined using the _calcVis method (completeness calculation).
293
+ If not None, the snr is calculated and used as a flat cutoff instead.
294
+ """
295
+
296
+ def __init__(self, snr_limit=None, **kwargs):
297
+ super().__init__(**kwargs)
298
+ self.snr_limit = snr_limit
299
+
300
+ def run(self, sso_obs, orb, hval):
301
+ vis = _set_vis(sso_obs, self.snr_limit, self.snr_col, self.vis_col)
302
+ if len(vis) == 0:
303
+ return 0
304
+ nights = len(np.unique(sso_obs[self.night_col][vis]))
305
+ return nights
306
+
307
+
308
+ class ObsArcMetric(BaseMoMetric):
309
+ """Calculate the difference in time between the first and last observation
310
+ of an SSobject.
311
+
312
+ Parameters
313
+ ----------
314
+ snr_limit : `float` or None
315
+ If the snr_limit is None, detection of the object in a visit is
316
+ determined using the _calcVis method (completeness calculation).
317
+ If not None, the snr is calculated and used as a flat cutoff instead.
318
+ """
319
+
320
+ def __init__(self, snr_limit=None, **kwargs):
321
+ super().__init__(**kwargs)
322
+ self.snr_limit = snr_limit
323
+
324
+ def run(self, sso_obs, orb, hval):
325
+ vis = _set_vis(sso_obs, self.snr_limit, self.snr_col, self.vis_col)
326
+ if len(vis) == 0:
327
+ return 0
328
+ arc = sso_obs[self.mjd_col][vis].max() - sso_obs[self.mjd_col][vis].min()
329
+ return arc
330
+
331
+
332
+ class DiscoveryMetric(BaseMoMetric):
333
+ """Identify the discovery opportunities for an SSobject.
334
+
335
+ Parameters
336
+ ----------
337
+ n_obs_per_night : `int`, opt
338
+ Number of observations required within a single night. Default 2.
339
+ t_min : `float`, opt
340
+ Minimum time span between observations in a single night, in days.
341
+ Default 5 minutes (5/60/24).
342
+ t_max : `float`, opt
343
+ Maximum time span between observations in a single night, in days.
344
+ Default 90 minutes.
345
+ n_nights_per_window : `int`, opt
346
+ Number of nights required with observations, within the track window.
347
+ Default 3.
348
+ t_window : `int`, opt
349
+ Number of nights included in the track window. Default 15.
350
+ snr_limit : None or `float`, opt
351
+ SNR limit to use for observations.
352
+ If snr_limit is None, (default), then it uses
353
+ the completeness calculation added to the 'vis' column
354
+ (probabilistic visibility, based on 5-sigma limit).
355
+ If snr_limit is not None, it uses this SNR value as a cutoff.
356
+ metricName : `str`, opt
357
+ The metric name to use.
358
+ Default will be to construct
359
+ Discovery_nObsPerNightxnNightsPerWindowintWindow.
360
+ """
361
+
362
+ def __init__(
363
+ self,
364
+ n_obs_per_night=2,
365
+ t_min=5.0 / 60.0 / 24.0,
366
+ t_max=90.0 / 60.0 / 24.0,
367
+ n_nights_per_window=3,
368
+ t_window=15,
369
+ snr_limit=None,
370
+ badval=None,
371
+ **kwargs,
372
+ ):
373
+ # Define anything needed by the child metrics first.
374
+ self.snr_limit = snr_limit
375
+ self.child_metrics = {
376
+ "N_Chances": DiscoveryNChancesMetric(self),
377
+ "N_Obs": DiscoveryNObsMetric(self),
378
+ "Time": DiscoveryTimeMetric(self),
379
+ "Distance": DiscoveryDistanceMetric(self),
380
+ "RADec": DiscoveryRadecMetric(self),
381
+ "EcLonLat": DiscoveryEclonlatMetric(self),
382
+ }
383
+ if "metric_name" in kwargs:
384
+ metric_name = kwargs.get("metric_name")
385
+ del kwargs["metric_name"]
386
+ else:
387
+ metric_name = "Discovery_%.0fx%.0fin%.0f" % (
388
+ n_obs_per_night,
389
+ n_nights_per_window,
390
+ t_window,
391
+ )
392
+ # Set up for inheriting from __init__.
393
+ super().__init__(metric_name=metric_name, child_metrics=self.child_metrics, badval=badval, **kwargs)
394
+ # Define anything needed for this metric.
395
+ self.n_obs_per_night = n_obs_per_night
396
+ self.t_min = t_min
397
+ self.t_max = t_max
398
+ self.n_nights_per_window = n_nights_per_window
399
+ self.t_window = t_window
400
+
401
+ def run(self, sso_obs, orb, hval):
402
+ vis = _set_vis(sso_obs, self.snr_limit, self.snr_col, self.vis_col)
403
+ if len(vis) == 0:
404
+ return self.badval
405
+ # Identify discovery opportunities.
406
+ # Identify visits where the 'night' changes.
407
+ vis_sort = np.argsort(sso_obs[self.mjd_col][vis])
408
+ nights = sso_obs[self.night_col][vis][vis_sort]
409
+ # print 'all nights', nights
410
+ n = np.unique(nights)
411
+ # Identify all the indexes where the night changes in value.
412
+ n_idx = np.searchsorted(nights, n)
413
+ # print 'nightchanges', nights[n_idx]
414
+ # Count the number of observations per night (except last night)
415
+ obs_per_night = (n_idx - np.roll(n_idx, 1))[1:]
416
+ # Add the number of observations on the last night.
417
+ obs_last_night = np.array([len(nights) - n_idx[-1]])
418
+ obs_per_night = np.concatenate((obs_per_night, obs_last_night))
419
+ # Find the nights with more than nObsPerNight.
420
+ n_with_x_obs = n[np.where(obs_per_night >= self.n_obs_per_night)]
421
+ n_idx_many = np.searchsorted(nights, n_with_x_obs)
422
+ n_idx_many_end = np.searchsorted(nights, n_with_x_obs, side="right") - 1
423
+ # Check that nObsPerNight observations are within tMin/tMax
424
+ times_start = sso_obs[self.mjd_col][vis][vis_sort][n_idx_many]
425
+ times_end = sso_obs[self.mjd_col][vis][vis_sort][n_idx_many_end]
426
+ # Identify the nights with 'clearly good' observations.
427
+ good = np.where(
428
+ (times_end - times_start >= self.t_min) & (times_end - times_start <= self.t_max),
429
+ 1,
430
+ 0,
431
+ )
432
+ # Identify the nights where we need more investigation
433
+ # (a subset of the visits may be within the interval).
434
+ check = np.where(
435
+ (good == 0)
436
+ & (n_idx_many_end + 1 - n_idx_many > self.n_obs_per_night)
437
+ & (times_end - times_start > self.t_max)
438
+ )[0]
439
+ for i, j, c in zip(vis_sort[n_idx_many][check], vis_sort[n_idx_many_end][check], check):
440
+ t = sso_obs[self.mjd_col][vis][vis_sort][i : j + 1]
441
+ dtimes = (np.roll(t, 1 - self.n_obs_per_night) - t)[:-1]
442
+ tidx = np.where((dtimes >= self.t_min) & (dtimes <= self.t_max))[0]
443
+ if len(tidx) > 0:
444
+ good[c] = 1
445
+ # 'good' provides mask for observations which could count as
446
+ # 'good to make tracklets' against sso_obs[vis_sort][n_idx_many].
447
+ # Now identify tracklets which can make tracks.
448
+ good_idx = vis_sort[n_idx_many][good == 1]
449
+ good_idx_ends = vis_sort[n_idx_many_end][good == 1]
450
+ # print 'good tracklets', nights[good_idx]
451
+ if len(good_idx) < self.n_nights_per_window:
452
+ return self.badval
453
+ delta_nights = (
454
+ np.roll(sso_obs[self.night_col][vis][good_idx], 1 - self.n_nights_per_window)
455
+ - sso_obs[self.night_col][vis][good_idx]
456
+ )
457
+ # Identify the index in sso_obs[vis][good_idx] (sorted by mjd)
458
+ # where the discovery opportunity starts.
459
+ start_idxs = np.where((delta_nights >= 0) & (delta_nights <= self.t_window))[0]
460
+ # Identify the index where the discovery opportunity ends.
461
+ end_idxs = np.zeros(len(start_idxs), dtype="int")
462
+ for i, sIdx in enumerate(start_idxs):
463
+ in_window = np.where(
464
+ sso_obs[self.night_col][vis][good_idx] - sso_obs[self.night_col][vis][good_idx][sIdx]
465
+ <= self.t_window
466
+ )[0]
467
+ end_idxs[i] = in_window.max()
468
+ # Convert back to index based on sso_obs[vis] (sorted by expMJD).
469
+ start_idxs = good_idx[start_idxs]
470
+ end_idxs = good_idx_ends[end_idxs]
471
+ return {
472
+ "start": start_idxs[0:1],
473
+ "end": end_idxs[0:1],
474
+ "n_chances": len(start_idxs),
475
+ # "trackletNights": sso_obs[self.night_col][vis][good_idx][0],
476
+ }
477
+
478
+
479
+ class DiscoveryNChancesMetric(BaseChildMetric):
480
+ """Calculate total number of discovery opportunities for an SSobject.
481
+
482
+ Returns total number of discovery opportunities.
483
+ Child metric to be used with the Discovery Metric.
484
+
485
+ Parameters
486
+ ----------
487
+ parentDiscoveryMetric : `~BaseMoMetric`
488
+ The 'parent' metric which generated the metric data used
489
+ calculate this 'child' metric.
490
+ badval : `float`, opt
491
+ Value to return when metric cannot be calculated.
492
+ """
493
+
494
+ def __init__(
495
+ self,
496
+ parent_discovery_metric,
497
+ badval=0,
498
+ **kwargs,
499
+ ):
500
+ super().__init__(parent_discovery_metric, badval=badval, **kwargs)
501
+ self.night_start = None # night_start
502
+ self.night_end = None # night_end
503
+ self.snr_limit = parent_discovery_metric.snr_limit
504
+ # Update the metric name to use the night_start/night_end values,
505
+ # if an overriding name is not given.
506
+ if "metric_name" not in kwargs:
507
+ if self.night_start is not None:
508
+ self.name = self.name + "_n%d" % (self.night_start)
509
+ if self.night_end is not None:
510
+ self.name = self.name + "_n%d" % (self.night_end)
511
+
512
+ def run(self, sso_obs, orb, hval, metric_values):
513
+ """Return the number of different discovery chances we
514
+ had for each object/H combination.
515
+ """
516
+ return metric_values["n_chances"]
517
+
518
+
519
+ class DiscoveryNObsMetric(BaseChildMetric):
520
+ """Calculates the number of observations in the first discovery
521
+ track of an SSobject.
522
+
523
+ Parameters
524
+ ----------
525
+ parentDiscoveryMetric : `~BaseMoMetric`
526
+ The 'parent' metric which generated the metric data used
527
+ calculate this 'child' metric.
528
+ badval : `float`, opt
529
+ Value to return when metric cannot be calculated.
530
+ """
531
+
532
+ def __init__(self, parent_discovery_metric, badval=0, **kwargs):
533
+ super().__init__(parent_discovery_metric, badval=badval, **kwargs)
534
+ # The number of the discovery chance to use.
535
+ self.i = 0
536
+
537
+ def run(self, sso_obs, orb, hval, metric_values):
538
+ if self.i >= len(metric_values["start"]):
539
+ return 0
540
+ start_idx = metric_values["start"][self.i]
541
+ end_idx = metric_values["end"][self.i]
542
+ nobs = end_idx - start_idx
543
+ return nobs
544
+
545
+
546
+ class DiscoveryTimeMetric(BaseChildMetric):
547
+ """Returns the time of the first discovery track of an SSobject.
548
+
549
+ Parameters
550
+ ----------
551
+ parentDiscoveryMetric : `~BaseMoMetric`
552
+ The 'parent' metric which generated the metric data used
553
+ calculate this 'child' metric.
554
+ badval : `float`, opt
555
+ Value to return when metric cannot be calculated.
556
+ """
557
+
558
+ def __init__(self, parent_discovery_metric, t_start=None, badval=-999, **kwargs):
559
+ super().__init__(parent_discovery_metric, badval=badval, **kwargs)
560
+ self.i = 0
561
+ self.t_start = t_start
562
+ self.snr_limit = parent_discovery_metric.snr_limit
563
+
564
+ def run(self, sso_obs, orb, hval, metric_values):
565
+ if self.i >= len(metric_values["start"]):
566
+ return self.badval
567
+ vis = _set_vis(sso_obs, self.snr_limit, self.snr_col, self.vis_col)
568
+ if len(vis) == 0:
569
+ return self.badval
570
+ vis_sort = np.argsort(sso_obs[self.mjd_col][vis])
571
+ times = sso_obs[self.mjd_col][vis][vis_sort]
572
+ start_idx = metric_values["start"][self.i]
573
+ t_disc = times[start_idx]
574
+ if self.t_start is not None:
575
+ t_disc = t_disc - self.t_start
576
+ return t_disc
577
+
578
+
579
+ class DiscoveryDistanceMetric(BaseChildMetric):
580
+ """Returns the distance of the first discovery track of an SSobject.
581
+
582
+ Parameters
583
+ ----------
584
+ parentDiscoveryMetric : `~BaseMoMetric`
585
+ The 'parent' metric which generated the metric data used
586
+ calculate this 'child' metric.
587
+ badval : `float`, opt
588
+ Value to return when metric cannot be calculated.
589
+ """
590
+
591
+ def __init__(self, parent_discovery_metric, distance_col="geo_dist", badval=-999, **kwargs):
592
+ super().__init__(parent_discovery_metric, badval=badval, **kwargs)
593
+ self.i = 0
594
+ self.distance_col = distance_col
595
+ self.snr_limit = parent_discovery_metric.snr_limit
596
+
597
+ def run(self, sso_obs, orb, hval, metric_values):
598
+ if self.i >= len(metric_values["start"]):
599
+ return self.badval
600
+ vis = _set_vis(sso_obs, self.snr_limit, self.snr_col, self.vis_col)
601
+ if len(vis) == 0:
602
+ return self.badval
603
+ vis_sort = np.argsort(sso_obs[self.mjd_col][vis])
604
+ dists = sso_obs[self.distance_col][vis][vis_sort]
605
+ start_idx = metric_values["start"][self.i]
606
+ dist_disc = dists[start_idx]
607
+ return dist_disc
608
+
609
+
610
+ class DiscoveryRadecMetric(BaseChildMetric):
611
+ """Returns the RA/Dec of the first discovery track of an SSobject.
612
+
613
+ Parameters
614
+ ----------
615
+ parentDiscoveryMetric : `~BaseMoMetric`
616
+ The 'parent' metric which generated the metric data used
617
+ calculate this 'child' metric.
618
+ badval : `float`, opt
619
+ Value to return when metric cannot be calculated.
620
+ """
621
+
622
+ def __init__(self, parent_discovery_metric, badval=None, **kwargs):
623
+ super().__init__(parent_discovery_metric, badval=badval, **kwargs)
624
+ self.i = 0
625
+ self.snr_limit = parent_discovery_metric.snr_limit
626
+ self.metric_dtype = "object"
627
+
628
+ def run(self, sso_obs, orb, hval, metric_values):
629
+ if self.i >= len(metric_values["start"]):
630
+ return self.badval
631
+ vis = _set_vis(sso_obs, self.snr_limit, self.snr_col, self.vis_col)
632
+ if len(vis) == 0:
633
+ return self.badval
634
+ vis_sort = np.argsort(sso_obs[self.mjd_col][vis])
635
+ ra = sso_obs[self.ra_col][vis][vis_sort]
636
+ dec = sso_obs[self.dec_col][vis][vis_sort]
637
+ start_idx = metric_values["start"][self.i]
638
+ return (ra[start_idx], dec[start_idx])
639
+
640
+
641
+ class DiscoveryEclonlatMetric(BaseChildMetric):
642
+ """Returns the ecliptic lon/lat and solar elong of the first discovery
643
+ track of an SSobject.
644
+
645
+ Parameters
646
+ ----------
647
+ parentDiscoveryMetric : `~BaseMoMetric`
648
+ The 'parent' metric which generated the metric data used
649
+ calculate this 'child' metric.
650
+ badval : `float`, opt
651
+ Value to return when metric cannot be calculated.
652
+ """
653
+
654
+ def __init__(self, parent_discovery_metric, badval=None, **kwargs):
655
+ super().__init__(parent_discovery_metric, badval=badval, **kwargs)
656
+ self.i = 0
657
+ self.snr_limit = parent_discovery_metric.snr_limit
658
+ self.metric_dtype = "object"
659
+
660
+ def run(self, sso_obs, orb, hval, metric_values):
661
+ if self.i >= len(metric_values["start"]):
662
+ return self.badval
663
+ vis = _set_vis(sso_obs, self.snr_limit, self.snr_col, self.vis_col)
664
+ if len(vis) == 0:
665
+ return self.badval
666
+ vis_sort = np.argsort(sso_obs[self.mjd_col][vis])
667
+ ec_lon = sso_obs["ec_lon"][vis][vis_sort]
668
+ ec_lat = sso_obs["ec_lat"][vis][vis_sort]
669
+ solar_elong = sso_obs["solar_elong"][vis][vis_sort]
670
+ start_idx = metric_values["start"][self.i]
671
+ return (ec_lon[start_idx], ec_lat[start_idx], solar_elong[start_idx])
672
+
673
+
674
+ class DiscoveryVelocityMetric(BaseChildMetric):
675
+ """Returns the sky velocity of the first discovery track of an SSobject.
676
+
677
+ Parameters
678
+ ----------
679
+ parentDiscoveryMetric : `~BaseMoMetric`
680
+ The 'parent' metric which generated the metric data used
681
+ calculate this 'child' metric.
682
+ badval : `float`, opt
683
+ Value to return when metric cannot be calculated.
684
+ """
685
+
686
+ def __init__(self, parent_discovery_metric, badval=-999, **kwargs):
687
+ super().__init__(parent_discovery_metric, badval=badval, **kwargs)
688
+ self.i = 0
689
+ self.snr_limit = parent_discovery_metric.snrLimit
690
+
691
+ def run(self, sso_obs, orb, hval, metric_values):
692
+ if self.i >= len(metric_values["start"]):
693
+ return self.badval
694
+ vis = _set_vis(sso_obs, self.snr_limit, self.snr_col, self.vis_col)
695
+ if len(vis) == 0:
696
+ return self.badval
697
+ vis_sort = np.argsort(sso_obs[self.mjd_col][vis])
698
+ velocity = sso_obs["velocity"][vis][vis_sort]
699
+ start_idx = metric_values["start"][self.i]
700
+ return velocity[start_idx]
701
+
702
+
703
+ class ActivityOverTimeMetric(BaseMoMetric):
704
+ """Count fraction of survey we could identify activity for an SSobject.
705
+
706
+ Counts the time periods where we would have a chance to detect activity on
707
+ a moving object.
708
+ Splits observations into time periods set by 'window',
709
+ then looks for observations within each window,
710
+ and reports what fraction of the total windows receive 'nObs' visits.
711
+
712
+ Parameters
713
+ ----------
714
+ window : `float`
715
+ The (repeated) time period to search for activity.
716
+ snr_limit : None or `float`, opt
717
+ SNR limit to use for observations.
718
+ If snr_limit is None, then it uses
719
+ the completeness calculation added to the 'vis' column
720
+ (probabilistic visibility, based on 5-sigma limit).
721
+ If snr_limit is not None, it uses this SNR value as a cutoff.
722
+ survey_years : `float`, opt
723
+ The length of time of the survey. The test `window` is repeated
724
+ over `survey_years`, and then a fraction calculated from the
725
+ number of bins in which observations were acquired compared to the
726
+ total number of bins.
727
+ metric_name : `str` or None, opt
728
+ Name for the metric. If None, one is created from the class name.
729
+ """
730
+
731
+ def __init__(self, window, snr_limit=5, survey_years=10.0, metric_name=None, **kwargs):
732
+ if metric_name is None:
733
+ metric_name = "Chance of detecting activity lasting %.0f days" % (window)
734
+ super().__init__(metric_name=metric_name, **kwargs)
735
+ self.snr_limit = snr_limit
736
+ self.window = window
737
+ self.survey_years = survey_years
738
+ self.window_bins = np.arange(0, self.survey_years * 365 + self.window / 2.0, self.window)
739
+ self.n_windows = len(self.window_bins)
740
+ self.units = "%.1f Day Windows" % (self.window)
741
+
742
+ def run(self, sso_obs, orb, hval):
743
+ # For cometary activity, expect activity at the same point in its
744
+ # orbit at the same time, mostly
745
+ # For collisions, expect activity at random times
746
+ vis = _set_vis(sso_obs, self.snr_limit, self.snr_col, self.vis_col)
747
+ if len(vis) == 0:
748
+ return self.badval
749
+ n, b = np.histogram(sso_obs[vis][self.night_col], bins=self.window_bins)
750
+ activity_windows = np.where(n > 0)[0].size
751
+ return activity_windows / float(self.n_windows)
752
+
753
+
754
+ class ActivityOverPeriodMetric(BaseMoMetric):
755
+ """Count fraction of object period we could identify activity
756
+ for an SSobject.
757
+
758
+ Count the fraction of the orbit (when split into n_bins) that receive
759
+ observations, in order to have a chance to detect activity.
760
+
761
+ Parameters
762
+ ----------
763
+ bin_size : `float`
764
+ Like `window` for the ActivityOverTimeMetric,
765
+ but describes how much of the orbit
766
+ (considered in mean motion) should be included in a given bin.
767
+ In degrees.
768
+ snr_limit : None or `float`, opt
769
+ SNR limit to use for observations.
770
+ If snr_limit is None, then it uses
771
+ the completeness calculation added to the 'vis' column
772
+ (probabilistic visibility, based on 5-sigma limit).
773
+ If snr_limit is not None, it uses this SNR value as a cutoff.
774
+ q_col : `str`, opt
775
+ The name of the q column in the objects orbit data.
776
+ e_col : `str`, opt
777
+ The name of the eccentricity column in the objects orbit data.
778
+ t_peri_col : `str`, opt
779
+ The name of the time of perihelion column in the objects orbit data.
780
+ anomaly_col : `str`, opt
781
+ The name of the mean anomaly column in the objects orbit data.
782
+ metric_name : `str` or None, opt
783
+ Name for the metric. If None, one is created from the class name.
784
+ """
785
+
786
+ def __init__(
787
+ self,
788
+ bin_size,
789
+ snr_limit=5,
790
+ q_col="q",
791
+ e_col="e",
792
+ a_col="a",
793
+ t_peri_col="tPeri",
794
+ anomaly_col="meanAnomaly",
795
+ metric_name=None,
796
+ **kwargs,
797
+ ):
798
+ """
799
+ @ bin_size : size of orbit slice, in degrees.
800
+ """
801
+ if metric_name is None:
802
+ metric_name = "Chance of detecting activity covering %.1f of the orbit" % (bin_size)
803
+ super().__init__(metric_name=metric_name, **kwargs)
804
+ self.q_col = q_col
805
+ self.e_col = e_col
806
+ self.a_col = a_col
807
+ self.t_peri_col = t_peri_col
808
+ self.anomaly_col = anomaly_col
809
+ self.snr_limit = snr_limit
810
+ self.bin_size = np.radians(bin_size)
811
+ self.anomaly_bins = np.arange(0, 2 * np.pi, self.bin_size)
812
+ self.anomaly_bins = np.concatenate([self.anomaly_bins, np.array([2 * np.pi])])
813
+ self.n_bins = len(self.anomaly_bins) - 1
814
+ self.units = "%.1f deg" % (np.degrees(self.bin_size))
815
+
816
+ def run(self, sso_obs, orb, hval):
817
+ # For cometary activity, expect activity at the same point in its
818
+ # orbit at the same time, mostly
819
+ # For collisions, expect activity at random times
820
+ if self.a_col in orb.keys():
821
+ a = orb[self.a_col]
822
+ elif self.q_col in orb.keys():
823
+ a = orb[self.q_col] / (1 - orb[self.e_col])
824
+ else:
825
+ return self.badval
826
+
827
+ period = np.power(a, 3.0 / 2.0) * 365.25 # days
828
+
829
+ if self.anomaly_col in orb.keys():
830
+ curranomaly = np.radians(
831
+ orb[self.anomaly_col] + (sso_obs[self.mjd_col] - orb["epoch"]) / period * 360.0
832
+ ) % (2 * np.pi)
833
+ elif self.t_peri_col in orb.keys():
834
+ curranomaly = ((sso_obs[self.mjd_col] - orb[self.t_peri_col]) / period) % (2 * np.pi)
835
+ else:
836
+ return self.badval
837
+
838
+ vis = _set_vis(sso_obs, self.snr_limit, self.snr_col, self.vis_col)
839
+ if len(vis) == 0:
840
+ return self.badval
841
+ n, b = np.histogram(curranomaly[vis], bins=self.anomaly_bins)
842
+ activity_windows = np.where(n > 0)[0].size
843
+ return activity_windows / float(self.n_bins)
844
+
845
+
846
+ class MagicDiscoveryMetric(BaseMoMetric):
847
+ """Count the number of nights with discovery opportunities
848
+ with very good software for an SSobject.
849
+
850
+ Parameters
851
+ ----------
852
+ n_obs : `int`, opt
853
+ Total number of observations required for discovery.
854
+ t_window : `float`, opt
855
+ The timespan of the discovery window (days).
856
+ snr_limit : `float` or None
857
+ If None, uses the probabilistic detection likelihood.
858
+ If float, uses the SNR value as a flat cutoff value.
859
+ """
860
+
861
+ def __init__(self, n_obs=6, t_window=60, snr_limit=None, **kwargs):
862
+ super().__init__(**kwargs)
863
+ self.snr_limit = snr_limit
864
+ self.n_obs = n_obs
865
+ self.t_window = t_window
866
+ self.badval = 0
867
+
868
+ def run(self, sso_obs, orb, hval):
869
+ # Calculate visibility for this orbit at this H.
870
+ vis = _set_vis(sso_obs, self.snr_limit, self.snr_col, self.vis_col)
871
+ if len(vis) < self.n_obs:
872
+ return self.badval
873
+ t_nights = np.sort(sso_obs[self.night_col])[vis]
874
+ u_nights = np.unique(t_nights)
875
+ indx_w = np.searchsorted(t_nights, u_nights, "left")
876
+ indx_p = np.searchsorted(t_nights - self.t_window, u_nights, "left")
877
+ n_disc = np.where(indx_p - indx_w >= self.n_obs)[0].size
878
+ return n_disc
879
+
880
+
881
+ class HighVelocityMetric(BaseMoMetric):
882
+ """Count number of times an SSobject appears trailed.
883
+
884
+ Count the number of times an asteroid is observed with a velocity
885
+ high enough to make it appear trailed by a factor of (psf_factor)*PSF -
886
+ i.e. velocity >= psf_factor * seeing / visitExpTime.
887
+ Simply counts the total number of observations with high velocity.
888
+
889
+ Parameters
890
+ ----------
891
+ psf_factor : `float`, opt
892
+ The factor to multiply the seeing/VisitExpTime by to compare against
893
+ velocity.
894
+ snr_limit : `float` or None
895
+ If None, uses the probabilistic detection likelihood.
896
+ If float, uses the SNR value as a flat cutoff value.
897
+ """
898
+
899
+ def __init__(self, psf_factor=2.0, snr_limit=None, velocity_col="velocity", **kwargs):
900
+ super().__init__(**kwargs)
901
+ self.velocity_col = velocity_col
902
+ self.snr_limit = snr_limit
903
+ self.psf_factor = psf_factor
904
+ self.badval = 0
905
+
906
+ def run(self, sso_obs, orb, hval):
907
+ vis = _set_vis(sso_obs, self.snr_limit, self.snr_col, self.vis_col)
908
+ if len(vis) == 0:
909
+ return self.badval
910
+ high_velocity_obs = np.where(
911
+ sso_obs[self.velocity_col][vis]
912
+ >= (24.0 * self.psf_factor * sso_obs[self.seeing_col][vis] / sso_obs[self.exp_time_col][vis])
913
+ )[0]
914
+ return high_velocity_obs.size
915
+
916
+
917
+ class HighVelocityNightsMetric(BaseMoMetric):
918
+ """Count the number of discovery opportunities (via trailing) for an
919
+ SSobject.
920
+
921
+ Determine the first time an asteroid is observed is observed with a
922
+ velocity high enough to make it appear trailed by a factor of
923
+ psf_factor*PSF with n_obs_per_night observations within a given night.
924
+
925
+ Parameters
926
+ ----------
927
+ psf_factor: `float`, opt
928
+ Object velocity (deg/day) must be
929
+ >= 24 * psf_factor * seeingGeom (") / visitExpTime (s).
930
+ Default is 2 (i.e. object trailed over 2 psf's).
931
+ n_obs_per_night : `int`, opt
932
+ Number of observations per night required. Default 2.
933
+ snr_limit : `float` or None
934
+ If snr_limit is set as a float, then requires object to be above
935
+ snr_limit SNR in the image.
936
+ If snr_limit is None, this uses the probabilistic 'visibility'
937
+ calculated by the vis stacker, which means SNR ~ 5.
938
+ velocity_col : `str`, opt
939
+ Name of the velocity column in the obs file.
940
+ Default 'velocity'. (note this is deg/day).
941
+
942
+ Returns
943
+ -------
944
+ time : `float`
945
+ The time of the first detection where the conditions are satisfed.
946
+ """
947
+
948
+ def __init__(self, psf_factor=2.0, n_obs_per_night=2, snr_limit=None, velocity_col="velocity", **kwargs):
949
+ super().__init__(**kwargs)
950
+ self.velocity_col = velocity_col
951
+ self.snr_limit = snr_limit
952
+ self.psf_factor = psf_factor
953
+ self.n_obs_per_night = n_obs_per_night
954
+
955
+ def run(self, sso_obs, orb, hval):
956
+ vis = _set_vis(sso_obs, self.snr_limit, self.snr_col, self.vis_col)
957
+ if len(vis) == 0:
958
+ return self.badval
959
+ high_velocity_obs = np.where(
960
+ sso_obs[self.velocity_col][vis]
961
+ >= (24.0 * self.psf_factor * sso_obs[self.seeing_col][vis] / sso_obs[self.exp_time_col][vis])
962
+ )[0]
963
+ if len(high_velocity_obs) == 0:
964
+ return 0
965
+ nights = sso_obs[self.night_col][vis][high_velocity_obs]
966
+ n = np.unique(nights)
967
+ n_idx = np.searchsorted(nights, n)
968
+ # Count the number of observations per night (except last night)
969
+ obs_per_night = (n_idx - np.roll(n_idx, 1))[1:]
970
+ # Add the number of observations on the last night.
971
+ obs_last_night = np.array([len(nights) - n_idx[-1]])
972
+ obs_per_night = np.concatenate((obs_per_night, obs_last_night))
973
+ # Find the nights with at least nObsPerNight visits
974
+ # (this is already looking at only high velocity observations).
975
+ n_with_x_obs = n[np.where(obs_per_night >= self.n_obs_per_night)]
976
+ if len(n_with_x_obs) > 0:
977
+ found = sso_obs[np.where(sso_obs[self.night_col] == n_with_x_obs[0])][self.mjd_col][0]
978
+ else:
979
+ found = self.badval
980
+ return found
981
+
982
+
983
+ class LightcurveInversionAsteroidMetric(BaseMoMetric):
984
+ """Evaluate the liklihood that the detections could be used to enable
985
+ lightcurve inversion. This metric is generally applicable only to inner
986
+ solar system objects (NEOs, MBAs).
987
+
988
+ Parameters
989
+ ----------
990
+ weight_det : `float`, opt
991
+ The SNR-weighted number of detections required (per bandpass in any
992
+ ONE of the filters in filterlist).
993
+ Default 50.
994
+ snr_limit : `float` or None, opt
995
+ If snr_limit is set as a float, then requires object to be
996
+ above snr_limit SNR in the image.
997
+ If snr_limit is None, this uses the probabilistic 'visibility'
998
+ calculated by the vis stacker,
999
+ which means SNR ~ 5. Default is None.
1000
+ snr_max : `float`, opt
1001
+ Maximum value toward the SNR-weighting to consider. Default 100.
1002
+ filterlist : `list` [`str`], opt
1003
+ The filters which the lightcurve inversion could be based on.
1004
+ Requirements must be met in one of these filters.
1005
+
1006
+ Returns
1007
+ -------
1008
+ metric_value : `int`
1009
+ 0 (could not perform lightcurve inversion) or 1 (could)
1010
+
1011
+ Notes
1012
+ -----
1013
+ This metric determines if the cumulative sum of observations of a
1014
+ target are enough to enable lightcurve inversion for shape modeling.
1015
+ For this to be true, multiple conditions need to be
1016
+ satisfied:
1017
+
1018
+ 1) The SNR-weighted number of observations (each observation is weighted
1019
+ by its SNR, up to a max of 100) must be larger than the
1020
+ threshold weight_det (default 50)
1021
+ 2) Ecliptic longitudinal coverage needs to be at least 90 degrees,
1022
+ and the absolute deviation needs to be at least 1/8th the
1023
+ longitudinal coverage.
1024
+ 3) The phase angle coverage needs to span at least 5 degrees.
1025
+
1026
+ For evaluation of condition 2, the median ecliptic longitude is
1027
+ subtracted from all longitudes, and the modulo 360 of those values
1028
+ is taken. This ensures that the wrap around 360 is handled correctly.
1029
+
1030
+ For more information on the above conditions, please see
1031
+ https://docs.google.com/document/d/1GAriM7trpTS08uanjUF7PyKALB2JBTjVT7Y6R30i0-8/edit?usp=sharing
1032
+
1033
+ Contributed by Steve Chesley, Wes Fraser, Josef Durech, and the
1034
+ inner solar system working group.
1035
+ """
1036
+
1037
+ def __init__(
1038
+ self, weight_det=50, snr_limit=None, snr_max=100, filterlist=("u", "g", "r", "i", "z", "y"), **kwargs
1039
+ ):
1040
+ super().__init__(**kwargs)
1041
+ self.snr_limit = snr_limit
1042
+ self.snr_max = snr_max
1043
+ self.weight_det = weight_det
1044
+ self.filterlist = filterlist
1045
+
1046
+ def run(self, sso_obs, orb, hval):
1047
+ # Calculate the clipped SNR - ranges from snrLimit / SNR+vis to snrMax.
1048
+ clip_snr = np.minimum(sso_obs[self.snr_col], self.snr_max)
1049
+ if self.snr_limit is not None:
1050
+ clip_snr = np.where(sso_obs[self.snr_col] <= self.snr_limit, 0, clip_snr)
1051
+ else:
1052
+ clip_snr = np.where(sso_obs[self.vis_col] == 0, 0, clip_snr)
1053
+ if len(np.where(clip_snr > 0)[0]) == 0:
1054
+ return 0
1055
+ # Check each filter in filterlist:
1056
+ # stop as soon as find a filter that matches requirements.
1057
+ inversion_possible = 0
1058
+ for f in self.filterlist:
1059
+ # Is the SNR-weight sum of observations in this filter high enough?
1060
+ match = np.where(sso_obs[self.filter_col] == f)
1061
+ snr_sum = np.sum(clip_snr[match]) / self.snr_max
1062
+ if snr_sum < self.weight_det:
1063
+ # Do not have enough SNR-weighted observations,
1064
+ # so skip on to the next filter.
1065
+ continue
1066
+ # Is the ecliptic longitude coverage for the visible
1067
+ # observations sufficient?
1068
+ # Is the phase coverage sufficient?
1069
+ vis = np.where(clip_snr[match] > 0)
1070
+ ec_l = sso_obs["ecLon"][match][vis]
1071
+ phase_angle = sso_obs["phase"][match][vis]
1072
+ # Calculate the absolute deviation and range of ecliptic longitude.
1073
+ ec_l_centred = (ec_l - np.median(ec_l)) % 360.0
1074
+ a_dev = np.sum(np.abs(ec_l_centred - np.mean(ec_l_centred))) / len(ec_l_centred)
1075
+ d_l = np.max(ec_l) - np.min(ec_l)
1076
+ # Calculate the range of the phase angle
1077
+ dp = np.max(phase_angle) - np.min(phase_angle)
1078
+ # Metric requirement is that d_l >= 90 deg, absolute
1079
+ # deviation is greater than d_l/8
1080
+ # and then that the phase coverage is more than 5 degrees.
1081
+ # Stop as soon as find a case where this is true.
1082
+ if d_l >= 90.0 and a_dev >= d_l / 8 and dp >= 5:
1083
+ inversion_possible += 1
1084
+ break
1085
+ return inversion_possible
1086
+
1087
+
1088
+ class ColorAsteroidMetric(BaseMoMetric):
1089
+ """Calculate the likelihood of being able to calculate the color of an
1090
+ object. This metric is appropriate for MBAs and NEOs,
1091
+ and other inner solar system objects.
1092
+
1093
+
1094
+ Parameters
1095
+ ----------
1096
+ weight_det: float, opt
1097
+ The SNR-weighted number of detections required (per bandpass in any
1098
+ ONE of the filters in filterlist).
1099
+ Default 10.
1100
+ snr_limit: float or None, opt
1101
+ If snr_limit is set as a float, then requires object to be above
1102
+ snr_limit SNR in the image.
1103
+ If snr_limit is None, this uses the probabilistic 'visibility'
1104
+ calculated by the vis stacker,
1105
+ which means SNR ~ 5. Default is None.
1106
+ snr_max: float, opt
1107
+ Maximum value toward the SNR-weighting to consider. Default 20.
1108
+
1109
+ Returns
1110
+ -------
1111
+ flag : `int`
1112
+ An integer 'flag' that indicates whether the mean magnitude
1113
+ (and thus a color) was determined in:
1114
+ 0 = no bands
1115
+ 1 = g and (r or i) and (z or y).
1116
+ i.e. obtain colors g-r or g-i PLUS g-z or g-y
1117
+ 2 = Any 4 different filters (from grizy).
1118
+ i.e. colors = g-r, r-i, i-z, OR r-i, i-z, z-y..
1119
+ 3 = All 5 from grizy. i.e. colors g-r, r-i, i-z, z-y.
1120
+ 4 = All 6 filters (ugrizy) -- best possible! add u-g.
1121
+
1122
+ Notes
1123
+ -----
1124
+ The metric evaluates if the SNR-weighted number of observations are
1125
+ enough to determine an approximate lightcurve and phase function --
1126
+ and from this, then a color for the asteroid can be determined.
1127
+ The assumption is that you must fit the lightcurve/phase function
1128
+ in each bandpass, and could do this well-enough if you have at least
1129
+ weight_det SNR-weighted observations in the bandpass.
1130
+ e.g. to find a g-r color, you must have 10 (SNR-weighted) obs in g
1131
+ and 10 in r.
1132
+
1133
+ For more details, see
1134
+ https://docs.google.com/document/d/1GAriM7trpTS08uanjUF7PyKALB2JBTjVT7Y6R30i0-8/edit?usp=sharing
1135
+
1136
+ Contributed by Wes Fraser, Steven Chesley
1137
+ & the inner solar system working group.
1138
+ """
1139
+
1140
+ def __init__(self, weight_det=10, snr_max=20, snr_limit=None, **kwargs):
1141
+ super().__init__(**kwargs)
1142
+ self.weight_det = weight_det
1143
+ self.snr_limit = snr_limit
1144
+ self.snr_max = snr_max
1145
+ self.filterlist = ("u", "g", "r", "i", "z", "y")
1146
+
1147
+ def run(self, sso_obs, orb, hval):
1148
+ clip_snr = np.minimum(sso_obs[self.snr_col], self.snr_max)
1149
+ if self.snr_limit is not None:
1150
+ clip_snr = np.where(sso_obs[self.snr_col] <= self.snr_limit, 0, clip_snr)
1151
+ else:
1152
+ clip_snr = np.where(sso_obs[self.vis_col] == 0, 0, clip_snr)
1153
+ if len(np.where(clip_snr > 0)[0]) == 0:
1154
+ return self.badval
1155
+
1156
+ # Evaluate SNR-weighted number of observations in each filter.
1157
+ filter_weight = {}
1158
+ for f in self.filterlist:
1159
+ match = np.where(sso_obs[self.filter_col] == f)
1160
+ snrweight = np.sum(clip_snr[match]) / self.snr_max
1161
+ # If the snrweight exceeds the weightDet, add it to the dictionary.
1162
+ if snrweight > self.weight_det:
1163
+ filter_weight[f] = snrweight
1164
+
1165
+ # Now assign a flag:
1166
+ # 0 = no bands
1167
+ # 1 = g and (r or i) and (z or y).
1168
+ # i.e. obtain colors g-r or g-i PLUS g-z or g-y
1169
+ # 2 = Any 4 different filters (from grizy).
1170
+ # i.e. colors = g-r, r-i, i-z, OR r-i, i-z, z-y..
1171
+ # 3 = All 5 from grizy. i.e. colors g-r, r-i, i-z, z-y.
1172
+ # 4 = All 6 filters (ugrizy) -- best possible! add u-g.
1173
+ all_six = set(self.filterlist)
1174
+ good_five = set(["g", "r", "i", "z", "y"])
1175
+
1176
+ if len(filter_weight) == 0:
1177
+ # this lets us stop evaluating here if possible.
1178
+ flag = 0
1179
+ elif all_six.intersection(filter_weight) == all_six:
1180
+ flag = 4
1181
+ elif good_five.intersection(filter_weight) == good_five:
1182
+ flag = 3
1183
+ elif len(good_five.intersection(filter_weight)) == 4:
1184
+ flag = 2
1185
+ elif "g" in filter_weight:
1186
+ # Have 'g' - do we have (r or i) and (z or y)
1187
+ if ("r" in filter_weight or "i" in filter_weight) and (
1188
+ "z" in filter_weight or "y" in filter_weight
1189
+ ):
1190
+ flag = 1
1191
+ else:
1192
+ flag = 0
1193
+ else:
1194
+ flag = 0
1195
+
1196
+ return flag
1197
+
1198
+
1199
+ class LightcurveColorOuterMetric(BaseMoMetric):
1200
+ """Calculate the liklihood of being able to calculate a color and
1201
+ lightcurve for outer solar system objects.
1202
+
1203
+ Parameters
1204
+ ----------
1205
+ snr_limit : `float` or None, opt
1206
+ If snr_limit is set as a float, then requires object to be above
1207
+ snr_limit SNR in the image.
1208
+ If snr_limit is None, this uses the probabilistic 'visibility'
1209
+ calculated by the vis stacker,
1210
+ which means SNR ~ 5. Default is None.
1211
+ num_req : `int`, opt
1212
+ Number of observations required for a lightcurve fitting. Default 30.
1213
+ num_sec_filt : `int`, opt
1214
+ Number of observations required in a secondary band for color only.
1215
+ Default 20.
1216
+ filterlist : `list` [`str`], opt
1217
+ Filters that the primary/secondary measurements can be in.
1218
+
1219
+ Returns
1220
+ -------
1221
+ flag : `int`
1222
+ A flag that indicates whether a color/lightcurve was generated in:
1223
+ 0 = no lightcurve
1224
+ (although may have had 'color' in one or more band)
1225
+ 1 = a lightcurve in a single filter
1226
+ (but no additional color information)
1227
+ 2+ = lightcurves in more than one filter (or lightcurve + color)
1228
+ e.g. lightcurve in 2 bands,
1229
+ with additional color information in another = 3.
1230
+
1231
+ Notes
1232
+ -----
1233
+ This metric is appropriate for outer solar system objects,
1234
+ such as TNOs and SDOs.
1235
+
1236
+ This metric evaluates whether the number of observations is
1237
+ sufficient to fit a lightcurve in a primary and secondary bandpass.
1238
+ The primary bandpass requires more observations than the secondary.
1239
+ Essentially, it's a complete lightcurve in one or both bandpasses, with at
1240
+ least a semi-complete lightcurve in the secondary band.
1241
+
1242
+ The lightcurve/color can be calculated with any two of the
1243
+ bandpasses in filterlist.
1244
+
1245
+ Contributed by Wes Fraser.
1246
+ """
1247
+
1248
+ def __init__(
1249
+ self, snr_limit=None, num_req=30, num_sec_filt=20, filterlist=("u", "g", "r", "i", "z", "y"), **kwargs
1250
+ ):
1251
+ super().__init__(**kwargs)
1252
+ self.snr_limit = snr_limit
1253
+ self.num_req = num_req
1254
+ self.num_sec_filt = num_sec_filt
1255
+ self.filterlist = filterlist
1256
+
1257
+ def run(self, sso_obs, orb, hval):
1258
+ vis = _set_vis(sso_obs, self.snr_limit, self.snr_col, self.vis_col)
1259
+ if len(vis) == 0:
1260
+ return 0
1261
+
1262
+ lightcurves = set()
1263
+ colors = set()
1264
+ for f in self.filterlist:
1265
+ nmatch = np.where(sso_obs[vis][self.filter_col] == f)[0]
1266
+ if len(nmatch) >= self.num_req:
1267
+ lightcurves.add(f)
1268
+ if len(nmatch) >= self.num_sec_filt:
1269
+ colors.add(f)
1270
+
1271
+ # Set the flags - first the number of filters with lightcurves.
1272
+ flag = len(lightcurves)
1273
+ # And check if there were extra filters which had enough for a color
1274
+ # but not enough for a full lightcurve.
1275
+ if len(colors.difference(lightcurves)) > 0:
1276
+ # If there was no lightcurve available to match against:
1277
+ if len(lightcurves) == 0:
1278
+ flag = 0
1279
+ else:
1280
+ # We had a lightcurve and now can add a color.
1281
+ flag += 1
1282
+ return flag
1283
+
1284
+
1285
+ class InstantaneousColorMetric(BaseMoMetric):
1286
+ """Identify SSobjects which could have observations suitable to
1287
+ determine instanteous colors.
1288
+
1289
+ This is roughly defined as objects which have more than n_pairs pairs
1290
+ of observations with SNR greater than snr_limit,
1291
+ in bands bandOne and bandTwo, within n_hours.
1292
+
1293
+ Parameters
1294
+ ----------
1295
+ n_pairs : `int`, opt
1296
+ The number of pairs of observations (in each band) that must be
1297
+ within n_hours. Default 1.
1298
+ snr_limit : `float`, opt
1299
+ The SNR limit for the observations. Default 10.
1300
+ n_hours : `float`, opt
1301
+ The time interval between observations in the two bandpasses (hours).
1302
+ Default 0.5 hours.
1303
+ b_one : `str`, opt
1304
+ The first bandpass for the color. Default 'g'.
1305
+ b_two : `str`, opt
1306
+ The second bandpass for the color. Default 'r'.
1307
+
1308
+ Returns
1309
+ -------
1310
+ flag : `int`
1311
+ 0 (no color possible under these constraints) or 1 (color possible).
1312
+ """
1313
+
1314
+ def __init__(self, n_pairs=1, snr_limit=10, n_hours=0.5, b_one="g", b_two="r", **kwargs):
1315
+ super().__init__(**kwargs)
1316
+ self.n_pairs = n_pairs
1317
+ self.snr_limit = snr_limit
1318
+ self.n_hours = n_hours
1319
+ self.b_one = b_one
1320
+ self.b_two = b_two
1321
+ self.badval = -666
1322
+
1323
+ def run(self, sso_obs, orb, hval):
1324
+ vis = np.where(sso_obs[self.snr_col] >= self.snr_limit)[0]
1325
+ if len(vis) < self.n_pairs * 2:
1326
+ return 0
1327
+ b_one_obs = np.where(sso_obs[self.filter_col][vis] == self.b_one)[0]
1328
+ b_two_obs = np.where(sso_obs[self.filter_col][vis] == self.b_two)[0]
1329
+ timesb_one = sso_obs[self.mjd_col][vis][b_one_obs]
1330
+ timesb_two = sso_obs[self.mjd_col][vis][b_two_obs]
1331
+ if len(timesb_one) == 0 or len(timesb_two) == 0:
1332
+ return 0
1333
+ d_time = self.n_hours / 24.0
1334
+ # Calculate the time between the closest pairs of observations.
1335
+ in_order = np.searchsorted(timesb_one, timesb_two, "right")
1336
+ in_order = np.where(in_order - 1 > 0, in_order - 1, 0)
1337
+ dt_pairs = timesb_two - timesb_one[in_order]
1338
+ if len(np.where(dt_pairs < d_time)[0]) >= self.n_pairs:
1339
+ found = 1
1340
+ else:
1341
+ found = 0
1342
+ return found
1343
+
1344
+
1345
+ class PeakVMagMetric(BaseMoMetric):
1346
+ """Pull out the peak V magnitude of all observations of the SSobject."""
1347
+
1348
+ def __init__(self, **kwargs):
1349
+ super().__init__(**kwargs)
1350
+
1351
+ def run(self, sso_obs, orb, hval):
1352
+ peak_vmag = np.min(sso_obs[self.app_mag_v_col])
1353
+ return peak_vmag
1354
+
1355
+
1356
+ class KnownObjectsMetric(BaseMoMetric):
1357
+ """Identify SSobjects which could be classified as 'previously known'
1358
+ based on their peak V magnitude.
1359
+ This is most appropriate for NEO surveys, where most of the sky has
1360
+ been covered so the exact location
1361
+ (beyond being in the visible sky) is not as important.
1362
+
1363
+ Default parameters tuned to match NEO survey capabilities.
1364
+ Returns the time at which each first reached that threshold V magnitude.
1365
+ The default values are calibrated using the NEOs larger than 140m
1366
+ discovered in the last 20 years and assuming a 30% completeness in 2017.
1367
+
1368
+ Note: the default parameters here were set up in ~2012, and are likely
1369
+ out of date (potentially adding another epoch of discovery).
1370
+
1371
+ Parameters
1372
+ -----------
1373
+ elong_thresh : `float`, opt
1374
+ The cutoff in solar elongation to consider an object 'visible'.
1375
+ Default 100 deg.
1376
+ v_mag_thresh1 : `float`, opt
1377
+ The magnitude threshold for previously known objects. Default 20.0.
1378
+ eff1 : `float`, opt
1379
+ The likelihood of actually achieving each individual input observation.
1380
+ If the input observations include one observation per day,
1381
+ an 'eff' value of 0.3 would mean that (on average) only one third
1382
+ of these observations would be achieved. This is similar to the level
1383
+ for LSST, which can cover the visible sky every 3-4 days.
1384
+ Default 0.1
1385
+ t_switch1 : `float`, opt
1386
+ The (MJD) time to switch between v_mag_thresh1 + eff1 to
1387
+ v_mag_thresh2 + eff2, e.g. the end of the first period.
1388
+ Default 53371 (2005).
1389
+ v_mag_thresh2 : `float`, opt
1390
+ The magnitude threshhold for previously known objects. Default 22.0.
1391
+ This is based on assuming PS and other surveys will be efficient
1392
+ down to V=22.
1393
+ eff2 : `float`, opt
1394
+ The efficiency of observations during the second period of time.
1395
+ Default 0.1
1396
+ t_switch2 : `float`, opt
1397
+ The (MJD) time to switch between v_mag_thresh2 + eff2 to
1398
+ v_mag_thresh3 + eff3.
1399
+ Default 57023 (2015).
1400
+ v_mag_thresh3 : `float`, opt
1401
+ The magnitude threshold during the third period.
1402
+ Default 22.0, based on PS1 + Catalina.
1403
+ eff3 : `float`, opt
1404
+ The efficiency of observations during the third period. Default 0.1
1405
+ t_switch3 : `float`, opt
1406
+ The (MJD) time to switch between v_mag_thresh3 + eff3
1407
+ to v_mag_thresh4 + eff4.
1408
+ Default 59580 (2022).
1409
+ v_mag_thresh4 : `float`, opt
1410
+ The magnitude threshhold during the fourth (last) period.
1411
+ Default 22.0, based on PS1 + Catalina.
1412
+ eff4 : `float`, opt
1413
+ The efficiency of observations during the fourth (last) period.
1414
+ Default 0.2
1415
+ """
1416
+
1417
+ def __init__(
1418
+ self,
1419
+ elong_thresh=100.0,
1420
+ v_mag_thresh1=20.0,
1421
+ eff1=0.1,
1422
+ t_switch1=53371,
1423
+ v_mag_thresh2=21.5,
1424
+ eff2=0.1,
1425
+ t_switch2=57023,
1426
+ v_mag_thresh3=22.0,
1427
+ eff3=0.1,
1428
+ t_switch3=59580,
1429
+ v_mag_thresh4=22.0,
1430
+ eff4=0.2,
1431
+ elong_col="Elongation",
1432
+ mjd_col="MJD(UTC)",
1433
+ **kwargs,
1434
+ ):
1435
+ super().__init__(**kwargs)
1436
+ self.elong_thresh = elong_thresh
1437
+ self.elong_col = elong_col
1438
+ self.v_mag_thresh1 = v_mag_thresh1
1439
+ self.eff1 = eff1
1440
+ self.t_switch1 = t_switch1
1441
+ self.v_mag_thresh2 = v_mag_thresh2
1442
+ self.eff2 = eff2
1443
+ self.t_switch2 = t_switch2
1444
+ self.v_mag_thresh3 = v_mag_thresh3
1445
+ self.eff3 = eff3
1446
+ self.t_switch3 = t_switch3
1447
+ self.v_mag_thresh4 = v_mag_thresh4
1448
+ self.eff4 = eff4
1449
+ self.mjd_col = mjd_col
1450
+ self.badval = int(t_switch3) + 365 * 1000
1451
+
1452
+ def _pick_obs(self, potential_obs_times, eff):
1453
+ # From a set of potential observations, apply an efficiency
1454
+ # And return the minimum time (if any)
1455
+ rand_pick = np.random.rand(len(potential_obs_times))
1456
+ picked = np.where(rand_pick <= eff)[0]
1457
+ if len(picked) > 0:
1458
+ disc_time = potential_obs_times[picked].min()
1459
+ else:
1460
+ disc_time = None
1461
+ return disc_time
1462
+
1463
+ def run(self, sso_obs, orb, hval):
1464
+ visible = np.where(sso_obs[self.elong_col] >= self.elong_thresh, 1, 0)
1465
+ discovery_time = None
1466
+ # Look for discovery in any of the three periods.
1467
+ # First period.
1468
+ obs1 = np.where((sso_obs[self.mjd_col] < self.t_switch1) & visible)[0]
1469
+ over_peak = np.where(sso_obs[self.app_mag_v_col][obs1] <= self.v_mag_thresh1)[0]
1470
+ if len(over_peak) > 0:
1471
+ discovery_time = self._pick_obs(sso_obs[self.mjd_col][obs1][over_peak], self.eff1)
1472
+ # Second period.
1473
+ if discovery_time is None:
1474
+ obs2 = np.where(
1475
+ (sso_obs[self.mjd_col] >= self.t_switch1) & (sso_obs[self.mjd_col] < self.t_switch2) & visible
1476
+ )[0]
1477
+ over_peak = np.where(sso_obs[self.app_mag_v_col][obs2] <= self.v_mag_thresh2)[0]
1478
+ if len(over_peak) > 0:
1479
+ discovery_time = self._pick_obs(sso_obs[self.mjd_col][obs2][over_peak], self.eff2)
1480
+ # Third period.
1481
+ if discovery_time is None:
1482
+ obs3 = np.where(
1483
+ (sso_obs[self.mjd_col] >= self.t_switch2) & (sso_obs[self.mjd_col] < self.t_switch3) & visible
1484
+ )[0]
1485
+ over_peak = np.where(sso_obs[self.app_mag_v_col][obs3] <= self.v_mag_thresh3)[0]
1486
+ if len(over_peak) > 0:
1487
+ discovery_time = self._pick_obs(sso_obs[self.mjd_col][obs3][over_peak], self.eff3)
1488
+ # Fourth period.
1489
+ if discovery_time is None:
1490
+ obs4 = np.where((sso_obs[self.mjd_col] >= self.t_switch3) & visible)[0]
1491
+ over_peak = np.where(sso_obs[self.app_mag_v_col][obs4] <= self.v_mag_thresh4)[0]
1492
+ if len(over_peak) > 0:
1493
+ discovery_time = self._pick_obs(sso_obs[self.mjd_col][obs4][over_peak], self.eff4)
1494
+ if discovery_time is None:
1495
+ discovery_time = self.badval
1496
+ return discovery_time