|
From: <js...@us...> - 2011-02-08 12:58:58
|
Revision: 8959
http://matplotlib.svn.sourceforge.net/matplotlib/?rev=8959&view=rev
Author: jswhit
Date: 2011-02-08 12:58:49 +0000 (Tue, 08 Feb 2011)
Log Message:
-----------
celestial allsky map example from Tom Loredo.
Added Paths:
-----------
trunk/toolkits/basemap/examples/allskymap.py
trunk/toolkits/basemap/examples/allskymap_cr_example.py
Added: trunk/toolkits/basemap/examples/allskymap.py
===================================================================
--- trunk/toolkits/basemap/examples/allskymap.py (rev 0)
+++ trunk/toolkits/basemap/examples/allskymap.py 2011-02-08 12:58:49 UTC (rev 8959)
@@ -0,0 +1,390 @@
+"""
+AllSkyMap is a subclass of Basemap, specialized for handling common plotting
+tasks for celestial data.
+
+It is essentially equivalent to using Basemap with full-sphere projections
+(e.g., 'hammer' or 'moll') and the `celestial` keyword set to `True`, but
+it adds a few new methods:
+
+* label_meridians for, well, labeling meridians with their longitude values;
+
+* geodesic, a replacement for Basemap.drawgreatcircle, that can correctly
+ handle geodesics that cross the limb of the map, and providing the user
+ easy control over clipping (which affects thick lines at or near the limb);
+
+* tissot, which overrides Basemap.tissot, correctly handling geodesics that
+ cross the limb of the map.
+
+Created Jan 2011 by Tom Loredo, based on Jeff Whitaker's code in Basemap's
+__init__.py module.
+"""
+
+from numpy import *
+import matplotlib.pyplot as pl
+from matplotlib.pyplot import *
+from mpl_toolkits.basemap import Basemap, pyproj
+from mpl_toolkits.basemap.pyproj import Geod
+
+__all__ = ['AllSkyMap']
+
+def angle_symbol(angle, round_to=1.0):
+ """
+ Return a string representing an angle, rounded and with a degree symbol.
+
+ This is adapted from code in mpl's projections.geo module.
+ """
+ value = np.round(angle / round_to) * round_to
+ if pl.rcParams['text.usetex'] and not pl.rcParams['text.latex.unicode']:
+ return r"$%0.0f^\circ$" % value
+ else:
+ return u"%0.0f\u00b0" % value
+
+
+class AllSkyMap(Basemap):
+ """
+ AllSkyMap is a subclass of Basemap, specialized for handling common plotting
+ tasks for celestial data.
+
+ It is essentially equivalent to using Basemap with full-sphere projections
+ (e.g., 'hammer' or 'moll') and the `celestial` keyword set to `True`, but
+ it adds a few new methods:
+
+ * label_meridians for, well, labeling meridians with their longitude values;
+
+ * geodesic, a replacement for Basemap.drawgreatcircle, that can correctly
+ handle geodesics that cross the limb of the map, and providing the user
+ easy control over clipping (which affects thick lines at or near the
+ limb);
+
+ * tissot, which overrides Basemap.tissot, correctly handling geodesics that
+ cross the limb of the map.
+ """
+
+ # Longitudes corresponding to east and west edges, reflecting the
+ # convention that 180 deg is the eastern edge, according to basemap's
+ # underlying projections:
+ east_lon = 180.
+ west_lon = 180.+1.e-10
+
+ def __init__(self,
+ projection='hammer',
+ lat_0=0., lon_0=0.,
+ suppress_ticks=True,
+ boundinglat=None,
+ fix_aspect=True,
+ anchor='C',
+ ax=None):
+
+ if projection != 'hammer' and projection !='moll':
+ raise ValueError('Only hammer and moll projections supported!')
+
+ # Use Basemap's init, enforcing the values of many parameters that
+ # aren't used or whose Basemap defaults would not be altered for all-sky
+ # celestial maps.
+ Basemap.__init__(self, llcrnrlon=None, llcrnrlat=None,
+ urcrnrlon=None, urcrnrlat=None,
+ llcrnrx=None, llcrnry=None,
+ urcrnrx=None, urcrnry=None,
+ width=None, height=None,
+ projection=projection, resolution=None,
+ area_thresh=None, rsphere=1.,
+ lat_ts=None,
+ lat_1=None, lat_2=None,
+ lat_0=lat_0, lon_0=lon_0,
+ suppress_ticks=suppress_ticks,
+ satellite_height=1.,
+ boundinglat=None,
+ fix_aspect=True,
+ anchor=anchor,
+ celestial=True,
+ ax=ax)
+
+ # Keep a local ref to lon_0 for hemisphere checking.
+ self._lon_0 = self.projparams['lon_0']
+ self._limb = None
+
+ def drawmapboundary(self,color='k',linewidth=1.0,fill_color=None,\
+ zorder=None,ax=None):
+ """
+ draw boundary around map projection region, optionally
+ filling interior of region.
+
+ .. tabularcolumns:: |l|L|
+
+ ============== ====================================================
+ Keyword Description
+ ============== ====================================================
+ linewidth line width for boundary (default 1.)
+ color color of boundary line (default black)
+ fill_color fill the map region background with this
+ color (default is no fill or fill with axis
+ background color).
+ zorder sets the zorder for filling map background
+ (default 0).
+ ax axes instance to use
+ (default None, use default axes instance).
+ ============== ====================================================
+
+ returns matplotlib.collections.PatchCollection representing map boundary.
+ """
+ # Just call the base class version, but keep a copy of the limb
+ # polygon for clipping.
+ self._limb = Basemap.drawmapboundary(self, color=color,
+ linewidth=linewidth, fill_color=fill_color, zorder=zorder, ax=ax)
+ return self._limb
+
+ def label_meridians(self, lons, fontsize=10, valign='bottom', vnudge=0,
+ halign='center', hnudge=0):
+ """
+ Label meridians with their longitude values in degrees.
+
+ This labels meridians with negative longitude l with the value 360-l;
+ for maps in celestial orientation, this means meridians to the right
+ of the central meridian are labeled from 360 to 180 (left to right).
+
+ `vnudge` and `hnudge` specify amounts in degress to nudge the labels
+ from their default placements, vertically and horizontally. This
+ values obey the map orientation, so to nudge to the right, use a
+ negative `hnudge` value.
+ """
+ # Run through (lon, lat) pairs, with lat=0 in each pair.
+ lats = len(lons)*[0.]
+ for lon,lat in zip(lons, lats):
+ x, y = self(lon+hnudge, lat+vnudge)
+ if lon < 0:
+ lon_lbl = 360 + lon
+ else:
+ lon_lbl = lon
+ pl.text(x, y, angle_symbol(lon_lbl), fontsize=fontsize,
+ verticalalignment=valign,
+ horizontalalignment=halign)
+
+ def east_hem(self, lon):
+ """
+ Return True if lon is in the eastern hemisphere of the map wrt lon_0.
+ """
+ if (lon-self._lon_0) % 360. <= self.east_lon:
+ return True
+ else:
+ return False
+
+ def geodesic(self, lon1, lat1, lon2, lat2, del_s=.01, clip=True, **kwargs):
+ """
+ Plot a geodesic curve from (lon1, lat1) to (lon2, lat2), with
+ points separated by arc length del_s. Return a list of Line2D
+ instances for the curves comprising the geodesic. If the geodesic does
+ not cross the map limb, there will be only a single curve; if it
+ crosses the limb, there will be two curves.
+ """
+
+ # TODO: Perhaps return a single Line2D instance when there is only a
+ # single segment, and a list of segments only when there are two segs?
+
+ # TODO: Check the units of del_s.
+
+ # This is based on Basemap.drawgreatcircle (which draws an *arc* of a
+ # great circle), but addresses a limitation of that method, supporting
+ # geodesics that cross the map boundary by breaking them into two
+ # segments, one in the eastern hemisphere and the other in the western.
+ gc = pyproj.Geod(a=self.rmajor,b=self.rminor)
+ az12,az21,dist = gc.inv(lon1,lat1,lon2,lat2)
+ npoints = int((dist+0.5**del_s)/del_s)
+ # Calculate lon & lat for points on the arc.
+ lonlats = gc.npts(lon1,lat1,lon2,lat2,npoints)
+ lons = [lon1]; lats = [lat1]
+ for lon, lat in lonlats:
+ lons.append(lon)
+ lats.append(lat)
+ lons.append(lon2); lats.append(lat2)
+ # Break the arc into segments as needed, when there is a longitudinal
+ # hemisphere crossing.
+ segs = []
+ seg_lons, seg_lats = [lon1], [lat1]
+ cur_hem = self.east_hem(lon1)
+ for lon, lat in zip(lons[1:], lats[1:]):
+ if self.east_hem(lon) == cur_hem:
+ seg_lons.append(lon)
+ seg_lats.append(lat)
+ else:
+ # We should interpolate a new pt at the boundary, but in
+ # the mean time just rely on the step size being small.
+ segs.append( (seg_lons, seg_lats) )
+ seg_lons, seg_lats = [lon], [lat]
+ cur_hem = not cur_hem
+ segs.append( (seg_lons, seg_lats) )
+ # Plot each segment; return a list of the mpl lines.
+ lines = []
+ for lons, lats in segs:
+ x, y = self(lons, lats)
+ if clip and self._limb:
+ line = plot(x, y, clip_path=self._limb, **kwargs)[0]
+ else:
+ line = plot(x, y, **kwargs)[0]
+ lines.append(line)
+ # If there are multiple segments and no color args, reconcile the
+ # colors, which mpl will have autoset to different values.
+ # *** Does this screw up mpl's color set sequence for later lines?
+ if not kwargs.has_key('c') or kwargs.has_key('color'):
+ if len(lines) > 1:
+ c1 = lines[0].get_color()
+ for line in lines[1:]:
+ line.set_color(c1)
+ return lines
+
+ def tissot(self,lon_0,lat_0,radius_deg,npts,ax=None,**kwargs):
+ """
+ Draw a polygon centered at ``lon_0,lat_0``. The polygon
+ approximates a circle on the surface of the earth with radius
+ ``radius_deg`` degrees latitude along longitude ``lon_0``,
+ made up of ``npts`` vertices.
+
+ The polygon represents a Tissot's indicatrix
+ (http://en.wikipedia.org/wiki/Tissot's_Indicatrix),
+ which when drawn on a map shows the distortion inherent in the map
+ projection. Tissots can be used to display azimuthally symmetric
+ directional uncertainties ("error circles").
+
+ Extra keyword ``ax`` can be used to override the default axis instance.
+
+ Other \**kwargs passed on to matplotlib.patches.Polygon.
+
+ returns a list of matplotlib.patches.Polygon objects, with two polygons
+ when the tissot crosses the limb, and just one polygon otherwise.
+ """
+
+ # TODO: Just return the polygon (not a list) when there is only one
+ # polygon? Or stick with the list for consistency?
+
+ # This is based on Basemap.tissot, but addresses a limitation of that
+ # method by handling tissots that cross the limb of the map by finding
+ # separate polygons in the eastern and western hemispheres comprising
+ # the tissot.
+ ax = kwargs.pop('ax', None) or self._check_ax()
+ g = pyproj.Geod(a=self.rmajor,b=self.rminor)
+ az12,az21,dist = g.inv(lon_0,lat_0,lon_0,lat_0+radius_deg)
+ start_hem = self.east_hem(lon_0)
+ segs1 = [self(lon_0,lat_0+radius_deg)]
+ over, segs2 = [], []
+ delaz = 360./npts
+ az = az12
+ last_lon = lon_0
+ # Note adjacent and opposite edge longitudes, in case the tissot
+ # runs over the edge.
+ if start_hem: # eastern case
+ adj_lon = self.east_lon
+ opp_lon = self.west_lon
+ else:
+ adj_lon = self.west_lon
+ opp_lon = self.east_lon
+ for n in range(npts):
+ az = az+delaz
+ # skip segments along equator (Geod can't handle equatorial arcs)
+ if np.allclose(0.,lat_0) and (np.allclose(90.,az) or np.allclose(270.,az)):
+ continue
+ else:
+ lon, lat, az21 = g.fwd(lon_0, lat_0, az, dist)
+ # If in the starting hemisphere, add to 1st polygon seg list.
+ if self.east_hem(lon) == start_hem:
+ x, y = self(lon, lat)
+ # Add segment if it is in the map projection region.
+ if x < 1.e20 and y < 1.e20:
+ segs1.append( (x, y) )
+ last_lon = lon
+ # Otherwise, we cross hemispheres.
+ else:
+ # Trace the edge of each hemisphere.
+ x, y = self(adj_lon, lat)
+ if x < 1.e20 and y < 1.e20:
+ segs1.append( (x, y) )
+ # We presume if adj projection is okay, opposite is.
+ segs2.append( self(opp_lon, lat) )
+ # Also store the overlap in the opposite hemisphere.
+ x, y = self(lon, lat)
+ if x < 1.e20 and y < 1.e20:
+ over.append( (x, y) )
+ last_lon = lon
+ poly1 = Polygon(segs1, **kwargs)
+ ax.add_patch(poly1)
+ if segs2:
+ over.reverse()
+ segs2.extend(over)
+ poly2 = Polygon(segs2, **kwargs)
+ ax.add_patch(poly2)
+ return [poly1, poly2]
+ else:
+ return [poly1]
+
+
+if __name__ == '__main__':
+
+ # Note that Hammer & Mollweide projections enforce a 2:1 aspect ratio.
+ # Use figure size good for a 2:1 plot.
+ fig = figure(figsize=(12,6))
+
+ # Set up the projection and draw a grid.
+ map = AllSkyMap(projection='hammer')
+ # Save the bounding limb to use as a clip path later.
+ limb = map.drawmapboundary(fill_color='white')
+ map.drawparallels(np.arange(-75,76,15), linewidth=0.5, dashes=[1,2],
+ labels=[1,0,0,0], fontsize=9)
+ map.drawmeridians(np.arange(-150,151,30), linewidth=0.5, dashes=[1,2])
+
+ # Label a subset of meridians.
+ lons = np.arange(-150,151,30)
+ map.label_meridians(lons, fontsize=9, vnudge=1,
+ halign='left', hnudge=-1) # hnudge<0 shifts to right
+
+ # x, y limits are [0, 4*rt2], [0, 2*rt2].
+ rt2 = sqrt(2)
+
+ # Draw a slanted green line crossing the map limb.
+ line = plot([rt2,0], [rt2,2*rt2], 'g-')
+
+ # Draw a slanted magenta line crossing the map limb but clipped.
+ line = plot([rt2+.1,0+.1], [rt2,2*rt2], 'm-', clip_path=limb)
+
+ # Draw some geodesics.
+ # First a transparent thick blue geodesic crossing the limb but not clipped,
+ # overlayed by a thinner red geodesic that is clipped (by default), to
+ # illustrate the effect of clipping.
+ lines = map.geodesic(120, 30, 240, 60, clip=False, c='b', lw=7, alpha=.5)
+ lines = map.geodesic(240, 60, 120, 30, c='r', lw=3, alpha=.5)
+
+ # Next two large limb-crossing geodesics with the same path, but rendered
+ # in opposite directions, one transparent blue, the other transparent
+ # yellow. They should be right on top of each other, giving a greenish
+ # brown hue.
+ lines = map.geodesic(240, -60, 120, 30, c='b', lw=2, alpha=.5)
+ lines = map.geodesic(120, 30, 240, -60, c='y', lw=2, alpha=.5)
+
+ # What happens if a geodesic is given coordinates spanning more than
+ # a single rotation? Not sure what to expect, but it shoots off the
+ # map (clipped here). Perhaps we should ensure lons are in [0, 360].
+ #lines = map.geodesic(120, 20, 240+360, 50, del_s=.2, c='g')
+
+ # Two tissots fully within the limb.
+ poly = map.tissot(60, -15, 10, 100)
+ poly = map.tissot(280, 60, 10, 100)
+ #poly = map.tissot(90, -85, 10, 100)
+
+ # Limb-spanning tissots in each quadrant.
+ # lower left:
+ poly = map.tissot(170, -60, 15, 100)
+ # upper left:
+ poly = map.tissot(175, 70, 15, 100)
+ # upper right (note negative longitude):
+ poly = map.tissot(-175, 30, 15, 100, color='r', alpha=.6)
+ # lower right:
+ poly = map.tissot(185, -40, 10, 100)
+
+ # Plot the tissot centers as "+" symbols. Note the top left symbol
+ # would cross the limb without the clip_path argument; this might be
+ # desired to enhance visibility.
+ lons = [170, 175, -175, 185]
+ lats = [-60, 70, 30, -40]
+ x, y = map(lons, lats)
+ map.scatter(x, y, s=40, marker='+', linewidths=1, edgecolors='g',
+ facecolors='none', clip_path=limb, zorder=10) # hi zorder -> top
+
+ title('AllSkyMap demo: Clipped lines, markers, geodesics, tissots')
+ show()
Added: trunk/toolkits/basemap/examples/allskymap_cr_example.py
===================================================================
--- trunk/toolkits/basemap/examples/allskymap_cr_example.py (rev 0)
+++ trunk/toolkits/basemap/examples/allskymap_cr_example.py 2011-02-08 12:58:49 UTC (rev 8959)
@@ -0,0 +1,225 @@
+"""
+Example of astronomical use of AllSkyMap class in allskymap.py module
+
+Plot an all-sky map showing locations of the 27 highest-energy ultra-high
+energy cosmic rays detected by the Auger (south) experiment as of Aug 2007,
+and locations of 18 (fictitious!) candidate sources. Indicate CR direction
+uncertainties and source scattering scales with tissots, and show the
+nearest candidate source to each CR with geodesics.
+
+Created 2011-02-07 by Tom Loredo
+"""
+
+from cStringIO import StringIO
+import numpy as np
+from numpy import cos, sin, arccos, deg2rad, rad2deg
+import csv, re
+
+import matplotlib.pyplot as plt
+from allskymap import AllSkyMap
+from matplotlib.colors import Normalize
+from matplotlib.colorbar import ColorbarBase
+import matplotlib.ticker as ticker
+
+
+class Source:
+ """
+ Parse and store data for a celestial source.
+ """
+
+ int_re = re.compile(r'^[-+]?[0-9]+$')
+ # float_re = re.compile(r'^[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?$')
+
+ def __init__(self, id, year, day, l, b, sig=None, **kwds):
+ self.id = int(id)
+ self.year = int(year)
+ self.day = int(day)
+ self.l = float(l)
+ self._l = deg2rad(self.l) # radians
+ self.b = float(b)
+ self._b = deg2rad(self.b) # radians
+ if sig is not None:
+ self.sig = float(sig)
+ self._sig = deg2rad(self.sig)
+ else:
+ self.sig, self._sig = None, None
+ # If extra values are specified as keywords, set them as
+ # attributes. The quick way is to use self.__dict__.update(kwds),
+ # but we want to convert to int or float values if possible.
+ # *** Consider using matplotlib.cbook.converter.
+ if kwds:
+ for key, val in kwds.items():
+ try:
+ nval = float(val)
+ # It's a number; but it may be an int.
+ if self.int_re.match(str(val)):
+ nval = int(val)
+ setattr(self, key, nval)
+ except ValueError: # non-numerical value
+ setattr(self, key, val)
+
+ def gcangle(self, src):
+ """
+ Calculate the great circle angle to another source.
+ """
+ # Use the law of cosines; note it is usually expressed with colattitude.
+ c = sin(self._b)*sin(src._b) + \
+ cos(self._b)*cos(src._b)*cos(self._l - src._l)
+ return rad2deg(arccos(c))
+
+
+# Auger UHE cosmic ray data, Jan 2004 to Aug 2007
+# From Appendix A of Abraham et al. (2008); "Correlation of the highest-energy
+# cosmic rays with the positions of nearby active galactic nuclei,"
+# Astropart.Phys.29:188-204,2008; Erratum-ibid.30:45,2008
+
+# Year day ang S(1000) E (EeV) RA Dec Longitude Latitude
+# * = w/i 3.2 deg of AGN
+AugerData = StringIO(
+"""2004 125 47.7 252 70 267.1 -11.4 15.4 8.4
+2004 142 59.2 212 84 199.7 -34.9 -50.8 27.6 *
+2004 282 26.5 328 66 208.0 -60.3 -49.6 1.7 *
+2004 339 44.7 316 83 268.5 -61.0 -27.7 -17.0 *
+2004 343 23.4 323 63 224.5 -44.2 -34.4 13.0 *
+2005 54 35.0 373 84 17.4 -37.9 -75.6 -78.6 *
+2005 63 54.5 214 71 331.2 -1.2 58.8 -42.4 *
+2005 81 17.2 308 58 199.1 -48.6 -52.8 14.1 *
+2005 295 15.4 311 57 332.9 -38.2 4.2 -54.9 *
+2005 306 40.1 248 59 315.3 -0.3 48.8 -28.7 *
+2005 306 14.2 445 84 114.6 -43.1 -103.7 -10.3
+2006 35 30.8 398 85 53.6 -7.8 -165.9 -46.9 *
+2006 55 37.9 255 59 267.7 -60.7 -27.6 -16.5 *
+2006 81 34.0 357 79 201.1 -55.3 -52.3 7.3
+2006 185 59.1 211 83 350.0 9.6 88.8 -47.1 *
+2006 296 54.0 208 69 52.8 -4.5 -170.6 -45.7 *
+2006 299 26.0 344 69 200.9 -45.3 -51.2 17.2 *
+2007 13 14.3 762 148 192.7 -21.0 -57.2 41.8
+2007 51 39.2 247 58 331.7 2.9 63.5 -40.2 *
+2007 69 30.4 332 70 200.2 -43.4 -51.4 19.2 **
+2007 84 17.3 340 64 143.2 -18.3 -109.4 23.8 *
+2007 145 23.9 392 78 47.7 -12.8 -163.8 -54.4 *
+2007 186 44.8 248 64 219.3 -53.8 -41.7 5.9
+2007 193 18.0 469 90 325.5 -33.5 12.1 -49.0 *
+2007 221 35.3 318 71 212.7 -3.3 -21.8 54.1 *
+2007 234 33.2 365 80 185.4 -27.9 -65.1 34.5
+2007 235 42.6 276 69 105.9 -22.9 -125.2 -7.7
+""")
+AugerTable = csv.reader(AugerData, dialect='excel-tab')
+CRs = {}
+for id, row in enumerate(AugerTable):
+ # Make an integer ID from Year+Day (presumes none on same day!).
+ src = Source(id, row[0], row[1], row[7], row[8], E=float(row[4]))
+ CRs[src.id] = src
+print 'Parsed data for', len(CRs), 'UHE CRs...'
+
+# Partly fictitious candidate source locations.
+# src.id src.l_deg src.b_deg src.xProj src.yProj
+# tab-delimited
+CandData = StringIO(
+"""1 270. -28.
+2 229. -80.
+3 141. -47.
+4 172. -51.
+5 251. -51.
+6 241. -36.
+7 281. 26.
+8 241. 64.
+9 240. 64.
+10 148. 70.
+11 305. 13.
+12 98. 79.
+13 309. 19.
+14 104. 68.
+15 104. 68.
+16 321. 15.
+17 328. -14.
+18 177.5 -35.
+""")
+# Add this line above to see a tissot overlapping the map limb.
+CandTable = csv.reader(CandData, dialect='excel-tab')
+cands = {}
+for row in CandTable:
+ src = Source(row[0], 0, 0, row[1], row[2])
+ cands[src.id] = src
+print 'Parsed data for', len(cands), 'candidate sources...'
+
+# Calculate the separation matrix; track the closest candidate to each CR.
+sepn = {}
+for cr in CRs.values():
+ id, sep = None, 181.
+ for cand in cands.values():
+ val = cr.gcangle(cand)
+ sepn[cr.id, cand.id] = val
+ if val < sep:
+ id, sep = cand.id, val
+ # Store the closest candidate id and separation as a CR attribute.
+ cr.near_id = id
+ cr.near_ang = sep
+
+
+# Note that Hammer & Mollweide projections enforce a 2:1 aspect ratio.
+# Choose figure size for a 2:1 plot, with room at bottom for colorbar.
+fig = plt.figure(figsize=(12,7))
+main_ax = plt.axes([0.05, .19, .9, .75]) # rect=L,B,W,H
+
+# Set up the projection and draw a grid.
+m = AllSkyMap(ax=main_ax, projection='hammer')
+m.drawmapboundary(fill_color='white')
+m.drawparallels(np.arange(-75,76,15), linewidth=0.5, dashes=[1,2],
+ labels=[1,0,0,0], fontsize=9)
+m.drawmeridians(np.arange(-150,151,30), linewidth=0.5, dashes=[1,2])
+
+# Label a subset of meridians.
+lons = np.arange(-150,151,30)
+m.label_meridians(lons, fontsize=9, vnudge=1,
+ halign='left', hnudge=-1) # nudge<0 shifts to right
+
+# Plot CR directions.
+lvals = [src.l for src in CRs.values()]
+bvals = [src.b for src in CRs.values()]
+x, y = m(lvals, bvals)
+# These symbols will be covered by opaque tissots; plot them anyway
+# so there is a collection for the legend.
+cr_pts = m.scatter(x, y, s=8, c='r', marker='o', linewidths=.5,
+ edgecolors='none')
+
+# Plot tissots showing uncertainties, colored by energy.
+# We use 1 deg uncertainties, which are probably ~2 sigma for most events.
+Evals = np.array([src.E for src in CRs.values()])
+norm_E = Normalize(Evals.min()-10, Evals.max()+20) # -+ for jet_r for brt clrs
+# jet_r color map is in spectral sequence, with a small unsaturated
+# green range, helping to distinguish CRs from candidates.
+cmap = plt.cm.get_cmap('jet_r')
+for cr in CRs.values():
+ color = cmap(norm_E(cr.E))[0:3] # ignore alpha
+ m.tissot(cr.l, cr.b, 2., 30, ec='none', color=color, alpha=1)
+
+# Plot candidate directions.
+lvals = [src.l for src in cands.values()]
+bvals = [src.b for src in cands.values()]
+x, y = m(lvals, bvals)
+cand_pts = m.scatter(x, y, marker='+', linewidths=.5,
+ edgecolors='k', facecolors='none', zorder=10) # hi zorder -> top
+
+# Plot tissots showing possible scale of candidate scatter.
+for l, b in zip(lvals, bvals):
+ m.tissot(l, b, 5., 30, ec='none', color='g', alpha=0.25)
+
+# Show the closest candidate to each CR.
+for cr in CRs.values():
+ cand = cands[cr.near_id]
+ m.geodesic(cr.l, cr.b, cand.l, cand.b, lw=0.5, ls='-', c='g')
+
+plt.title('UHE Cosmic Rays and Candidate Sources')
+plt.legend([cr_pts, cand_pts], ['UHE CR', 'Candidate'],
+ frameon=False, loc='lower right', scatterpoints=1)
+
+# Plot a colorbar for the CR energies.
+cb_ax = plt.axes([0.25, .1, .5, .03], frameon=False) # rect=L,B,W,H
+#bar = ColorbarBase(cb_ax, cmap=cmap, orientation='horizontal', drawedges=False)
+vals = np.linspace(Evals.min(), Evals.max(), 100)
+bar = ColorbarBase(cb_ax, values=vals, norm=norm_E, cmap=cmap,
+ orientation='horizontal', drawedges=False)
+bar.set_label('CR Energy (EeV)')
+
+plt.show()
This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.
|