icecube-skyreader 1.3.0__tar.gz → 1.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. {icecube-skyreader-1.3.0 → icecube-skyreader-1.3.2}/PKG-INFO +1 -1
  2. {icecube-skyreader-1.3.0 → icecube-skyreader-1.3.2}/icecube_skyreader.egg-info/PKG-INFO +1 -1
  3. {icecube-skyreader-1.3.0 → icecube-skyreader-1.3.2}/icecube_skyreader.egg-info/requires.txt +1 -0
  4. {icecube-skyreader-1.3.0 → icecube-skyreader-1.3.2}/setup.cfg +1 -0
  5. {icecube-skyreader-1.3.0 → icecube-skyreader-1.3.2}/skyreader/__init__.py +1 -1
  6. {icecube-skyreader-1.3.0 → icecube-skyreader-1.3.2}/skyreader/plot/plot.py +105 -96
  7. {icecube-skyreader-1.3.0 → icecube-skyreader-1.3.2}/skyreader/plot/plotting_tools.py +8 -6
  8. {icecube-skyreader-1.3.0 → icecube-skyreader-1.3.2}/skyreader/result.py +7 -3
  9. icecube-skyreader-1.3.2/skyreader/utils/handle_map_data.py +407 -0
  10. icecube-skyreader-1.3.0/skyreader/utils/handle_map_data.py +0 -202
  11. {icecube-skyreader-1.3.0 → icecube-skyreader-1.3.2}/LICENSE +0 -0
  12. {icecube-skyreader-1.3.0 → icecube-skyreader-1.3.2}/README.md +0 -0
  13. {icecube-skyreader-1.3.0 → icecube-skyreader-1.3.2}/icecube_skyreader.egg-info/SOURCES.txt +0 -0
  14. {icecube-skyreader-1.3.0 → icecube-skyreader-1.3.2}/icecube_skyreader.egg-info/dependency_links.txt +0 -0
  15. {icecube-skyreader-1.3.0 → icecube-skyreader-1.3.2}/icecube_skyreader.egg-info/top_level.txt +0 -0
  16. {icecube-skyreader-1.3.0 → icecube-skyreader-1.3.2}/setup.py +0 -0
  17. {icecube-skyreader-1.3.0 → icecube-skyreader-1.3.2}/skyreader/event_metadata.py +0 -0
  18. {icecube-skyreader-1.3.0 → icecube-skyreader-1.3.2}/skyreader/plot/__init__.py +0 -0
  19. {icecube-skyreader-1.3.0 → icecube-skyreader-1.3.2}/skyreader/py.typed +0 -0
  20. {icecube-skyreader-1.3.0 → icecube-skyreader-1.3.2}/skyreader/utils/__init__.py +0 -0
  21. {icecube-skyreader-1.3.0 → icecube-skyreader-1.3.2}/skyreader/utils/areas.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: icecube-skyreader
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary: An API for Results Produced by SkyDriver & the Skymap Scanner
5
5
  Home-page: https://github.com/icecube/skyreader
6
6
  Download-URL: https://pypi.org/project/icecube-skyreader/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: icecube-skyreader
3
- Version: 1.3.0
3
+ Version: 1.3.2
4
4
  Summary: An API for Results Produced by SkyDriver & the Skymap Scanner
5
5
  Home-page: https://github.com/icecube/skyreader
6
6
  Download-URL: https://pypi.org/project/icecube-skyreader/
@@ -2,6 +2,7 @@ astropy
2
2
  healpy
3
3
  matplotlib
4
4
  meander
5
+ mhealpy
5
6
  numpy
6
7
  pandas
7
8
  scipy
@@ -51,6 +51,7 @@ install_requires =
51
51
  healpy
52
52
  matplotlib
53
53
  meander
54
+ mhealpy
54
55
  numpy
55
56
  pandas
56
57
  scipy
@@ -17,7 +17,7 @@ __all__ = [
17
17
  # is zero for an official release, positive for a development branch,
18
18
  # or negative for a release candidate or beta (after the base version
19
19
  # number has been incremented)
20
- __version__ = "1.3.0"
20
+ __version__ = "1.3.2"
21
21
  version_info = (
22
22
  int(__version__.split(".")[0]),
23
23
  int(__version__.split(".")[1]),
@@ -8,8 +8,8 @@ import pickle
8
8
  from pathlib import Path
9
9
  from typing import List, Union
10
10
 
11
+ import copy
11
12
  import healpy # type: ignore[import]
12
- # import mhealpy
13
13
  import matplotlib # type: ignore[import]
14
14
  import meander # type: ignore[import]
15
15
  import numpy as np
@@ -28,7 +28,12 @@ from .plotting_tools import (
28
28
  )
29
29
 
30
30
  from ..utils.areas import calculate_area, get_contour_areas
31
- from ..utils.handle_map_data import extract_map, get_contour_levels
31
+ from ..utils.handle_map_data import (
32
+ extract_map,
33
+ get_contour_levels,
34
+ prepare_flattened_map,
35
+ prepare_multiorder_map,
36
+ )
32
37
  from ..result import SkyScanResult
33
38
 
34
39
  LOGGER = logging.getLogger("skyreader.plot")
@@ -92,10 +97,11 @@ class SkyScanPlotter:
92
97
  plot_filename = f"{unique_id}.{addition_to_filename}pdf"
93
98
  LOGGER.info(f"saving plot to {plot_filename}")
94
99
 
95
- (
96
- grid_value, grid_ra, grid_dec, equatorial_map
97
- ) = extract_map(
98
- result, llh_map, angular_error_floor, remove_min_val=not llh_map
100
+ grid_value, grid_ra, grid_dec, equatorial_map, _ = extract_map(
101
+ result,
102
+ llh_map,
103
+ angular_error_floor,
104
+ remove_min_val=not llh_map,
99
105
  )
100
106
 
101
107
  grid_pix = healpy.ang2pix(max(nsides), np.pi/2. - DEC, RA)
@@ -129,9 +135,17 @@ class SkyScanPlotter:
129
135
  self.PLOT_COLORMAP.name.rstrip('_r')
130
136
  ]
131
137
  text_colorbar = r"log10$(p)$"
132
- vmin = np.min(np.log10(equatorial_map))
133
- vmax = np.max(np.log10(equatorial_map))
134
- map_to_plot = np.log10(plotting_map)
138
+ vmin = np.nanmin(
139
+ np.log10(equatorial_map[equatorial_map != 0.])
140
+ )
141
+ vmax = np.nanmax(
142
+ np.log10(equatorial_map[equatorial_map != 0.])
143
+ )
144
+ map_to_plot = copy.copy(plotting_map)
145
+ map_to_plot[plotting_map != 0.] = np.log10(
146
+ plotting_map[plotting_map != 0.]
147
+ )
148
+ map_to_plot[plotting_map == 0.] = np.nan
135
149
  equatorial_map = np.ma.masked_invalid(equatorial_map)
136
150
  map_to_plot = np.ma.masked_invalid(map_to_plot)
137
151
 
@@ -378,20 +392,20 @@ class SkyScanPlotter:
378
392
  return Theta, Phi
379
393
 
380
394
  def create_plot_zoomed(
381
- self,
382
- result: SkyScanResult,
383
- extra_ra=np.nan,
384
- extra_dec=np.nan,
385
- extra_radius=np.nan,
386
- systematics=False,
387
- plot_bounding_box=False,
388
- plot_4fgl=False,
389
- circular=False,
390
- circular_err50=0.2,
391
- circular_err90=0.7,
392
- angular_error_floor=None, # if not None, sigma of the
393
- # gaussian to convolute the map with in deg.
394
- llh_map=True
395
+ self,
396
+ result: SkyScanResult,
397
+ extra_ra=np.nan,
398
+ extra_dec=np.nan,
399
+ extra_radius=np.nan,
400
+ systematics=False,
401
+ plot_bounding_box=False,
402
+ plot_4fgl=False,
403
+ circular=False,
404
+ circular_err50=0.2,
405
+ circular_err90=0.7,
406
+ angular_error_floor=None, # if not None, sigma of the
407
+ # gaussian to convolute the map with in deg.
408
+ llh_map=True
395
409
  ):
396
410
  """Uses healpy to plot a map."""
397
411
 
@@ -427,7 +441,7 @@ class SkyScanPlotter:
427
441
  plot_filename = unique_id + ".plot_zoomed.pdf"
428
442
 
429
443
  (
430
- grid_value, grid_ra, grid_dec, equatorial_map
444
+ grid_value, grid_ra, grid_dec, equatorial_map, uniq_array
431
445
  ) = extract_map(result, llh_map, angular_error_floor)
432
446
  min_dec = grid_dec[0]
433
447
  min_ra = grid_ra[0]
@@ -444,11 +458,24 @@ class SkyScanPlotter:
444
458
  sample_points = np.array([np.pi/2 - grid_dec, grid_ra]).T
445
459
 
446
460
  if not circular:
461
+ grid_values_for_contours = copy.copy(grid_value)
447
462
  if llh_map:
448
- grid_values_for_contours = grid_value
463
+ # get rid of nan values only for the contours
464
+ # this avoids crashes during plotting
465
+ grid_values_for_contours[
466
+ np.isnan(grid_values_for_contours)
467
+ ] = np.nanmax(equatorial_map)
449
468
  contour_levels_for_contours = contour_levels
450
469
  else:
451
- grid_values_for_contours = np.log(grid_value)
470
+ grid_values_for_contours[
471
+ np.logical_or(
472
+ np.isnan(grid_values_for_contours),
473
+ grid_values_for_contours == 0.
474
+ )
475
+ ] = np.nanmin(equatorial_map[equatorial_map > 0.])
476
+ grid_values_for_contours = np.log(
477
+ grid_values_for_contours
478
+ )
452
479
  contour_levels_for_contours = np.log(contour_levels)
453
480
  # Get contours from healpix map
454
481
  contours_by_level = meander.spherical_contours(
@@ -599,8 +626,7 @@ class SkyScanPlotter:
599
626
  contour_colors,
600
627
  contours_by_level
601
628
  ):
602
- contour_label = contour_label + ' - area: {0:.2f} sqdeg'.format(
603
- contour_area_sqdeg)
629
+ contour_label = contour_label + f' - area: {contour_area_sqdeg:.2f} sqdeg'
604
630
  first = True
605
631
  for contour in contours:
606
632
  theta, phi = contour.T
@@ -640,7 +666,6 @@ class SkyScanPlotter:
640
666
  rot=(lon, lat, 0),
641
667
  bounds=(lower_lon, upper_lon, lower_lat, upper_lat)
642
668
  )
643
-
644
669
  if plot_4fgl:
645
670
  # Overlay 4FGL sources
646
671
  plot_catalog(
@@ -669,13 +694,11 @@ class SkyScanPlotter:
669
694
  print(contain_txt)
670
695
 
671
696
  print(
672
- "Contour Area (50%):",
673
- contour_areas[0],
697
+ f"Contour Area (50%): {contour_areas[0]}",
674
698
  "square degrees (scaled)"
675
699
  )
676
700
  print(
677
- "Contour Area (90%):",
678
- contour_areas[1],
701
+ f"Contour Area (90%): {contour_areas[1]}",
679
702
  "square degrees (scaled)"
680
703
  )
681
704
 
@@ -712,7 +735,6 @@ class SkyScanPlotter:
712
735
  # join end to beginning
713
736
  bounding_ras_list.append(bounding_ras_list[0])
714
737
  bounding_decs_list.append(bounding_decs_list[0])
715
-
716
738
  bounding_ras: np.ndarray = np.asarray(bounding_ras_list)
717
739
  bounding_decs: np.ndarray = np.asarray(bounding_decs_list)
718
740
  bounding_phi = np.radians(bounding_ras)
@@ -723,7 +745,7 @@ class SkyScanPlotter:
723
745
  # convert to square-degrees
724
746
  bounding_contour_area *= (180.*180.)/(np.pi*np.pi)
725
747
  contour_label = r'90% Bounding rectangle' + \
726
- ' - area: {0:.2f} sqdeg'.format(bounding_contour_area)
748
+ f' - area: {bounding_contour_area:.2f} sqdeg'
727
749
  healpy.projplot(
728
750
  bounding_theta,
729
751
  bounding_phi,
@@ -733,32 +755,6 @@ class SkyScanPlotter:
733
755
  label=contour_label
734
756
  )
735
757
 
736
- # Output contours in RA, dec instead of theta, phi
737
- saving_contours: list = []
738
- for contours in contours_by_level:
739
- saving_contours.append([])
740
- for contour in contours:
741
- saving_contours[-1].append([])
742
- theta, phi = contour.T
743
- ras = phi
744
- decs = np.pi/2 - theta
745
- for tmp_ra, tmp_dec in zip(ras, decs):
746
- saving_contours[-1][-1].append([tmp_ra, tmp_dec])
747
-
748
- # Save the individual contours, send messages
749
- for i, val in enumerate(["50", "90"]):
750
- ras = list(np.asarray(saving_contours[i][0]).T[0])
751
- decs = list(np.asarray(saving_contours[i][0]).T[1])
752
- tab = {"ra (rad)": ras, "dec (rad)": decs}
753
- savename = unique_id + ".contour_" + val + ".txt"
754
- try:
755
- LOGGER.info("Dumping to {savename}")
756
- ascii.write(tab, savename, overwrite=True)
757
- except OSError:
758
- LOGGER.error(
759
- "OS Error prevented contours from being written, "
760
- "maybe a memory issue. Error is:\n{err}")
761
-
762
758
  uncertainty = [(ra_minus, ra_plus), (dec_minus, dec_plus)]
763
759
  fits_header = format_fits_header(
764
760
  (
@@ -776,18 +772,15 @@ class SkyScanPlotter:
776
772
 
777
773
  # Plot the original online reconstruction location
778
774
  if np.sum(np.isnan([extra_ra, extra_dec, extra_radius])) == 0:
779
-
780
775
  # dist = angular_distance(minRA, minDec, extra_ra * np.pi/180.,
781
776
  # extra_dec * np.pi/180.)
782
777
  # print("Millipede best fit is", dist /(np.pi *
783
778
  # extra_radius/(1.177 * 180.)), "sigma from reported best fit")
784
-
785
779
  extra_ra_rad = np.radians(extra_ra)
786
780
  extra_dec_rad = np.radians(extra_dec)
787
781
  extra_radius_rad = np.radians(extra_radius)
788
782
  extra_lon = extra_ra_rad
789
783
  extra_lat = extra_dec_rad
790
-
791
784
  healpy.projscatter(
792
785
  np.degrees(extra_lon),
793
786
  np.degrees(extra_lat),
@@ -816,52 +809,34 @@ class SkyScanPlotter:
816
809
  color=cont_col,
817
810
  linestyle=cont_sty
818
811
  )
819
-
820
812
  plt.legend(fontsize=6, loc="lower left")
821
813
 
822
- # Dump the whole contour
823
- path = unique_id + ".contour.pkl"
824
- print("Saving contour to", path)
825
- with open(path, "wb") as f:
826
- pickle.dump(saving_contours, f)
827
-
828
- # logic for multiordermaps -> for future developements
829
-
830
- # save multiorder version of the map
831
- # multiorder_map = mhealpy.HealpixMap(
832
- # grid_value, uniq_array
833
- # )
834
- # multiorder_map.write_map(
835
- # f"{unique_id}.skymap_nside_{mmap_nside}.multiorder.fits.gz",
836
- # column_names=["PROBABILITY"],
837
- # extra_header=fits_header,
838
- # overwrite=True,
839
- # )
840
-
841
- if llh_map:
842
- column_names = ['2DLLH']
843
- else:
844
- # avoid excessively heavy data format for the flattened map
845
- equatorial_map[
846
- equatorial_map < 1e-16
847
- ] = np.mean(
848
- equatorial_map[equatorial_map < 1e-16]
849
- )
850
- column_names = ["PROBABILITY"]
851
-
852
814
  # save flattened map
815
+ equatorial_map, column_names = prepare_flattened_map(
816
+ equatorial_map, llh_map
817
+ )
853
818
  if llh_map:
854
819
  type_map = "llh"
855
820
  else:
856
821
  type_map = "probability"
822
+ filename_main = f"{unique_id}.skymap_nside_{mmap_nside}_{type_map}"
857
823
  healpy.write_map(
858
- f"{unique_id}.skymap_nside_{mmap_nside}_{type_map}.fits.gz",
824
+ self.output_dir / f"{filename_main}.fits.gz",
859
825
  equatorial_map,
860
826
  coord='C',
861
827
  column_names=column_names,
862
828
  extra_header=fits_header,
863
829
  overwrite=True
864
830
  )
831
+ multiorder_map, column_names = prepare_multiorder_map(
832
+ grid_value, uniq_array, llh_map, column_names
833
+ )
834
+ multiorder_map.write_map(
835
+ self.output_dir / f"{filename_main}.multiorder.fits.gz",
836
+ column_names=column_names,
837
+ extra_header=fits_header,
838
+ overwrite=True,
839
+ )
865
840
 
866
841
  # Save the figure
867
842
  LOGGER.info(f"Saving: {plot_filename}...")
@@ -873,6 +848,40 @@ class SkyScanPlotter:
873
848
  bbox_inches='tight'
874
849
  )
875
850
 
851
+ self._save_contours(contours_by_level, unique_id)
876
852
  LOGGER.info("done.")
877
853
 
878
854
  plt.close()
855
+
856
+ def _save_contours(self, contours_by_level, unique_id) -> None:
857
+ # Output contours in RA, dec instead of theta, phi
858
+ saving_contours: list = []
859
+ for contours in contours_by_level:
860
+ saving_contours.append([])
861
+ for contour in contours:
862
+ saving_contours[-1].append([])
863
+ theta, phi = contour.T
864
+ ras = phi
865
+ decs = np.pi/2 - theta
866
+ for tmp_ra, tmp_dec in zip(ras, decs):
867
+ saving_contours[-1][-1].append([tmp_ra, tmp_dec])
868
+
869
+ # Save the individual contours
870
+ for i, val in enumerate(["50", "90"]):
871
+ ras = list(np.asarray(saving_contours[i][0]).T[0])
872
+ decs = list(np.asarray(saving_contours[i][0]).T[1])
873
+ tab = {"ra (rad)": ras, "dec (rad)": decs}
874
+ savepath = self.output_dir / f"{unique_id}.contour_{val}.txt"
875
+ try:
876
+ LOGGER.info(f"Dumping to {savepath}")
877
+ ascii.write(tab, savepath, overwrite=True)
878
+ except OSError:
879
+ LOGGER.error(
880
+ "OS Error prevented contours from being written, "
881
+ "maybe a memory issue. Error is:\n{err}")
882
+
883
+ # Dump the whole contour
884
+ path = self.output_dir / f"{unique_id}.contour.pkl"
885
+ print("Saving contour to", path)
886
+ with open(path, "wb") as f:
887
+ pickle.dump(saving_contours, f)
@@ -16,7 +16,9 @@ from matplotlib.transforms import Affine2D # type: ignore[import]
16
16
 
17
17
  matplotlib.use('agg')
18
18
 
19
- def format_fits_header(event_id_tuple, mjd, ra, dec, uncertainty, llh_map):
19
+ def format_fits_header(
20
+ event_id_tuple, mjd, ra, dec, uncertainty, llh_map,
21
+ ):
20
22
  """Prepare some of the relevant event information for a fits file
21
23
  header."""
22
24
  run_id, event_id, event_type = event_id_tuple
@@ -24,14 +26,14 @@ def format_fits_header(event_id_tuple, mjd, ra, dec, uncertainty, llh_map):
24
26
  if llh_map:
25
27
  uncertainty_comment = 'Change in 2LLH based on Wilks theorem'
26
28
  else:
27
- uncertainty_comment = 'Probability-per-pixel (all pixels with same area)'
29
+ uncertainty_comment = 'Highest posterior density 90% credible region'
28
30
 
29
31
  header = [
30
32
  ('RUNID', run_id),
31
33
  ('EVENTID', event_id),
32
34
  ('SENDER', 'IceCube Collaboration'),
33
35
  ('EventMJD', mjd),
34
- ('I3TYPE', '%s'%event_type,'Alert Type'),
36
+ ('I3TYPE', f'{event_type}','Alert Type'),
35
37
  ('RA', np.round(ra,2),'Degree'),
36
38
  ('DEC', np.round(dec,2),'Degree'),
37
39
  ('RA_ERR_PLUS', np.round(uncertainty[0][1],2),
@@ -42,7 +44,7 @@ def format_fits_header(event_id_tuple, mjd, ra, dec, uncertainty, llh_map):
42
44
  '90% containment error high'),
43
45
  ('DEC_ERR_MINUS', np.round(np.abs(uncertainty[1][0]),2),
44
46
  '90% containment error low'),
45
- ('COMMENTS', '50%(90%) uncertainty location' \
47
+ ('COMMENTS', '90% uncertainty location' \
46
48
  + ' => ' + uncertainty_comment),
47
49
  ('NOTE', 'Please ignore pixels with infinite or NaN values.' \
48
50
  + ' They are rare cases of the minimizer failing to converge')
@@ -97,12 +99,12 @@ def hp_ticklabels(zoom=False, lonra=None, latra=None, rot=None, bounds=None):
97
99
  pe = [path_effects.Stroke(linewidth=1.5, foreground='white'),
98
100
  path_effects.Normal()]
99
101
  for _ in lats:
100
- healpy.projtext(lon_offset, _, "{:.0f}$^\circ$".format(_),
102
+ healpy.projtext(lon_offset, _, f"{_:.0f}$^\\circ$",
101
103
  lonlat=True, path_effects=pe, fontsize=10)
102
104
  if zoom:
103
105
  for _ in lons:
104
106
  healpy.projtext(_, lat_offset,
105
- "{:.0f}$^\circ$".format(_), lonlat=True,
107
+ f"{_:.0f}$^\\circ$", lonlat=True,
106
108
  path_effects=pe, fontsize=10)
107
109
  else:
108
110
  ax.annotate(r"$\bf{-180^\circ}$", xy=(1.7, 0.625), size="medium")
@@ -217,6 +217,10 @@ class SkyScanResult:
217
217
  else:
218
218
  self.logger.warning("Metadata doesn't seem to exist and will not be used for plotting.")
219
219
  return EventMetadata(0, 0, '', 0, False)
220
+
221
+ def get_results_per_nside(self, nside: int) -> np.ndarray:
222
+ "get the results for the pixels at a given nside"
223
+ return self.result[f"nside-{nside}"]
220
224
 
221
225
  def isclose_nside(self,
222
226
  other: "SkyScanResult",
@@ -388,7 +392,7 @@ class SkyScanResult:
388
392
  try:
389
393
  first = next(iter(self.result.values()))
390
394
  except StopIteration: # no results yet
391
- np.savez(filename, **self.result)
395
+ np.savez(filename, **self.result) # type: ignore
392
396
  return Path(filename)
393
397
 
394
398
  try:
@@ -405,9 +409,9 @@ class SkyScanResult:
405
409
  ],
406
410
  dtype=metadata_dtype,
407
411
  )
408
- np.savez(filename, header=header, **self.result)
412
+ np.savez(filename, header=header, **self.result) # type: ignore
409
413
  except (TypeError, AttributeError):
410
- np.savez(filename, **self.result)
414
+ np.savez(filename, **self.result) # type: ignore
411
415
 
412
416
  return Path(filename)
413
417
 
@@ -0,0 +1,407 @@
1
+ from typing import Union, Tuple, List
2
+ import logging
3
+
4
+ import healpy # type: ignore[import]
5
+ import mhealpy # type: ignore[import]
6
+ import numpy as np
7
+
8
+ from ..result import SkyScanResult
9
+
10
+ LOGGER = logging.getLogger("skyreader.extract_map")
11
+
12
+
13
+ def extract_map(
14
+ result: SkyScanResult,
15
+ llh_map: bool = True,
16
+ angular_error_floor: Union[None, float] = None,
17
+ remove_min_val: bool = True,
18
+ ) -> Tuple[
19
+ np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray
20
+ ]:
21
+ """
22
+ Extract from the output of skymap_scanner the healpy map
23
+ args:
24
+ - result: SkyScanResult. The output of the Skymap Scanner
25
+ - llh_map: bool = True. If True the likelihood will be plotted,
26
+ otherwise the probability
27
+ - angular_error_floor: Union[None, float] = None. if not None,
28
+ sigma of the gaussian to convolute the map with in deg.
29
+ - remove_min_val: bool = True. Remove minimum value from -llh
30
+ no effect if probability map.
31
+
32
+ returns:
33
+ - grid_value: value-per-scanned-pixel (pixels with
34
+ different nsides)
35
+ - grid_ra: right ascension for each pixel in grid_value
36
+ - grid_dec: declination for each pixel in grid_value
37
+ - equatorial_map: healpix map with maximum nside (all pixels
38
+ with same nside)
39
+ - uniq_array: uniqs for all the pixels of the map
40
+ """
41
+
42
+ grid_map = dict()
43
+
44
+ nsides = result.nsides
45
+ max_nside = max(nsides)
46
+ equatorial_map = np.full(healpy.nside2npix(max_nside), np.nan)
47
+ uniq_list = []
48
+
49
+ for nside in nsides:
50
+ LOGGER.info(f"constructing map for nside {nside}...")
51
+ npix = healpy.nside2npix(nside)
52
+ map_data = result.get_results_per_nside(nside)
53
+ pixels = map_data['index']
54
+ values = map_data['llh']
55
+ this_map = np.full(npix, np.nan)
56
+ this_map[pixels] = values
57
+ if nside < max_nside:
58
+ this_map = healpy.ud_grade(this_map, max_nside)
59
+ mask = np.logical_and(~np.isnan(this_map), np.isfinite(this_map))
60
+ equatorial_map[mask] = this_map[mask]
61
+
62
+ for pixel_data in result.get_results_per_nside(nside):
63
+ pixel = pixel_data['index']
64
+ value = pixel_data['llh']
65
+ nested_pixel = healpy.ring2nest(nside, pixel)
66
+ uniq = 4*nside*nside + nested_pixel
67
+ uniq_list.append(uniq)
68
+ tmp_theta, tmp_phi = healpy.pix2ang(nside, pixel)
69
+ tmp_dec = np.pi/2 - tmp_theta
70
+ tmp_ra = tmp_phi
71
+ grid_map[(tmp_dec, tmp_ra)] = value
72
+
73
+ # In case of pointed scans, it helps filling the first nside
74
+ # with empty pixels (especially for saving the multiorder map)
75
+ if nside == nsides[0]:
76
+ grid_map, uniq_list = _fill_first_nside_empty(
77
+ nside, result, grid_map, uniq_list
78
+ )
79
+
80
+ LOGGER.info(f"done with map for nside {nside}...")
81
+
82
+ grid_dec_list, grid_ra_list, grid_value_list = [], [], []
83
+
84
+ for (dec, ra), value in grid_map.items():
85
+ grid_dec_list.append(dec)
86
+ grid_ra_list.append(ra)
87
+ grid_value_list.append(value)
88
+ grid_dec: np.ndarray = np.asarray(grid_dec_list)
89
+ grid_ra: np.ndarray = np.asarray(grid_ra_list)
90
+ grid_value: np.ndarray = np.asarray(grid_value_list)
91
+ uniq_array: np.ndarray = np.asarray(uniq_list)
92
+
93
+ sorting_indices = np.argsort(grid_value)
94
+ grid_value = grid_value[sorting_indices]
95
+ grid_dec = grid_dec[sorting_indices]
96
+ grid_ra = grid_ra[sorting_indices]
97
+ uniq_array = uniq_array[sorting_indices]
98
+
99
+ min_value = grid_value[0]
100
+
101
+ if remove_min_val or (not llh_map):
102
+ # renormalize
103
+ grid_value = grid_value - min_value
104
+ min_value = 0.
105
+
106
+ # renormalize
107
+ equatorial_map[np.isinf(equatorial_map)] = np.nan
108
+ equatorial_map -= np.nanmin(equatorial_map)
109
+
110
+ if llh_map:
111
+ # show 2 * delta_LLH
112
+ grid_value = grid_value * 2.
113
+ equatorial_map *= 2.
114
+ else:
115
+ # Convert to probability
116
+ equatorial_map = np.exp(-1. * equatorial_map)
117
+ equatorial_map = equatorial_map / np.nansum(equatorial_map)
118
+ min_map = np.nanmin(equatorial_map)
119
+
120
+ if angular_error_floor is not None:
121
+ # convolute with a gaussian. angular_error_floor is the
122
+ # sigma in deg.
123
+ # nan values are a problem for the convolution and the contours
124
+ equatorial_map = healpy.smoothing(
125
+ np.nan_to_num(equatorial_map),
126
+ sigma=np.deg2rad(angular_error_floor),
127
+ )
128
+
129
+ # normalize map
130
+ min_map = np.nanmin(equatorial_map[equatorial_map >= 0.])
131
+ equatorial_map = equatorial_map.clip(min_map, None)
132
+ normalization = np.nansum(equatorial_map)
133
+ equatorial_map = equatorial_map / normalization
134
+
135
+ # obtain values for grid map
136
+ grid_value = healpy.get_interp_val(
137
+ equatorial_map, np.pi/2 - grid_dec, grid_ra
138
+ )
139
+ grid_value = grid_value.clip(min_map, None)
140
+
141
+ return grid_value, grid_ra, grid_dec, equatorial_map, uniq_array
142
+
143
+
144
+ def _fill_first_nside_empty(
145
+ nside: int,
146
+ result: SkyScanResult,
147
+ grid_map: dict,
148
+ uniq_list: list,
149
+ ):
150
+ """
151
+ Fill the grid_map at the first nside with empty pixels
152
+ """
153
+ tot_npix = healpy.nside2npix(nside)
154
+ if tot_npix > len(result.get_results_per_nside(nside)):
155
+ ring_pixels = np.arange(tot_npix)
156
+ nest_pixels = healpy.ring2nest(nside, ring_pixels)
157
+ uniq_pixels = mhealpy.nest2uniq(nside, nest_pixels)
158
+ for uni, rin in zip(uniq_pixels, ring_pixels):
159
+ if uni not in uniq_list:
160
+ uniq_list.append(uni)
161
+ tmp_theta, tmp_phi = healpy.pix2ang(nside, rin)
162
+ tmp_dec = np.pi/2 - tmp_theta
163
+ tmp_ra = tmp_phi
164
+ grid_map[(tmp_dec, tmp_ra)] = np.nan
165
+ return grid_map, uniq_list
166
+
167
+
168
+ def get_contour_levels(
169
+ equatorial_map: np.ndarray,
170
+ llh_map: bool = True,
171
+ systematics: bool = False,
172
+ ):
173
+ """
174
+ get contour levels for the desired map
175
+
176
+ args:
177
+ - equatorial_map: np.ndarray. Necessary in case of
178
+ probability map.
179
+ - llh_map: bool. If True llh levels, otherwise probability
180
+ levels.
181
+ - systematics: bool. Only for llh maps. If True include
182
+ recalibrated llh values from Pan-Starrs event 127852
183
+ (IC160427A syst.)
184
+
185
+ returns:
186
+ - contour_levels, levels of the contours
187
+ - contour_labels, respective labels for the contours
188
+ - contour_colors, respective colors for the contours
189
+ """
190
+
191
+ # Calculate the contour levels
192
+ if llh_map: # likelihood map
193
+ min_value = np.nanmin(equatorial_map)
194
+ if systematics:
195
+ # from Pan-Starrs event 127852
196
+ # these are values determined from MC by Will on the TS (2*LLH)
197
+ # Not clear yet how to translate this for the probability map
198
+ contour_levels = (np.array([22.2, 64.2])+min_value)
199
+ contour_labels = [
200
+ r'50% (IC160427A syst.)', r'90% (IC160427A syst.)'
201
+ ]
202
+ contour_colors = ['k', 'r']
203
+ # Wilks
204
+ else:
205
+ contour_levels = (
206
+ np.array([1.39, 4.61, 11.83, 28.74])+min_value
207
+ )[:3]
208
+ contour_labels = [
209
+ r'50%', r'90%', r'3$\sigma$', r'5$\sigma$'
210
+ ][:3]
211
+ contour_colors = ['k', 'r', 'g', 'b'][:3]
212
+ else: # probability map
213
+ if systematics:
214
+ raise AssertionError(
215
+ "No corrected values for contours in probability maps"
216
+ )
217
+ else:
218
+ sorted_values = np.sort(equatorial_map)[::-1]
219
+ probability_levels = (
220
+ np.array([0.5, 0.9, 1-1.35e-3, 1-2.87e-7])
221
+ )[:3]
222
+ contour_levels = []
223
+ for prob in probability_levels:
224
+ level_index = (
225
+ np.nancumsum(sorted_values) >= prob
226
+ ).tolist().index(True)
227
+ level = sorted_values[level_index]
228
+ contour_levels.append(level)
229
+ contour_labels = [r'50%', r'90%', r'3$\sigma$', r'5$\sigma$'][:3]
230
+ contour_colors = ['k', 'r', 'g', 'b'][:3]
231
+
232
+ return contour_levels, contour_labels, contour_colors
233
+
234
+
235
+ def find_pixels_double_nside(
236
+ nside: int,
237
+ indexes: np.ndarray,
238
+ ):
239
+ """
240
+ Given indexes of pixels at a given nside, find which are
241
+ the pixels inside these pixels with a double nside
242
+
243
+ args:
244
+ - nside: int. nside of the pixels to investigate.
245
+ - indexes: np.ndarray. indexes on ring ordering of the pixels
246
+ to investigate.
247
+
248
+ returns:
249
+ - idxs_inside: np.ndarray, array containing an array for each
250
+ initial pixel which tells which are the pixels with double
251
+ nside inside that pixel. The pixels are in ring ordering.
252
+ """
253
+
254
+ vecs = healpy.boundaries(nside, indexes)
255
+ transposed_vecs = np.transpose(vecs, axes=(0,2,1))
256
+ idxs_inside = np.array(
257
+ [healpy.query_polygon(nside*2, vs) for vs in transposed_vecs]
258
+ )
259
+ return idxs_inside
260
+
261
+
262
+ def already_filled_uniqs_for_nside(
263
+ nside: int,
264
+ next_nside: int,
265
+ uniqs: np.ndarray
266
+ ):
267
+ """
268
+ Check if among the input uniqs there are pixels at a finer nside
269
+ which are inside other pixels with a bigger nside
270
+
271
+ args:
272
+ -nside: int. nside of the pixels to see if there are finer pixels
273
+ inside.
274
+ -next_nside: int. nside of the finer pixes which we want to see
275
+ if they are inside the coarser pixels
276
+ -uniqs: np.ndarray. Uniqs for all the pixels in the map
277
+
278
+ returns:
279
+ already_filled_uniqs: np.ndarray. Uniqs of the coarser pixels
280
+ which are already filled.
281
+ """
282
+ nside_per_pixel = mhealpy.uniq2nside(uniqs)
283
+ uniqs_nside = uniqs[nside_per_pixel == nside]
284
+ uniqs_next_nside = uniqs[nside_per_pixel == next_nside]
285
+ pixels_nside = healpy.nest2ring(
286
+ nside, mhealpy.uniq2nest(uniqs_nside)[1]
287
+ )
288
+ idxs_double_nside = find_pixels_double_nside(nside, pixels_nside)
289
+ already_filled_uniqs = []
290
+ for uniq_original_nside, idxs_pixel in zip(
291
+ uniqs_nside, idxs_double_nside
292
+ ):
293
+ progressive_nside = nside*2
294
+ while progressive_nside != next_nside:
295
+ idxs_pixel = np.concatenate(
296
+ find_pixels_double_nside(progressive_nside, idxs_pixel)
297
+ )
298
+ progressive_nside *= 2
299
+ uniqs_pixel = mhealpy.nest2uniq(
300
+ next_nside, healpy.ring2nest(next_nside, idxs_pixel)
301
+ )
302
+ pixel_already_filled = False
303
+ for uniq in uniqs_next_nside:
304
+ if uniq in uniqs_pixel:
305
+ pixel_already_filled = True
306
+ continue
307
+ if pixel_already_filled:
308
+ already_filled_uniqs.append(uniq_original_nside)
309
+ return np.array(already_filled_uniqs)
310
+
311
+
312
+ def find_filled_pixels(uniqs: np.ndarray):
313
+ """
314
+ given an array of uniqs, finds which pixels already have finer
315
+ pixels inside and returns array with the indeces of these pixels
316
+
317
+ args:
318
+ - uniqs: np.ndarray. Array of uniqs per each pixel of the map
319
+
320
+ returns:
321
+ - already_filled_indeces: np.ndarray. Array with the indeces
322
+ of the already_filled_uniqs in the input array
323
+ """
324
+ nside_per_pixel = mhealpy.uniq2nside(uniqs)
325
+ nsides = np.unique(nside_per_pixel)
326
+ already_filled_uniqs = []
327
+ for nside_index, nside in enumerate(nsides[:-1]):
328
+ next_nside = nsides[nside_index + 1]
329
+ already_filled_uniqs_nside = already_filled_uniqs_for_nside(
330
+ nside, next_nside, uniqs
331
+ )
332
+ already_filled_uniqs.append(already_filled_uniqs_nside)
333
+ already_filled_uniqs = np.concatenate(already_filled_uniqs)
334
+ already_filled_indeces = np.array(
335
+ [np.where(uniqs == uni)[0][0] for uni in already_filled_uniqs]
336
+ )
337
+ return already_filled_indeces
338
+
339
+
340
+ def clean_data_multiorder_map(
341
+ grid_value: np.ndarray, uniqs: np.ndarray
342
+ ):
343
+ """
344
+ Clean a map from the pixels which have a finer scan inside
345
+
346
+ args:
347
+ - grid_value: np.ndarray. Value of probability (likelihood)
348
+ per each scanned pixel.
349
+ - uniqs: np.ndarray. Uniqs per each scanned pixel to univocally
350
+ identify them
351
+
352
+ returns:
353
+ - grid_value: np.ndarray. Cleaned array
354
+ - uniqs: np.ndarray. Cleaned array
355
+ """
356
+ filled_indeces = find_filled_pixels(uniqs)
357
+ grid_value = np.delete(grid_value, filled_indeces)
358
+ uniqs = np.delete(uniqs, filled_indeces)
359
+ return grid_value, uniqs
360
+
361
+
362
+ def prepare_flattened_map(
363
+ equatorial_map: np.ndarray,
364
+ llh_map: bool,
365
+ ) -> Tuple[np.ndarray, List[str]]:
366
+ """
367
+ Create the healpix map that needs to be saved keeping
368
+ into account if it is a probability or a llh map
369
+ """
370
+ if llh_map:
371
+ column_names = ['2DLLH']
372
+ else:
373
+ # avoid excessively heavy data format for the flattened map
374
+ equatorial_map[equatorial_map < 1e-16] = np.nanmean(
375
+ equatorial_map[equatorial_map < 1e-16]
376
+ )
377
+ column_names = ["PROBABILITY"]
378
+ return equatorial_map, column_names
379
+
380
+
381
+ def prepare_multiorder_map(
382
+ grid_value: np.ndarray,
383
+ uniq_array: np.ndarray,
384
+ llh_map: bool,
385
+ column_names: List[str]
386
+ ) -> Tuple[mhealpy.HealpixMap, List[str]]:
387
+ """
388
+ Create the mhealpix map that needs to be saved keeping
389
+ into account if it is a probability or a llh map
390
+ """
391
+ # clean from redundant pixels
392
+ grid_value, uniq_array = clean_data_multiorder_map(
393
+ grid_value, uniq_array
394
+ )
395
+ # save multiorder version of the map
396
+ if llh_map:
397
+ multiorder_map = mhealpy.HealpixMap(grid_value, uniq_array)
398
+ else:
399
+ all_nsides = mhealpy.uniq2nside(uniq_array)
400
+ max_nside = np.max(all_nsides)
401
+ multiorder_map = mhealpy.HealpixMap(
402
+ grid_value / healpy.nside2pixarea(
403
+ max_nside, degrees=True,
404
+ ), uniq_array
405
+ )
406
+ column_names = [f"{column_names[0]} DENSITY [deg-2]"]
407
+ return multiorder_map, column_names
@@ -1,202 +0,0 @@
1
- from typing import Union
2
- import logging
3
-
4
- import healpy # type: ignore[import]
5
- import numpy as np
6
-
7
- from ..result import SkyScanResult
8
-
9
- LOGGER = logging.getLogger("skyreader.extract_map")
10
-
11
-
12
- def extract_map(
13
- result: SkyScanResult,
14
- llh_map: bool = True,
15
- angular_error_floor: Union[None, float] = None,
16
- remove_min_val: bool = True,
17
- ):
18
- """
19
- Extract from the output of skymap_scanner the healpy map
20
- args:
21
- - result: SkyScanResult. The output of the Skymap Scanner
22
- - llh_map: bool = True. If True the likelihood will be plotted,
23
- otherwise the probability
24
- - angular_error_floor: Union[None, float] = None. if not None,
25
- sigma of the gaussian to convolute the map with in deg.
26
- - remove_min_val: bool = True. Remove minimum value from -llh
27
- no effect if probability map.
28
-
29
- returns:
30
- - grid_value: value-per-scanned-pixel (pixels with
31
- different nsides)
32
- - grid_ra: right ascension for each pixel in grid_value
33
- - grid_dec: declination for each pixel in grid_value
34
- - equatorial_map: healpix map with maximum nside (all pixels
35
- with same nside)
36
- """
37
-
38
- grid_map = dict()
39
-
40
- nsides = result.nsides
41
- max_nside = max(nsides)
42
- equatorial_map = np.full(healpy.nside2npix(max_nside), np.nan)
43
-
44
- for nside in nsides:
45
- LOGGER.info(f"constructing map for nside {nside}...")
46
- npix = healpy.nside2npix(nside)
47
- map_data = result.result[f'nside-{nside}']
48
- pixels = map_data['index']
49
- values = map_data['llh']
50
- this_map = np.full(npix, np.nan)
51
- this_map[pixels] = values
52
- if nside < max_nside:
53
- this_map = healpy.ud_grade(this_map, max_nside)
54
- mask = np.logical_and(~np.isnan(this_map), np.isfinite(this_map))
55
- equatorial_map[mask] = this_map[mask]
56
-
57
- for pixel_data in result.result[f"nside-{nside}"]:
58
- pixel = pixel_data['index']
59
- value = pixel_data['llh']
60
- if np.isfinite(value) and not np.isnan(value):
61
- tmp_theta, tmp_phi = healpy.pix2ang(nside, pixel)
62
- tmp_dec = np.pi/2 - tmp_theta
63
- tmp_ra = tmp_phi
64
- grid_map[(tmp_dec, tmp_ra)] = value
65
- # nested_pixel = healpy.ang2pix(
66
- # nside, tmp_theta, tmp_phi, nest=True
67
- # )
68
- # uniq = 4*nside*nside + nested_pixel
69
- # uniq_list.append(uniq)
70
- LOGGER.info(f"done with map for nside {nside}...")
71
-
72
- grid_dec_list, grid_ra_list, grid_value_list = [], [], []
73
-
74
- for (dec, ra), value in grid_map.items():
75
- grid_dec_list.append(dec)
76
- grid_ra_list.append(ra)
77
- grid_value_list.append(value)
78
- grid_dec: np.ndarray = np.asarray(grid_dec_list)
79
- grid_ra: np.ndarray = np.asarray(grid_ra_list)
80
- grid_value: np.ndarray = np.asarray(grid_value_list)
81
- # uniq_array: np.ndarray = np.asarray(uniq_list)
82
-
83
- sorting_indices = np.argsort(grid_value)
84
- grid_value = grid_value[sorting_indices]
85
- grid_dec = grid_dec[sorting_indices]
86
- grid_ra = grid_ra[sorting_indices]
87
- # uniq_array = uniq_array[sorting_indices]
88
-
89
- min_value = grid_value[0]
90
-
91
- if remove_min_val or (not llh_map):
92
- # renormalize
93
- grid_value = grid_value - min_value
94
- min_value = 0.
95
-
96
- # renormalize
97
- equatorial_map[np.isinf(equatorial_map)] = np.nan
98
- equatorial_map -= np.nanmin(equatorial_map)
99
-
100
- if llh_map:
101
- # show 2 * delta_LLH
102
- grid_value = grid_value * 2.
103
- equatorial_map *= 2.
104
- else:
105
- # Convert to probability
106
- equatorial_map = np.exp(-1. * equatorial_map)
107
- equatorial_map = equatorial_map / np.nansum(equatorial_map)
108
-
109
- # nan values are a problem for the convolution and the contours
110
- min_map = np.nanmin(equatorial_map)
111
- equatorial_map[np.isnan(equatorial_map)] = min_map
112
-
113
- if angular_error_floor is not None:
114
- # convolute with a gaussian. angular_error_floor is the
115
- # sigma in deg.
116
- equatorial_map = healpy.smoothing(
117
- equatorial_map,
118
- sigma=np.deg2rad(angular_error_floor),
119
- )
120
-
121
- # normalize map
122
- min_map = np.nanmin(equatorial_map[equatorial_map > 0.0])
123
- equatorial_map[np.isnan(equatorial_map)] = min_map
124
- equatorial_map = equatorial_map.clip(min_map, None)
125
- normalization = np.nansum(equatorial_map)
126
- equatorial_map = equatorial_map / normalization
127
-
128
- # obtain values for grid map
129
- grid_value = healpy.get_interp_val(
130
- equatorial_map, np.pi/2 - grid_dec, grid_ra
131
- )
132
- grid_value[np.isnan(grid_value)] = min_map
133
- grid_value = grid_value.clip(min_map, None)
134
-
135
- return grid_value, grid_ra, grid_dec, equatorial_map
136
-
137
-
138
- def get_contour_levels(
139
- equatorial_map: np.ndarray,
140
- llh_map: bool = True,
141
- systematics: bool = False,
142
- ):
143
- """
144
- get contour levels for the desired map
145
-
146
- args:
147
- - equatorial_map: np.ndarray. Necessary in case of
148
- probability map.
149
- - llh_map: bool. If True llh levels, otherwise probability
150
- levels.
151
- - systematics: bool. Only for llh maps. If True include
152
- recalibrated llh values from Pan-Starrs event 127852
153
- (IC160427A syst.)
154
-
155
- returns:
156
- - contour_levels, levels of the contours
157
- - contour_labels, respective labels for the contours
158
- - contour_colors, respective colors for the contours
159
- """
160
-
161
- # Calculate the contour levels
162
- if llh_map: # likelihood map
163
- min_value = np.min(equatorial_map)
164
- if systematics:
165
- # from Pan-Starrs event 127852
166
- # these are values determined from MC by Will on the TS (2*LLH)
167
- # Not clear yet how to translate this for the probability map
168
- contour_levels = (np.array([22.2, 64.2])+min_value)
169
- contour_labels = [
170
- r'50% (IC160427A syst.)', r'90% (IC160427A syst.)'
171
- ]
172
- contour_colors = ['k', 'r']
173
- # Wilks
174
- else:
175
- contour_levels = (
176
- np.array([1.39, 4.61, 11.83, 28.74])+min_value
177
- )[:3]
178
- contour_labels = [
179
- r'50%', r'90%', r'3$\sigma$', r'5$\sigma$'
180
- ][:3]
181
- contour_colors = ['k', 'r', 'g', 'b'][:3]
182
- else: # probability map
183
- if systematics:
184
- raise AssertionError(
185
- "No corrected values for contours in probability maps"
186
- )
187
- else:
188
- sorted_values = np.sort(equatorial_map)[::-1]
189
- probability_levels = (
190
- np.array([0.5, 0.9, 1-1.35e-3, 1-2.87e-7])
191
- )[:3]
192
- contour_levels = list()
193
- for prob in probability_levels:
194
- level_index = (
195
- np.nancumsum(sorted_values) >= prob
196
- ).tolist().index(True)
197
- level = sorted_values[level_index]
198
- contour_levels.append(level)
199
- contour_labels = [r'50%', r'90%', r'3$\sigma$', r'5$\sigma$'][:3]
200
- contour_colors = ['k', 'r', 'g', 'b'][:3]
201
-
202
- return contour_levels, contour_labels, contour_colors