# lib.py: Module containing various plotting functions # # Authors: William Cleveland (USRA), # Adam Goldstein (USRA) and # Daniel Kocevski (NASA) # # Portions of the code are Copyright 2020 William Cleveland and # Adam Goldstein, Universities Space Research Association # All rights reserved. # # Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import matplotlib.pyplot as plt from astropy.coordinates import SkyCoord from matplotlib.colors import colorConverter from matplotlib.patches import Polygon from .globals import * from .lal_post_subs import * from ..coords import calc_mcilwain_l, saa_boundary, radec_to_spacecraft # ---------- Lightcurve and Spectra ----------# def histo(bins, ax, color='C0', edges_to_zero=False, **kwargs): """Plot a rate histogram either lightcurves or count spectra Args: bins (:class:`gbm.data.primitives.TimeBins` or \ :class:`gbm.data.primitives.EnergyBins`) The lightcurve or count spectrum histograms ax (:class:`matplotlib.axes`): The axis on which to plot color (str, optional): The color of the histogram. Default is 'C0' edges_to_zero (bool, optional): If True, then the farthest edges of the histogram will drop to zero. Default is True. **kwargs: Other plotting options Returns: list: The reference to the plot objects """ bin_segs = bins.contiguous_bins() refs = [] for seg in bin_segs: edges = np.concatenate( ([seg.lo_edges[0]], seg.lo_edges, [seg.hi_edges[-1]])) if edges_to_zero: rates = np.concatenate(([0.0], seg.rates, [0.0])) else: rates = np.concatenate( ([seg.rates[0]], seg.rates, [seg.rates[-1]])) p = ax.step(edges, rates, where='post', color=color, **kwargs) refs.append(p) return refs def histo_errorbars(bins, ax, color='C0', **kwargs): """Plot errorbars for lightcurves or count spectra Args: bins (:class:`gbm.data.primitives.TimeBins` or \ :class:`gbm.data.primitives.EnergyBins`) The lightcurve or count spectrum histograms ax (:class:`matplotlib.axes`): The axis on which to plot color (str, optional): The color of the errorbars. Default is 'C0' **kwargs: Other plotting options Returns: list: The reference to the plot objects """ bin_segs = bins.contiguous_bins() refs = [] for seg in bin_segs: p = ax.errorbar(seg.centroids, seg.rates, seg.rate_uncertainty, capsize=0, fmt='none', color=color, **kwargs) refs.append(p) return refs def histo_filled(bins, ax, color=DATA_SELECTED_COLOR, fill_alpha=DATA_SELECTED_ALPHA, **kwargs): """Plot a filled histogram Args: bins (:class:`gbm.data.primitives.TimeBins` or \ :class:`gbm.data.primitives.EnergyBins`) The lightcurve or count spectrum histograms ax (:class:`matplotlib.axes`): The axis on which to plot color (str, optional): The color of the filled histogram fill_alpha (float, optional): The alpha of the fill **kwargs: Other plotting options Returns: list: The reference to the plot objects """ h = histo(bins, ax, color=color, zorder=3, **kwargs) zeros = np.zeros(bins.size + 1) rates = np.append(bins.rates, bins.rates[-1]) edges = np.append(bins.lo_edges, bins.hi_edges[-1]) b1 = ax.plot((edges[0], edges[0]), (zeros[0], rates[0]), color=color, zorder=2) b2 = ax.plot((edges[-1], edges[-1]), (zeros[-1], rates[-1]), color=color, zorder=2) f = ax.fill_between(edges, zeros, rates, color=color, step='post', alpha=fill_alpha, zorder=4, **kwargs) refs = [h, b1, b2, f] return refs def selection_line(xpos, ax, **kwargs): """Plot a selection line Args: xpos (float): The position of the selection line ax (:class:`matplotlib.axes`): The axis on which to plot **kwargs: Other plotting options Returns: list: The reference to the plot objects """ ylim = ax.get_ylim() ref = ax.plot([xpos, xpos], ylim, **kwargs) return ref def selections(bounds, ax, **kwargs): """Plot selection bounds Args: bounds (list of tuples): List of selection bounds ax (:class:`matplotlib.axes`): The axis on which to plot **kwargs: Other plotting options Returns: tuple: The reference to the lower and upper selection """ refs1 = [] refs2 = [] for bound in bounds: p = selection_line(bound[0], ax, **kwargs) refs1.append(p[0]) p = selection_line(bound[1], ax, **kwargs) refs2.append(p[0]) return (refs1, refs2) def errorband(x, y_upper, y_lower, ax, **kwargs): """Plot an error band Args: x (np.array): The x values y_upper (np.array): The upper y values of the error band y_lower (np.array): The lower y values of the error band ax (:class:`matplotlib.axes`): The axis on which to plot **kwargs: Other plotting options Returns: list: The reference to the lower and upper selection """ refs = ax.fill_between(x, y_upper.squeeze(), y_lower.squeeze(), **kwargs) return refs def lightcurve_background(backrates, ax, cent_color=None, err_color=None, cent_alpha=None, err_alpha=None, **kwargs): """Plot a lightcurve background model with an error band Args: backrates (:class:`~gbm.background.BackgroundRates`): The background rates object integrated over energy. If there is more than one remaining energy channel, the background will be integrated over the remaining energy channels. ax (:class:`matplotlib.axes`): The axis on which to plot cent_color (str): Color of the centroid line err_color (str): Color of the errorband cent_alpha (float): Alpha of the centroid line err_alpha (fl): Alpha of the errorband **kwargs: Other plotting options Returns: list: The reference to the lower and upper selection """ if backrates.numchans > 1: backrates = backrates.integrate_energy() times = backrates.time_centroids rates = backrates.rates uncert = backrates.rate_uncertainty p2 = errorband(times, rates + uncert, rates - uncert, ax, alpha=err_alpha, color=err_color, linestyle='-', **kwargs) p1 = ax.plot(times, rates, color=cent_color, alpha=cent_alpha, **kwargs) refs = [p1, p2] return refs def spectrum_background(backspec, ax, cent_color=None, err_color=None, cent_alpha=None, err_alpha=None, **kwargs): """Plot a count spectrum background model with an error band Args: backspec (:class:`~gbm.background.BackgroundSpectrum`): The background rates object integrated over energy. If there is more than one remaining energy channel, the background will be integrated over the remaining energy channels. ax (:class:`matplotlib.axes`): The axis on which to plot cent_color (str): Color of the centroid line err_color (str): Color of the errorband cent_alpha (float): Alpha of the centroid line err_alpha (fl): Alpha of the errorband **kwargs: Other plotting options Returns: list: The reference to the lower and upper selection """ rates = backspec.rates / backspec.widths uncert = backspec.rate_uncertainty / backspec.widths edges = np.append(backspec.lo_edges, backspec.hi_edges[-1]) # plot the centroid of the model p1 = ax.step(edges, np.append(rates, rates[-1]), where='post', color=cent_color, alpha=cent_alpha, zorder=1, **kwargs) # construct the stepped errorband to fill between energies = np.array( (backspec.lo_edges, backspec.hi_edges)).T.flatten() # .tolist() upper = np.array((rates + uncert, rates + uncert)).T.flatten() # .tolist() lower = np.array((rates - uncert, rates - uncert)).T.flatten() # .tolist() p2 = errorband(energies, upper, lower, ax, color=err_color, alpha=err_alpha, linestyle='-', **kwargs) refs = [p1, p2] return refs # ---------- DRM ----------# def response_matrix(phot_energies, chan_energies, matrix, ax, cmap='Greens', num_contours=100, norm=None, **kwargs): """Make a filled contour plot of a response matrix Parameters: ----------- phot_energies: np.array The incident photon energy bin centroids chan_energies: np.array The recorded energy channel centroids matrix: np.array The effective area matrix corresponding to the photon bin and energy channels ax: matplotlib.axes The axis on which to plot cmap: str, optional The color map to use. Default is 'Greens' num_contours: int, optional The number of contours to draw. These will be equally spaced in log-space. Default is 100 norm: matplotlib.colors.Normalize or similar, optional The normalization used to scale the colormapping to the heatmap values. This can be initialized by Normalize, LogNorm, SymLogNorm, PowerNorm, or some custom normalization. **kwargs: optional Other keyword arguments to be passed to matplotlib.pyplot.contourf Returns: ----------- image: matplotlib.collections.QuadMesh The reference to the plot object """ mask = (matrix > 0.0) levels = np.logspace(np.log10(matrix[mask].min()), np.log10(matrix.max()), num_contours) image = ax.contourf(phot_energies, chan_energies, matrix, levels=levels, cmap=cmap, norm=norm) return image def effective_area(bins, ax, color='C0', orientation='vertical', **kwargs): """Plot a histogram of the effective area of an instrument response Parameters: ----------- bins: Bins The histogram of effective area ax: matplotlib.axes The axis on which to plot **kwargs: optional Other plotting options Returns: ----------- refs: list The reference to the plot objects """ bin_segs = bins.contiguous_bins() refs = [] for seg in bin_segs: edges = np.concatenate( ([seg.lo_edges[0]], seg.lo_edges, [seg.lo_edges[-1]])) counts = np.concatenate(([0.0], seg.counts, [0.0])) if orientation == 'horizontal': p = ax.step(counts, edges, where='post', color=color, **kwargs) else: p = ax.step(edges, counts, where='post', color=color, **kwargs) refs.append(p) return refs # ---------- Earth and Orbital ----------# def saa_polygon(m, color='darkred', alpha=0.4, **kwargs): """Plot the SAA polygon on Basemap Parameters: ----------- m: Basemap The basemap references color: str, optional The color of the polygon alpha: float, optional The alpha opacity of the interior of the polygon kwargs: optional Other plotting keywordss Returns: ----------- poly: Polygon The SAA polygon object """ # Define the SAA boundaries lat_saa, lon_saa = saa_boundary() edge = colorConverter.to_rgba(color, alpha=1.0) face = colorConverter.to_rgba(color, alpha=alpha) # plot the polygon x, y = m(lon_saa, lat_saa) xy = list(zip(x, y)) poly = Polygon(xy, edgecolor=edge, facecolor=face, **kwargs) return poly def mcilwain_map(lat_range, lon_range, m, ax, saa_mask=False, color=None, alpha=0.5, **kwargs): """Plot the McIlwain L heatmap on a Basemap Parameters: ----------- lat_range: (float, float) The latitude range lon_range: (float, float) The longitude range m: Basemap The basemap references ax: matplotlib.axes The plot axes references saa_mask: bool, optional If True, mask out the SAA from the heatmap. Default is False. color: str, optional The color of the heatmap alpha: float, optional The alpha opacity of the heatmap kwargs: optional Other plotting keywords Returns: ----------- image: QuadContourSet The heatmap plot object """ # do create an array on the earth lat_array = np.linspace(*lat_range, 108) lon_array = np.linspace(*lon_range, 720) LAT, LON = np.meshgrid(lat_array, lon_array) # convert to projection coordinates mLON, mLAT = m(LON, LAT) # mcilwain l over the grid mcl = calc_mcilwain_l(LAT, LON) # if we want to mask out the SAA if saa_mask: saa_path = saa_polygon(m).get_path() mask = saa_path.contains_points( np.array((mLON.ravel(), mLAT.ravel())).T) mcl[mask.reshape(mcl.shape)] = 0.0 # do the plot levels = [0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7] image = ax.contourf(mLON, mLAT, mcl, levels=levels, alpha=alpha, **kwargs) return image def earth_line(lat, lon, m, color='black', alpha=0.4, **kwargs): """Plot a line on the Earth (e.g. orbit) Parameters: ----------- lat: np.array Array of latitudes lon: np.array Array of longitudes m: Basemap The basemap references color: str, optional The color of the lines alpha: float, optional The alpha opacity of line kwargs: optional Other plotting keywords Returns: ----------- refs: list The list of line plot object references """ lon[(lon > 180.0)] -= 360.0 path = np.vstack((lon, lat)) isplit = np.nonzero(np.abs(np.diff(path[0])) > 5.0)[0] segments = np.split(path, isplit + 1, axis=1) refs = [] for segment in segments: x, y = m(segment[0], segment[1]) refs.append(m.plot(x, y, color=color, alpha=alpha, **kwargs)) return refs def earth_points(lat, lon, m, color='black', alpha=1.0, **kwargs): """Plot a point or points on the Earth Parameters: ----------- lat: np.array Array of latitudes lon: np.array Array of longitudes m: Basemap The basemap references color: str, optional The color of the lines alpha: float, optional The alpha opacity of line kwargs: optional Other plotting keywords Returns: ----------- ref: The scatter plot object reference """ lon[(lon > 180.0)] -= 360.0 x, y = m(lon, lat) if 's' not in kwargs.keys(): kwargs['s'] = 1000 ref = m.scatter(x, y, color=color, alpha=alpha, **kwargs) return [ref] # ---------- Sky and Fermi Inertial Coordinates ----------# def sky_point(x, y, ax, flipped=True, fermi=False, **kwargs): """Plot a point on the sky defined by the RA and Dec Inputs: ----------- x: float The azimuthal coordinate (RA or Fermi azimuth), in degrees y: float The polar coordinate (Dec or Fermi zenith), in degrees ax: matplotlib.axes Plot axes object flipped: bool, optional If True, the azimuthal axis is flipped, following equatorial convention fermi: bool, optional If True, plot in Fermi spacecraft coordinates, else plot in equatorial. Default is False. **kwargs: optional Other plotting options Returns: ---------- point: matplotlib.collections.PathCollection The plot object """ theta = np.array(np.deg2rad(y)) phi = np.array(np.deg2rad(x - 180.0)) if fermi: flipped = False theta = np.pi / 2.0 - theta phi -= np.pi phi[phi < -np.pi] += 2 * np.pi if flipped: phi = -phi point = ax.scatter(phi, theta, **kwargs) return point def sky_line(x, y, ax, flipped=True, fermi=False, **kwargs): """Plot a line on a sky map, wrapping at the meridian Inputs: ----------- x: np.array The azimuthal coordinates (RA or Fermi azimuth), in degrees y: np.array The polar coordinates (Dec or Fermi zenith), in degrees ax: matplotlib.axes Plot axes object flipped: bool, optional If True, the azimuthal axis is flipped, following equatorial convention fermi: bool, optional If True, plot in Fermi spacecraft coordinates, else plot in equatorial. Default is False. **kwargs: optional Other plotting options """ theta = np.deg2rad(y) phi = np.deg2rad(x - 180.0) if fermi: flipped = False theta = np.pi / 2.0 - theta phi -= np.pi phi[phi < -np.pi] += 2 * np.pi if flipped: phi = -phi seg = np.vstack((phi, theta)) # here is where we split the segments at the meridian isplit = np.nonzero(np.abs(np.diff(seg[0])) > np.pi / 16.0)[0] subsegs = np.split(seg, isplit + 1, axis=1) # plot each path segment segrefs = [] for seg in subsegs: ref = ax.plot(seg[0], seg[1], **kwargs) segrefs.append(ref) return segrefs def sky_circle(center_x, center_y, radius, ax, flipped=True, fermi=False, face_color=None, face_alpha=None, edge_color=None, edge_alpha=None, **kwargs): """Plot a circle on the sky Inputs: ----------- center_x: float The azimuthal center (RA or Fermi azimuth), in degrees center_y: float The polar center (Dec or Fermi zenith), in degrees radius: float The ROI radius in degrees color: str The color of the ROI ax: matplotlib.axes Plot axes object flipped: bool, optional If True, the azimuthal axis is flipped, following equatorial convention fermi: bool, optional If True, plot in Fermi spacecraft coordinates, else plot in equatorial. Default is False. face_color: str, optional The color of the circle fill face_alpha: float, optional The alpha of the circle fill edge_color: str, optional The color of the circle edge edge_alpha: float, optional The alpha of the circle edge **kwargs: optional Other plotting options Returns: ----------- patches: list of matplotlib.patches.Polygon The circle polygons """ try: import mpl_toolkits.basemap except: raise ImportError('Cannot execute sky_circle due to missing Basemap.') theta = np.deg2rad(90.0 - center_y) phi = np.deg2rad(center_x) if fermi: flipped = False theta = np.pi / 2.0 - theta phi -= np.pi if phi < -np.pi: phi += 2 * np.pi rad = np.deg2rad(radius) # Call Leo's lalinference plotting helper functions # The native matplotlib functions don't cut and display the circle polygon # correctly on map projections roi = make_circle_poly(rad, theta, phi, 100) roi = cut_prime_meridian(roi) # plot each polygon section edge = colorConverter.to_rgba(edge_color, alpha=edge_alpha) face = colorConverter.to_rgba(face_color, alpha=face_alpha) patches = [] for section in roi: section[:, 0] -= np.pi if flipped: section[:, 0] *= -1.0 patch = ax.add_patch( plt.Polygon(section, facecolor=face, edgecolor=edge, \ **kwargs)) patches.append(patch) return patches def sky_annulus(center_x, center_y, radius, width, ax, color='black', alpha=0.3, fermi=False, flipped=True, **kwargs): """Plot an annulus on the sky defined by its center, radius, and width Inputs: ----------- center_x: float The azimuthal center (RA or Fermi azimuth), in degrees center_y: float The polar center (Dec or Fermi zenith), in degrees radius: float The radius in degrees, defined as the angular distance from the center to the middle of the width of the annulus band width: float The width of the annulus in degrees ax: matplotlib.axes Plot axes object color: string, optional The color of the annulus. Default is black. alpha: float, optional The opacity of the annulus. Default is 0.3 fermi: bool, optional If True, plot in Fermi spacecraft coordinates, else plot in equatorial. Default is False. flipped: bool, optional If True, the azimuthal axis is flipped, following equatorial convention **kwargs: optional Other plotting options """ try: import mpl_toolkits.basemap except: raise ImportError('Cannot execute sky_annulus due to missing Basemap.') edge = colorConverter.to_rgba(color, alpha=1.0) face = colorConverter.to_rgba(color, alpha=alpha) inner_radius = np.deg2rad(radius - width / 2.0) outer_radius = np.deg2rad(radius + width / 2.0) center_theta = np.deg2rad(90.0 - center_y) center_phi = np.deg2rad(center_x) if fermi: flipped = False center_theta = np.pi / 2.0 - center_theta center_phi = np.deg2rad(center_x + 180.0) # get the plot points for the inner and outer circles inner = make_circle_poly(inner_radius, center_theta, center_phi, 100) inner = cut_prime_meridian(inner) outer = make_circle_poly(outer_radius, center_theta, center_phi, 100) outer = cut_prime_meridian(outer) x1 = [] y1 = [] x2 = [] y2 = [] polys = [] # plot the inner circle for section in inner: section[:, 0] -= np.pi if flipped: section[:, 0] *= -1.0 polys.append(plt.Polygon(section, ec=edge, fill=False, **kwargs)) ax.add_patch(polys[-1]) x1.extend(section[:, 0]) y1.extend(section[:, 1]) # plot the outer circle for section in outer: section[:, 0] -= np.pi if flipped: section[:, 0] *= -1.0 polys.append(plt.Polygon(section, ec=edge, fill=False, **kwargs)) ax.add_patch(polys[-1]) x2.extend(section[:, 0]) y2.extend(section[:, 1]) # organize and plot the fill between the circles # organize and plot the fill between the circles x1.append(x1[0]) y1.append(y1[0]) x2.append(x2[0]) y2.append(y2[0]) x2 = x2[::-1] y2 = y2[::-1] xs = np.concatenate((x1, x2)) ys = np.concatenate((y1, y2)) f = ax.fill(np.ravel(xs), np.ravel(ys), facecolor=face, zorder=1000) refs = polys refs.append(f) return refs def sky_polygon(x, y, ax, face_color=None, edge_color=None, edge_alpha=1.0, face_alpha=0.3, flipped=True, fermi=False, **kwargs): """Plot single polygon on a sky map, wrapping at the meridian Inputs: ----------- paths: matplotlib.path.Path Object containing the contour path ax: matplotlib.axes Plot axes object face_color: str, optional The color of the polygon fill face_alpha: float, optional The alpha of the polygon fill edge_color: str, optional The color of the polygon edge edge_alpha: float, optional The alpha of the polygon edge fermi: bool, optional If True, plot in Fermi spacecraft coordinates, else plot in equatorial. Default is False. flipped: bool, optional If True, the azimuthal axis is flipped, following equatorial convention **kwargs: optional Other plotting options """ line = sky_line(x, y, ax, color=edge_color, alpha=edge_alpha, flipped=flipped, fermi=fermi, **kwargs) refs = line theta = np.deg2rad(y) phi = np.deg2rad(x - 180.0) if fermi: flipped = False theta = np.pi / 2.0 - theta phi -= np.pi phi[phi < -np.pi] += 2 * np.pi if flipped: phi = -phi # this is needed to determine when contour spans the full ra range if (np.min(phi) == -np.pi) and (np.max(phi) == np.pi): if np.mean(theta) > 0.0: # fill in positive dec theta = np.insert(theta, 0, np.pi / 2.0) theta = np.append(theta, np.pi / 2.0) phi = np.insert(phi, 0, -np.pi) phi = np.append(phi, np.pi) else: # fill in negative dec theta = np.insert(theta, 0, -np.pi / 2.0) theta = np.append(theta, -np.pi / 2.0) phi = np.insert(phi, 0, np.pi) phi = np.append(phi, -np.pi) f = ax.fill(phi, theta, color=face_color, alpha=face_alpha, **kwargs) refs.append(f) return refs def galactic_plane(ax, flipped=True, fermi_quat=None, outer_color='dimgray', inner_color='black', line_alpha=0.5, center_alpha=0.75, **kwargs): """Plot the galactic plane on the sky Inputs: ----------- ax: matplotlib.axes Plot axes object outer_color: str, optional The color of the outer line inner_color: str, optional The color of the inner line line_alpha: float, optional The alpha of the line center_alpha: float, optional The alpha of the center fermi_quat: np.array, optional If set, rotate the galactic plane into the Fermi frame flipped: bool, optional If True, the azimuthal axis is flipped, following equatorial convention **kwargs: optional Other plotting options """ fermi = False # if a quaternion is sent, then plot in the Fermi frame if fermi_quat is not None: flipped = False fermi = True # longitude and latitude arrays lon_array = np.arange(0, 360, dtype=float) lat = np.zeros_like(lon_array) gc = SkyCoord(l=lon_array * u.degree, b=lat * u.degree, frame='galactic') ra, dec = gc.icrs.ra.deg, gc.icrs.dec.deg # plot in Fermi frame if fermi_quat is not None: ra, dec = radec_to_spacecraft(ra, dec, fermi_quat) line1 = sky_line(ra, dec, ax, color=outer_color, linewidth=3, alpha=line_alpha, flipped=flipped, fermi=fermi) line2 = sky_line(ra, dec, ax, color=inner_color, linewidth=1, alpha=line_alpha, flipped=flipped, fermi=fermi) # plot Galactic center pt1 = sky_point(ra[0], dec[0], ax, marker='o', c=outer_color, s=100, alpha=center_alpha, edgecolor=None, flipped=flipped, fermi=fermi) pt2 = sky_point(ra[0], dec[0], ax, marker='o', c=inner_color, s=20, alpha=center_alpha, edgecolor=None, flipped=flipped, fermi=fermi) return [line1, line2, pt1, pt2] def sky_heatmap(x, y, heatmap, ax, cmap='RdPu', norm=None, flipped=True, fermi=False, **kwargs): """Plot a heatmap on the sky as a colormap gradient Inputs: ----------- x: np.array The azimuthal coordinate array of the heatmap grid y: np.array The polar coordinate array of the heatmap grid heatmap: np.array The heatmap array, of shape (num_x, num_y) ax: matplotlib.axes Plot axes object cmap: str, optional The colormap. Default is 'RdPu' norm: matplotlib.colors.Normalize or similar, optional The normalization used to scale the colormapping to the heatmap values. This can be initialized by Normalize, LogNorm, SymLogNorm, PowerNorm, or some custom normalization. Default is PowerNorm(gamma=0.3). flipped: bool, optional If True, the azimuthal axis is flipped, following equatorial convention fermi: bool, optional If True, plot in Fermi spacecraft coordinates, else plot in equatorial. Default is False. **kwargs: optional Other plotting options Returns: -------- image: matplotlib.collections.QuadMesh The reference to the heatmap plot object """ theta = np.deg2rad(y) phi = np.deg2rad(x - 180.0) if fermi: flipped = False theta = np.pi / 2.0 - theta phi -= np.pi phi[phi < -np.pi] += 2 * np.pi shift = int(phi.shape[0] / 2.0) phi = np.roll(phi, shift) heatmap = np.roll(heatmap, shift, axis=1) if flipped: phi = -phi image = ax.pcolormesh(phi, theta, heatmap, rasterized=True, cmap=cmap, norm=norm) return image