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.
- batch/compile_prenight_metadata_cache.sh +34 -0
- batch/run_auxtel_prenight_sims.sh +163 -0
- batch/run_prenight_sims.sh +239 -0
- docs/.gitignore +3 -0
- docs/Makefile +32 -0
- docs/api.rst +25 -0
- docs/conf.py +8 -0
- docs/data-api.rst +12 -0
- docs/data-download.rst +70 -0
- docs/developer.rst +14 -0
- docs/documenteer.toml +11 -0
- docs/index.rst +22 -0
- docs/installation.rst +76 -0
- docs/introduction.rst +37 -0
- docs/maf-api-batches.rst +12 -0
- docs/maf-api-db.rst +12 -0
- docs/maf-api-maf-contrib.rst +12 -0
- docs/maf-api-maps.rst +12 -0
- docs/maf-api-metricbundles.rst +12 -0
- docs/maf-api-metrics.rst +12 -0
- docs/maf-api-plots.rst +12 -0
- docs/maf-api-run-comparison.rst +12 -0
- docs/maf-api-slicers.rst +12 -0
- docs/maf-api-stackers.rst +12 -0
- docs/maf-api-utils.rst +12 -0
- docs/maf-api.rst +22 -0
- docs/maf.rst +60 -0
- docs/metric_list.py +49 -0
- docs/moving-objects-api.rst +12 -0
- docs/moving-objects.rst +22 -0
- docs/phot-utils-api.rst +12 -0
- docs/phot-utils.rst +11 -0
- docs/prenight.rst +117 -0
- docs/protoarchive.rst +276 -0
- docs/satellite-constellations-api.rst +12 -0
- docs/satellite-constellations.rst +13 -0
- docs/selfcal-api.rst +12 -0
- docs/selfcal.rst +15 -0
- docs/sim-archive-api.rst +12 -0
- docs/skybrightness-api.rst +12 -0
- docs/skybrightness.rst +23 -0
- docs/user-guide.rst +28 -0
- docs/vseqarchive-architecture.rst +438 -0
- docs/vseqarchive-setup.rst +238 -0
- docs/vseqarchive.rst +225 -0
- rubin_sim/__init__.py +7 -0
- rubin_sim/data/__init__.py +1 -0
- rubin_sim/data/rs_download_data.py +155 -0
- rubin_sim/maf/__init__.py +34 -0
- rubin_sim/maf/batches/__init__.py +18 -0
- rubin_sim/maf/batches/altaz_batch.py +161 -0
- rubin_sim/maf/batches/col_map_dict.py +274 -0
- rubin_sim/maf/batches/common.py +388 -0
- rubin_sim/maf/batches/ddf_batch.py +728 -0
- rubin_sim/maf/batches/filterchange_batch.py +182 -0
- rubin_sim/maf/batches/glance_batch.py +591 -0
- rubin_sim/maf/batches/hourglass_batch.py +79 -0
- rubin_sim/maf/batches/info_batch.py +114 -0
- rubin_sim/maf/batches/metadata_batch.py +558 -0
- rubin_sim/maf/batches/moving_objects_batch.py +1587 -0
- rubin_sim/maf/batches/openshutter_batch.py +100 -0
- rubin_sim/maf/batches/radar_limited.py +1100 -0
- rubin_sim/maf/batches/science_radar_batch.py +1910 -0
- rubin_sim/maf/batches/skycoverage.py +155 -0
- rubin_sim/maf/batches/slew_batch.py +146 -0
- rubin_sim/maf/batches/srd_batch.py +637 -0
- rubin_sim/maf/batches/time_batch.py +757 -0
- rubin_sim/maf/batches/time_sci_batch.py +105 -0
- rubin_sim/maf/batches/visitdepth_batch.py +485 -0
- rubin_sim/maf/db/__init__.py +2 -0
- rubin_sim/maf/db/add_run.py +34 -0
- rubin_sim/maf/db/results_db.py +1074 -0
- rubin_sim/maf/db/tracking_db.py +332 -0
- rubin_sim/maf/ddf_dir.py +53 -0
- rubin_sim/maf/generate_ss.py +218 -0
- rubin_sim/maf/glance_dir.py +58 -0
- rubin_sim/maf/maf_contrib/__init__.py +25 -0
- rubin_sim/maf/maf_contrib/calculate_lsst_field_visibility_astropy.py +170 -0
- rubin_sim/maf/maf_contrib/depth_limited_num_gal_metric.py +109 -0
- rubin_sim/maf/maf_contrib/example_new_metrics.py +40 -0
- rubin_sim/maf/maf_contrib/filter_pair_t_gaps_metric.py +208 -0
- rubin_sim/maf/maf_contrib/grb_transient_metric.py +249 -0
- rubin_sim/maf/maf_contrib/gw170817_det_metric.py +72 -0
- rubin_sim/maf/maf_contrib/intervals_between_obs_metric.py +68 -0
- rubin_sim/maf/maf_contrib/kne_metrics.py +473 -0
- rubin_sim/maf/maf_contrib/lss_metrics.py +45 -0
- rubin_sim/maf/maf_contrib/lss_obs_strategy/__init__.py +3 -0
- rubin_sim/maf/maf_contrib/lss_obs_strategy/constants_for_pipeline.py +73 -0
- rubin_sim/maf/maf_contrib/lss_obs_strategy/galaxy_counts_metric_extended.py +205 -0
- rubin_sim/maf/maf_contrib/lss_obs_strategy/galaxy_counts_with_pixel_calibration.py +171 -0
- rubin_sim/maf/maf_contrib/lv_dwarfs/__init__.py +1 -0
- rubin_sim/maf/maf_contrib/lv_dwarfs/lv_dwarfs_metrics.py +388 -0
- rubin_sim/maf/maf_contrib/microlensing_metric.py +593 -0
- rubin_sim/maf/maf_contrib/num_obs_in_survey_time_overlap_metric.py +43 -0
- rubin_sim/maf/maf_contrib/periodic_metric.py +51 -0
- rubin_sim/maf/maf_contrib/periodic_star_metric.py +186 -0
- rubin_sim/maf/maf_contrib/periodic_star_modulation_metric.py +236 -0
- rubin_sim/maf/maf_contrib/presto_color_kne_pop_metric.py +441 -0
- rubin_sim/maf/maf_contrib/selfcal_uniformity_metric.py +171 -0
- rubin_sim/maf/maf_contrib/star_count_mass_metric.py +64 -0
- rubin_sim/maf/maf_contrib/star_count_metric.py +56 -0
- rubin_sim/maf/maf_contrib/star_counts/__init__.py +8 -0
- rubin_sim/maf/maf_contrib/star_counts/abs_mag.py +324 -0
- rubin_sim/maf/maf_contrib/star_counts/coords.py +99 -0
- rubin_sim/maf/maf_contrib/star_counts/readme.txt +1 -0
- rubin_sim/maf/maf_contrib/star_counts/spec_type.py +91 -0
- rubin_sim/maf/maf_contrib/star_counts/starcount.py +30 -0
- rubin_sim/maf/maf_contrib/star_counts/starcount_bymass.py +93 -0
- rubin_sim/maf/maf_contrib/star_counts/stellardensity.py +59 -0
- rubin_sim/maf/maf_contrib/static_probes_fom_summary_metric.py +386 -0
- rubin_sim/maf/maf_contrib/tdes_pop_metric.py +362 -0
- rubin_sim/maf/maf_contrib/transient_ascii_sed_metric.py +652 -0
- rubin_sim/maf/maf_contrib/triplet_metric.py +156 -0
- rubin_sim/maf/maf_contrib/var_depth_metric.py +157 -0
- rubin_sim/maf/maf_contrib/xrb_metrics.py +523 -0
- rubin_sim/maf/maf_contrib/young_stellar_objects_metric.py +200 -0
- rubin_sim/maf/maf_night_report.py +200 -0
- rubin_sim/maf/make_fbs_tracking_db.py +90 -0
- rubin_sim/maf/maps/__init__.py +8 -0
- rubin_sim/maf/maps/base_map.py +72 -0
- rubin_sim/maf/maps/create_gaia_density_map.py +158 -0
- rubin_sim/maf/maps/dust_map.py +66 -0
- rubin_sim/maf/maps/dust_map_3d.py +190 -0
- rubin_sim/maf/maps/ebv_3d_hp.py +170 -0
- rubin_sim/maf/maps/ebv_hp.py +64 -0
- rubin_sim/maf/maps/gal_coords_map.py +22 -0
- rubin_sim/maf/maps/galactic_plane_priority_maps.py +210 -0
- rubin_sim/maf/maps/stellar_density_map.py +73 -0
- rubin_sim/maf/maps/trilegal_map.py +82 -0
- rubin_sim/maf/metadata_dir.py +90 -0
- rubin_sim/maf/metric_bundles/__init__.py +3 -0
- rubin_sim/maf/metric_bundles/metric_bundle.py +816 -0
- rubin_sim/maf/metric_bundles/metric_bundle_group.py +834 -0
- rubin_sim/maf/metric_bundles/mo_metric_bundle.py +646 -0
- rubin_sim/maf/metrics/__init__.py +52 -0
- rubin_sim/maf/metrics/agn_time_lag_metric.py +118 -0
- rubin_sim/maf/metrics/agnstructure.py +141 -0
- rubin_sim/maf/metrics/area_summary_metrics.py +120 -0
- rubin_sim/maf/metrics/base_metric.py +215 -0
- rubin_sim/maf/metrics/brown_dwarf_metric.py +162 -0
- rubin_sim/maf/metrics/cadence_metrics.py +400 -0
- rubin_sim/maf/metrics/calibration_metrics.py +618 -0
- rubin_sim/maf/metrics/chip_vendor_metric.py +43 -0
- rubin_sim/maf/metrics/color_slope_metrics.py +189 -0
- rubin_sim/maf/metrics/cosmology_summary_metrics.py +1212 -0
- rubin_sim/maf/metrics/coverage_metric.py +48 -0
- rubin_sim/maf/metrics/crowding_metric.py +255 -0
- rubin_sim/maf/metrics/cumulative_metric.py +46 -0
- rubin_sim/maf/metrics/dcr_metric.py +93 -0
- rubin_sim/maf/metrics/exgal_m5.py +57 -0
- rubin_sim/maf/metrics/fft_metric.py +27 -0
- rubin_sim/maf/metrics/galactic_plane_metrics.py +375 -0
- rubin_sim/maf/metrics/galplane_time_sampling_metrics.py +236 -0
- rubin_sim/maf/metrics/hourglass_metric.py +130 -0
- rubin_sim/maf/metrics/incremental_template_metric.py +255 -0
- rubin_sim/maf/metrics/kuiper_metrics.py +26 -0
- rubin_sim/maf/metrics/mo_metrics.py +1496 -0
- rubin_sim/maf/metrics/mo_summary_metrics.py +464 -0
- rubin_sim/maf/metrics/night_pointing_metric.py +70 -0
- rubin_sim/maf/metrics/optimal_m5_metric.py +94 -0
- rubin_sim/maf/metrics/pair_metric.py +69 -0
- rubin_sim/maf/metrics/periodic_detect_metric.py +133 -0
- rubin_sim/maf/metrics/phase_gap_metric.py +172 -0
- rubin_sim/maf/metrics/qso_number_counts_metric.py +138 -0
- rubin_sim/maf/metrics/scaling_metrics.py +148 -0
- rubin_sim/maf/metrics/schedview_metrics.py +39 -0
- rubin_sim/maf/metrics/season_metrics.py +344 -0
- rubin_sim/maf/metrics/simple_metrics.py +546 -0
- rubin_sim/maf/metrics/sky_sat_metric.py +18 -0
- rubin_sim/maf/metrics/sn_cadence_metric.py +104 -0
- rubin_sim/maf/metrics/sn_n_sn_metric.py +1364 -0
- rubin_sim/maf/metrics/sn_sl_metric.py +216 -0
- rubin_sim/maf/metrics/sn_snr_metric.py +715 -0
- rubin_sim/maf/metrics/snr_weight.py +24 -0
- rubin_sim/maf/metrics/star_density.py +77 -0
- rubin_sim/maf/metrics/string_count_metric.py +95 -0
- rubin_sim/maf/metrics/summary_metrics.py +201 -0
- rubin_sim/maf/metrics/surfb_metric.py +175 -0
- rubin_sim/maf/metrics/technical_metrics.py +271 -0
- rubin_sim/maf/metrics/tgaps.py +412 -0
- rubin_sim/maf/metrics/tomography_models.py +2034 -0
- rubin_sim/maf/metrics/transient_metrics.py +268 -0
- rubin_sim/maf/metrics/uniformity_metrics.py +418 -0
- rubin_sim/maf/metrics/use_metrics.py +55 -0
- rubin_sim/maf/metrics/vector_metrics.py +190 -0
- rubin_sim/maf/metrics/visit_groups_metric.py +341 -0
- rubin_sim/maf/metrics/weak_lensing_systematics_metric.py +210 -0
- rubin_sim/maf/plots/__init__.py +14 -0
- rubin_sim/maf/plots/hg_plotters.py +1117 -0
- rubin_sim/maf/plots/hourglass_plotters.py +120 -0
- rubin_sim/maf/plots/mo_plotters.py +358 -0
- rubin_sim/maf/plots/nd_plotters.py +220 -0
- rubin_sim/maf/plots/neo_distance_plotter.py +140 -0
- rubin_sim/maf/plots/night_pointing_plotter.py +96 -0
- rubin_sim/maf/plots/oned_plotters.py +114 -0
- rubin_sim/maf/plots/perceptual_rainbow.py +30 -0
- rubin_sim/maf/plots/plot_handler.py +766 -0
- rubin_sim/maf/plots/skyproj_plotters.py +540 -0
- rubin_sim/maf/plots/spatial_plotters.py +1029 -0
- rubin_sim/maf/plots/special_plotters.py +256 -0
- rubin_sim/maf/plots/two_d_plotters.py +177 -0
- rubin_sim/maf/plots/xyplotter.py +37 -0
- rubin_sim/maf/run_comparison/__init__.py +5 -0
- rubin_sim/maf/run_comparison/archive.py +584 -0
- rubin_sim/maf/run_comparison/gather_summaries.py +125 -0
- rubin_sim/maf/run_comparison/microlensing_compare.py +514 -0
- rubin_sim/maf/run_comparison/radar_plot.py +202 -0
- rubin_sim/maf/run_comparison/summary_plots.py +710 -0
- rubin_sim/maf/run_moving_calc.py +239 -0
- rubin_sim/maf/run_moving_fractions.py +128 -0
- rubin_sim/maf/run_moving_join.py +103 -0
- rubin_sim/maf/run_selfcal_metric.py +92 -0
- rubin_sim/maf/scimaf_dir.py +87 -0
- rubin_sim/maf/show_maf.py +227 -0
- rubin_sim/maf/slicers/__init__.py +13 -0
- rubin_sim/maf/slicers/base_slicer.py +513 -0
- rubin_sim/maf/slicers/base_spatial_slicer.py +202 -0
- rubin_sim/maf/slicers/healpix_sdss_slicer.py +118 -0
- rubin_sim/maf/slicers/healpix_slicer.py +189 -0
- rubin_sim/maf/slicers/healpix_subset_slicer.py +293 -0
- rubin_sim/maf/slicers/hourglass_slicer.py +37 -0
- rubin_sim/maf/slicers/mo_slicer.py +171 -0
- rubin_sim/maf/slicers/movie_slicer.py +288 -0
- rubin_sim/maf/slicers/nd_slicer.py +147 -0
- rubin_sim/maf/slicers/one_d_slicer.py +210 -0
- rubin_sim/maf/slicers/time_interval_slicers.py +283 -0
- rubin_sim/maf/slicers/uni_slicer.py +57 -0
- rubin_sim/maf/slicers/user_points_slicer.py +141 -0
- rubin_sim/maf/stackers/__init__.py +13 -0
- rubin_sim/maf/stackers/base_stacker.py +221 -0
- rubin_sim/maf/stackers/coord_stackers.py +224 -0
- rubin_sim/maf/stackers/date_stackers.py +192 -0
- rubin_sim/maf/stackers/general_stackers.py +683 -0
- rubin_sim/maf/stackers/get_col_info.py +104 -0
- rubin_sim/maf/stackers/label_stackers.py +114 -0
- rubin_sim/maf/stackers/m5_optimal_stacker.py +126 -0
- rubin_sim/maf/stackers/mo_phase.py +265 -0
- rubin_sim/maf/stackers/mo_stackers.py +486 -0
- rubin_sim/maf/stackers/n_follow_stacker.py +187 -0
- rubin_sim/maf/stackers/neo_dist_stacker.py +126 -0
- rubin_sim/maf/stackers/sdss_stackers.py +30 -0
- rubin_sim/maf/stackers/sn_stacker.py +152 -0
- rubin_sim/maf/stackers/teff_stacker.py +154 -0
- rubin_sim/maf/utils/__init__.py +6 -0
- rubin_sim/maf/utils/astrometry_utils.py +93 -0
- rubin_sim/maf/utils/generate_fov_map.py +58 -0
- rubin_sim/maf/utils/get_date_version.py +28 -0
- rubin_sim/maf/utils/maf_utils.py +212 -0
- rubin_sim/maf/utils/opsim_utils.py +259 -0
- rubin_sim/maf/utils/output_utils.py +34 -0
- rubin_sim/maf/utils/sn_n_sn_utils.py +894 -0
- rubin_sim/maf/utils/sn_utils.py +369 -0
- rubin_sim/maf/utils/stellar_mags.py +197 -0
- rubin_sim/maf/web/__init__.py +2 -0
- rubin_sim/maf/web/favicon.ico +0 -0
- rubin_sim/maf/web/maf_run_results.py +645 -0
- rubin_sim/maf/web/maf_tracking.py +139 -0
- rubin_sim/maf/web/sorttable.js +496 -0
- rubin_sim/maf/web/templates/allmetricresults.html +72 -0
- rubin_sim/maf/web/templates/configs.html +28 -0
- rubin_sim/maf/web/templates/macros.html +243 -0
- rubin_sim/maf/web/templates/master.html +294 -0
- rubin_sim/maf/web/templates/metricselect.html +91 -0
- rubin_sim/maf/web/templates/multicolor.html +147 -0
- rubin_sim/maf/web/templates/results.html +82 -0
- rubin_sim/maf/web/templates/runselect.html +58 -0
- rubin_sim/maf/web/templates/stats.html +50 -0
- rubin_sim/moving_objects/__init__.py +8 -0
- rubin_sim/moving_objects/base_obs.py +468 -0
- rubin_sim/moving_objects/cheby_fits.py +689 -0
- rubin_sim/moving_objects/cheby_values.py +231 -0
- rubin_sim/moving_objects/chebyshev_utils.py +365 -0
- rubin_sim/moving_objects/direct_obs.py +376 -0
- rubin_sim/moving_objects/make_lsst_obs.py +282 -0
- rubin_sim/moving_objects/ooephemerides.py +643 -0
- rubin_sim/moving_objects/orbits.py +507 -0
- rubin_sim/moving_objects/pre_generate.py +37 -0
- rubin_sim/moving_objects/utils.py +61 -0
- rubin_sim/phot_utils/__init__.py +7 -0
- rubin_sim/phot_utils/bandpass.py +425 -0
- rubin_sim/phot_utils/photometric_parameters.py +449 -0
- rubin_sim/phot_utils/physical_parameters.py +50 -0
- rubin_sim/phot_utils/predicted_zeropoints.py +245 -0
- rubin_sim/phot_utils/sed.py +1773 -0
- rubin_sim/phot_utils/sed_utils.py +52 -0
- rubin_sim/phot_utils/signaltonoise.py +695 -0
- rubin_sim/phot_utils/spectral_resampling.py +161 -0
- rubin_sim/satellite_constellations/__init__.py +3 -0
- rubin_sim/satellite_constellations/basis_function.py +40 -0
- rubin_sim/satellite_constellations/model_observatory.py +157 -0
- rubin_sim/satellite_constellations/sat_utils.py +488 -0
- rubin_sim/selfcal/__init__.py +4 -0
- rubin_sim/selfcal/generate_catalog.py +188 -0
- rubin_sim/selfcal/offsets.py +119 -0
- rubin_sim/selfcal/solver.py +143 -0
- rubin_sim/selfcal/star_tools.py +42 -0
- rubin_sim/sim_archive/__init__.py +14 -0
- rubin_sim/sim_archive/conda_packages.py +65 -0
- rubin_sim/sim_archive/future_vsarch.py +480 -0
- rubin_sim/sim_archive/make_snapshot.py +255 -0
- rubin_sim/sim_archive/prenight.py +216 -0
- rubin_sim/sim_archive/prenightindex.py +229 -0
- rubin_sim/sim_archive/prototype.py +1029 -0
- rubin_sim/sim_archive/sim_archive.py +680 -0
- rubin_sim/sim_archive/tempdb.py +56 -0
- rubin_sim/sim_archive/util.py +146 -0
- rubin_sim/sim_archive/vseqarchive.py +965 -0
- rubin_sim/sim_archive/vseqmetadata.py +1721 -0
- rubin_sim/skybrightness/__init__.py +5 -0
- rubin_sim/skybrightness/allsky_db.py +93 -0
- rubin_sim/skybrightness/data/ESO_Spectra/eso_tools.py +505 -0
- rubin_sim/skybrightness/data/solarSpec/package.py +22 -0
- rubin_sim/skybrightness/generate_hdf5.py +243 -0
- rubin_sim/skybrightness/interp_components.py +798 -0
- rubin_sim/skybrightness/sky_model.py +771 -0
- rubin_sim/skybrightness/twilight_func.py +83 -0
- rubin_sim/skybrightness/utils.py +91 -0
- rubin_sim-2.6.1.dev2.dist-info/METADATA +95 -0
- rubin_sim-2.6.1.dev2.dist-info/RECORD +387 -0
- rubin_sim-2.6.1.dev2.dist-info/WHEEL +5 -0
- rubin_sim-2.6.1.dev2.dist-info/entry_points.txt +24 -0
- rubin_sim-2.6.1.dev2.dist-info/licenses/LICENSE +674 -0
- rubin_sim-2.6.1.dev2.dist-info/top_level.txt +5 -0
- sql/make_vseqmeta.sql +251 -0
- tests/data/test_data.py +26 -0
- tests/data/test_ddf_grid.py +17 -0
- tests/maf/test_3x2fom.py +68 -0
- tests/maf/test_archive.py +162 -0
- tests/maf/test_basemetrics.py +99 -0
- tests/maf/test_batchcommon.py +36 -0
- tests/maf/test_batches.py +159 -0
- tests/maf/test_cadencemetrics.py +388 -0
- tests/maf/test_calibrationmetrics.py +242 -0
- tests/maf/test_color_slopes.py +71 -0
- tests/maf/test_cosmology_summaries.py +20 -0
- tests/maf/test_gathersummaries.py +48 -0
- tests/maf/test_healpixslicer.py +439 -0
- tests/maf/test_healpixsubsetslicer.py +273 -0
- tests/maf/test_hgplotters.py +145 -0
- tests/maf/test_hourglassmetric.py +33 -0
- tests/maf/test_io.py +187 -0
- tests/maf/test_json.py +169 -0
- tests/maf/test_maps.py +199 -0
- tests/maf/test_metricbundle.py +69 -0
- tests/maf/test_mometrics.py +422 -0
- tests/maf/test_movieslicer.py +216 -0
- tests/maf/test_ndslicer.py +240 -0
- tests/maf/test_neodistanceplotter.py +41 -0
- tests/maf/test_onedslicer.py +245 -0
- tests/maf/test_opsimutils.py +80 -0
- tests/maf/test_plotters.py +134 -0
- tests/maf/test_resultsdb.py +145 -0
- tests/maf/test_schedviewmetrics.py +14 -0
- tests/maf/test_simplemetrics.py +200 -0
- tests/maf/test_snmetrics.py +335 -0
- tests/maf/test_snmetrics_nsn.py +123 -0
- tests/maf/test_stackers.py +423 -0
- tests/maf/test_stellarmags.py +50 -0
- tests/maf/test_stringcount.py +26 -0
- tests/maf/test_summary_plots.py +197 -0
- tests/maf/test_summarymetrics.py +82 -0
- tests/maf/test_technicalmetrics.py +267 -0
- tests/maf/test_templatemetrics.py +86 -0
- tests/maf/test_timeintervalslicers.py +97 -0
- tests/maf/test_trackingdb.py +88 -0
- tests/maf/test_unislicer.py +116 -0
- tests/maf/test_vectormetrics.py +225 -0
- tests/maf/test_visitgroupsmetric.py +181 -0
- tests/moving_objects/test_camera.py +39 -0
- tests/moving_objects/test_chebyfits.py +188 -0
- tests/moving_objects/test_chebyshevutils.py +76 -0
- tests/moving_objects/test_chebyvalues.py +245 -0
- tests/moving_objects/test_ephemerides.py +219 -0
- tests/moving_objects/test_orbits.py +170 -0
- tests/moving_objects/test_sed_data.py +61 -0
- tests/phot_utils/test_approximatebandpasses.py +91 -0
- tests/phot_utils/test_photometricparameters.py +268 -0
- tests/phot_utils/test_photometry.py +61 -0
- tests/phot_utils/test_predicted_zeropoints.py +53 -0
- tests/phot_utils/test_read_bandpasses.py +29 -0
- tests/phot_utils/test_sed.py +368 -0
- tests/phot_utils/test_snr.py +370 -0
- tests/satellite_constellations/test_satellites.py +36 -0
- tests/sim_archive/test_make_snapshot.py +24 -0
- tests/sim_archive/test_prototype_sim_archive.py +204 -0
- tests/sim_archive/test_vseqarchive.py +947 -0
- tests/skybrightness/test_skymodel.py +359 -0
|
@@ -0,0 +1,1721 @@
|
|
|
1
|
+
__all__ = ["compute_visits_sha256", "VisitSequenceArchiveMetadata"]
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import warnings
|
|
8
|
+
from datetime import date, datetime, timedelta, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from tempfile import TemporaryDirectory
|
|
11
|
+
from typing import Any, Dict, Mapping, Tuple
|
|
12
|
+
from uuid import UUID
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import pandas as pd
|
|
16
|
+
import psycopg2
|
|
17
|
+
import psycopg2.extras
|
|
18
|
+
import psycopg2.pool
|
|
19
|
+
import sqlalchemy
|
|
20
|
+
import yaml
|
|
21
|
+
from astropy.time import Time
|
|
22
|
+
from lsst.resources import ResourcePath
|
|
23
|
+
from psycopg2 import sql
|
|
24
|
+
|
|
25
|
+
from .util import compute_conda_env, dayobs_to_date, opsimdb_to_hdf5
|
|
26
|
+
|
|
27
|
+
VSARCHIVE_PGDATABASE = "opsim_log"
|
|
28
|
+
VSARCHIVE_PGHOST = "usdf-maf-visit-seq-archive-tx-ro.sdf.slac.stanford.edu"
|
|
29
|
+
VSARCHIVE_PGUSER = "reader"
|
|
30
|
+
VSARCHIVE_PGSCHEMA = "vsmd"
|
|
31
|
+
|
|
32
|
+
JSON_DUMP_LIMIT = 4096
|
|
33
|
+
|
|
34
|
+
LOGGER = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
psycopg2.extras.register_uuid()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def compute_visits_sha256(visits: pd.DataFrame) -> bytes:
|
|
40
|
+
recs = visits.to_records()
|
|
41
|
+
visitseq_hash = hashlib.sha256(str(recs.dtype).encode())
|
|
42
|
+
visitseq_hash.update(np.ascontiguousarray(recs).data.tobytes())
|
|
43
|
+
visitseq_sha256 = bytes.fromhex(visitseq_hash.hexdigest())
|
|
44
|
+
return visitseq_sha256
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def metadata_yaml_from_prototype(
|
|
48
|
+
sim_date: str | date,
|
|
49
|
+
sim_index: str | int,
|
|
50
|
+
proto_sim_archive_uri: str = "s3://rubin:rubin-scheduler-prenight/opsim/",
|
|
51
|
+
) -> str:
|
|
52
|
+
"""Return a sim metadata YAML derived from the prototype sim archive.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
sim_date : `str` or `datetime.date`
|
|
57
|
+
The simulation date in ISO format or a ``date`` object.
|
|
58
|
+
sim_index : `str` or `int`
|
|
59
|
+
The simulation index identifying the specific simulation
|
|
60
|
+
within the date directory.
|
|
61
|
+
proto_sim_archive_uri : `str`, optional
|
|
62
|
+
Base URI of the simulation archive.
|
|
63
|
+
Defaults to the prototype location in the S3 bucket.
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
sim_metadata: `str`
|
|
68
|
+
`yaml` representation of the simulation metadata.
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
if isinstance(sim_date, date):
|
|
72
|
+
sim_date = sim_date.isoformat()
|
|
73
|
+
assert isinstance(sim_date, str)
|
|
74
|
+
|
|
75
|
+
sim_archive_rp = (
|
|
76
|
+
ResourcePath(proto_sim_archive_uri)
|
|
77
|
+
.join(sim_date, forceDirectory=True)
|
|
78
|
+
.join(str(sim_index), forceDirectory=True)
|
|
79
|
+
)
|
|
80
|
+
sims_found = sim_archive_rp.exists() and sim_archive_rp.join("sim_metadata.yaml").exists()
|
|
81
|
+
if not sim_archive_rp.exists():
|
|
82
|
+
raise ValueError(f"No simulations found at {sim_archive_rp.geturl()}.")
|
|
83
|
+
|
|
84
|
+
if not sim_archive_rp.join("sim_metadata.yaml").exists():
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"No simulation metadata found at {sim_archive_rp.join('sim_metadata.yaml').geturl()}"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if sims_found:
|
|
90
|
+
metadata = yaml.safe_load(sim_archive_rp.join("sim_metadata.yaml").read().decode())
|
|
91
|
+
else:
|
|
92
|
+
raise ValueError("No simulation found")
|
|
93
|
+
|
|
94
|
+
if "label" not in metadata:
|
|
95
|
+
raise ValueError("Metadata yaml must include a label")
|
|
96
|
+
|
|
97
|
+
label = metadata["label"]
|
|
98
|
+
|
|
99
|
+
# If there is no creation time given, try extracting it from the label
|
|
100
|
+
if "creation_time" not in metadata:
|
|
101
|
+
# The prenight labels usually end in the creation time.
|
|
102
|
+
if " ".join(label.split()[-3:-1]) == "run at":
|
|
103
|
+
maybe_date_str = label.split()[-1]
|
|
104
|
+
is_date_str = False
|
|
105
|
+
try:
|
|
106
|
+
datetime.fromisoformat(maybe_date_str)
|
|
107
|
+
is_date_str = True
|
|
108
|
+
except ValueError:
|
|
109
|
+
is_date_str = False
|
|
110
|
+
|
|
111
|
+
if is_date_str:
|
|
112
|
+
metadata["creation_time"] = maybe_date_str
|
|
113
|
+
|
|
114
|
+
# If we still do not have a creation time, invent one from the date
|
|
115
|
+
# and index.
|
|
116
|
+
if "creation_time" not in metadata:
|
|
117
|
+
creation_time = datetime.fromisoformat(sim_date + "T12:00:00Z") + timedelta(seconds=int(sim_index))
|
|
118
|
+
metadata["creation_time"] = creation_time.isoformat()
|
|
119
|
+
|
|
120
|
+
if "files" in metadata:
|
|
121
|
+
for file_type in metadata["files"]:
|
|
122
|
+
if "url" not in metadata["files"][file_type]:
|
|
123
|
+
fname = metadata["files"][file_type]["name"]
|
|
124
|
+
metadata["files"][file_type]["url"] = sim_archive_rp.join(fname).geturl()
|
|
125
|
+
|
|
126
|
+
metadata_yaml = yaml.dump(metadata)
|
|
127
|
+
|
|
128
|
+
return metadata_yaml
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class VisitSequenceArchiveMetadata:
|
|
132
|
+
"""Interface to metadata database that tracks sequences
|
|
133
|
+
of visits.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
metadata_db_kwargs: `Mapping` or `None`
|
|
138
|
+
A dictionary or other mapping defining the connection
|
|
139
|
+
parameters for connecting to the postgresql database
|
|
140
|
+
that holds the sequence metadata. Keys are passed as keyword
|
|
141
|
+
arguments to ``psycopg2.pool.SimpleConnectionPool``.
|
|
142
|
+
If ``None``, the dictionary is built from the environment
|
|
143
|
+
variables ``VSARCHIVE_PGDATABASE``, ``VSARCHIVE_PGHOST``,
|
|
144
|
+
``VSARCHIVE_PGUSER``, and ``VSARCHIVE_PGPORT`` (if they
|
|
145
|
+
exist) or corresponding module variables in
|
|
146
|
+
``rubin_sim.sim_archive.vseqarchiv`` (if they do not).
|
|
147
|
+
metadata_db_schema: `str` or `None`
|
|
148
|
+
The schema in the database holding the metadata. If
|
|
149
|
+
``None``, defaults to
|
|
150
|
+
``rubin_sim.sim_archive.vseqarchive.VSARCHIVE_PGSCHEMA``.
|
|
151
|
+
Default is ``None``.
|
|
152
|
+
|
|
153
|
+
Notes
|
|
154
|
+
-----
|
|
155
|
+
The visit sequence archive consists of two primary architectural
|
|
156
|
+
components: an archive in which files associated with a sequence
|
|
157
|
+
of visits are stored (including, optionally, the table of visits
|
|
158
|
+
themselves); and postegresql database with tables with metadata
|
|
159
|
+
about the sequneces of visits, including the
|
|
160
|
+
provenance of the visits, the locations of the files associated
|
|
161
|
+
with each sequence of visits, comments and tags associated with
|
|
162
|
+
each sequence of visits, and basic statistics of the visits.
|
|
163
|
+
This class is an interface to the postgresql metadata database.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
metadata_db_kwargs: Mapping | None = None,
|
|
169
|
+
metadata_db_schema: str | None = None,
|
|
170
|
+
):
|
|
171
|
+
if metadata_db_schema is None:
|
|
172
|
+
metadata_db_schema = VSARCHIVE_PGSCHEMA
|
|
173
|
+
assert isinstance(metadata_db_schema, str)
|
|
174
|
+
|
|
175
|
+
if not isinstance(metadata_db_kwargs, dict):
|
|
176
|
+
metadata_db_kwargs = {} if metadata_db_kwargs is None else dict(metadata_db_kwargs)
|
|
177
|
+
assert isinstance(metadata_db_kwargs, dict)
|
|
178
|
+
|
|
179
|
+
if "database" not in metadata_db_kwargs:
|
|
180
|
+
if "VSARCHIVE_PGDATABASE" in os.environ:
|
|
181
|
+
metadata_db_kwargs["database"] = os.environ["VSARCHIVE_PGDATABASE"]
|
|
182
|
+
else:
|
|
183
|
+
metadata_db_kwargs["database"] = VSARCHIVE_PGDATABASE
|
|
184
|
+
|
|
185
|
+
if "host" not in metadata_db_kwargs:
|
|
186
|
+
if "VSARCHIVE_PGHOST" in os.environ:
|
|
187
|
+
metadata_db_kwargs["host"] = os.environ["VSARCHIVE_PGHOST"]
|
|
188
|
+
else:
|
|
189
|
+
metadata_db_kwargs["host"] = VSARCHIVE_PGHOST
|
|
190
|
+
|
|
191
|
+
if "user" not in metadata_db_kwargs:
|
|
192
|
+
if "VSARCHIVE_PGUSER" in os.environ:
|
|
193
|
+
metadata_db_kwargs["user"] = os.environ["VSARCHIVE_PGUSER"]
|
|
194
|
+
else:
|
|
195
|
+
metadata_db_kwargs["user"] = VSARCHIVE_PGUSER
|
|
196
|
+
|
|
197
|
+
if "port" not in metadata_db_kwargs and "VSARCHIVE_PGPORT" in os.environ:
|
|
198
|
+
metadata_db_kwargs["port"] = os.environ["VSARCHIVE_PGPORT"]
|
|
199
|
+
|
|
200
|
+
self.pg_pool = psycopg2.pool.SimpleConnectionPool(1, 5, **metadata_db_kwargs)
|
|
201
|
+
# On some operations, pandas does not
|
|
202
|
+
# work well directly with the psycopg2
|
|
203
|
+
# connection, but can work with sqlalchemy.
|
|
204
|
+
# Make and alchemy engine that acts as
|
|
205
|
+
# a middle-man between pandas and psycopg2,
|
|
206
|
+
# using the same connection pool.
|
|
207
|
+
# There is a potential issue in that SA
|
|
208
|
+
# does not return connections to the
|
|
209
|
+
# psycopg pool when it is done. But, it
|
|
210
|
+
# has its own pool, and so in expected use
|
|
211
|
+
# it will just get one and keep reusing it.
|
|
212
|
+
self.sa_engine = sqlalchemy.create_engine(
|
|
213
|
+
"postgresql+psycopg2://", creator=self.pg_pool.getconn, pool_pre_ping=True
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
self.metadata_db_schema: str = metadata_db_schema
|
|
217
|
+
|
|
218
|
+
def query(
|
|
219
|
+
self,
|
|
220
|
+
query: str | sql.SQL | sql.Composed,
|
|
221
|
+
data: dict = {},
|
|
222
|
+
commit: bool = False,
|
|
223
|
+
return_result: bool = True,
|
|
224
|
+
) -> tuple:
|
|
225
|
+
"""Run a simple query on the visit sequence database.
|
|
226
|
+
|
|
227
|
+
Parameters
|
|
228
|
+
----------
|
|
229
|
+
query : `str` or `sql.SQL`
|
|
230
|
+
The query to send
|
|
231
|
+
data : `dict`
|
|
232
|
+
Data to include in the query
|
|
233
|
+
commit : `bool`
|
|
234
|
+
Commit the query (e.g. for an INSERT), defaults to False
|
|
235
|
+
return_result : `bool`
|
|
236
|
+
Return the result, defaults to True
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
result : `tuple`
|
|
241
|
+
The result of the query
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
connection = None
|
|
245
|
+
try:
|
|
246
|
+
# Get a connection from the pool
|
|
247
|
+
connection = self.pg_pool.getconn()
|
|
248
|
+
with connection.cursor() as cursor:
|
|
249
|
+
search_path_query = sql.SQL("SET search_path TO {};").format(
|
|
250
|
+
sql.Identifier(self.metadata_db_schema)
|
|
251
|
+
)
|
|
252
|
+
cursor.execute(search_path_query)
|
|
253
|
+
|
|
254
|
+
if len(data) > 0:
|
|
255
|
+
cursor.execute(query, data)
|
|
256
|
+
else:
|
|
257
|
+
cursor.execute(query)
|
|
258
|
+
result = cursor.fetchall() if return_result else (None,)
|
|
259
|
+
if commit:
|
|
260
|
+
connection.commit()
|
|
261
|
+
else:
|
|
262
|
+
# If a commit was not requested, make sure
|
|
263
|
+
# there were no alterations to the database
|
|
264
|
+
# in this transaction.
|
|
265
|
+
connection.rollback()
|
|
266
|
+
except Exception as e:
|
|
267
|
+
# If something went wrong, restore the
|
|
268
|
+
# connection to a stable state before
|
|
269
|
+
# returning it to the pool.
|
|
270
|
+
if connection:
|
|
271
|
+
try:
|
|
272
|
+
connection.rollback()
|
|
273
|
+
except Exception:
|
|
274
|
+
pass
|
|
275
|
+
LOGGER.exception("Failed to execute query: %s", e)
|
|
276
|
+
raise
|
|
277
|
+
finally:
|
|
278
|
+
# Make sure we always return the connection to
|
|
279
|
+
# the pool.
|
|
280
|
+
if connection:
|
|
281
|
+
self.pg_pool.putconn(connection)
|
|
282
|
+
|
|
283
|
+
return result
|
|
284
|
+
|
|
285
|
+
def create_schema_in_database(self) -> None:
|
|
286
|
+
"""Create the visit sequence metadata schema in a database."""
|
|
287
|
+
|
|
288
|
+
# Creation of the production schema should be
|
|
289
|
+
# a one-off execution.
|
|
290
|
+
assert "test" in self.metadata_db_schema
|
|
291
|
+
|
|
292
|
+
# Make sure the schema does not already exist
|
|
293
|
+
schema_test_query = sql.SQL(
|
|
294
|
+
"SELECT EXISTS(SELECT * FROM information_schema.schemata WHERE schema_name = {});"
|
|
295
|
+
).format(sql.Placeholder("schema"))
|
|
296
|
+
data = {"schema": self.metadata_db_schema}
|
|
297
|
+
schema_exists_return = self.query(schema_test_query, data, return_result=True)
|
|
298
|
+
schema_exists = schema_exists_return[0][0]
|
|
299
|
+
if schema_exists:
|
|
300
|
+
raise ValueError("Schema already exists.")
|
|
301
|
+
|
|
302
|
+
creation_sql_file = Path(__file__).parent.parent.parent / "sql" / "make_vseqmeta.sql"
|
|
303
|
+
with open(creation_sql_file, "r") as f:
|
|
304
|
+
query_template = f.read()
|
|
305
|
+
|
|
306
|
+
# Sanity check what we read.
|
|
307
|
+
assert "CREATE SCHEMA" in query_template
|
|
308
|
+
|
|
309
|
+
query = sql.SQL(query_template).format(
|
|
310
|
+
sql.Identifier(self.metadata_db_schema), sql.Identifier(self.metadata_db_schema)
|
|
311
|
+
)
|
|
312
|
+
self.query(query, {}, commit=True, return_result=False)
|
|
313
|
+
print("Created test database and schema ", self.metadata_db_schema)
|
|
314
|
+
|
|
315
|
+
def pd_read_sql(
|
|
316
|
+
self,
|
|
317
|
+
query_template: str,
|
|
318
|
+
sql_params: list[sql.Composable] | None = None,
|
|
319
|
+
query_params: tuple | None = None,
|
|
320
|
+
) -> pd.DataFrame:
|
|
321
|
+
"""Execute a SQL query using the internal PostgreSQL connection pool
|
|
322
|
+
and return the results as a pandas DataFrame.
|
|
323
|
+
|
|
324
|
+
Parameters
|
|
325
|
+
----------
|
|
326
|
+
query_template : `str`
|
|
327
|
+
A query template.
|
|
328
|
+
sql_params : `list` [`sql.Composable`]
|
|
329
|
+
Elements that will be substituted into the template (e.g. table or
|
|
330
|
+
schema identifiers) using ``psycopg2.sql.SQL.format``.
|
|
331
|
+
To make type checkers happy, you may need to explictly declare
|
|
332
|
+
variables passed as ``list[sql.Composable]``.
|
|
333
|
+
query_params : `tuple`
|
|
334
|
+
Positional parameters that will be bound by sqlalchemy.
|
|
335
|
+
|
|
336
|
+
Returns
|
|
337
|
+
-------
|
|
338
|
+
df : `pd.DataFrame`
|
|
339
|
+
The result set of the query, wrapped in a DataFrame.
|
|
340
|
+
|
|
341
|
+
Notes
|
|
342
|
+
-----
|
|
343
|
+
This works around limitations in pandas when dealing with
|
|
344
|
+
complex datatypes in raw postgresql connections.
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
if sql_params is None:
|
|
348
|
+
sql_params = []
|
|
349
|
+
assert isinstance(sql_params, list)
|
|
350
|
+
|
|
351
|
+
if query_params is None:
|
|
352
|
+
query_params = tuple([])
|
|
353
|
+
assert isinstance(query_params, tuple)
|
|
354
|
+
|
|
355
|
+
# Pandas sometimes has trouble with postgresql when working with it
|
|
356
|
+
# directly, so interact with it by way of sqlalchemy.
|
|
357
|
+
# But, we need to use postgresql's tools for inserting identifiers
|
|
358
|
+
# like the schema name, followed by the sqlalchemy templating
|
|
359
|
+
# for actual parameters.
|
|
360
|
+
query = sql.SQL(query_template).format(*sql_params)
|
|
361
|
+
conn = None
|
|
362
|
+
try:
|
|
363
|
+
conn = self.pg_pool.getconn()
|
|
364
|
+
text_query = query.as_string(conn)
|
|
365
|
+
finally:
|
|
366
|
+
if conn:
|
|
367
|
+
self.pg_pool.putconn(conn)
|
|
368
|
+
|
|
369
|
+
with self.sa_engine.connect() as sa_conn:
|
|
370
|
+
df = pd.read_sql(text_query, sa_conn, params=query_params)
|
|
371
|
+
|
|
372
|
+
return df
|
|
373
|
+
|
|
374
|
+
def record_visitseq_metadata(
|
|
375
|
+
self,
|
|
376
|
+
visits: pd.DataFrame,
|
|
377
|
+
label: str,
|
|
378
|
+
telescope: str = "simonyi",
|
|
379
|
+
table: str = "visitseq",
|
|
380
|
+
url: str | None = None,
|
|
381
|
+
first_day_obs: date | int | str | None = None,
|
|
382
|
+
last_day_obs: date | int | str | None = None,
|
|
383
|
+
creation_time: Time | None = None,
|
|
384
|
+
) -> UUID:
|
|
385
|
+
"""Record metadata for a new visit sequence.
|
|
386
|
+
|
|
387
|
+
Parameters
|
|
388
|
+
----------
|
|
389
|
+
visits : `pd.DataFrame`
|
|
390
|
+
A DataFrame of visits, with column names following those
|
|
391
|
+
in consdb.
|
|
392
|
+
label : `str`
|
|
393
|
+
A label for the sequence.
|
|
394
|
+
telescope : `str`
|
|
395
|
+
The telescope used, either "simonyi" or "auxtel".
|
|
396
|
+
Defaults to "simonyi".
|
|
397
|
+
table : `str`
|
|
398
|
+
The table in the metadata archive database in which
|
|
399
|
+
to insert the entry: one of ``simulations``,
|
|
400
|
+
``completed``, ``mixedvisitseq``, or ``visitseq``.
|
|
401
|
+
url : `str`, optional
|
|
402
|
+
A URL for the file from thich the table of visits can
|
|
403
|
+
be download.
|
|
404
|
+
first_day_obs : `date` or `int` or `str`, optional
|
|
405
|
+
The first night of observations, defined according
|
|
406
|
+
to SITCOMTN-032 (UTC-12hrs).
|
|
407
|
+
last_day_obs : `date` or `int` or `str`, optional
|
|
408
|
+
The last day of observations, defined according
|
|
409
|
+
to SITCOMTN-032 (UTC-12hrs).
|
|
410
|
+
creation_time : `Time`, optional
|
|
411
|
+
The time the sequence was created, defaults no now.
|
|
412
|
+
|
|
413
|
+
Returns
|
|
414
|
+
-------
|
|
415
|
+
visitseq_uuid : `UUID`
|
|
416
|
+
The UUID of the new visit sequence.
|
|
417
|
+
|
|
418
|
+
Notes
|
|
419
|
+
-----
|
|
420
|
+
This method is not normally used externally, but may be
|
|
421
|
+
occasionally needed to add non-standard sequences of visits.
|
|
422
|
+
Standard simulations and completed sets of visits should be
|
|
423
|
+
registered with the ``record_simulation_metadata`` and
|
|
424
|
+
``record_completed_metadata`` methods instead.
|
|
425
|
+
"""
|
|
426
|
+
|
|
427
|
+
sha256 = compute_visits_sha256(visits)
|
|
428
|
+
|
|
429
|
+
columns = [
|
|
430
|
+
sql.Identifier("visitseq_sha256"),
|
|
431
|
+
sql.Identifier("visitseq_label"),
|
|
432
|
+
sql.Identifier("telescope"),
|
|
433
|
+
]
|
|
434
|
+
data_placeholders = [
|
|
435
|
+
sql.Placeholder("visitseq_sha256"),
|
|
436
|
+
sql.Placeholder("visitseq_label"),
|
|
437
|
+
sql.Placeholder("telescope"),
|
|
438
|
+
]
|
|
439
|
+
data: Dict[str, Any] = {"visitseq_sha256": sha256, "visitseq_label": label, "telescope": telescope}
|
|
440
|
+
|
|
441
|
+
if url is not None:
|
|
442
|
+
columns.append(sql.Identifier("visitseq_url"))
|
|
443
|
+
data_placeholders.append(sql.Placeholder("visitseq_url"))
|
|
444
|
+
data["visitseq_url"] = url
|
|
445
|
+
|
|
446
|
+
if first_day_obs is not None:
|
|
447
|
+
columns.append(sql.Identifier("first_day_obs"))
|
|
448
|
+
data_placeholders.append(sql.Placeholder("first_day_obs"))
|
|
449
|
+
data["first_day_obs"] = dayobs_to_date(first_day_obs)
|
|
450
|
+
|
|
451
|
+
if last_day_obs is not None:
|
|
452
|
+
columns.append(sql.Identifier("last_day_obs"))
|
|
453
|
+
data_placeholders.append(sql.Placeholder("last_day_obs"))
|
|
454
|
+
data["last_day_obs"] = dayobs_to_date(last_day_obs)
|
|
455
|
+
|
|
456
|
+
if creation_time is not None:
|
|
457
|
+
assert not creation_time.masked
|
|
458
|
+
assert creation_time.isscalar
|
|
459
|
+
try:
|
|
460
|
+
creation_datetime = creation_time.utc[0].datetime
|
|
461
|
+
except AttributeError:
|
|
462
|
+
# astropy Times, even those for which .isscalar
|
|
463
|
+
# is true, can sometimes need to be indexed
|
|
464
|
+
# by 0 to get a true scalar, and sometimes
|
|
465
|
+
# cannot be.
|
|
466
|
+
creation_utc = creation_time.utc
|
|
467
|
+
assert isinstance(creation_utc, Time)
|
|
468
|
+
creation_datetime = creation_utc.datetime
|
|
469
|
+
columns.append(sql.Identifier("creation_time"))
|
|
470
|
+
data_placeholders.append(sql.Placeholder("creation_time"))
|
|
471
|
+
data["creation_time"] = creation_datetime
|
|
472
|
+
|
|
473
|
+
query = sql.SQL("INSERT INTO {}.{} ({}) VALUES ({}) RETURNING visitseq_uuid").format(
|
|
474
|
+
sql.Identifier(self.metadata_db_schema),
|
|
475
|
+
sql.Identifier(table),
|
|
476
|
+
sql.SQL(", ").join(columns),
|
|
477
|
+
sql.SQL(", ").join(data_placeholders),
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
result = self.query(query, data, commit=True)[0][0]
|
|
481
|
+
return result
|
|
482
|
+
|
|
483
|
+
def get_visitseq_metadata(
|
|
484
|
+
self,
|
|
485
|
+
visitseq_uuid: UUID,
|
|
486
|
+
table: str = "visitseq",
|
|
487
|
+
) -> pd.Series:
|
|
488
|
+
"""Retrieve metadata for a visit sequence.
|
|
489
|
+
|
|
490
|
+
Parameters
|
|
491
|
+
----------
|
|
492
|
+
visitseq_uuid : `UUID`
|
|
493
|
+
The UUID of the visit sequence to retrieve metadata for.
|
|
494
|
+
table : `str`, optional
|
|
495
|
+
The table in the metadata archive database in which to search.
|
|
496
|
+
Must be one of "visitseq", "mixedvisitseq", "completed", or
|
|
497
|
+
"simulations".
|
|
498
|
+
Defaults to "visitseq".
|
|
499
|
+
|
|
500
|
+
Returns
|
|
501
|
+
-------
|
|
502
|
+
visitseq : `pd.Series`
|
|
503
|
+
A Pandas Series containing the metadata for the visit sequence.
|
|
504
|
+
|
|
505
|
+
Raises
|
|
506
|
+
------
|
|
507
|
+
ValueError
|
|
508
|
+
If the table is not one of "visitseq", "mixedvisitseq",
|
|
509
|
+
"completed", or "simulations".
|
|
510
|
+
"""
|
|
511
|
+
if table not in {"visitseq", "mixedvisitseq", "completed", "simulations", "simulations_extra"}:
|
|
512
|
+
raise ValueError()
|
|
513
|
+
|
|
514
|
+
query_template = "SELECT * FROM {}.{} WHERE visitseq_uuid=%s"
|
|
515
|
+
sql_params: list[sql.Composable] = [sql.Identifier(self.metadata_db_schema), sql.Identifier(table)]
|
|
516
|
+
query_params = (visitseq_uuid,)
|
|
517
|
+
visitseq = self.pd_read_sql(query_template, sql_params, query_params).iloc[0, :]
|
|
518
|
+
return visitseq
|
|
519
|
+
|
|
520
|
+
def set_visitseq_url(self, visitseq_uuid: UUID, visitseq_url: str) -> None:
|
|
521
|
+
"""Update the URL for the visits file for a visit sequence.
|
|
522
|
+
|
|
523
|
+
Parameters
|
|
524
|
+
----------
|
|
525
|
+
visitseq_uuid : `UUID`
|
|
526
|
+
The UUID of the visit sequence to update.
|
|
527
|
+
visitseq_url : `str`
|
|
528
|
+
The new URL for the visit sequence.
|
|
529
|
+
|
|
530
|
+
Raises
|
|
531
|
+
------
|
|
532
|
+
`ValueError`
|
|
533
|
+
If the table is not one of "visitseq", "mixedvisitseq",
|
|
534
|
+
"completed", or "simulations".
|
|
535
|
+
"""
|
|
536
|
+
table = self._find_visitseq_table(visitseq_uuid)
|
|
537
|
+
query = sql.SQL("UPDATE {}.{} SET visitseq_url={} WHERE visitseq_uuid={} RETURNING *;").format(
|
|
538
|
+
sql.Identifier(self.metadata_db_schema),
|
|
539
|
+
sql.Identifier(table),
|
|
540
|
+
sql.Placeholder("visitseq_url"),
|
|
541
|
+
sql.Placeholder("visitseq_uuid"),
|
|
542
|
+
)
|
|
543
|
+
data = {"visitseq_url": visitseq_url, "visitseq_uuid": visitseq_uuid}
|
|
544
|
+
self.query(query, data, return_result=False, commit=True)
|
|
545
|
+
|
|
546
|
+
def get_visitseq_url(self, visitseq_uuid: UUID) -> str:
|
|
547
|
+
"""Retrieve the URL for the file with the visits in a visit sequence.
|
|
548
|
+
|
|
549
|
+
Parameters
|
|
550
|
+
----------
|
|
551
|
+
visitseq_uuid : `UUID`
|
|
552
|
+
The UUID of the visit sequence to search for.
|
|
553
|
+
|
|
554
|
+
Returns
|
|
555
|
+
-------
|
|
556
|
+
url : `str`
|
|
557
|
+
The URL for the visit sequence.
|
|
558
|
+
|
|
559
|
+
Raises
|
|
560
|
+
------
|
|
561
|
+
`ValueError`
|
|
562
|
+
If no URL is found for the visit sequence.
|
|
563
|
+
"""
|
|
564
|
+
query = sql.SQL("SELECT visitseq_url FROM {}.visitseq WHERE visitseq_uuid={}").format(
|
|
565
|
+
sql.Identifier(self.metadata_db_schema),
|
|
566
|
+
sql.Placeholder("visitseq_uuid"),
|
|
567
|
+
)
|
|
568
|
+
data = {"visitseq_uuid": visitseq_uuid}
|
|
569
|
+
response = self.query(query, data)
|
|
570
|
+
if len(response) < 1:
|
|
571
|
+
raise ValueError(f"No URL for {visitseq_uuid} found")
|
|
572
|
+
assert len(response) == 1, f"Datatabase has too many visit sequinces with UUID={visitseq_uuid}"
|
|
573
|
+
return response[0][0]
|
|
574
|
+
|
|
575
|
+
def record_simulation_metadata(
|
|
576
|
+
self,
|
|
577
|
+
visits: pd.DataFrame,
|
|
578
|
+
label: str,
|
|
579
|
+
telescope: str = "simonyi",
|
|
580
|
+
url: str | None = None,
|
|
581
|
+
first_day_obs: date | int | str | None = None,
|
|
582
|
+
last_day_obs: date | int | str | None = None,
|
|
583
|
+
creation_time: Time | None = None,
|
|
584
|
+
scheduler_version: str | None = None,
|
|
585
|
+
config_url: str | None = None,
|
|
586
|
+
conda_env_sha256: bytes | None = None,
|
|
587
|
+
parent_visitseq_uuid: UUID | None = None,
|
|
588
|
+
sim_runner_kwargs: dict | None = None,
|
|
589
|
+
parent_last_day_obs: str | date | int | None = None,
|
|
590
|
+
) -> UUID:
|
|
591
|
+
"""Record metadata for a new simulation sequence.
|
|
592
|
+
|
|
593
|
+
Parameters
|
|
594
|
+
----------
|
|
595
|
+
visits : pd.DataFrame
|
|
596
|
+
A DataFrame of visits, with column names following those
|
|
597
|
+
in consdb.
|
|
598
|
+
label : str
|
|
599
|
+
A label for the sequence.
|
|
600
|
+
telescope : `str`
|
|
601
|
+
The telescope used, either "simonyi" or "auxtel".
|
|
602
|
+
Defaults to "simonyi".
|
|
603
|
+
url : `str`, optional
|
|
604
|
+
A URL for the file from thich the table of visits can
|
|
605
|
+
be download.
|
|
606
|
+
first_day_obs : `date` or `int` or `str`, optional
|
|
607
|
+
The first night of observations, defined according
|
|
608
|
+
to SITCOMTN-032 (UTC-12hrs).
|
|
609
|
+
last_day_obs : `date` or `int` or `str`, optional
|
|
610
|
+
The last day of observations, defined according
|
|
611
|
+
to SITCOMTN-032 (UTC-12hrs).
|
|
612
|
+
creation_time : `Time`, optional
|
|
613
|
+
The time the sequence was created, defaults to now.
|
|
614
|
+
scheduler_version : `str`, optional
|
|
615
|
+
Version of ``rubin_scheduler`` used to run the simulation,
|
|
616
|
+
by default None.
|
|
617
|
+
config_url : `str`, optional
|
|
618
|
+
URL for the config script, perhaps on github,
|
|
619
|
+
by default `None`.
|
|
620
|
+
conda_env_sha256 : `bytes`, optional
|
|
621
|
+
SHA256 hash of output of ``conda list --json``, by default None.
|
|
622
|
+
parent_visitseq_uuid : `UUID`, optional
|
|
623
|
+
UUID of visitseq loaded into scheduler before running,
|
|
624
|
+
by default `None`.
|
|
625
|
+
sim_runner_kwargs : `dict`, optional
|
|
626
|
+
Arguments to ``sim_runner`` as a `dict`, by default `None`.
|
|
627
|
+
parent_last_day_obs : `str`, optional
|
|
628
|
+
day_obs of last visit loaded into scheduler before running,
|
|
629
|
+
by default `None`.
|
|
630
|
+
|
|
631
|
+
Returns
|
|
632
|
+
-------
|
|
633
|
+
visitseq_uuid : UUID
|
|
634
|
+
The UUID of the new visit sequence.
|
|
635
|
+
"""
|
|
636
|
+
# I would have preferred to use kwargs here, but was not
|
|
637
|
+
# able to get type checking happy with it.
|
|
638
|
+
visitseq_uuid = self.record_visitseq_metadata(
|
|
639
|
+
visits,
|
|
640
|
+
label,
|
|
641
|
+
telescope=telescope,
|
|
642
|
+
table="simulations",
|
|
643
|
+
url=url,
|
|
644
|
+
first_day_obs=first_day_obs,
|
|
645
|
+
last_day_obs=last_day_obs,
|
|
646
|
+
creation_time=creation_time,
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
def make_set_clause(column: str) -> sql.Composed:
|
|
650
|
+
set_clause = sql.SQL("{} = {}").format(sql.Identifier(column), sql.Placeholder(column))
|
|
651
|
+
return set_clause
|
|
652
|
+
|
|
653
|
+
set_clauses = []
|
|
654
|
+
data: Dict[str, Any] = {}
|
|
655
|
+
if scheduler_version is not None:
|
|
656
|
+
set_clauses.append(make_set_clause("scheduler_version"))
|
|
657
|
+
data["scheduler_version"] = scheduler_version
|
|
658
|
+
|
|
659
|
+
if config_url is not None:
|
|
660
|
+
set_clauses.append(make_set_clause("config_url"))
|
|
661
|
+
data["config_url"] = config_url
|
|
662
|
+
|
|
663
|
+
if conda_env_sha256 is not None:
|
|
664
|
+
set_clauses.append(make_set_clause("conda_env_sha256"))
|
|
665
|
+
data["conda_env_sha256"] = conda_env_sha256
|
|
666
|
+
|
|
667
|
+
if parent_visitseq_uuid is not None:
|
|
668
|
+
set_clauses.append(make_set_clause("parent_visitseq_uuid"))
|
|
669
|
+
data["parent_visitseq_uuid"] = str(parent_visitseq_uuid)
|
|
670
|
+
|
|
671
|
+
if sim_runner_kwargs is not None:
|
|
672
|
+
sim_runner_munged_kwargs = {}
|
|
673
|
+
for key in sim_runner_kwargs:
|
|
674
|
+
try:
|
|
675
|
+
# Throws a TypeError if it cannot convert it to json
|
|
676
|
+
json_result = json.dumps(sim_runner_kwargs[key])
|
|
677
|
+
json_failed = len(json_result) > JSON_DUMP_LIMIT
|
|
678
|
+
except TypeError:
|
|
679
|
+
# Cannot convert to json, just store a string
|
|
680
|
+
# representation instead.
|
|
681
|
+
json_failed = True
|
|
682
|
+
|
|
683
|
+
# if we can save an argument as serealized json in reasonable
|
|
684
|
+
# space, do, otherwise store a (possibly truncated) result of
|
|
685
|
+
# repr.
|
|
686
|
+
sim_runner_munged_kwargs[key] = (
|
|
687
|
+
("Not json serializable", repr(sim_runner_kwargs[key])[:JSON_DUMP_LIMIT])
|
|
688
|
+
if json_failed
|
|
689
|
+
else sim_runner_kwargs[key]
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
set_clauses.append(make_set_clause("sim_runner_kwargs"))
|
|
693
|
+
data["sim_runner_kwargs"] = psycopg2.extras.Json(sim_runner_munged_kwargs)
|
|
694
|
+
|
|
695
|
+
if parent_last_day_obs is not None:
|
|
696
|
+
set_clauses.append(make_set_clause("parent_last_day_obs"))
|
|
697
|
+
data["parent_last_day_obs"] = parent_last_day_obs
|
|
698
|
+
|
|
699
|
+
num_columns_to_update = len(set_clauses)
|
|
700
|
+
|
|
701
|
+
if num_columns_to_update > 0:
|
|
702
|
+
query = sql.SQL("UPDATE {}.simulations SET {} WHERE visitseq_uuid={} RETURNING *;").format(
|
|
703
|
+
sql.Identifier(self.metadata_db_schema),
|
|
704
|
+
sql.SQL(", ").join(set_clauses),
|
|
705
|
+
sql.Placeholder("visitseq_uuid"),
|
|
706
|
+
)
|
|
707
|
+
data["visitseq_uuid"] = visitseq_uuid
|
|
708
|
+
|
|
709
|
+
conn = None
|
|
710
|
+
try:
|
|
711
|
+
conn = self.pg_pool.getconn()
|
|
712
|
+
cursor = conn.cursor()
|
|
713
|
+
cursor.execute(query, data)
|
|
714
|
+
result = cursor.fetchall()
|
|
715
|
+
|
|
716
|
+
# Be extra cautious, and check that everything looks
|
|
717
|
+
# reasonable before commiting the update.
|
|
718
|
+
assert len(result) == 1
|
|
719
|
+
cursor.execute("COMMIT;")
|
|
720
|
+
finally:
|
|
721
|
+
if conn:
|
|
722
|
+
self.pg_pool.putconn(conn)
|
|
723
|
+
|
|
724
|
+
return visitseq_uuid
|
|
725
|
+
|
|
726
|
+
def update_visitseq_metadata(self, visitseq_uuid: UUID, field: str, value: object) -> None:
|
|
727
|
+
"""Update a single metadata field for a visit sequence.
|
|
728
|
+
Automatically finds the table in which to make the update.
|
|
729
|
+
|
|
730
|
+
Parameters
|
|
731
|
+
----------
|
|
732
|
+
visitseq_uuid : `UUID`
|
|
733
|
+
The unique identifier of the visit sequence to update.
|
|
734
|
+
field : `str`
|
|
735
|
+
The name of the column in the metadata table to modify.
|
|
736
|
+
value : `object`
|
|
737
|
+
The new value to assign to ``field``. The type must be
|
|
738
|
+
compatible with the column's database type.
|
|
739
|
+
"""
|
|
740
|
+
table = self._find_visitseq_table(visitseq_uuid)
|
|
741
|
+
query = sql.SQL("UPDATE {}.{} SET {}={} WHERE visitseq_uuid={};").format(
|
|
742
|
+
sql.Identifier(self.metadata_db_schema),
|
|
743
|
+
sql.Identifier(table),
|
|
744
|
+
sql.Identifier(field),
|
|
745
|
+
sql.Placeholder("value"),
|
|
746
|
+
sql.Placeholder("visitseq_uuid"),
|
|
747
|
+
)
|
|
748
|
+
data = {"value": value, "visitseq_uuid": visitseq_uuid}
|
|
749
|
+
self.query(query, data, return_result=False, commit=True)
|
|
750
|
+
|
|
751
|
+
def record_completed_metadata(
|
|
752
|
+
self,
|
|
753
|
+
visits: pd.DataFrame,
|
|
754
|
+
label: str,
|
|
755
|
+
telescope: str = "simonyi",
|
|
756
|
+
url: str | None = None,
|
|
757
|
+
first_day_obs: date | int | str | None = None,
|
|
758
|
+
last_day_obs: date | int | str | None = None,
|
|
759
|
+
creation_time: Time | None = None,
|
|
760
|
+
query: str | None = None,
|
|
761
|
+
) -> UUID:
|
|
762
|
+
"""Record metadata for a sequence of visits queried from
|
|
763
|
+
the consdb.
|
|
764
|
+
|
|
765
|
+
Parameters
|
|
766
|
+
----------
|
|
767
|
+
visits : `pd.DataFrame`
|
|
768
|
+
A DataFrame of visits, with column names following those
|
|
769
|
+
in consdb.
|
|
770
|
+
label : `str`
|
|
771
|
+
A label for the sequence.
|
|
772
|
+
telescope : `str`
|
|
773
|
+
The telescope used, either "simonyi" or "auxtel".
|
|
774
|
+
Defaults to "simonyi".
|
|
775
|
+
url : `str`, optional
|
|
776
|
+
A URL for the file from thich the table of visits can
|
|
777
|
+
be download.
|
|
778
|
+
first_day_obs : `date` or `int` or `str`, optional
|
|
779
|
+
The first night of observations, defined according
|
|
780
|
+
to SITCOMTN-032 (UTC-12hrs).
|
|
781
|
+
last_day_obs : `date` or `int` or `str`, optional
|
|
782
|
+
The last day of observations, defined according
|
|
783
|
+
to SITCOMTN-032 (UTC-12hrs).
|
|
784
|
+
creation_time : `Time`, optional
|
|
785
|
+
The time the sequence was created, defaults no now.
|
|
786
|
+
query : `str`, optional
|
|
787
|
+
The query to the consdb used to get the completed
|
|
788
|
+
visits, by default None.
|
|
789
|
+
|
|
790
|
+
Returns
|
|
791
|
+
-------
|
|
792
|
+
visitseq_uuid : `UUID`
|
|
793
|
+
The UUID of the new visit sequence.
|
|
794
|
+
"""
|
|
795
|
+
|
|
796
|
+
# I would have preferred to use kwargs here, but was not
|
|
797
|
+
# able to get type checking happy with it.
|
|
798
|
+
visitseq_uuid = self.record_visitseq_metadata(
|
|
799
|
+
visits,
|
|
800
|
+
label,
|
|
801
|
+
telescope=telescope,
|
|
802
|
+
table="completed",
|
|
803
|
+
url=url,
|
|
804
|
+
first_day_obs=first_day_obs,
|
|
805
|
+
last_day_obs=last_day_obs,
|
|
806
|
+
creation_time=creation_time,
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
if query is not None:
|
|
810
|
+
composed_query = sql.SQL(
|
|
811
|
+
"UPDATE {}.completed SET query={} WHERE visitseq_uuid={} RETURNING *"
|
|
812
|
+
).format(
|
|
813
|
+
sql.Identifier(self.metadata_db_schema),
|
|
814
|
+
sql.Placeholder("query"),
|
|
815
|
+
sql.Placeholder("visitseq_uuid"),
|
|
816
|
+
)
|
|
817
|
+
data = {"query": query, "visitseq_uuid": visitseq_uuid}
|
|
818
|
+
|
|
819
|
+
conn = None
|
|
820
|
+
try:
|
|
821
|
+
conn = self.pg_pool.getconn()
|
|
822
|
+
cursor = conn.cursor()
|
|
823
|
+
cursor.execute(composed_query, data)
|
|
824
|
+
result = cursor.fetchall()
|
|
825
|
+
|
|
826
|
+
# Be extra cautious, and check that everything looks
|
|
827
|
+
# reasonable before commiting the update.
|
|
828
|
+
assert len(result) == 1
|
|
829
|
+
cursor.execute("COMMIT;")
|
|
830
|
+
finally:
|
|
831
|
+
if conn:
|
|
832
|
+
self.pg_pool.putconn(conn)
|
|
833
|
+
|
|
834
|
+
return visitseq_uuid
|
|
835
|
+
|
|
836
|
+
def record_mixed_metadata(
|
|
837
|
+
self,
|
|
838
|
+
visits: pd.DataFrame,
|
|
839
|
+
label: str,
|
|
840
|
+
last_early_day_obs: date | int | str,
|
|
841
|
+
first_late_day_obs: date | int | str,
|
|
842
|
+
early_parent_uuid: UUID,
|
|
843
|
+
late_parent_uuid: UUID,
|
|
844
|
+
telescope: str = "simonyi",
|
|
845
|
+
url: str | None = None,
|
|
846
|
+
first_day_obs: date | int | str | None = None,
|
|
847
|
+
last_day_obs: date | int | str | None = None,
|
|
848
|
+
creation_time: Time | None = None,
|
|
849
|
+
) -> UUID:
|
|
850
|
+
"""Record metadata for a new mixed visit sequence.
|
|
851
|
+
|
|
852
|
+
Parameters
|
|
853
|
+
----------
|
|
854
|
+
visits : `pd.DataFrame`
|
|
855
|
+
A DataFrame of visits, with column names following those
|
|
856
|
+
in consdb.
|
|
857
|
+
label : `str`
|
|
858
|
+
A label for the sequence.
|
|
859
|
+
last_early_day_obs : `date` or `int` or `str`
|
|
860
|
+
The last day obs of the early parent.
|
|
861
|
+
first_late_day_obs : `date` or `int` or `str`
|
|
862
|
+
The first day obs of the late parent.
|
|
863
|
+
early_parent_uuid : `UUID`
|
|
864
|
+
The UUID of the early parent.
|
|
865
|
+
late_parent_uuid : `UUID`
|
|
866
|
+
The UUID of the late parent.
|
|
867
|
+
telescope : `str`, optional
|
|
868
|
+
The telescope used, either "simonyi" or "auxtel".
|
|
869
|
+
Defaults to "simonyi".
|
|
870
|
+
first_day_obs : `date` or `int` or `str` or `None`, optional
|
|
871
|
+
The first night of observations, defined according
|
|
872
|
+
to SITCOMTN-032 (UTC-12hrs).
|
|
873
|
+
last_day_obs : `date` or `int` or `str` or `None`, optional
|
|
874
|
+
The last day of observations, defined according
|
|
875
|
+
to SITCOMTN-032 (UTC-12hrs).
|
|
876
|
+
creation_time : `Time` or `None`, optional
|
|
877
|
+
The time the sequence was created, defaults to now.
|
|
878
|
+
|
|
879
|
+
Returns
|
|
880
|
+
-------
|
|
881
|
+
visitseq_uuid : `UUID`
|
|
882
|
+
The UUID of the new visit sequence.
|
|
883
|
+
"""
|
|
884
|
+
visitseq_uuid = self.record_visitseq_metadata(
|
|
885
|
+
visits,
|
|
886
|
+
label,
|
|
887
|
+
telescope=telescope,
|
|
888
|
+
table="mixedvisitseq",
|
|
889
|
+
url=url,
|
|
890
|
+
first_day_obs=first_day_obs,
|
|
891
|
+
last_day_obs=last_day_obs,
|
|
892
|
+
creation_time=creation_time,
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
update_query = sql.SQL(
|
|
896
|
+
"""UPDATE {}.mixedvisitseq
|
|
897
|
+
SET last_early_day_obs={},
|
|
898
|
+
first_late_day_obs={},
|
|
899
|
+
early_parent_uuid={},
|
|
900
|
+
late_parent_uuid={}\
|
|
901
|
+
WHERE visitseq_uuid={} RETURNING *
|
|
902
|
+
"""
|
|
903
|
+
).format(
|
|
904
|
+
sql.Identifier(self.metadata_db_schema),
|
|
905
|
+
sql.Placeholder("last_early_day_obs"),
|
|
906
|
+
sql.Placeholder("first_late_day_obs"),
|
|
907
|
+
sql.Placeholder("early_parent_uuid"),
|
|
908
|
+
sql.Placeholder("late_parent_uuid"),
|
|
909
|
+
sql.Placeholder("visitseq_uuid"),
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
data = {
|
|
913
|
+
"last_early_day_obs": dayobs_to_date(last_early_day_obs),
|
|
914
|
+
"first_late_day_obs": dayobs_to_date(first_late_day_obs),
|
|
915
|
+
"early_parent_uuid": early_parent_uuid,
|
|
916
|
+
"late_parent_uuid": late_parent_uuid,
|
|
917
|
+
"visitseq_uuid": visitseq_uuid,
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
conn = None
|
|
921
|
+
try:
|
|
922
|
+
conn = self.pg_pool.getconn()
|
|
923
|
+
cursor = conn.cursor()
|
|
924
|
+
cursor.execute(update_query, data)
|
|
925
|
+
result = cursor.fetchall()
|
|
926
|
+
|
|
927
|
+
# Be extra cautious, and check that everything looks
|
|
928
|
+
# reasonable before commiting the update.
|
|
929
|
+
assert len(result) == 1
|
|
930
|
+
cursor.execute("COMMIT;")
|
|
931
|
+
finally:
|
|
932
|
+
if conn:
|
|
933
|
+
self.pg_pool.putconn(conn)
|
|
934
|
+
|
|
935
|
+
return visitseq_uuid
|
|
936
|
+
|
|
937
|
+
def is_tagged(self, visitseq_uuid: UUID, tag: str) -> bool:
|
|
938
|
+
"""Return whether a visit sequence is tagged with a given tag.
|
|
939
|
+
|
|
940
|
+
Parameters
|
|
941
|
+
----------
|
|
942
|
+
visitseq_uuid : `UUID`
|
|
943
|
+
The UUID of the visit sequence.
|
|
944
|
+
tag : `str`
|
|
945
|
+
The tag to check for.
|
|
946
|
+
|
|
947
|
+
Returns
|
|
948
|
+
-------
|
|
949
|
+
is_tagged : `bool`
|
|
950
|
+
True if the visit sequence is tagged with the given tag,
|
|
951
|
+
False otherwise.
|
|
952
|
+
"""
|
|
953
|
+
query = sql.SQL("SELECT COUNT(*)>=1 FROM {}.tags WHERE visitseq_uuid={} AND TAG={}").format(
|
|
954
|
+
sql.Identifier(self.metadata_db_schema), sql.Placeholder("visitseq_uuid"), sql.Placeholder("tag")
|
|
955
|
+
)
|
|
956
|
+
data = {"visitseq_uuid": visitseq_uuid, "tag": tag}
|
|
957
|
+
is_tagged = self.query(query, data)[0][0] > 0
|
|
958
|
+
return is_tagged
|
|
959
|
+
|
|
960
|
+
def tag(self, visitseq_uuid: UUID, *tags: str) -> None:
|
|
961
|
+
"""Tag a visit sequence with one or more tags.
|
|
962
|
+
|
|
963
|
+
Parameters
|
|
964
|
+
----------
|
|
965
|
+
visitseq_uuid : `UUID`
|
|
966
|
+
The UUID of the visit sequence.
|
|
967
|
+
*tags : `str`
|
|
968
|
+
One or more tags to add to the visit sequence.
|
|
969
|
+
"""
|
|
970
|
+
query = sql.SQL("INSERT INTO {}.tags (visitseq_uuid, tag) VALUES ({}, {})").format(
|
|
971
|
+
sql.Identifier(self.metadata_db_schema), sql.Placeholder("visitseq_uuid"), sql.Placeholder("tag")
|
|
972
|
+
)
|
|
973
|
+
data: Dict[str, Any] = {"visitseq_uuid": visitseq_uuid}
|
|
974
|
+
for tag in tags:
|
|
975
|
+
if not self.is_tagged(visitseq_uuid, tag):
|
|
976
|
+
data["tag"] = tag
|
|
977
|
+
self.query(query, data, commit=True, return_result=False)
|
|
978
|
+
|
|
979
|
+
def untag(self, visitseq_uuid: UUID, tag: str) -> None:
|
|
980
|
+
"""Untag a visit sequence with a given tag.
|
|
981
|
+
|
|
982
|
+
Parameters
|
|
983
|
+
----------
|
|
984
|
+
visitseq_uuid : `UUID`
|
|
985
|
+
The UUID of the visit sequence.
|
|
986
|
+
tag : `str`
|
|
987
|
+
The tag to remove.
|
|
988
|
+
|
|
989
|
+
Raises
|
|
990
|
+
------
|
|
991
|
+
`ValueError`
|
|
992
|
+
If the visit sequence is not tagged with the given tag.
|
|
993
|
+
"""
|
|
994
|
+
if not self.is_tagged(visitseq_uuid, tag):
|
|
995
|
+
raise ValueError(f"Visit sequence {visitseq_uuid} is not tagged with {tag}")
|
|
996
|
+
|
|
997
|
+
query = sql.SQL("DELETE FROM {}.tags WHERE visitseq_uuid={} AND tag={}").format(
|
|
998
|
+
sql.Identifier(self.metadata_db_schema),
|
|
999
|
+
sql.Placeholder("visitseq_uuid"),
|
|
1000
|
+
sql.Placeholder("tag"),
|
|
1001
|
+
)
|
|
1002
|
+
data = {"visitseq_uuid": visitseq_uuid, "tag": tag}
|
|
1003
|
+
self.query(query, data, commit=True, return_result=False)
|
|
1004
|
+
|
|
1005
|
+
def comment(self, visitseq_uuid: UUID, comment: str, author: str | None = None) -> None:
|
|
1006
|
+
"""Attach a comment to a visit sequence in the
|
|
1007
|
+
metadata database..
|
|
1008
|
+
|
|
1009
|
+
Parameters
|
|
1010
|
+
----------
|
|
1011
|
+
visitseq_uuid: `uuid.UUID`
|
|
1012
|
+
The UUID of the visit sequence to comment on.
|
|
1013
|
+
comment : `str`
|
|
1014
|
+
The comment to attach.
|
|
1015
|
+
author : `str`, optional
|
|
1016
|
+
The author of the comment.
|
|
1017
|
+
Defaults to `None`.
|
|
1018
|
+
"""
|
|
1019
|
+
comment_utc = Time.now().utc
|
|
1020
|
+
# mypy cannot figure out that comment_utc cannot be masked
|
|
1021
|
+
# so assert that it is not as a hint to it.
|
|
1022
|
+
assert isinstance(comment_utc, Time)
|
|
1023
|
+
comment_time = comment_utc.datetime
|
|
1024
|
+
query = ""
|
|
1025
|
+
data = {"visitseq_uuid": visitseq_uuid, "comment_time": comment_time, "comment": comment}
|
|
1026
|
+
if author is None:
|
|
1027
|
+
query = sql.SQL(
|
|
1028
|
+
"INSERT INTO {}.comments (visitseq_uuid, comment_time, comment) VALUES ({}, {}, {})"
|
|
1029
|
+
).format(
|
|
1030
|
+
sql.Identifier(self.metadata_db_schema),
|
|
1031
|
+
sql.Placeholder("visitseq_uuid"),
|
|
1032
|
+
sql.Placeholder("comment_time"),
|
|
1033
|
+
sql.Placeholder("comment"),
|
|
1034
|
+
)
|
|
1035
|
+
else:
|
|
1036
|
+
query = sql.SQL(
|
|
1037
|
+
"INSERT INTO {}.comments (visitseq_uuid, comment_time, author, comment)"
|
|
1038
|
+
+ " VALUES ({}, {}, {}, {})"
|
|
1039
|
+
).format(
|
|
1040
|
+
sql.Identifier(self.metadata_db_schema),
|
|
1041
|
+
sql.Placeholder("visitseq_uuid"),
|
|
1042
|
+
sql.Placeholder("comment_time"),
|
|
1043
|
+
sql.Placeholder("author"),
|
|
1044
|
+
sql.Placeholder("comment"),
|
|
1045
|
+
)
|
|
1046
|
+
data["author"] = author
|
|
1047
|
+
|
|
1048
|
+
self.query(query, data, commit=True, return_result=False)
|
|
1049
|
+
|
|
1050
|
+
def get_comments(self, visitseq_uuid: UUID) -> pd.DataFrame:
|
|
1051
|
+
"""Retrieve all comments attached to a specific visit sequence.
|
|
1052
|
+
|
|
1053
|
+
Parameters
|
|
1054
|
+
----------
|
|
1055
|
+
visitseq_uuid : `UUID`
|
|
1056
|
+
The UUID of the visit sequence whose comments should be fetched.
|
|
1057
|
+
|
|
1058
|
+
Returns
|
|
1059
|
+
-------
|
|
1060
|
+
comments: `pd.DataFrame`
|
|
1061
|
+
A DataFrame containing the comments for the requested visit
|
|
1062
|
+
sequence. The DataFrame has the following columns:
|
|
1063
|
+
|
|
1064
|
+
``"visitseq_uuid"``
|
|
1065
|
+
The ID of the visit sequence.
|
|
1066
|
+
``"comment_time"``
|
|
1067
|
+
Timestamp of the comment.
|
|
1068
|
+
``"author"``
|
|
1069
|
+
Name of the user who added the comment; may be ``None``.
|
|
1070
|
+
``"comment"``
|
|
1071
|
+
The comment text.
|
|
1072
|
+
|
|
1073
|
+
If the visit sequence has no comments, an empty DataFrame with the
|
|
1074
|
+
above column names is returned.
|
|
1075
|
+
|
|
1076
|
+
"""
|
|
1077
|
+
query_template = "SELECT * FROM {}.comments WHERE visitseq_uuid = %s"
|
|
1078
|
+
sql_params: list[sql.Composable] = [sql.Identifier(self.metadata_db_schema)]
|
|
1079
|
+
query_params = (visitseq_uuid,)
|
|
1080
|
+
comments = self.pd_read_sql(query_template, sql_params, query_params)
|
|
1081
|
+
|
|
1082
|
+
# sqlalchemy seems to mess up the dtypes. Fix them.
|
|
1083
|
+
comments["comment"] = comments["comment"].astype("string")
|
|
1084
|
+
|
|
1085
|
+
return comments
|
|
1086
|
+
|
|
1087
|
+
def register_file(
|
|
1088
|
+
self,
|
|
1089
|
+
visitseq_uuid: UUID,
|
|
1090
|
+
file_type: str,
|
|
1091
|
+
file_sha256: bytes,
|
|
1092
|
+
location: str | ResourcePath,
|
|
1093
|
+
update: bool = False,
|
|
1094
|
+
) -> None:
|
|
1095
|
+
"""Register a file in the visit sequence metadata database.
|
|
1096
|
+
|
|
1097
|
+
Parameters
|
|
1098
|
+
----------
|
|
1099
|
+
visitseq_uuid : `UUID`
|
|
1100
|
+
The UUID of the visit sequence with which the file should be
|
|
1101
|
+
associated.
|
|
1102
|
+
file_type : `str`
|
|
1103
|
+
The handle for the file type.
|
|
1104
|
+
The ``"visits"`` file type is not allowed
|
|
1105
|
+
because visit tables are recorded using `set_visitseq_url`.
|
|
1106
|
+
file_sha256 : `bytes`
|
|
1107
|
+
The hash of the file contents.
|
|
1108
|
+
location : `str` or `lsst.resources.ResourcePath`
|
|
1109
|
+
The location of the file to be registered.
|
|
1110
|
+
update : `bool`, optional
|
|
1111
|
+
If ``True`` and a record for the same ``visitseq_uuid`` and
|
|
1112
|
+
``file_type`` already exists, the existing row will be updated with
|
|
1113
|
+
the new SHA‑256 hash and URL. If ``False`` and a record already
|
|
1114
|
+
exists a ``ValueError`` is raised.
|
|
1115
|
+
|
|
1116
|
+
Raises
|
|
1117
|
+
------
|
|
1118
|
+
`ValueError`
|
|
1119
|
+
If ``file_type`` is ``"visits"`` (use `set_visitseq_url` instead),
|
|
1120
|
+
or if ``update`` is ``False`` and a record already exists for the
|
|
1121
|
+
given ``visitseq_uuid`` and ``file_type``.
|
|
1122
|
+
`TypeError`
|
|
1123
|
+
If ``location`` is neither a string nor a `ResourcePath`.
|
|
1124
|
+
"""
|
|
1125
|
+
if file_type == "visits":
|
|
1126
|
+
raise ValueError("Use set_visitseq_url to register sets of visits themselves")
|
|
1127
|
+
|
|
1128
|
+
file_url: str = ""
|
|
1129
|
+
match location:
|
|
1130
|
+
case ResourcePath():
|
|
1131
|
+
file_url = location.geturl()
|
|
1132
|
+
case str():
|
|
1133
|
+
file_url = location
|
|
1134
|
+
case _:
|
|
1135
|
+
raise ValueError("Unrecognised location type")
|
|
1136
|
+
|
|
1137
|
+
# Check whether the file is already registered
|
|
1138
|
+
already_exists = False
|
|
1139
|
+
try:
|
|
1140
|
+
self.get_file_url(visitseq_uuid, file_type)
|
|
1141
|
+
already_exists = True
|
|
1142
|
+
except ValueError:
|
|
1143
|
+
already_exists = False
|
|
1144
|
+
|
|
1145
|
+
if already_exists:
|
|
1146
|
+
if not update:
|
|
1147
|
+
raise ValueError("A file of type {file_type} is already registered for {visitseq_uuid}")
|
|
1148
|
+
|
|
1149
|
+
query = sql.SQL(
|
|
1150
|
+
"UPDATE {}.files SET file_sha256={}, file_url={} WHERE visitseq_uuid={} AND file_type={}"
|
|
1151
|
+
).format(
|
|
1152
|
+
sql.Identifier(self.metadata_db_schema),
|
|
1153
|
+
sql.Placeholder("file_sha256"),
|
|
1154
|
+
sql.Placeholder("file_url"),
|
|
1155
|
+
sql.Placeholder("visitseq_uuid"),
|
|
1156
|
+
sql.Placeholder("file_type"),
|
|
1157
|
+
)
|
|
1158
|
+
else:
|
|
1159
|
+
query = sql.SQL(
|
|
1160
|
+
"INSERT INTO {}.files (visitseq_uuid, file_type, file_sha256, file_url)"
|
|
1161
|
+
+ " VALUES ({}, {}, {}, {})"
|
|
1162
|
+
).format(
|
|
1163
|
+
sql.Identifier(self.metadata_db_schema),
|
|
1164
|
+
sql.Placeholder("visitseq_uuid"),
|
|
1165
|
+
sql.Placeholder("file_type"),
|
|
1166
|
+
sql.Placeholder("file_sha256"),
|
|
1167
|
+
sql.Placeholder("file_url"),
|
|
1168
|
+
)
|
|
1169
|
+
|
|
1170
|
+
data = {
|
|
1171
|
+
"visitseq_uuid": visitseq_uuid,
|
|
1172
|
+
"file_type": file_type,
|
|
1173
|
+
"file_sha256": file_sha256,
|
|
1174
|
+
"file_url": file_url,
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
self.query(query, data, commit=True, return_result=False)
|
|
1178
|
+
|
|
1179
|
+
def get_file_url(self, visitseq_uuid: UUID, file_type: str) -> str:
|
|
1180
|
+
"""Return the URL for a registered file of a given type and
|
|
1181
|
+
visit sequence UUID.
|
|
1182
|
+
|
|
1183
|
+
Parameters
|
|
1184
|
+
----------
|
|
1185
|
+
visitseq_uuid : `UUID`
|
|
1186
|
+
The UUID of the visit sequence with which the desired
|
|
1187
|
+
file is associated.
|
|
1188
|
+
file_type : `str`
|
|
1189
|
+
Handle for the file type.
|
|
1190
|
+
|
|
1191
|
+
Returns
|
|
1192
|
+
-------
|
|
1193
|
+
url : `str`
|
|
1194
|
+
The URL pointing to the requested file.
|
|
1195
|
+
"""
|
|
1196
|
+
if file_type == "visits":
|
|
1197
|
+
# It's as easy to just do it as it is to raise
|
|
1198
|
+
# an exception
|
|
1199
|
+
return self.get_visitseq_url(visitseq_uuid)
|
|
1200
|
+
|
|
1201
|
+
query = sql.SQL("SELECT file_url FROM {}.files WHERE visitseq_uuid={} AND file_type={}").format(
|
|
1202
|
+
sql.Identifier(self.metadata_db_schema),
|
|
1203
|
+
sql.Placeholder("visitseq_uuid"),
|
|
1204
|
+
sql.Placeholder("file_type"),
|
|
1205
|
+
)
|
|
1206
|
+
data = {"visitseq_uuid": visitseq_uuid, "file_type": file_type}
|
|
1207
|
+
result = self.query(query, data, return_result=True)
|
|
1208
|
+
if len(result) < 1:
|
|
1209
|
+
raise ValueError(f"No URLs found for {file_type} for visitseq {visitseq_uuid}")
|
|
1210
|
+
if len(result) > 1:
|
|
1211
|
+
raise ValueError(f"Too many URLs found for {file_type} for visitseq {visitseq_uuid}!")
|
|
1212
|
+
|
|
1213
|
+
url = result[0][0]
|
|
1214
|
+
return url
|
|
1215
|
+
|
|
1216
|
+
def get_file_sha256(self, visitseq_uuid: UUID, file_type: str) -> bytes:
|
|
1217
|
+
"""Retrieve the SHA‑256 digest of a registered file.
|
|
1218
|
+
|
|
1219
|
+
Parameters
|
|
1220
|
+
----------
|
|
1221
|
+
visitseq_uuid : `UUID`
|
|
1222
|
+
The UUID of the visit sequence the file is associated with.
|
|
1223
|
+
file_type : `str`
|
|
1224
|
+
Handle for the file type.
|
|
1225
|
+
|
|
1226
|
+
Returns
|
|
1227
|
+
-------
|
|
1228
|
+
file_sha256 : `bytes`
|
|
1229
|
+
The raw SHA‑256 digest of the file contents. The value
|
|
1230
|
+
is returned as a 32‑byte object (the output of
|
|
1231
|
+
``hashlib.sha256``).
|
|
1232
|
+
"""
|
|
1233
|
+
|
|
1234
|
+
query = sql.SQL("SELECT file_sha256 FROM {}.files WHERE visitseq_uuid={} AND file_type={}").format(
|
|
1235
|
+
sql.Identifier(self.metadata_db_schema),
|
|
1236
|
+
sql.Placeholder("visitseq_uuid"),
|
|
1237
|
+
sql.Placeholder("file_type"),
|
|
1238
|
+
)
|
|
1239
|
+
data = {"visitseq_uuid": visitseq_uuid, "file_type": file_type}
|
|
1240
|
+
result = self.query(query, data, return_result=True)
|
|
1241
|
+
if len(result) < 1:
|
|
1242
|
+
raise ValueError(f"No URLs found for {file_type} for visitseq {visitseq_uuid}")
|
|
1243
|
+
if len(result) > 1:
|
|
1244
|
+
raise ValueError(f"Too many URLs found for {file_type} for visitseq {visitseq_uuid}!")
|
|
1245
|
+
|
|
1246
|
+
file_sha256 = result[0][0].tobytes()
|
|
1247
|
+
return file_sha256
|
|
1248
|
+
|
|
1249
|
+
def insert_nightly_stats(self, visitseq_uuid: UUID, nightly_stats: pd.DataFrame) -> None:
|
|
1250
|
+
"""Instert by-night statistics into associoted with
|
|
1251
|
+
a visit sequence into the visit sequence metadata database.
|
|
1252
|
+
|
|
1253
|
+
Parameters
|
|
1254
|
+
----------
|
|
1255
|
+
visitseq_uuid : `UUID`
|
|
1256
|
+
The UUID of the visit sequence whose statistics are being
|
|
1257
|
+
recorded.
|
|
1258
|
+
stats_df : `pd.DataFrame`
|
|
1259
|
+
A DataFrame containing the nightly statistics that were
|
|
1260
|
+
written to the database. The columns are:
|
|
1261
|
+
|
|
1262
|
+
``"day_obs"``
|
|
1263
|
+
The observation day.
|
|
1264
|
+
``"value_name"``
|
|
1265
|
+
The value for which statistics were computed.
|
|
1266
|
+
``"p05"``
|
|
1267
|
+
5th percentile.
|
|
1268
|
+
``"q1"``
|
|
1269
|
+
25th percentile (first quartile).
|
|
1270
|
+
``"median"``
|
|
1271
|
+
50th percentile (median).
|
|
1272
|
+
``"q3"``
|
|
1273
|
+
75th percentile (third quartile).
|
|
1274
|
+
``"p95"``
|
|
1275
|
+
95th percentile.
|
|
1276
|
+
``"visitseq_uuid"``
|
|
1277
|
+
The UUID of the sequence.
|
|
1278
|
+
``"accumulated"``
|
|
1279
|
+
Always ``False`` for data written by
|
|
1280
|
+
this method.
|
|
1281
|
+
"""
|
|
1282
|
+
# We are adding a column, but do not want
|
|
1283
|
+
# to change the original df, so
|
|
1284
|
+
# create a copy.
|
|
1285
|
+
stats_df = nightly_stats.copy()
|
|
1286
|
+
stats_df["visitseq_uuid"] = visitseq_uuid
|
|
1287
|
+
|
|
1288
|
+
# pandas to_sql fails with the native psycopg2, only
|
|
1289
|
+
# works with sqlalchemy
|
|
1290
|
+
engine = sqlalchemy.create_engine("postgresql+psycopg2://", creator=self.pg_pool.getconn)
|
|
1291
|
+
with engine.connect() as conn:
|
|
1292
|
+
num_rows_added = stats_df.to_sql(
|
|
1293
|
+
"nightly_stats", conn, schema=self.metadata_db_schema, if_exists="append", index=False
|
|
1294
|
+
)
|
|
1295
|
+
assert num_rows_added == len(stats_df)
|
|
1296
|
+
|
|
1297
|
+
def query_nightly_stats(self, visitseq_uuid: UUID) -> pd.DataFrame:
|
|
1298
|
+
"""Query the visit sequence metadata database for
|
|
1299
|
+
nightly statistics.
|
|
1300
|
+
|
|
1301
|
+
Parameters
|
|
1302
|
+
----------
|
|
1303
|
+
visitseq_uuid : `UUID`
|
|
1304
|
+
The UUID of the visit sequence whose statistics are being
|
|
1305
|
+
recorded.
|
|
1306
|
+
|
|
1307
|
+
Returns
|
|
1308
|
+
-------
|
|
1309
|
+
stats_df : `pd.DataFrame`
|
|
1310
|
+
A table of nightly statistics.
|
|
1311
|
+
"""
|
|
1312
|
+
query_template = "SELECT * FROM {}.nightly_stats WHERE visitseq_uuid=%s"
|
|
1313
|
+
sql_params: list[sql.Composable] = [sql.Identifier(self.metadata_db_schema)]
|
|
1314
|
+
query_params = (visitseq_uuid,)
|
|
1315
|
+
stats_df = self.pd_read_sql(query_template, sql_params, query_params)
|
|
1316
|
+
return stats_df
|
|
1317
|
+
|
|
1318
|
+
def conda_env_is_saved(self, conda_env_hash: bytes) -> bool:
|
|
1319
|
+
"""Check whether a conda environment with the given hash is already
|
|
1320
|
+
stored in the database.
|
|
1321
|
+
|
|
1322
|
+
Parameters
|
|
1323
|
+
----------
|
|
1324
|
+
conda_env_hash : `bytes`
|
|
1325
|
+
The SHA‑256 digest of the conda environment JSON representation.
|
|
1326
|
+
This value is expected to be the same output returned by
|
|
1327
|
+
`compute_conda_env`.
|
|
1328
|
+
|
|
1329
|
+
Returns
|
|
1330
|
+
-------
|
|
1331
|
+
env_exists : `bool`
|
|
1332
|
+
``True`` if a row with the supplied hash exists in the
|
|
1333
|
+
``conda_env`` table; ``False`` otherwise.
|
|
1334
|
+
"""
|
|
1335
|
+
query = sql.SQL("SELECT EXISTS(SELECT true from {}.conda_env WHERE conda_env_hash={})").format(
|
|
1336
|
+
sql.Identifier(self.metadata_db_schema), sql.Placeholder("conda_env_hash")
|
|
1337
|
+
)
|
|
1338
|
+
data = {"conda_env_hash": conda_env_hash}
|
|
1339
|
+
result = self.query(query, data, commit=False, return_result=True)
|
|
1340
|
+
env_exists = result[0][0]
|
|
1341
|
+
assert isinstance(env_exists, bool)
|
|
1342
|
+
return env_exists
|
|
1343
|
+
|
|
1344
|
+
def record_conda_env(
|
|
1345
|
+
self, conda_env_hash: bytes | None = None, conda_env_json: str | None = None
|
|
1346
|
+
) -> bytes:
|
|
1347
|
+
"""Record the current Conda environment in the database.
|
|
1348
|
+
|
|
1349
|
+
Parameters
|
|
1350
|
+
----------
|
|
1351
|
+
conda_env_hash : `bytes` or `None`
|
|
1352
|
+
The SHA‑256 digest of the Conda environment JSON
|
|
1353
|
+
representation. ``None`` (the default)
|
|
1354
|
+
uses the current conda environment.
|
|
1355
|
+
conda_env_json : `str` or `None`
|
|
1356
|
+
The json representing the conda environment.
|
|
1357
|
+
``None`` (the default) uses the current
|
|
1358
|
+
conda environment.
|
|
1359
|
+
"""
|
|
1360
|
+
if conda_env_hash is None != conda_env_json is None:
|
|
1361
|
+
raise ValueError("conda_env_hash and conda_env_json must both be set, or neither.")
|
|
1362
|
+
|
|
1363
|
+
if conda_env_hash is None:
|
|
1364
|
+
conda_env_hash, conda_env_json = compute_conda_env()
|
|
1365
|
+
|
|
1366
|
+
assert isinstance(conda_env_hash, bytes)
|
|
1367
|
+
assert isinstance(conda_env_json, str)
|
|
1368
|
+
|
|
1369
|
+
if self.conda_env_is_saved(conda_env_hash):
|
|
1370
|
+
warnings.warn("Conda env with hash already exists, not saving again.")
|
|
1371
|
+
return conda_env_hash
|
|
1372
|
+
|
|
1373
|
+
query = sql.SQL("INSERT INTO {}.conda_env (conda_env_hash, conda_env) VALUES ({}, {})").format(
|
|
1374
|
+
sql.Identifier(self.metadata_db_schema),
|
|
1375
|
+
sql.Placeholder("conda_env_hash"),
|
|
1376
|
+
sql.Placeholder("conda_env"),
|
|
1377
|
+
)
|
|
1378
|
+
data = {"conda_env_hash": conda_env_hash, "conda_env": conda_env_json}
|
|
1379
|
+
self.query(query, data, commit=True, return_result=False)
|
|
1380
|
+
return conda_env_hash
|
|
1381
|
+
|
|
1382
|
+
def _find_visitseq_table(self, visitseq_uuid: UUID) -> str:
|
|
1383
|
+
# Figure out which child table the visitseq_uuid is in by
|
|
1384
|
+
# querying each in turn.
|
|
1385
|
+
|
|
1386
|
+
for table in ("mixedvisitseq", "completed", "simulations", "visitseq"):
|
|
1387
|
+
query = sql.SQL("SELECT COUNT(*) FROM {}.{} WHERE visitseq_uuid={};").format(
|
|
1388
|
+
sql.Identifier(self.metadata_db_schema),
|
|
1389
|
+
sql.Identifier(table),
|
|
1390
|
+
sql.Placeholder("visitseq_uuid"),
|
|
1391
|
+
)
|
|
1392
|
+
data = {"visitseq_uuid": visitseq_uuid}
|
|
1393
|
+
result = self.query(query, data)
|
|
1394
|
+
assert isinstance(result[0][0], int)
|
|
1395
|
+
num_rows: int = result[0][0]
|
|
1396
|
+
if num_rows > 0:
|
|
1397
|
+
return table
|
|
1398
|
+
|
|
1399
|
+
raise ValueError("Visit sequence not found.")
|
|
1400
|
+
|
|
1401
|
+
def sims_on_nights(
|
|
1402
|
+
self,
|
|
1403
|
+
first_day_obs: str | date | int | None = None,
|
|
1404
|
+
last_day_obs: str | date | int | None = None,
|
|
1405
|
+
tags: tuple[str, ...] = ("prenight", "ideal", "nominal"),
|
|
1406
|
+
telescope: str = "simonyi",
|
|
1407
|
+
max_simulation_age: int = 2,
|
|
1408
|
+
) -> pd.DataFrame:
|
|
1409
|
+
"""Return a table of simulations that cover a given night range.
|
|
1410
|
+
|
|
1411
|
+
Parameters
|
|
1412
|
+
----------
|
|
1413
|
+
first_day_obs : `str` | `date` | `int` | `None`, optional
|
|
1414
|
+
The first observation day of the window for which simulations are
|
|
1415
|
+
requested. It can be specified as a `datetime.date` object, an
|
|
1416
|
+
ISO‑formatted string (e.g. ``'2025-12-01'``), or an integer in
|
|
1417
|
+
SMTN‑032 format (e.g. ``20251201``). If ``None`` (the default),
|
|
1418
|
+
the method uses the current dayobs.
|
|
1419
|
+
last_day_obs : `str` | `date` | `int` | `None`, optional
|
|
1420
|
+
The last observation day of the window. It accepts the same
|
|
1421
|
+
types as ``first_day_obs``. If ``None`` the same value as
|
|
1422
|
+
``first_day_obs`` is used, giving a single‑night query.
|
|
1423
|
+
tags : `tuple[str]`, optional
|
|
1424
|
+
A sequence of tags that must be present on the simulation record.
|
|
1425
|
+
The default tags are ``("prenight", "ideal", "nominal")``. If
|
|
1426
|
+
an empty tuple is supplied, the tag test is omitted.
|
|
1427
|
+
telescope : `str`, optional
|
|
1428
|
+
The telescope simulated. The default is ``simonyi``.
|
|
1429
|
+
max_simulation_age : `int`, optional
|
|
1430
|
+
The maximum age of a simulation in days.
|
|
1431
|
+
The default is 2 days.
|
|
1432
|
+
|
|
1433
|
+
Returns
|
|
1434
|
+
-------
|
|
1435
|
+
vseqs : `pd.DataFrame`
|
|
1436
|
+
A table of metadata for matching simulations.
|
|
1437
|
+
"""
|
|
1438
|
+
|
|
1439
|
+
if first_day_obs is None:
|
|
1440
|
+
first_day_obs = datetime.now(timezone(timedelta(hours=-12))).date()
|
|
1441
|
+
if last_day_obs is None:
|
|
1442
|
+
last_day_obs = first_day_obs
|
|
1443
|
+
|
|
1444
|
+
first_day_obs = dayobs_to_date(first_day_obs)
|
|
1445
|
+
last_day_obs = dayobs_to_date(last_day_obs)
|
|
1446
|
+
|
|
1447
|
+
assert isinstance(first_day_obs, date)
|
|
1448
|
+
assert isinstance(last_day_obs, date)
|
|
1449
|
+
|
|
1450
|
+
if len(tags) > 0:
|
|
1451
|
+
tags_json = json.dumps(list(tags))
|
|
1452
|
+
query_template = """
|
|
1453
|
+
SELECT *
|
|
1454
|
+
FROM {}.simulations_extra AS s
|
|
1455
|
+
WHERE %s BETWEEN first_day_obs AND last_day_obs
|
|
1456
|
+
AND %s BETWEEN first_day_obs AND last_day_obs
|
|
1457
|
+
AND telescope = %s
|
|
1458
|
+
AND tags @> %s::JSONB
|
|
1459
|
+
AND creation_time >= NOW() - INTERVAL '%s days'
|
|
1460
|
+
"""
|
|
1461
|
+
query_params: Tuple = (first_day_obs, last_day_obs, telescope, tags_json, max_simulation_age)
|
|
1462
|
+
else:
|
|
1463
|
+
query_template = """
|
|
1464
|
+
SELECT *
|
|
1465
|
+
FROM {}.simulations_extra AS s
|
|
1466
|
+
WHERE %s BETWEEN first_day_obs AND last_day_obs
|
|
1467
|
+
AND %s BETWEEN first_day_obs AND last_day_obs
|
|
1468
|
+
AND telescope = %
|
|
1469
|
+
AND creation_time >= NOW() - INTERVAL '%s days'
|
|
1470
|
+
"""
|
|
1471
|
+
query_params = (first_day_obs, last_day_obs, telescope, max_simulation_age)
|
|
1472
|
+
|
|
1473
|
+
vseqs = self.pd_read_sql(
|
|
1474
|
+
query_template,
|
|
1475
|
+
[sql.Identifier(self.metadata_db_schema)],
|
|
1476
|
+
query_params,
|
|
1477
|
+
)
|
|
1478
|
+
return vseqs
|
|
1479
|
+
|
|
1480
|
+
def sims_on_night_with_stats(
|
|
1481
|
+
self,
|
|
1482
|
+
day_obs: str | date | int | None = None,
|
|
1483
|
+
tags: tuple[str, ...] = ("prenight", "ideal", "nominal"),
|
|
1484
|
+
telescope: str = "simonyi",
|
|
1485
|
+
max_simulation_age: int = 2,
|
|
1486
|
+
) -> pd.DataFrame:
|
|
1487
|
+
"""Return a table of simulations that cover a given night range.
|
|
1488
|
+
|
|
1489
|
+
Parameters
|
|
1490
|
+
----------
|
|
1491
|
+
day_obs : `str` | `date` | `int` | `None`, optional
|
|
1492
|
+
The day_obs for which to get simulations.
|
|
1493
|
+
tags : `tuple[str]`, optional
|
|
1494
|
+
A sequence of tags that must be present on the simulation record.
|
|
1495
|
+
The default tags are ``("prenight", "ideal", "nominal")``. If
|
|
1496
|
+
an empty tuple is supplied, the tag test is omitted.
|
|
1497
|
+
telescope : `str`, optional
|
|
1498
|
+
The telescope simulated. The default is ``simonyi``.
|
|
1499
|
+
max_simulation_age : `int`, optional
|
|
1500
|
+
The maximum age of a simulation in days.
|
|
1501
|
+
The default is 2 days.
|
|
1502
|
+
|
|
1503
|
+
Returns
|
|
1504
|
+
-------
|
|
1505
|
+
vseqs : `pd.DataFrame`
|
|
1506
|
+
A table of metadata for matching simulations.
|
|
1507
|
+
"""
|
|
1508
|
+
|
|
1509
|
+
if day_obs is None:
|
|
1510
|
+
day_obs = datetime.now(timezone(timedelta(hours=-12))).date()
|
|
1511
|
+
|
|
1512
|
+
day_obs = dayobs_to_date(day_obs)
|
|
1513
|
+
assert isinstance(day_obs, date)
|
|
1514
|
+
|
|
1515
|
+
if len(tags) > 0:
|
|
1516
|
+
tags_json = json.dumps(list(tags))
|
|
1517
|
+
query_template = """
|
|
1518
|
+
WITH aggstats AS (
|
|
1519
|
+
SELECT
|
|
1520
|
+
visitseq_uuid,
|
|
1521
|
+
JSONB_OBJECT_AGG(
|
|
1522
|
+
value_name,
|
|
1523
|
+
TO_JSONB(ns) - 'value_name' - 'visitseq_uuid' - 'day_obs'
|
|
1524
|
+
) AS stats
|
|
1525
|
+
FROM {}.nightly_stats AS ns
|
|
1526
|
+
GROUP BY ns.visitseq_uuid
|
|
1527
|
+
)
|
|
1528
|
+
SELECT s.*, ns.stats
|
|
1529
|
+
FROM {}.simulations_extra AS s
|
|
1530
|
+
LEFT JOIN aggstats AS ns ON s.visitseq_uuid=ns.visitseq_uuid
|
|
1531
|
+
WHERE %s BETWEEN first_day_obs AND last_day_obs
|
|
1532
|
+
AND telescope = %s
|
|
1533
|
+
AND tags @> %s::JSONB
|
|
1534
|
+
AND creation_time >= NOW() - INTERVAL '%s days'
|
|
1535
|
+
"""
|
|
1536
|
+
query_params: Tuple = (day_obs, telescope, tags_json, max_simulation_age)
|
|
1537
|
+
else:
|
|
1538
|
+
query_template = """
|
|
1539
|
+
WITH aggstats AS (
|
|
1540
|
+
SELECT
|
|
1541
|
+
visitseq_uuid,
|
|
1542
|
+
JSONB_OBJECT_AGG(
|
|
1543
|
+
value_name,
|
|
1544
|
+
TO_JSONB(ns) - 'value_name' - 'visitseq_uuid' - 'day_obs'
|
|
1545
|
+
) AS stats
|
|
1546
|
+
FROM {}.nightly_stats AS ns
|
|
1547
|
+
GROUP BY ns.visitseq_uuid
|
|
1548
|
+
)
|
|
1549
|
+
SELECT s.*, ns.stats
|
|
1550
|
+
FROM {}.simulations_extra AS s
|
|
1551
|
+
LEFT JOIN aggstats AS ns ON s.visitseq_uuid=ns.visitseq_uuid
|
|
1552
|
+
WHERE %s BETWEEN first_day_obs AND last_day_obs
|
|
1553
|
+
AND telescope = %
|
|
1554
|
+
AND creation_time >= NOW() - INTERVAL '%s days'
|
|
1555
|
+
"""
|
|
1556
|
+
query_params = (day_obs, telescope, max_simulation_age)
|
|
1557
|
+
|
|
1558
|
+
vseqs = self.pd_read_sql(
|
|
1559
|
+
query_template,
|
|
1560
|
+
[sql.Identifier(self.metadata_db_schema), sql.Identifier(self.metadata_db_schema)],
|
|
1561
|
+
query_params,
|
|
1562
|
+
)
|
|
1563
|
+
return vseqs
|
|
1564
|
+
|
|
1565
|
+
def import_sim_from_yaml(self, metadata_yaml: str, archive_base: str | None = None) -> UUID:
|
|
1566
|
+
"""Import a simulation from a YAML metadata description.
|
|
1567
|
+
|
|
1568
|
+
Parameters
|
|
1569
|
+
----------
|
|
1570
|
+
metadata_yaml : `str`
|
|
1571
|
+
YAML string with simulation metadata.
|
|
1572
|
+
archive_base : `str` or `None`
|
|
1573
|
+
Base location of the archive to which to copy the visit
|
|
1574
|
+
sequence. If ``None``, the visits will not be copied.
|
|
1575
|
+
Defaults to ``None``.
|
|
1576
|
+
|
|
1577
|
+
Returns
|
|
1578
|
+
-------
|
|
1579
|
+
visitseq_uuid : `UUID`
|
|
1580
|
+
The UUID of the newly registered visit sequence.
|
|
1581
|
+
"""
|
|
1582
|
+
|
|
1583
|
+
sim_archive_metadata = yaml.safe_load(metadata_yaml)
|
|
1584
|
+
|
|
1585
|
+
if "label" not in sim_archive_metadata:
|
|
1586
|
+
raise ValueError("Metadata yaml must include a label")
|
|
1587
|
+
|
|
1588
|
+
if "files" not in sim_archive_metadata or "observations" not in sim_archive_metadata["files"]:
|
|
1589
|
+
raise ValueError("Metadata yaml must include the observations file")
|
|
1590
|
+
|
|
1591
|
+
label = sim_archive_metadata["label"]
|
|
1592
|
+
record_sim_kwargs = {"label": label}
|
|
1593
|
+
|
|
1594
|
+
# The prenight labels usually end in the creation time.
|
|
1595
|
+
if " ".join(label.split()[-3:-1]) == "run at":
|
|
1596
|
+
maybe_date_str = label.split()[-1]
|
|
1597
|
+
try:
|
|
1598
|
+
record_sim_kwargs["creation_time"] = Time(datetime.fromisoformat(maybe_date_str))
|
|
1599
|
+
except ValueError:
|
|
1600
|
+
pass
|
|
1601
|
+
|
|
1602
|
+
for key in ["scheduler_version", "telescope"]:
|
|
1603
|
+
if key in sim_archive_metadata:
|
|
1604
|
+
record_sim_kwargs[key] = sim_archive_metadata[key]
|
|
1605
|
+
|
|
1606
|
+
for side in ["first", "last"]:
|
|
1607
|
+
try:
|
|
1608
|
+
record_sim_kwargs[f"{side}_day_obs"] = sim_archive_metadata["simulated_dates"][side]
|
|
1609
|
+
except KeyError:
|
|
1610
|
+
pass
|
|
1611
|
+
|
|
1612
|
+
# Get the visits hdf5 file
|
|
1613
|
+
with TemporaryDirectory() as temp_dir:
|
|
1614
|
+
visits_h5_fname = Path(temp_dir) / "visits.h5"
|
|
1615
|
+
obs_rp = ResourcePath(sim_archive_metadata["files"]["observations"]["url"])
|
|
1616
|
+
with obs_rp.as_local() as obs_local_rp:
|
|
1617
|
+
opsimdb_to_hdf5(obs_local_rp.ospath, visits_h5_fname)
|
|
1618
|
+
|
|
1619
|
+
record_sim_kwargs["visits"] = pd.read_hdf(visits_h5_fname, "observations")
|
|
1620
|
+
|
|
1621
|
+
sim_uuid = self.record_simulation_metadata(**record_sim_kwargs)
|
|
1622
|
+
|
|
1623
|
+
if archive_base is not None:
|
|
1624
|
+
# Import here to avoid circular imports
|
|
1625
|
+
from .vseqarchive import add_file
|
|
1626
|
+
|
|
1627
|
+
add_file(self, sim_uuid, visits_h5_fname, "visits", archive_base)
|
|
1628
|
+
|
|
1629
|
+
if "files" in sim_archive_metadata:
|
|
1630
|
+
for file_type in sim_archive_metadata["files"]:
|
|
1631
|
+
file_rp = ResourcePath(sim_archive_metadata["files"][file_type]["url"])
|
|
1632
|
+
file_sha256 = bytes.fromhex(hashlib.sha256(file_rp.read()).hexdigest())
|
|
1633
|
+
self.register_file(sim_uuid, file_type, file_sha256, file_rp)
|
|
1634
|
+
|
|
1635
|
+
if "tags" in sim_archive_metadata:
|
|
1636
|
+
valid_tags = [t for t in sim_archive_metadata["tags"] if isinstance(t, str)]
|
|
1637
|
+
self.tag(sim_uuid, *valid_tags)
|
|
1638
|
+
|
|
1639
|
+
return sim_uuid
|
|
1640
|
+
|
|
1641
|
+
def import_sim_from_prototype_sim_archive(
|
|
1642
|
+
self, archive_base: str, sim_date: str | date, sim_index: str | int, proto_sim_archive_uri: str
|
|
1643
|
+
) -> UUID:
|
|
1644
|
+
"""Import a simulation metadata from the prototype achive.
|
|
1645
|
+
|
|
1646
|
+
Parameters
|
|
1647
|
+
----------
|
|
1648
|
+
archive_base : `str`
|
|
1649
|
+
Base location where the new archive entry will be written
|
|
1650
|
+
(e.g. an S3 bucket or a local directory). The method
|
|
1651
|
+
will write and hdf5 with visits to this location and
|
|
1652
|
+
register the resulting URL in the database.
|
|
1653
|
+
sim_date : `str` or `datetime.date`
|
|
1654
|
+
The ISO‑formatted date (e.g. ``'2025-03-12'``) or a
|
|
1655
|
+
``date`` object that identifies (with the sim_index)
|
|
1656
|
+
a simulation in the prototype archive.
|
|
1657
|
+
sim_index : `str` or `int`
|
|
1658
|
+
The index of the simulation which completes the
|
|
1659
|
+
identification of the simulation in the prototype.
|
|
1660
|
+
proto_sim_archive_uri : `str`
|
|
1661
|
+
Base URI of the prototype simulation archive. By default
|
|
1662
|
+
this is ``'s3://rubin:rubin-scheduler-prenight/opsim/'``.
|
|
1663
|
+
|
|
1664
|
+
Returns
|
|
1665
|
+
-------
|
|
1666
|
+
visitseq_uuid : `UUID`
|
|
1667
|
+
The UUID of the visit sequence that was created and
|
|
1668
|
+
registered.
|
|
1669
|
+
"""
|
|
1670
|
+
|
|
1671
|
+
metadata_yaml = metadata_yaml_from_prototype(sim_date, sim_index, proto_sim_archive_uri)
|
|
1672
|
+
sim_uuid = self.import_sim_from_yaml(metadata_yaml, archive_base)
|
|
1673
|
+
self.tag(sim_uuid, "from_prototype_sim_archive")
|
|
1674
|
+
|
|
1675
|
+
sim_date_str = sim_date if isinstance(sim_date, str) else sim_date.isoformat()
|
|
1676
|
+
self.comment(sim_uuid, f"Imported from prototype sim_archive {sim_date_str}, {sim_index}")
|
|
1677
|
+
return sim_uuid
|
|
1678
|
+
|
|
1679
|
+
def sim_metadata_yaml(self, visitseq_uuid: UUID) -> str:
|
|
1680
|
+
"""Return a YAML representation of a simulation's metadata.
|
|
1681
|
+
|
|
1682
|
+
Parameters
|
|
1683
|
+
----------
|
|
1684
|
+
visitseq_uuid : `UUID`
|
|
1685
|
+
The unique identifier of the visit sequence for which
|
|
1686
|
+
metadata should be exported.
|
|
1687
|
+
|
|
1688
|
+
Returns
|
|
1689
|
+
-------
|
|
1690
|
+
md_yaml: `str`
|
|
1691
|
+
yaml representation of (some of) the simulation metadata
|
|
1692
|
+
in the metadata database.
|
|
1693
|
+
"""
|
|
1694
|
+
|
|
1695
|
+
metadata_seq = self.get_visitseq_metadata(visitseq_uuid, "simulations_extra")
|
|
1696
|
+
|
|
1697
|
+
metadata: dict = {"uuid": str(metadata_seq.visitseq_uuid), "label": str(metadata_seq.visitseq_label)}
|
|
1698
|
+
for key in ["telescope", "tags", "scheduler_version"]:
|
|
1699
|
+
if metadata_seq[key] is not None:
|
|
1700
|
+
metadata[key] = metadata_seq[key]
|
|
1701
|
+
|
|
1702
|
+
if metadata_seq["creation_time"] is not None:
|
|
1703
|
+
metadata["creation_time"] = metadata_seq["creation_time"].isoformat()
|
|
1704
|
+
|
|
1705
|
+
if metadata_seq.first_day_obs is not None or metadata_seq.last_day_obs is not None:
|
|
1706
|
+
metadata["simulation_dates"] = {}
|
|
1707
|
+
if metadata_seq.first_day_obs is not None:
|
|
1708
|
+
metadata["simulation_dates"]["first"] = metadata_seq.first_day_obs
|
|
1709
|
+
if metadata_seq.last_day_obs is not None:
|
|
1710
|
+
metadata["simulation_dates"]["last"] = metadata_seq.last_day_obs
|
|
1711
|
+
|
|
1712
|
+
if metadata_seq.files is not None:
|
|
1713
|
+
metadata["files"] = {}
|
|
1714
|
+
for file_type in metadata_seq.files:
|
|
1715
|
+
metadata["files"][file_type] = {
|
|
1716
|
+
"url": metadata_seq.files[file_type],
|
|
1717
|
+
"name": ResourcePath(metadata_seq.files[file_type]).basename(),
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
metadata_yaml = yaml.dump(metadata)
|
|
1721
|
+
return metadata_yaml
|