gammapbh 1.1.3__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.
Potentially problematic release.
This version of gammapbh might be problematic. Click here for more details.
- gammapbh/__init__.py +42 -0
- gammapbh/__main__.py +29 -0
- gammapbh/blackhawk_data/1.0e+14/1.0e+14.txt +59 -0
- gammapbh/blackhawk_data/1.0e+14/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/1.0e+14/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/1.0e+14/final_state_radiation_prim.txt +238 -0
- gammapbh/blackhawk_data/1.0e+14/final_state_radiation_sec.txt +169 -0
- gammapbh/blackhawk_data/1.0e+14/inflight_annihilation_prim.txt +238 -0
- gammapbh/blackhawk_data/1.0e+14/inflight_annihilation_sec.txt +169 -0
- gammapbh/blackhawk_data/1.0e+14/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.0e+14/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/1.0e+14/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.0e+15/1e+15.txt +59 -0
- gammapbh/blackhawk_data/1.0e+15/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/1.0e+15/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/1.0e+15/final_state_radiation_prim.txt +193 -0
- gammapbh/blackhawk_data/1.0e+15/final_state_radiation_sec.txt +193 -0
- gammapbh/blackhawk_data/1.0e+15/inflight_annihilation_prim.txt +193 -0
- gammapbh/blackhawk_data/1.0e+15/inflight_annihilation_sec.txt +193 -0
- gammapbh/blackhawk_data/1.0e+15/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.0e+15/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/1.0e+15/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.0e+16/1e+16.txt +59 -0
- gammapbh/blackhawk_data/1.0e+16/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/1.0e+16/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/1.0e+16/final_state_radiation_prim.txt +147 -0
- gammapbh/blackhawk_data/1.0e+16/final_state_radiation_sec.txt +147 -0
- gammapbh/blackhawk_data/1.0e+16/inflight_annihilation_prim.txt +147 -0
- gammapbh/blackhawk_data/1.0e+16/inflight_annihilation_sec.txt +147 -0
- gammapbh/blackhawk_data/1.0e+16/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.0e+16/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/1.0e+16/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.0e+17/1.0e+17.txt +59 -0
- gammapbh/blackhawk_data/1.0e+17/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/1.0e+17/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/1.0e+17/final_state_radiation_prim.txt +102 -0
- gammapbh/blackhawk_data/1.0e+17/final_state_radiation_sec.txt +101 -0
- gammapbh/blackhawk_data/1.0e+17/inflight_annihilation_prim.txt +102 -0
- gammapbh/blackhawk_data/1.0e+17/inflight_annihilation_sec.txt +101 -0
- gammapbh/blackhawk_data/1.0e+17/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.0e+17/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/1.0e+17/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.0e+18/1.0e+18.txt +59 -0
- gammapbh/blackhawk_data/1.0e+18/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/1.0e+18/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/1.0e+18/final_state_radiation_prim.txt +57 -0
- gammapbh/blackhawk_data/1.0e+18/final_state_radiation_sec.txt +56 -0
- gammapbh/blackhawk_data/1.0e+18/inflight_annihilation_prim.txt +57 -0
- gammapbh/blackhawk_data/1.0e+18/inflight_annihilation_sec.txt +56 -0
- gammapbh/blackhawk_data/1.0e+18/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.0e+18/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/1.0e+18/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.0e+19/1e+19.txt +59 -0
- gammapbh/blackhawk_data/1.0e+19/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/1.0e+19/final_state_radiation_prim.txt +11 -0
- gammapbh/blackhawk_data/1.0e+19/final_state_radiation_sec.txt +12 -0
- gammapbh/blackhawk_data/1.0e+19/inflight_annihilation_prim.txt +11 -0
- gammapbh/blackhawk_data/1.0e+19/inflight_annihilation_sec.txt +12 -0
- gammapbh/blackhawk_data/1.0e+19/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.0e+19/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/1.0e+19/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.5e+14/1.5e+14.txt +59 -0
- gammapbh/blackhawk_data/1.5e+14/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/1.5e+14/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/1.5e+14/final_state_radiation_prim.txt +230 -0
- gammapbh/blackhawk_data/1.5e+14/final_state_radiation_sec.txt +149 -0
- gammapbh/blackhawk_data/1.5e+14/inflight_annihilation_prim.txt +230 -0
- gammapbh/blackhawk_data/1.5e+14/inflight_annihilation_sec.txt +149 -0
- gammapbh/blackhawk_data/1.5e+14/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.5e+14/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/1.5e+14/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.5e+15/1.5e+15.txt +59 -0
- gammapbh/blackhawk_data/1.5e+15/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/1.5e+15/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/1.5e+15/final_state_radiation_prim.txt +185 -0
- gammapbh/blackhawk_data/1.5e+15/final_state_radiation_sec.txt +185 -0
- gammapbh/blackhawk_data/1.5e+15/inflight_annihilation_prim.txt +185 -0
- gammapbh/blackhawk_data/1.5e+15/inflight_annihilation_sec.txt +185 -0
- gammapbh/blackhawk_data/1.5e+15/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.5e+15/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/1.5e+15/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.5e+16/1.5e+16.txt +59 -0
- gammapbh/blackhawk_data/1.5e+16/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/1.5e+16/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/1.5e+16/final_state_radiation_prim.txt +139 -0
- gammapbh/blackhawk_data/1.5e+16/final_state_radiation_sec.txt +138 -0
- gammapbh/blackhawk_data/1.5e+16/inflight_annihilation_prim.txt +139 -0
- gammapbh/blackhawk_data/1.5e+16/inflight_annihilation_sec.txt +138 -0
- gammapbh/blackhawk_data/1.5e+16/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.5e+16/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/1.5e+16/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.5e+17/1.5e+17.txt +59 -0
- gammapbh/blackhawk_data/1.5e+17/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/1.5e+17/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/1.5e+17/final_state_radiation_prim.txt +94 -0
- gammapbh/blackhawk_data/1.5e+17/final_state_radiation_sec.txt +93 -0
- gammapbh/blackhawk_data/1.5e+17/inflight_annihilation_prim.txt +94 -0
- gammapbh/blackhawk_data/1.5e+17/inflight_annihilation_sec.txt +93 -0
- gammapbh/blackhawk_data/1.5e+17/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.5e+17/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/1.5e+17/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.5e+18/1.5e+18.txt +59 -0
- gammapbh/blackhawk_data/1.5e+18/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/1.5e+18/final_state_radiation_prim.txt +49 -0
- gammapbh/blackhawk_data/1.5e+18/final_state_radiation_sec.txt +49 -0
- gammapbh/blackhawk_data/1.5e+18/inflight_annihilation_prim.txt +49 -0
- gammapbh/blackhawk_data/1.5e+18/inflight_annihilation_sec.txt +49 -0
- gammapbh/blackhawk_data/1.5e+18/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/1.5e+18/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/1.5e+18/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/2.0e+14/2.0e+14.txt +59 -0
- gammapbh/blackhawk_data/2.0e+14/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/2.0e+14/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/2.0e+14/final_state_radiation_prim.txt +224 -0
- gammapbh/blackhawk_data/2.0e+14/final_state_radiation_sec.txt +143 -0
- gammapbh/blackhawk_data/2.0e+14/inflight_annihilation_prim.txt +224 -0
- gammapbh/blackhawk_data/2.0e+14/inflight_annihilation_sec.txt +143 -0
- gammapbh/blackhawk_data/2.0e+14/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/2.0e+14/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/2.0e+14/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/2.0e+15/2e+15.txt +59 -0
- gammapbh/blackhawk_data/2.0e+15/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/2.0e+15/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/2.0e+15/final_state_radiation_prim.txt +179 -0
- gammapbh/blackhawk_data/2.0e+15/final_state_radiation_sec.txt +178 -0
- gammapbh/blackhawk_data/2.0e+15/inflight_annihilation_prim.txt +179 -0
- gammapbh/blackhawk_data/2.0e+15/inflight_annihilation_sec.txt +178 -0
- gammapbh/blackhawk_data/2.0e+15/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/2.0e+15/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/2.0e+15/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/2.0e+16/2.0e+16.txt +59 -0
- gammapbh/blackhawk_data/2.0e+16/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/2.0e+16/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/2.0e+16/downloaded_components/direct_hawking.txt +501 -0
- gammapbh/blackhawk_data/2.0e+16/downloaded_components/final_state_radiation.txt +501 -0
- gammapbh/blackhawk_data/2.0e+16/downloaded_components/inflight_annihilation.txt +501 -0
- gammapbh/blackhawk_data/2.0e+16/downloaded_components/total_spectrum.txt +501 -0
- gammapbh/blackhawk_data/2.0e+16/final_state_radiation_prim.txt +134 -0
- gammapbh/blackhawk_data/2.0e+16/final_state_radiation_sec.txt +133 -0
- gammapbh/blackhawk_data/2.0e+16/inflight_annihilation_prim.txt +134 -0
- gammapbh/blackhawk_data/2.0e+16/inflight_annihilation_sec.txt +133 -0
- gammapbh/blackhawk_data/2.0e+16/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/2.0e+16/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/2.0e+16/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/2.0e+17/2.0e+17.txt +59 -0
- gammapbh/blackhawk_data/2.0e+17/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/2.0e+17/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/2.0e+17/final_state_radiation_prim.txt +88 -0
- gammapbh/blackhawk_data/2.0e+17/final_state_radiation_sec.txt +87 -0
- gammapbh/blackhawk_data/2.0e+17/inflight_annihilation_prim.txt +88 -0
- gammapbh/blackhawk_data/2.0e+17/inflight_annihilation_sec.txt +87 -0
- gammapbh/blackhawk_data/2.0e+17/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/2.0e+17/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/2.0e+17/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/2.0e+18/2.0e+18.txt +59 -0
- gammapbh/blackhawk_data/2.0e+18/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/2.0e+18/final_state_radiation_prim.txt +43 -0
- gammapbh/blackhawk_data/2.0e+18/final_state_radiation_sec.txt +44 -0
- gammapbh/blackhawk_data/2.0e+18/inflight_annihilation_prim.txt +43 -0
- gammapbh/blackhawk_data/2.0e+18/inflight_annihilation_sec.txt +44 -0
- gammapbh/blackhawk_data/2.0e+18/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/2.0e+18/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/2.0e+18/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/3.0e+14/3.0e+14.txt +59 -0
- gammapbh/blackhawk_data/3.0e+14/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/3.0e+14/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/3.0e+14/final_state_radiation_prim.txt +216 -0
- gammapbh/blackhawk_data/3.0e+14/final_state_radiation_sec.txt +134 -0
- gammapbh/blackhawk_data/3.0e+14/inflight_annihilation_prim.txt +216 -0
- gammapbh/blackhawk_data/3.0e+14/inflight_annihilation_sec.txt +134 -0
- gammapbh/blackhawk_data/3.0e+14/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/3.0e+14/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/3.0e+14/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/3.0e+15/3.0e+15.txt +59 -0
- gammapbh/blackhawk_data/3.0e+15/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/3.0e+15/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/3.0e+15/final_state_radiation_prim.txt +171 -0
- gammapbh/blackhawk_data/3.0e+15/final_state_radiation_sec.txt +171 -0
- gammapbh/blackhawk_data/3.0e+15/inflight_annihilation_prim.txt +171 -0
- gammapbh/blackhawk_data/3.0e+15/inflight_annihilation_sec.txt +171 -0
- gammapbh/blackhawk_data/3.0e+15/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/3.0e+15/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/3.0e+15/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/3.0e+15/list5.txt +172 -0
- gammapbh/blackhawk_data/3.0e+16/3.0e+16.txt +59 -0
- gammapbh/blackhawk_data/3.0e+16/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/3.0e+16/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/3.0e+16/final_state_radiation_prim.txt +126 -0
- gammapbh/blackhawk_data/3.0e+16/final_state_radiation_sec.txt +125 -0
- gammapbh/blackhawk_data/3.0e+16/inflight_annihilation_prim.txt +126 -0
- gammapbh/blackhawk_data/3.0e+16/inflight_annihilation_sec.txt +125 -0
- gammapbh/blackhawk_data/3.0e+16/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/3.0e+16/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/3.0e+16/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/3.0e+17/3.0e+17.txt +59 -0
- gammapbh/blackhawk_data/3.0e+17/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/3.0e+17/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/3.0e+17/final_state_radiation_prim.txt +80 -0
- gammapbh/blackhawk_data/3.0e+17/final_state_radiation_sec.txt +80 -0
- gammapbh/blackhawk_data/3.0e+17/inflight_annihilation_prim.txt +80 -0
- gammapbh/blackhawk_data/3.0e+17/inflight_annihilation_sec.txt +80 -0
- gammapbh/blackhawk_data/3.0e+17/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/3.0e+17/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/3.0e+17/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/3.0e+18/3.0e+18.txt +59 -0
- gammapbh/blackhawk_data/3.0e+18/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/3.0e+18/final_state_radiation_prim.txt +35 -0
- gammapbh/blackhawk_data/3.0e+18/final_state_radiation_sec.txt +35 -0
- gammapbh/blackhawk_data/3.0e+18/inflight_annihilation_prim.txt +35 -0
- gammapbh/blackhawk_data/3.0e+18/inflight_annihilation_sec.txt +35 -0
- gammapbh/blackhawk_data/3.0e+18/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/3.0e+18/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/3.0e+18/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/4.0e+14/4.0e+14.txt +59 -0
- gammapbh/blackhawk_data/4.0e+14/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/4.0e+14/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/4.0e+14/final_state_radiation_prim.txt +211 -0
- gammapbh/blackhawk_data/4.0e+14/final_state_radiation_sec.txt +123 -0
- gammapbh/blackhawk_data/4.0e+14/inflight_annihilation_prim.txt +211 -0
- gammapbh/blackhawk_data/4.0e+14/inflight_annihilation_sec.txt +123 -0
- gammapbh/blackhawk_data/4.0e+14/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/4.0e+14/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/4.0e+14/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/4.0e+15/4e+15.txt +59 -0
- gammapbh/blackhawk_data/4.0e+15/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/4.0e+15/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/4.0e+15/final_state_radiation_prim.txt +165 -0
- gammapbh/blackhawk_data/4.0e+15/final_state_radiation_sec.txt +165 -0
- gammapbh/blackhawk_data/4.0e+15/inflight_annihilation_prim.txt +165 -0
- gammapbh/blackhawk_data/4.0e+15/inflight_annihilation_sec.txt +165 -0
- gammapbh/blackhawk_data/4.0e+15/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/4.0e+15/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/4.0e+15/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/4.0e+16/4.0e+16.txt +59 -0
- gammapbh/blackhawk_data/4.0e+16/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/4.0e+16/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/4.0e+16/final_state_radiation_prim.txt +120 -0
- gammapbh/blackhawk_data/4.0e+16/final_state_radiation_sec.txt +120 -0
- gammapbh/blackhawk_data/4.0e+16/inflight_annihilation_prim.txt +120 -0
- gammapbh/blackhawk_data/4.0e+16/inflight_annihilation_sec.txt +120 -0
- gammapbh/blackhawk_data/4.0e+16/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/4.0e+16/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/4.0e+16/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/4.0e+17/4.0e+17.txt +59 -0
- gammapbh/blackhawk_data/4.0e+17/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/4.0e+17/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/4.0e+17/final_state_radiation_prim.txt +75 -0
- gammapbh/blackhawk_data/4.0e+17/final_state_radiation_sec.txt +75 -0
- gammapbh/blackhawk_data/4.0e+17/inflight_annihilation_prim.txt +75 -0
- gammapbh/blackhawk_data/4.0e+17/inflight_annihilation_sec.txt +75 -0
- gammapbh/blackhawk_data/4.0e+17/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/4.0e+17/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/4.0e+17/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/4.0e+18/4.0e+18.txt +59 -0
- gammapbh/blackhawk_data/4.0e+18/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/4.0e+18/final_state_radiation_prim.txt +29 -0
- gammapbh/blackhawk_data/4.0e+18/final_state_radiation_sec.txt +30 -0
- gammapbh/blackhawk_data/4.0e+18/inflight_annihilation_prim.txt +29 -0
- gammapbh/blackhawk_data/4.0e+18/inflight_annihilation_sec.txt +30 -0
- gammapbh/blackhawk_data/4.0e+18/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/4.0e+18/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/4.0e+18/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/5.0e+13/5.0e+13.txt +59 -0
- gammapbh/blackhawk_data/5.0e+13/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/5.0e+13/final_state_radiation_prim.txt +252 -0
- gammapbh/blackhawk_data/5.0e+13/final_state_radiation_sec.txt +179 -0
- gammapbh/blackhawk_data/5.0e+13/inflight_annihilation_prim.txt +252 -0
- gammapbh/blackhawk_data/5.0e+13/inflight_annihilation_sec.txt +179 -0
- gammapbh/blackhawk_data/5.0e+13/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/5.0e+13/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/5.0e+13/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/5.0e+14/5.0e+14.txt +59 -0
- gammapbh/blackhawk_data/5.0e+14/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/5.0e+14/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/5.0e+14/final_state_radiation_prim.txt +206 -0
- gammapbh/blackhawk_data/5.0e+14/final_state_radiation_sec.txt +119 -0
- gammapbh/blackhawk_data/5.0e+14/inflight_annihilation_prim.txt +206 -0
- gammapbh/blackhawk_data/5.0e+14/inflight_annihilation_sec.txt +119 -0
- gammapbh/blackhawk_data/5.0e+14/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/5.0e+14/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/5.0e+14/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/5.0e+15/5e+15.txt +59 -0
- gammapbh/blackhawk_data/5.0e+15/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/5.0e+15/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/5.0e+15/final_state_radiation_prim.txt +161 -0
- gammapbh/blackhawk_data/5.0e+15/final_state_radiation_sec.txt +160 -0
- gammapbh/blackhawk_data/5.0e+15/inflight_annihilation_prim.txt +161 -0
- gammapbh/blackhawk_data/5.0e+15/inflight_annihilation_sec.txt +160 -0
- gammapbh/blackhawk_data/5.0e+15/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/5.0e+15/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/5.0e+15/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/5.0e+16/5.0e+16.txt +59 -0
- gammapbh/blackhawk_data/5.0e+16/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/5.0e+16/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/5.0e+16/final_state_radiation_prim.txt +116 -0
- gammapbh/blackhawk_data/5.0e+16/final_state_radiation_sec.txt +115 -0
- gammapbh/blackhawk_data/5.0e+16/inflight_annihilation_prim.txt +116 -0
- gammapbh/blackhawk_data/5.0e+16/inflight_annihilation_sec.txt +115 -0
- gammapbh/blackhawk_data/5.0e+16/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/5.0e+16/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/5.0e+16/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/5.0e+17/5.0e+17.txt +59 -0
- gammapbh/blackhawk_data/5.0e+17/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/5.0e+17/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/5.0e+17/final_state_radiation_prim.txt +70 -0
- gammapbh/blackhawk_data/5.0e+17/final_state_radiation_sec.txt +69 -0
- gammapbh/blackhawk_data/5.0e+17/inflight_annihilation_prim.txt +70 -0
- gammapbh/blackhawk_data/5.0e+17/inflight_annihilation_sec.txt +69 -0
- gammapbh/blackhawk_data/5.0e+17/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/5.0e+17/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/5.0e+17/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/5.0e+18/5.0e+18.txt +59 -0
- gammapbh/blackhawk_data/5.0e+18/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/5.0e+18/final_state_radiation_prim.txt +25 -0
- gammapbh/blackhawk_data/5.0e+18/final_state_radiation_sec.txt +25 -0
- gammapbh/blackhawk_data/5.0e+18/inflight_annihilation_prim.txt +25 -0
- gammapbh/blackhawk_data/5.0e+18/inflight_annihilation_sec.txt +25 -0
- gammapbh/blackhawk_data/5.0e+18/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/5.0e+18/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/5.0e+18/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/6.0e+13/6.0e+13.txt +59 -0
- gammapbh/blackhawk_data/6.0e+13/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/6.0e+13/final_state_radiation_prim.txt +248 -0
- gammapbh/blackhawk_data/6.0e+13/final_state_radiation_sec.txt +166 -0
- gammapbh/blackhawk_data/6.0e+13/inflight_annihilation_prim.txt +248 -0
- gammapbh/blackhawk_data/6.0e+13/inflight_annihilation_sec.txt +166 -0
- gammapbh/blackhawk_data/6.0e+13/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/6.0e+13/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/6.0e+13/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/6.0e+14/6.0e+14.txt +59 -0
- gammapbh/blackhawk_data/6.0e+14/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/6.0e+14/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/6.0e+14/final_state_radiation_prim.txt +203 -0
- gammapbh/blackhawk_data/6.0e+14/final_state_radiation_sec.txt +111 -0
- gammapbh/blackhawk_data/6.0e+14/inflight_annihilation_prim.txt +203 -0
- gammapbh/blackhawk_data/6.0e+14/inflight_annihilation_sec.txt +111 -0
- gammapbh/blackhawk_data/6.0e+14/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/6.0e+14/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/6.0e+14/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/6.0e+15/6e+15.txt +59 -0
- gammapbh/blackhawk_data/6.0e+15/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/6.0e+15/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/6.0e+15/final_state_radiation_prim.txt +157 -0
- gammapbh/blackhawk_data/6.0e+15/final_state_radiation_sec.txt +156 -0
- gammapbh/blackhawk_data/6.0e+15/inflight_annihilation_prim.txt +157 -0
- gammapbh/blackhawk_data/6.0e+15/inflight_annihilation_sec.txt +156 -0
- gammapbh/blackhawk_data/6.0e+15/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/6.0e+15/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/6.0e+15/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/6.0e+16/6.0e+16.txt +59 -0
- gammapbh/blackhawk_data/6.0e+16/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/6.0e+16/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/6.0e+16/final_state_radiation_prim.txt +112 -0
- gammapbh/blackhawk_data/6.0e+16/final_state_radiation_sec.txt +111 -0
- gammapbh/blackhawk_data/6.0e+16/inflight_annihilation_prim.txt +112 -0
- gammapbh/blackhawk_data/6.0e+16/inflight_annihilation_sec.txt +111 -0
- gammapbh/blackhawk_data/6.0e+16/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/6.0e+16/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/6.0e+16/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/6.0e+17/6.0e+17.txt +59 -0
- gammapbh/blackhawk_data/6.0e+17/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/6.0e+17/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/6.0e+17/final_state_radiation_prim.txt +67 -0
- gammapbh/blackhawk_data/6.0e+17/final_state_radiation_sec.txt +67 -0
- gammapbh/blackhawk_data/6.0e+17/inflight_annihilation_prim.txt +67 -0
- gammapbh/blackhawk_data/6.0e+17/inflight_annihilation_sec.txt +67 -0
- gammapbh/blackhawk_data/6.0e+17/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/6.0e+17/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/6.0e+17/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/6.0e+18/6.0e+18.txt +59 -0
- gammapbh/blackhawk_data/6.0e+18/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/6.0e+18/final_state_radiation_prim.txt +21 -0
- gammapbh/blackhawk_data/6.0e+18/final_state_radiation_sec.txt +22 -0
- gammapbh/blackhawk_data/6.0e+18/inflight_annihilation_prim.txt +21 -0
- gammapbh/blackhawk_data/6.0e+18/inflight_annihilation_sec.txt +22 -0
- gammapbh/blackhawk_data/6.0e+18/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/6.0e+18/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/6.0e+18/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/7.0e+13/7.0e+13.txt +59 -0
- gammapbh/blackhawk_data/7.0e+13/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/7.0e+13/final_state_radiation_prim.txt +245 -0
- gammapbh/blackhawk_data/7.0e+13/final_state_radiation_sec.txt +169 -0
- gammapbh/blackhawk_data/7.0e+13/inflight_annihilation_prim.txt +245 -0
- gammapbh/blackhawk_data/7.0e+13/inflight_annihilation_sec.txt +169 -0
- gammapbh/blackhawk_data/7.0e+13/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/7.0e+13/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/7.0e+13/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/7.0e+14/7.0e+14.txt +59 -0
- gammapbh/blackhawk_data/7.0e+14/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/7.0e+14/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/7.0e+14/final_state_radiation_prim.txt +200 -0
- gammapbh/blackhawk_data/7.0e+14/final_state_radiation_sec.txt +113 -0
- gammapbh/blackhawk_data/7.0e+14/inflight_annihilation_prim.txt +200 -0
- gammapbh/blackhawk_data/7.0e+14/inflight_annihilation_sec.txt +113 -0
- gammapbh/blackhawk_data/7.0e+14/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/7.0e+14/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/7.0e+14/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/7.0e+15/7e+15.txt +59 -0
- gammapbh/blackhawk_data/7.0e+15/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/7.0e+15/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/7.0e+15/final_state_radiation_prim.txt +154 -0
- gammapbh/blackhawk_data/7.0e+15/final_state_radiation_sec.txt +153 -0
- gammapbh/blackhawk_data/7.0e+15/inflight_annihilation_prim.txt +154 -0
- gammapbh/blackhawk_data/7.0e+15/inflight_annihilation_sec.txt +153 -0
- gammapbh/blackhawk_data/7.0e+15/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/7.0e+15/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/7.0e+15/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/7.0e+16/7.0e+16.txt +59 -0
- gammapbh/blackhawk_data/7.0e+16/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/7.0e+16/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/7.0e+16/final_state_radiation_prim.txt +109 -0
- gammapbh/blackhawk_data/7.0e+16/final_state_radiation_sec.txt +109 -0
- gammapbh/blackhawk_data/7.0e+16/inflight_annihilation_prim.txt +109 -0
- gammapbh/blackhawk_data/7.0e+16/inflight_annihilation_sec.txt +109 -0
- gammapbh/blackhawk_data/7.0e+16/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/7.0e+16/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/7.0e+16/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/7.0e+17/7.0e+17.txt +59 -0
- gammapbh/blackhawk_data/7.0e+17/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/7.0e+17/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/7.0e+17/final_state_radiation_prim.txt +64 -0
- gammapbh/blackhawk_data/7.0e+17/final_state_radiation_sec.txt +63 -0
- gammapbh/blackhawk_data/7.0e+17/inflight_annihilation_prim.txt +64 -0
- gammapbh/blackhawk_data/7.0e+17/inflight_annihilation_sec.txt +63 -0
- gammapbh/blackhawk_data/7.0e+17/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/7.0e+17/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/7.0e+17/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/7.0e+18/7.0e+18.txt +59 -0
- gammapbh/blackhawk_data/7.0e+18/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/7.0e+18/final_state_radiation_prim.txt +18 -0
- gammapbh/blackhawk_data/7.0e+18/final_state_radiation_sec.txt +19 -0
- gammapbh/blackhawk_data/7.0e+18/inflight_annihilation_prim.txt +18 -0
- gammapbh/blackhawk_data/7.0e+18/inflight_annihilation_sec.txt +19 -0
- gammapbh/blackhawk_data/7.0e+18/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/7.0e+18/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/7.0e+18/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/8.0e+13/8.0e+13.txt +59 -0
- gammapbh/blackhawk_data/8.0e+13/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/8.0e+13/final_state_radiation_prim.txt +242 -0
- gammapbh/blackhawk_data/8.0e+13/final_state_radiation_sec.txt +157 -0
- gammapbh/blackhawk_data/8.0e+13/inflight_annihilation_prim.txt +242 -0
- gammapbh/blackhawk_data/8.0e+13/inflight_annihilation_sec.txt +157 -0
- gammapbh/blackhawk_data/8.0e+13/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/8.0e+13/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/8.0e+13/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/8.0e+14/8.0e+14.txt +59 -0
- gammapbh/blackhawk_data/8.0e+14/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/8.0e+14/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/8.0e+14/final_state_radiation_prim.txt +197 -0
- gammapbh/blackhawk_data/8.0e+14/final_state_radiation_sec.txt +122 -0
- gammapbh/blackhawk_data/8.0e+14/inflight_annihilation_prim.txt +197 -0
- gammapbh/blackhawk_data/8.0e+14/inflight_annihilation_sec.txt +122 -0
- gammapbh/blackhawk_data/8.0e+14/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/8.0e+14/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/8.0e+14/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/8.0e+15/8e+15.txt +59 -0
- gammapbh/blackhawk_data/8.0e+15/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/8.0e+15/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/8.0e+15/final_state_radiation_prim.txt +152 -0
- gammapbh/blackhawk_data/8.0e+15/final_state_radiation_sec.txt +152 -0
- gammapbh/blackhawk_data/8.0e+15/inflight_annihilation_prim.txt +152 -0
- gammapbh/blackhawk_data/8.0e+15/inflight_annihilation_sec.txt +152 -0
- gammapbh/blackhawk_data/8.0e+15/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/8.0e+15/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/8.0e+15/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/8.0e+16/8.0e+16.txt +59 -0
- gammapbh/blackhawk_data/8.0e+16/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/8.0e+16/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/8.0e+16/final_state_radiation_prim.txt +106 -0
- gammapbh/blackhawk_data/8.0e+16/final_state_radiation_sec.txt +106 -0
- gammapbh/blackhawk_data/8.0e+16/inflight_annihilation_prim.txt +106 -0
- gammapbh/blackhawk_data/8.0e+16/inflight_annihilation_sec.txt +106 -0
- gammapbh/blackhawk_data/8.0e+16/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/8.0e+16/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/8.0e+16/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/8.0e+17/8.0e+17.txt +59 -0
- gammapbh/blackhawk_data/8.0e+17/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/8.0e+17/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/8.0e+17/final_state_radiation_prim.txt +61 -0
- gammapbh/blackhawk_data/8.0e+17/final_state_radiation_sec.txt +61 -0
- gammapbh/blackhawk_data/8.0e+17/inflight_annihilation_prim.txt +61 -0
- gammapbh/blackhawk_data/8.0e+17/inflight_annihilation_sec.txt +61 -0
- gammapbh/blackhawk_data/8.0e+17/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/8.0e+17/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/8.0e+17/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/8.0e+18/8.0e+18.txt +59 -0
- gammapbh/blackhawk_data/8.0e+18/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/8.0e+18/final_state_radiation_prim.txt +16 -0
- gammapbh/blackhawk_data/8.0e+18/final_state_radiation_sec.txt +17 -0
- gammapbh/blackhawk_data/8.0e+18/inflight_annihilation_prim.txt +16 -0
- gammapbh/blackhawk_data/8.0e+18/inflight_annihilation_sec.txt +17 -0
- gammapbh/blackhawk_data/8.0e+18/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/8.0e+18/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/8.0e+18/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/9.0e+13/9.0e+13.txt +59 -0
- gammapbh/blackhawk_data/9.0e+13/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/9.0e+13/final_state_radiation_prim.txt +240 -0
- gammapbh/blackhawk_data/9.0e+13/final_state_radiation_sec.txt +156 -0
- gammapbh/blackhawk_data/9.0e+13/inflight_annihilation_prim.txt +240 -0
- gammapbh/blackhawk_data/9.0e+13/inflight_annihilation_sec.txt +156 -0
- gammapbh/blackhawk_data/9.0e+13/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/9.0e+13/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/9.0e+13/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/9.0e+14/9.0e+14.txt +59 -0
- gammapbh/blackhawk_data/9.0e+14/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/9.0e+14/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/9.0e+14/final_state_radiation_prim.txt +195 -0
- gammapbh/blackhawk_data/9.0e+14/final_state_radiation_sec.txt +122 -0
- gammapbh/blackhawk_data/9.0e+14/inflight_annihilation_prim.txt +195 -0
- gammapbh/blackhawk_data/9.0e+14/inflight_annihilation_sec.txt +122 -0
- gammapbh/blackhawk_data/9.0e+14/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/9.0e+14/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/9.0e+14/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/9.0e+15/9e+15.txt +59 -0
- gammapbh/blackhawk_data/9.0e+15/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/9.0e+15/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/9.0e+15/final_state_radiation_prim.txt +149 -0
- gammapbh/blackhawk_data/9.0e+15/final_state_radiation_sec.txt +148 -0
- gammapbh/blackhawk_data/9.0e+15/inflight_annihilation_prim.txt +149 -0
- gammapbh/blackhawk_data/9.0e+15/inflight_annihilation_sec.txt +148 -0
- gammapbh/blackhawk_data/9.0e+15/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/9.0e+15/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/9.0e+15/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/9.0e+16/9.0e+16.txt +59 -0
- gammapbh/blackhawk_data/9.0e+16/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/9.0e+16/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/9.0e+16/final_state_radiation_prim.txt +104 -0
- gammapbh/blackhawk_data/9.0e+16/final_state_radiation_sec.txt +103 -0
- gammapbh/blackhawk_data/9.0e+16/inflight_annihilation_prim.txt +104 -0
- gammapbh/blackhawk_data/9.0e+16/inflight_annihilation_sec.txt +103 -0
- gammapbh/blackhawk_data/9.0e+16/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/9.0e+16/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/9.0e+16/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/9.0e+17/9.0e+17.txt +59 -0
- gammapbh/blackhawk_data/9.0e+17/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/9.0e+17/Probabilities.txt +346 -0
- gammapbh/blackhawk_data/9.0e+17/final_state_radiation_prim.txt +59 -0
- gammapbh/blackhawk_data/9.0e+17/final_state_radiation_sec.txt +59 -0
- gammapbh/blackhawk_data/9.0e+17/inflight_annihilation_prim.txt +59 -0
- gammapbh/blackhawk_data/9.0e+17/inflight_annihilation_sec.txt +59 -0
- gammapbh/blackhawk_data/9.0e+17/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/9.0e+17/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/9.0e+17/instantaneous_total_spectra.txt +502 -0
- gammapbh/blackhawk_data/9.0e+18/9.0e+18.txt +59 -0
- gammapbh/blackhawk_data/9.0e+18/BH_spectrum.txt +4 -0
- gammapbh/blackhawk_data/9.0e+18/final_state_radiation_prim.txt +13 -0
- gammapbh/blackhawk_data/9.0e+18/final_state_radiation_sec.txt +14 -0
- gammapbh/blackhawk_data/9.0e+18/inflight_annihilation_prim.txt +13 -0
- gammapbh/blackhawk_data/9.0e+18/inflight_annihilation_sec.txt +14 -0
- gammapbh/blackhawk_data/9.0e+18/instantaneous_primary_spectra.txt +502 -0
- gammapbh/blackhawk_data/9.0e+18/instantaneous_secondary_spectra.txt +378 -0
- gammapbh/blackhawk_data/9.0e+18/instantaneous_total_spectra.txt +502 -0
- gammapbh/cli.py +2850 -0
- gammapbh/results/custom_equation/1.38e+15_custom_eq/distributed_spectrum.txt +378 -0
- gammapbh/results/custom_equation/1.38e+15_custom_eq/equation.txt +5 -0
- gammapbh/results/custom_equation/1.38e+15_custom_eq/samples_sorted.txt +1001 -0
- gammapbh/results/gaussian/peak_3.00e+15_/342/225/247/320/2230.1_N1000/distributed_spectrum.txt +378 -0
- gammapbh/results/gaussian/peak_3.00e+15_/342/225/247/320/2230.1_N1000/mass_distribution.txt +1001 -0
- gammapbh/results/lognormal/peak_3.00e+15_/342/225/247/320/2230.1_N1000/distributed_spectrum.txt +378 -0
- gammapbh/results/lognormal/peak_3.00e+15_/342/225/247/320/2230.1_N1000/mass_distribution.txt +1001 -0
- gammapbh/results/monochromatic/3.00e+15_spectrum.txt +378 -0
- gammapbh/results/monochromatic/3.50e+15_mono_generated.txt +378 -0
- gammapbh/results/non_gaussian/peak_3.00e+15_/342/225/247/320/223X0.1_N1000/distributed_spectrum.txt +378 -0
- gammapbh/results/non_gaussian/peak_3.00e+15_/342/225/247/320/223X0.1_N1000/mass_distribution.txt +1001 -0
- gammapbh-1.1.3.dist-info/METADATA +238 -0
- gammapbh-1.1.3.dist-info/RECORD +570 -0
- gammapbh-1.1.3.dist-info/WHEEL +5 -0
- gammapbh-1.1.3.dist-info/entry_points.txt +2 -0
- gammapbh-1.1.3.dist-info/licenses/LICENSE.md +1348 -0
- gammapbh-1.1.3.dist-info/top_level.txt +1 -0
gammapbh/cli.py
ADDED
|
@@ -0,0 +1,2850 @@
|
|
|
1
|
+
# src/gammapbh/cli.py
|
|
2
|
+
"""
|
|
3
|
+
GammaPBHPlotter — interactive CLI to analyze and visualize Hawking-radiation
|
|
4
|
+
gamma-ray spectra of primordial black holes (PBHs).
|
|
5
|
+
|
|
6
|
+
This module provides:
|
|
7
|
+
- Monochromatic spectra visualization for selected PBH masses.
|
|
8
|
+
- Distributed spectra from physically motivated mass PDFs:
|
|
9
|
+
- Gaussian collapse (Press–Schechter–like).
|
|
10
|
+
- Non-Gaussian collapse (Biagetti et al. formulation).
|
|
11
|
+
- Log-normal mass function.
|
|
12
|
+
- A custom-equation mass PDF tool that lets users enter f(m) directly.
|
|
13
|
+
- A viewer for previously saved runs (with spectrum overlays and
|
|
14
|
+
per-selection mass histograms, including analytic/KDE overlays).
|
|
15
|
+
|
|
16
|
+
All user-facing plotting is log–log with stable zero-flooring in linear space
|
|
17
|
+
to avoid numerical warnings. Interpolations are performed in (logM, logE) space
|
|
18
|
+
with linear/cubic bivariate splines, and inflight-annihilation tails are
|
|
19
|
+
sanity-trimmed to prevent staircase artifacts in the rightmost bins.
|
|
20
|
+
|
|
21
|
+
Conventions
|
|
22
|
+
-----------
|
|
23
|
+
- Masses are in grams [g].
|
|
24
|
+
- Energies are in MeV.
|
|
25
|
+
- Spectra are per energy [MeV^-1 s^-1].
|
|
26
|
+
- “E² dN/dE” overlays are used for SED-style views.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import sys
|
|
32
|
+
import os
|
|
33
|
+
import re
|
|
34
|
+
import numpy as np
|
|
35
|
+
import matplotlib.pyplot as plt
|
|
36
|
+
from tqdm import tqdm
|
|
37
|
+
from scipy.special import erf
|
|
38
|
+
from scipy.interpolate import RectBivariateSpline
|
|
39
|
+
from scipy.integrate import trapezoid
|
|
40
|
+
from types import SimpleNamespace
|
|
41
|
+
from colorama import Fore, Style
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
# works when invoked as `python -m gammapbh.cli` or via installed entry-point
|
|
45
|
+
from . import __version__ # type: ignore
|
|
46
|
+
except Exception:
|
|
47
|
+
try:
|
|
48
|
+
# works when invoked as a plain script `python path/to/cli.py`
|
|
49
|
+
from gammapbh import __version__ # type: ignore
|
|
50
|
+
except Exception:
|
|
51
|
+
__version__ = "dev"
|
|
52
|
+
|
|
53
|
+
def pause(msg="Press Enter to continue…"):
|
|
54
|
+
# Only pause if running interactively
|
|
55
|
+
if sys.stdin.isatty():
|
|
56
|
+
input(msg)
|
|
57
|
+
|
|
58
|
+
# … then in view_previous_spectra(), keep your `pause()` calls unchanged.
|
|
59
|
+
# Under pytest (non-tty), pause() becomes a no-op and won’t consume feeder in
|
|
60
|
+
|
|
61
|
+
# ---------------------------
|
|
62
|
+
# Matplotlib/NumPy basics
|
|
63
|
+
# ---------------------------
|
|
64
|
+
plt.rcParams.update({'font.size': 12})
|
|
65
|
+
# Suppress harmless warnings when we intentionally clamp underflows to ~0
|
|
66
|
+
np.seterr(divide='ignore', invalid='ignore')
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------
|
|
70
|
+
# Paths (package-internal only)
|
|
71
|
+
# ---------------------------
|
|
72
|
+
def _resolve_data_dir() -> str:
|
|
73
|
+
"""
|
|
74
|
+
Resolve the *package-internal* data directory that contains BlackHawk tables.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
str
|
|
79
|
+
Absolute path to the packaged `blackhawk_data` directory.
|
|
80
|
+
|
|
81
|
+
Notes
|
|
82
|
+
-----
|
|
83
|
+
- We do not permit user-provided paths here; reproducibility requires the
|
|
84
|
+
tables bundled with the installed package to be used.
|
|
85
|
+
"""
|
|
86
|
+
pkg_dir = os.path.dirname(os.path.abspath(__file__))
|
|
87
|
+
return os.path.join(pkg_dir, "blackhawk_data")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _resolve_results_root() -> str:
|
|
91
|
+
"""
|
|
92
|
+
Resolve the *package-internal* results directory used for all outputs.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
str
|
|
97
|
+
Absolute path to the packaged `results` directory.
|
|
98
|
+
|
|
99
|
+
Raises
|
|
100
|
+
------
|
|
101
|
+
RuntimeError
|
|
102
|
+
If the directory cannot be created or written to.
|
|
103
|
+
"""
|
|
104
|
+
pkg_dir = os.path.dirname(os.path.abspath(__file__))
|
|
105
|
+
dest = os.path.join(pkg_dir, "results")
|
|
106
|
+
os.makedirs(dest, exist_ok=True)
|
|
107
|
+
# Writability quick check: create and remove a tiny temp file
|
|
108
|
+
try:
|
|
109
|
+
test = os.path.join(dest, ".writetest.tmp")
|
|
110
|
+
with open(test, "w") as fh:
|
|
111
|
+
fh.write("ok")
|
|
112
|
+
os.remove(test)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
raise RuntimeError(f"Results directory is not writable: {dest}\n{e}")
|
|
115
|
+
return dest
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
DATA_DIR = _resolve_data_dir()
|
|
119
|
+
RESULTS_DIR = _resolve_results_root()
|
|
120
|
+
|
|
121
|
+
MONO_RESULTS_DIR = os.path.join(RESULTS_DIR, "monochromatic")
|
|
122
|
+
CUSTOM_RESULTS_DIR = os.path.join(RESULTS_DIR, "custom_equation")
|
|
123
|
+
GAUSS_RESULTS_DIR = os.path.join(RESULTS_DIR, "gaussian")
|
|
124
|
+
NGAUSS_RESULTS_DIR = os.path.join(RESULTS_DIR, "non_gaussian")
|
|
125
|
+
LOGN_RESULTS_DIR = os.path.join(RESULTS_DIR, "lognormal")
|
|
126
|
+
|
|
127
|
+
for _d in (MONO_RESULTS_DIR, CUSTOM_RESULTS_DIR, GAUSS_RESULTS_DIR, NGAUSS_RESULTS_DIR, LOGN_RESULTS_DIR):
|
|
128
|
+
os.makedirs(_d, exist_ok=True)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------
|
|
132
|
+
# Labels
|
|
133
|
+
# ---------------------------
|
|
134
|
+
GAUSSIAN_METHOD = "Gaussian collapse"
|
|
135
|
+
NON_GAUSSIAN_METHOD = "Non-Gaussian Collapse"
|
|
136
|
+
LOGNORMAL_METHOD = "Log-Normal Distribution"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ---------------------------
|
|
140
|
+
# Required files within each mass folder
|
|
141
|
+
# ---------------------------
|
|
142
|
+
REQUIRED_FILES = [
|
|
143
|
+
"instantaneous_primary_spectra.txt",
|
|
144
|
+
"instantaneous_secondary_spectra.txt",
|
|
145
|
+
"inflight_annihilation_prim.txt",
|
|
146
|
+
"inflight_annihilation_sec.txt",
|
|
147
|
+
"final_state_radiation_prim.txt",
|
|
148
|
+
"final_state_radiation_sec.txt",
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ---------------------------
|
|
153
|
+
# Back navigation support
|
|
154
|
+
# ---------------------------
|
|
155
|
+
class BackRequested(Exception):
|
|
156
|
+
"""Raised when the user enters 'b' or 'back' to return to the prior screen."""
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------------------------
|
|
161
|
+
# Discovery helpers
|
|
162
|
+
# ---------------------------
|
|
163
|
+
def discover_mass_folders(data_dir: str) -> tuple[list[float], list[str]]:
|
|
164
|
+
"""
|
|
165
|
+
Discover valid mass folders within `data_dir` that contain all required files.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
data_dir : str
|
|
170
|
+
Absolute or relative path to the BlackHawk data directory.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
(list[float], list[str])
|
|
175
|
+
A pair (masses, names) sorted by mass, where `masses[i]` corresponds to
|
|
176
|
+
directory name `names[i]`.
|
|
177
|
+
|
|
178
|
+
Notes
|
|
179
|
+
-----
|
|
180
|
+
- Folders are expected to be named as a float mass in grams (e.g., "1.00e+16").
|
|
181
|
+
- Only folders containing the full REQUIRED_FILES set are returned.
|
|
182
|
+
"""
|
|
183
|
+
masses, names = [], []
|
|
184
|
+
try:
|
|
185
|
+
for name in os.listdir(data_dir):
|
|
186
|
+
p = os.path.join(data_dir, name)
|
|
187
|
+
if not os.path.isdir(p):
|
|
188
|
+
continue
|
|
189
|
+
try:
|
|
190
|
+
m = float(name)
|
|
191
|
+
except ValueError:
|
|
192
|
+
continue
|
|
193
|
+
if all(os.path.isfile(os.path.join(p, f)) for f in REQUIRED_FILES):
|
|
194
|
+
masses.append(m); names.append(name)
|
|
195
|
+
except FileNotFoundError:
|
|
196
|
+
return [], []
|
|
197
|
+
if not masses:
|
|
198
|
+
return [], []
|
|
199
|
+
order = np.argsort(masses)
|
|
200
|
+
return [float(masses[i]) for i in order], [names[i] for i in order]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---------------------------
|
|
204
|
+
# CLI + parsing helpers
|
|
205
|
+
# ---------------------------
|
|
206
|
+
def info(msg: str) -> None:
|
|
207
|
+
"""Print an informational (cyan) line."""
|
|
208
|
+
print(Fore.CYAN + "ℹ " + msg + Style.RESET_ALL)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def warn(msg: str) -> None:
|
|
212
|
+
"""Print a warning (yellow) line."""
|
|
213
|
+
print(Fore.YELLOW + "⚠ " + msg + Style.RESET_ALL)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def err(msg: str) -> None:
|
|
217
|
+
"""Print an error (red) line."""
|
|
218
|
+
print(Fore.RED + "✖ " + msg + Style.RESET_ALL)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def user_input(prompt: str, *, allow_back: bool = False, allow_exit: bool = True) -> str:
|
|
222
|
+
"""
|
|
223
|
+
Wrapper for `input()` that also understands navigation commands.
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
prompt : str
|
|
228
|
+
Text to display for input.
|
|
229
|
+
allow_back : bool, optional
|
|
230
|
+
If True, entering 'b' or 'back' raises BackRequested.
|
|
231
|
+
allow_exit : bool, optional
|
|
232
|
+
If True, entering 'q' or 'exit' terminates the program.
|
|
233
|
+
|
|
234
|
+
Returns
|
|
235
|
+
-------
|
|
236
|
+
str
|
|
237
|
+
The raw input provided by the user, stripped of whitespace.
|
|
238
|
+
|
|
239
|
+
Raises
|
|
240
|
+
------
|
|
241
|
+
BackRequested
|
|
242
|
+
If the user requests to go back and `allow_back=True`.
|
|
243
|
+
SystemExit
|
|
244
|
+
If the user requests to exit and `allow_exit=True`.
|
|
245
|
+
"""
|
|
246
|
+
txt = input(prompt).strip()
|
|
247
|
+
low = txt.lower()
|
|
248
|
+
if allow_exit and low in ('exit', 'q'):
|
|
249
|
+
print("Exiting software.")
|
|
250
|
+
sys.exit(0)
|
|
251
|
+
if allow_back and low in ('b', 'back'):
|
|
252
|
+
raise BackRequested()
|
|
253
|
+
return txt
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def list_saved_runs(base_dir: str) -> list[str]:
|
|
257
|
+
"""
|
|
258
|
+
List child directories beneath `base_dir`.
|
|
259
|
+
|
|
260
|
+
Parameters
|
|
261
|
+
----------
|
|
262
|
+
base_dir : str
|
|
263
|
+
Root directory containing saved runs.
|
|
264
|
+
|
|
265
|
+
Returns
|
|
266
|
+
-------
|
|
267
|
+
list[str]
|
|
268
|
+
Sorted child directory names (no files).
|
|
269
|
+
"""
|
|
270
|
+
try:
|
|
271
|
+
return sorted(d for d in os.listdir(base_dir) if os.path.isdir(os.path.join(base_dir, d)))
|
|
272
|
+
except FileNotFoundError:
|
|
273
|
+
return []
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def snap_to_available(mval: float, available: list[float], tol: float = 1e-12) -> float | None:
|
|
277
|
+
"""
|
|
278
|
+
If `mval` is essentially equal (in log space) to one of `available` masses,
|
|
279
|
+
return that available mass. Otherwise return None.
|
|
280
|
+
|
|
281
|
+
Parameters
|
|
282
|
+
----------
|
|
283
|
+
mval : float
|
|
284
|
+
Desired mass value [g].
|
|
285
|
+
available : list[float]
|
|
286
|
+
Pre-rendered masses available.
|
|
287
|
+
tol : float
|
|
288
|
+
Allowed absolute difference in ln-space for a snap.
|
|
289
|
+
|
|
290
|
+
Returns
|
|
291
|
+
-------
|
|
292
|
+
float or None
|
|
293
|
+
"""
|
|
294
|
+
if not available:
|
|
295
|
+
return None
|
|
296
|
+
log_m = np.log(mval)
|
|
297
|
+
log_available = np.log(np.array(available))
|
|
298
|
+
diffs = np.abs(log_available - log_m)
|
|
299
|
+
idx = np.argmin(diffs)
|
|
300
|
+
return available[idx] if diffs[idx] < tol else None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def parse_float_list_verbose(
|
|
304
|
+
s: str,
|
|
305
|
+
*,
|
|
306
|
+
name: str = "value",
|
|
307
|
+
bounds: tuple[float | None, float | None] | None = None,
|
|
308
|
+
allow_empty: bool = False,
|
|
309
|
+
positive_only: bool = False,
|
|
310
|
+
strict_gt: bool = False,
|
|
311
|
+
strict_lt: bool = False,
|
|
312
|
+
) -> list[float]:
|
|
313
|
+
"""
|
|
314
|
+
Parse a comma-separated list of floats with verbose validation.
|
|
315
|
+
|
|
316
|
+
Parameters
|
|
317
|
+
----------
|
|
318
|
+
s : str
|
|
319
|
+
Input string, e.g. "1e15, 2e15".
|
|
320
|
+
name : str
|
|
321
|
+
Friendly name used in warning messages.
|
|
322
|
+
bounds : (float|None, float|None) or None
|
|
323
|
+
Inclusive (lo, hi) bounds if provided.
|
|
324
|
+
allow_empty : bool
|
|
325
|
+
If False and parsing yields nothing, a warning is printed.
|
|
326
|
+
positive_only : bool
|
|
327
|
+
If True, keep only values > 0.
|
|
328
|
+
strict_gt : bool
|
|
329
|
+
If True and bounds[0] not None: enforce v > lo; else v ≥ lo.
|
|
330
|
+
strict_lt : bool
|
|
331
|
+
If True and bounds[1] not None: enforce v < hi; else v ≤ hi.
|
|
332
|
+
|
|
333
|
+
Returns
|
|
334
|
+
-------
|
|
335
|
+
list[float]
|
|
336
|
+
Validated, de-duplicated floats (first occurrence kept).
|
|
337
|
+
"""
|
|
338
|
+
if (s is None or s.strip() == ""):
|
|
339
|
+
if not allow_empty:
|
|
340
|
+
warn(f"No {name}s provided.")
|
|
341
|
+
return []
|
|
342
|
+
vals, seen = [], set()
|
|
343
|
+
lo, hi = (bounds or (None, None))
|
|
344
|
+
for tok in s.split(","):
|
|
345
|
+
t = tok.strip()
|
|
346
|
+
if not t:
|
|
347
|
+
continue
|
|
348
|
+
try:
|
|
349
|
+
v = float(t)
|
|
350
|
+
except Exception:
|
|
351
|
+
warn(f"Skipping token '{t}': {name} is not a valid number.")
|
|
352
|
+
continue
|
|
353
|
+
if positive_only and v <= 0:
|
|
354
|
+
warn(f"Skipping {name} {v:g}: must be > 0.")
|
|
355
|
+
continue
|
|
356
|
+
if lo is not None:
|
|
357
|
+
if (strict_gt and not (v > lo)) or (not strict_gt and not (v >= lo)):
|
|
358
|
+
cmp = ">" if strict_gt else "≥"
|
|
359
|
+
warn(f"Skipping {name} {v:g}: must be {cmp} {lo:g}.")
|
|
360
|
+
continue
|
|
361
|
+
if hi is not None:
|
|
362
|
+
if (strict_lt and not (v < hi)) or (not strict_lt and not (v <= hi)):
|
|
363
|
+
cmp = "<" if strict_lt else "≤"
|
|
364
|
+
warn(f"Skipping {name} {v:g}: must be {cmp} {hi:g}.")
|
|
365
|
+
continue
|
|
366
|
+
if v in seen:
|
|
367
|
+
warn(f"Duplicate {name} {v:g}: keeping first, skipping this one.")
|
|
368
|
+
continue
|
|
369
|
+
vals.append(v); seen.add(v)
|
|
370
|
+
if not vals and not allow_empty:
|
|
371
|
+
warn(f"No usable {name}s parsed.")
|
|
372
|
+
return vals
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# ---------------------------
|
|
376
|
+
# PDFs (collapse space)
|
|
377
|
+
# ---------------------------
|
|
378
|
+
def delta_l(mass_ratio: np.ndarray, kappa: float, delta_c: float, gamma: float) -> np.ndarray:
|
|
379
|
+
"""
|
|
380
|
+
Convert mass ratio to the linear threshold δ_l used in collapse models.
|
|
381
|
+
|
|
382
|
+
Parameters
|
|
383
|
+
----------
|
|
384
|
+
mass_ratio : ndarray
|
|
385
|
+
Dimensionless M/M_peak (or equivalent model-specific scaling).
|
|
386
|
+
kappa : float
|
|
387
|
+
delta_c : float
|
|
388
|
+
gamma : float
|
|
389
|
+
|
|
390
|
+
Returns
|
|
391
|
+
-------
|
|
392
|
+
ndarray
|
|
393
|
+
δ_l(mass_ratio) with the analytic mapping and a safe clip for the sqrt argument.
|
|
394
|
+
"""
|
|
395
|
+
y = (mass_ratio / kappa)**(1.0 / gamma)
|
|
396
|
+
arg = 64 - 96 * (delta_c + y)
|
|
397
|
+
arg = np.clip(arg, 0.0, None)
|
|
398
|
+
return (8 - np.sqrt(arg)) / 6
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def mass_function(delta_l_val: np.ndarray, sigma_x: float, delta_c: float, gamma: float) -> np.ndarray:
|
|
402
|
+
"""
|
|
403
|
+
Gaussian-collapse proxy mass function in δ_l-space.
|
|
404
|
+
|
|
405
|
+
Parameters
|
|
406
|
+
----------
|
|
407
|
+
delta_l_val : ndarray
|
|
408
|
+
δ_l grid.
|
|
409
|
+
sigma_x : float
|
|
410
|
+
Collapse dispersion parameter.
|
|
411
|
+
delta_c : float
|
|
412
|
+
Critical collapse threshold.
|
|
413
|
+
gamma : float
|
|
414
|
+
Shape parameter.
|
|
415
|
+
|
|
416
|
+
Returns
|
|
417
|
+
-------
|
|
418
|
+
ndarray
|
|
419
|
+
Unnormalized mass function (shape same as input).
|
|
420
|
+
"""
|
|
421
|
+
term1 = 1.0 / (np.sqrt(2 * np.pi) * sigma_x)
|
|
422
|
+
term2 = np.exp(-delta_l_val**2 / (2 * sigma_x**2))
|
|
423
|
+
term3 = delta_l_val - (3/8) * delta_l_val**2 - delta_c
|
|
424
|
+
term4 = gamma * np.abs(1 - (3/4) * delta_l_val)
|
|
425
|
+
return term1 * term2 * term3 / term4
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def mass_function_exact(
|
|
429
|
+
delta_l_val: np.ndarray,
|
|
430
|
+
sigma_X: float,
|
|
431
|
+
sigma_Y: float,
|
|
432
|
+
delta_c: float,
|
|
433
|
+
gamma: float
|
|
434
|
+
) -> np.ndarray:
|
|
435
|
+
"""
|
|
436
|
+
Non-Gaussian mass function (Biagetti et al., Eq. 20 shape—up to constants),
|
|
437
|
+
mapped into δ_l with a Jacobian consistent with the collapse mapping used above.
|
|
438
|
+
|
|
439
|
+
Parameters
|
|
440
|
+
----------
|
|
441
|
+
delta_l_val : ndarray
|
|
442
|
+
δ_l grid.
|
|
443
|
+
sigma_X : float
|
|
444
|
+
Dispersion along X-direction.
|
|
445
|
+
sigma_Y : float
|
|
446
|
+
Dispersion along Y-direction (often tied to sigma_X via ratio).
|
|
447
|
+
delta_c : float
|
|
448
|
+
Critical threshold.
|
|
449
|
+
gamma : float
|
|
450
|
+
Shape parameter for the mapping.
|
|
451
|
+
|
|
452
|
+
Returns
|
|
453
|
+
-------
|
|
454
|
+
ndarray
|
|
455
|
+
Unnormalized mass function (shape same as input).
|
|
456
|
+
"""
|
|
457
|
+
A = sigma_X**2 + (sigma_Y * delta_l_val)**2
|
|
458
|
+
exp_pref = np.exp(-1.0 / (2.0 * sigma_Y**2))
|
|
459
|
+
term1 = 2.0 * sigma_Y * np.sqrt(A)
|
|
460
|
+
inner_exp = np.exp(sigma_X**2 / (2.0 * sigma_Y**2 * (sigma_X**2 + 2.0 * (sigma_Y * delta_l_val)**2)))
|
|
461
|
+
erf_arg = sigma_X * np.sqrt(2.0) / np.sqrt(A) # stable
|
|
462
|
+
term2 = np.sqrt(2.0 * np.pi) * sigma_X * inner_exp * erf(erf_arg)
|
|
463
|
+
bracket = term1 + term2
|
|
464
|
+
norm = exp_pref * sigma_X / (2.0 * np.pi * A**1.5)
|
|
465
|
+
jacobian = ((delta_l_val - 0.375 * delta_l_val**2 - delta_c) /
|
|
466
|
+
(gamma * np.abs(1.0 - 0.75 * delta_l_val)))
|
|
467
|
+
return norm * bracket * jacobian
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def mass_function_lognormal(x: np.ndarray, mu: float, sigma: float) -> np.ndarray:
|
|
471
|
+
"""
|
|
472
|
+
Standard log-normal PDF in variable x.
|
|
473
|
+
|
|
474
|
+
Parameters
|
|
475
|
+
----------
|
|
476
|
+
x : ndarray
|
|
477
|
+
Positive support (will be clipped below to avoid divide-by-zero).
|
|
478
|
+
mu : float
|
|
479
|
+
Mean in ln-space.
|
|
480
|
+
sigma : float
|
|
481
|
+
Std. dev. in ln-space (must be > 0).
|
|
482
|
+
|
|
483
|
+
Returns
|
|
484
|
+
-------
|
|
485
|
+
ndarray
|
|
486
|
+
Log-normal PDF values at x.
|
|
487
|
+
"""
|
|
488
|
+
x_clipped = np.clip(x, 1e-16, None)
|
|
489
|
+
return (1.0 / (x_clipped * sigma * np.sqrt(2 * np.pi))
|
|
490
|
+
* np.exp(- (np.log(x_clipped) - mu)**2 / (2 * sigma**2)))
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
# ---------------------------
|
|
494
|
+
# Data loaders
|
|
495
|
+
# ---------------------------
|
|
496
|
+
def load_data(filepath: str, skip_header: int = 0) -> np.ndarray:
|
|
497
|
+
"""
|
|
498
|
+
A thin wrapper around `numpy.genfromtxt` with explicit header skipping.
|
|
499
|
+
|
|
500
|
+
Parameters
|
|
501
|
+
----------
|
|
502
|
+
filepath : str
|
|
503
|
+
Path to file.
|
|
504
|
+
skip_header : int
|
|
505
|
+
Number of header lines to skip.
|
|
506
|
+
|
|
507
|
+
Returns
|
|
508
|
+
-------
|
|
509
|
+
ndarray
|
|
510
|
+
Parsed numeric array.
|
|
511
|
+
|
|
512
|
+
Raises
|
|
513
|
+
------
|
|
514
|
+
FileNotFoundError
|
|
515
|
+
If file does not exist.
|
|
516
|
+
ValueError
|
|
517
|
+
If `genfromtxt` fails due to column inconsistency.
|
|
518
|
+
"""
|
|
519
|
+
if not os.path.isfile(filepath):
|
|
520
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
521
|
+
return np.genfromtxt(filepath, skip_header=skip_header)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def load_xy_lenient(filepath: str, skip_header: int = 0, min_cols: int = 2) -> np.ndarray:
|
|
525
|
+
"""
|
|
526
|
+
Robustly load at least two numeric columns from a whitespace/CSV-like text file,
|
|
527
|
+
skipping blank lines, comment lines, and any lines with fewer than `min_cols` tokens.
|
|
528
|
+
|
|
529
|
+
This specifically fixes files where the first data row contains a single integer
|
|
530
|
+
(e.g., a length or counter), followed by proper 2-column numeric rows; vanilla
|
|
531
|
+
`genfromtxt` would lock onto the one-column width and then error.
|
|
532
|
+
|
|
533
|
+
Parameters
|
|
534
|
+
----------
|
|
535
|
+
filepath : str
|
|
536
|
+
Path to the file to read.
|
|
537
|
+
skip_header : int, optional
|
|
538
|
+
Number of initial lines to skip unconditionally.
|
|
539
|
+
min_cols : int, optional
|
|
540
|
+
Minimum number of numeric columns required to accept a line (default 2).
|
|
541
|
+
|
|
542
|
+
Returns
|
|
543
|
+
-------
|
|
544
|
+
ndarray
|
|
545
|
+
Array of shape (N, >=min_cols). Only the first `min_cols` columns are guaranteed.
|
|
546
|
+
|
|
547
|
+
Raises
|
|
548
|
+
------
|
|
549
|
+
FileNotFoundError
|
|
550
|
+
If the file does not exist.
|
|
551
|
+
ValueError
|
|
552
|
+
If no usable numeric rows are found.
|
|
553
|
+
|
|
554
|
+
Notes
|
|
555
|
+
-----
|
|
556
|
+
- Treats lines starting with '#' as comments.
|
|
557
|
+
- Replaces commas with spaces to tolerate CSV-ish files.
|
|
558
|
+
- Silently skips lines that fail float conversion or are too short.
|
|
559
|
+
"""
|
|
560
|
+
rows = []
|
|
561
|
+
if not os.path.isfile(filepath):
|
|
562
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
563
|
+
with open(filepath, "r", encoding="utf-8", errors="replace") as fh:
|
|
564
|
+
for i, raw in enumerate(fh):
|
|
565
|
+
if i < skip_header:
|
|
566
|
+
continue
|
|
567
|
+
line = raw.strip()
|
|
568
|
+
if not line or line.startswith("#"):
|
|
569
|
+
continue
|
|
570
|
+
line = line.replace(",", " ")
|
|
571
|
+
parts = [p for p in line.split() if p]
|
|
572
|
+
if len(parts) < min_cols:
|
|
573
|
+
continue
|
|
574
|
+
try:
|
|
575
|
+
nums = [float(parts[j]) for j in range(min_cols)]
|
|
576
|
+
except Exception:
|
|
577
|
+
continue
|
|
578
|
+
rows.append(nums)
|
|
579
|
+
if not rows:
|
|
580
|
+
raise ValueError(f"No usable numeric rows with ≥{min_cols} columns in {filepath}")
|
|
581
|
+
return np.asarray(rows, dtype=float)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def load_spectra_components(directory: str) -> dict[str, np.ndarray]:
|
|
585
|
+
"""
|
|
586
|
+
Load and align spectral components for a given mass-directory.
|
|
587
|
+
|
|
588
|
+
Files expected in `directory`
|
|
589
|
+
-----------------------------
|
|
590
|
+
instantaneous_primary_spectra.txt
|
|
591
|
+
Columns: E(GeV) dN/dE (GeV^-1 s^-1) [we later convert E to MeV and flux to MeV^-1 s^-1]
|
|
592
|
+
instantaneous_secondary_spectra.txt
|
|
593
|
+
Columns: E(MeV) dN/dE (MeV^-1 s^-1)
|
|
594
|
+
inflight_annihilation_prim.txt
|
|
595
|
+
Typically two columns (E(MeV), rate) but may contain a spurious single-number line first.
|
|
596
|
+
inflight_annihilation_sec.txt
|
|
597
|
+
Same caveat as above.
|
|
598
|
+
final_state_radiation_prim.txt
|
|
599
|
+
Typically two columns, sometimes with one header line to skip.
|
|
600
|
+
final_state_radiation_sec.txt
|
|
601
|
+
Typically two columns, sometimes with one header line to skip.
|
|
602
|
+
|
|
603
|
+
Returns
|
|
604
|
+
-------
|
|
605
|
+
dict[str, ndarray]
|
|
606
|
+
Keys:
|
|
607
|
+
energy_primary, energy_secondary,
|
|
608
|
+
direct_gamma_primary, direct_gamma_secondary,
|
|
609
|
+
IFA_primary, IFA_secondary,
|
|
610
|
+
FSR_primary, FSR_secondary
|
|
611
|
+
|
|
612
|
+
Notes
|
|
613
|
+
-----
|
|
614
|
+
- This function now uses `load_xy_lenient` for IFA/FSR files to survive files with
|
|
615
|
+
leading single-value rows.
|
|
616
|
+
"""
|
|
617
|
+
primary = load_data(os.path.join(directory, "instantaneous_primary_spectra.txt"), skip_header=2)[123:]
|
|
618
|
+
secondary = load_data(os.path.join(directory, "instantaneous_secondary_spectra.txt"), skip_header=1)
|
|
619
|
+
|
|
620
|
+
# lenient loads for files that sometimes start with a single-number row
|
|
621
|
+
IFA_prim = load_xy_lenient(os.path.join(directory, "inflight_annihilation_prim.txt"))
|
|
622
|
+
IFA_sec = load_xy_lenient(os.path.join(directory, "inflight_annihilation_sec.txt"))
|
|
623
|
+
FSR_prim = load_xy_lenient(os.path.join(directory, "final_state_radiation_prim.txt"), skip_header=1)
|
|
624
|
+
FSR_sec = load_xy_lenient(os.path.join(directory, "final_state_radiation_sec.txt"), skip_header=1)
|
|
625
|
+
|
|
626
|
+
E_prim = primary[:, 0] * 1e3 # convert GeV → MeV
|
|
627
|
+
E_sec = secondary[:, 0] # already in MeV
|
|
628
|
+
|
|
629
|
+
return {
|
|
630
|
+
'energy_primary': E_prim,
|
|
631
|
+
'energy_secondary': E_sec,
|
|
632
|
+
'direct_gamma_primary': primary[:, 1] / 1e3, # GeV^-1 → MeV^-1
|
|
633
|
+
'direct_gamma_secondary': secondary[:, 1],
|
|
634
|
+
'IFA_primary': np.interp(E_prim, IFA_prim[:, 0], IFA_prim[:, 1], left=0.0, right=0.0),
|
|
635
|
+
'IFA_secondary': np.interp(E_sec, IFA_sec[:, 0], IFA_sec[:, 1], left=0.0, right=0.0),
|
|
636
|
+
'FSR_primary': np.interp(E_prim, FSR_prim[:, 0], FSR_prim[:, 1]),
|
|
637
|
+
'FSR_secondary': np.interp(E_sec, FSR_sec[:, 0], FSR_sec[:, 1]),
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
# ---------------------------
|
|
642
|
+
# Monochromatic
|
|
643
|
+
# ---------------------------
|
|
644
|
+
def generate_monochromatic_for_mass(target_mass: float, data_dir: str, out_dir: str) -> str:
|
|
645
|
+
"""
|
|
646
|
+
Generate (or more precisely, assemble) a monochromatic spectrum file for the
|
|
647
|
+
nearest available pre-rendered mass to `target_mass`.
|
|
648
|
+
|
|
649
|
+
Parameters
|
|
650
|
+
----------
|
|
651
|
+
target_mass : float
|
|
652
|
+
Desired PBH mass [g].
|
|
653
|
+
data_dir : str
|
|
654
|
+
Directory containing the BlackHawk mass folders.
|
|
655
|
+
out_dir : str
|
|
656
|
+
Directory to write the output TXT file.
|
|
657
|
+
|
|
658
|
+
Returns
|
|
659
|
+
-------
|
|
660
|
+
str
|
|
661
|
+
Path to the saved monochromatic spectrum file. Columns:
|
|
662
|
+
E_gamma(MeV), TotalSpectrum(MeV^-1 s^-1)
|
|
663
|
+
|
|
664
|
+
Notes
|
|
665
|
+
-----
|
|
666
|
+
- We re-compute the *total* as Direct + Secondary + IFA + FSR aligned onto the
|
|
667
|
+
primary energy grid for consistency with plotting routines.
|
|
668
|
+
- The output file name encodes the requested mass, not the snapped mass, to
|
|
669
|
+
reflect the user's intention; the data inside reflects the snapped folder.
|
|
670
|
+
"""
|
|
671
|
+
masses, names = discover_mass_folders(data_dir)
|
|
672
|
+
if not masses:
|
|
673
|
+
raise RuntimeError("No valid mass folders found to generate monochromatic spectrum.")
|
|
674
|
+
snap = snap_to_available(target_mass, masses)
|
|
675
|
+
if snap is None:
|
|
676
|
+
# choose nearest in log-space
|
|
677
|
+
log_t = np.log(target_mass)
|
|
678
|
+
idx = int(np.argmin(np.abs(np.log(masses) - log_t)))
|
|
679
|
+
snap = masses[idx]
|
|
680
|
+
idx_snap = np.where(np.isclose(masses, snap, rtol=0, atol=0))[0][0]
|
|
681
|
+
sub = os.path.join(data_dir, names[idx_snap])
|
|
682
|
+
S = load_spectra_components(sub)
|
|
683
|
+
|
|
684
|
+
# align everything on the primary grid
|
|
685
|
+
E = S['energy_primary']
|
|
686
|
+
total = (
|
|
687
|
+
S['direct_gamma_primary']
|
|
688
|
+
+ np.interp(E, S['energy_secondary'], S['direct_gamma_secondary'], left=0, right=0)
|
|
689
|
+
+ S['IFA_primary'] + np.interp(E, S['energy_secondary'], S['IFA_secondary'], left=0, right=0)
|
|
690
|
+
+ S['FSR_primary'] + np.interp(E, S['energy_secondary'], S['FSR_secondary'], left=0, right=0)
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
out_name = os.path.join(out_dir, f"{target_mass:.2e}_mono_generated.txt")
|
|
694
|
+
np.savetxt(out_name, np.column_stack((E, total)),
|
|
695
|
+
header="E_gamma(MeV) TotalSpectrum (MeV^-1 s^-1)", fmt="%.10e")
|
|
696
|
+
return out_name
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def monochromatic_spectra() -> None:
|
|
700
|
+
"""
|
|
701
|
+
Interactive tool to plot one or more monochromatic spectra.
|
|
702
|
+
|
|
703
|
+
Flow
|
|
704
|
+
----
|
|
705
|
+
1) Discover available pre-rendered masses.
|
|
706
|
+
2) Ask user to enter a comma-separated list of target masses.
|
|
707
|
+
3) Build logM–logE splines across the full mass grid to allow interpolation
|
|
708
|
+
for off-grid masses as needed.
|
|
709
|
+
4) For each requested mass, plot component curves and total; then offer to
|
|
710
|
+
save selected spectra into the monochromatic results folder.
|
|
711
|
+
"""
|
|
712
|
+
masses, names = discover_mass_folders(DATA_DIR)
|
|
713
|
+
if not masses:
|
|
714
|
+
warn(f"No valid mass folders found under: {DATA_DIR}")
|
|
715
|
+
return
|
|
716
|
+
MIN_MASS, MAX_MASS = min(masses), max(masses)
|
|
717
|
+
|
|
718
|
+
try:
|
|
719
|
+
masses_str = user_input(
|
|
720
|
+
f"Enter PBH masses (g) to simulate (comma-separated; allowed range [{MIN_MASS:.2e}, {MAX_MASS:.2e}]): ",
|
|
721
|
+
allow_back=True
|
|
722
|
+
)
|
|
723
|
+
except BackRequested:
|
|
724
|
+
return
|
|
725
|
+
|
|
726
|
+
mass_list = []
|
|
727
|
+
if masses_str.strip():
|
|
728
|
+
for tok in masses_str.split(','):
|
|
729
|
+
t = tok.strip()
|
|
730
|
+
if not t:
|
|
731
|
+
continue
|
|
732
|
+
try:
|
|
733
|
+
mval = float(t)
|
|
734
|
+
except Exception:
|
|
735
|
+
warn(f"Skipping mass token '{t}': not a number.")
|
|
736
|
+
continue
|
|
737
|
+
if not (MIN_MASS <= mval <= MAX_MASS):
|
|
738
|
+
warn(f"Skipping mass {mval:.3e} g: outside allowed range [{MIN_MASS:.2e}, {MAX_MASS:.2e}].")
|
|
739
|
+
continue
|
|
740
|
+
mass_list.append(mval)
|
|
741
|
+
if not mass_list:
|
|
742
|
+
warn("No valid masses provided. Returning to menu.")
|
|
743
|
+
return
|
|
744
|
+
|
|
745
|
+
info("Pre-loading pre-rendered components …")
|
|
746
|
+
first_S = load_spectra_components(os.path.join(DATA_DIR, names[0]))
|
|
747
|
+
E_ref = first_S['energy_primary']
|
|
748
|
+
N_E = len(E_ref)
|
|
749
|
+
N_M = len(masses)
|
|
750
|
+
|
|
751
|
+
direct_mat = np.zeros((N_M, N_E))
|
|
752
|
+
secondary_mat = np.zeros((N_M, N_E))
|
|
753
|
+
inflight_mat = np.zeros((N_M, N_E))
|
|
754
|
+
finalstate_mat = np.zeros((N_M, N_E))
|
|
755
|
+
Emax_ifa = np.zeros(N_M)
|
|
756
|
+
|
|
757
|
+
for i, m in enumerate(masses):
|
|
758
|
+
sub = os.path.join(DATA_DIR, names[i])
|
|
759
|
+
S = load_spectra_components(sub)
|
|
760
|
+
direct_mat[i] = S['direct_gamma_primary']
|
|
761
|
+
secondary_mat[i] = np.interp(E_ref, S['energy_secondary'], S['direct_gamma_secondary'], left=0, right=0)
|
|
762
|
+
inflight_mat[i] = S['IFA_primary'] + np.interp(E_ref, S['energy_secondary'], S['IFA_secondary'], left=0, right=0)
|
|
763
|
+
finalstate_mat[i] = S['FSR_primary'] + np.interp(E_ref, S['energy_secondary'], S['FSR_secondary'], left=0, right=0)
|
|
764
|
+
p = load_xy_lenient(os.path.join(sub, "inflight_annihilation_prim.txt"))
|
|
765
|
+
s = load_xy_lenient(os.path.join(sub, "inflight_annihilation_sec.txt"))
|
|
766
|
+
Emax_ifa[i] = max(p[:,0].max() if p.size else 0, s[:,0].max() if s.size else 0)
|
|
767
|
+
|
|
768
|
+
logM_all = np.log(masses)
|
|
769
|
+
logE = np.log(E_ref)
|
|
770
|
+
tiny = 1e-300
|
|
771
|
+
|
|
772
|
+
ld = np.log(np.where(direct_mat>tiny, direct_mat, tiny))
|
|
773
|
+
ls = np.log(np.where(secondary_mat>tiny, secondary_mat, tiny))
|
|
774
|
+
li = np.log(np.where(inflight_mat>tiny, inflight_mat, tiny))
|
|
775
|
+
lf = np.log(np.where(finalstate_mat>tiny, finalstate_mat, tiny))
|
|
776
|
+
|
|
777
|
+
spline_direct = RectBivariateSpline(logM_all, logE, ld, kx=1, ky=3, s=0)
|
|
778
|
+
spline_secondary = RectBivariateSpline(logM_all, logE, ls, kx=1, ky=3, s=0)
|
|
779
|
+
spline_inflight = RectBivariateSpline(logM_all, logE, li, kx=1, ky=3, s=0)
|
|
780
|
+
spline_finalstate = RectBivariateSpline(logM_all, logE, lf, kx=1, ky=3, s=0)
|
|
781
|
+
info("Built splines (linear in logM, cubic in logE).")
|
|
782
|
+
|
|
783
|
+
all_data = []
|
|
784
|
+
for mval in mass_list:
|
|
785
|
+
snapped = snap_to_available(mval, masses)
|
|
786
|
+
if snapped is not None:
|
|
787
|
+
i = np.where(np.isclose(masses, snapped, rtol=0, atol=0))[0][0]
|
|
788
|
+
kind = 'pre-rendered'
|
|
789
|
+
d = direct_mat[i].copy()
|
|
790
|
+
s = secondary_mat[i].copy()
|
|
791
|
+
it= inflight_mat[i].copy()
|
|
792
|
+
f = finalstate_mat[i].copy()
|
|
793
|
+
else:
|
|
794
|
+
kind = 'interpolated'
|
|
795
|
+
idx_up = int(np.searchsorted(masses, mval, side='left'))
|
|
796
|
+
idx_low = max(0, idx_up-1)
|
|
797
|
+
idx_up = min(idx_up, N_M-1)
|
|
798
|
+
Ecut = min(Emax_ifa[idx_low], Emax_ifa[idx_up])
|
|
799
|
+
logm = np.log(mval)
|
|
800
|
+
d = np.exp(spline_direct(logm, logE, grid=False))
|
|
801
|
+
s = np.exp(spline_secondary(logm, logE, grid=False))
|
|
802
|
+
it = np.exp(spline_inflight(logm, logE, grid=False))
|
|
803
|
+
f = np.exp(spline_finalstate(logm, logE, grid=False))
|
|
804
|
+
# Guard tails in inflight
|
|
805
|
+
for k in range(len(it)-1, 0, -1):
|
|
806
|
+
if np.isclose(it[k], it[k-1], rtol=1e-8):
|
|
807
|
+
it[k] = 0.0
|
|
808
|
+
else:
|
|
809
|
+
break
|
|
810
|
+
log10i = np.log10(np.where(it>0, it, tiny))
|
|
811
|
+
for j in range(1, len(log10i)):
|
|
812
|
+
if log10i[j] - log10i[j-1] < -50:
|
|
813
|
+
it[j:] = 0.0
|
|
814
|
+
break
|
|
815
|
+
it[E_ref >= Ecut] = 0.0
|
|
816
|
+
|
|
817
|
+
tot = d + s + it + f
|
|
818
|
+
tol = 1e-299
|
|
819
|
+
for arr in (d, s, it, f, tot):
|
|
820
|
+
arr[arr < tol] = 0.0
|
|
821
|
+
|
|
822
|
+
# Plot components and total
|
|
823
|
+
plt.figure(figsize=(10,7))
|
|
824
|
+
if np.any(d>0): plt.plot(E_ref[d>0], d[d>0], label="Direct Hawking", lw=2)
|
|
825
|
+
if np.any(s>0): plt.plot(E_ref[s>0], s[s>0], label="Secondary", lw=2, linestyle='--')
|
|
826
|
+
if np.any(it>0): plt.plot(E_ref[it>0], it[it>0], label="Inflight", lw=2)
|
|
827
|
+
if np.any(f>0): plt.plot(E_ref[f>0], f[f>0], label="Final State", lw=2)
|
|
828
|
+
if np.any(tot>0):plt.plot(E_ref[tot>0],tot[tot>0],'k.', label="Total Spectrum")
|
|
829
|
+
plt.xlabel(r'$E_\gamma$ (MeV)')
|
|
830
|
+
plt.ylabel(r'$dN_\gamma/dE_\gamma$ (MeV$^{-1}$ s$^{-1}$)')
|
|
831
|
+
plt.xscale('log'); plt.yscale('log')
|
|
832
|
+
peak_total = tot.max() if tot.size else 1e-20
|
|
833
|
+
plt.ylim(peak_total/1e3, peak_total*1e1)
|
|
834
|
+
plt.xlim(0.5, 5000.0)
|
|
835
|
+
plt.grid(True, which='both', linestyle='--')
|
|
836
|
+
plt.legend()
|
|
837
|
+
plt.title(f'Components for {mval:.2e} g ({kind})')
|
|
838
|
+
plt.tight_layout()
|
|
839
|
+
plt.show()
|
|
840
|
+
plt.close()
|
|
841
|
+
|
|
842
|
+
all_data.append({
|
|
843
|
+
'mass': mval, 'kind': kind, 'E': E_ref.copy(),
|
|
844
|
+
'direct': d.copy(), 'secondary': s.copy(),
|
|
845
|
+
'inflight': it.copy(), 'finalstate': f.copy(),
|
|
846
|
+
'total': tot.copy()
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
# Overlaid E² dN/dE plot across all requested masses
|
|
850
|
+
if all_data:
|
|
851
|
+
fig = plt.figure(figsize=(10,7))
|
|
852
|
+
summed = np.zeros_like(all_data[0]['E'])
|
|
853
|
+
peaks = []
|
|
854
|
+
for entry in all_data:
|
|
855
|
+
Ecur = entry['E']; tot = entry['total']; valid = tot>0
|
|
856
|
+
if np.any(valid):
|
|
857
|
+
plt.plot(Ecur[valid], Ecur[valid]**2 * tot[valid], lw=2,
|
|
858
|
+
label=f"{entry['mass']:.2e} g ({entry['kind']})")
|
|
859
|
+
summed += tot
|
|
860
|
+
peaks.append((Ecur[valid]**2 * tot[valid]).max())
|
|
861
|
+
vs = summed > 0
|
|
862
|
+
plt.plot(all_data[0]['E'][vs], all_data[0]['E'][vs]**2 * summed[vs],
|
|
863
|
+
'k:', lw=3, label="Summed")
|
|
864
|
+
ymax_o = max(peaks) * 1e1
|
|
865
|
+
ymin_o = ymax_o / 1e3
|
|
866
|
+
plt.xlabel(r'$E_\gamma$ (MeV)')
|
|
867
|
+
plt.ylabel(r'$E^2 dN_\gamma/dE_\gamma$ (MeV s$^{-1}$)')
|
|
868
|
+
plt.xscale('log'); plt.yscale('log')
|
|
869
|
+
plt.xlim(0.5, 5000.0); plt.ylim(ymin_o, ymax_o)
|
|
870
|
+
plt.grid(True, which='both', linestyle='--')
|
|
871
|
+
plt.legend()
|
|
872
|
+
plt.title('Total Hawking Radiation Spectra (E²·dN/dE)')
|
|
873
|
+
plt.tight_layout()
|
|
874
|
+
plt.show()
|
|
875
|
+
plt.close(fig)
|
|
876
|
+
|
|
877
|
+
sv = user_input("Save any spectra? (y/n): ", allow_back=False, allow_exit=True).strip().lower()
|
|
878
|
+
if sv in ['y', 'yes']:
|
|
879
|
+
print("Select spectra by index to save (single file each):")
|
|
880
|
+
for idx, e in enumerate(all_data, start=1):
|
|
881
|
+
print(f" {idx}: {e['mass']:.2e} g ({e['kind']})")
|
|
882
|
+
choice = user_input("Enter comma-separated indices (e.g. 1,3,5) or '0' to save ALL: ",
|
|
883
|
+
allow_back=False, allow_exit=True).strip().lower()
|
|
884
|
+
if choice == '0':
|
|
885
|
+
picks = list(range(1, len(all_data)+1))
|
|
886
|
+
else:
|
|
887
|
+
try:
|
|
888
|
+
picks = [int(x) for x in choice.split(',')]
|
|
889
|
+
except ValueError:
|
|
890
|
+
err("Invalid indices; skipping save.")
|
|
891
|
+
picks = []
|
|
892
|
+
for i in picks:
|
|
893
|
+
if 1 <= i <= len(all_data):
|
|
894
|
+
e = all_data[i - 1]
|
|
895
|
+
mass_label = f"{e['mass']:.2e}"
|
|
896
|
+
filename = os.path.join(MONO_RESULTS_DIR, f"{mass_label}_spectrum.txt")
|
|
897
|
+
data_cols = np.column_stack((
|
|
898
|
+
e['E'],
|
|
899
|
+
e['direct'], e['secondary'], e['inflight'], e['finalstate'], e['total']
|
|
900
|
+
))
|
|
901
|
+
header = "E_gamma(MeV) Direct Secondary Inflight FinalState Total (MeV^-1 s^-1)"
|
|
902
|
+
np.savetxt(filename, data_cols, header=header, fmt="%e")
|
|
903
|
+
print(f"Saved → {filename}")
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
# ---------------------------
|
|
907
|
+
# Right-edge spike trimming helper (kept for reference)
|
|
908
|
+
# ---------------------------
|
|
909
|
+
def _trim_right_spike(
|
|
910
|
+
x_line: np.ndarray,
|
|
911
|
+
y_line: np.ndarray,
|
|
912
|
+
up_thresh: float = 1.35,
|
|
913
|
+
down_thresh: float = 0.35,
|
|
914
|
+
max_trim_frac: float = 0.10
|
|
915
|
+
) -> int:
|
|
916
|
+
"""
|
|
917
|
+
Heuristic to trim a suspicious final spike/drop on the right edge of a curve.
|
|
918
|
+
|
|
919
|
+
Parameters
|
|
920
|
+
----------
|
|
921
|
+
x_line : ndarray
|
|
922
|
+
X grid (unused in logic; provided for potential future use).
|
|
923
|
+
y_line : ndarray
|
|
924
|
+
Y values to inspect.
|
|
925
|
+
up_thresh : float
|
|
926
|
+
If y[-1]/y[-2] > up_thresh, treat as spike.
|
|
927
|
+
down_thresh : float
|
|
928
|
+
If y[-1]/y[-2] < down_thresh, treat as plunge.
|
|
929
|
+
max_trim_frac : float
|
|
930
|
+
Do not trim more than this fraction of the array length.
|
|
931
|
+
|
|
932
|
+
Returns
|
|
933
|
+
-------
|
|
934
|
+
int
|
|
935
|
+
New usable length index (exclusive). Caller may slice up to this index.
|
|
936
|
+
"""
|
|
937
|
+
y = np.asarray(y_line, dtype=float)
|
|
938
|
+
n = y.size
|
|
939
|
+
if n < 3:
|
|
940
|
+
return n - 1
|
|
941
|
+
y_nm1, y_nm2 = y[-1], y[-2]
|
|
942
|
+
if not (np.isfinite(y_nm1) and np.isfinite(y_nm2)) or y_nm2 == 0:
|
|
943
|
+
return n - 1
|
|
944
|
+
ratio = y_nm1 / max(y_nm2, 1e-300)
|
|
945
|
+
if (ratio <= up_thresh) and (ratio >= down_thresh):
|
|
946
|
+
return n - 1
|
|
947
|
+
max_trim = max(3, int(max_trim_frac * n))
|
|
948
|
+
j = n - 1
|
|
949
|
+
trimmed = 0
|
|
950
|
+
if ratio > up_thresh:
|
|
951
|
+
while (j > 1 and trimmed < max_trim and np.isfinite(y[j]) and np.isfinite(y[j-1]) and
|
|
952
|
+
(y[j] / max(y[j-1], 1e-300) > up_thresh)):
|
|
953
|
+
j -= 1; trimmed += 1
|
|
954
|
+
return max(j, 2)
|
|
955
|
+
while (j > 1 and trimmed < max_trim and np.isfinite(y[j]) and np.isfinite(y[j-1]) and
|
|
956
|
+
(y[j] / max(y[j-1], 1e-300) < down_thresh)):
|
|
957
|
+
j -= 1; trimmed += 1
|
|
958
|
+
return max(j, 2)
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
# ---------------------------
|
|
962
|
+
# Distributed (Gaussian collapse / Non-Gaussian / Lognormal)
|
|
963
|
+
# ---------------------------
|
|
964
|
+
def distributed_spectrum(distribution_method: str) -> None:
|
|
965
|
+
"""
|
|
966
|
+
Generate distributed spectra using one of the supported PBH mass distributions.
|
|
967
|
+
|
|
968
|
+
Parameters
|
|
969
|
+
----------
|
|
970
|
+
distribution_method : str
|
|
971
|
+
One of:
|
|
972
|
+
- GAUSSIAN_METHOD ("Gaussian collapse")
|
|
973
|
+
- NON_GAUSSIAN_METHOD ("Non-Gaussian Collapse")
|
|
974
|
+
- LOGNORMAL_METHOD ("Log-Normal Distribution")
|
|
975
|
+
|
|
976
|
+
Interactive Flow
|
|
977
|
+
----------------
|
|
978
|
+
1) Prompt for one or more peak masses (must lie within available pre-rendered grid).
|
|
979
|
+
2) Prompt for target sample size N.
|
|
980
|
+
3) Prompt for distribution-specific width parameter(s) (σ / σ_X / σ in ln-space).
|
|
981
|
+
4) Sample masses, accumulate average spectra via log–log splines (with IFA tail guards).
|
|
982
|
+
5) Plot dN/dE and E² dN/dE overlays across all chosen parameter sets.
|
|
983
|
+
6) For each set, plot its mass histogram with a counts-scaled analytic PDF overlay.
|
|
984
|
+
7) Offer to save results into a unique directory under the method-specific results root.
|
|
985
|
+
|
|
986
|
+
Notes
|
|
987
|
+
-----
|
|
988
|
+
- For Non-Gaussian, we enforce 0.04 ≤ σ_X ≤ 0.16 and set σ_Y/σ_X = 0.75 (typical choice).
|
|
989
|
+
- For Log-Normal, we interpret the user's σ as the ln-space standard deviation and choose
|
|
990
|
+
μ such that the mode equals the requested peak (μ_eff = ln(peak) + σ²).
|
|
991
|
+
- All interpolation occurs in (logM, logE) space; inflight annihilation tails are trimmed.
|
|
992
|
+
"""
|
|
993
|
+
is_g = (distribution_method == GAUSSIAN_METHOD)
|
|
994
|
+
is_ng = (distribution_method == NON_GAUSSIAN_METHOD)
|
|
995
|
+
is_ln = (distribution_method == LOGNORMAL_METHOD)
|
|
996
|
+
|
|
997
|
+
masses, names = discover_mass_folders(DATA_DIR)
|
|
998
|
+
if not masses:
|
|
999
|
+
warn(f"No valid mass folders found under: {DATA_DIR}")
|
|
1000
|
+
return
|
|
1001
|
+
MIN_MASS, MAX_MASS = min(masses), max(masses)
|
|
1002
|
+
|
|
1003
|
+
try:
|
|
1004
|
+
pstr = user_input(
|
|
1005
|
+
f"Enter peak PBH masses (g) (comma-separated; each must be within [{MIN_MASS:.2e}, {MAX_MASS:.2e}]): ",
|
|
1006
|
+
allow_back=True, allow_exit=True
|
|
1007
|
+
)
|
|
1008
|
+
except BackRequested:
|
|
1009
|
+
return
|
|
1010
|
+
|
|
1011
|
+
peaks = parse_float_list_verbose(pstr, name="peak mass (g)", bounds=(MIN_MASS, MAX_MASS), allow_empty=False)
|
|
1012
|
+
if not peaks:
|
|
1013
|
+
warn("No valid peaks; returning.")
|
|
1014
|
+
return
|
|
1015
|
+
|
|
1016
|
+
try:
|
|
1017
|
+
nstr = user_input("Enter target N (integer, e.g. 1000): ",
|
|
1018
|
+
allow_back=True, allow_exit=True)
|
|
1019
|
+
except BackRequested:
|
|
1020
|
+
return
|
|
1021
|
+
|
|
1022
|
+
try:
|
|
1023
|
+
N_target = int(nstr)
|
|
1024
|
+
if N_target <= 0:
|
|
1025
|
+
err("N must be > 0. Returning.")
|
|
1026
|
+
return
|
|
1027
|
+
except Exception:
|
|
1028
|
+
err("Invalid N (not an integer). Returning.")
|
|
1029
|
+
return
|
|
1030
|
+
|
|
1031
|
+
# collapse parameters (shared constants used in the literature fitting)
|
|
1032
|
+
kappa, gamma_p, delta_c = 3.3, 0.36, 0.59
|
|
1033
|
+
|
|
1034
|
+
# read parameter lists
|
|
1035
|
+
param_sets = []
|
|
1036
|
+
if is_g:
|
|
1037
|
+
try:
|
|
1038
|
+
sstr = user_input("Enter σ list for Gaussian collapse (comma-separated; each must be within [0.03, 0.255]): ",
|
|
1039
|
+
allow_back=True, allow_exit=True).strip()
|
|
1040
|
+
except BackRequested:
|
|
1041
|
+
return
|
|
1042
|
+
sigmas = parse_float_list_verbose(sstr, name="σ", bounds=(0.03, 0.255), allow_empty=False)
|
|
1043
|
+
if not sigmas:
|
|
1044
|
+
warn("No valid σ for Gaussian; returning.")
|
|
1045
|
+
return
|
|
1046
|
+
for sx in sigmas:
|
|
1047
|
+
param_sets.append({"sigma_x": sx})
|
|
1048
|
+
|
|
1049
|
+
elif is_ng:
|
|
1050
|
+
try:
|
|
1051
|
+
sx_str = user_input("Enter σ_X list for Non-Gaussian collapse (comma-separated; σ must be within [0.04, 0.16]): ",
|
|
1052
|
+
allow_back=True, allow_exit=True).strip()
|
|
1053
|
+
except BackRequested:
|
|
1054
|
+
return
|
|
1055
|
+
sigmas_X = parse_float_list_verbose(sx_str, name="σ_X", bounds=(0.04, 0.16), allow_empty=False)
|
|
1056
|
+
if not sigmas_X:
|
|
1057
|
+
warn("No valid σ for Non-Gaussian; returning.")
|
|
1058
|
+
return
|
|
1059
|
+
for sX in sigmas_X:
|
|
1060
|
+
param_sets.append({"sigma_X": sX, "ratio": 0.75})
|
|
1061
|
+
|
|
1062
|
+
else: # is_ln
|
|
1063
|
+
try:
|
|
1064
|
+
sig_str = user_input("Enter σ list (log-space std) for Log-Normal (comma-separated; each > 0): ",
|
|
1065
|
+
allow_back=True, allow_exit=True).strip()
|
|
1066
|
+
except BackRequested:
|
|
1067
|
+
return
|
|
1068
|
+
sigmas_ln = parse_float_list_verbose(sig_str, name="σ", bounds=(1e-12, None), allow_empty=False, strict_gt=True)
|
|
1069
|
+
if not sigmas_ln:
|
|
1070
|
+
warn("No valid σ for Log-Normal; returning.")
|
|
1071
|
+
return
|
|
1072
|
+
for sln in sigmas_ln:
|
|
1073
|
+
param_sets.append({"sigma_ln": sln})
|
|
1074
|
+
|
|
1075
|
+
# pre-load all component matrices on a shared energy grid
|
|
1076
|
+
first = load_spectra_components(os.path.join(DATA_DIR, names[0]))
|
|
1077
|
+
E_grid = first['energy_primary']
|
|
1078
|
+
logE = np.log(E_grid)
|
|
1079
|
+
N_M = len(masses)
|
|
1080
|
+
|
|
1081
|
+
direct_mat = np.zeros((N_M, len(E_grid)))
|
|
1082
|
+
secondary_mat = np.zeros_like(direct_mat)
|
|
1083
|
+
inflight_mat = np.zeros_like(direct_mat)
|
|
1084
|
+
final_mat = np.zeros_like(direct_mat)
|
|
1085
|
+
Emax_ifa = np.zeros(N_M)
|
|
1086
|
+
|
|
1087
|
+
for i, m in enumerate(masses):
|
|
1088
|
+
sub = os.path.join(DATA_DIR, names[i])
|
|
1089
|
+
S = load_spectra_components(sub)
|
|
1090
|
+
direct_mat[i] = S['direct_gamma_primary']
|
|
1091
|
+
secondary_mat[i] = np.interp(E_grid, S['energy_secondary'], S['direct_gamma_secondary'], left=0, right=0)
|
|
1092
|
+
inflight_mat[i] = S['IFA_primary'] + np.interp(E_grid, S['energy_secondary'], S['IFA_secondary'], left=0, right=0)
|
|
1093
|
+
final_mat[i] = S['FSR_primary'] + np.interp(E_grid, S['energy_secondary'], S['FSR_secondary'], left=0, right=0)
|
|
1094
|
+
|
|
1095
|
+
p = load_xy_lenient(os.path.join(sub, "inflight_annihilation_prim.txt"))
|
|
1096
|
+
s = load_xy_lenient(os.path.join(sub, "inflight_annihilation_sec.txt"))
|
|
1097
|
+
Emax_ifa[i] = max(p[:,0].max() if p.size else 0, s[:,0].max() if s.size else 0)
|
|
1098
|
+
|
|
1099
|
+
logM_all = np.log(masses)
|
|
1100
|
+
floor = 1e-300
|
|
1101
|
+
|
|
1102
|
+
ld = np.log(np.where(direct_mat > floor, direct_mat, floor))
|
|
1103
|
+
ls = np.log(np.where(secondary_mat > floor, secondary_mat, floor))
|
|
1104
|
+
li = np.log(np.where(inflight_mat > floor, inflight_mat, floor))
|
|
1105
|
+
lf = np.log(np.where(final_mat > floor, final_mat, floor))
|
|
1106
|
+
|
|
1107
|
+
sp_d = RectBivariateSpline(logM_all, logE, ld, kx=1, ky=3, s=0)
|
|
1108
|
+
sp_s = RectBivariateSpline(logM_all, logE, ls, kx=1, ky=3, s=0)
|
|
1109
|
+
sp_i = RectBivariateSpline(logM_all, logE, li, kx=1, ky=3, s=0)
|
|
1110
|
+
sp_f = RectBivariateSpline(logM_all, logE, lf, kx=1, ky=3, s=0)
|
|
1111
|
+
|
|
1112
|
+
results: list[dict] = []
|
|
1113
|
+
|
|
1114
|
+
for params in param_sets:
|
|
1115
|
+
|
|
1116
|
+
if is_g:
|
|
1117
|
+
sigma_x = params["sigma_x"]
|
|
1118
|
+
x = np.linspace(0.001, 1.30909, 2000)
|
|
1119
|
+
mf = mass_function(delta_l(x, 3.3, 0.59, 0.36), sigma_x, 0.59, 0.36)
|
|
1120
|
+
label_param = f"σ={sigma_x:.3g}"
|
|
1121
|
+
mf = np.where(np.isfinite(mf) & (mf > 0), mf, 0.0)
|
|
1122
|
+
if mf.sum() <= 0:
|
|
1123
|
+
warn(f"Underlying PDF vanished for σ={sigma_x:g}; skipping.")
|
|
1124
|
+
continue
|
|
1125
|
+
probabilities = mf / mf.sum()
|
|
1126
|
+
r_mode = x[np.argmax(mf)] if np.any(mf) else x[len(x)//2]
|
|
1127
|
+
|
|
1128
|
+
elif is_ng:
|
|
1129
|
+
sigma_X = params["sigma_X"]; ratio = params["ratio"]; sigma_Y = ratio * sigma_X
|
|
1130
|
+
x = np.linspace(0.001, 1.30909, 2000)
|
|
1131
|
+
mf = mass_function_exact(delta_l(x, 3.3, 0.59, 0.36), sigma_X, sigma_Y, 0.59, 0.36)
|
|
1132
|
+
label_param = f"σX={sigma_X:.3g}"
|
|
1133
|
+
mf = np.where(np.isfinite(mf) & (mf > 0), mf, 0.0)
|
|
1134
|
+
if mf.sum() <= 0:
|
|
1135
|
+
warn(f"Underlying PDF vanished for σ_X={sigma_X:g}; skipping.")
|
|
1136
|
+
continue
|
|
1137
|
+
probabilities = mf / mf.sum()
|
|
1138
|
+
r_mode = x[np.argmax(mf)] if np.any(mf) else x[len(x)//2]
|
|
1139
|
+
|
|
1140
|
+
else: # is_ln
|
|
1141
|
+
sigma_ln = params["sigma_ln"]
|
|
1142
|
+
label_param = f"σ={sigma_ln:.3g}"
|
|
1143
|
+
|
|
1144
|
+
for peak in peaks:
|
|
1145
|
+
sum_d = np.zeros_like(E_grid); sum_s = np.zeros_like(E_grid)
|
|
1146
|
+
sum_i = np.zeros_like(E_grid); sum_f = np.zeros_like(E_grid)
|
|
1147
|
+
md = []
|
|
1148
|
+
|
|
1149
|
+
bar = tqdm(total=N_target, desc=f"Sampling peak {peak:.2e} [{label_param}]", unit="BH")
|
|
1150
|
+
|
|
1151
|
+
if is_ln:
|
|
1152
|
+
mu_eff = np.log(peak) + sigma_ln**2
|
|
1153
|
+
try:
|
|
1154
|
+
masses_drawn = np.random.lognormal(mean=mu_eff, sigma=sigma_ln, size=N_target)
|
|
1155
|
+
except Exception as e:
|
|
1156
|
+
err(f"Sampling error (lognormal, peak {peak:.3e}, σ={sigma_ln:g}): {e}. Skipping.")
|
|
1157
|
+
bar.close()
|
|
1158
|
+
continue
|
|
1159
|
+
for mraw in masses_drawn:
|
|
1160
|
+
md.append(float(mraw))
|
|
1161
|
+
if mraw < MIN_MASS or mraw > MAX_MASS:
|
|
1162
|
+
d_vals = s_vals = i_vals = f_vals = np.zeros_like(E_grid)
|
|
1163
|
+
else:
|
|
1164
|
+
try:
|
|
1165
|
+
snap = snap_to_available(mraw, masses)
|
|
1166
|
+
mval = snap if snap else mraw
|
|
1167
|
+
idx_up = int(np.searchsorted(masses, mval, side='left'))
|
|
1168
|
+
idx_low = max(0, idx_up-1)
|
|
1169
|
+
idx_up = min(idx_up, N_M-1)
|
|
1170
|
+
Ecut = min(Emax_ifa[idx_low], Emax_ifa[idx_up])
|
|
1171
|
+
logm = np.log(mval)
|
|
1172
|
+
d_vals = np.exp(sp_d(logm, logE, grid=False))
|
|
1173
|
+
s_vals = np.exp(sp_s(logm, logE, grid=False))
|
|
1174
|
+
i_vals = np.exp(sp_i(logm, logE, grid=False))
|
|
1175
|
+
f_vals = np.exp(sp_f(logm, logE, grid=False))
|
|
1176
|
+
except Exception as e:
|
|
1177
|
+
warn(f"Interpolation error at mass {mraw:.3e} g: {e}. Skipping draw.")
|
|
1178
|
+
d_vals = s_vals = i_vals = f_vals = np.zeros_like(E_grid)
|
|
1179
|
+
# guard inflight tails
|
|
1180
|
+
for j in range(len(i_vals)-1,0,-1):
|
|
1181
|
+
if np.isclose(i_vals[j], i_vals[j-1], rtol=1e-8): i_vals[j] = 0.0
|
|
1182
|
+
else: break
|
|
1183
|
+
log10i = np.log10(np.where(i_vals>0, i_vals, floor))
|
|
1184
|
+
for j in range(1,len(log10i)):
|
|
1185
|
+
if log10i[j] - log10i[j-1] < -50:
|
|
1186
|
+
i_vals[j:] = 0.0; break
|
|
1187
|
+
i_vals[E_grid >= Ecut] = 0.0
|
|
1188
|
+
sum_d += d_vals; sum_s += s_vals; sum_i += i_vals; sum_f += f_vals
|
|
1189
|
+
bar.update(1)
|
|
1190
|
+
|
|
1191
|
+
else:
|
|
1192
|
+
scale = peak / r_mode
|
|
1193
|
+
for _ in range(N_target):
|
|
1194
|
+
r = np.random.choice(x, p=probabilities)
|
|
1195
|
+
mraw = r * scale
|
|
1196
|
+
md.append(mraw)
|
|
1197
|
+
if mraw < MIN_MASS or mraw > MAX_MASS:
|
|
1198
|
+
d_vals = s_vals = i_vals = f_vals = np.zeros_like(E_grid)
|
|
1199
|
+
else:
|
|
1200
|
+
try:
|
|
1201
|
+
snap = snap_to_available(mraw, masses)
|
|
1202
|
+
mval = snap if snap else mraw
|
|
1203
|
+
idx_up = int(np.searchsorted(masses, mval, side='left'))
|
|
1204
|
+
idx_low = max(0, idx_up-1)
|
|
1205
|
+
idx_up = min(idx_up, N_M-1)
|
|
1206
|
+
Ecut = min(Emax_ifa[idx_low], Emax_ifa[idx_up])
|
|
1207
|
+
logm = np.log(mval)
|
|
1208
|
+
d_vals = np.exp(sp_d(logm, logE, grid=False))
|
|
1209
|
+
s_vals = np.exp(sp_s(logm, logE, grid=False))
|
|
1210
|
+
i_vals = np.exp(sp_i(logm, logE, grid=False))
|
|
1211
|
+
f_vals = np.exp(sp_f(logm, logE, grid=False))
|
|
1212
|
+
except Exception as e:
|
|
1213
|
+
warn(f"Interpolation error at mass {mraw:.3e} g: {e}. Skipping draw.")
|
|
1214
|
+
d_vals = s_vals = i_vals = f_vals = np.zeros_like(E_grid)
|
|
1215
|
+
# guard inflight tails
|
|
1216
|
+
for j in range(len(i_vals)-1,0,-1):
|
|
1217
|
+
if np.isclose(i_vals[j], i_vals[j-1], rtol=1e-8): i_vals[j] = 0.0
|
|
1218
|
+
else: break
|
|
1219
|
+
log10i = np.log10(np.where(i_vals>0, i_vals, floor))
|
|
1220
|
+
for j in range(1,len(log10i)):
|
|
1221
|
+
if log10i[j] - log10i[j-1] < -50:
|
|
1222
|
+
i_vals[j:] = 0.0; break
|
|
1223
|
+
i_vals[E_grid >= Ecut] = 0.0
|
|
1224
|
+
sum_d += d_vals; sum_s += s_vals; sum_i += i_vals; sum_f += f_vals
|
|
1225
|
+
bar.update(1)
|
|
1226
|
+
|
|
1227
|
+
bar.close()
|
|
1228
|
+
|
|
1229
|
+
avg_d = sum_d / N_target; avg_s = sum_s / N_target
|
|
1230
|
+
avg_i = sum_i / N_target; avg_f = sum_f / N_target
|
|
1231
|
+
avg_tot = avg_d + avg_s + avg_i + avg_f
|
|
1232
|
+
tol = 1e-299
|
|
1233
|
+
for arr in (avg_d, avg_s, avg_i, avg_f, avg_tot):
|
|
1234
|
+
arr[arr < tol] = 0.0
|
|
1235
|
+
|
|
1236
|
+
results.append({
|
|
1237
|
+
"method": ("gaussian" if is_g else "non_gaussian" if is_ng else "lognormal"),
|
|
1238
|
+
"peak": peak,
|
|
1239
|
+
"params": params.copy(),
|
|
1240
|
+
"E": E_grid.copy(),
|
|
1241
|
+
"spectrum": avg_tot.copy(),
|
|
1242
|
+
"mdist": md[:],
|
|
1243
|
+
"label_param": label_param,
|
|
1244
|
+
"nsamp": N_target
|
|
1245
|
+
})
|
|
1246
|
+
|
|
1247
|
+
if not results:
|
|
1248
|
+
return
|
|
1249
|
+
|
|
1250
|
+
# dN/dE overlays
|
|
1251
|
+
fig1 = plt.figure(figsize=(10,7))
|
|
1252
|
+
peaks_dn = []
|
|
1253
|
+
for r in results:
|
|
1254
|
+
E = r["E"]; sp = r["spectrum"]; m = sp > 0
|
|
1255
|
+
plt.plot(E[m], sp[m], lw=2,
|
|
1256
|
+
label=f"{distribution_method} {r['peak']:.1e}_{r['label_param'].replace('σ=','').replace('σX=','')}")
|
|
1257
|
+
peaks_dn.append(sp.max())
|
|
1258
|
+
plt.xscale('log'); plt.yscale('log')
|
|
1259
|
+
plt.xlabel(r'$E_\gamma$ (MeV)'); plt.ylabel(r'$dN_\gamma/dE_\gamma$')
|
|
1260
|
+
if peaks_dn: plt.ylim(min(peaks_dn)/1e3, max(peaks_dn)*10)
|
|
1261
|
+
plt.xlim(0.5, 5e3); plt.grid(True, which='both', linestyle='--'); plt.legend()
|
|
1262
|
+
plt.title("Comparison: dN/dE"); plt.tight_layout(); plt.show(); plt.close(fig1)
|
|
1263
|
+
|
|
1264
|
+
# E^2 dN/dE overlays
|
|
1265
|
+
fig2 = plt.figure(figsize=(10,7))
|
|
1266
|
+
peaks_e2 = []
|
|
1267
|
+
for r in results:
|
|
1268
|
+
E = r["E"]; sp = r["spectrum"]; m = sp > 0
|
|
1269
|
+
plt.plot(E[m], E[m]**2 * sp[m], lw=2,
|
|
1270
|
+
label=f"{distribution_method} {r['peak']:.1e}_{r['label_param'].replace('σ=','').replace('σX=','')}")
|
|
1271
|
+
peaks_e2.append((E[m]**2 * sp[m]).max() if np.any(m) else 0.0)
|
|
1272
|
+
plt.xscale('log'); plt.yscale('log')
|
|
1273
|
+
plt.xlabel(r'$E_\gamma$ (MeV)'); plt.ylabel(r'$E^2\,dN_\gamma/dE_\gamma$')
|
|
1274
|
+
if peaks_e2: plt.ylim(min(peaks_e2)/1e3, max(peaks_e2)*10)
|
|
1275
|
+
plt.xlim(0.5, 5e3); plt.grid(True, which='both', linestyle='--'); plt.legend()
|
|
1276
|
+
plt.title("Comparison: $E^2$ dN/dE"); plt.tight_layout(); plt.show(); plt.close(fig2)
|
|
1277
|
+
|
|
1278
|
+
# Histograms + theoretical mass-PDF overlays (in counts space)
|
|
1279
|
+
def _hist_common_bins(md: np.ndarray):
|
|
1280
|
+
"""Compute histogram bins via Freedman–Diaconis, clamped to [1,50]."""
|
|
1281
|
+
md = np.asarray(md, dtype=float)
|
|
1282
|
+
md = md[np.isfinite(md)]
|
|
1283
|
+
if md.size < 2 or (md.size > 0 and md.min() == md.max()):
|
|
1284
|
+
center = md[0] if md.size else 0.0
|
|
1285
|
+
eps = abs(center)*1e-9 if center != 0 else 1e-9
|
|
1286
|
+
return 1, (center - eps, center + eps), None
|
|
1287
|
+
q25, q75 = np.percentile(md, [25, 75])
|
|
1288
|
+
iqr = q75 - q25
|
|
1289
|
+
if iqr > 0:
|
|
1290
|
+
bw = 2 * iqr * md.size ** (-1/3)
|
|
1291
|
+
k = int(np.clip(np.ceil((md.max() - md.min()) / bw), 1, 50))
|
|
1292
|
+
else:
|
|
1293
|
+
k = int(np.clip(np.sqrt(md.size), 1, 50))
|
|
1294
|
+
return k, None, md
|
|
1295
|
+
|
|
1296
|
+
for r in results:
|
|
1297
|
+
method = r["method"]
|
|
1298
|
+
figH = plt.figure(figsize=(10,6))
|
|
1299
|
+
|
|
1300
|
+
md = np.asarray(r["mdist"], dtype=float)
|
|
1301
|
+
md = md[np.isfinite(md)]
|
|
1302
|
+
|
|
1303
|
+
k, fixed_range, md_safe = _hist_common_bins(md)
|
|
1304
|
+
if fixed_range is not None:
|
|
1305
|
+
_, bins, _ = plt.hist(md, bins=1, range=fixed_range, alpha=0.7, edgecolor='k',
|
|
1306
|
+
label=f'{distribution_method} samples ({r["label_param"]})')
|
|
1307
|
+
else:
|
|
1308
|
+
_, bins, _ = plt.hist(md_safe, bins=k, alpha=0.7, edgecolor='k',
|
|
1309
|
+
label=f'{distribution_method} samples ({r["label_param"]})')
|
|
1310
|
+
|
|
1311
|
+
bin_widths = (bins[1:] - bins[:-1])
|
|
1312
|
+
ref_width = float(np.median(bin_widths)) if bin_widths.size else 1.0
|
|
1313
|
+
|
|
1314
|
+
if method == "gaussian":
|
|
1315
|
+
sigma_x = r["params"]["sigma_x"]
|
|
1316
|
+
x = np.linspace(0.001, 1.30909, 2000)
|
|
1317
|
+
mf = mass_function(delta_l(x, 3.3, 0.59, 0.36), sigma_x, 0.59, 0.36)
|
|
1318
|
+
mf = np.where(np.isfinite(mf) & (mf > 0), mf, 0.0)
|
|
1319
|
+
if mf.sum() > 0:
|
|
1320
|
+
probabilities = mf / mf.sum()
|
|
1321
|
+
r_mode = x[np.argmax(mf)] if np.any(mf) else x[len(x)//2]
|
|
1322
|
+
scale = r["peak"] / r_mode
|
|
1323
|
+
dx = x[1] - x[0]; dm = dx * scale
|
|
1324
|
+
pdf_mass = probabilities / dm
|
|
1325
|
+
m_line = x * scale
|
|
1326
|
+
mask = (m_line >= bins[0]) & (m_line <= bins[-1]) & np.isfinite(pdf_mass) & (pdf_mass > 0)
|
|
1327
|
+
if np.any(mask):
|
|
1328
|
+
y_line = pdf_mass[mask] * ref_width * len(r["mdist"])
|
|
1329
|
+
plt.plot(m_line[mask], y_line, 'r--', lw=2, zorder=3, label='Underlying PDF (counts)')
|
|
1330
|
+
|
|
1331
|
+
elif method == "non_gaussian":
|
|
1332
|
+
sigma_X = r["params"]["sigma_X"]; ratio = 0.75; sigma_Y = ratio * sigma_X
|
|
1333
|
+
x = np.linspace(0.001, 1.30909, 2000)
|
|
1334
|
+
mf = mass_function_exact(delta_l(x, 3.3, 0.59, 0.36), sigma_X, sigma_Y, 0.59, 0.36)
|
|
1335
|
+
mf = np.where(np.isfinite(mf) & (mf > 0), mf, 0.0)
|
|
1336
|
+
if mf.sum() > 0:
|
|
1337
|
+
probabilities = mf / mf.sum()
|
|
1338
|
+
r_mode = x[np.argmax(mf)] if np.any(mf) else x[len(x)//2]
|
|
1339
|
+
scale = r["peak"] / r_mode
|
|
1340
|
+
dx = x[1] - x[0]; dm = dx * scale
|
|
1341
|
+
pdf_mass = probabilities / dm
|
|
1342
|
+
m_line = x * scale
|
|
1343
|
+
mask = (m_line >= bins[0]) & (m_line <= bins[-1]) & np.isfinite(pdf_mass) & (pdf_mass > 0)
|
|
1344
|
+
if np.any(mask):
|
|
1345
|
+
y_line = pdf_mass[mask] * ref_width * len(r["mdist"])
|
|
1346
|
+
plt.plot(m_line[mask], y_line, 'r--', lw=2, zorder=3, label='Underlying PDF (counts)')
|
|
1347
|
+
|
|
1348
|
+
else: # lognormal
|
|
1349
|
+
sigma_ln = r["params"]["sigma_ln"]; mu_eff = np.log(r["peak"]) + sigma_ln**2
|
|
1350
|
+
mlo_tail = np.exp(mu_eff - 6.0*sigma_ln); mhi_tail = np.exp(mu_eff + 6.0*sigma_ln)
|
|
1351
|
+
m_plot = np.logspace(np.log10(min(bins[0], mlo_tail)), np.log10(max(bins[-1], mhi_tail)), 2000)
|
|
1352
|
+
pdf = (1.0/(m_plot*sigma_ln*np.sqrt(2*np.pi))) * np.exp( - (np.log(m_plot)-mu_eff)**2 / (2*sigma_ln**2) )
|
|
1353
|
+
y_plot = pdf * ref_width * len(r["mdist"])
|
|
1354
|
+
plt.plot(m_plot, y_plot, 'r--', lw=2, zorder=3, label='Underlying PDF (counts)')
|
|
1355
|
+
plt.legend(title=f"σ={sigma_ln:.3f}")
|
|
1356
|
+
|
|
1357
|
+
plt.xlabel('Simulated PBH Mass (g)')
|
|
1358
|
+
plt.ylabel('Count')
|
|
1359
|
+
plt.title(f'Mass Distribution & PDF for Peak {r["peak"]:.2e} g')
|
|
1360
|
+
plt.grid(True, which='both', linestyle='--')
|
|
1361
|
+
plt.legend()
|
|
1362
|
+
plt.tight_layout()
|
|
1363
|
+
plt.show(); plt.close(figH)
|
|
1364
|
+
|
|
1365
|
+
# === Save distributed results ===
|
|
1366
|
+
try:
|
|
1367
|
+
tosave = user_input("Save distributed results? (y/n): ",
|
|
1368
|
+
allow_back=True, allow_exit=True).strip().lower()
|
|
1369
|
+
except BackRequested:
|
|
1370
|
+
tosave = 'n'
|
|
1371
|
+
if tosave in ('y', 'yes'):
|
|
1372
|
+
for r in results:
|
|
1373
|
+
method = r["method"]
|
|
1374
|
+
if method == "gaussian":
|
|
1375
|
+
base = GAUSS_RESULTS_DIR
|
|
1376
|
+
tag = f"peak_{r['peak']:.2e}_{r['label_param'].replace('=','')}_N{r['nsamp']}"
|
|
1377
|
+
elif method == "non_gaussian":
|
|
1378
|
+
base = NGAUSS_RESULTS_DIR
|
|
1379
|
+
tag = f"peak_{r['peak']:.2e}_{r['label_param'].replace('=','')}_N{r['nsamp']}"
|
|
1380
|
+
else:
|
|
1381
|
+
base = LOGN_RESULTS_DIR
|
|
1382
|
+
tag = f"peak_{r['peak']:.2e}_{r['label_param'].replace('=','')}_N{r['nsamp']}"
|
|
1383
|
+
outdir = os.path.join(base, tag)
|
|
1384
|
+
k = 1; unique = outdir
|
|
1385
|
+
while os.path.exists(unique):
|
|
1386
|
+
unique = f"{outdir}_{k}"; k += 1
|
|
1387
|
+
os.makedirs(unique, exist_ok=True)
|
|
1388
|
+
np.savetxt(os.path.join(unique, "distributed_spectrum.txt"),
|
|
1389
|
+
np.column_stack((r["E"], r["spectrum"])),
|
|
1390
|
+
header="E_gamma(MeV) TotalSpectrum", fmt="%.10e")
|
|
1391
|
+
np.savetxt(os.path.join(unique, "mass_distribution.txt"),
|
|
1392
|
+
np.asarray(r["mdist"], dtype=float),
|
|
1393
|
+
header="Sampled masses (g)", fmt="%.12e")
|
|
1394
|
+
print(f"Saved → {unique}")
|
|
1395
|
+
|
|
1396
|
+
|
|
1397
|
+
# ---------------------------
|
|
1398
|
+
# Helpers for Custom Equation: safe eval + variable prompting
|
|
1399
|
+
# ---------------------------
|
|
1400
|
+
def _build_safe_numpy_namespace() -> SimpleNamespace:
|
|
1401
|
+
"""
|
|
1402
|
+
Build a restricted numpy-like namespace exposing only safe math functions.
|
|
1403
|
+
|
|
1404
|
+
Returns
|
|
1405
|
+
-------
|
|
1406
|
+
SimpleNamespace
|
|
1407
|
+
Object exposing e.g. log, exp, sqrt, sin/cos/tan, etc.
|
|
1408
|
+
"""
|
|
1409
|
+
safe_np = SimpleNamespace(
|
|
1410
|
+
log=np.log, log10=np.log10, log1p=np.log1p, exp=np.exp, sqrt=np.sqrt, power=np.power,
|
|
1411
|
+
sin=np.sin, cos=np.cos, tan=np.tan, arctan=np.arctan,
|
|
1412
|
+
abs=np.abs, minimum=np.minimum, maximum=np.maximum, clip=np.clip, erf=erf,
|
|
1413
|
+
pi=np.pi, e=np.e
|
|
1414
|
+
)
|
|
1415
|
+
return safe_np
|
|
1416
|
+
|
|
1417
|
+
|
|
1418
|
+
SAFE_FUNCS = {
|
|
1419
|
+
"log","log10","log1p","exp","sqrt","pow","sin","cos","tan","arctan",
|
|
1420
|
+
"abs","minimum","maximum","clip","erf","pi","e","m","np","numpy"
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
def _detect_custom_variables(expr: str) -> list[str]:
|
|
1425
|
+
"""
|
|
1426
|
+
Detect identifiers in a user expression that are not known safe names.
|
|
1427
|
+
|
|
1428
|
+
Parameters
|
|
1429
|
+
----------
|
|
1430
|
+
expr : str
|
|
1431
|
+
RHS expression in variable `m` (grams).
|
|
1432
|
+
|
|
1433
|
+
Returns
|
|
1434
|
+
-------
|
|
1435
|
+
list[str]
|
|
1436
|
+
Sorted names of variables that require values from the user.
|
|
1437
|
+
|
|
1438
|
+
Notes
|
|
1439
|
+
-----
|
|
1440
|
+
- Greek letters like 'μ','α','β' are supported as identifiers.
|
|
1441
|
+
- Strings are stripped to avoid false positives.
|
|
1442
|
+
"""
|
|
1443
|
+
expr_wo_strings = re.sub(r"(\".*?\"|'.*?')", "", expr)
|
|
1444
|
+
tokens = set(re.findall(r"\b[^\W\d]\w*\b", expr_wo_strings, flags=re.UNICODE))
|
|
1445
|
+
unknown = sorted([t for t in tokens if t not in SAFE_FUNCS])
|
|
1446
|
+
return unknown
|
|
1447
|
+
|
|
1448
|
+
|
|
1449
|
+
def _prompt_variable_values(var_names: list[str]) -> dict[str, float]:
|
|
1450
|
+
"""
|
|
1451
|
+
Prompt the user for each variable value. Accepts numeric expressions
|
|
1452
|
+
using pi, e, and np.*.
|
|
1453
|
+
|
|
1454
|
+
Parameters
|
|
1455
|
+
----------
|
|
1456
|
+
var_names : list[str]
|
|
1457
|
+
Variables needing values.
|
|
1458
|
+
|
|
1459
|
+
Returns
|
|
1460
|
+
-------
|
|
1461
|
+
dict[str, float]
|
|
1462
|
+
Mapping from name → float value.
|
|
1463
|
+
|
|
1464
|
+
Raises
|
|
1465
|
+
------
|
|
1466
|
+
BackRequested
|
|
1467
|
+
If the user backs out.
|
|
1468
|
+
SystemExit
|
|
1469
|
+
If the user exits.
|
|
1470
|
+
"""
|
|
1471
|
+
vals: dict[str, float] = {}
|
|
1472
|
+
safe_np = _build_safe_numpy_namespace()
|
|
1473
|
+
num_ctx = {"__builtins__": None, "pi": np.pi, "e": np.e, "np": safe_np, "numpy": safe_np}
|
|
1474
|
+
for name in var_names:
|
|
1475
|
+
while True:
|
|
1476
|
+
try:
|
|
1477
|
+
s = user_input(f"Enter value for variable '{name}': ",
|
|
1478
|
+
allow_back=True, allow_exit=True).strip()
|
|
1479
|
+
val = eval(s, {"__builtins__": None}, num_ctx)
|
|
1480
|
+
val = float(val)
|
|
1481
|
+
vals[name] = val
|
|
1482
|
+
break
|
|
1483
|
+
except BackRequested:
|
|
1484
|
+
raise
|
|
1485
|
+
except SystemExit:
|
|
1486
|
+
raise
|
|
1487
|
+
except Exception:
|
|
1488
|
+
err("Could not parse value. Use a number or an expression like '1e16' or '2*np.pi'. Try again.")
|
|
1489
|
+
return vals
|
|
1490
|
+
|
|
1491
|
+
|
|
1492
|
+
# ---------------------------
|
|
1493
|
+
# Custom Mass PDF from user-entered EQUATION
|
|
1494
|
+
# ---------------------------
|
|
1495
|
+
def custom_equation_pdf_tool() -> None:
|
|
1496
|
+
"""
|
|
1497
|
+
Build a PBH mass PDF from a user-entered equation f(m, params...), normalize it per gram,
|
|
1498
|
+
sample N masses, accumulate ONLY the TOTAL spectrum, then show:
|
|
1499
|
+
|
|
1500
|
+
(1) total dN/dE,
|
|
1501
|
+
(2) total E^2 dN/dE,
|
|
1502
|
+
(3) mass histogram (counts) with analytic PDF scaled to counts (log bins).
|
|
1503
|
+
|
|
1504
|
+
Saved outputs (if requested)
|
|
1505
|
+
----------------------------
|
|
1506
|
+
- equation.txt : The expression and any variable values (commented).
|
|
1507
|
+
- samples_sorted.txt : Sorted sampled masses (g).
|
|
1508
|
+
- distributed_spectrum.txt : Columns: E_gamma(MeV), TotalSpectrum
|
|
1509
|
+
|
|
1510
|
+
Notes
|
|
1511
|
+
-----
|
|
1512
|
+
- The expression must be a *right-hand-side* function of `m` (no "f(m)=" prefix needed).
|
|
1513
|
+
- Allowed functions: subset from numpy (log/exp/sqrt/sin/cos/tan/arctan/abs/clip/min/max/erf).
|
|
1514
|
+
- Variables unknown to the safe namespace will be auto-detected and prompted for.
|
|
1515
|
+
"""
|
|
1516
|
+
# Discover data domain
|
|
1517
|
+
masses, names = discover_mass_folders(DATA_DIR)
|
|
1518
|
+
if masses:
|
|
1519
|
+
M_MIN, M_MAX = min(masses), max(masses)
|
|
1520
|
+
else:
|
|
1521
|
+
M_MIN, M_MAX = 5e13, 1e19
|
|
1522
|
+
|
|
1523
|
+
N_BINS = 50
|
|
1524
|
+
|
|
1525
|
+
def log_edges(a, b, k):
|
|
1526
|
+
return np.logspace(np.log10(a), np.log10(b), k + 1)
|
|
1527
|
+
|
|
1528
|
+
def safe_eval_on_grid(expr, m_grid, user_vars):
|
|
1529
|
+
safe_np = _build_safe_numpy_namespace()
|
|
1530
|
+
safe = {
|
|
1531
|
+
"m": m_grid,
|
|
1532
|
+
"log": np.log, "log10": np.log10, "log1p": np.log1p,
|
|
1533
|
+
"exp": np.exp, "sqrt": np.sqrt, "pow": np.power,
|
|
1534
|
+
"sin": np.sin, "cos": np.cos, "tan": np.tan, "arctan": np.arctan,
|
|
1535
|
+
"abs": np.abs, "minimum": np.minimum, "maximum": np.maximum, "clip": np.clip,
|
|
1536
|
+
"erf": erf, "pi": np.pi, "e": np.e,
|
|
1537
|
+
"np": safe_np, "numpy": safe_np
|
|
1538
|
+
}
|
|
1539
|
+
safe.update(user_vars)
|
|
1540
|
+
try:
|
|
1541
|
+
y = eval(expr, {"__builtins__": None}, safe)
|
|
1542
|
+
except BackRequested:
|
|
1543
|
+
raise
|
|
1544
|
+
except SystemExit:
|
|
1545
|
+
raise
|
|
1546
|
+
except Exception as e:
|
|
1547
|
+
raise ValueError(f"Could not evaluate expression: {e}")
|
|
1548
|
+
y = np.asarray(y, dtype=float)
|
|
1549
|
+
if y.size == 1:
|
|
1550
|
+
y = np.full_like(m_grid, float(y))
|
|
1551
|
+
if y.shape != m_grid.shape:
|
|
1552
|
+
raise ValueError("Expression did not return an array of the same shape as m.")
|
|
1553
|
+
return y
|
|
1554
|
+
|
|
1555
|
+
def cdf_from_pdf(m, pdf):
|
|
1556
|
+
cdf = np.empty_like(pdf)
|
|
1557
|
+
cdf[0] = 0.0
|
|
1558
|
+
dm = np.diff(m)
|
|
1559
|
+
cdf[1:] = np.cumsum(0.5 * (pdf[1:] + pdf[:-1]) * dm)
|
|
1560
|
+
total = cdf[-1]
|
|
1561
|
+
if not np.isfinite(total) or total <= 0:
|
|
1562
|
+
raise ValueError("PDF integrates to non-positive value.")
|
|
1563
|
+
cdf /= total
|
|
1564
|
+
return cdf
|
|
1565
|
+
|
|
1566
|
+
# ---- read the equation & prompt variables ----
|
|
1567
|
+
print("\n=== Custom Equation Mass PDF ===")
|
|
1568
|
+
print("Domain: m in [{:.2e}, {:.2e}] g".format(M_MIN, M_MAX))
|
|
1569
|
+
print("Enter a Python expression for your PDF f(m) using 'm' in grams and any constants/variables you define.")
|
|
1570
|
+
print("Examples:")
|
|
1571
|
+
print("f(m) = (m/mp)**(-(alpha0 + beta*log(m/mp))) / m")
|
|
1572
|
+
print("f(m) = exp(-m/5e17) / m")
|
|
1573
|
+
try:
|
|
1574
|
+
expr = user_input("f(m) = ", allow_back=True, allow_exit=True).strip()
|
|
1575
|
+
except BackRequested:
|
|
1576
|
+
return
|
|
1577
|
+
|
|
1578
|
+
# If someone pastes "f(m) = ..." or "fm = ...", strip the prefix anyway.
|
|
1579
|
+
expr = re.sub(r'^\s*(?:f\s*\(\s*m\s*\)|fm)\s*=\s*', '', expr, flags=re.IGNORECASE)
|
|
1580
|
+
|
|
1581
|
+
# Detect custom variables (excluding allowed function names, m, pi, e, np, numpy)
|
|
1582
|
+
vars_needed = _detect_custom_variables(expr)
|
|
1583
|
+
user_vars = {}
|
|
1584
|
+
if vars_needed:
|
|
1585
|
+
info(f"Variables detected: {', '.join(vars_needed)}")
|
|
1586
|
+
try:
|
|
1587
|
+
user_vars = _prompt_variable_values(vars_needed)
|
|
1588
|
+
except BackRequested:
|
|
1589
|
+
return
|
|
1590
|
+
|
|
1591
|
+
# ---- build normalized PDF on a fine m-grid ----
|
|
1592
|
+
m_grid = np.logspace(np.log10(M_MIN), np.log10(M_MAX), 20000)
|
|
1593
|
+
try:
|
|
1594
|
+
f = safe_eval_on_grid(expr, m_grid, user_vars)
|
|
1595
|
+
except BackRequested:
|
|
1596
|
+
return
|
|
1597
|
+
except ValueError as e:
|
|
1598
|
+
err(str(e))
|
|
1599
|
+
return
|
|
1600
|
+
|
|
1601
|
+
f = np.clip(f, 0.0, None)
|
|
1602
|
+
area = trapezoid(f, m_grid)
|
|
1603
|
+
if not np.isfinite(area) or area <= 0.0:
|
|
1604
|
+
err("Your f(m) is nonpositive or non-integrable over the domain.")
|
|
1605
|
+
return
|
|
1606
|
+
pdf = f / area # per gram
|
|
1607
|
+
cdf = cdf_from_pdf(m_grid, pdf)
|
|
1608
|
+
|
|
1609
|
+
# ---- ask for N ----
|
|
1610
|
+
try:
|
|
1611
|
+
n_default = 1000
|
|
1612
|
+
n_str = user_input(f"Enter target N (integer, e.g. 1000): ",
|
|
1613
|
+
allow_back=True, allow_exit=True).strip()
|
|
1614
|
+
if n_str == "":
|
|
1615
|
+
N = n_default
|
|
1616
|
+
else:
|
|
1617
|
+
N = int(n_str)
|
|
1618
|
+
if N <= 0:
|
|
1619
|
+
err("N must be > 0.")
|
|
1620
|
+
return
|
|
1621
|
+
except BackRequested:
|
|
1622
|
+
return
|
|
1623
|
+
except Exception:
|
|
1624
|
+
err("Invalid N (must be a positive integer).")
|
|
1625
|
+
return
|
|
1626
|
+
|
|
1627
|
+
# ---- pre-load spectral grids & splines ----
|
|
1628
|
+
if not masses:
|
|
1629
|
+
err("No valid mass folders found under the data directory.")
|
|
1630
|
+
return
|
|
1631
|
+
|
|
1632
|
+
first = load_spectra_components(os.path.join(DATA_DIR, names[0]))
|
|
1633
|
+
E_grid = first['energy_primary']
|
|
1634
|
+
logE = np.log(E_grid)
|
|
1635
|
+
N_M = len(masses)
|
|
1636
|
+
|
|
1637
|
+
direct_mat = np.zeros((N_M, len(E_grid)))
|
|
1638
|
+
secondary_mat = np.zeros_like(direct_mat)
|
|
1639
|
+
inflight_mat = np.zeros_like(direct_mat)
|
|
1640
|
+
final_mat = np.zeros_like(direct_mat)
|
|
1641
|
+
Emax_ifa = np.zeros(N_M)
|
|
1642
|
+
|
|
1643
|
+
for i, m in enumerate(masses):
|
|
1644
|
+
sub = os.path.join(DATA_DIR, names[i])
|
|
1645
|
+
S = load_spectra_components(sub)
|
|
1646
|
+
direct_mat[i] = S['direct_gamma_primary']
|
|
1647
|
+
secondary_mat[i] = np.interp(E_grid, S['energy_secondary'], S['direct_gamma_secondary'], left=0, right=0)
|
|
1648
|
+
inflight_mat[i] = S['IFA_primary'] + np.interp(E_grid, S['energy_secondary'], S['IFA_secondary'], left=0, right=0)
|
|
1649
|
+
final_mat[i] = S['FSR_primary'] + np.interp(E_grid, S['energy_secondary'], S['FSR_secondary'], left=0, right=0)
|
|
1650
|
+
|
|
1651
|
+
p = load_xy_lenient(os.path.join(sub, "inflight_annihilation_prim.txt"))
|
|
1652
|
+
s = load_xy_lenient(os.path.join(sub, "inflight_annihilation_sec.txt"))
|
|
1653
|
+
Emax_ifa[i] = max(p[:,0].max() if p.size else 0, s[:,0].max() if s.size else 0)
|
|
1654
|
+
|
|
1655
|
+
logM_all = np.log(masses)
|
|
1656
|
+
floor = 1e-300
|
|
1657
|
+
ld = np.log(np.where(direct_mat > floor, direct_mat, floor))
|
|
1658
|
+
ls = np.log(np.where(secondary_mat > floor, secondary_mat, floor))
|
|
1659
|
+
li = np.log(np.where(inflight_mat > floor, inflight_mat, floor))
|
|
1660
|
+
lf = np.log(np.where(final_mat > floor, final_mat, floor))
|
|
1661
|
+
|
|
1662
|
+
sp_d = RectBivariateSpline(logM_all, logE, ld, kx=1, ky=3, s=0)
|
|
1663
|
+
sp_s = RectBivariateSpline(logM_all, logE, ls, kx=1, ky=3, s=0)
|
|
1664
|
+
sp_i = RectBivariateSpline(logM_all, logE, li, kx=1, ky=3, s=0)
|
|
1665
|
+
sp_f = RectBivariateSpline(logM_all, logE, lf, kx=1, ky=3, s=0)
|
|
1666
|
+
|
|
1667
|
+
# ---- sample masses via inverse CDF and accumulate ONLY total spectrum ----
|
|
1668
|
+
rng = np.random.default_rng()
|
|
1669
|
+
u = rng.random(N)
|
|
1670
|
+
samples = np.interp(u, cdf, m_grid)
|
|
1671
|
+
samples.sort()
|
|
1672
|
+
|
|
1673
|
+
sum_tot = np.zeros_like(E_grid)
|
|
1674
|
+
|
|
1675
|
+
bar = tqdm(total=N, desc="Sampling custom PDF", unit="BH")
|
|
1676
|
+
for mraw in samples:
|
|
1677
|
+
if mraw < masses[0] or mraw > masses[-1]:
|
|
1678
|
+
bar.update(1)
|
|
1679
|
+
continue
|
|
1680
|
+
try:
|
|
1681
|
+
snap = snap_to_available(mraw, masses)
|
|
1682
|
+
mval = snap if snap else mraw
|
|
1683
|
+
idx_up = int(np.searchsorted(masses, mval, side='left'))
|
|
1684
|
+
idx_low = max(0, idx_up-1)
|
|
1685
|
+
idx_up = min(idx_up, len(masses)-1)
|
|
1686
|
+
Ecut = min(Emax_ifa[idx_low], Emax_ifa[idx_up])
|
|
1687
|
+
logm = np.log(mval)
|
|
1688
|
+
d_vals = np.exp(sp_d(logm, logE, grid=False))
|
|
1689
|
+
s_vals = np.exp(sp_s(logm, logE, grid=False))
|
|
1690
|
+
i_vals = np.exp(sp_i(logm, logE, grid=False))
|
|
1691
|
+
f_vals = np.exp(sp_f(logm, logE, grid=False))
|
|
1692
|
+
except Exception:
|
|
1693
|
+
bar.update(1)
|
|
1694
|
+
continue
|
|
1695
|
+
|
|
1696
|
+
# trim inflight tails (stability)
|
|
1697
|
+
for j in range(len(i_vals)-1, 0, -1):
|
|
1698
|
+
if np.isclose(i_vals[j], i_vals[j-1], rtol=1e-8):
|
|
1699
|
+
i_vals[j] = 0.0
|
|
1700
|
+
else:
|
|
1701
|
+
break
|
|
1702
|
+
log10i = np.log10(np.where(i_vals > 0, i_vals, floor))
|
|
1703
|
+
for j in range(1, len(log10i)):
|
|
1704
|
+
if log10i[j] - log10i[j-1] < -50:
|
|
1705
|
+
i_vals[j:] = 0.0
|
|
1706
|
+
break
|
|
1707
|
+
i_vals[E_grid >= Ecut] = 0.0
|
|
1708
|
+
|
|
1709
|
+
sum_tot += (d_vals + s_vals + i_vals + f_vals)
|
|
1710
|
+
bar.update(1)
|
|
1711
|
+
bar.close()
|
|
1712
|
+
|
|
1713
|
+
avg_tot = sum_tot / max(N, 1)
|
|
1714
|
+
avg_tot[avg_tot < 1e-299] = 0.0
|
|
1715
|
+
|
|
1716
|
+
# ---- FIGURE A: total dN/dE ----
|
|
1717
|
+
msk = avg_tot > 0
|
|
1718
|
+
plt.figure(figsize=(10, 7))
|
|
1719
|
+
plt.plot(E_grid[msk], avg_tot[msk], lw=2, label="Total spectrum")
|
|
1720
|
+
plt.xscale('log'); plt.yscale('log')
|
|
1721
|
+
plt.xlim(0.5, 5e3)
|
|
1722
|
+
if np.any(msk):
|
|
1723
|
+
peak = avg_tot[msk].max()
|
|
1724
|
+
plt.ylim(peak/1e3, peak*10)
|
|
1725
|
+
plt.xlabel(r'$E_\gamma$ (MeV)')
|
|
1726
|
+
plt.ylabel(r'$dN_\gamma/dE_\gamma$ (MeV$^{-1}$ s$^{-1}$)')
|
|
1727
|
+
plt.grid(True, which='both', linestyle='--')
|
|
1728
|
+
plt.legend()
|
|
1729
|
+
plt.title("Custom Equation — Total $dN/dE$")
|
|
1730
|
+
plt.tight_layout()
|
|
1731
|
+
plt.show()
|
|
1732
|
+
|
|
1733
|
+
# ---- FIGURE B: total E^2 dN/dE ----
|
|
1734
|
+
plt.figure(figsize=(10, 7))
|
|
1735
|
+
if np.any(msk):
|
|
1736
|
+
plt.plot(E_grid[msk], (E_grid[msk]**2) * avg_tot[msk], lw=2, label="Total")
|
|
1737
|
+
peak_e2 = ((E_grid[msk]**2) * avg_tot[msk]).max()
|
|
1738
|
+
plt.ylim(peak_e2/1e3, peak_e2*10)
|
|
1739
|
+
plt.xscale('log'); plt.yscale('log')
|
|
1740
|
+
plt.xlim(0.5, 5e3)
|
|
1741
|
+
plt.xlabel(r'$E_\gamma$ (MeV)')
|
|
1742
|
+
plt.ylabel(r'$E^2\,dN_\gamma/dE_\gamma$ (MeV s$^{-1}$)')
|
|
1743
|
+
plt.grid(True, which='both', linestyle='--')
|
|
1744
|
+
plt.legend()
|
|
1745
|
+
plt.title("Custom Equation — Total $E^2 dN/dE$")
|
|
1746
|
+
plt.tight_layout()
|
|
1747
|
+
plt.show()
|
|
1748
|
+
|
|
1749
|
+
# ---- FIGURE C: Mass histogram (counts) + SMOOTH analytic PDF scaled to counts ----
|
|
1750
|
+
edges = log_edges(masses[0], masses[-1], N_BINS)
|
|
1751
|
+
plt.figure(figsize=(10, 6))
|
|
1752
|
+
# Blue = sampled counts per (log) bin
|
|
1753
|
+
plt.hist(samples, bins=edges, density=False, alpha=0.6, edgecolor='k',
|
|
1754
|
+
label=f"Sampled counts per bin (N={N})")
|
|
1755
|
+
|
|
1756
|
+
# Orange = smooth line proportional to expected counts/bin for log bins:
|
|
1757
|
+
# expected counts in a narrow log bin: N * pdf(m) * m * d(ln m)
|
|
1758
|
+
dln = (np.log(masses[-1]) - np.log(masses[0])) / N_BINS
|
|
1759
|
+
counts_line = N * pdf * m_grid * dln
|
|
1760
|
+
plt.plot(m_grid, counts_line, lw=2.5, label="Analytic PDF (scaled to counts)")
|
|
1761
|
+
plt.xscale("log")
|
|
1762
|
+
plt.xlabel("Mass m (g)")
|
|
1763
|
+
plt.ylabel("Count per bin")
|
|
1764
|
+
plt.title("Custom Equation — Mass Histogram (counts) + Smooth PDF overlay")
|
|
1765
|
+
plt.grid(True, which='both', linestyle='--', alpha=0.5)
|
|
1766
|
+
plt.legend()
|
|
1767
|
+
plt.tight_layout()
|
|
1768
|
+
plt.show()
|
|
1769
|
+
|
|
1770
|
+
# ---- Save exactly 3 files for custom: equation, mass distribution, distributed spectrum ----
|
|
1771
|
+
try:
|
|
1772
|
+
sv = user_input("\nSave this custom spectrum? (y/n): ",
|
|
1773
|
+
allow_back=True, allow_exit=True).strip().lower()
|
|
1774
|
+
except BackRequested:
|
|
1775
|
+
return
|
|
1776
|
+
if sv in ('y', 'yes'):
|
|
1777
|
+
median_mass = float(np.median(samples)) if samples.size else 0.0
|
|
1778
|
+
folder = f"{median_mass:.2e}_custom_eq"
|
|
1779
|
+
outdir = os.path.join(CUSTOM_RESULTS_DIR, folder)
|
|
1780
|
+
base = outdir; k = 1
|
|
1781
|
+
while os.path.exists(outdir):
|
|
1782
|
+
outdir = f"{base}_{k}"; k += 1
|
|
1783
|
+
os.makedirs(outdir, exist_ok=True)
|
|
1784
|
+
|
|
1785
|
+
with open(os.path.join(outdir, "equation.txt"), "w", encoding="utf-8") as fh:
|
|
1786
|
+
if user_vars:
|
|
1787
|
+
fh.write("# Variables:\n")
|
|
1788
|
+
for kname, kval in user_vars.items():
|
|
1789
|
+
fh.write(f"# {kname} = {kval:.10e}\n")
|
|
1790
|
+
fh.write(expr + "\n")
|
|
1791
|
+
np.savetxt(os.path.join(outdir, "samples_sorted.txt"), samples,
|
|
1792
|
+
header="Simulated masses (g), sorted ascending", fmt="%.12e")
|
|
1793
|
+
np.savetxt(os.path.join(outdir, "distributed_spectrum.txt"),
|
|
1794
|
+
np.column_stack((E_grid, avg_tot)),
|
|
1795
|
+
header="E_gamma(MeV) TotalSpectrum", fmt="%.10e")
|
|
1796
|
+
print(f"Saved → {outdir}")
|
|
1797
|
+
|
|
1798
|
+
|
|
1799
|
+
# ---------------------------
|
|
1800
|
+
# View previous spectra (with queue)
|
|
1801
|
+
# ---------------------------
|
|
1802
|
+
def view_previous_spectra() -> None:
|
|
1803
|
+
"""
|
|
1804
|
+
View previously saved spectra with a queue:
|
|
1805
|
+
- Selecting items adds them to the queue only.
|
|
1806
|
+
- Press '0' to plot ALL queued items: spectra first (dN/dE, E^2 dN/dE), then histograms.
|
|
1807
|
+
- Queue auto-clears after plotting.
|
|
1808
|
+
"""
|
|
1809
|
+
# --- allowed mono input range (use discovered data domain) ---
|
|
1810
|
+
masses_all, names_all = discover_mass_folders(DATA_DIR)
|
|
1811
|
+
if masses_all:
|
|
1812
|
+
M_MIN_MONO, M_MAX_MONO = min(masses_all), max(masses_all)
|
|
1813
|
+
else:
|
|
1814
|
+
M_MIN_MONO, M_MAX_MONO = 5e13, 1e19
|
|
1815
|
+
|
|
1816
|
+
cat_map = {
|
|
1817
|
+
'1': ("Monochromatic Distribution", MONO_RESULTS_DIR, None, "mono"),
|
|
1818
|
+
'2': (GAUSSIAN_METHOD, GAUSS_RESULTS_DIR, "distributed_spectrum.txt", "gaussian"),
|
|
1819
|
+
'3': (NON_GAUSSIAN_METHOD, NGAUSS_RESULTS_DIR, "distributed_spectrum.txt", "non_gaussian"),
|
|
1820
|
+
'4': (LOGNORMAL_METHOD, LOGN_RESULTS_DIR, "distributed_spectrum.txt", "lognormal"),
|
|
1821
|
+
'5': ("Custom equation (user-defined mass PDF)", CUSTOM_RESULTS_DIR, "distributed_spectrum.txt", "custom"),
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
# ---------- helpers: text cleanup & equation parsing ----------
|
|
1825
|
+
def _strip_invisibles(s: str) -> str:
|
|
1826
|
+
for ch in (
|
|
1827
|
+
"\ufeff","\u200b","\u200c","\u200d","\u2060","\u200e","\u200f",
|
|
1828
|
+
"\u202a","\u202b","\u202c","\u202d","\u202e","\u202f",
|
|
1829
|
+
"\u00a0","\r"
|
|
1830
|
+
):
|
|
1831
|
+
s = s.replace(ch, "")
|
|
1832
|
+
return s
|
|
1833
|
+
|
|
1834
|
+
def _normalize_expr_line(s: str) -> str:
|
|
1835
|
+
s = _strip_invisibles(s.strip())
|
|
1836
|
+
s = re.sub(r'^\s*(?:f\s*\(\s*m\s*\)|fm)\s*=\s*','',s,flags=re.IGNORECASE)
|
|
1837
|
+
s = (s.replace('^','**')
|
|
1838
|
+
.replace('×','*')
|
|
1839
|
+
.replace('·','*')
|
|
1840
|
+
.replace('÷','/')
|
|
1841
|
+
.replace('−','-')
|
|
1842
|
+
.replace('—','-')
|
|
1843
|
+
.replace('–','-')
|
|
1844
|
+
.replace('“','"').replace('”','"')
|
|
1845
|
+
.replace('’',"'").replace('‘',"'"))
|
|
1846
|
+
out, in_sin, in_dbl = [], False, False
|
|
1847
|
+
for ch in s:
|
|
1848
|
+
if ch == "'" and not in_dbl:
|
|
1849
|
+
in_sin = not in_sin
|
|
1850
|
+
elif ch == '"' and not in_sin:
|
|
1851
|
+
in_dbl = not in_dbl
|
|
1852
|
+
if ch == '#' and not in_sin and not in_dbl:
|
|
1853
|
+
break
|
|
1854
|
+
out.append(ch)
|
|
1855
|
+
return ''.join(out).strip()
|
|
1856
|
+
|
|
1857
|
+
def _read_equation_file(run_dir: str) -> tuple[str, dict[str, float]]:
|
|
1858
|
+
eq_path = os.path.join(run_dir, "equation.txt")
|
|
1859
|
+
try:
|
|
1860
|
+
try:
|
|
1861
|
+
lines = open(eq_path,"r",encoding="utf-8-sig").readlines()
|
|
1862
|
+
except UnicodeDecodeError:
|
|
1863
|
+
lines = open(eq_path,"r",encoding="latin-1").readlines()
|
|
1864
|
+
except Exception as e:
|
|
1865
|
+
raise RuntimeError(f"Cannot read equation.txt: {e}")
|
|
1866
|
+
|
|
1867
|
+
user_vars: dict[str, float] = {}
|
|
1868
|
+
expr = None
|
|
1869
|
+
for raw in lines:
|
|
1870
|
+
s = _strip_invisibles(raw).strip()
|
|
1871
|
+
if not s:
|
|
1872
|
+
continue
|
|
1873
|
+
if s.startswith("#"):
|
|
1874
|
+
if "=" in s[1:]:
|
|
1875
|
+
try:
|
|
1876
|
+
k,v = s[1:].split("=",1)
|
|
1877
|
+
user_vars[k.strip()] = float(_strip_invisibles(v).strip())
|
|
1878
|
+
except Exception:
|
|
1879
|
+
pass
|
|
1880
|
+
continue
|
|
1881
|
+
norm = _normalize_expr_line(s)
|
|
1882
|
+
if norm:
|
|
1883
|
+
expr = norm
|
|
1884
|
+
if not expr:
|
|
1885
|
+
for raw in reversed(lines):
|
|
1886
|
+
s = raw.strip()
|
|
1887
|
+
if s and not s.lstrip().startswith("#"):
|
|
1888
|
+
s = _normalize_expr_line(s)
|
|
1889
|
+
if s:
|
|
1890
|
+
expr = s
|
|
1891
|
+
break
|
|
1892
|
+
if not expr:
|
|
1893
|
+
raise RuntimeError("No custom equation found in equation.txt.")
|
|
1894
|
+
return expr, user_vars
|
|
1895
|
+
|
|
1896
|
+
def _safe_eval_on_grid(expr: str, m_grid: np.ndarray, user_vars: dict[str, float]) -> np.ndarray:
|
|
1897
|
+
safe_np = _build_safe_numpy_namespace()
|
|
1898
|
+
safe = {
|
|
1899
|
+
"m": m_grid,
|
|
1900
|
+
"log": np.log, "log10": np.log10, "log1p": np.log1p,
|
|
1901
|
+
"exp": np.exp, "sqrt": np.sqrt, "pow": np.power,
|
|
1902
|
+
"sin": np.sin, "cos": np.cos, "tan": np.tan, "arctan": np.arctan,
|
|
1903
|
+
"abs": np.abs, "minimum": np.minimum, "maximum": np.maximum, "clip": np.clip,
|
|
1904
|
+
"erf": erf, "pi": np.pi, "e": np.e,
|
|
1905
|
+
"np": _build_safe_numpy_namespace(), "numpy": _build_safe_numpy_namespace()
|
|
1906
|
+
}
|
|
1907
|
+
safe.update(user_vars)
|
|
1908
|
+
y = eval(expr, {"__builtins__": None}, safe)
|
|
1909
|
+
y = np.asarray(y, dtype=float)
|
|
1910
|
+
if y.size == 1:
|
|
1911
|
+
y = np.full_like(m_grid, float(y))
|
|
1912
|
+
if y.shape != m_grid.shape:
|
|
1913
|
+
raise ValueError("Expression did not return an array of the same shape as m.")
|
|
1914
|
+
return y
|
|
1915
|
+
|
|
1916
|
+
# ---------- parse run_name for peak and sigma ----------
|
|
1917
|
+
def _extract_peak_sigma(run_name: str, kind: str) -> tuple[float | None, float | None, str | None]:
|
|
1918
|
+
peak_val = None
|
|
1919
|
+
sigma_val = None
|
|
1920
|
+
sigma_str = None
|
|
1921
|
+
m_peak = re.search(r"peak_([0-9.+\-eE]+)", run_name)
|
|
1922
|
+
if m_peak:
|
|
1923
|
+
try:
|
|
1924
|
+
peak_val = float(m_peak.group(1))
|
|
1925
|
+
except Exception:
|
|
1926
|
+
peak_val = None
|
|
1927
|
+
if kind == "non_gaussian":
|
|
1928
|
+
m_sigx = re.search(r"σX([0-9.]+)", run_name)
|
|
1929
|
+
if m_sigx:
|
|
1930
|
+
try:
|
|
1931
|
+
sigma_val = float(m_sigx.group(1))
|
|
1932
|
+
except Exception:
|
|
1933
|
+
sigma_val = None
|
|
1934
|
+
sigma_str = f"σX={sigma_val:.3g}" if sigma_val is not None else "σX=?"
|
|
1935
|
+
else:
|
|
1936
|
+
m_sig = re.search(r"σ([0-9.]+)", run_name)
|
|
1937
|
+
if m_sig:
|
|
1938
|
+
try:
|
|
1939
|
+
sigma_val = float(m_sig.group(1))
|
|
1940
|
+
except Exception:
|
|
1941
|
+
sigma_val = None
|
|
1942
|
+
sigma_str = f"σ={sigma_val:.3g}" if sigma_val is not None else "σ=?"
|
|
1943
|
+
return peak_val, sigma_val, sigma_str
|
|
1944
|
+
|
|
1945
|
+
# ---------- plotting helpers (updated sizes) ----------
|
|
1946
|
+
def _plot_dn(results: list[tuple[str, tuple[np.ndarray, np.ndarray]]]) -> None:
|
|
1947
|
+
if not results:
|
|
1948
|
+
return
|
|
1949
|
+
fig = plt.figure(figsize=(10,7))
|
|
1950
|
+
peaks = []
|
|
1951
|
+
for lab, (E, S) in results:
|
|
1952
|
+
msk = S > 0
|
|
1953
|
+
plt.plot(E[msk], S[msk], lw=2, label=lab)
|
|
1954
|
+
if np.any(msk):
|
|
1955
|
+
peaks.append(S[msk].max())
|
|
1956
|
+
plt.xscale('log'); plt.yscale('log')
|
|
1957
|
+
plt.xlabel(r'$E_\gamma$ (MeV)')
|
|
1958
|
+
plt.ylabel(r'$dN_\gamma/dE_\gamma$')
|
|
1959
|
+
if peaks:
|
|
1960
|
+
plt.ylim(min(peaks)/1e3, max(peaks)*10)
|
|
1961
|
+
plt.xlim(0.5, 5e3)
|
|
1962
|
+
plt.grid(True, which='both', linestyle='--')
|
|
1963
|
+
plt.legend()
|
|
1964
|
+
plt.title("Comparison: dN/dE")
|
|
1965
|
+
plt.tight_layout()
|
|
1966
|
+
plt.show()
|
|
1967
|
+
plt.close(fig)
|
|
1968
|
+
|
|
1969
|
+
def _plot_e2(results: list[tuple[str, tuple[np.ndarray, np.ndarray]]]) -> None:
|
|
1970
|
+
if not results:
|
|
1971
|
+
return
|
|
1972
|
+
fig = plt.figure(figsize=(10,7))
|
|
1973
|
+
peaks = []
|
|
1974
|
+
for lab, (E, S) in results:
|
|
1975
|
+
msk = S > 0
|
|
1976
|
+
plt.plot(E[msk], (E[msk]**2)*S[msk], lw=2, label=lab)
|
|
1977
|
+
if np.any(msk):
|
|
1978
|
+
peaks.append(((E[msk]**2)*S[msk]).max())
|
|
1979
|
+
plt.xscale('log'); plt.yscale('log')
|
|
1980
|
+
plt.xlabel(r'$E_\gamma$ (MeV)')
|
|
1981
|
+
plt.ylabel(r'$E^2\,dN_\gamma/dE_\gamma$')
|
|
1982
|
+
if peaks:
|
|
1983
|
+
plt.ylim(min(peaks)/1e3, max(peaks)*10)
|
|
1984
|
+
plt.xlim(0.5, 5e3)
|
|
1985
|
+
plt.grid(True, which='both', linestyle='--')
|
|
1986
|
+
plt.legend()
|
|
1987
|
+
plt.title("Comparison: $E^2$ dN/dE")
|
|
1988
|
+
plt.tight_layout()
|
|
1989
|
+
plt.show()
|
|
1990
|
+
plt.close(fig)
|
|
1991
|
+
|
|
1992
|
+
def _hist_gaussian(samples, peak, sigma_x, title_prefix):
|
|
1993
|
+
"""Histogram + counts-scaled Gaussian-collapse PDF overlay (bigger figure)."""
|
|
1994
|
+
md = np.asarray(samples, dtype=float)
|
|
1995
|
+
md = md[np.isfinite(md)]
|
|
1996
|
+
if md.size == 0:
|
|
1997
|
+
return
|
|
1998
|
+
plt.figure(figsize=(10,6)) # <-- bigger now
|
|
1999
|
+
if md.size < 2 or md.min() == md.max():
|
|
2000
|
+
center = md[0]
|
|
2001
|
+
eps = abs(center)*1e-9 if center != 0 else 1e-9
|
|
2002
|
+
_, bins, _ = plt.hist(md, bins=1, range=(center-eps, center+eps),
|
|
2003
|
+
alpha=0.7, edgecolor='k', label='samples')
|
|
2004
|
+
else:
|
|
2005
|
+
q25, q75 = np.percentile(md, [25, 75])
|
|
2006
|
+
iqr = q75 - q25
|
|
2007
|
+
if iqr > 0:
|
|
2008
|
+
bw = 2 * iqr * md.size ** (-1/3)
|
|
2009
|
+
k = int(np.clip(np.ceil((md.max() - md.min()) / bw), 1, 50))
|
|
2010
|
+
else:
|
|
2011
|
+
k = int(np.clip(np.sqrt(md.size), 1, 50))
|
|
2012
|
+
_, bins, _ = plt.hist(md, bins=k, alpha=0.7, edgecolor='k', label='samples')
|
|
2013
|
+
|
|
2014
|
+
x = np.linspace(0.001, 1.30909, 2000)
|
|
2015
|
+
mf = mass_function(delta_l(x,3.3,0.59,0.36), sigma_x, 0.59, 0.36)
|
|
2016
|
+
mf = np.where(np.isfinite(mf) & (mf>0), mf, 0.0)
|
|
2017
|
+
if mf.sum() > 0:
|
|
2018
|
+
probabilities = mf / mf.sum()
|
|
2019
|
+
r_mode = x[np.argmax(mf)] if np.any(mf) else x[len(x)//2]
|
|
2020
|
+
scale = peak / r_mode if peak is not None else 1.0
|
|
2021
|
+
dx = x[1] - x[0]
|
|
2022
|
+
dm = dx * scale
|
|
2023
|
+
pdf_mass = probabilities / dm
|
|
2024
|
+
m_line = x * scale
|
|
2025
|
+
bin_widths = (bins[1:] - bins[:-1])
|
|
2026
|
+
ref_width = float(np.median(bin_widths)) if bin_widths.size else 1.0
|
|
2027
|
+
mask = ((m_line >= bins[0]) & (m_line <= bins[-1]) &
|
|
2028
|
+
np.isfinite(pdf_mass) & (pdf_mass > 0))
|
|
2029
|
+
if np.any(mask):
|
|
2030
|
+
y_line = pdf_mass[mask] * ref_width * len(md)
|
|
2031
|
+
plt.plot(m_line[mask], y_line, 'r--', lw=2, zorder=3,
|
|
2032
|
+
label='Underlying PDF (counts)')
|
|
2033
|
+
|
|
2034
|
+
plt.xlabel('Simulated PBH Mass (g)')
|
|
2035
|
+
plt.ylabel('Count')
|
|
2036
|
+
plt.title(f'{title_prefix} — Mass Distribution & PDF overlay')
|
|
2037
|
+
plt.grid(True, which='both', linestyle='--')
|
|
2038
|
+
plt.legend()
|
|
2039
|
+
plt.tight_layout()
|
|
2040
|
+
plt.show()
|
|
2041
|
+
|
|
2042
|
+
def _hist_nongaussian(samples, peak, sigma_X, title_prefix):
|
|
2043
|
+
"""Histogram + counts-scaled Non-Gaussian PDF overlay (bigger figure)."""
|
|
2044
|
+
md = np.asarray(samples, dtype=float)
|
|
2045
|
+
md = md[np.isfinite(md)]
|
|
2046
|
+
if md.size == 0:
|
|
2047
|
+
return
|
|
2048
|
+
plt.figure(figsize=(10,6)) # <-- bigger now
|
|
2049
|
+
if md.size < 2 or md.min() == md.max():
|
|
2050
|
+
center = md[0]
|
|
2051
|
+
eps = abs(center)*1e-9 if center != 0 else 1e-9
|
|
2052
|
+
_, bins, _ = plt.hist(md, bins=1, range=(center-eps, center+eps),
|
|
2053
|
+
alpha=0.7, edgecolor='k', label='samples')
|
|
2054
|
+
else:
|
|
2055
|
+
q25, q75 = np.percentile(md, [25, 75])
|
|
2056
|
+
iqr = q75 - q25
|
|
2057
|
+
if iqr > 0:
|
|
2058
|
+
bw = 2 * iqr * md.size ** (-1/3)
|
|
2059
|
+
k = int(np.clip(np.ceil((md.max() - md.min()) / bw), 1, 50))
|
|
2060
|
+
else:
|
|
2061
|
+
k = int(np.clip(np.sqrt(md.size), 1, 50))
|
|
2062
|
+
_, bins, _ = plt.hist(md, bins=k, alpha=0.7, edgecolor='k', label='samples')
|
|
2063
|
+
|
|
2064
|
+
x = np.linspace(0.001, 1.30909, 2000)
|
|
2065
|
+
sigma_Y = 0.75 * (sigma_X if sigma_X is not None else 0.0)
|
|
2066
|
+
mf = mass_function_exact(delta_l(x,3.3,0.59,0.36),
|
|
2067
|
+
sigma_X if sigma_X is not None else 0.0,
|
|
2068
|
+
sigma_Y,
|
|
2069
|
+
0.59, 0.36)
|
|
2070
|
+
mf = np.where(np.isfinite(mf) & (mf>0), mf, 0.0)
|
|
2071
|
+
if mf.sum() > 0:
|
|
2072
|
+
probabilities = mf / mf.sum()
|
|
2073
|
+
r_mode = x[np.argmax(mf)] if np.any(mf) else x[len(x)//2]
|
|
2074
|
+
scale = peak / r_mode if peak is not None else 1.0
|
|
2075
|
+
dx = x[1] - x[0]
|
|
2076
|
+
dm = dx * scale
|
|
2077
|
+
pdf_mass = probabilities / dm
|
|
2078
|
+
m_line = x * scale
|
|
2079
|
+
bin_widths = (bins[1:] - bins[:-1])
|
|
2080
|
+
ref_width = float(np.median(bin_widths)) if bin_widths.size else 1.0
|
|
2081
|
+
mask = ((m_line >= bins[0]) & (m_line <= bins[-1]) &
|
|
2082
|
+
np.isfinite(pdf_mass) & (pdf_mass > 0))
|
|
2083
|
+
if np.any(mask):
|
|
2084
|
+
y_line = pdf_mass[mask] * ref_width * len(md)
|
|
2085
|
+
plt.plot(m_line[mask], y_line, 'r--', lw=2, zorder=3,
|
|
2086
|
+
label='Underlying PDF (counts)')
|
|
2087
|
+
|
|
2088
|
+
plt.xlabel('Simulated PBH Mass (g)')
|
|
2089
|
+
plt.ylabel('Count')
|
|
2090
|
+
plt.title(f'{title_prefix} — Mass Distribution & PDF overlay')
|
|
2091
|
+
plt.grid(True, which='both', linestyle='--')
|
|
2092
|
+
plt.legend()
|
|
2093
|
+
plt.tight_layout()
|
|
2094
|
+
plt.show()
|
|
2095
|
+
|
|
2096
|
+
def _hist_lognormal(samples, peak, sigma_ln, title_prefix):
|
|
2097
|
+
"""Histogram + counts-scaled Log-normal PDF overlay (bigger figure)."""
|
|
2098
|
+
md = np.asarray(samples, dtype=float)
|
|
2099
|
+
md = md[np.isfinite(md)]
|
|
2100
|
+
if md.size == 0:
|
|
2101
|
+
return
|
|
2102
|
+
plt.figure(figsize=(10,6)) # <-- bigger now
|
|
2103
|
+
if md.size < 2 or md.min() == md.max():
|
|
2104
|
+
center = md[0]
|
|
2105
|
+
eps = abs(center)*1e-9 if center != 0 else 1e-9
|
|
2106
|
+
_, bins, _ = plt.hist(md, bins=1, range=(center-eps, center+eps),
|
|
2107
|
+
alpha=0.7, edgecolor='k', label='samples')
|
|
2108
|
+
else:
|
|
2109
|
+
q25, q75 = np.percentile(md, [25, 75])
|
|
2110
|
+
iqr = q75 - q25
|
|
2111
|
+
if iqr > 0:
|
|
2112
|
+
bw = 2 * iqr * md.size ** (-1/3)
|
|
2113
|
+
k = int(np.clip(np.ceil((md.max() - md.min()) / bw), 1, 50))
|
|
2114
|
+
else:
|
|
2115
|
+
k = int(np.clip(np.sqrt(md.size), 1, 50))
|
|
2116
|
+
_, bins, _ = plt.hist(md, bins=k, alpha=0.7, edgecolor='k', label='samples')
|
|
2117
|
+
|
|
2118
|
+
bin_widths = (bins[1:] - bins[:-1])
|
|
2119
|
+
ref_width = float(np.median(bin_widths)) if bin_widths.size else 1.0
|
|
2120
|
+
|
|
2121
|
+
mu_eff = None
|
|
2122
|
+
if peak is not None and sigma_ln is not None:
|
|
2123
|
+
mu_eff = np.log(peak) + sigma_ln**2
|
|
2124
|
+
|
|
2125
|
+
if mu_eff is not None:
|
|
2126
|
+
mlo_tail = np.exp(mu_eff - 6.0*sigma_ln)
|
|
2127
|
+
mhi_tail = np.exp(mu_eff + 6.0*sigma_ln)
|
|
2128
|
+
m_plot = np.logspace(np.log10(min(bins[0], mlo_tail)),
|
|
2129
|
+
np.log10(max(bins[-1], mhi_tail)),
|
|
2130
|
+
2000)
|
|
2131
|
+
pdf = (1.0/(m_plot*sigma_ln*np.sqrt(2*np.pi))) * np.exp(
|
|
2132
|
+
- (np.log(m_plot)-mu_eff)**2 / (2*sigma_ln**2)
|
|
2133
|
+
)
|
|
2134
|
+
y_plot = pdf * ref_width * len(md)
|
|
2135
|
+
plt.plot(m_plot, y_plot, 'r--', lw=2, zorder=3,
|
|
2136
|
+
label='Underlying PDF (counts)')
|
|
2137
|
+
|
|
2138
|
+
plt.xlabel('Simulated PBH Mass (g)')
|
|
2139
|
+
plt.ylabel('Count')
|
|
2140
|
+
plt.title(f'{title_prefix} — Mass Distribution & PDF overlay')
|
|
2141
|
+
plt.grid(True, which='both', linestyle='--')
|
|
2142
|
+
plt.legend()
|
|
2143
|
+
plt.tight_layout()
|
|
2144
|
+
plt.show()
|
|
2145
|
+
|
|
2146
|
+
def _hist_custom(run_dir, title_prefix):
|
|
2147
|
+
"""Histogram for a custom-equation run (size already large at 10×6 in generator)."""
|
|
2148
|
+
spath = os.path.join(run_dir, "samples_sorted.txt")
|
|
2149
|
+
try:
|
|
2150
|
+
samples = np.loadtxt(spath)
|
|
2151
|
+
except Exception:
|
|
2152
|
+
return
|
|
2153
|
+
samples = np.asarray(samples, dtype=float)
|
|
2154
|
+
samples = samples[np.isfinite(samples)]
|
|
2155
|
+
if samples.size == 0:
|
|
2156
|
+
return
|
|
2157
|
+
|
|
2158
|
+
try:
|
|
2159
|
+
expr, user_vars = _read_equation_file(run_dir)
|
|
2160
|
+
except Exception:
|
|
2161
|
+
expr = None
|
|
2162
|
+
user_vars = {}
|
|
2163
|
+
|
|
2164
|
+
masses_all2, _ = discover_mass_folders(DATA_DIR)
|
|
2165
|
+
if masses_all2:
|
|
2166
|
+
M_MIN, M_MAX = min(masses_all2), max(masses_all2)
|
|
2167
|
+
else:
|
|
2168
|
+
M_MIN, M_MAX = 5e13, 1e19
|
|
2169
|
+
|
|
2170
|
+
pdf = None
|
|
2171
|
+
if expr is not None:
|
|
2172
|
+
m_grid = np.logspace(np.log10(M_MIN), np.log10(M_MAX), 20000)
|
|
2173
|
+
try:
|
|
2174
|
+
f = _safe_eval_on_grid(expr, m_grid, user_vars)
|
|
2175
|
+
except Exception:
|
|
2176
|
+
f = None
|
|
2177
|
+
if f is not None:
|
|
2178
|
+
f = np.clip(f, 0.0, None)
|
|
2179
|
+
area = trapezoid(f, m_grid)
|
|
2180
|
+
if np.isfinite(area) and area > 0:
|
|
2181
|
+
pdf = f / area
|
|
2182
|
+
|
|
2183
|
+
N_BINS = 50
|
|
2184
|
+
edges = np.logspace(np.log10(M_MIN), np.log10(M_MAX), N_BINS + 1)
|
|
2185
|
+
plt.figure(figsize=(10,6))
|
|
2186
|
+
plt.hist(samples, bins=edges, density=False, alpha=0.6, edgecolor='k',
|
|
2187
|
+
label=f"Sampled counts per bin (N={len(samples)})")
|
|
2188
|
+
|
|
2189
|
+
if pdf is not None:
|
|
2190
|
+
dln = (np.log(M_MAX) - np.log(M_MIN)) / N_BINS
|
|
2191
|
+
counts_line = len(samples) * pdf * m_grid * dln
|
|
2192
|
+
plt.plot(m_grid, counts_line, lw=2.5, label="Analytic PDF (scaled to counts)")
|
|
2193
|
+
|
|
2194
|
+
plt.xscale("log")
|
|
2195
|
+
plt.xlabel("Mass m (g)")
|
|
2196
|
+
plt.ylabel("Count per bin")
|
|
2197
|
+
plt.title(f"{title_prefix} — Mass Histogram (counts) + Smooth PDF")
|
|
2198
|
+
plt.grid(True, which='both', linestyle='--', alpha=0.5)
|
|
2199
|
+
plt.legend()
|
|
2200
|
+
plt.tight_layout()
|
|
2201
|
+
plt.show()
|
|
2202
|
+
|
|
2203
|
+
# ---------- UI ----------
|
|
2204
|
+
def _print_menu():
|
|
2205
|
+
print("\nView Previous — choose:")
|
|
2206
|
+
print(" 1: Monochromatic Distribution")
|
|
2207
|
+
print(f" 2: {GAUSSIAN_METHOD}")
|
|
2208
|
+
print(f" 3: {NON_GAUSSIAN_METHOD}")
|
|
2209
|
+
print(f" 4: {LOGNORMAL_METHOD}")
|
|
2210
|
+
print(f" 5: Custom equation (user-defined mass PDF)")
|
|
2211
|
+
print(" 0: Plot all Queued | b: Back | q: Quit")
|
|
2212
|
+
|
|
2213
|
+
# queue elements:
|
|
2214
|
+
queue: list[dict] = []
|
|
2215
|
+
|
|
2216
|
+
# Preload matrices/splines once for fast monochromatic interpolation in this view
|
|
2217
|
+
mono_ready = False
|
|
2218
|
+
mono_E = None
|
|
2219
|
+
sp_d = sp_s = sp_i = sp_f = None
|
|
2220
|
+
logE = None
|
|
2221
|
+
Emax_ifa = None
|
|
2222
|
+
floor = 1e-300
|
|
2223
|
+
N_M = 0
|
|
2224
|
+
|
|
2225
|
+
def _ensure_mono_interpolator():
|
|
2226
|
+
nonlocal mono_ready, mono_E, sp_d, sp_s, sp_i, sp_f, logE, Emax_ifa, N_M
|
|
2227
|
+
if mono_ready:
|
|
2228
|
+
return
|
|
2229
|
+
if not masses_all:
|
|
2230
|
+
raise RuntimeError("No valid mass folders found under data directory.")
|
|
2231
|
+
first = load_spectra_components(os.path.join(DATA_DIR, names_all[0]))
|
|
2232
|
+
mono_E = first['energy_primary']
|
|
2233
|
+
logE = np.log(mono_E)
|
|
2234
|
+
N_M = len(masses_all)
|
|
2235
|
+
|
|
2236
|
+
direct_mat = np.zeros((N_M, len(mono_E)))
|
|
2237
|
+
secondary_mat = np.zeros_like(direct_mat)
|
|
2238
|
+
inflight_mat = np.zeros_like(direct_mat)
|
|
2239
|
+
final_mat = np.zeros_like(direct_mat)
|
|
2240
|
+
Emax_ifa = np.zeros(N_M)
|
|
2241
|
+
|
|
2242
|
+
for i, m in enumerate(masses_all):
|
|
2243
|
+
sub = os.path.join(DATA_DIR, names_all[i])
|
|
2244
|
+
S = load_spectra_components(sub)
|
|
2245
|
+
direct_mat[i] = S['direct_gamma_primary']
|
|
2246
|
+
secondary_mat[i] = np.interp(mono_E, S['energy_secondary'], S['direct_gamma_secondary'], left=0, right=0)
|
|
2247
|
+
inflight_mat[i] = S['IFA_primary'] + np.interp(mono_E, S['energy_secondary'], S['IFA_secondary'], left=0, right=0)
|
|
2248
|
+
final_mat[i] = S['FSR_primary'] + np.interp(mono_E, S['energy_secondary'], S['FSR_secondary'], left=0, right=0)
|
|
2249
|
+
p = load_xy_lenient(os.path.join(sub, "inflight_annihilation_prim.txt"))
|
|
2250
|
+
s = load_xy_lenient(os.path.join(sub, "inflight_annihilation_sec.txt"))
|
|
2251
|
+
Emax_ifa[i] = max(p[:,0].max() if p.size else 0, s[:,0].max() if s.size else 0)
|
|
2252
|
+
|
|
2253
|
+
logM_all = np.log(masses_all)
|
|
2254
|
+
ld = np.log(np.where(direct_mat > floor, direct_mat, floor))
|
|
2255
|
+
ls = np.log(np.where(secondary_mat > floor, secondary_mat, floor))
|
|
2256
|
+
li = np.log(np.where(inflight_mat > floor, inflight_mat, floor))
|
|
2257
|
+
lf = np.log(np.where(final_mat > floor, final_mat, floor))
|
|
2258
|
+
|
|
2259
|
+
sp_d = RectBivariateSpline(logM_all, logE, ld, kx=1, ky=3, s=0)
|
|
2260
|
+
sp_s = RectBivariateSpline(logM_all, logE, ls, kx=1, ky=3, s=0)
|
|
2261
|
+
sp_i = RectBivariateSpline(logM_all, logE, li, kx=1, ky=3, s=0)
|
|
2262
|
+
sp_f = RectBivariateSpline(logM_all, logE, lf, kx=1, ky=3, s=0)
|
|
2263
|
+
mono_ready = True
|
|
2264
|
+
|
|
2265
|
+
while True:
|
|
2266
|
+
_print_menu()
|
|
2267
|
+
try:
|
|
2268
|
+
choice = user_input("Choice: ", allow_back=True, allow_exit=True).strip().lower()
|
|
2269
|
+
except BackRequested:
|
|
2270
|
+
return
|
|
2271
|
+
# Handle feeders that don't raise BackRequested / SystemExit
|
|
2272
|
+
if choice in ("b", "back"):
|
|
2273
|
+
return
|
|
2274
|
+
if choice in ("q", "exit"):
|
|
2275
|
+
return # don't sys.exit() here; returning keeps tests happy
|
|
2276
|
+
|
|
2277
|
+
# ----- plot all queued -----
|
|
2278
|
+
if choice == '0':
|
|
2279
|
+
if not queue:
|
|
2280
|
+
warn("Queue is empty.")
|
|
2281
|
+
continue
|
|
2282
|
+
plot_pack = [(item['label'], (item['E'], item['S'])) for item in queue]
|
|
2283
|
+
_plot_dn(plot_pack)
|
|
2284
|
+
_plot_e2(plot_pack)
|
|
2285
|
+
|
|
2286
|
+
for item in queue:
|
|
2287
|
+
h = item.get('hist')
|
|
2288
|
+
if not h: # monochromatic has no histogram
|
|
2289
|
+
continue
|
|
2290
|
+
kind = h['kind']
|
|
2291
|
+
run_dir = h['run_dir']
|
|
2292
|
+
label_for_hist = item['label']
|
|
2293
|
+
peak_val = h.get('peak')
|
|
2294
|
+
sigma_val = h.get('sigma') # for non_gaussian this is sigma_X
|
|
2295
|
+
try:
|
|
2296
|
+
if kind in ("gaussian", "non_gaussian", "lognormal"):
|
|
2297
|
+
md_path = os.path.join(run_dir, "mass_distribution.txt")
|
|
2298
|
+
md = np.loadtxt(md_path)
|
|
2299
|
+
if kind == "gaussian":
|
|
2300
|
+
_hist_gaussian(md,
|
|
2301
|
+
peak_val if peak_val is not None else np.median(md),
|
|
2302
|
+
sigma_val if sigma_val is not None else 0.05,
|
|
2303
|
+
label_for_hist)
|
|
2304
|
+
elif kind == "non_gaussian":
|
|
2305
|
+
_hist_nongaussian(md,
|
|
2306
|
+
peak_val if peak_val is not None else np.median(md),
|
|
2307
|
+
sigma_val if sigma_val is not None else 0.08,
|
|
2308
|
+
label_for_hist)
|
|
2309
|
+
else: # lognormal
|
|
2310
|
+
_hist_lognormal(md,
|
|
2311
|
+
peak_val if peak_val is not None else np.median(md),
|
|
2312
|
+
sigma_val if sigma_val is not None else 1.0,
|
|
2313
|
+
label_for_hist)
|
|
2314
|
+
elif kind == "custom":
|
|
2315
|
+
_hist_custom(run_dir, label_for_hist)
|
|
2316
|
+
except FileNotFoundError as e:
|
|
2317
|
+
warn(f"Histogram inputs missing in {run_dir}: {e}")
|
|
2318
|
+
except Exception as e:
|
|
2319
|
+
warn(f"Histogram reconstruction failed for {run_dir}: {e}")
|
|
2320
|
+
|
|
2321
|
+
queue.clear()
|
|
2322
|
+
info("Queue cleared.")
|
|
2323
|
+
continue
|
|
2324
|
+
|
|
2325
|
+
if choice not in cat_map:
|
|
2326
|
+
warn("Invalid choice; try again.")
|
|
2327
|
+
continue
|
|
2328
|
+
|
|
2329
|
+
label_group, root, spec_file, kind = cat_map[choice]
|
|
2330
|
+
|
|
2331
|
+
# ----- NEW Monochromatic branch (no listing, no rounding; interpolate & queue) -----
|
|
2332
|
+
if kind == "mono":
|
|
2333
|
+
print(f"Enter PBH masses (g) to QUEUE for monochromatic plots (range [{M_MIN_MONO:.2e}, {M_MAX_MONO:.2e}]).")
|
|
2334
|
+
try:
|
|
2335
|
+
mstr = user_input("Masses (comma-separated): ", allow_back=True, allow_exit=True).strip()
|
|
2336
|
+
except BackRequested:
|
|
2337
|
+
continue
|
|
2338
|
+
if not mstr:
|
|
2339
|
+
continue
|
|
2340
|
+
|
|
2341
|
+
# build interpolators once
|
|
2342
|
+
try:
|
|
2343
|
+
_ensure_mono_interpolator()
|
|
2344
|
+
except Exception as e:
|
|
2345
|
+
err(f"Cannot prepare interpolator: {e}")
|
|
2346
|
+
continue
|
|
2347
|
+
|
|
2348
|
+
req_masses = parse_float_list_verbose(
|
|
2349
|
+
mstr, name="mass (g)", bounds=(M_MIN_MONO, M_MAX_MONO), allow_empty=False
|
|
2350
|
+
)
|
|
2351
|
+
if not req_masses:
|
|
2352
|
+
continue
|
|
2353
|
+
|
|
2354
|
+
for mval in req_masses:
|
|
2355
|
+
try:
|
|
2356
|
+
# no snapping: always interpolate in (logM, logE)
|
|
2357
|
+
idx_up = int(np.searchsorted(masses_all, mval, side='left'))
|
|
2358
|
+
idx_low = max(0, idx_up-1)
|
|
2359
|
+
idx_up = min(idx_up, len(masses_all)-1)
|
|
2360
|
+
Ecut = min(Emax_ifa[idx_low], Emax_ifa[idx_up])
|
|
2361
|
+
logm = np.log(mval)
|
|
2362
|
+
d_vals = np.exp(sp_d(logm, logE, grid=False))
|
|
2363
|
+
s_vals = np.exp(sp_s(logm, logE, grid=False))
|
|
2364
|
+
i_vals = np.exp(sp_i(logm, logE, grid=False))
|
|
2365
|
+
f_vals = np.exp(sp_f(logm, logE, grid=False))
|
|
2366
|
+
# guard inflight tails
|
|
2367
|
+
for j in range(len(i_vals)-1,0,-1):
|
|
2368
|
+
if np.isclose(i_vals[j], i_vals[j-1], rtol=1e-8): i_vals[j] = 0.0
|
|
2369
|
+
else: break
|
|
2370
|
+
log10i = np.log10(np.where(i_vals>0, i_vals, floor))
|
|
2371
|
+
for j in range(1,len(log10i)):
|
|
2372
|
+
if log10i[j] - log10i[j-1] < -50:
|
|
2373
|
+
i_vals[j:] = 0.0; break
|
|
2374
|
+
i_vals[mono_E >= Ecut] = 0.0
|
|
2375
|
+
T = d_vals + s_vals + i_vals + f_vals
|
|
2376
|
+
T[T < 1e-299] = 0.0
|
|
2377
|
+
|
|
2378
|
+
queue.append({
|
|
2379
|
+
'label': f"Monochromatic {mval:.2e} g (interp)",
|
|
2380
|
+
'E': mono_E.copy(),
|
|
2381
|
+
'S': T.copy(),
|
|
2382
|
+
'hist': None
|
|
2383
|
+
})
|
|
2384
|
+
info(f"Queued Monochromatic {mval:.2e} g (interpolated)")
|
|
2385
|
+
except Exception as e:
|
|
2386
|
+
warn(f"Failed to queue {mval:.2e} g: {e}")
|
|
2387
|
+
continue
|
|
2388
|
+
|
|
2389
|
+
# ----- Distributed branches (unchanged, except bigger hist funcs will be used later) -----
|
|
2390
|
+
try:
|
|
2391
|
+
subdirs = [
|
|
2392
|
+
d for d in sorted(os.listdir(root))
|
|
2393
|
+
if os.path.isdir(os.path.join(root, d))
|
|
2394
|
+
]
|
|
2395
|
+
except FileNotFoundError:
|
|
2396
|
+
subdirs = []
|
|
2397
|
+
|
|
2398
|
+
# Pretty listing entries
|
|
2399
|
+
pretty_entries = []
|
|
2400
|
+
for d in subdirs:
|
|
2401
|
+
peak_val, sigma_val, sigma_str = _extract_peak_sigma(d, kind)
|
|
2402
|
+
if kind == "gaussian":
|
|
2403
|
+
pretty = (f"{GAUSSIAN_METHOD} peak {peak_val:.2e} g ({sigma_str})"
|
|
2404
|
+
if (peak_val is not None and sigma_str is not None) else f"{GAUSSIAN_METHOD} {d}")
|
|
2405
|
+
elif kind == "non_gaussian":
|
|
2406
|
+
pretty = (f"{NON_GAUSSIAN_METHOD} peak {peak_val:.2e} g ({sigma_str})"
|
|
2407
|
+
if (peak_val is not None and sigma_str is not None) else f"{NON_GAUSSIAN_METHOD} {d}")
|
|
2408
|
+
elif kind == "lognormal":
|
|
2409
|
+
pretty = (f"{LOGNORMAL_METHOD} peak {peak_val:.2e} g ({sigma_str})"
|
|
2410
|
+
if (peak_val is not None and sigma_str is not None) else f"{LOGNORMAL_METHOD} {d}")
|
|
2411
|
+
elif kind == "custom":
|
|
2412
|
+
pretty = d
|
|
2413
|
+
else:
|
|
2414
|
+
pretty = d
|
|
2415
|
+
pretty_entries.append((d, pretty, peak_val, sigma_val))
|
|
2416
|
+
|
|
2417
|
+
print(f"Available in {label_group}:")
|
|
2418
|
+
for i, (_, pretty, _, _) in enumerate(pretty_entries, start=1):
|
|
2419
|
+
print(f" {i}: {pretty}")
|
|
2420
|
+
|
|
2421
|
+
sel = user_input(
|
|
2422
|
+
"Enter indices to QUEUE (comma-separated): ",
|
|
2423
|
+
allow_back=True, allow_exit=True
|
|
2424
|
+
).strip()
|
|
2425
|
+
if not sel:
|
|
2426
|
+
continue
|
|
2427
|
+
|
|
2428
|
+
try:
|
|
2429
|
+
idxs = [int(x) for x in sel.split(",") if x.strip()]
|
|
2430
|
+
except Exception:
|
|
2431
|
+
warn("Invalid indices input.")
|
|
2432
|
+
continue
|
|
2433
|
+
|
|
2434
|
+
for i_sel in idxs:
|
|
2435
|
+
if not (1 <= i_sel <= len(pretty_entries)):
|
|
2436
|
+
warn(f"Index {i_sel} out of range.")
|
|
2437
|
+
continue
|
|
2438
|
+
|
|
2439
|
+
run_name, pretty_label, peak_val, sigma_val = pretty_entries[i_sel - 1]
|
|
2440
|
+
run_dir = os.path.join(root, run_name)
|
|
2441
|
+
spec_path = os.path.join(run_dir, spec_file) if spec_file else None
|
|
2442
|
+
|
|
2443
|
+
try:
|
|
2444
|
+
if spec_path and os.path.isfile(spec_path):
|
|
2445
|
+
arr = np.loadtxt(spec_path)
|
|
2446
|
+
if arr.ndim >= 2 and arr.shape[1] >= 2:
|
|
2447
|
+
E = arr[:,0]
|
|
2448
|
+
S = arr[:,1]
|
|
2449
|
+
if kind == "gaussian":
|
|
2450
|
+
plot_label = (f"{GAUSSIAN_METHOD} peak {peak_val:.2e} g (σ={sigma_val:.3g})"
|
|
2451
|
+
if peak_val is not None and sigma_val is not None else
|
|
2452
|
+
f"{GAUSSIAN_METHOD} {run_name}")
|
|
2453
|
+
elif kind == "non_gaussian":
|
|
2454
|
+
plot_label = (f"{NON_GAUSSIAN_METHOD} peak {peak_val:.2e} g (σX={sigma_val:.3g})"
|
|
2455
|
+
if peak_val is not None and sigma_val is not None else
|
|
2456
|
+
f"{NON_GAUSSIAN_METHOD} {run_name}")
|
|
2457
|
+
elif kind == "lognormal":
|
|
2458
|
+
plot_label = (f"{LOGNORMAL_METHOD} peak {peak_val:.2e} g (σ={sigma_val:.3g})"
|
|
2459
|
+
if peak_val is not None and sigma_val is not None else
|
|
2460
|
+
f"{LOGNORMAL_METHOD} {run_name}")
|
|
2461
|
+
elif kind == "custom":
|
|
2462
|
+
plot_label = run_name
|
|
2463
|
+
else:
|
|
2464
|
+
plot_label = run_name
|
|
2465
|
+
|
|
2466
|
+
queue.append({
|
|
2467
|
+
'label': plot_label,
|
|
2468
|
+
'E': E,
|
|
2469
|
+
'S': S,
|
|
2470
|
+
'hist': {
|
|
2471
|
+
'kind': kind,
|
|
2472
|
+
'run_dir': run_dir,
|
|
2473
|
+
'peak': peak_val,
|
|
2474
|
+
'sigma': sigma_val
|
|
2475
|
+
}
|
|
2476
|
+
})
|
|
2477
|
+
info(f"Queued {plot_label}")
|
|
2478
|
+
else:
|
|
2479
|
+
warn(f"{spec_file} malformed in {run_name}.")
|
|
2480
|
+
else:
|
|
2481
|
+
warn(f"No '{spec_file}' found in {run_name}; skipping queue.")
|
|
2482
|
+
except Exception as e:
|
|
2483
|
+
warn(f"Failed to queue {run_name}: {e}")
|
|
2484
|
+
|
|
2485
|
+
# ---------- UI ----------
|
|
2486
|
+
def _print_menu():
|
|
2487
|
+
print("\nView Previous — choose:")
|
|
2488
|
+
print(" 1: Monochromatic Distribution")
|
|
2489
|
+
print(f" 2: {GAUSSIAN_METHOD}")
|
|
2490
|
+
print(f" 3: {NON_GAUSSIAN_METHOD}")
|
|
2491
|
+
print(f" 4: {LOGNORMAL_METHOD}")
|
|
2492
|
+
print(f" 5: Custom equation (user-defined mass PDF)")
|
|
2493
|
+
print(" 0: Plot all Queued | b: Back | q: Quit")
|
|
2494
|
+
|
|
2495
|
+
# queue elements:
|
|
2496
|
+
# {
|
|
2497
|
+
# 'label': str (pretty label for overlay / hist title),
|
|
2498
|
+
# 'E': ndarray,
|
|
2499
|
+
# 'S': ndarray,
|
|
2500
|
+
# 'hist': {'kind': kind, 'run_dir': run_dir, 'peak': float?, 'sigma': float?} or None
|
|
2501
|
+
# }
|
|
2502
|
+
queue: list[dict] = []
|
|
2503
|
+
|
|
2504
|
+
while True:
|
|
2505
|
+
_print_menu()
|
|
2506
|
+
try:
|
|
2507
|
+
choice = user_input("Choice: ", allow_back=True, allow_exit=True).strip().lower()
|
|
2508
|
+
except BackRequested:
|
|
2509
|
+
return
|
|
2510
|
+
|
|
2511
|
+
# ----- plot all queued -----
|
|
2512
|
+
if choice == '0':
|
|
2513
|
+
if not queue:
|
|
2514
|
+
warn("Queue is empty.")
|
|
2515
|
+
continue
|
|
2516
|
+
|
|
2517
|
+
# spectra first
|
|
2518
|
+
plot_pack = [(item['label'], (item['E'], item['S'])) for item in queue]
|
|
2519
|
+
_plot_dn(plot_pack)
|
|
2520
|
+
_plot_e2(plot_pack)
|
|
2521
|
+
|
|
2522
|
+
# histograms second
|
|
2523
|
+
for item in queue:
|
|
2524
|
+
h = item.get('hist')
|
|
2525
|
+
if not h:
|
|
2526
|
+
continue
|
|
2527
|
+
kind = h['kind']
|
|
2528
|
+
run_dir = h['run_dir']
|
|
2529
|
+
label_for_hist = item['label']
|
|
2530
|
+
peak_val = h.get('peak')
|
|
2531
|
+
sigma_val = h.get('sigma') # for non_gaussian this is sigma_X
|
|
2532
|
+
|
|
2533
|
+
try:
|
|
2534
|
+
if kind in ("gaussian", "non_gaussian", "lognormal"):
|
|
2535
|
+
md_path = os.path.join(run_dir, "mass_distribution.txt")
|
|
2536
|
+
md = np.loadtxt(md_path)
|
|
2537
|
+
|
|
2538
|
+
if kind == "gaussian":
|
|
2539
|
+
_hist_gaussian(md,
|
|
2540
|
+
peak_val if peak_val is not None else np.median(md),
|
|
2541
|
+
sigma_val if sigma_val is not None else 0.05,
|
|
2542
|
+
label_for_hist)
|
|
2543
|
+
|
|
2544
|
+
elif kind == "non_gaussian":
|
|
2545
|
+
_hist_nongaussian(md,
|
|
2546
|
+
peak_val if peak_val is not None else np.median(md),
|
|
2547
|
+
sigma_val if sigma_val is not None else 0.08,
|
|
2548
|
+
label_for_hist)
|
|
2549
|
+
|
|
2550
|
+
else: # lognormal
|
|
2551
|
+
_hist_lognormal(md,
|
|
2552
|
+
peak_val if peak_val is not None else np.median(md),
|
|
2553
|
+
sigma_val if sigma_val is not None else 1.0,
|
|
2554
|
+
label_for_hist)
|
|
2555
|
+
|
|
2556
|
+
elif kind == "custom":
|
|
2557
|
+
_hist_custom(run_dir, label_for_hist)
|
|
2558
|
+
|
|
2559
|
+
except FileNotFoundError as e:
|
|
2560
|
+
warn(f"Histogram inputs missing in {run_dir}: {e}")
|
|
2561
|
+
except Exception as e:
|
|
2562
|
+
warn(f"Histogram reconstruction failed for {run_dir}: {e}")
|
|
2563
|
+
|
|
2564
|
+
# clear queue
|
|
2565
|
+
queue.clear()
|
|
2566
|
+
info("Queue cleared.")
|
|
2567
|
+
continue
|
|
2568
|
+
|
|
2569
|
+
if choice not in cat_map:
|
|
2570
|
+
warn("Invalid choice; try again.")
|
|
2571
|
+
continue
|
|
2572
|
+
|
|
2573
|
+
label_group, root, spec_file, kind = cat_map[choice]
|
|
2574
|
+
|
|
2575
|
+
# ----- Monochromatic branch -----
|
|
2576
|
+
if kind == "mono":
|
|
2577
|
+
runs = []
|
|
2578
|
+
try:
|
|
2579
|
+
for fn in sorted(os.listdir(root)):
|
|
2580
|
+
if fn.lower().endswith(".txt"):
|
|
2581
|
+
runs.append(("file", fn))
|
|
2582
|
+
except FileNotFoundError:
|
|
2583
|
+
pass
|
|
2584
|
+
|
|
2585
|
+
print(f"Available in {label_group}:")
|
|
2586
|
+
for i,(_,fn) in enumerate(runs, start=1):
|
|
2587
|
+
print(f" {i}: {fn}")
|
|
2588
|
+
|
|
2589
|
+
print("\nYou can also request a target mass (in grams) to generate the nearest pre-rendered mono spectrum.")
|
|
2590
|
+
sel = user_input(
|
|
2591
|
+
"Enter indices to QUEUE (comma-separated) OR a mass (e.g. 1e15), or Enter to cancel: ",
|
|
2592
|
+
allow_back=True, allow_exit=True
|
|
2593
|
+
).strip()
|
|
2594
|
+
if not sel:
|
|
2595
|
+
continue
|
|
2596
|
+
|
|
2597
|
+
# numeric mass path?
|
|
2598
|
+
try:
|
|
2599
|
+
mass_try = float(sel)
|
|
2600
|
+
except Exception:
|
|
2601
|
+
mass_try = None
|
|
2602
|
+
|
|
2603
|
+
if mass_try is not None:
|
|
2604
|
+
if not (M_MIN_MONO <= mass_try <= M_MAX_MONO):
|
|
2605
|
+
warn(f"Mass outside allowed view window [{M_MIN_MONO:.2e}, {M_MAX_MONO:.2e}].")
|
|
2606
|
+
continue
|
|
2607
|
+
try:
|
|
2608
|
+
fname = generate_monochromatic_for_mass(mass_try, DATA_DIR, MONO_RESULTS_DIR)
|
|
2609
|
+
arr = np.loadtxt(fname)
|
|
2610
|
+
E = arr[:,0]
|
|
2611
|
+
T = arr[:,1]
|
|
2612
|
+
queue.append({
|
|
2613
|
+
'label': f"Monochromatic {mass_try:.2e} g",
|
|
2614
|
+
'E': E,
|
|
2615
|
+
'S': T,
|
|
2616
|
+
'hist': None
|
|
2617
|
+
})
|
|
2618
|
+
info(f"Queued Monochromatic {mass_try:.2e} g → {os.path.basename(fname)}")
|
|
2619
|
+
except Exception as e:
|
|
2620
|
+
err(f"Could not generate/queue mono spectrum: {e}")
|
|
2621
|
+
continue
|
|
2622
|
+
|
|
2623
|
+
# index list path
|
|
2624
|
+
try:
|
|
2625
|
+
idxs = [int(x) for x in sel.split(",") if x.strip()]
|
|
2626
|
+
except Exception:
|
|
2627
|
+
warn("Invalid indices input.")
|
|
2628
|
+
continue
|
|
2629
|
+
|
|
2630
|
+
for i in idxs:
|
|
2631
|
+
if 1 <= i <= len(runs):
|
|
2632
|
+
_, fn = runs[i-1]
|
|
2633
|
+
path = os.path.join(root, fn)
|
|
2634
|
+
try:
|
|
2635
|
+
arr = np.loadtxt(path)
|
|
2636
|
+
E = arr[:,0]
|
|
2637
|
+
T = arr[:,1] if arr.ndim > 1 and arr.shape[1] >= 2 else np.zeros_like(E)
|
|
2638
|
+
queue.append({
|
|
2639
|
+
'label': f"Monochromatic {fn}",
|
|
2640
|
+
'E' : E,
|
|
2641
|
+
'S' : T,
|
|
2642
|
+
'hist': None
|
|
2643
|
+
})
|
|
2644
|
+
info(f"Queued {fn}")
|
|
2645
|
+
except Exception as e:
|
|
2646
|
+
warn(f"Failed to read {fn}: {e}")
|
|
2647
|
+
continue
|
|
2648
|
+
|
|
2649
|
+
# ----- Distributed branches -----
|
|
2650
|
+
try:
|
|
2651
|
+
subdirs = [
|
|
2652
|
+
d for d in sorted(os.listdir(root))
|
|
2653
|
+
if os.path.isdir(os.path.join(root, d))
|
|
2654
|
+
]
|
|
2655
|
+
except FileNotFoundError:
|
|
2656
|
+
subdirs = []
|
|
2657
|
+
|
|
2658
|
+
# Build pretty listing entries
|
|
2659
|
+
pretty_entries = []
|
|
2660
|
+
for d in subdirs:
|
|
2661
|
+
peak_val, sigma_val, sigma_str = _extract_peak_sigma(d, kind)
|
|
2662
|
+
|
|
2663
|
+
if kind == "gaussian":
|
|
2664
|
+
if peak_val is not None and sigma_str is not None:
|
|
2665
|
+
pretty = f"{GAUSSIAN_METHOD} peak {peak_val:.2e} g ({sigma_str})"
|
|
2666
|
+
else:
|
|
2667
|
+
pretty = f"{GAUSSIAN_METHOD} {d}"
|
|
2668
|
+
|
|
2669
|
+
elif kind == "non_gaussian":
|
|
2670
|
+
if peak_val is not None and sigma_str is not None:
|
|
2671
|
+
pretty = f"{NON_GAUSSIAN_METHOD} peak {peak_val:.2e} g ({sigma_str})"
|
|
2672
|
+
else:
|
|
2673
|
+
pretty = f"{NON_GAUSSIAN_METHOD} {d}"
|
|
2674
|
+
|
|
2675
|
+
elif kind == "lognormal":
|
|
2676
|
+
if peak_val is not None and sigma_str is not None:
|
|
2677
|
+
pretty = f"{LOGNORMAL_METHOD} peak {peak_val:.2e} g ({sigma_str})"
|
|
2678
|
+
else:
|
|
2679
|
+
pretty = f"{LOGNORMAL_METHOD} {d}"
|
|
2680
|
+
|
|
2681
|
+
elif kind == "custom":
|
|
2682
|
+
# Just show folder name (no equation)
|
|
2683
|
+
pretty = d
|
|
2684
|
+
|
|
2685
|
+
else:
|
|
2686
|
+
pretty = d
|
|
2687
|
+
|
|
2688
|
+
pretty_entries.append((d, pretty, peak_val, sigma_val))
|
|
2689
|
+
|
|
2690
|
+
print(f"Available in {label_group}:")
|
|
2691
|
+
for i, (_, pretty, _, _) in enumerate(pretty_entries, start=1):
|
|
2692
|
+
print(f" {i}: {pretty}")
|
|
2693
|
+
|
|
2694
|
+
sel = user_input(
|
|
2695
|
+
"Enter indices to QUEUE (comma-separated): ",
|
|
2696
|
+
allow_back=True, allow_exit=True
|
|
2697
|
+
).strip()
|
|
2698
|
+
if not sel:
|
|
2699
|
+
continue
|
|
2700
|
+
|
|
2701
|
+
try:
|
|
2702
|
+
idxs = [int(x) for x in sel.split(",") if x.strip()]
|
|
2703
|
+
except Exception:
|
|
2704
|
+
warn("Invalid indices input.")
|
|
2705
|
+
continue
|
|
2706
|
+
|
|
2707
|
+
for i_sel in idxs:
|
|
2708
|
+
if not (1 <= i_sel <= len(pretty_entries)):
|
|
2709
|
+
warn(f"Index {i_sel} out of range.")
|
|
2710
|
+
continue
|
|
2711
|
+
|
|
2712
|
+
run_name, pretty_label, peak_val, sigma_val = pretty_entries[i_sel - 1]
|
|
2713
|
+
run_dir = os.path.join(root, run_name)
|
|
2714
|
+
spec_path = os.path.join(run_dir, spec_file) if spec_file else None
|
|
2715
|
+
|
|
2716
|
+
try:
|
|
2717
|
+
if spec_path and os.path.isfile(spec_path):
|
|
2718
|
+
arr = np.loadtxt(spec_path)
|
|
2719
|
+
if arr.ndim >= 2 and arr.shape[1] >= 2:
|
|
2720
|
+
E = arr[:,0]
|
|
2721
|
+
S = arr[:,1]
|
|
2722
|
+
|
|
2723
|
+
# Build final label for plots:
|
|
2724
|
+
if kind == "gaussian":
|
|
2725
|
+
if peak_val is not None and sigma_val is not None:
|
|
2726
|
+
plot_label = f"{GAUSSIAN_METHOD} peak {peak_val:.2e} g (σ={sigma_val:.3g})"
|
|
2727
|
+
else:
|
|
2728
|
+
plot_label = f"{GAUSSIAN_METHOD} {run_name}"
|
|
2729
|
+
|
|
2730
|
+
elif kind == "non_gaussian":
|
|
2731
|
+
if peak_val is not None and sigma_val is not None:
|
|
2732
|
+
plot_label = f"{NON_GAUSSIAN_METHOD} peak {peak_val:.2e} g (σX={sigma_val:.3g})"
|
|
2733
|
+
else:
|
|
2734
|
+
plot_label = f"{NON_GAUSSIAN_METHOD} {run_name}"
|
|
2735
|
+
|
|
2736
|
+
elif kind == "lognormal":
|
|
2737
|
+
if peak_val is not None and sigma_val is not None:
|
|
2738
|
+
plot_label = f"{LOGNORMAL_METHOD} peak {peak_val:.2e} g (σ={sigma_val:.3g})"
|
|
2739
|
+
else:
|
|
2740
|
+
plot_label = f"{LOGNORMAL_METHOD} {run_name}"
|
|
2741
|
+
|
|
2742
|
+
elif kind == "custom":
|
|
2743
|
+
plot_label = run_name
|
|
2744
|
+
|
|
2745
|
+
else:
|
|
2746
|
+
plot_label = run_name
|
|
2747
|
+
|
|
2748
|
+
queue.append({
|
|
2749
|
+
'label': plot_label,
|
|
2750
|
+
'E': E,
|
|
2751
|
+
'S': S,
|
|
2752
|
+
'hist': {
|
|
2753
|
+
'kind': kind,
|
|
2754
|
+
'run_dir': run_dir,
|
|
2755
|
+
'peak': peak_val,
|
|
2756
|
+
'sigma': sigma_val
|
|
2757
|
+
}
|
|
2758
|
+
})
|
|
2759
|
+
info(f"Queued {plot_label}")
|
|
2760
|
+
else:
|
|
2761
|
+
warn(f"{spec_file} malformed in {run_name}.")
|
|
2762
|
+
else:
|
|
2763
|
+
warn(f"No '{spec_file}' found in {run_name}; skipping queue.")
|
|
2764
|
+
except Exception as e:
|
|
2765
|
+
warn(f"Failed to queue {run_name}: {e}")
|
|
2766
|
+
|
|
2767
|
+
|
|
2768
|
+
# ---------------------------
|
|
2769
|
+
# UI
|
|
2770
|
+
# ---------------------------
|
|
2771
|
+
from colorama import Fore, Style, init as colorama_init
|
|
2772
|
+
colorama_init(autoreset=True)
|
|
2773
|
+
|
|
2774
|
+
def show_start_screen() -> None:
|
|
2775
|
+
"""
|
|
2776
|
+
Print the program banner and helpful usage hints.
|
|
2777
|
+
"""
|
|
2778
|
+
width = 56 # inner width of the box
|
|
2779
|
+
top = "╔" + "═" * width + "╗"
|
|
2780
|
+
bot = "╚" + "═" * width + "╝"
|
|
2781
|
+
title = "GammaPBHPlotter: PBH Spectrum Tool"
|
|
2782
|
+
ver = f"Version {__version__}"
|
|
2783
|
+
|
|
2784
|
+
print("\n" + Fore.CYAN + Style.BRIGHT + top)
|
|
2785
|
+
print( Fore.CYAN + Style.BRIGHT + f"║{title.center(width)}║")
|
|
2786
|
+
print( Fore.CYAN + Style.BRIGHT + f"║{ver.center(width)}║")
|
|
2787
|
+
print( Fore.CYAN + Style.BRIGHT + bot + Style.RESET_ALL)
|
|
2788
|
+
print()
|
|
2789
|
+
print("Analyze and visualize Hawking radiation spectra of primordial black holes.\n")
|
|
2790
|
+
print(Fore.YELLOW + "📄 Associated Publication:" + Style.RESET_ALL)
|
|
2791
|
+
print(" John Carlini & Ilias Cholis — Particle Astrophysics Research\n")
|
|
2792
|
+
print("At any prompt: 'b' = back, 'q' = quit.")
|
|
2793
|
+
|
|
2794
|
+
def main() -> None:
|
|
2795
|
+
"""
|
|
2796
|
+
Entry point for the interactive CLI loop.
|
|
2797
|
+
|
|
2798
|
+
Menu
|
|
2799
|
+
----
|
|
2800
|
+
1: Monochromatic spectra
|
|
2801
|
+
2: Distributed spectra (Gaussian collapse)
|
|
2802
|
+
3: Distributed spectra (Non-Gaussian Collapse)
|
|
2803
|
+
4: Distributed spectra (Log-Normal Distribution)
|
|
2804
|
+
5: Distributed spectra (Custom mass PDF)
|
|
2805
|
+
6: View previous spectra
|
|
2806
|
+
0: Exit
|
|
2807
|
+
"""
|
|
2808
|
+
show_start_screen()
|
|
2809
|
+
while True:
|
|
2810
|
+
print("\nSelect:")
|
|
2811
|
+
print("1: Monochromatic spectra")
|
|
2812
|
+
print(f"2: Distributed spectra ({GAUSSIAN_METHOD})")
|
|
2813
|
+
print(f"3: Distributed spectra ({NON_GAUSSIAN_METHOD})")
|
|
2814
|
+
print(f"4: Distributed spectra ({LOGNORMAL_METHOD})")
|
|
2815
|
+
print("5: Distributed spectra (Custom mass PDF)")
|
|
2816
|
+
print("6: View previous spectra")
|
|
2817
|
+
print("0: Exit")
|
|
2818
|
+
choice = user_input("Choice: ", allow_back=False, allow_exit=True).strip().lower()
|
|
2819
|
+
if choice == '1':
|
|
2820
|
+
monochromatic_spectra()
|
|
2821
|
+
elif choice == '2':
|
|
2822
|
+
distributed_spectrum(GAUSSIAN_METHOD)
|
|
2823
|
+
elif choice == '3':
|
|
2824
|
+
distributed_spectrum(NON_GAUSSIAN_METHOD)
|
|
2825
|
+
elif choice == '4':
|
|
2826
|
+
distributed_spectrum(LOGNORMAL_METHOD)
|
|
2827
|
+
elif choice == '5':
|
|
2828
|
+
custom_equation_pdf_tool()
|
|
2829
|
+
elif choice == '6':
|
|
2830
|
+
view_previous_spectra()
|
|
2831
|
+
elif choice in ['0','exit','q']:
|
|
2832
|
+
print("Goodbye.")
|
|
2833
|
+
break
|
|
2834
|
+
else:
|
|
2835
|
+
print("Invalid; try again.")
|
|
2836
|
+
|
|
2837
|
+
|
|
2838
|
+
if __name__ == '__main__':
|
|
2839
|
+
try:
|
|
2840
|
+
main()
|
|
2841
|
+
except BackRequested:
|
|
2842
|
+
# If a BackRequested was thrown at the top-level, just exit cleanly.
|
|
2843
|
+
pass
|
|
2844
|
+
except Exception:
|
|
2845
|
+
import traceback
|
|
2846
|
+
traceback.print_exc()
|
|
2847
|
+
try:
|
|
2848
|
+
input("\nAn error occurred. Press Enter to exit…")
|
|
2849
|
+
except Exception:
|
|
2850
|
+
pass
|