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.

Files changed (570) hide show
  1. gammapbh/__init__.py +42 -0
  2. gammapbh/__main__.py +29 -0
  3. gammapbh/blackhawk_data/1.0e+14/1.0e+14.txt +59 -0
  4. gammapbh/blackhawk_data/1.0e+14/BH_spectrum.txt +4 -0
  5. gammapbh/blackhawk_data/1.0e+14/Probabilities.txt +346 -0
  6. gammapbh/blackhawk_data/1.0e+14/final_state_radiation_prim.txt +238 -0
  7. gammapbh/blackhawk_data/1.0e+14/final_state_radiation_sec.txt +169 -0
  8. gammapbh/blackhawk_data/1.0e+14/inflight_annihilation_prim.txt +238 -0
  9. gammapbh/blackhawk_data/1.0e+14/inflight_annihilation_sec.txt +169 -0
  10. gammapbh/blackhawk_data/1.0e+14/instantaneous_primary_spectra.txt +502 -0
  11. gammapbh/blackhawk_data/1.0e+14/instantaneous_secondary_spectra.txt +378 -0
  12. gammapbh/blackhawk_data/1.0e+14/instantaneous_total_spectra.txt +502 -0
  13. gammapbh/blackhawk_data/1.0e+15/1e+15.txt +59 -0
  14. gammapbh/blackhawk_data/1.0e+15/BH_spectrum.txt +4 -0
  15. gammapbh/blackhawk_data/1.0e+15/Probabilities.txt +346 -0
  16. gammapbh/blackhawk_data/1.0e+15/final_state_radiation_prim.txt +193 -0
  17. gammapbh/blackhawk_data/1.0e+15/final_state_radiation_sec.txt +193 -0
  18. gammapbh/blackhawk_data/1.0e+15/inflight_annihilation_prim.txt +193 -0
  19. gammapbh/blackhawk_data/1.0e+15/inflight_annihilation_sec.txt +193 -0
  20. gammapbh/blackhawk_data/1.0e+15/instantaneous_primary_spectra.txt +502 -0
  21. gammapbh/blackhawk_data/1.0e+15/instantaneous_secondary_spectra.txt +378 -0
  22. gammapbh/blackhawk_data/1.0e+15/instantaneous_total_spectra.txt +502 -0
  23. gammapbh/blackhawk_data/1.0e+16/1e+16.txt +59 -0
  24. gammapbh/blackhawk_data/1.0e+16/BH_spectrum.txt +4 -0
  25. gammapbh/blackhawk_data/1.0e+16/Probabilities.txt +346 -0
  26. gammapbh/blackhawk_data/1.0e+16/final_state_radiation_prim.txt +147 -0
  27. gammapbh/blackhawk_data/1.0e+16/final_state_radiation_sec.txt +147 -0
  28. gammapbh/blackhawk_data/1.0e+16/inflight_annihilation_prim.txt +147 -0
  29. gammapbh/blackhawk_data/1.0e+16/inflight_annihilation_sec.txt +147 -0
  30. gammapbh/blackhawk_data/1.0e+16/instantaneous_primary_spectra.txt +502 -0
  31. gammapbh/blackhawk_data/1.0e+16/instantaneous_secondary_spectra.txt +378 -0
  32. gammapbh/blackhawk_data/1.0e+16/instantaneous_total_spectra.txt +502 -0
  33. gammapbh/blackhawk_data/1.0e+17/1.0e+17.txt +59 -0
  34. gammapbh/blackhawk_data/1.0e+17/BH_spectrum.txt +4 -0
  35. gammapbh/blackhawk_data/1.0e+17/Probabilities.txt +346 -0
  36. gammapbh/blackhawk_data/1.0e+17/final_state_radiation_prim.txt +102 -0
  37. gammapbh/blackhawk_data/1.0e+17/final_state_radiation_sec.txt +101 -0
  38. gammapbh/blackhawk_data/1.0e+17/inflight_annihilation_prim.txt +102 -0
  39. gammapbh/blackhawk_data/1.0e+17/inflight_annihilation_sec.txt +101 -0
  40. gammapbh/blackhawk_data/1.0e+17/instantaneous_primary_spectra.txt +502 -0
  41. gammapbh/blackhawk_data/1.0e+17/instantaneous_secondary_spectra.txt +378 -0
  42. gammapbh/blackhawk_data/1.0e+17/instantaneous_total_spectra.txt +502 -0
  43. gammapbh/blackhawk_data/1.0e+18/1.0e+18.txt +59 -0
  44. gammapbh/blackhawk_data/1.0e+18/BH_spectrum.txt +4 -0
  45. gammapbh/blackhawk_data/1.0e+18/Probabilities.txt +346 -0
  46. gammapbh/blackhawk_data/1.0e+18/final_state_radiation_prim.txt +57 -0
  47. gammapbh/blackhawk_data/1.0e+18/final_state_radiation_sec.txt +56 -0
  48. gammapbh/blackhawk_data/1.0e+18/inflight_annihilation_prim.txt +57 -0
  49. gammapbh/blackhawk_data/1.0e+18/inflight_annihilation_sec.txt +56 -0
  50. gammapbh/blackhawk_data/1.0e+18/instantaneous_primary_spectra.txt +502 -0
  51. gammapbh/blackhawk_data/1.0e+18/instantaneous_secondary_spectra.txt +378 -0
  52. gammapbh/blackhawk_data/1.0e+18/instantaneous_total_spectra.txt +502 -0
  53. gammapbh/blackhawk_data/1.0e+19/1e+19.txt +59 -0
  54. gammapbh/blackhawk_data/1.0e+19/BH_spectrum.txt +4 -0
  55. gammapbh/blackhawk_data/1.0e+19/final_state_radiation_prim.txt +11 -0
  56. gammapbh/blackhawk_data/1.0e+19/final_state_radiation_sec.txt +12 -0
  57. gammapbh/blackhawk_data/1.0e+19/inflight_annihilation_prim.txt +11 -0
  58. gammapbh/blackhawk_data/1.0e+19/inflight_annihilation_sec.txt +12 -0
  59. gammapbh/blackhawk_data/1.0e+19/instantaneous_primary_spectra.txt +502 -0
  60. gammapbh/blackhawk_data/1.0e+19/instantaneous_secondary_spectra.txt +378 -0
  61. gammapbh/blackhawk_data/1.0e+19/instantaneous_total_spectra.txt +502 -0
  62. gammapbh/blackhawk_data/1.5e+14/1.5e+14.txt +59 -0
  63. gammapbh/blackhawk_data/1.5e+14/BH_spectrum.txt +4 -0
  64. gammapbh/blackhawk_data/1.5e+14/Probabilities.txt +346 -0
  65. gammapbh/blackhawk_data/1.5e+14/final_state_radiation_prim.txt +230 -0
  66. gammapbh/blackhawk_data/1.5e+14/final_state_radiation_sec.txt +149 -0
  67. gammapbh/blackhawk_data/1.5e+14/inflight_annihilation_prim.txt +230 -0
  68. gammapbh/blackhawk_data/1.5e+14/inflight_annihilation_sec.txt +149 -0
  69. gammapbh/blackhawk_data/1.5e+14/instantaneous_primary_spectra.txt +502 -0
  70. gammapbh/blackhawk_data/1.5e+14/instantaneous_secondary_spectra.txt +378 -0
  71. gammapbh/blackhawk_data/1.5e+14/instantaneous_total_spectra.txt +502 -0
  72. gammapbh/blackhawk_data/1.5e+15/1.5e+15.txt +59 -0
  73. gammapbh/blackhawk_data/1.5e+15/BH_spectrum.txt +4 -0
  74. gammapbh/blackhawk_data/1.5e+15/Probabilities.txt +346 -0
  75. gammapbh/blackhawk_data/1.5e+15/final_state_radiation_prim.txt +185 -0
  76. gammapbh/blackhawk_data/1.5e+15/final_state_radiation_sec.txt +185 -0
  77. gammapbh/blackhawk_data/1.5e+15/inflight_annihilation_prim.txt +185 -0
  78. gammapbh/blackhawk_data/1.5e+15/inflight_annihilation_sec.txt +185 -0
  79. gammapbh/blackhawk_data/1.5e+15/instantaneous_primary_spectra.txt +502 -0
  80. gammapbh/blackhawk_data/1.5e+15/instantaneous_secondary_spectra.txt +378 -0
  81. gammapbh/blackhawk_data/1.5e+15/instantaneous_total_spectra.txt +502 -0
  82. gammapbh/blackhawk_data/1.5e+16/1.5e+16.txt +59 -0
  83. gammapbh/blackhawk_data/1.5e+16/BH_spectrum.txt +4 -0
  84. gammapbh/blackhawk_data/1.5e+16/Probabilities.txt +346 -0
  85. gammapbh/blackhawk_data/1.5e+16/final_state_radiation_prim.txt +139 -0
  86. gammapbh/blackhawk_data/1.5e+16/final_state_radiation_sec.txt +138 -0
  87. gammapbh/blackhawk_data/1.5e+16/inflight_annihilation_prim.txt +139 -0
  88. gammapbh/blackhawk_data/1.5e+16/inflight_annihilation_sec.txt +138 -0
  89. gammapbh/blackhawk_data/1.5e+16/instantaneous_primary_spectra.txt +502 -0
  90. gammapbh/blackhawk_data/1.5e+16/instantaneous_secondary_spectra.txt +378 -0
  91. gammapbh/blackhawk_data/1.5e+16/instantaneous_total_spectra.txt +502 -0
  92. gammapbh/blackhawk_data/1.5e+17/1.5e+17.txt +59 -0
  93. gammapbh/blackhawk_data/1.5e+17/BH_spectrum.txt +4 -0
  94. gammapbh/blackhawk_data/1.5e+17/Probabilities.txt +346 -0
  95. gammapbh/blackhawk_data/1.5e+17/final_state_radiation_prim.txt +94 -0
  96. gammapbh/blackhawk_data/1.5e+17/final_state_radiation_sec.txt +93 -0
  97. gammapbh/blackhawk_data/1.5e+17/inflight_annihilation_prim.txt +94 -0
  98. gammapbh/blackhawk_data/1.5e+17/inflight_annihilation_sec.txt +93 -0
  99. gammapbh/blackhawk_data/1.5e+17/instantaneous_primary_spectra.txt +502 -0
  100. gammapbh/blackhawk_data/1.5e+17/instantaneous_secondary_spectra.txt +378 -0
  101. gammapbh/blackhawk_data/1.5e+17/instantaneous_total_spectra.txt +502 -0
  102. gammapbh/blackhawk_data/1.5e+18/1.5e+18.txt +59 -0
  103. gammapbh/blackhawk_data/1.5e+18/BH_spectrum.txt +4 -0
  104. gammapbh/blackhawk_data/1.5e+18/final_state_radiation_prim.txt +49 -0
  105. gammapbh/blackhawk_data/1.5e+18/final_state_radiation_sec.txt +49 -0
  106. gammapbh/blackhawk_data/1.5e+18/inflight_annihilation_prim.txt +49 -0
  107. gammapbh/blackhawk_data/1.5e+18/inflight_annihilation_sec.txt +49 -0
  108. gammapbh/blackhawk_data/1.5e+18/instantaneous_primary_spectra.txt +502 -0
  109. gammapbh/blackhawk_data/1.5e+18/instantaneous_secondary_spectra.txt +378 -0
  110. gammapbh/blackhawk_data/1.5e+18/instantaneous_total_spectra.txt +502 -0
  111. gammapbh/blackhawk_data/2.0e+14/2.0e+14.txt +59 -0
  112. gammapbh/blackhawk_data/2.0e+14/BH_spectrum.txt +4 -0
  113. gammapbh/blackhawk_data/2.0e+14/Probabilities.txt +346 -0
  114. gammapbh/blackhawk_data/2.0e+14/final_state_radiation_prim.txt +224 -0
  115. gammapbh/blackhawk_data/2.0e+14/final_state_radiation_sec.txt +143 -0
  116. gammapbh/blackhawk_data/2.0e+14/inflight_annihilation_prim.txt +224 -0
  117. gammapbh/blackhawk_data/2.0e+14/inflight_annihilation_sec.txt +143 -0
  118. gammapbh/blackhawk_data/2.0e+14/instantaneous_primary_spectra.txt +502 -0
  119. gammapbh/blackhawk_data/2.0e+14/instantaneous_secondary_spectra.txt +378 -0
  120. gammapbh/blackhawk_data/2.0e+14/instantaneous_total_spectra.txt +502 -0
  121. gammapbh/blackhawk_data/2.0e+15/2e+15.txt +59 -0
  122. gammapbh/blackhawk_data/2.0e+15/BH_spectrum.txt +4 -0
  123. gammapbh/blackhawk_data/2.0e+15/Probabilities.txt +346 -0
  124. gammapbh/blackhawk_data/2.0e+15/final_state_radiation_prim.txt +179 -0
  125. gammapbh/blackhawk_data/2.0e+15/final_state_radiation_sec.txt +178 -0
  126. gammapbh/blackhawk_data/2.0e+15/inflight_annihilation_prim.txt +179 -0
  127. gammapbh/blackhawk_data/2.0e+15/inflight_annihilation_sec.txt +178 -0
  128. gammapbh/blackhawk_data/2.0e+15/instantaneous_primary_spectra.txt +502 -0
  129. gammapbh/blackhawk_data/2.0e+15/instantaneous_secondary_spectra.txt +378 -0
  130. gammapbh/blackhawk_data/2.0e+15/instantaneous_total_spectra.txt +502 -0
  131. gammapbh/blackhawk_data/2.0e+16/2.0e+16.txt +59 -0
  132. gammapbh/blackhawk_data/2.0e+16/BH_spectrum.txt +4 -0
  133. gammapbh/blackhawk_data/2.0e+16/Probabilities.txt +346 -0
  134. gammapbh/blackhawk_data/2.0e+16/downloaded_components/direct_hawking.txt +501 -0
  135. gammapbh/blackhawk_data/2.0e+16/downloaded_components/final_state_radiation.txt +501 -0
  136. gammapbh/blackhawk_data/2.0e+16/downloaded_components/inflight_annihilation.txt +501 -0
  137. gammapbh/blackhawk_data/2.0e+16/downloaded_components/total_spectrum.txt +501 -0
  138. gammapbh/blackhawk_data/2.0e+16/final_state_radiation_prim.txt +134 -0
  139. gammapbh/blackhawk_data/2.0e+16/final_state_radiation_sec.txt +133 -0
  140. gammapbh/blackhawk_data/2.0e+16/inflight_annihilation_prim.txt +134 -0
  141. gammapbh/blackhawk_data/2.0e+16/inflight_annihilation_sec.txt +133 -0
  142. gammapbh/blackhawk_data/2.0e+16/instantaneous_primary_spectra.txt +502 -0
  143. gammapbh/blackhawk_data/2.0e+16/instantaneous_secondary_spectra.txt +378 -0
  144. gammapbh/blackhawk_data/2.0e+16/instantaneous_total_spectra.txt +502 -0
  145. gammapbh/blackhawk_data/2.0e+17/2.0e+17.txt +59 -0
  146. gammapbh/blackhawk_data/2.0e+17/BH_spectrum.txt +4 -0
  147. gammapbh/blackhawk_data/2.0e+17/Probabilities.txt +346 -0
  148. gammapbh/blackhawk_data/2.0e+17/final_state_radiation_prim.txt +88 -0
  149. gammapbh/blackhawk_data/2.0e+17/final_state_radiation_sec.txt +87 -0
  150. gammapbh/blackhawk_data/2.0e+17/inflight_annihilation_prim.txt +88 -0
  151. gammapbh/blackhawk_data/2.0e+17/inflight_annihilation_sec.txt +87 -0
  152. gammapbh/blackhawk_data/2.0e+17/instantaneous_primary_spectra.txt +502 -0
  153. gammapbh/blackhawk_data/2.0e+17/instantaneous_secondary_spectra.txt +378 -0
  154. gammapbh/blackhawk_data/2.0e+17/instantaneous_total_spectra.txt +502 -0
  155. gammapbh/blackhawk_data/2.0e+18/2.0e+18.txt +59 -0
  156. gammapbh/blackhawk_data/2.0e+18/BH_spectrum.txt +4 -0
  157. gammapbh/blackhawk_data/2.0e+18/final_state_radiation_prim.txt +43 -0
  158. gammapbh/blackhawk_data/2.0e+18/final_state_radiation_sec.txt +44 -0
  159. gammapbh/blackhawk_data/2.0e+18/inflight_annihilation_prim.txt +43 -0
  160. gammapbh/blackhawk_data/2.0e+18/inflight_annihilation_sec.txt +44 -0
  161. gammapbh/blackhawk_data/2.0e+18/instantaneous_primary_spectra.txt +502 -0
  162. gammapbh/blackhawk_data/2.0e+18/instantaneous_secondary_spectra.txt +378 -0
  163. gammapbh/blackhawk_data/2.0e+18/instantaneous_total_spectra.txt +502 -0
  164. gammapbh/blackhawk_data/3.0e+14/3.0e+14.txt +59 -0
  165. gammapbh/blackhawk_data/3.0e+14/BH_spectrum.txt +4 -0
  166. gammapbh/blackhawk_data/3.0e+14/Probabilities.txt +346 -0
  167. gammapbh/blackhawk_data/3.0e+14/final_state_radiation_prim.txt +216 -0
  168. gammapbh/blackhawk_data/3.0e+14/final_state_radiation_sec.txt +134 -0
  169. gammapbh/blackhawk_data/3.0e+14/inflight_annihilation_prim.txt +216 -0
  170. gammapbh/blackhawk_data/3.0e+14/inflight_annihilation_sec.txt +134 -0
  171. gammapbh/blackhawk_data/3.0e+14/instantaneous_primary_spectra.txt +502 -0
  172. gammapbh/blackhawk_data/3.0e+14/instantaneous_secondary_spectra.txt +378 -0
  173. gammapbh/blackhawk_data/3.0e+14/instantaneous_total_spectra.txt +502 -0
  174. gammapbh/blackhawk_data/3.0e+15/3.0e+15.txt +59 -0
  175. gammapbh/blackhawk_data/3.0e+15/BH_spectrum.txt +4 -0
  176. gammapbh/blackhawk_data/3.0e+15/Probabilities.txt +346 -0
  177. gammapbh/blackhawk_data/3.0e+15/final_state_radiation_prim.txt +171 -0
  178. gammapbh/blackhawk_data/3.0e+15/final_state_radiation_sec.txt +171 -0
  179. gammapbh/blackhawk_data/3.0e+15/inflight_annihilation_prim.txt +171 -0
  180. gammapbh/blackhawk_data/3.0e+15/inflight_annihilation_sec.txt +171 -0
  181. gammapbh/blackhawk_data/3.0e+15/instantaneous_primary_spectra.txt +502 -0
  182. gammapbh/blackhawk_data/3.0e+15/instantaneous_secondary_spectra.txt +378 -0
  183. gammapbh/blackhawk_data/3.0e+15/instantaneous_total_spectra.txt +502 -0
  184. gammapbh/blackhawk_data/3.0e+15/list5.txt +172 -0
  185. gammapbh/blackhawk_data/3.0e+16/3.0e+16.txt +59 -0
  186. gammapbh/blackhawk_data/3.0e+16/BH_spectrum.txt +4 -0
  187. gammapbh/blackhawk_data/3.0e+16/Probabilities.txt +346 -0
  188. gammapbh/blackhawk_data/3.0e+16/final_state_radiation_prim.txt +126 -0
  189. gammapbh/blackhawk_data/3.0e+16/final_state_radiation_sec.txt +125 -0
  190. gammapbh/blackhawk_data/3.0e+16/inflight_annihilation_prim.txt +126 -0
  191. gammapbh/blackhawk_data/3.0e+16/inflight_annihilation_sec.txt +125 -0
  192. gammapbh/blackhawk_data/3.0e+16/instantaneous_primary_spectra.txt +502 -0
  193. gammapbh/blackhawk_data/3.0e+16/instantaneous_secondary_spectra.txt +378 -0
  194. gammapbh/blackhawk_data/3.0e+16/instantaneous_total_spectra.txt +502 -0
  195. gammapbh/blackhawk_data/3.0e+17/3.0e+17.txt +59 -0
  196. gammapbh/blackhawk_data/3.0e+17/BH_spectrum.txt +4 -0
  197. gammapbh/blackhawk_data/3.0e+17/Probabilities.txt +346 -0
  198. gammapbh/blackhawk_data/3.0e+17/final_state_radiation_prim.txt +80 -0
  199. gammapbh/blackhawk_data/3.0e+17/final_state_radiation_sec.txt +80 -0
  200. gammapbh/blackhawk_data/3.0e+17/inflight_annihilation_prim.txt +80 -0
  201. gammapbh/blackhawk_data/3.0e+17/inflight_annihilation_sec.txt +80 -0
  202. gammapbh/blackhawk_data/3.0e+17/instantaneous_primary_spectra.txt +502 -0
  203. gammapbh/blackhawk_data/3.0e+17/instantaneous_secondary_spectra.txt +378 -0
  204. gammapbh/blackhawk_data/3.0e+17/instantaneous_total_spectra.txt +502 -0
  205. gammapbh/blackhawk_data/3.0e+18/3.0e+18.txt +59 -0
  206. gammapbh/blackhawk_data/3.0e+18/BH_spectrum.txt +4 -0
  207. gammapbh/blackhawk_data/3.0e+18/final_state_radiation_prim.txt +35 -0
  208. gammapbh/blackhawk_data/3.0e+18/final_state_radiation_sec.txt +35 -0
  209. gammapbh/blackhawk_data/3.0e+18/inflight_annihilation_prim.txt +35 -0
  210. gammapbh/blackhawk_data/3.0e+18/inflight_annihilation_sec.txt +35 -0
  211. gammapbh/blackhawk_data/3.0e+18/instantaneous_primary_spectra.txt +502 -0
  212. gammapbh/blackhawk_data/3.0e+18/instantaneous_secondary_spectra.txt +378 -0
  213. gammapbh/blackhawk_data/3.0e+18/instantaneous_total_spectra.txt +502 -0
  214. gammapbh/blackhawk_data/4.0e+14/4.0e+14.txt +59 -0
  215. gammapbh/blackhawk_data/4.0e+14/BH_spectrum.txt +4 -0
  216. gammapbh/blackhawk_data/4.0e+14/Probabilities.txt +346 -0
  217. gammapbh/blackhawk_data/4.0e+14/final_state_radiation_prim.txt +211 -0
  218. gammapbh/blackhawk_data/4.0e+14/final_state_radiation_sec.txt +123 -0
  219. gammapbh/blackhawk_data/4.0e+14/inflight_annihilation_prim.txt +211 -0
  220. gammapbh/blackhawk_data/4.0e+14/inflight_annihilation_sec.txt +123 -0
  221. gammapbh/blackhawk_data/4.0e+14/instantaneous_primary_spectra.txt +502 -0
  222. gammapbh/blackhawk_data/4.0e+14/instantaneous_secondary_spectra.txt +378 -0
  223. gammapbh/blackhawk_data/4.0e+14/instantaneous_total_spectra.txt +502 -0
  224. gammapbh/blackhawk_data/4.0e+15/4e+15.txt +59 -0
  225. gammapbh/blackhawk_data/4.0e+15/BH_spectrum.txt +4 -0
  226. gammapbh/blackhawk_data/4.0e+15/Probabilities.txt +346 -0
  227. gammapbh/blackhawk_data/4.0e+15/final_state_radiation_prim.txt +165 -0
  228. gammapbh/blackhawk_data/4.0e+15/final_state_radiation_sec.txt +165 -0
  229. gammapbh/blackhawk_data/4.0e+15/inflight_annihilation_prim.txt +165 -0
  230. gammapbh/blackhawk_data/4.0e+15/inflight_annihilation_sec.txt +165 -0
  231. gammapbh/blackhawk_data/4.0e+15/instantaneous_primary_spectra.txt +502 -0
  232. gammapbh/blackhawk_data/4.0e+15/instantaneous_secondary_spectra.txt +378 -0
  233. gammapbh/blackhawk_data/4.0e+15/instantaneous_total_spectra.txt +502 -0
  234. gammapbh/blackhawk_data/4.0e+16/4.0e+16.txt +59 -0
  235. gammapbh/blackhawk_data/4.0e+16/BH_spectrum.txt +4 -0
  236. gammapbh/blackhawk_data/4.0e+16/Probabilities.txt +346 -0
  237. gammapbh/blackhawk_data/4.0e+16/final_state_radiation_prim.txt +120 -0
  238. gammapbh/blackhawk_data/4.0e+16/final_state_radiation_sec.txt +120 -0
  239. gammapbh/blackhawk_data/4.0e+16/inflight_annihilation_prim.txt +120 -0
  240. gammapbh/blackhawk_data/4.0e+16/inflight_annihilation_sec.txt +120 -0
  241. gammapbh/blackhawk_data/4.0e+16/instantaneous_primary_spectra.txt +502 -0
  242. gammapbh/blackhawk_data/4.0e+16/instantaneous_secondary_spectra.txt +378 -0
  243. gammapbh/blackhawk_data/4.0e+16/instantaneous_total_spectra.txt +502 -0
  244. gammapbh/blackhawk_data/4.0e+17/4.0e+17.txt +59 -0
  245. gammapbh/blackhawk_data/4.0e+17/BH_spectrum.txt +4 -0
  246. gammapbh/blackhawk_data/4.0e+17/Probabilities.txt +346 -0
  247. gammapbh/blackhawk_data/4.0e+17/final_state_radiation_prim.txt +75 -0
  248. gammapbh/blackhawk_data/4.0e+17/final_state_radiation_sec.txt +75 -0
  249. gammapbh/blackhawk_data/4.0e+17/inflight_annihilation_prim.txt +75 -0
  250. gammapbh/blackhawk_data/4.0e+17/inflight_annihilation_sec.txt +75 -0
  251. gammapbh/blackhawk_data/4.0e+17/instantaneous_primary_spectra.txt +502 -0
  252. gammapbh/blackhawk_data/4.0e+17/instantaneous_secondary_spectra.txt +378 -0
  253. gammapbh/blackhawk_data/4.0e+17/instantaneous_total_spectra.txt +502 -0
  254. gammapbh/blackhawk_data/4.0e+18/4.0e+18.txt +59 -0
  255. gammapbh/blackhawk_data/4.0e+18/BH_spectrum.txt +4 -0
  256. gammapbh/blackhawk_data/4.0e+18/final_state_radiation_prim.txt +29 -0
  257. gammapbh/blackhawk_data/4.0e+18/final_state_radiation_sec.txt +30 -0
  258. gammapbh/blackhawk_data/4.0e+18/inflight_annihilation_prim.txt +29 -0
  259. gammapbh/blackhawk_data/4.0e+18/inflight_annihilation_sec.txt +30 -0
  260. gammapbh/blackhawk_data/4.0e+18/instantaneous_primary_spectra.txt +502 -0
  261. gammapbh/blackhawk_data/4.0e+18/instantaneous_secondary_spectra.txt +378 -0
  262. gammapbh/blackhawk_data/4.0e+18/instantaneous_total_spectra.txt +502 -0
  263. gammapbh/blackhawk_data/5.0e+13/5.0e+13.txt +59 -0
  264. gammapbh/blackhawk_data/5.0e+13/BH_spectrum.txt +4 -0
  265. gammapbh/blackhawk_data/5.0e+13/final_state_radiation_prim.txt +252 -0
  266. gammapbh/blackhawk_data/5.0e+13/final_state_radiation_sec.txt +179 -0
  267. gammapbh/blackhawk_data/5.0e+13/inflight_annihilation_prim.txt +252 -0
  268. gammapbh/blackhawk_data/5.0e+13/inflight_annihilation_sec.txt +179 -0
  269. gammapbh/blackhawk_data/5.0e+13/instantaneous_primary_spectra.txt +502 -0
  270. gammapbh/blackhawk_data/5.0e+13/instantaneous_secondary_spectra.txt +378 -0
  271. gammapbh/blackhawk_data/5.0e+13/instantaneous_total_spectra.txt +502 -0
  272. gammapbh/blackhawk_data/5.0e+14/5.0e+14.txt +59 -0
  273. gammapbh/blackhawk_data/5.0e+14/BH_spectrum.txt +4 -0
  274. gammapbh/blackhawk_data/5.0e+14/Probabilities.txt +346 -0
  275. gammapbh/blackhawk_data/5.0e+14/final_state_radiation_prim.txt +206 -0
  276. gammapbh/blackhawk_data/5.0e+14/final_state_radiation_sec.txt +119 -0
  277. gammapbh/blackhawk_data/5.0e+14/inflight_annihilation_prim.txt +206 -0
  278. gammapbh/blackhawk_data/5.0e+14/inflight_annihilation_sec.txt +119 -0
  279. gammapbh/blackhawk_data/5.0e+14/instantaneous_primary_spectra.txt +502 -0
  280. gammapbh/blackhawk_data/5.0e+14/instantaneous_secondary_spectra.txt +378 -0
  281. gammapbh/blackhawk_data/5.0e+14/instantaneous_total_spectra.txt +502 -0
  282. gammapbh/blackhawk_data/5.0e+15/5e+15.txt +59 -0
  283. gammapbh/blackhawk_data/5.0e+15/BH_spectrum.txt +4 -0
  284. gammapbh/blackhawk_data/5.0e+15/Probabilities.txt +346 -0
  285. gammapbh/blackhawk_data/5.0e+15/final_state_radiation_prim.txt +161 -0
  286. gammapbh/blackhawk_data/5.0e+15/final_state_radiation_sec.txt +160 -0
  287. gammapbh/blackhawk_data/5.0e+15/inflight_annihilation_prim.txt +161 -0
  288. gammapbh/blackhawk_data/5.0e+15/inflight_annihilation_sec.txt +160 -0
  289. gammapbh/blackhawk_data/5.0e+15/instantaneous_primary_spectra.txt +502 -0
  290. gammapbh/blackhawk_data/5.0e+15/instantaneous_secondary_spectra.txt +378 -0
  291. gammapbh/blackhawk_data/5.0e+15/instantaneous_total_spectra.txt +502 -0
  292. gammapbh/blackhawk_data/5.0e+16/5.0e+16.txt +59 -0
  293. gammapbh/blackhawk_data/5.0e+16/BH_spectrum.txt +4 -0
  294. gammapbh/blackhawk_data/5.0e+16/Probabilities.txt +346 -0
  295. gammapbh/blackhawk_data/5.0e+16/final_state_radiation_prim.txt +116 -0
  296. gammapbh/blackhawk_data/5.0e+16/final_state_radiation_sec.txt +115 -0
  297. gammapbh/blackhawk_data/5.0e+16/inflight_annihilation_prim.txt +116 -0
  298. gammapbh/blackhawk_data/5.0e+16/inflight_annihilation_sec.txt +115 -0
  299. gammapbh/blackhawk_data/5.0e+16/instantaneous_primary_spectra.txt +502 -0
  300. gammapbh/blackhawk_data/5.0e+16/instantaneous_secondary_spectra.txt +378 -0
  301. gammapbh/blackhawk_data/5.0e+16/instantaneous_total_spectra.txt +502 -0
  302. gammapbh/blackhawk_data/5.0e+17/5.0e+17.txt +59 -0
  303. gammapbh/blackhawk_data/5.0e+17/BH_spectrum.txt +4 -0
  304. gammapbh/blackhawk_data/5.0e+17/Probabilities.txt +346 -0
  305. gammapbh/blackhawk_data/5.0e+17/final_state_radiation_prim.txt +70 -0
  306. gammapbh/blackhawk_data/5.0e+17/final_state_radiation_sec.txt +69 -0
  307. gammapbh/blackhawk_data/5.0e+17/inflight_annihilation_prim.txt +70 -0
  308. gammapbh/blackhawk_data/5.0e+17/inflight_annihilation_sec.txt +69 -0
  309. gammapbh/blackhawk_data/5.0e+17/instantaneous_primary_spectra.txt +502 -0
  310. gammapbh/blackhawk_data/5.0e+17/instantaneous_secondary_spectra.txt +378 -0
  311. gammapbh/blackhawk_data/5.0e+17/instantaneous_total_spectra.txt +502 -0
  312. gammapbh/blackhawk_data/5.0e+18/5.0e+18.txt +59 -0
  313. gammapbh/blackhawk_data/5.0e+18/BH_spectrum.txt +4 -0
  314. gammapbh/blackhawk_data/5.0e+18/final_state_radiation_prim.txt +25 -0
  315. gammapbh/blackhawk_data/5.0e+18/final_state_radiation_sec.txt +25 -0
  316. gammapbh/blackhawk_data/5.0e+18/inflight_annihilation_prim.txt +25 -0
  317. gammapbh/blackhawk_data/5.0e+18/inflight_annihilation_sec.txt +25 -0
  318. gammapbh/blackhawk_data/5.0e+18/instantaneous_primary_spectra.txt +502 -0
  319. gammapbh/blackhawk_data/5.0e+18/instantaneous_secondary_spectra.txt +378 -0
  320. gammapbh/blackhawk_data/5.0e+18/instantaneous_total_spectra.txt +502 -0
  321. gammapbh/blackhawk_data/6.0e+13/6.0e+13.txt +59 -0
  322. gammapbh/blackhawk_data/6.0e+13/BH_spectrum.txt +4 -0
  323. gammapbh/blackhawk_data/6.0e+13/final_state_radiation_prim.txt +248 -0
  324. gammapbh/blackhawk_data/6.0e+13/final_state_radiation_sec.txt +166 -0
  325. gammapbh/blackhawk_data/6.0e+13/inflight_annihilation_prim.txt +248 -0
  326. gammapbh/blackhawk_data/6.0e+13/inflight_annihilation_sec.txt +166 -0
  327. gammapbh/blackhawk_data/6.0e+13/instantaneous_primary_spectra.txt +502 -0
  328. gammapbh/blackhawk_data/6.0e+13/instantaneous_secondary_spectra.txt +378 -0
  329. gammapbh/blackhawk_data/6.0e+13/instantaneous_total_spectra.txt +502 -0
  330. gammapbh/blackhawk_data/6.0e+14/6.0e+14.txt +59 -0
  331. gammapbh/blackhawk_data/6.0e+14/BH_spectrum.txt +4 -0
  332. gammapbh/blackhawk_data/6.0e+14/Probabilities.txt +346 -0
  333. gammapbh/blackhawk_data/6.0e+14/final_state_radiation_prim.txt +203 -0
  334. gammapbh/blackhawk_data/6.0e+14/final_state_radiation_sec.txt +111 -0
  335. gammapbh/blackhawk_data/6.0e+14/inflight_annihilation_prim.txt +203 -0
  336. gammapbh/blackhawk_data/6.0e+14/inflight_annihilation_sec.txt +111 -0
  337. gammapbh/blackhawk_data/6.0e+14/instantaneous_primary_spectra.txt +502 -0
  338. gammapbh/blackhawk_data/6.0e+14/instantaneous_secondary_spectra.txt +378 -0
  339. gammapbh/blackhawk_data/6.0e+14/instantaneous_total_spectra.txt +502 -0
  340. gammapbh/blackhawk_data/6.0e+15/6e+15.txt +59 -0
  341. gammapbh/blackhawk_data/6.0e+15/BH_spectrum.txt +4 -0
  342. gammapbh/blackhawk_data/6.0e+15/Probabilities.txt +346 -0
  343. gammapbh/blackhawk_data/6.0e+15/final_state_radiation_prim.txt +157 -0
  344. gammapbh/blackhawk_data/6.0e+15/final_state_radiation_sec.txt +156 -0
  345. gammapbh/blackhawk_data/6.0e+15/inflight_annihilation_prim.txt +157 -0
  346. gammapbh/blackhawk_data/6.0e+15/inflight_annihilation_sec.txt +156 -0
  347. gammapbh/blackhawk_data/6.0e+15/instantaneous_primary_spectra.txt +502 -0
  348. gammapbh/blackhawk_data/6.0e+15/instantaneous_secondary_spectra.txt +378 -0
  349. gammapbh/blackhawk_data/6.0e+15/instantaneous_total_spectra.txt +502 -0
  350. gammapbh/blackhawk_data/6.0e+16/6.0e+16.txt +59 -0
  351. gammapbh/blackhawk_data/6.0e+16/BH_spectrum.txt +4 -0
  352. gammapbh/blackhawk_data/6.0e+16/Probabilities.txt +346 -0
  353. gammapbh/blackhawk_data/6.0e+16/final_state_radiation_prim.txt +112 -0
  354. gammapbh/blackhawk_data/6.0e+16/final_state_radiation_sec.txt +111 -0
  355. gammapbh/blackhawk_data/6.0e+16/inflight_annihilation_prim.txt +112 -0
  356. gammapbh/blackhawk_data/6.0e+16/inflight_annihilation_sec.txt +111 -0
  357. gammapbh/blackhawk_data/6.0e+16/instantaneous_primary_spectra.txt +502 -0
  358. gammapbh/blackhawk_data/6.0e+16/instantaneous_secondary_spectra.txt +378 -0
  359. gammapbh/blackhawk_data/6.0e+16/instantaneous_total_spectra.txt +502 -0
  360. gammapbh/blackhawk_data/6.0e+17/6.0e+17.txt +59 -0
  361. gammapbh/blackhawk_data/6.0e+17/BH_spectrum.txt +4 -0
  362. gammapbh/blackhawk_data/6.0e+17/Probabilities.txt +346 -0
  363. gammapbh/blackhawk_data/6.0e+17/final_state_radiation_prim.txt +67 -0
  364. gammapbh/blackhawk_data/6.0e+17/final_state_radiation_sec.txt +67 -0
  365. gammapbh/blackhawk_data/6.0e+17/inflight_annihilation_prim.txt +67 -0
  366. gammapbh/blackhawk_data/6.0e+17/inflight_annihilation_sec.txt +67 -0
  367. gammapbh/blackhawk_data/6.0e+17/instantaneous_primary_spectra.txt +502 -0
  368. gammapbh/blackhawk_data/6.0e+17/instantaneous_secondary_spectra.txt +378 -0
  369. gammapbh/blackhawk_data/6.0e+17/instantaneous_total_spectra.txt +502 -0
  370. gammapbh/blackhawk_data/6.0e+18/6.0e+18.txt +59 -0
  371. gammapbh/blackhawk_data/6.0e+18/BH_spectrum.txt +4 -0
  372. gammapbh/blackhawk_data/6.0e+18/final_state_radiation_prim.txt +21 -0
  373. gammapbh/blackhawk_data/6.0e+18/final_state_radiation_sec.txt +22 -0
  374. gammapbh/blackhawk_data/6.0e+18/inflight_annihilation_prim.txt +21 -0
  375. gammapbh/blackhawk_data/6.0e+18/inflight_annihilation_sec.txt +22 -0
  376. gammapbh/blackhawk_data/6.0e+18/instantaneous_primary_spectra.txt +502 -0
  377. gammapbh/blackhawk_data/6.0e+18/instantaneous_secondary_spectra.txt +378 -0
  378. gammapbh/blackhawk_data/6.0e+18/instantaneous_total_spectra.txt +502 -0
  379. gammapbh/blackhawk_data/7.0e+13/7.0e+13.txt +59 -0
  380. gammapbh/blackhawk_data/7.0e+13/BH_spectrum.txt +4 -0
  381. gammapbh/blackhawk_data/7.0e+13/final_state_radiation_prim.txt +245 -0
  382. gammapbh/blackhawk_data/7.0e+13/final_state_radiation_sec.txt +169 -0
  383. gammapbh/blackhawk_data/7.0e+13/inflight_annihilation_prim.txt +245 -0
  384. gammapbh/blackhawk_data/7.0e+13/inflight_annihilation_sec.txt +169 -0
  385. gammapbh/blackhawk_data/7.0e+13/instantaneous_primary_spectra.txt +502 -0
  386. gammapbh/blackhawk_data/7.0e+13/instantaneous_secondary_spectra.txt +378 -0
  387. gammapbh/blackhawk_data/7.0e+13/instantaneous_total_spectra.txt +502 -0
  388. gammapbh/blackhawk_data/7.0e+14/7.0e+14.txt +59 -0
  389. gammapbh/blackhawk_data/7.0e+14/BH_spectrum.txt +4 -0
  390. gammapbh/blackhawk_data/7.0e+14/Probabilities.txt +346 -0
  391. gammapbh/blackhawk_data/7.0e+14/final_state_radiation_prim.txt +200 -0
  392. gammapbh/blackhawk_data/7.0e+14/final_state_radiation_sec.txt +113 -0
  393. gammapbh/blackhawk_data/7.0e+14/inflight_annihilation_prim.txt +200 -0
  394. gammapbh/blackhawk_data/7.0e+14/inflight_annihilation_sec.txt +113 -0
  395. gammapbh/blackhawk_data/7.0e+14/instantaneous_primary_spectra.txt +502 -0
  396. gammapbh/blackhawk_data/7.0e+14/instantaneous_secondary_spectra.txt +378 -0
  397. gammapbh/blackhawk_data/7.0e+14/instantaneous_total_spectra.txt +502 -0
  398. gammapbh/blackhawk_data/7.0e+15/7e+15.txt +59 -0
  399. gammapbh/blackhawk_data/7.0e+15/BH_spectrum.txt +4 -0
  400. gammapbh/blackhawk_data/7.0e+15/Probabilities.txt +346 -0
  401. gammapbh/blackhawk_data/7.0e+15/final_state_radiation_prim.txt +154 -0
  402. gammapbh/blackhawk_data/7.0e+15/final_state_radiation_sec.txt +153 -0
  403. gammapbh/blackhawk_data/7.0e+15/inflight_annihilation_prim.txt +154 -0
  404. gammapbh/blackhawk_data/7.0e+15/inflight_annihilation_sec.txt +153 -0
  405. gammapbh/blackhawk_data/7.0e+15/instantaneous_primary_spectra.txt +502 -0
  406. gammapbh/blackhawk_data/7.0e+15/instantaneous_secondary_spectra.txt +378 -0
  407. gammapbh/blackhawk_data/7.0e+15/instantaneous_total_spectra.txt +502 -0
  408. gammapbh/blackhawk_data/7.0e+16/7.0e+16.txt +59 -0
  409. gammapbh/blackhawk_data/7.0e+16/BH_spectrum.txt +4 -0
  410. gammapbh/blackhawk_data/7.0e+16/Probabilities.txt +346 -0
  411. gammapbh/blackhawk_data/7.0e+16/final_state_radiation_prim.txt +109 -0
  412. gammapbh/blackhawk_data/7.0e+16/final_state_radiation_sec.txt +109 -0
  413. gammapbh/blackhawk_data/7.0e+16/inflight_annihilation_prim.txt +109 -0
  414. gammapbh/blackhawk_data/7.0e+16/inflight_annihilation_sec.txt +109 -0
  415. gammapbh/blackhawk_data/7.0e+16/instantaneous_primary_spectra.txt +502 -0
  416. gammapbh/blackhawk_data/7.0e+16/instantaneous_secondary_spectra.txt +378 -0
  417. gammapbh/blackhawk_data/7.0e+16/instantaneous_total_spectra.txt +502 -0
  418. gammapbh/blackhawk_data/7.0e+17/7.0e+17.txt +59 -0
  419. gammapbh/blackhawk_data/7.0e+17/BH_spectrum.txt +4 -0
  420. gammapbh/blackhawk_data/7.0e+17/Probabilities.txt +346 -0
  421. gammapbh/blackhawk_data/7.0e+17/final_state_radiation_prim.txt +64 -0
  422. gammapbh/blackhawk_data/7.0e+17/final_state_radiation_sec.txt +63 -0
  423. gammapbh/blackhawk_data/7.0e+17/inflight_annihilation_prim.txt +64 -0
  424. gammapbh/blackhawk_data/7.0e+17/inflight_annihilation_sec.txt +63 -0
  425. gammapbh/blackhawk_data/7.0e+17/instantaneous_primary_spectra.txt +502 -0
  426. gammapbh/blackhawk_data/7.0e+17/instantaneous_secondary_spectra.txt +378 -0
  427. gammapbh/blackhawk_data/7.0e+17/instantaneous_total_spectra.txt +502 -0
  428. gammapbh/blackhawk_data/7.0e+18/7.0e+18.txt +59 -0
  429. gammapbh/blackhawk_data/7.0e+18/BH_spectrum.txt +4 -0
  430. gammapbh/blackhawk_data/7.0e+18/final_state_radiation_prim.txt +18 -0
  431. gammapbh/blackhawk_data/7.0e+18/final_state_radiation_sec.txt +19 -0
  432. gammapbh/blackhawk_data/7.0e+18/inflight_annihilation_prim.txt +18 -0
  433. gammapbh/blackhawk_data/7.0e+18/inflight_annihilation_sec.txt +19 -0
  434. gammapbh/blackhawk_data/7.0e+18/instantaneous_primary_spectra.txt +502 -0
  435. gammapbh/blackhawk_data/7.0e+18/instantaneous_secondary_spectra.txt +378 -0
  436. gammapbh/blackhawk_data/7.0e+18/instantaneous_total_spectra.txt +502 -0
  437. gammapbh/blackhawk_data/8.0e+13/8.0e+13.txt +59 -0
  438. gammapbh/blackhawk_data/8.0e+13/BH_spectrum.txt +4 -0
  439. gammapbh/blackhawk_data/8.0e+13/final_state_radiation_prim.txt +242 -0
  440. gammapbh/blackhawk_data/8.0e+13/final_state_radiation_sec.txt +157 -0
  441. gammapbh/blackhawk_data/8.0e+13/inflight_annihilation_prim.txt +242 -0
  442. gammapbh/blackhawk_data/8.0e+13/inflight_annihilation_sec.txt +157 -0
  443. gammapbh/blackhawk_data/8.0e+13/instantaneous_primary_spectra.txt +502 -0
  444. gammapbh/blackhawk_data/8.0e+13/instantaneous_secondary_spectra.txt +378 -0
  445. gammapbh/blackhawk_data/8.0e+13/instantaneous_total_spectra.txt +502 -0
  446. gammapbh/blackhawk_data/8.0e+14/8.0e+14.txt +59 -0
  447. gammapbh/blackhawk_data/8.0e+14/BH_spectrum.txt +4 -0
  448. gammapbh/blackhawk_data/8.0e+14/Probabilities.txt +346 -0
  449. gammapbh/blackhawk_data/8.0e+14/final_state_radiation_prim.txt +197 -0
  450. gammapbh/blackhawk_data/8.0e+14/final_state_radiation_sec.txt +122 -0
  451. gammapbh/blackhawk_data/8.0e+14/inflight_annihilation_prim.txt +197 -0
  452. gammapbh/blackhawk_data/8.0e+14/inflight_annihilation_sec.txt +122 -0
  453. gammapbh/blackhawk_data/8.0e+14/instantaneous_primary_spectra.txt +502 -0
  454. gammapbh/blackhawk_data/8.0e+14/instantaneous_secondary_spectra.txt +378 -0
  455. gammapbh/blackhawk_data/8.0e+14/instantaneous_total_spectra.txt +502 -0
  456. gammapbh/blackhawk_data/8.0e+15/8e+15.txt +59 -0
  457. gammapbh/blackhawk_data/8.0e+15/BH_spectrum.txt +4 -0
  458. gammapbh/blackhawk_data/8.0e+15/Probabilities.txt +346 -0
  459. gammapbh/blackhawk_data/8.0e+15/final_state_radiation_prim.txt +152 -0
  460. gammapbh/blackhawk_data/8.0e+15/final_state_radiation_sec.txt +152 -0
  461. gammapbh/blackhawk_data/8.0e+15/inflight_annihilation_prim.txt +152 -0
  462. gammapbh/blackhawk_data/8.0e+15/inflight_annihilation_sec.txt +152 -0
  463. gammapbh/blackhawk_data/8.0e+15/instantaneous_primary_spectra.txt +502 -0
  464. gammapbh/blackhawk_data/8.0e+15/instantaneous_secondary_spectra.txt +378 -0
  465. gammapbh/blackhawk_data/8.0e+15/instantaneous_total_spectra.txt +502 -0
  466. gammapbh/blackhawk_data/8.0e+16/8.0e+16.txt +59 -0
  467. gammapbh/blackhawk_data/8.0e+16/BH_spectrum.txt +4 -0
  468. gammapbh/blackhawk_data/8.0e+16/Probabilities.txt +346 -0
  469. gammapbh/blackhawk_data/8.0e+16/final_state_radiation_prim.txt +106 -0
  470. gammapbh/blackhawk_data/8.0e+16/final_state_radiation_sec.txt +106 -0
  471. gammapbh/blackhawk_data/8.0e+16/inflight_annihilation_prim.txt +106 -0
  472. gammapbh/blackhawk_data/8.0e+16/inflight_annihilation_sec.txt +106 -0
  473. gammapbh/blackhawk_data/8.0e+16/instantaneous_primary_spectra.txt +502 -0
  474. gammapbh/blackhawk_data/8.0e+16/instantaneous_secondary_spectra.txt +378 -0
  475. gammapbh/blackhawk_data/8.0e+16/instantaneous_total_spectra.txt +502 -0
  476. gammapbh/blackhawk_data/8.0e+17/8.0e+17.txt +59 -0
  477. gammapbh/blackhawk_data/8.0e+17/BH_spectrum.txt +4 -0
  478. gammapbh/blackhawk_data/8.0e+17/Probabilities.txt +346 -0
  479. gammapbh/blackhawk_data/8.0e+17/final_state_radiation_prim.txt +61 -0
  480. gammapbh/blackhawk_data/8.0e+17/final_state_radiation_sec.txt +61 -0
  481. gammapbh/blackhawk_data/8.0e+17/inflight_annihilation_prim.txt +61 -0
  482. gammapbh/blackhawk_data/8.0e+17/inflight_annihilation_sec.txt +61 -0
  483. gammapbh/blackhawk_data/8.0e+17/instantaneous_primary_spectra.txt +502 -0
  484. gammapbh/blackhawk_data/8.0e+17/instantaneous_secondary_spectra.txt +378 -0
  485. gammapbh/blackhawk_data/8.0e+17/instantaneous_total_spectra.txt +502 -0
  486. gammapbh/blackhawk_data/8.0e+18/8.0e+18.txt +59 -0
  487. gammapbh/blackhawk_data/8.0e+18/BH_spectrum.txt +4 -0
  488. gammapbh/blackhawk_data/8.0e+18/final_state_radiation_prim.txt +16 -0
  489. gammapbh/blackhawk_data/8.0e+18/final_state_radiation_sec.txt +17 -0
  490. gammapbh/blackhawk_data/8.0e+18/inflight_annihilation_prim.txt +16 -0
  491. gammapbh/blackhawk_data/8.0e+18/inflight_annihilation_sec.txt +17 -0
  492. gammapbh/blackhawk_data/8.0e+18/instantaneous_primary_spectra.txt +502 -0
  493. gammapbh/blackhawk_data/8.0e+18/instantaneous_secondary_spectra.txt +378 -0
  494. gammapbh/blackhawk_data/8.0e+18/instantaneous_total_spectra.txt +502 -0
  495. gammapbh/blackhawk_data/9.0e+13/9.0e+13.txt +59 -0
  496. gammapbh/blackhawk_data/9.0e+13/BH_spectrum.txt +4 -0
  497. gammapbh/blackhawk_data/9.0e+13/final_state_radiation_prim.txt +240 -0
  498. gammapbh/blackhawk_data/9.0e+13/final_state_radiation_sec.txt +156 -0
  499. gammapbh/blackhawk_data/9.0e+13/inflight_annihilation_prim.txt +240 -0
  500. gammapbh/blackhawk_data/9.0e+13/inflight_annihilation_sec.txt +156 -0
  501. gammapbh/blackhawk_data/9.0e+13/instantaneous_primary_spectra.txt +502 -0
  502. gammapbh/blackhawk_data/9.0e+13/instantaneous_secondary_spectra.txt +378 -0
  503. gammapbh/blackhawk_data/9.0e+13/instantaneous_total_spectra.txt +502 -0
  504. gammapbh/blackhawk_data/9.0e+14/9.0e+14.txt +59 -0
  505. gammapbh/blackhawk_data/9.0e+14/BH_spectrum.txt +4 -0
  506. gammapbh/blackhawk_data/9.0e+14/Probabilities.txt +346 -0
  507. gammapbh/blackhawk_data/9.0e+14/final_state_radiation_prim.txt +195 -0
  508. gammapbh/blackhawk_data/9.0e+14/final_state_radiation_sec.txt +122 -0
  509. gammapbh/blackhawk_data/9.0e+14/inflight_annihilation_prim.txt +195 -0
  510. gammapbh/blackhawk_data/9.0e+14/inflight_annihilation_sec.txt +122 -0
  511. gammapbh/blackhawk_data/9.0e+14/instantaneous_primary_spectra.txt +502 -0
  512. gammapbh/blackhawk_data/9.0e+14/instantaneous_secondary_spectra.txt +378 -0
  513. gammapbh/blackhawk_data/9.0e+14/instantaneous_total_spectra.txt +502 -0
  514. gammapbh/blackhawk_data/9.0e+15/9e+15.txt +59 -0
  515. gammapbh/blackhawk_data/9.0e+15/BH_spectrum.txt +4 -0
  516. gammapbh/blackhawk_data/9.0e+15/Probabilities.txt +346 -0
  517. gammapbh/blackhawk_data/9.0e+15/final_state_radiation_prim.txt +149 -0
  518. gammapbh/blackhawk_data/9.0e+15/final_state_radiation_sec.txt +148 -0
  519. gammapbh/blackhawk_data/9.0e+15/inflight_annihilation_prim.txt +149 -0
  520. gammapbh/blackhawk_data/9.0e+15/inflight_annihilation_sec.txt +148 -0
  521. gammapbh/blackhawk_data/9.0e+15/instantaneous_primary_spectra.txt +502 -0
  522. gammapbh/blackhawk_data/9.0e+15/instantaneous_secondary_spectra.txt +378 -0
  523. gammapbh/blackhawk_data/9.0e+15/instantaneous_total_spectra.txt +502 -0
  524. gammapbh/blackhawk_data/9.0e+16/9.0e+16.txt +59 -0
  525. gammapbh/blackhawk_data/9.0e+16/BH_spectrum.txt +4 -0
  526. gammapbh/blackhawk_data/9.0e+16/Probabilities.txt +346 -0
  527. gammapbh/blackhawk_data/9.0e+16/final_state_radiation_prim.txt +104 -0
  528. gammapbh/blackhawk_data/9.0e+16/final_state_radiation_sec.txt +103 -0
  529. gammapbh/blackhawk_data/9.0e+16/inflight_annihilation_prim.txt +104 -0
  530. gammapbh/blackhawk_data/9.0e+16/inflight_annihilation_sec.txt +103 -0
  531. gammapbh/blackhawk_data/9.0e+16/instantaneous_primary_spectra.txt +502 -0
  532. gammapbh/blackhawk_data/9.0e+16/instantaneous_secondary_spectra.txt +378 -0
  533. gammapbh/blackhawk_data/9.0e+16/instantaneous_total_spectra.txt +502 -0
  534. gammapbh/blackhawk_data/9.0e+17/9.0e+17.txt +59 -0
  535. gammapbh/blackhawk_data/9.0e+17/BH_spectrum.txt +4 -0
  536. gammapbh/blackhawk_data/9.0e+17/Probabilities.txt +346 -0
  537. gammapbh/blackhawk_data/9.0e+17/final_state_radiation_prim.txt +59 -0
  538. gammapbh/blackhawk_data/9.0e+17/final_state_radiation_sec.txt +59 -0
  539. gammapbh/blackhawk_data/9.0e+17/inflight_annihilation_prim.txt +59 -0
  540. gammapbh/blackhawk_data/9.0e+17/inflight_annihilation_sec.txt +59 -0
  541. gammapbh/blackhawk_data/9.0e+17/instantaneous_primary_spectra.txt +502 -0
  542. gammapbh/blackhawk_data/9.0e+17/instantaneous_secondary_spectra.txt +378 -0
  543. gammapbh/blackhawk_data/9.0e+17/instantaneous_total_spectra.txt +502 -0
  544. gammapbh/blackhawk_data/9.0e+18/9.0e+18.txt +59 -0
  545. gammapbh/blackhawk_data/9.0e+18/BH_spectrum.txt +4 -0
  546. gammapbh/blackhawk_data/9.0e+18/final_state_radiation_prim.txt +13 -0
  547. gammapbh/blackhawk_data/9.0e+18/final_state_radiation_sec.txt +14 -0
  548. gammapbh/blackhawk_data/9.0e+18/inflight_annihilation_prim.txt +13 -0
  549. gammapbh/blackhawk_data/9.0e+18/inflight_annihilation_sec.txt +14 -0
  550. gammapbh/blackhawk_data/9.0e+18/instantaneous_primary_spectra.txt +502 -0
  551. gammapbh/blackhawk_data/9.0e+18/instantaneous_secondary_spectra.txt +378 -0
  552. gammapbh/blackhawk_data/9.0e+18/instantaneous_total_spectra.txt +502 -0
  553. gammapbh/cli.py +2850 -0
  554. gammapbh/results/custom_equation/1.38e+15_custom_eq/distributed_spectrum.txt +378 -0
  555. gammapbh/results/custom_equation/1.38e+15_custom_eq/equation.txt +5 -0
  556. gammapbh/results/custom_equation/1.38e+15_custom_eq/samples_sorted.txt +1001 -0
  557. gammapbh/results/gaussian/peak_3.00e+15_/342/225/247/320/2230.1_N1000/distributed_spectrum.txt +378 -0
  558. gammapbh/results/gaussian/peak_3.00e+15_/342/225/247/320/2230.1_N1000/mass_distribution.txt +1001 -0
  559. gammapbh/results/lognormal/peak_3.00e+15_/342/225/247/320/2230.1_N1000/distributed_spectrum.txt +378 -0
  560. gammapbh/results/lognormal/peak_3.00e+15_/342/225/247/320/2230.1_N1000/mass_distribution.txt +1001 -0
  561. gammapbh/results/monochromatic/3.00e+15_spectrum.txt +378 -0
  562. gammapbh/results/monochromatic/3.50e+15_mono_generated.txt +378 -0
  563. gammapbh/results/non_gaussian/peak_3.00e+15_/342/225/247/320/223X0.1_N1000/distributed_spectrum.txt +378 -0
  564. gammapbh/results/non_gaussian/peak_3.00e+15_/342/225/247/320/223X0.1_N1000/mass_distribution.txt +1001 -0
  565. gammapbh-1.1.3.dist-info/METADATA +238 -0
  566. gammapbh-1.1.3.dist-info/RECORD +570 -0
  567. gammapbh-1.1.3.dist-info/WHEEL +5 -0
  568. gammapbh-1.1.3.dist-info/entry_points.txt +2 -0
  569. gammapbh-1.1.3.dist-info/licenses/LICENSE.md +1348 -0
  570. 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