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,1773 @@
1
+ #
2
+ # LSST Data Management System
3
+ # Copyright 2008, 2009, 2010, 2011, 2012 LSST Corporation.
4
+ #
5
+ # This product includes software developed by the
6
+ # LSST Project (http://www.lsst.org/).
7
+ #
8
+ # This program is free software: you can redistribute it and/or modify
9
+ # it under the terms of the GNU General Public License as published by
10
+ # the Free Software Foundation, either version 3 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # This program is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the LSST License Statement and
19
+ # the GNU General Public License along with this program. If not,
20
+ # see <http://www.lsstcorp.org/LegalNotices/>.
21
+ #
22
+
23
+ """
24
+ Sed -
25
+
26
+ Class data:
27
+ wavelen (nm)
28
+ flambda (ergs/cm^2/s/nm)
29
+ fnu (Jansky)
30
+ zp (translates to units of fnu = -8.9 (if Janskys) or 48.6 (ergs/cm^2/s/hz))
31
+ the name of the sed file
32
+
33
+ It is important to note the units are NANOMETERS, not ANGSTROMS.
34
+
35
+ Methods:
36
+ Because of how these methods will be applied for catalog generation,
37
+ (taking one base SED and then applying various dust extinctions and redshifts),
38
+ many of the methods will either work on, and update self, OR they can be given
39
+ a set of lambda/flambda arrays and then will return new versions of
40
+ these arrays. In general, the methods will not explicitly set flambda or fnu to
41
+ something you (the user) did not specify. So, for example, when calculating
42
+ magnitudes (which depend on a wavelength/fnu gridded to match the given
43
+ bandpass) the wavelength and fnu used are temporary copies and the object
44
+ itself is not changed.
45
+
46
+ In general, the philosophy of Sed.py is to not define the wavelength
47
+ grid for the object until necessary (so, not until needed for the
48
+ magnitude calculation or resample_sed is called). At that time the min/max/step
49
+ wavelengths or the bandpass wavelengths are used to define a new wavelength
50
+ grid for the sed object.
51
+
52
+ When considering whether to use the internal wavelen/flambda (self) values,
53
+ versus input values:
54
+ For consistency, anytime self.wavelen/flambda is used, it will be updated
55
+ if the values are changed (except in the special case of calculating
56
+ magnitudes), and if self.wavelen/flambda is updated, self.fnu will be set to
57
+ None. This is because many operations are typically chained together
58
+ which alter flambda -- so it is more efficient to wait and recalculate fnu at
59
+ the end, plus it avoids possible de-synchronization errors
60
+ (flambda reflecting the addition of dust while fnu does
61
+ not, for example). If arrays are passed into a method, they will not be
62
+ altered and the arrays which are returned will be allocated new memory.
63
+
64
+ Another general philosophy for Sed.py is use separate methods for items which
65
+ only need to be generated once for several objects (such as the dust A_x, b_x
66
+ arrays). This allows the user to optimize their code for faster operation,
67
+ depending on what their requirements are (see example_SedBandpass_star.py and
68
+ exampleSedBandpass_galaxy for examples).
69
+ """
70
+
71
+ __all__ = ("Sed", "cache_lsst_seds", "read_close__kurucz")
72
+
73
+ import gzip
74
+ import os
75
+ import pickle
76
+ import sys
77
+ import time
78
+ import warnings
79
+
80
+ import numpy
81
+
82
+ try:
83
+ from numpy import trapezoid as trapezoid
84
+ except ImportError:
85
+ from numpy import trapz as trapezoid
86
+ from rubin_scheduler.data import get_data_dir
87
+
88
+ from .physical_parameters import PhysicalParameters
89
+ from .spectral_resampling import spectres
90
+
91
+ _global_lsst_sed_cache = None
92
+
93
+ # a cache for ASCII files read-in by the user
94
+ _global_misc_sed_cache = None
95
+
96
+
97
+ class SedCacheError(Exception):
98
+ pass
99
+
100
+
101
+ class SedUnpickler(pickle.Unpickler):
102
+ _allowed_obj = (
103
+ ("numpy", "ndarray"),
104
+ ("numpy", "dtype"),
105
+ ("numpy.core.multiarray", "_reconstruct"),
106
+ )
107
+
108
+ def find_class(self, module, name):
109
+ allowed = False
110
+ for _module, _name in self._allowed_obj:
111
+ if module == _module and name == _name:
112
+ allowed = True
113
+ break
114
+
115
+ if not allowed:
116
+ raise RuntimeError(
117
+ "Cannot call find_class() on %s, %s with SedUnpickler " % (module, name)
118
+ + "this is for security reasons\n"
119
+ + "https://docs.python.org/3.1/library/pickle.html#pickle-restrict"
120
+ )
121
+
122
+ if module == "numpy":
123
+ if name == "ndarray":
124
+ return getattr(numpy, name)
125
+ elif name == "dtype":
126
+ return getattr(numpy, name)
127
+ else:
128
+ raise RuntimeError("SedUnpickler not meant to load numpy.%s" % name)
129
+ elif module == "numpy.core.multiarray":
130
+ return getattr(numpy.core.multiarray, name)
131
+ else:
132
+ raise RuntimeError("SedUnpickler cannot handle module %s" % module)
133
+
134
+
135
+ def _validate_sed_cache():
136
+ """
137
+ Verifies that the pickled SED cache exists, is a dict, and contains
138
+ an entry for every SED in starSED/ and galaxySED. Does nothing if so,
139
+ raises a RuntimeError if false.
140
+
141
+ We are doing this here so that sims_sed_library does not have to depend
142
+ on any lsst testing software (in which case, users would have to get
143
+ a new copy of sims_sed_library every time the upstream software changed).
144
+
145
+ We are doing this through a method (rather than giving users access to
146
+ _global_lsst_sed_cache) so that users do not accidentally ruin
147
+ _global_lsst_sed_cache.
148
+ """
149
+ global _global_lsst_sed_cache
150
+ if _global_lsst_sed_cache is None:
151
+ raise SedCacheError("_global_lsst_sed_cache does not exist")
152
+ if not isinstance(_global_lsst_sed_cache, dict):
153
+ raise SedCacheError("_global_lsst_sed_cache is a %s; not a dict" % str(type(_global_lsst_sed_cache)))
154
+ sed_dir = os.path.join(get_data_dir(), "sims_sed_library")
155
+ sub_dir_list = ["galaxySED", "starSED"]
156
+ file_ct = 0
157
+ for sub_dir in sub_dir_list:
158
+ tree = os.walk(os.path.join(sed_dir, sub_dir))
159
+ for entry in tree:
160
+ local_dir = entry[0]
161
+ file_list = entry[2]
162
+ for file_name in file_list:
163
+ if file_name.endswith(".gz"):
164
+ full_name = os.path.join(sed_dir, sub_dir, local_dir, file_name)
165
+ if full_name not in _global_lsst_sed_cache:
166
+ raise SedCacheError("%s is not in _global_lsst_sed_cache" % full_name)
167
+ file_ct += 1
168
+ if file_ct == 0:
169
+ raise SedCacheError("There were not files in _global_lsst_sed_cache")
170
+
171
+ return
172
+
173
+
174
+ def _compare_cached_versus_uncached():
175
+ """
176
+ Verify that loading an SED from the pickled cache give identical
177
+ results to loading the same SED from ASCII
178
+ """
179
+ sed_dir = os.path.join(get_data_dir(), "sims_sed_library", "starSED", "kurucz")
180
+
181
+ dtype = numpy.dtype([("wavelen", float), ("flambda", float)])
182
+
183
+ sed_name_list = os.listdir(sed_dir)
184
+ msg = (
185
+ "An SED loaded from the pickled cache is not "
186
+ "identical to the same SED loaded from ASCII; "
187
+ "it is possible that the pickled cache was incorrectly "
188
+ "created in sims_sed_library\n\n"
189
+ "Try removing the cache file (the name should hav been printed "
190
+ "to stdout above) and re-running sims_phot_utils.cache_LSST_seds()"
191
+ )
192
+ for ix in range(5):
193
+ full_name = os.path.join(sed_dir, sed_name_list[ix])
194
+ from_np = numpy.genfromtxt(full_name, dtype=dtype)
195
+ ss_cache = Sed()
196
+ ss_cache.read_sed_flambda(full_name)
197
+ ss_uncache = Sed(wavelen=from_np["wavelen"], flambda=from_np["flambda"], name=full_name)
198
+
199
+ if not ss_cache == ss_uncache:
200
+ raise SedCacheError(msg)
201
+
202
+
203
+ def _generate_sed_cache(cache_dir, cache_name):
204
+ """
205
+ Read all of the SEDs from sims_sed_library into a dict.
206
+ Pickle the dict and store it in
207
+ sims_phot_utils/cacheDir/lsst_sed_cache.p
208
+
209
+ Parameters
210
+ ----------
211
+ cache_dir : `str`
212
+ The directory where the cache will be created.
213
+ cache_name : `str`
214
+ The name of the cache to be created.
215
+
216
+ Returns
217
+ -------
218
+ cache : `dict` [`str`, `Sed`]
219
+ The dict of SEDs (keyed to their full file name).
220
+ """
221
+ sed_root = os.path.join(get_data_dir(), "sims_sed_library")
222
+ dtype = numpy.dtype([("wavelen", float), ("flambda", float)])
223
+
224
+ sub_dir_list = ["agnSED", "flatSED", "ssmSED", "starSED", "galaxySED"]
225
+
226
+ cache = {}
227
+
228
+ total_files = 0
229
+ for sub_dir in sub_dir_list:
230
+ dir_tree = os.walk(os.path.join(sed_root, sub_dir))
231
+ for sub_tree in dir_tree:
232
+ total_files += len([name for name in sub_tree[2] if name.endswith(".gz")])
233
+
234
+ t_start = time.time()
235
+ print("This could take about 15 minutes.")
236
+ print("Note: not all SED files are the same size. ")
237
+ print("Do not expect the loading rate to be uniform.\n")
238
+
239
+ for sub_dir in sub_dir_list:
240
+ dir_tree = os.walk(os.path.join(sed_root, sub_dir))
241
+ for sub_tree in dir_tree:
242
+ dir_name = sub_tree[0]
243
+ file_list = sub_tree[2]
244
+
245
+ for file_name in file_list:
246
+ if file_name.endswith(".gz"):
247
+ try:
248
+ full_name = os.path.join(dir_name, file_name)
249
+ data = numpy.genfromtxt(full_name, dtype=dtype)
250
+ cache[full_name] = (data["wavelen"], data["flambda"])
251
+ if len(cache) % (total_files // 20) == 0:
252
+ if len(cache) > total_files // 20:
253
+ sys.stdout.write("\r")
254
+ sys.stdout.write(
255
+ "loaded %d of %d files in about %.2f seconds"
256
+ % (len(cache), total_files, time.time() - t_start)
257
+ )
258
+ sys.stdout.flush()
259
+ except Exception:
260
+ pass
261
+
262
+ print("\n")
263
+
264
+ with open(os.path.join(cache_dir, cache_name), "wb") as file_handle:
265
+ pickle.dump(cache, file_handle)
266
+
267
+ print("LSST SED cache saved to:\n")
268
+ print("%s" % os.path.join(cache_dir, cache_name))
269
+
270
+ # record the specific sims_sed_library directory being cached so that
271
+ # a new cache will be generated if sims_sed_library gets updated
272
+ with open(os.path.join(cache_dir, "cache_version_%d.txt" % sys.version_info.major), "w") as file_handle:
273
+ file_handle.write("%s %s" % (sed_root, cache_name))
274
+
275
+ return cache
276
+
277
+
278
+ def cache_lsst_seds(wavelen_min=None, wavelen_max=None, cache_dir=None):
279
+ """
280
+ Read all of the SEDs in sims_sed_library into a dict. Pickle the dict
281
+ and store it in phot_utils/cacheDir/lsst_sed_cache.p for future use.
282
+
283
+ After the file has initially been created,
284
+ the next time you run this script, it will just use the pickle.
285
+
286
+ Once the dict is loaded, Sed.read_sed_flambda() will be able to read any
287
+ LSST-shipped SED directly from memory, rather than using I/O to read it
288
+ from an ASCII file stored on disk.
289
+
290
+ Note: the dict of cached SEDs will take up about 5GB on disk. Once loaded,
291
+ the cache will take up about 1.5GB of memory.
292
+
293
+ Parameters
294
+ -----------
295
+ wavelen_min : `float`
296
+ Wavelength minimum value to store for each Sed.
297
+ wavelen_max : `float`
298
+ Wavelength maximum value to store for each Sed.
299
+ cache_dir : `str`
300
+ The directory to place the cache pickle.
301
+
302
+ If either of wavelen_min or wavelen_max are not None,
303
+ then every SED in the cache will be
304
+ truncated to only include the wavelength range (in nm) between
305
+ wavelen_min and wavelen_max
306
+ """
307
+
308
+ global _global_lsst_sed_cache
309
+
310
+ sed_cache_name = os.path.join("lsst_sed_cache_%d.p" % sys.version_info.major)
311
+ sed_dir = os.path.join(get_data_dir(), "sims_sed_library")
312
+ if cache_dir is None:
313
+ cache_dir = os.path.join(get_data_dir(), "sims_sed_library", "lsst_sed_cache_dir")
314
+
315
+ if not os.path.exists(cache_dir):
316
+ os.mkdir(cache_dir)
317
+
318
+ must_generate = False
319
+ if not os.path.exists(os.path.join(cache_dir, sed_cache_name)):
320
+ must_generate = True
321
+ if not os.path.exists(os.path.join(cache_dir, "cache_version_%d.txt" % sys.version_info.major)):
322
+ must_generate = True
323
+ else:
324
+ with open(
325
+ os.path.join(cache_dir, "cache_version_%d.txt" % sys.version_info.major),
326
+ "r",
327
+ ) as input_file:
328
+ lines = input_file.readlines()
329
+ if len(lines) != 1:
330
+ must_generate = True
331
+ else:
332
+ info = lines[0].split()
333
+ if len(info) != 2:
334
+ must_generate = True
335
+ elif info[0] != sed_dir:
336
+ must_generate = True
337
+ elif info[1] != sed_cache_name:
338
+ must_generate = True
339
+
340
+ if must_generate:
341
+ print("\nCreating cache of LSST SEDs in:\n%s" % os.path.join(cache_dir, sed_cache_name))
342
+ cache = _generate_sed_cache(cache_dir, sed_cache_name)
343
+ _global_lsst_sed_cache = cache
344
+ else:
345
+ print("\nOpening cache of LSST SEDs in:\n%s" % os.path.join(cache_dir, sed_cache_name))
346
+ with open(os.path.join(cache_dir, sed_cache_name), "rb") as input_file:
347
+ _global_lsst_sed_cache = SedUnpickler(input_file).load()
348
+
349
+ # Now that we have generated/loaded the cache, we must run tests
350
+ # to make sure that the cache is correctly constructed. If these
351
+ # fail, _global_lsst_sed_cache will be set to 'None' and the code will
352
+ # continue running.
353
+ try:
354
+ _validate_sed_cache()
355
+ _compare_cached_versus_uncached()
356
+ except SedCacheError as ee:
357
+ print(ee.message)
358
+ print("Cannot use cache of LSST SEDs")
359
+ _global_lsst_sed_cache = None
360
+ pass
361
+
362
+ if wavelen_min is not None or wavelen_max is not None:
363
+ if wavelen_min is None:
364
+ wavelen_min = 0.0
365
+ if wavelen_max is None:
366
+ wavelen_max = numpy.inf
367
+
368
+ new_cache = {}
369
+ list_of_sed_names = list(_global_lsst_sed_cache.keys())
370
+ for file_name in list_of_sed_names:
371
+ wav, fl = _global_lsst_sed_cache.pop(file_name)
372
+ valid_dexes = numpy.where(numpy.logical_and(wav >= wavelen_min, wav <= wavelen_max))
373
+ new_cache[file_name] = (wav[valid_dexes], fl[valid_dexes])
374
+
375
+ _global_lsst_sed_cache = new_cache
376
+
377
+ return
378
+
379
+
380
+ class Sed:
381
+ """ "Hold and use spectral energy distributions (SEDs)"""
382
+
383
+ def __init__(self, wavelen=None, flambda=None, fnu=None, badval=numpy.nan, name=None):
384
+ """
385
+ Initialize sed object by giving filename or lambda/flambda array.
386
+
387
+ Note that this does *not* regrid flambda and leaves fnu undefined.
388
+ """
389
+ self.fnu = None
390
+ self.wavelen = None
391
+ self.flambda = None
392
+ # self.zp = -8.9 # default units, Jansky.
393
+ self.zp = -2.5 * numpy.log10(3631)
394
+ self.name = name
395
+ self.badval = badval
396
+
397
+ self._phys_params = PhysicalParameters()
398
+
399
+ # If init was given data to initialize class, use it.
400
+ if (wavelen is not None) and ((flambda is not None) or (fnu is not None)):
401
+ if name is None:
402
+ name = "FromArray"
403
+ self.set_sed(wavelen, flambda=flambda, fnu=fnu, name=name)
404
+ return
405
+
406
+ def __eq__(self, other):
407
+ if self.name != other.name:
408
+ return False
409
+ if self.zp != other.zp:
410
+ return False
411
+ if not numpy.isnan(self.badval):
412
+ if self.badval != other.badval:
413
+ return False
414
+ else:
415
+ if not numpy.isnan(other.badval):
416
+ return False
417
+ if self.fnu is not None and other.fnu is None:
418
+ return False
419
+ if self.fnu is None and other.fnu is not None:
420
+ return False
421
+ if self.fnu is not None:
422
+ try:
423
+ numpy.testing.assert_array_equal(self.fnu, other.fnu)
424
+ except AssertionError:
425
+ return False
426
+
427
+ if self.flambda is None and other.flambda is not None:
428
+ return False
429
+ if other.flambda is not None and self.flambda is None:
430
+ return False
431
+ if self.flambda is not None:
432
+ try:
433
+ numpy.testing.assert_array_equal(self.flambda, other.flambda)
434
+ except AssertionError:
435
+ return False
436
+
437
+ if self.wavelen is None and other.wavelen is not None:
438
+ return False
439
+ if self.wavelen is not None and other.wavelen is None:
440
+ return False
441
+ if self.wavelen is not None:
442
+ try:
443
+ numpy.testing.assert_array_equal(self.wavelen, other.wavelen)
444
+ except AssertionError:
445
+ return False
446
+
447
+ return True
448
+
449
+ def __ne__(self, other):
450
+ return not self.__eq__(other)
451
+
452
+ # Methods for getters and setters.
453
+
454
+ def set_sed(self, wavelen, flambda=None, fnu=None, name="FromArray"):
455
+ """
456
+ Populate wavelen/flambda fields in sed by giving lambda/flambda
457
+ or lambda/fnu array.
458
+
459
+ If flambda present, this overrides fnu.
460
+ Method sets fnu=None unless only fnu is given.
461
+ """
462
+ # Check wavelen array for type matches.
463
+ if isinstance(wavelen, numpy.ndarray) is False:
464
+ raise ValueError("Wavelength must be a numpy array")
465
+ # Wavelen type ok - make new copy of data for self.
466
+ self.wavelen = numpy.copy(wavelen)
467
+ self.flambda = None
468
+ self.fnu = None
469
+ # Check if given flambda or fnu.
470
+ if flambda is not None:
471
+ # Check flambda data type and length.
472
+ if (isinstance(flambda, numpy.ndarray) is False) or (len(flambda) != len(self.wavelen)):
473
+ raise ValueError("Flambda must be a numpy array of same length as Wavelen.")
474
+ # Flambda ok, make a new copy of data for self.
475
+ self.flambda = numpy.copy(flambda)
476
+ else:
477
+ # Were passed fnu instead : check fnu data type and length.
478
+ if fnu is None:
479
+ raise ValueError("Both fnu and flambda are 'None', cannot set the SED.")
480
+ elif (isinstance(fnu, numpy.ndarray) is False) or (len(fnu) != len(self.wavelen)):
481
+ raise ValueError("(No Flambda) - Fnu must be numpy array of same length as Wavelen.")
482
+ # Convert fnu to flambda.
483
+ self.wavelen, self.flambda = self.fnu_toflambda(wavelen, fnu)
484
+ self.fnu = fnu
485
+ self.name = name
486
+ return
487
+
488
+ def set_flat_sed(self, wavelen_min=300.0, wavelen_max=1150.0, wavelen_step=0.1, name="Flat"):
489
+ """
490
+ Populate the wavelength/flambda/fnu fields in sed according to a
491
+ flat fnu source.
492
+ """
493
+
494
+ self.wavelen = numpy.arange(wavelen_min, wavelen_max + wavelen_step, wavelen_step, dtype="float")
495
+ self.fnu = numpy.ones(len(self.wavelen), dtype="float") * 3631 # jansky
496
+ self.fnu_toflambda()
497
+ self.name = name
498
+ return
499
+
500
+ def read_sed_flambda(self, filename, name=None, cache_sed=True):
501
+ """
502
+ Read a file containing [lambda Flambda]
503
+ (lambda in nm) (Flambda erg/cm^2/s/nm).
504
+
505
+ Does not resample wavelen/flambda onto grid; leave fnu=None.
506
+ """
507
+ global _global_lsst_sed_cache
508
+ global _global_misc_sed_cache
509
+
510
+ # Try to open data file.
511
+ # ASSUME that if filename ends with '.gz' that the file is gzipped.
512
+ # Otherwise, regular file.
513
+ if filename.endswith(".gz"):
514
+ gzipped_filename = filename
515
+ unzipped_filename = filename[:-3]
516
+ else:
517
+ gzipped_filename = filename + ".gz"
518
+ unzipped_filename = filename
519
+
520
+ cached_source = None
521
+ if _global_lsst_sed_cache is not None:
522
+ if gzipped_filename in _global_lsst_sed_cache:
523
+ cached_source = _global_lsst_sed_cache[gzipped_filename]
524
+ elif unzipped_filename in _global_lsst_sed_cache:
525
+ cached_source = _global_lsst_sed_cache[unzipped_filename]
526
+
527
+ if cached_source is None and _global_misc_sed_cache is not None:
528
+ if gzipped_filename in _global_misc_sed_cache:
529
+ cached_source = _global_misc_sed_cache[gzipped_filename]
530
+ if unzipped_filename in _global_misc_sed_cache:
531
+ cached_source = _global_misc_sed_cache[unzipped_filename]
532
+
533
+ if cached_source is not None:
534
+ sourcewavelen = numpy.copy(cached_source[0])
535
+ sourceflambda = numpy.copy(cached_source[1])
536
+
537
+ if cached_source is None:
538
+ # Read source SED from file - lambda, flambda should be first
539
+ # two columns in the file.
540
+ # lambda should be in nm and flambda should be in ergs/cm2/s/nm
541
+ dtype = numpy.dtype([("wavelen", float), ("flambda", float)])
542
+ try:
543
+ data = numpy.genfromtxt(gzipped_filename, dtype=dtype)
544
+ except IOError:
545
+ try:
546
+ data = numpy.genfromtxt(unzipped_filename, dtype=dtype)
547
+ except Exception as err:
548
+ new_args = [
549
+ err.args[0] + "\n\nError reading sed file %s; " % filename + "it may not exist."
550
+ ]
551
+ for aa in err.args[1:]:
552
+ new_args.append(aa)
553
+ err.args = tuple(new_args)
554
+ raise
555
+
556
+ sourcewavelen = data["wavelen"]
557
+ sourceflambda = data["flambda"]
558
+
559
+ if cache_sed:
560
+ if _global_misc_sed_cache is None:
561
+ _global_misc_sed_cache = {}
562
+ _global_misc_sed_cache[filename] = (
563
+ numpy.copy(sourcewavelen),
564
+ numpy.copy(sourceflambda),
565
+ )
566
+
567
+ self.wavelen = sourcewavelen
568
+ self.flambda = sourceflambda
569
+ self.fnu = None
570
+ if name is None:
571
+ self.name = filename
572
+ else:
573
+ self.name = name
574
+ return
575
+
576
+ def read_sed_fnu(self, filename, name=None):
577
+ """
578
+ Read a file containing [lambda Fnu]
579
+ (lambda in nm) (Fnu in Jansky).
580
+
581
+ Does not resample wavelen/fnu/flambda onto a grid; leaves fnu set.
582
+ """
583
+ # Try to open the data file.
584
+ try:
585
+ if filename.endswith(".gz"):
586
+ f = gzip.open(filename, "rt")
587
+ else:
588
+ f = open(filename, "r")
589
+ # if the above fails, look for the file with and without the gz
590
+ except IOError:
591
+ try:
592
+ if filename.endswith(".gz"):
593
+ f = open(filename[:-3], "r")
594
+ else:
595
+ f = gzip.open(filename + ".gz", "rt")
596
+ except IOError:
597
+ raise IOError("The throughput file %s does not exist" % (filename))
598
+ # Read source SED from file - lambda, fnu should be
599
+ # the first two columns in the file.
600
+ # lambda should be in nm and fnu should be in Jansky.
601
+ sourcewavelen = []
602
+ sourcefnu = []
603
+ for line in f:
604
+ if line.startswith("#"):
605
+ continue
606
+ values = line.split()
607
+ sourcewavelen.append(float(values[0]))
608
+ sourcefnu.append(float(values[1]))
609
+ f.close()
610
+ # Convert to numpy arrays and set members
611
+ self.wavelen = numpy.array(sourcewavelen)
612
+ self.fnu = numpy.array(sourcefnu)
613
+ # convert fnu to flambda
614
+ self.fnu_toflambda()
615
+ if name is None:
616
+ self.name = filename
617
+ else:
618
+ self.name = name
619
+ return
620
+
621
+ def get_sed_flambda(self):
622
+ """
623
+ Return copy of wavelen/flambda.
624
+ """
625
+ # Get new memory copies of the arrays.
626
+ wavelen = numpy.copy(self.wavelen)
627
+ flambda = numpy.copy(self.flambda)
628
+ return wavelen, flambda
629
+
630
+ def get_sed_fnu(self):
631
+ """
632
+ Return copy of wavelen/fnu, without altering self.
633
+ """
634
+ wavelen = numpy.copy(self.wavelen)
635
+ # Check if fnu currently set.
636
+ if self.fnu is not None:
637
+ # Get new memory copy of fnu.
638
+ fnu = numpy.copy(self.fnu)
639
+ else:
640
+ # Fnu was not set .. grab copy fnu without changing self.
641
+ wavelen, fnu = self.flambda_tofnu(self.wavelen, self.flambda)
642
+ # Now wavelen/fnu (new mem) are gridded evenly,
643
+ # but self.wavelen/flambda/fnu remain unchanged.
644
+ return wavelen, fnu
645
+
646
+ # Methods that update or change self.
647
+
648
+ def clear_sed(self):
649
+ """
650
+ Reset all data in sed to None.
651
+ """
652
+ self.wavelen = None
653
+ self.fnu = None
654
+ self.flambda = None
655
+ self.zp = -8.9
656
+ self.name = None
657
+ return
658
+
659
+ def synchronize_sed(self, wavelen_min=None, wavelen_max=None, wavelen_step=None):
660
+ """
661
+ Set all wavelen/flambda/fnu values, potentially on min/max/step grid.
662
+
663
+ Uses flambda to recalculate fnu.
664
+ If wavelen min/max/step are given, resamples
665
+ wavelength/flambda/fnu onto an even grid with these values.
666
+ """
667
+ # Grid wavelength/flambda/fnu if desired.
668
+ if (wavelen_min is not None) and (wavelen_max is not None) and (wavelen_step is not None):
669
+ self.resample_sed(
670
+ wavelen_min=wavelen_min,
671
+ wavelen_max=wavelen_max,
672
+ wavelen_step=wavelen_step,
673
+ )
674
+ # Reset or set fnu.
675
+ self.flambda_tofnu()
676
+ return
677
+
678
+ # Utilities common to several later methods.
679
+
680
+ def _check_use_self(self, wavelen, flux):
681
+ """
682
+ Simple utility to check if should be using self's data or
683
+ passed arrays.
684
+
685
+ Also does data integrity check on wavelen/flux if not self.
686
+ """
687
+ update_self = False
688
+ if (wavelen is None) or (flux is None):
689
+ # Then one of the arrays was not passed -
690
+ # check if this is true for both arrays.
691
+ if (wavelen is not None) or (flux is not None):
692
+ # Then one of the arrays was passed - raise exception.
693
+ raise ValueError("Must either pass *both* wavelen/flux pair, or use defaults.")
694
+ update_self = True
695
+ else:
696
+ # Both of the arrays were passed in - check their validity.
697
+ if (isinstance(wavelen, numpy.ndarray) is False) or (isinstance(flux, numpy.ndarray) is False):
698
+ raise ValueError("Must pass wavelen/flux as numpy arrays.")
699
+ if len(wavelen) != len(flux):
700
+ raise ValueError("Must pass equal length wavelen/flux arrays.")
701
+ return update_self
702
+
703
+ def _need_resample(
704
+ self,
705
+ wavelen_match=None,
706
+ wavelen=None,
707
+ wavelen_min=None,
708
+ wavelen_max=None,
709
+ wavelen_step=None,
710
+ ):
711
+ """
712
+ Check if wavelen or self.wavelen matches wavelen
713
+ or wavelen_min/max/step grid.
714
+ """
715
+ # Check if should use self or passed wavelen.
716
+ if wavelen is None:
717
+ wavelen = self.wavelen
718
+ # Check if wavelength arrays are equal, if wavelen_match passed.
719
+ if wavelen_match is not None:
720
+ if numpy.shape(wavelen_match) != numpy.shape(wavelen):
721
+ need_regrid = True
722
+ else:
723
+ # check the elements to see if any vary
724
+ need_regrid = numpy.any(abs(wavelen_match - wavelen) > 1e-10)
725
+ else:
726
+ need_regrid = True
727
+ # Check if wavelen_min/max/step are set -
728
+ # if ==None, then return (no regridding).
729
+ # It's possible (writeSED) to call this routine,
730
+ # even with no final grid in mind.
731
+ if (wavelen_min is None) and (wavelen_max is None) and (wavelen_step is None):
732
+ need_regrid = False
733
+ else:
734
+ # Okay, now look at comparison of wavelen to the grid.
735
+ wavelen_max_in = wavelen[len(wavelen) - 1]
736
+ wavelen_min_in = wavelen[0]
737
+ # First check match to minimum/maximum :
738
+ if (wavelen_min_in == wavelen_min) and (wavelen_max_in == wavelen_max):
739
+ # Then check on step size in wavelength array.
740
+ stepsize = numpy.unique(numpy.diff(wavelen))
741
+ if (len(stepsize) == 1) and (stepsize[0] == wavelen_step):
742
+ need_regrid = False
743
+ # At this point, need_grid=True unless it's proven to be False,
744
+ # so return value.
745
+ return need_regrid
746
+
747
+ def resample_sed(
748
+ self,
749
+ wavelen=None,
750
+ flux=None,
751
+ wavelen_match=None,
752
+ wavelen_min=None,
753
+ wavelen_max=None,
754
+ wavelen_step=None,
755
+ force=False,
756
+ fill=numpy.nan,
757
+ ):
758
+ """
759
+ Resample flux onto grid defined by min/max/step OR
760
+ another wavelength array.
761
+
762
+ Give method wavelen/flux OR default to self.wavelen/self.flambda.
763
+ Method either returns wavelen/flambda (if given those arrays) or
764
+ updates wavelen/flambda in self.
765
+ If updating self, resets fnu to None.
766
+ Method will first check if resampling needs to be done or not,
767
+ unless 'force' is True.
768
+ """
769
+ # Check if need resampling:
770
+ if force or (
771
+ self._need_resample(
772
+ wavelen_match=wavelen_match,
773
+ wavelen=wavelen,
774
+ wavelen_min=wavelen_min,
775
+ wavelen_max=wavelen_max,
776
+ wavelen_step=wavelen_step,
777
+ )
778
+ ):
779
+ # Is method acting on self.wavelen/flambda or
780
+ # passed in wavelen/flux arrays?
781
+ update_self = self._check_use_self(wavelen, flux)
782
+ if update_self:
783
+ wavelen = self.wavelen
784
+ flux = self.flambda
785
+ self.fnu = None
786
+ # Now, on with the resampling.
787
+ # Set up gridded wavelength or copy of wavelen array to match.
788
+ if wavelen_match is None:
789
+ if (wavelen_min is None) and (wavelen_max is None) and (wavelen_step is None):
790
+ raise ValueError("Must set either wavelen_match or wavelen_min/max/step.")
791
+ wavelen_grid = numpy.arange(
792
+ wavelen_min, wavelen_max + wavelen_step, wavelen_step, dtype="float"
793
+ )
794
+ else:
795
+ wavelen_grid = numpy.copy(wavelen_match)
796
+ # Check if the wavelength range desired and the wavelength
797
+ # range of the object overlap.
798
+ # If there is any non-overlap, raise warning.
799
+ if (wavelen.max() < wavelen_grid.max()) or (wavelen.min() > wavelen_grid.min()):
800
+ warnings.warn(
801
+ "There is an area of non-overlap between desired wavelength range "
802
+ + " (%.2f to %.2f)" % (wavelen_grid.min(), wavelen_grid.max())
803
+ + "and sed %s (%.2f to %.2f)" % (self.name, wavelen.min(), wavelen.max())
804
+ )
805
+ # rebin the spectra. Fill with NaNs if there's non-overlap regions.
806
+ flux_grid = spectres(wavelen_grid, wavelen, flux, fill=fill, verbose=False)
807
+
808
+ # Update self values if necessary.
809
+ if update_self:
810
+ self.wavelen = wavelen_grid
811
+ self.flambda = flux_grid
812
+ return
813
+ return wavelen_grid, flux_grid
814
+ else: # wavelength grids already match.
815
+ update_self = self._check_use_self(wavelen, flux)
816
+ if update_self:
817
+ return
818
+ return wavelen, flux
819
+
820
+ def flambda_tofnu(self, wavelen=None, flambda=None):
821
+ """
822
+ Convert flambda into fnu.
823
+
824
+ This routine assumes that flambda is in ergs/cm^s/s/nm and
825
+ produces fnu in Jansky.
826
+ Can act on self or user can provide wavelen/flambda and
827
+ get back wavelen/fnu.
828
+ """
829
+ # Change Flamda to Fnu by multiplying Flambda * lambda^2 = Fv
830
+ # Fv dv = Fl dl .. Fv = Fl dl / dv = Fl dl / (dl*c/l/l) = Fl*l*l/c
831
+ # Check - Is the method acting on self.wavelen/flambda/fnu
832
+ # or passed wavelen/flambda arrays?
833
+ update_self = self._check_use_self(wavelen, flambda)
834
+ if update_self:
835
+ wavelen = self.wavelen
836
+ flambda = self.flambda
837
+ self.fnu = None
838
+ # Now on with the calculation.
839
+ # Calculate fnu.
840
+ fnu = flambda * wavelen * wavelen * self._phys_params.nm2m / self._phys_params.lightspeed
841
+ fnu = fnu * self._phys_params.ergsetc2jansky
842
+ # If are using/updating self, then *all* wavelen/flambda/fnu
843
+ # will be gridded.
844
+ # This is so wavelen/fnu AND wavelen/flambda can be kept in sync.
845
+ if update_self:
846
+ self.wavelen = wavelen
847
+ self.flambda = flambda
848
+ self.fnu = fnu
849
+ return
850
+ # Return wavelen, fnu, unless updating self (then does not return).
851
+ return wavelen, fnu
852
+
853
+ def fnu_toflambda(self, wavelen=None, fnu=None):
854
+ """
855
+ Convert fnu into flambda.
856
+
857
+ Assumes fnu in units of Jansky and flambda in ergs/cm^s/s/nm.
858
+ Can act on self or user can give wavelen/fnu and
859
+ get wavelen/flambda returned.
860
+ """
861
+ # Fv dv = Fl dl .. Fv = Fl dl / dv = Fl dl / (dl*c/l/l) = Fl*l*l/c
862
+ # Is method acting on self or passed arrays?
863
+ update_self = self._check_use_self(wavelen, fnu)
864
+ if update_self:
865
+ wavelen = self.wavelen
866
+ fnu = self.fnu
867
+ # On with the calculation.
868
+ # Calculate flambda.
869
+ flambda = fnu / wavelen / wavelen * self._phys_params.lightspeed / self._phys_params.nm2m
870
+ flambda = flambda / self._phys_params.ergsetc2jansky
871
+ # If updating self, then *all of wavelen/fnu/flambda will be updated.
872
+ # This is so wavelen/fnu AND wavelen/flambda can be kept in sync.
873
+ if update_self:
874
+ self.wavelen = wavelen
875
+ self.flambda = flambda
876
+ self.fnu = fnu
877
+ return
878
+ # Return wavelen/flambda.
879
+ return wavelen, flambda
880
+
881
+ # methods to alter the sed
882
+
883
+ def redshift_sed(self, redshift, dimming=False, wavelen=None, flambda=None):
884
+ """
885
+ Redshift an SED, optionally adding cosmological dimming.
886
+
887
+ Pass wavelen/flambda or redshift/update self.wavelen/flambda
888
+ (unsets fnu).
889
+ """
890
+ # Updating self or passed arrays?
891
+ update_self = self._check_use_self(wavelen, flambda)
892
+ if update_self:
893
+ wavelen = self.wavelen
894
+ flambda = self.flambda
895
+ self.fnu = None
896
+ else:
897
+ # Make a copy of input data, because will change its values.
898
+ wavelen = numpy.copy(wavelen)
899
+ flambda = numpy.copy(flambda)
900
+ # Okay, move onto redshifting the wavelen/flambda pair.
901
+ # Or blueshift, as the case may be.
902
+ if redshift < 0:
903
+ wavelen = wavelen / (1.0 - redshift)
904
+ else:
905
+ wavelen = wavelen * (1.0 + redshift)
906
+ # Flambda now just has different wavelength for each value.
907
+ # Add cosmological dimming if required.
908
+ if dimming:
909
+ if redshift < 0:
910
+ flambda = flambda * (1.0 - redshift)
911
+ else:
912
+ flambda = flambda / (1.0 + redshift)
913
+ # Update self, if required - but just flambda (still no grid required).
914
+ if update_self:
915
+ self.wavelen = wavelen
916
+ self.flambda = flambda
917
+ return
918
+ return wavelen, flambda
919
+
920
+ def setup_cc_mab(self, wavelen=None):
921
+ """
922
+ Calculate a(x) and b(x) for CCM dust model. (x=1/wavelen).
923
+
924
+ If wavelen not specified, calculates a and b on the own object's
925
+ wavelength grid.
926
+ Returns a(x) and b(x) can be common to many seds, wavelen is the same.
927
+
928
+ This method sets up extinction due to the model of
929
+ Cardelli, Clayton and Mathis 1989 (ApJ 345, 245)
930
+ """
931
+ warnings.warn(
932
+ "Sed.setup_cc_mab is now deprecated in favor of Sed.setup_ccm_ab",
933
+ DeprecationWarning,
934
+ )
935
+
936
+ return self.setup_ccm_ab(wavelen=wavelen)
937
+
938
+ def setup_ccm_ab(self, wavelen=None):
939
+ """
940
+ Calculate a(x) and b(x) for CCM dust model. (x=1/wavelen).
941
+
942
+ If wavelen not specified, calculates a and b on the own object's
943
+ wavelength grid.
944
+ Returns a(x) and b(x) can be common to many seds, wavelen is the same.
945
+
946
+ This method sets up extinction due to the model of
947
+ Cardelli, Clayton and Mathis 1989 (ApJ 345, 245)
948
+ """
949
+ # This extinction law taken from Cardelli, Clayton and Mathis ApJ 1989.
950
+ # The general form is A_l / A(V) = a(x) + b(x)/R_V
951
+ # (where x=1/lambda in microns),
952
+ # then different values for a(x) and b(x) depending on wavelength.
953
+ # Also, the extinction is parametrized as R_v = a_v / E(B-V).
954
+ # Magnitudes of extinction (A_l) translates to flux by
955
+ # a_l = -2.5log(f_red / f_nonred).
956
+ if wavelen is None:
957
+ wavelen = numpy.copy(self.wavelen)
958
+ a_x = numpy.zeros(len(wavelen), dtype="float")
959
+ b_x = numpy.zeros(len(wavelen), dtype="float")
960
+ # Convert wavelength to x (in inverse microns).
961
+ x = numpy.empty(len(wavelen), dtype=float)
962
+ nm_to_micron = 1 / 1000.0
963
+ x = 1.0 / (wavelen * nm_to_micron)
964
+ # Dust in infrared 0.3 /mu < x < 1.1 /mu (inverse microns).
965
+ condition = (x >= 0.3) & (x <= 1.1)
966
+ if len(a_x[condition]) > 0:
967
+ y = x[condition]
968
+ a_x[condition] = 0.574 * y**1.61
969
+ b_x[condition] = -0.527 * y**1.61
970
+ # Dust in optical/NIR 1.1 /mu < x < 3.3 /mu region.
971
+ condition = (x >= 1.1) & (x <= 3.3)
972
+ if len(a_x[condition]) > 0:
973
+ y = x[condition] - 1.82
974
+ a_x[condition] = 1 + 0.17699 * y - 0.50447 * y**2 - 0.02427 * y**3 + 0.72085 * y**4
975
+ a_x[condition] = a_x[condition] + 0.01979 * y**5 - 0.77530 * y**6 + 0.32999 * y**7
976
+ b_x[condition] = 1.41338 * y + 2.28305 * y**2 + 1.07233 * y**3 - 5.38434 * y**4
977
+ b_x[condition] = b_x[condition] - 0.62251 * y**5 + 5.30260 * y**6 - 2.09002 * y**7
978
+ # Dust in ultraviolet and UV (if needed for high-z) 3.3 /mu< x< 8 /mu.
979
+ condition = (x >= 3.3) & (x < 5.9)
980
+ if len(a_x[condition]) > 0:
981
+ y = x[condition]
982
+ a_x[condition] = 1.752 - 0.316 * y - 0.104 / ((y - 4.67) ** 2 + 0.341)
983
+ b_x[condition] = -3.090 + 1.825 * y + 1.206 / ((y - 4.62) ** 2 + 0.263)
984
+ condition = (x > 5.9) & (x < 8)
985
+ if len(a_x[condition]) > 0:
986
+ y = x[condition]
987
+ fa_x = numpy.empty(len(a_x[condition]), dtype=float)
988
+ fb_x = numpy.empty(len(a_x[condition]), dtype=float)
989
+ fa_x = -0.04473 * (y - 5.9) ** 2 - 0.009779 * (y - 5.9) ** 3
990
+ fb_x = 0.2130 * (y - 5.9) ** 2 + 0.1207 * (y - 5.9) ** 3
991
+ a_x[condition] = 1.752 - 0.316 * y - 0.104 / ((y - 4.67) ** 2 + 0.341) + fa_x
992
+ b_x[condition] = -3.090 + 1.825 * y + 1.206 / ((y - 4.62) ** 2 + 0.263) + fb_x
993
+ # Dust in far UV (if needed for high-z) 8 /mu < x < 10 /mu region.
994
+ condition = (x >= 8) & (x <= 11.0)
995
+ if len(a_x[condition]) > 0:
996
+ y = x[condition] - 8.0
997
+ a_x[condition] = -1.073 - 0.628 * (y) + 0.137 * (y) ** 2 - 0.070 * (y) ** 3
998
+ b_x[condition] = 13.670 + 4.257 * (y) - 0.420 * (y) ** 2 + 0.374 * (y) ** 3
999
+ return a_x, b_x
1000
+
1001
+ def setup_o_donnell_ab(self, wavelen=None):
1002
+ """
1003
+ Calculate a(x) and b(x) for O'Donnell dust model. (x=1/wavelen).
1004
+
1005
+ If wavelen not specified, calculates a and b on the own object's
1006
+ wavelength grid.
1007
+ Returns a(x) and b(x) can be common to many seds, wavelen is the same.
1008
+
1009
+ This method sets up the extinction parameters from the model of
1010
+ O'Donnel 1994 (ApJ 422, 158)
1011
+ """
1012
+ # The general form is A_l / A(V) = a(x) + b(x)/R_V
1013
+ # (where x=1/lambda in microns),
1014
+ # then different values for a(x) and b(x) depending on wavelength.
1015
+ # Also, the extinction is parametrized as R_v = a_v / E(B-V).
1016
+ # Magnitudes of extinction (A_l) translates to flux by
1017
+ # a_l = -2.5log(f_red / f_nonred).
1018
+ if wavelen is None:
1019
+ wavelen = numpy.copy(self.wavelen)
1020
+ a_x = numpy.zeros(len(wavelen), dtype="float")
1021
+ b_x = numpy.zeros(len(wavelen), dtype="float")
1022
+ # Convert wavelength to x (in inverse microns).
1023
+ x = numpy.empty(len(wavelen), dtype=float)
1024
+ nm_to_micron = 1 / 1000.0
1025
+ x = 1.0 / (wavelen * nm_to_micron)
1026
+ # Dust in infrared 0.3 /mu < x < 1.1 /mu (inverse microns).
1027
+ condition = (x >= 0.3) & (x <= 1.1)
1028
+ if len(a_x[condition]) > 0:
1029
+ y = x[condition]
1030
+ a_x[condition] = 0.574 * y**1.61
1031
+ b_x[condition] = -0.527 * y**1.61
1032
+ # Dust in optical/NIR 1.1 /mu < x < 3.3 /mu region.
1033
+ condition = (x >= 1.1) & (x <= 3.3)
1034
+ if len(a_x[condition]) > 0:
1035
+ y = x[condition] - 1.82
1036
+ a_x[condition] = 1 + 0.104 * y - 0.609 * y**2 + 0.701 * y**3 + 1.137 * y**4
1037
+ a_x[condition] = a_x[condition] - 1.718 * y**5 - 0.827 * y**6 + 1.647 * y**7 - 0.505 * y**8
1038
+ b_x[condition] = 1.952 * y + 2.908 * y**2 - 3.989 * y**3 - 7.985 * y**4
1039
+ b_x[condition] = b_x[condition] + 11.102 * y**5 + 5.491 * y**6 - 10.805 * y**7 + 3.347 * y**8
1040
+ # Dust in ultraviolet and UV (if needed for high-z) 3.3 /mu< x< 8 /mu.
1041
+ condition = (x >= 3.3) & (x < 5.9)
1042
+ if len(a_x[condition]) > 0:
1043
+ y = x[condition]
1044
+ a_x[condition] = 1.752 - 0.316 * y - 0.104 / ((y - 4.67) ** 2 + 0.341)
1045
+ b_x[condition] = -3.090 + 1.825 * y + 1.206 / ((y - 4.62) ** 2 + 0.263)
1046
+ condition = (x > 5.9) & (x < 8)
1047
+ if len(a_x[condition]) > 0:
1048
+ y = x[condition]
1049
+ fa_x = numpy.empty(len(a_x[condition]), dtype=float)
1050
+ fb_x = numpy.empty(len(a_x[condition]), dtype=float)
1051
+ fa_x = -0.04473 * (y - 5.9) ** 2 - 0.009779 * (y - 5.9) ** 3
1052
+ fb_x = 0.2130 * (y - 5.9) ** 2 + 0.1207 * (y - 5.9) ** 3
1053
+ a_x[condition] = 1.752 - 0.316 * y - 0.104 / ((y - 4.67) ** 2 + 0.341) + fa_x
1054
+ b_x[condition] = -3.090 + 1.825 * y + 1.206 / ((y - 4.62) ** 2 + 0.263) + fb_x
1055
+ # Dust in far UV (if needed for high-z) 8 /mu < x < 10 /mu region.
1056
+ condition = (x >= 8) & (x <= 11.0)
1057
+ if len(a_x[condition]) > 0:
1058
+ y = x[condition] - 8.0
1059
+ a_x[condition] = -1.073 - 0.628 * (y) + 0.137 * (y) ** 2 - 0.070 * (y) ** 3
1060
+ b_x[condition] = 13.670 + 4.257 * (y) - 0.420 * (y) ** 2 + 0.374 * (y) ** 3
1061
+ return a_x, b_x
1062
+
1063
+ def add_ccm_dust(self, a_x, b_x, a_v=None, ebv=None, r_v=3.1, wavelen=None, flambda=None):
1064
+ """
1065
+ Add dust model extinction to the SED, modifying flambda and fnu.
1066
+
1067
+ Get a_x and b_x either from setupCCMab or setupODonnell_ab
1068
+
1069
+ Specify any two of A_V, E(B-V) or R_V (=3.1 default).
1070
+ """
1071
+ warnings.warn(
1072
+ "Sed.add_ccm_dust is now deprecated in favor of Sed.add_dust",
1073
+ DeprecationWarning,
1074
+ )
1075
+ return self.add_dust(a_x, b_x, a_v=a_v, ebv=ebv, r_v=r_v, wavelen=wavelen, flambda=flambda)
1076
+
1077
+ def add_dust(self, a_x, b_x, a_v=None, ebv=None, r_v=3.1, wavelen=None, flambda=None):
1078
+ """
1079
+ Add dust model extinction to the SED, modifying flambda and fnu.
1080
+
1081
+ Get a_x and b_x either from setupCCMab or setupODonnell_ab
1082
+
1083
+ Specify any two of A_V, E(B-V) or R_V (=3.1 default).
1084
+ """
1085
+ if not hasattr(self, "_ln10_04"):
1086
+ self._ln10_04 = 0.4 * numpy.log(10.0)
1087
+
1088
+ # The extinction law taken from Cardelli, Clayton and Mathis ApJ 1989.
1089
+ # The general form is A_l / A(V) = a(x) + b(x)/R_V
1090
+ # (where x=1/lambda in microns).
1091
+ # Then, different values for a(x) and b(x) depending on wavelength
1092
+ # regime.
1093
+ # Also, the extinction is parametrized as r_v = a_v / E(B-V).
1094
+ # The magnitudes of extinction (A_l) translates to flux by
1095
+ # a_l = -2.5log(f_red / f_nonred).
1096
+ #
1097
+ # Figure out if updating self or passed arrays.
1098
+ update_self = self._check_use_self(wavelen, flambda)
1099
+ if update_self:
1100
+ wavelen = self.wavelen
1101
+ flambda = self.flambda
1102
+ self.fnu = None
1103
+ else:
1104
+ wavelen = numpy.copy(wavelen)
1105
+ flambda = numpy.copy(flambda)
1106
+ # Input parameters for reddening can include any of 3 parameters;
1107
+ # only 2 are independent.
1108
+ # Figure out what parameters were given, and see if self-consistent.
1109
+ if r_v == 3.1:
1110
+ if a_v is None:
1111
+ a_v = r_v * ebv
1112
+ elif (a_v is not None) and (ebv is not None):
1113
+ # Specified a_v and ebv, so r_v should be nondefault.
1114
+ r_v = a_v / ebv
1115
+ if r_v != 3.1:
1116
+ if (a_v is not None) and (ebv is not None):
1117
+ calc_rv = a_v / ebv
1118
+ if calc_rv != r_v:
1119
+ raise ValueError(
1120
+ "CCM parametrization expects r_v = a_v / E(B-V);",
1121
+ "Please check input values, because values are inconsistent.",
1122
+ )
1123
+ elif a_v is None:
1124
+ a_v = r_v * ebv
1125
+ # r_v and a_v values are specified or calculated.
1126
+
1127
+ a_lambda = (a_x + b_x / r_v) * a_v
1128
+ # dmag_red(dust) = -2.5 log10 (f_red / f_nored) :
1129
+ # (f_red / f_nored) = 10**-0.4*dmag_red
1130
+ dust = numpy.exp(-a_lambda * self._ln10_04)
1131
+ flambda *= dust
1132
+ # Update self if required.
1133
+ if update_self:
1134
+ self.flambda = flambda
1135
+ return
1136
+ return wavelen, flambda
1137
+
1138
+ def multiply_sed(self, other_sed, wavelen_step=None):
1139
+ """
1140
+ Multiply two SEDs together - flambda * flambda -
1141
+ and return a new sed object.
1142
+
1143
+ Unless the two wavelength arrays are equal, returns a SED
1144
+ gridded with stepsize wavelen_step
1145
+ over intersecting wavelength region. Does not alter self or other_sed.
1146
+ """
1147
+
1148
+ if wavelen_step is None:
1149
+ wavelen_step = self._phys_params.wavelenstep
1150
+
1151
+ # Check if the wavelength arrays are equal
1152
+ # (in which case do not resample)
1153
+ if numpy.all(self.wavelen == other_sed.wavelen):
1154
+ flambda = self.flambda * other_sed.flambda
1155
+ new_sed = Sed(self.wavelen, flambda=flambda)
1156
+ else:
1157
+ # Find overlapping wavelength region.
1158
+ wavelen_max = min(self.wavelen.max(), other_sed.wavelen.max())
1159
+ wavelen_min = max(self.wavelen.min(), other_sed.wavelen.min())
1160
+ if wavelen_max < wavelen_min:
1161
+ raise Exception("The two SEDS do not overlap in wavelength space.")
1162
+ # Set up wavelen/flambda of first object, on grid.
1163
+ wavelen_1, flambda_1 = self.resample_sed(
1164
+ self.wavelen,
1165
+ self.flambda,
1166
+ wavelen_min=wavelen_min,
1167
+ wavelen_max=wavelen_max,
1168
+ wavelen_step=wavelen_step,
1169
+ )
1170
+ # Set up wavelen/flambda of second object, on grid.
1171
+ wavelen_2, flambda_2 = self.resample_sed(
1172
+ wavelen=other_sed.wavelen,
1173
+ flux=other_sed.flambda,
1174
+ wavelen_min=wavelen_min,
1175
+ wavelen_max=wavelen_max,
1176
+ wavelen_step=wavelen_step,
1177
+ )
1178
+ # Multiply the two flambda together.
1179
+ flambda = flambda_1 * flambda_2
1180
+ # Instantiate new sed object.
1181
+ # wavelen_1 == wavelen_2 as both are on grid.
1182
+ new_sed = Sed(wavelen_1, flambda)
1183
+ return new_sed
1184
+
1185
+ # routines related to magnitudes and fluxes
1186
+
1187
+ def calc_adu(self, bandpass, phot_params, wavelen=None, fnu=None):
1188
+ """
1189
+ Calculate the number of adu from camera, using sb and fnu.
1190
+
1191
+ Given wavelen/fnu arrays or use self.
1192
+ Self or passed wavelen/fnu arrays will be unchanged.
1193
+ Calculating the AB mag requires the wavelen/fnu pair to be
1194
+ on the same grid as bandpass;
1195
+ (temporary values of these are used).
1196
+
1197
+ Parameters
1198
+ ----------
1199
+ bandpass : `rubin_sim.phot_utils.Bandpass`
1200
+ phot_params : `rubin_sim.phot_utils.PhotometricParameters`
1201
+ wavelen : `np.ndarray`, optional
1202
+ wavelength grid in nm
1203
+ fnu : `np.ndarray`, optional
1204
+ flux in Janskys
1205
+
1206
+ If wavelen and fnu are not specified, this will just use self.wavelen
1207
+ and self.fnu
1208
+ """
1209
+
1210
+ use_self = self._check_use_self(wavelen, fnu)
1211
+ # Use self values if desired, otherwise use values passed to function.
1212
+ if use_self:
1213
+ # Calculate fnu if required.
1214
+ if self.fnu is None:
1215
+ # If fnu not present, calculate. (does not regrid).
1216
+ self.flambda_tofnu()
1217
+ wavelen = self.wavelen
1218
+ fnu = self.fnu
1219
+ # Make sure wavelen/fnu are on the same wavelength grid as bandpass.
1220
+ wavelen, fnu = self.resample_sed(wavelen, fnu, wavelen_match=bandpass.wavelen)
1221
+ # Calculate the number of photons.
1222
+ dlambda = wavelen[1] - wavelen[0]
1223
+ # Nphoton in units of 10^-23 ergs/cm^s/nm.
1224
+ nphoton = (fnu / wavelen * bandpass.sb).sum()
1225
+ adu = (
1226
+ nphoton
1227
+ * (phot_params.exptime * phot_params.nexp * phot_params.effarea / phot_params.gain)
1228
+ * (1 / self._phys_params.ergsetc2jansky)
1229
+ * (1 / self._phys_params.planck)
1230
+ * dlambda
1231
+ )
1232
+ return adu
1233
+
1234
+ def flux_from_mag(self, mag):
1235
+ """
1236
+ Convert a magnitude back into a flux (implies knowledge of the
1237
+ zeropoint, which is stored in this class)
1238
+ """
1239
+
1240
+ return numpy.power(10.0, -0.4 * (mag + self.zp))
1241
+
1242
+ def mag_from_flux(self, flux):
1243
+ """
1244
+ Convert a flux into a magnitude (implies knowledge of the
1245
+ zeropoint, which is stored in this class)
1246
+ """
1247
+
1248
+ return -2.5 * numpy.log10(flux) - self.zp
1249
+
1250
+ def calc_ergs(self, bandpass, fill=numpy.nan):
1251
+ r"""
1252
+ Integrate the SED over a bandpass directly. If self.flambda
1253
+ is in ergs/s/cm^2/nm and bandpass.sb is the unitless probability
1254
+ that a photon of a given wavelength will pass through the system,
1255
+ this method will return the ergs/s/cm^2 of the source observed
1256
+ through that bandpass (i.e. it will return the integral
1257
+
1258
+ \int self.flambda(lambda) * bandpass.sb(lambda) * dlambda
1259
+
1260
+ This is to be contrasted with self.calc_flux(), which returns
1261
+ the integral of the source's specific flux density over the
1262
+ normalized response function of bandpass, giving a flux in
1263
+ Janskys (10^-23 erg/cm^2/s/Hz), which should be though of as
1264
+ a weighted average of the specific flux density of the source
1265
+ over the normalized response function, as detailed in Section
1266
+ 4.1 of the LSST design document LSE-180.
1267
+
1268
+ Parameters
1269
+ ----------
1270
+ bandpass is an instantiation of the Bandpass class
1271
+
1272
+ Returns
1273
+ -------
1274
+ The flux of the current SED through the bandpass in ergs/s/cm^2
1275
+ """
1276
+ wavelen, flambda = self.resample_sed(
1277
+ wavelen=self.wavelen, flux=self.flambda, wavelen_match=bandpass.wavelen, fill=fill
1278
+ )
1279
+
1280
+ dlambda = wavelen[1] - wavelen[0]
1281
+
1282
+ # use the trapezoid rule
1283
+ energy = (0.5 * (flambda[1:] * bandpass.sb[1:] + flambda[:-1] * bandpass.sb[:-1]) * dlambda).sum()
1284
+ return energy
1285
+
1286
+ def calc_flux(self, bandpass, wavelen=None, fnu=None, fill=numpy.nan):
1287
+ """
1288
+ Integrate the specific flux density of the object over the normalized
1289
+ response curve of a bandpass, giving a flux in Janskys
1290
+ (10^-23 ergs/s/cm^2/Hz) through the normalized response curve, as
1291
+ detailed in Section 4.1 of the LSST design document LSE-180 and
1292
+ Section 2.6 of the LSST Science Book
1293
+ (http://ww.lsst.org/scientists/scibook).
1294
+ This flux in Janskys (which is usually thought of as a unit of
1295
+ specific flux density), should be considered a weighted average of
1296
+ the specific flux density over the normalized response curve of the
1297
+ bandpass. Because we are using the normalized response curve
1298
+ (phi in LSE-180), this quantity will depend only on the shape of the
1299
+ response curve, not its absolute normalization.
1300
+
1301
+ Note: the way that the normalized response curve has been defined
1302
+ (see equation 5 of LSE-180) is appropriate for photon-counting
1303
+ detectors, not calorimeters.
1304
+
1305
+ Passed wavelen/fnu arrays will be unchanged, but if uses self will
1306
+ check if fnu is set.
1307
+
1308
+ Calculating the AB mag requires the wavelen/fnu pair to be on the
1309
+ same grid as bandpass;
1310
+ (temporary values of these are used).
1311
+ """
1312
+ use_self = self._check_use_self(wavelen, fnu)
1313
+ # Use self values if desired, otherwise use values passed to function.
1314
+ if use_self:
1315
+ # Calculate fnu if required.
1316
+ if self.fnu is None:
1317
+ self.flambda_tofnu()
1318
+ wavelen = self.wavelen
1319
+ fnu = self.fnu
1320
+ # Go on with magnitude calculation.
1321
+ wavelen, fnu = self.resample_sed(wavelen, fnu, wavelen_match=bandpass.wavelen, fill=fill)
1322
+ # Calculate bandpass phi value if required.
1323
+ if bandpass.phi is None:
1324
+ bandpass.sb_tophi()
1325
+ # Calculate flux in bandpass and return this value.
1326
+ flux = trapezoid(fnu * bandpass.phi, x=wavelen)
1327
+ return flux
1328
+
1329
+ def calc_mag(self, bandpass, wavelen=None, fnu=None, fill=numpy.nan):
1330
+ """
1331
+ Calculate the AB magnitude of an object using the normalized system
1332
+ response (phi from Section 4.1 of the LSST design document LSE-180).
1333
+
1334
+ Can pass wavelen/fnu arrays or use self. Self or passed wavelen/fnu
1335
+ arrays will be unchanged. Calculating the AB mag requires the
1336
+ wavelen/fnu pair to be on the same grid as bandpass;
1337
+ (but only temporary values of these are used).
1338
+ """
1339
+ flux = self.calc_flux(bandpass, wavelen=wavelen, fnu=fnu, fill=fill)
1340
+ if flux < 1e-300:
1341
+ raise ValueError("This SED has no flux within this bandpass.")
1342
+ mag = self.mag_from_flux(flux)
1343
+ return mag
1344
+
1345
+ def calc_flux_norm(self, magmatch, bandpass, wavelen=None, fnu=None, fill=numpy.nan):
1346
+ """
1347
+ Calculate the fluxNorm (SED normalization value for a given mag)
1348
+ for a sed.
1349
+
1350
+ Equivalent to adjusting a particular f_nu to Jansky's appropriate
1351
+ for the desired mag. Can pass wavelen/fnu or apply to self.
1352
+ """
1353
+ use_self = self._check_use_self(wavelen, fnu)
1354
+ if use_self:
1355
+ # Check possibility that fnu is not calculated yet.
1356
+ if self.fnu is None:
1357
+ self.flambda_tofnu()
1358
+ wavelen = self.wavelen
1359
+ fnu = self.fnu
1360
+ # Fluxnorm gets applied to f_nu
1361
+ # (fluxnorm * SED(f_nu) * PHI = mag - 8.9 (AB zeropoint).
1362
+ # FluxNorm * SED => correct magnitudes for this object.
1363
+ # Calculate fluxnorm.
1364
+ curmag = self.calc_mag(bandpass, wavelen, fnu, fill=fill)
1365
+ if curmag == self.badval:
1366
+ return self.badval
1367
+ dmag = magmatch - curmag
1368
+ fluxnorm = numpy.power(10, (-0.4 * dmag))
1369
+ return fluxnorm
1370
+
1371
+ def multiply_flux_norm(self, flux_norm, wavelen=None, fnu=None):
1372
+ """
1373
+ Multiply wavelen/fnu (or self.wavelen/fnu) by fluxnorm.
1374
+
1375
+ Returns wavelen/fnu arrays (or updates self).
1376
+ Note that multiply_flux_norm does not regrid self.wavelen/flambda/fnu
1377
+ at all.
1378
+ """
1379
+ # Note that flux_norm is intended to be applied to f_nu,
1380
+ # so that fluxnorm*fnu*phi = mag (expected magnitude).
1381
+ update_self = self._check_use_self(wavelen, fnu)
1382
+ if update_self:
1383
+ # Make sure fnu is defined.
1384
+ if self.fnu is None:
1385
+ self.flambda_tofnu()
1386
+ wavelen = self.wavelen
1387
+ fnu = self.fnu
1388
+ else:
1389
+ # Require new copy of the data for multiply.
1390
+ wavelen = numpy.copy(wavelen)
1391
+ fnu = numpy.copy(fnu)
1392
+ # Apply fluxnorm.
1393
+ fnu = fnu * flux_norm
1394
+ # Update self.
1395
+ if update_self:
1396
+ self.wavelen = wavelen
1397
+ self.fnu = fnu
1398
+ # Update flambda as well.
1399
+ self.fnu_toflambda()
1400
+ return
1401
+ # Else return new wavelen/fnu pairs.
1402
+ return wavelen, fnu
1403
+
1404
+ def renormalize_sed(
1405
+ self,
1406
+ wavelen=None,
1407
+ flambda=None,
1408
+ fnu=None,
1409
+ lambdanorm=500,
1410
+ normvalue=1,
1411
+ gap=0,
1412
+ normflux="flambda",
1413
+ wavelen_step=None,
1414
+ ):
1415
+ """
1416
+ Renormalize sed in flambda to have normflux=normvalue @ lambdanorm
1417
+ averaged over gap.
1418
+
1419
+ Can normalized in flambda or fnu values. wavelen_step specifies
1420
+ the wavelength spacing when using 'gap'.
1421
+
1422
+ Either returns wavelen/flambda values or updates self.
1423
+ """
1424
+ # Normalizes the fnu/flambda SED at one wavelength or average value
1425
+ # over small range (gap).
1426
+ # This is useful for generating SED catalogs, mostly, to make them
1427
+ # match schema.
1428
+ # Do not use this for calculating specific magnitudes -- use
1429
+ # calcfluxNorm and multiply_flux_norm.
1430
+ # Start normalizing wavelen/flambda.
1431
+
1432
+ if wavelen_step is None:
1433
+ wavelen_step = self._phys_params.wavelenstep
1434
+
1435
+ if normflux == "flambda":
1436
+ update_self = self._check_use_self(wavelen, flambda)
1437
+ if update_self:
1438
+ wavelen = self.wavelen
1439
+ flambda = self.flambda
1440
+ else:
1441
+ # Make a copy of the input data.
1442
+ wavelen = numpy.copy(wavelen)
1443
+ # Look for either flambda or fnu in input data.
1444
+ if flambda is None:
1445
+ if fnu is None:
1446
+ raise Exception("If passing wavelength, must also pass fnu or flambda.")
1447
+ # If not given flambda, must calculate from fnu.
1448
+ wavelen, flambda = self.fnu_toflambda(wavelen, fnu)
1449
+ # Make a copy of the input data.
1450
+ else:
1451
+ flambda = numpy.copy(flambda)
1452
+ # Calculate renormalization values.
1453
+ # Check that flambda is defined at the wavelength want to use for
1454
+ # renormalization.
1455
+ if (lambdanorm > wavelen.max()) or (lambdanorm < wavelen.min()):
1456
+ raise Exception(
1457
+ "Desired wavelength for renormalization, %f, " % (lambdanorm)
1458
+ + "is outside defined wavelength range."
1459
+ )
1460
+ # "standard" schema have flambda = 1 at 500 nm.
1461
+ if gap == 0:
1462
+ flambda_atpt = numpy.interp(lambdanorm, wavelen, flambda, left=None, right=None)
1463
+ gapval = flambda_atpt
1464
+ else:
1465
+ lambdapt = numpy.arange(lambdanorm - gap, lambdanorm + gap, wavelen_step, dtype=float)
1466
+ flambda_atpt = numpy.zeros(len(lambdapt), dtype="float")
1467
+ flambda_atpt = numpy.interp(lambdapt, wavelen, flambda, left=None, right=None)
1468
+ gapval = flambda_atpt.sum() / len(lambdapt)
1469
+ # Now renormalize fnu and flambda, in the case of normalizing
1470
+ # flambda.
1471
+ if gapval == 0:
1472
+ raise Exception(
1473
+ "Original flambda is 0 at the desired point of normalization. " "Cannot renormalize."
1474
+ )
1475
+ konst = normvalue / gapval
1476
+ flambda = flambda * konst
1477
+ wavelen, fnu = self.flambda_tofnu(wavelen, flambda)
1478
+ elif normflux == "fnu":
1479
+ update_self = self._check_use_self(wavelen, fnu)
1480
+ if update_self:
1481
+ wavelen = self.wavelen
1482
+ if self.fnu is None:
1483
+ self.flambda_tofnu()
1484
+ fnu = self.fnu
1485
+ else:
1486
+ # Make a copy of the input data.
1487
+ wavelen = numpy.copy(wavelen)
1488
+ # Look for either flambda or fnu in input data.
1489
+ if fnu is None:
1490
+ if flambda is None:
1491
+ raise Exception("If passing wavelength, must also pass fnu or flambda.")
1492
+ wavelen, fnu = self.flambda_tofnu(wavelen, fnu)
1493
+ # Make a copy of the input data.
1494
+ else:
1495
+ fnu = numpy.copy(fnu)
1496
+ # Calculate renormalization values.
1497
+ # Check that flambda is defined at the wavelength want to use
1498
+ # for renormalization.
1499
+ if (lambdanorm > wavelen.max()) or (lambdanorm < wavelen.min()):
1500
+ raise Exception(
1501
+ "Desired wavelength for renormalization, %f, " % (lambdanorm)
1502
+ + "is outside defined wavelength range."
1503
+ )
1504
+ if gap == 0:
1505
+ fnu_atpt = numpy.interp(lambdanorm, wavelen, flambda, left=None, right=None)
1506
+ gapval = fnu_atpt
1507
+ else:
1508
+ lambdapt = numpy.arange(lambdanorm - gap, lambdanorm + gap, wavelen_step, dtype=float)
1509
+ fnu_atpt = numpy.zeros(len(lambdapt), dtype="float")
1510
+ fnu_atpt = numpy.interp(lambdapt, wavelen, fnu, left=None, right=None)
1511
+ gapval = fnu_atpt.sum() / len(lambdapt)
1512
+ # Now renormalize fnu and flambda in the case of normalizing fnu.
1513
+ if gapval == 0:
1514
+ raise Exception(
1515
+ "Original fnu is 0 at the desired point of normalization. " "Cannot renormalize."
1516
+ )
1517
+ konst = normvalue / gapval
1518
+ fnu = fnu * konst
1519
+ wavelen, flambda = self.fnutoflambda(wavelen, fnu)
1520
+ if update_self:
1521
+ self.wavelen = wavelen
1522
+ self.flambda = flambda
1523
+ self.fnu = fnu
1524
+ return
1525
+ new_sed = Sed(wavelen=wavelen, flambda=flambda)
1526
+ return new_sed
1527
+
1528
+ def write_sed(
1529
+ self,
1530
+ filename,
1531
+ print_header=None,
1532
+ print_fnu=False,
1533
+ wavelen_min=None,
1534
+ wavelen_max=None,
1535
+ wavelen_step=None,
1536
+ ):
1537
+ """
1538
+ Write SED (wavelen, flambda, optional fnu) out to file.
1539
+
1540
+ Option of adding a header line (such as version info) to output file.
1541
+ Does not alter self, regardless of grid or presence/absence of fnu.
1542
+ """
1543
+ # This can be useful for debugging or recording an SED.
1544
+ f = open(filename, "w")
1545
+ wavelen = self.wavelen
1546
+ flambda = self.flambda
1547
+ wavelen, flambda = self.resample_sed(
1548
+ wavelen,
1549
+ flambda,
1550
+ wavelen_min=wavelen_min,
1551
+ wavelen_max=wavelen_max,
1552
+ wavelen_step=wavelen_step,
1553
+ )
1554
+ # Then just use this gridded wavelen/flambda to calculate fnu.
1555
+ # Print header.
1556
+ if print_header is not None:
1557
+ if not print_header.startswith("#"):
1558
+ print_header = "# " + print_header
1559
+ f.write(print_header)
1560
+ # Print standard header info.
1561
+ if print_fnu:
1562
+ wavelen, fnu = self.flambda_tofnu(wavelen, flambda)
1563
+ print("# Wavelength(nm) Flambda(ergs/cm^s/s/nm) Fnu(Jansky)", file=f)
1564
+ else:
1565
+ print("# Wavelength(nm) Flambda(ergs/cm^s/s/nm)", file=f)
1566
+ for i in range(0, len(wavelen), 1):
1567
+ if print_fnu:
1568
+ fnu = self.flambda_tofnu(wavelen=wavelen, flambda=flambda)
1569
+ print(wavelen[i], flambda[i], fnu[i], file=f)
1570
+ else:
1571
+ print("%.2f %.7g" % (wavelen[i], flambda[i]), file=f)
1572
+ # Done writing, close file.
1573
+ f.close()
1574
+ return
1575
+
1576
+ # Bonus, functions for many-magnitude calculation for many SEDs with
1577
+ # a single bandpass
1578
+
1579
+ def setup_phi_array(self, bandpasslist):
1580
+ """
1581
+ Sets up a 2-d numpy phi array from bandpasslist suitable for input
1582
+ to Sed's many_mag_calc.
1583
+
1584
+ This is intended to be used once, most likely before using Sed's
1585
+ many_mag_calc many times on many SEDs.
1586
+ Returns 2-d phi array and the wavelen_step (dlambda) appropriate for
1587
+ that array.
1588
+ """
1589
+ # Calculate dlambda for phi array.
1590
+ wavelen_step = bandpasslist[0].wavelen[1] - bandpasslist[0].wavelen[0]
1591
+ wavelen_min = numpy.min([bandpass.wavelen[0] for bandpass in bandpasslist])
1592
+ wavelen_max = numpy.max([bandpass.wavelen[-1] for bandpass in bandpasslist])
1593
+ # Set up
1594
+ phiarray = numpy.empty(
1595
+ (
1596
+ len(bandpasslist),
1597
+ numpy.size(numpy.arange(wavelen_min, wavelen_max + wavelen_step, wavelen_step)),
1598
+ ),
1599
+ dtype="float",
1600
+ )
1601
+ # Check phis calculated and on same wavelength grid.
1602
+ i = 0
1603
+ for bp in bandpasslist:
1604
+ # Be sure bandpasses on same grid and calculate phi.
1605
+ bp.resample_bandpass(
1606
+ wavelen_min=wavelen_min,
1607
+ wavelen_max=wavelen_max,
1608
+ wavelen_step=wavelen_step,
1609
+ )
1610
+ bp.sb_tophi()
1611
+ phiarray[i] = bp.phi
1612
+ i = i + 1
1613
+ return phiarray, wavelen_step
1614
+
1615
+ def many_flux_calc(self, phiarray, wavelen_step, observed_bandpass_ind=None):
1616
+ """
1617
+ Calculate fluxes of a single sed for which fnu has been evaluated in a
1618
+ set of bandpasses for which phiarray has been set up to have the same
1619
+ wavelength grid as the SED in units of ergs/cm^2/sec. It is assumed
1620
+ that `self.fnu` is set before calling this method, and that phiArray
1621
+ has the same wavelength grid as the Sed.
1622
+
1623
+
1624
+ Parameters
1625
+ ----------
1626
+ phiarray : `np.ndarray`
1627
+ phiarray corresponding to the list of bandpasses in which the band
1628
+ fluxes need to be calculated, in the same wavelength grid as Sed
1629
+ wavelen_step : `float`
1630
+ the uniform grid size of the SED
1631
+ observed_bandpass_ind : `list` [`int`], optional
1632
+ list of indices of phiarray corresponding to observed bandpasses,
1633
+ if None, the original phiarray is returned
1634
+
1635
+ Returns
1636
+ -------
1637
+ `np.ndarray` with size equal to number of bandpass filters band flux
1638
+ values in units of ergs/cm^2/sec
1639
+
1640
+ .. note: Sed.many_flux_calc `assumes` phiArray has the same wavelength
1641
+ grid as the Sed and that `sed.fnu` has been calculated for the sed,
1642
+ perhaps using `sed.flambda_tofnu()`. This requires calling
1643
+ `sed.setupPhiArray()` first. These assumptions are to avoid error
1644
+ checking within this function (for speed), but could lead to errors if
1645
+ method is used incorrectly.
1646
+
1647
+ Note on units: Fluxes calculated this way will be the flux density
1648
+ integrated over the weighted response curve of the bandpass.
1649
+ See equaiton 2.1 of the LSST Science Book
1650
+
1651
+ http://www.lsst.org/scientists/scibook
1652
+ """
1653
+
1654
+ if observed_bandpass_ind is not None:
1655
+ phiarray = phiarray[observed_bandpass_ind]
1656
+ flux = numpy.sum(phiarray * self.fnu, axis=1) * wavelen_step
1657
+ return flux
1658
+
1659
+ def many_mag_calc(self, phiarray, wavelen_step, observed_bandpass_ind=None):
1660
+ """
1661
+ Calculate many magnitudes for many bandpasses using a single sed.
1662
+
1663
+ This method assumes that there will be flux within a particular
1664
+ bandpass
1665
+ (could return '-Inf' for a magnitude if there is none).
1666
+ Use setupPhiArray first, and note that Sed.many_mag_calc *assumes*
1667
+ phiArray has the same wavelength grid as the Sed, and that fnu has
1668
+ already been calculated for Sed.
1669
+ These assumptions are to avoid error checking within this function
1670
+ (for speed), but could lead to errors if method is used incorrectly.
1671
+
1672
+ Parameters
1673
+ ----------
1674
+ phiarray : `np.ndarray`, mandatory
1675
+ phiarray corresponding to the list of bandpasses in which the band
1676
+ fluxes need to be calculated, in the same wavelength grid as SED
1677
+ wavelen_step : `float`, mandatory
1678
+ the uniform grid size of the SED
1679
+ observed_bandpass_ind : `list` [`int`], optional
1680
+ list of indices of phiarray corresponding to observed bandpasses,
1681
+ if None, the original phiarray is returned
1682
+
1683
+ """
1684
+ fluxes = self.many_flux_calc(phiarray, wavelen_step, observed_bandpass_ind)
1685
+ mags = -2.5 * numpy.log10(fluxes) - self.zp
1686
+ return mags
1687
+
1688
+
1689
+ def read_close__kurucz(teff, fe_h, logg):
1690
+ """
1691
+ Check the cached Kurucz models and load the model closest to the
1692
+ input stellar parameters.
1693
+ Parameters are matched in order of Teff, fe_h, and logg.
1694
+
1695
+ Parameters
1696
+ ----------
1697
+ teff : `float`
1698
+ Effective temperature of the stellar template.
1699
+ Reasonable range is 3830-11,100 K.
1700
+ fe_h : `float`
1701
+ Metallicity [Fe/H] of stellar template. Values in range -5 to 1.
1702
+ logg : `float`
1703
+ Log of the surface gravity for the stellar template.
1704
+ Values in range 0. to 50.
1705
+
1706
+ Returns
1707
+ -------
1708
+ sed : `rubin_sim.phot_utils.Sed`
1709
+ The SED of the closest matching stellar template
1710
+ paramDict : `dict`
1711
+ Dictionary of the teff, fe_h, logg that were actually loaded
1712
+
1713
+ """
1714
+ global _global_lsst_sed_cache
1715
+
1716
+ # Load the cache if it hasn't been done
1717
+ if _global_lsst_sed_cache is None:
1718
+ cache_lsst_seds()
1719
+ # Build an array with all the files in the cache
1720
+ if not hasattr(read_close__kurucz, "param_combos"):
1721
+ kurucz_files = [
1722
+ filename
1723
+ for filename in _global_lsst_sed_cache
1724
+ if ("kurucz" in filename) & ("_g" in os.path.basename(filename))
1725
+ ]
1726
+ kurucz_files = list(set(kurucz_files))
1727
+ read_close__kurucz.param_combos = numpy.zeros(
1728
+ len(kurucz_files),
1729
+ dtype=[
1730
+ ("filename", ("|U200")),
1731
+ ("teff", float),
1732
+ ("fe_h", float),
1733
+ ("logg", float),
1734
+ ],
1735
+ )
1736
+ for i, filename in enumerate(kurucz_files):
1737
+ read_close__kurucz.param_combos["filename"][i] = filename
1738
+ filename = os.path.basename(filename)
1739
+ if filename[1] == "m":
1740
+ sign = -1
1741
+ else:
1742
+ sign = 1
1743
+ logz = sign * float(filename.split("_")[0][2:]) / 10.0
1744
+ read_close__kurucz.param_combos["fe_h"][i] = logz
1745
+ logg_temp = float(filename.split("g")[1].split("_")[0])
1746
+ read_close__kurucz.param_combos["logg"][i] = logg_temp
1747
+ teff_temp = float(filename.split("_")[-1].split(".")[0])
1748
+ read_close__kurucz.param_combos["teff"][i] = teff_temp
1749
+ read_close__kurucz.param_combos = numpy.sort(
1750
+ read_close__kurucz.param_combos, order=["teff", "fe_h", "logg"]
1751
+ )
1752
+
1753
+ # Lookup the closest match. Prob a faster way to do this.
1754
+ teff_diff = numpy.abs(read_close__kurucz.param_combos["teff"] - teff)
1755
+ g1 = numpy.where(teff_diff == teff_diff.min())[0]
1756
+ fe_h_diff = numpy.abs(read_close__kurucz.param_combos["fe_h"][g1] - fe_h)
1757
+ g2 = numpy.where(fe_h_diff == fe_h_diff.min())[0]
1758
+ logg_diff = numpy.abs(read_close__kurucz.param_combos["logg"][g1][g2] - logg)
1759
+ g3 = numpy.where(logg_diff == logg_diff.min())[0]
1760
+ file_match = read_close__kurucz.param_combos["filename"][g1][g2][g3]
1761
+ if numpy.size(file_match > 1):
1762
+ warnings.warn("Multiple close files")
1763
+ file_match = file_match[0]
1764
+
1765
+ # Record what Parameters were actually loaded
1766
+ teff = read_close__kurucz.param_combos["teff"][g1][g2][g3][0]
1767
+ fe_h = read_close__kurucz.param_combos["fe_h"][g1][g2][g3][0]
1768
+ logg = read_close__kurucz.param_combos["logg"][g1][g2][g3][0]
1769
+
1770
+ # Read in the matching file
1771
+ sed = Sed()
1772
+ sed.read_sed_flambda(file_match)
1773
+ return sed, {"teff": teff, "fe_h": fe_h, "logg": logg}