def draw_nodes(self, net=None, node_colors={}, node_radius={}): if not net: net = self.net if type(node_colors) == str: node_colors = {node: node_colors for node in net.nodes()} nodeCircles = [] for n in net.nodes(): c = NodeCircle(tuple(net.pos[n]), node_radius.get(n, 8.0), color=node_colors.get(n, 'r'), ec='k', lw=1.0, ls='solid', picker=3) nodeCircles.append(c) node_collection = PatchCollection(nodeCircles, match_original=True) node_collection.set_picker(3) self.axes.add_collection(node_collection) return node_collection
class CameraDisplay: """ Camera Display using matplotlib. Parameters ---------- geometry : `~ctapipe.io.CameraGeometry` Definition of the Camera/Image image: array_like array of values corresponding to the pixels in the CameraGeometry. ax : `matplotlib.axes.Axes` A matplotlib axes object to plot on, or None to create a new one title : str (default "Camera") Title to put on camera plot norm : str or `matplotlib.color.Normalize` instance (default 'lin') Normalization for the color scale. Supported str arguments are - 'lin': linear scale - 'log': logarithmic scale (base 10) cmap : str or `matplotlib.colors.Colormap` (default 'hot') Color map to use (see `matplotlib.cm`) allow_pick : bool (default False) if True, allow user to click and select a pixel autoupdate : bool (default True) redraw automatically (otherwise need to call plt.draw()) autoscale : bool (default True) rescale the vmin/vmax values when the image changes. This is set to False if `set_limits_*` is called to explicity set data limits. antialiased : bool (default True) whether to draw in antialiased mode or not. Notes ----- Speed: CameraDisplay is not intended to be very fast (matplotlib is not a very speed performant graphics library, it is intended for nice output plots). However, most of the slowness of CameraDisplay is in the constructor. Once one is displayed, changing the image that is displayed is relatively fast and efficient. Therefore it is best to initialize an instance, and change the data, rather than generating new CameraDisplays. Pixel Implementation: Pixels are rendered as a `matplotlib.collections.PatchCollection` of Polygons (either 6 or 4 sided). You can access the PatchCollection directly (to e.g. change low-level style parameters) via `CameraDisplay.pixels` Output: Since CameraDisplay uses matplotlib, any display can be saved to any output file supported via plt.savefig(filename). This includes `.pdf` and `.png`. """ def __init__( self, geometry, image=None, ax=None, title="Camera", norm="lin", cmap="hot", allow_pick=False, autoupdate=True, autoscale=True, antialiased=True, ): self.axes = ax if ax is not None else plt.gca() self.geom = geometry self.pixels = None self.colorbar = None self.autoupdate = autoupdate self.autoscale = autoscale self._active_pixel = None self._active_pixel_label = None # initialize the plot and generate the pixels as a # RegularPolyCollection patches = [] for xx, yy, aa in zip( u.Quantity(self.geom.pix_x).value, u.Quantity(self.geom.pix_y).value, u.Quantity(np.array(self.geom.pix_area))): if self.geom.pix_type.startswith("hex"): rr = sqrt(aa * 2 / 3 / sqrt(3)) poly = RegularPolygon( (xx, yy), 6, radius=rr, orientation=self.geom.pix_rotation.rad, fill=True, ) else: rr = sqrt(aa) poly = Rectangle( (xx - rr / 2., yy - rr / 2.), width=rr, height=rr, angle=self.geom.pix_rotation.deg, fill=True, ) patches.append(poly) self.pixels = PatchCollection(patches, cmap=cmap, linewidth=0) self.axes.add_collection(self.pixels) self.pixel_highlighting = copy.copy(self.pixels) self.pixel_highlighting.set_facecolor('none') self.pixel_highlighting.set_linewidth(0) self.axes.add_collection(self.pixel_highlighting) # Set up some nice plot defaults self.axes.set_aspect('equal', 'datalim') self.axes.set_title(title) self.axes.set_xlabel("X position ({})".format(self.geom.pix_x.unit)) self.axes.set_ylabel("Y position ({})".format(self.geom.pix_y.unit)) self.axes.autoscale_view() # set up a patch to display when a pixel is clicked (and # pixel_picker is enabled): self._active_pixel = copy.copy(patches[0]) self._active_pixel.set_facecolor('r') self._active_pixel.set_alpha(0.5) self._active_pixel.set_linewidth(2.0) self._active_pixel.set_visible(False) self.axes.add_patch(self._active_pixel) self._active_pixel_label = self.axes.text(self._active_pixel.xy[0], self._active_pixel.xy[1], "0", horizontalalignment='center', verticalalignment='center') self._active_pixel_label.set_visible(False) # enable ability to click on pixel and do something (can be # enabled on-the-fly later as well: if allow_pick: self.enable_pixel_picker() if image is not None: self.image = image else: self.image = np.zeros_like(self.geom.pix_id, dtype=np.float) self.norm = norm def highlight_pixels(self, pixels, color='g', linewidth=1, alpha=0.75): ''' Highlight the given pixels with a colored line around them Parameters ---------- pixels : index-like The pixels to highlight. Can either be a list or array of integers or a boolean mask of length number of pixels color: a matplotlib conform color the color for the pixel highlighting linewidth: float linewidth of the highlighting in points alpha: 0 <= alpha <= 1 The transparency ''' l = np.zeros_like(self.image) l[pixels] = linewidth self.pixel_highlighting.set_linewidth(l) self.pixel_highlighting.set_alpha(alpha) self.pixel_highlighting.set_edgecolor(color) self.update() def enable_pixel_picker(self): """ enable ability to click on pixels """ self.pixels.set_picker(True) # enable click self.pixels.set_pickradius( sqrt(u.Quantity(self.geom.pix_area[0]).value) / np.pi) self.pixels.set_snap(True) # snap cursor to pixel center self.axes.figure.canvas.mpl_connect('pick_event', self._on_pick) def set_limits_minmax(self, zmin, zmax): """ set the color scale limits from min to max """ self.pixels.set_clim(zmin, zmax) self.autoscale = False self.update() def set_limits_percent(self, percent=95): """ auto-scale the color range to percent of maximum """ zmin = self.pixels.get_array().min() zmax = self.pixels.get_array().max() dz = zmax - zmin frac = percent / 100.0 self.autoscale = False self.set_limits_minmax(zmin, zmax - (1.0 - frac) * dz) @property def norm(self): ''' The norm instance of the Display Possible values: - "lin": linear scale - "log": log scale - any matplotlib.colors.Normalize instance, e. g. PowerNorm(gamma=-2) ''' return self.pixels.norm @norm.setter def norm(self, norm): if norm == 'lin': self.pixels.norm = Normalize() elif norm == 'log': self.pixels.norm = LogNorm() self.pixels.autoscale() # this is to handle matplotlib bug #5424 elif isinstance(norm, Normalize): self.pixels.norm = norm else: raise ValueError('Unsupported norm: {}'.format(norm)) self.update(force=True) self.pixels.autoscale() @property def cmap(self): """ Color map to use. Either a name or `matplotlib.colors.ColorMap` instance, e.g. from `matplotlib.pyplot.cm` """ return self.pixels.get_cmap() @cmap.setter def cmap(self, cmap): self.pixels.set_cmap(cmap) self.update() @property def image(self): """The image displayed on the camera (1D array of pixel values)""" return self.pixels.get_array() @image.setter def image(self, image): """ Change the image displayed on the Camera. Parameters ---------- image: array_like array of values corresponding to the pixels in the CameraGeometry. """ image = np.asanyarray(image) if image.shape != self.geom.pix_x.shape: raise ValueError("Image has a different shape {} than the" "given CameraGeometry {}".format( image.shape, self.geom.pix_x.shape)) self.pixels.set_array(image) self.pixels.changed() if self.autoscale: self.pixels.autoscale() self.update() def update(self, force=False): """ signal a redraw if necessary """ if self.autoupdate: if self.colorbar is not None: if force is True: self.colorbar.update_bruteforce(self.pixels) else: self.colorbar.update_normal(self.pixels) self.colorbar.draw_all() self.axes.figure.canvas.draw() def add_colorbar(self, **kwargs): """ add a colobar to the camera plot kwargs are passed to `figure.colorbar(self.pixels, **kwargs)` See matplotlib documentation for the supported kwargs: http://matplotlib.org/api/figure_api.html#matplotlib.figure.Figure.colorbar """ if self.colorbar is not None: raise ValueError( 'There is already a colorbar attached to this CameraDisplay') else: self.colorbar = self.axes.figure.colorbar(self.pixels, **kwargs) self.update() def add_ellipse(self, centroid, length, width, angle, asymmetry=0.0, **kwargs): """ plot an ellipse on top of the camera Parameters ---------- centroid: (float, float) position of centroid length: float major axis width: float minor axis angle: float rotation angle wrt "up" about the centroid, clockwise, in radians asymmetry: float 3rd-order moment for directionality if known kwargs: any MatPlotLib style arguments to pass to the Ellipse patch """ ellipse = Ellipse(xy=centroid, width=width, height=length, angle=np.degrees(angle), fill=False, **kwargs) self.axes.add_patch(ellipse) self.update() return ellipse def overlay_moments(self, momparams, **kwargs): """helper to overlay ellipse from a `reco.MomentParameters` structure Parameters ---------- momparams: `reco.MomentParameters` structuring containing Hillas-style parameterization kwargs: key=value any style keywords to pass to matplotlib (e.g. color='red' or linewidth=6) """ el = self.add_ellipse(centroid=(momparams.cen_x.value, momparams.cen_y.value), length=momparams.length.value, width=momparams.width.value, angle=momparams.psi.to(u.rad).value, **kwargs) self.axes.text(momparams.cen_x.value, momparams.cen_y.value, ("({:.02f},{:.02f})\n" "[w={:.02f},l={:.02f}]").format( momparams.cen_x, momparams.cen_y, momparams.width, momparams.length), color=el.get_edgecolor()) def _on_pick(self, event): """ handler for when a pixel is clicked """ pix_id = event.ind[-1] xx, yy, aa = u.Quantity(self.geom.pix_x[pix_id]).value, \ u.Quantity(self.geom.pix_y[pix_id]).value, \ u.Quantity(np.array(self.geom.pix_area)[pix_id]) if self.geom.pix_type.startswith("hex"): self._active_pixel.xy = (xx, yy) else: rr = sqrt(aa) self._active_pixel.xy = (xx - rr / 2., yy - rr / 2.) self._active_pixel.set_visible(True) self._active_pixel_label.set_x(xx) self._active_pixel_label.set_y(yy) self._active_pixel_label.set_text("{:003d}".format(pix_id)) self._active_pixel_label.set_visible(True) self.update() self.on_pixel_clicked(pix_id) # call user-function def on_pixel_clicked(self, pix_id): """virtual function to overide in sub-classes to do something special when a pixel is clicked """ print("Clicked pixel_id {}".format(pix_id)) def show(self): self.axes.figure.show()
class CameraDisplay: """ Camera Display using matplotlib. Parameters ---------- geometry : `~ctapipe.instrument.CameraGeometry` Definition of the Camera/Image image: array_like array of values corresponding to the pixels in the CameraGeometry. ax : `matplotlib.axes.Axes` A matplotlib axes object to plot on, or None to create a new one title : str (default "Camera") Title to put on camera plot norm : str or `matplotlib.color.Normalize` instance (default 'lin') Normalization for the color scale. Supported str arguments are - 'lin': linear scale - 'log': logarithmic scale (base 10) cmap : str or `matplotlib.colors.Colormap` (default 'hot') Color map to use (see `matplotlib.cm`) allow_pick : bool (default False) if True, allow user to click and select a pixel autoupdate : bool (default True) redraw automatically (otherwise need to call plt.draw()) autoscale : bool (default True) rescale the vmin/vmax values when the image changes. This is set to False if `set_limits_*` is called to explicity set data limits. Notes ----- Speed: CameraDisplay is not intended to be very fast (matplotlib is not a very speed performant graphics library, it is intended for nice output plots). However, most of the slowness of CameraDisplay is in the constructor. Once one is displayed, changing the image that is displayed is relatively fast and efficient. Therefore it is best to initialize an instance, and change the data, rather than generating new CameraDisplays. Pixel Implementation: Pixels are rendered as a `matplotlib.collections.PatchCollection` of Polygons (either 6 or 4 sided). You can access the PatchCollection directly (to e.g. change low-level style parameters) via `CameraDisplay.pixels` Output: Since CameraDisplay uses matplotlib, any display can be saved to any output file supported via plt.savefig(filename). This includes ``.pdf`` and ``.png``. """ def __init__( self, geometry, image=None, ax=None, title=None, norm="lin", cmap=None, allow_pick=False, autoupdate=True, autoscale=True, ): self.axes = ax if ax is not None else plt.gca() self.pixels = None self.colorbar = None self.autoupdate = autoupdate self.autoscale = autoscale self._active_pixel = None self._active_pixel_label = None self._axes_overlays = [] self.geom = geometry if title is None: title = geometry.camera_name # initialize the plot and generate the pixels as a # RegularPolyCollection patches = [] if hasattr(self.geom, "mask"): self.mask = self.geom.mask else: self.mask = np.ones_like(self.geom.pix_x.value, dtype=bool) pix_x = self.geom.pix_x.value[self.mask] pix_y = self.geom.pix_y.value[self.mask] pix_area = self.geom.pix_area.value[self.mask] for x, y, area in zip(pix_x, pix_y, pix_area): if self.geom.pix_type.startswith("hex"): r = sqrt(area * 2 / 3 / sqrt(3)) + 2 * PIXEL_EPSILON poly = RegularPolygon( (x, y), 6, radius=r, orientation=self.geom.pix_rotation.to_value(u.rad), fill=True, ) else: r = sqrt(area) + PIXEL_EPSILON poly = Rectangle( (x - r / 2, y - r / 2), width=r, height=r, angle=self.geom.pix_rotation.to_value(u.deg), fill=True, ) patches.append(poly) self.pixels = PatchCollection(patches, cmap=cmap, linewidth=0) self.axes.add_collection(self.pixels) self.pixel_highlighting = copy.copy(self.pixels) self.pixel_highlighting.set_facecolor("none") self.pixel_highlighting.set_linewidth(0) self.axes.add_collection(self.pixel_highlighting) # Set up some nice plot defaults self.axes.set_aspect("equal", "datalim") self.axes.set_title(title) self.axes.set_xlabel(f"X position ({self.geom.pix_x.unit})") self.axes.set_ylabel(f"Y position ({self.geom.pix_y.unit})") self.axes.autoscale_view() # set up a patch to display when a pixel is clicked (and # pixel_picker is enabled): self._active_pixel = copy.copy(patches[0]) self._active_pixel.set_facecolor("r") self._active_pixel.set_alpha(0.5) self._active_pixel.set_linewidth(2.0) self._active_pixel.set_visible(False) self.axes.add_patch(self._active_pixel) self._active_pixel_label = self.axes.text( self._active_pixel.xy[0], self._active_pixel.xy[1], "0", horizontalalignment="center", verticalalignment="center", ) self._active_pixel_label.set_visible(False) # enable ability to click on pixel and do something (can be # enabled on-the-fly later as well: if allow_pick: self.enable_pixel_picker() if image is not None: self.image = image else: self.image = np.zeros_like(self.geom.pix_id, dtype=np.float) self.norm = norm def highlight_pixels(self, pixels, color="g", linewidth=1, alpha=0.75): """ Highlight the given pixels with a colored line around them Parameters ---------- pixels : index-like The pixels to highlight. Can either be a list or array of integers or a boolean mask of length number of pixels color: a matplotlib conform color the color for the pixel highlighting linewidth: float linewidth of the highlighting in points alpha: 0 <= alpha <= 1 The transparency """ l = np.zeros_like(self.image) l[pixels] = linewidth self.pixel_highlighting.set_linewidth(l) self.pixel_highlighting.set_alpha(alpha) self.pixel_highlighting.set_edgecolor(color) self._update() def enable_pixel_picker(self): """ enable ability to click on pixels """ self.pixels.set_picker(True) # enable click self.pixels.set_pickradius( sqrt(u.Quantity(self.geom.pix_area[0]).value) / np.pi) self.pixels.set_snap(True) # snap cursor to pixel center self.axes.figure.canvas.mpl_connect("pick_event", self._on_pick) def set_limits_minmax(self, zmin, zmax): """ set the color scale limits from min to max """ self.pixels.set_clim(zmin, zmax) self.autoscale = False self._update() def set_limits_percent(self, percent=95): """ auto-scale the color range to percent of maximum """ zmin = np.nanmin(self.pixels.get_array()) zmax = np.nanmax(self.pixels.get_array()) dz = zmax - zmin frac = percent / 100.0 self.autoscale = False self.set_limits_minmax(zmin, zmax - (1.0 - frac) * dz) @property def norm(self): """ The norm instance of the Display Possible values: - "lin": linear scale - "log": log scale (cannot have negative values) - "symlog": symmetric log scale (negative values are ok) - any matplotlib.colors.Normalize instance, e. g. PowerNorm(gamma=-2) """ return self.pixels.norm @norm.setter def norm(self, norm): if norm == "lin": self.pixels.norm = Normalize() elif norm == "log": self.pixels.norm = LogNorm() self.pixels.autoscale() # this is to handle matplotlib bug #5424 elif norm == "symlog": self.pixels.norm = SymLogNorm(linthresh=1.0) self.pixels.autoscale() elif isinstance(norm, Normalize): self.pixels.norm = norm else: raise ValueError( "Unsupported norm: '{}', options are 'lin'," "'log','symlog', or a matplotlib Normalize object".format( norm)) self.update(force=True) self.pixels.autoscale() @property def cmap(self): """ Color map to use. Either a name or `matplotlib.colors.ColorMap` instance, e.g. from `matplotlib.pyplot.cm` """ return self.pixels.get_cmap() @cmap.setter def cmap(self, cmap): self.pixels.set_cmap(cmap) self._update() @property def image(self): """The image displayed on the camera (1D array of pixel values)""" return self.pixels.get_array() @image.setter def image(self, image): """ Change the image displayed on the Camera. Parameters ---------- image: array_like array of values corresponding to the pixels in the CameraGeometry. """ image = np.asanyarray(image) if image.shape != self.geom.pix_x.shape: raise ValueError( ("Image has a different shape {} than the " "given CameraGeometry {}").format(image.shape, self.geom.pix_x.shape)) self.pixels.set_array(np.ma.masked_invalid(image[self.mask])) self.pixels.changed() if self.autoscale: self.pixels.autoscale() self._update() def _update(self, force=False): """ signal a redraw if autoupdate is turned on """ if self.autoupdate: self.update(force) def update(self, force=False): """ redraw the display now """ self.axes.figure.canvas.draw() if self.colorbar is not None: if force is True: self.colorbar.update_bruteforce(self.pixels) else: self.colorbar.update_normal(self.pixels) self.colorbar.draw_all() def add_colorbar(self, **kwargs): """ add a colorbar to the camera plot kwargs are passed to `figure.colorbar(self.pixels, **kwargs)` See matplotlib documentation for the supported kwargs: http://matplotlib.org/api/figure_api.html#matplotlib.figure.Figure.colorbar """ if self.colorbar is not None: raise ValueError( "There is already a colorbar attached to this CameraDisplay") else: if "ax" not in kwargs: kwargs["ax"] = self.axes self.colorbar = self.axes.figure.colorbar(self.pixels, **kwargs) self.update() def add_ellipse(self, centroid, length, width, angle, asymmetry=0.0, **kwargs): """ plot an ellipse on top of the camera Parameters ---------- centroid: (float, float) position of centroid length: float major axis width: float minor axis angle: float rotation angle wrt x-axis about the centroid, anticlockwise, in radians asymmetry: float 3rd-order moment for directionality if known kwargs: any MatPlotLib style arguments to pass to the Ellipse patch """ ellipse = Ellipse( xy=centroid, width=length, height=width, angle=np.degrees(angle), fill=False, **kwargs, ) self.axes.add_patch(ellipse) self.update() return ellipse def overlay_moments(self, hillas_parameters, with_label=True, keep_old=False, **kwargs): """helper to overlay ellipse from a `HillasParametersContainer` structure Parameters ---------- hillas_parameters: `HillasParametersContainer` structuring containing Hillas-style parameterization with_label: bool If True, show coordinates of centroid and width and length keep_old: bool If True, to not remove old overlays kwargs: key=value any style keywords to pass to matplotlib (e.g. color='red' or linewidth=6) """ if not keep_old: self.clear_overlays() # strip off any units cen_x = u.Quantity(hillas_parameters.x).value cen_y = u.Quantity(hillas_parameters.y).value length = u.Quantity(hillas_parameters.length).value width = u.Quantity(hillas_parameters.width).value el = self.add_ellipse( centroid=(cen_x, cen_y), length=length * 2, width=width * 2, angle=hillas_parameters.psi.rad, **kwargs, ) self._axes_overlays.append(el) if with_label: text = self.axes.text( cen_x, cen_y, "({:.02f},{:.02f})\n[w={:.02f},l={:.02f}]".format( hillas_parameters.x, hillas_parameters.y, hillas_parameters.width, hillas_parameters.length, ), color=el.get_edgecolor(), ) self._axes_overlays.append(text) def clear_overlays(self): """ Remove added overlays from the axes """ while self._axes_overlays: overlay = self._axes_overlays.pop() overlay.remove() def _on_pick(self, event): """ handler for when a pixel is clicked """ pix_id = event.ind[-1] xx, yy, aa = ( u.Quantity(self.geom.pix_x[pix_id]).value, u.Quantity(self.geom.pix_y[pix_id]).value, u.Quantity(np.array(self.geom.pix_area)[pix_id]), ) if self.geom.pix_type.startswith("hex"): self._active_pixel.xy = (xx, yy) else: rr = sqrt(aa) self._active_pixel.xy = (xx - rr / 2.0, yy - rr / 2.0) self._active_pixel.set_visible(True) self._active_pixel_label.set_x(xx) self._active_pixel_label.set_y(yy) self._active_pixel_label.set_text(f"{pix_id:003d}") self._active_pixel_label.set_visible(True) self._update() self.on_pixel_clicked(pix_id) # call user-function def on_pixel_clicked(self, pix_id): """virtual function to overide in sub-classes to do something special when a pixel is clicked """ print(f"Clicked pixel_id {pix_id}") def show(self): self.axes.figure.show()
class CameraDisplay: """ Camera Display using matplotlib. Parameters ---------- geometry : `~ctapipe.instrument.CameraGeometry` Definition of the Camera/Image image: array_like array of values corresponding to the pixels in the CameraGeometry. ax : `matplotlib.axes.Axes` A matplotlib axes object to plot on, or None to create a new one title : str (default "Camera") Title to put on camera plot norm : str or `matplotlib.color.Normalize` instance (default 'lin') Normalization for the color scale. Supported str arguments are - 'lin': linear scale - 'log': logarithmic scale (base 10) cmap : str or `matplotlib.colors.Colormap` (default 'hot') Color map to use (see `matplotlib.cm`) allow_pick : bool (default False) if True, allow user to click and select a pixel autoupdate : bool (default True) redraw automatically (otherwise need to call plt.draw()) autoscale : bool (default True) rescale the vmin/vmax values when the image changes. This is set to False if `set_limits_*` is called to explicity set data limits. antialiased : bool (default True) whether to draw in antialiased mode or not. Notes ----- Speed: CameraDisplay is not intended to be very fast (matplotlib is not a very speed performant graphics library, it is intended for nice output plots). However, most of the slowness of CameraDisplay is in the constructor. Once one is displayed, changing the image that is displayed is relatively fast and efficient. Therefore it is best to initialize an instance, and change the data, rather than generating new CameraDisplays. Pixel Implementation: Pixels are rendered as a `matplotlib.collections.PatchCollection` of Polygons (either 6 or 4 sided). You can access the PatchCollection directly (to e.g. change low-level style parameters) via `CameraDisplay.pixels` Output: Since CameraDisplay uses matplotlib, any display can be saved to any output file supported via plt.savefig(filename). This includes `.pdf` and `.png`. """ def __init__( self, geometry, image=None, ax=None, title=None, norm="lin", cmap=None, allow_pick=False, autoupdate=True, autoscale=True, antialiased=True, ): self.axes = ax if ax is not None else plt.gca() self.geom = geometry self.pixels = None self.colorbar = None self.autoupdate = autoupdate self.autoscale = autoscale self._active_pixel = None self._active_pixel_label = None if title is None: title = geometry.cam_id # initialize the plot and generate the pixels as a # RegularPolyCollection patches = [] if not hasattr(self.geom, "mask"): self.geom.mask = np.ones_like(self.geom.pix_x.value, dtype=bool) for xx, yy, aa in zip( u.Quantity(self.geom.pix_x[self.geom.mask]).value, u.Quantity(self.geom.pix_y[self.geom.mask]).value, u.Quantity(np.array(self.geom.pix_area)[self.geom.mask]).value): if self.geom.pix_type.startswith("hex"): rr = sqrt(aa * 2 / 3 / sqrt(3)) + 2*PIXEL_EPSILON poly = RegularPolygon( (xx, yy), 6, radius=rr, orientation=self.geom.pix_rotation.rad, fill=True, ) else: rr = sqrt(aa) + PIXEL_EPSILON poly = Rectangle( (xx-rr/2., yy-rr/2.), width=rr, height=rr, angle=self.geom.pix_rotation.deg, fill=True, ) patches.append(poly) self.pixels = PatchCollection(patches, cmap=cmap, linewidth=0) self.axes.add_collection(self.pixels) self.pixel_highlighting = copy.copy(self.pixels) self.pixel_highlighting.set_facecolor('none') self.pixel_highlighting.set_linewidth(0) self.axes.add_collection(self.pixel_highlighting) # Set up some nice plot defaults self.axes.set_aspect('equal', 'datalim') self.axes.set_title(title) self.axes.set_xlabel("X position ({})".format(self.geom.pix_x.unit)) self.axes.set_ylabel("Y position ({})".format(self.geom.pix_y.unit)) self.axes.autoscale_view() # set up a patch to display when a pixel is clicked (and # pixel_picker is enabled): self._active_pixel = copy.copy(patches[0]) self._active_pixel.set_facecolor('r') self._active_pixel.set_alpha(0.5) self._active_pixel.set_linewidth(2.0) self._active_pixel.set_visible(False) self.axes.add_patch(self._active_pixel) self._active_pixel_label = self.axes.text(self._active_pixel.xy[0], self._active_pixel.xy[1], "0", horizontalalignment='center', verticalalignment='center') self._active_pixel_label.set_visible(False) # enable ability to click on pixel and do something (can be # enabled on-the-fly later as well: if allow_pick: self.enable_pixel_picker() if image is not None: self.image = image else: self.image = np.zeros_like(self.geom.pix_id, dtype=np.float) self.norm = norm def highlight_pixels(self, pixels, color='g', linewidth=1, alpha=0.75): ''' Highlight the given pixels with a colored line around them Parameters ---------- pixels : index-like The pixels to highlight. Can either be a list or array of integers or a boolean mask of length number of pixels color: a matplotlib conform color the color for the pixel highlighting linewidth: float linewidth of the highlighting in points alpha: 0 <= alpha <= 1 The transparency ''' l = np.zeros_like(self.image) l[pixels] = linewidth self.pixel_highlighting.set_linewidth(l) self.pixel_highlighting.set_alpha(alpha) self.pixel_highlighting.set_edgecolor(color) self._update() def enable_pixel_picker(self): """ enable ability to click on pixels """ self.pixels.set_picker(True) # enable click self.pixels.set_pickradius(sqrt(u.Quantity(self.geom.pix_area[0]) .value) / np.pi) self.pixels.set_snap(True) # snap cursor to pixel center self.axes.figure.canvas.mpl_connect('pick_event', self._on_pick) def set_limits_minmax(self, zmin, zmax): """ set the color scale limits from min to max """ self.pixels.set_clim(zmin, zmax) self.autoscale = False self._update() def set_limits_percent(self, percent=95): """ auto-scale the color range to percent of maximum """ zmin = self.pixels.get_array().min() zmax = self.pixels.get_array().max() dz = zmax - zmin frac = percent / 100.0 self.autoscale = False self.set_limits_minmax(zmin, zmax - (1.0 - frac) * dz) @property def norm(self): ''' The norm instance of the Display Possible values: - "lin": linear scale - "log": log scale (cannot have negative values) - "symlog": symmetric log scale (negative values are ok) - any matplotlib.colors.Normalize instance, e. g. PowerNorm(gamma=-2) ''' return self.pixels.norm @norm.setter def norm(self, norm): if norm == 'lin': self.pixels.norm = Normalize() elif norm == 'log': self.pixels.norm = LogNorm() self.pixels.autoscale() # this is to handle matplotlib bug #5424 elif norm == 'symlog': self.pixels.norm = SymLogNorm(linthresh=1.0) self.pixels.autoscale() elif isinstance(norm, Normalize): self.pixels.norm = norm else: raise ValueError("Unsupported norm: '{}', options are 'lin'," "'log','symlog', or a matplotlib Normalize object" .format(norm)) self.update(force=True) self.pixels.autoscale() @property def cmap(self): """ Color map to use. Either a name or `matplotlib.colors.ColorMap` instance, e.g. from `matplotlib.pyplot.cm` """ return self.pixels.get_cmap() @cmap.setter def cmap(self, cmap): self.pixels.set_cmap(cmap) self._update() @property def image(self): """The image displayed on the camera (1D array of pixel values)""" return self.pixels.get_array() @image.setter def image(self, image): """ Change the image displayed on the Camera. Parameters ---------- image: array_like array of values corresponding to the pixels in the CameraGeometry. """ image = np.asanyarray(image) if image.shape != self.geom.pix_x.shape: raise ValueError( "Image has a different shape {} than the " "given CameraGeometry {}" .format(image.shape, self.geom.pix_x.shape) ) self.pixels.set_array(image[self.geom.mask]) self.pixels.changed() if self.autoscale: self.pixels.autoscale() self._update() def _update(self, force=False): """ signal a redraw if autoupdate is turned on """ if self.autoupdate: self.update(force) def update(self, force=False): """ redraw the display now """ self.axes.figure.canvas.draw() if self.colorbar is not None: if force is True: self.colorbar.update_bruteforce(self.pixels) else: self.colorbar.update_normal(self.pixels) self.colorbar.draw_all() def add_colorbar(self, **kwargs): """ add a colobar to the camera plot kwargs are passed to `figure.colorbar(self.pixels, **kwargs)` See matplotlib documentation for the supported kwargs: http://matplotlib.org/api/figure_api.html#matplotlib.figure.Figure.colorbar """ if self.colorbar is not None: raise ValueError( 'There is already a colorbar attached to this CameraDisplay' ) else: self.colorbar = self.axes.figure.colorbar(self.pixels, **kwargs) self.update() def add_ellipse(self, centroid, length, width, angle, asymmetry=0.0, **kwargs): """ plot an ellipse on top of the camera Parameters ---------- centroid: (float, float) position of centroid length: float major axis width: float minor axis angle: float rotation angle wrt x-axis about the centroid, anticlockwise, in radians asymmetry: float 3rd-order moment for directionality if known kwargs: any MatPlotLib style arguments to pass to the Ellipse patch """ ellipse = Ellipse(xy=centroid, width=length, height=width, angle=np.degrees(angle), fill=False, **kwargs) self.axes.add_patch(ellipse) self.update() return ellipse def overlay_moments(self, momparams, with_label=True, **kwargs): """helper to overlay ellipse from a `reco.MomentParameters` structure Parameters ---------- momparams: `reco.MomentParameters` structuring containing Hillas-style parameterization kwargs: key=value any style keywords to pass to matplotlib (e.g. color='red' or linewidth=6) """ # strip off any units cen_x = u.Quantity(momparams.cen_x).value cen_y = u.Quantity(momparams.cen_y).value length = u.Quantity(momparams.length).value width = u.Quantity(momparams.width).value el = self.add_ellipse(centroid=(cen_x, cen_y), length=length*2, width=width*2, angle=momparams.psi.rad, **kwargs) if with_label: self.axes.text(cen_x, cen_y, ("({:.02f},{:.02f})\n" "[w={:.02f},l={:.02f}]") .format(momparams.cen_x, momparams.cen_y, momparams.width, momparams.length), color=el.get_edgecolor()) def _on_pick(self, event): """ handler for when a pixel is clicked """ pix_id = event.ind[-1] xx, yy, aa = u.Quantity(self.geom.pix_x[pix_id]).value, \ u.Quantity(self.geom.pix_y[pix_id]).value, \ u.Quantity(np.array(self.geom.pix_area)[pix_id]) if self.geom.pix_type.startswith("hex"): self._active_pixel.xy = (xx, yy) else: rr = sqrt(aa) self._active_pixel.xy = (xx - rr / 2., yy - rr / 2.) self._active_pixel.set_visible(True) self._active_pixel_label.set_x(xx) self._active_pixel_label.set_y(yy) self._active_pixel_label.set_text("{:003d}".format(pix_id)) self._active_pixel_label.set_visible(True) self._update() self.on_pixel_clicked(pix_id) # call user-function def on_pixel_clicked(self, pix_id): """virtual function to overide in sub-classes to do something special when a pixel is clicked """ print("Clicked pixel_id {}".format(pix_id)) def show(self): self.axes.figure.show()
class SimulationGui(QMainWindow): def __init__(self, net=None, parent=None, fname=None): QMainWindow.__init__(self) self.ui = Ui_SimulationWindow() self.ui.setupUi(self) if fname: self.set_title(fname) # context menu self.ui.nodeInspector.addAction(self.ui.actionCopyInspectorData) self.ui.nodeInspector.addAction(self.ui.actionShowLocalizedSubclusters) # callbacks self.ui.actionCopyInspectorData.activated\ .connect(self.on_actionCopyInspectorData_triggered) self.ui.actionShowLocalizedSubclusters.activated\ .connect(self.on_actionShowLocalizedSubclusters_triggered) self.dpi = 72 # take size of networDisplayWidget self.fig = Figure((700 / self.dpi, 731 / self.dpi), self.dpi, facecolor='0.9') self.canvas = FigureCanvas(self.fig) self.canvas.setParent(self.ui.networkDisplayWidget) self.nav = NavigationToolbar(self.canvas, self.ui.networkDisplayWidget, coordinates=True) self.nav.setGeometry(QRect(0, 0, 651, 36)) self.nav.setIconSize(QSize(24, 24)) self.axes = self.fig.add_subplot(111) # matplotlib.org/api/figure_api.html#matplotlib.figure.SubplotParams self.fig.subplots_adjust(left=0.03, right=0.99, top=0.92) if net: self.init_sim(net) self.connect(self.ui.showNodes, SIGNAL('stateChanged(int)'), self.refresh_visibility) self.connect(self.ui.showEdges, SIGNAL('stateChanged(int)'), self.refresh_visibility) self.connect(self.ui.showMessages, SIGNAL('stateChanged(int)'), self.refresh_visibility) self.connect(self.ui.showLabels, SIGNAL('stateChanged(int)'), self.refresh_visibility) self.connect(self.ui.redrawNetworkButton, SIGNAL('clicked(bool)'), self.redraw) self.connect(self.ui.treeGroupBox, SIGNAL('toggled(bool)'), self.refresh_visibility) self.connect(self.ui.treeKey, SIGNAL('textEdited(QString)'), self.redraw) self.connect(self.ui.propagationError, SIGNAL('toggled(bool)'), self.refresh_visibility) self.connect(self.ui.locKey, SIGNAL('textEdited(QString)'), self.redraw) # callbacks self.ui.actionOpenNetwork.activated\ .connect(self.on_actionOpenNetwork_triggered) self.ui.actionSaveNetwork.activated\ .connect(self.on_actionSaveNetwork_triggered) self.ui.actionRun.activated.connect(self.on_actionRun_triggered) self.ui.actionStep.activated.connect(self.on_actionStep_triggered) self.ui.actionReset.activated.connect(self.on_actionReset_triggered) self.canvas.mpl_connect('pick_event', self.on_pick) def handleInspectorMenu(self, pos): menu = QMenu() menu.addAction('Add') menu.addAction('Delete') menu.exec_(QCursor.pos()) def init_sim(self, net): self.net = net self.sim = Simulation(net) self.connect(self.sim, SIGNAL("redraw()"), self.redraw) self.connect(self.sim, SIGNAL("updateLog(QString)"), self.update_log) self.redraw() def update_log(self, text): """ Add item to list widget """ print "Add: " + text self.ui.logListWidget.insertItem(0, text) # self.ui.logListWidget.sortItems() def redraw(self): self.refresh_network_inspector() self.draw_network() self.reset_zoom() self.refresh_visibility() def draw_network(self, net=None, clear=True, subclusters=None, drawMessages=True): if not net: net = self.net currentAlgorithm = self.net.get_current_algorithm() if clear: self.axes.clear() self.axes.imshow(net.environment.im, vmin=0, cmap='binary_r', origin='lower') self.draw_tree(str(self.ui.treeKey.text()), net) self.draw_edges(net) self.draw_propagation_errors(str(self.ui.locKey.text()), net) if subclusters: node_colors = self.get_node_colors(net, subclusters=subclusters) else: node_colors = self.get_node_colors(net, algorithm=currentAlgorithm) self.node_collection = self.draw_nodes(net, node_colors) if drawMessages: self.draw_messages(net) self.draw_labels(net) self.drawnNet = net step_text = ' (step %d)' % self.net.algorithmState['step'] \ if isinstance(currentAlgorithm, NodeAlgorithm) else '' self.axes.set_title((currentAlgorithm.name if currentAlgorithm else '') + step_text) self.refresh_visibility() # To save multiple figs of the simulation uncomment next two lines: #self.fig.savefig('network-alg-%d-step-%d.png' % # (self.net.algorithmState['index'], self.net.algorithmState['step'])) def draw_nodes(self, net=None, node_colors={}, node_radius={}): if not net: net = self.net if type(node_colors) == str: node_colors = {node: node_colors for node in net.nodes()} nodeCircles = [] for n in net.nodes(): c = NodeCircle(tuple(net.pos[n]), node_radius.get(n, 8.0), color=node_colors.get(n, 'r'), ec='k', lw=1.0, ls='solid', picker=3) nodeCircles.append(c) node_collection = PatchCollection(nodeCircles, match_original=True) node_collection.set_picker(3) self.axes.add_collection(node_collection) return node_collection def get_node_colors(self, net, algorithm=None, subclusters=None, drawLegend=True): COLORS = 'rgbcmyw' * 100 node_colors = {} if algorithm: color_map = {} if isinstance(algorithm, NodeAlgorithm): for ind, status in enumerate(algorithm.STATUS.keys()): if status == 'IDLE': color_map.update({status: 'k'}) else: color_map.update({status: COLORS[ind]}) if drawLegend: proxy = [] labels = [] for status, color in color_map.items(): proxy.append(Circle((0, 0), radius=8.0, color=color, ec='k', lw=1.0, ls='solid')) labels.append(status) self.fig.legends = [] self.fig.legend(proxy, labels, loc=8, prop={'size': '10.0'}, ncol=len(proxy), title='Statuses for %s:' % algorithm.name) for n in net.nodes(): if n.status == '' or not n.status in color_map.keys(): node_colors[n] = 'r' else: node_colors[n] = color_map[n.status] elif subclusters: for i, sc in enumerate(subclusters): for n in sc: if n in node_colors: node_colors[n] = 'k' else: node_colors[n] = COLORS[i] return node_colors def draw_edges(self, net=None): if not net: net = self.net self.edge_collection = nx.draw_networkx_edges(net, net.pos, alpha=0.6, edgelist=None, ax=self.axes) def draw_messages(self, net=None): if not net: net = self.net self.messages = [] msgCircles = [] for node in net.nodes(): for msg in node.outbox: # broadcast if msg.destination is None: for neighbor in net.adj[node].keys(): nbr_msg = msg.copy() nbr_msg.destination = neighbor c = MessageCircle(nbr_msg, net, 'out', 3.0, lw=0, picker=3, zorder=3, color='b') self.messages.append(nbr_msg) msgCircles.append(c) else: c = MessageCircle(msg, net, 'out', 3.0, lw=0, picker=3, zorder=3, color='b') self.messages.append(msg) msgCircles.append(c) for msg in node.inbox: c = MessageCircle(msg, net, 'in', 3.0, lw=0, picker=3, zorder=3, color='g') self.messages.append(msg) msgCircles.append(c) if self.messages: self.message_collection = PatchCollection(msgCircles, match_original=True) self.message_collection.set_picker(3) self.axes.add_collection(self.message_collection) def draw_labels(self, net=None): if not net: net = self.net label_pos = {} for n in net.nodes(): label_pos[n] = net.pos[n].copy() + 10 self.label_collection = nx.draw_networkx_labels(net, label_pos, labels=net.labels, ax=self.axes) def refresh_visibility(self): try: self.node_collection.set_visible(self.ui.showNodes.isChecked()) self.edge_collection.set_visible(self.ui.showEdges.isChecked()) for label in self.label_collection.values(): label.set_visible(self.ui.showLabels.isChecked()) self.tree_collection.set_visible(self.ui.treeGroupBox.isChecked()) self.ini_error_collection.set_visible(self.ui.propagationError\ .isChecked()) self.propagation_error_collection.set_visible(self.ui\ .propagationError\ .isChecked()) # sould be last, sometimes there are no messages self.message_collection.set_visible(self.ui.showMessages\ .isChecked()) except AttributeError: print 'Refresh visibility warning' self.canvas.draw() def draw_tree(self, treeKey, net=None): """ Show tree representation of network. Attributes: treeKey (str): key in nodes memory (dictionary) where tree data is stored storage format can be a list off tree neighbors or a dict: {'parent': parent_node, 'children': [child_node1, child_node2 ...]} """ if not net: net = self.net treeNet = net.get_tree_net(treeKey) if treeNet: self.tree_collection = draw_networkx_edges(treeNet, treeNet.pos, treeNet.edges(), width=1.8, alpha=0.6, ax=self.axes) def draw_propagation_errors(self, locKey, net): SCALE_FACTOR = 0.6 if not net: net = self.net if any([not locKey in node.memory for node in net.nodes()]): self.propagation_error_collection = [] self.ini_error_collection = [] return rms = {'iniRms': {}, 'stitchRms': {}} for node in net.nodes(): rms['iniRms'][node] = get_rms(self.net.pos, (node.memory['iniLocs']), True) * \ SCALE_FACTOR rms['stitchRms'][node] = get_rms(self.net.pos, node.memory[locKey], True) * SCALE_FACTOR self.propagation_error_collection = \ self.draw_nodes(net=net, node_colors='g', node_radius=rms['stitchRms']) self.ini_error_collection = self.draw_nodes(net=net, node_colors='b', node_radius=rms['iniRms']) def reset_zoom(self): self.axes.set_xlim((0, self.net.environment.im.shape[1])) self.axes.set_ylim((0, self.net.environment.im.shape[0])) def set_title(self, fname): new = ' - '.join([str(self.windowTitle()).split(' - ')[0], str(fname)]) self.setWindowTitle(new) def refresh_network_inspector(self): niModel = DictionaryTreeModel(dic=self.net.get_dic()) self.ui.networkInspector.setModel(niModel) self.ui.networkInspector.expandToDepth(0) """ Callbacks """ def on_actionRun_triggered(self): self.ui.logListWidget.clear() print 'running ...', self.sim.stepping = True self.sim.run_all() def on_actionStep_triggered(self): print 'next step ...', self.sim.run(self.ui.stepSize.value()) def on_actionReset_triggered(self): print 'reset ...', self.sim.reset() self.redraw() def on_actionCopyInspectorData_triggered(self): string = 'Node inspector data\n-------------------' # raise() for qModelIndex in self.ui.nodeInspector.selectedIndexes(): string += '\n' + qModelIndex.internalPointer().toString(' ') clipboard = app.clipboard() clipboard.setText(string) event = QEvent(QEvent.Clipboard) app.sendEvent(clipboard, event) def on_actionShowLocalizedSubclusters_triggered(self): if len(self.ui.nodeInspector.selectedIndexes()) == 1: qModelIndex = self.ui.nodeInspector.selectedIndexes()[0] treeItem = qModelIndex.internalPointer() assert(isinstance(treeItem.itemDataValue, Positions)) estimated = deepcopy(treeItem.itemDataValue) estimatedsub = estimated.subclusters[0] # rotate, translate and optionally scale # w.r.t. original positions (pos) align_clusters(Positions.create(self.net.pos), estimated, True) net = self.net.subnetwork(estimatedsub.keys(), pos=estimatedsub) self.draw_network(net=net, drawMessages=False) edge_pos = numpy.asarray([(self.net.pos[node], estimatedsub[node][:2]) for node in net]) error_collection = LineCollection(edge_pos, colors='r') self.axes.add_collection(error_collection) rms = get_rms(self.net.pos, estimated, scale=False) self.update_log('rms = %.3f' % rms) self.update_log('localized = %.2f%% (%d/%d)' % (len(estimatedsub) * 1. / len(self.net.pos) * 100, len(estimatedsub), len(self.net.pos))) def on_actionSaveNetwork_triggered(self, *args): default_filetype = 'gz' start = datetime.now().strftime('%Y%m%d') + default_filetype filters = ['Network pickle (*.gz)', 'All files (*)'] selectedFilter = 'Network pickle (gz)' filters = ';;'.join(filters) fname = QFileDialog.getSaveFileName(self, "Choose a filename", start, filters, selectedFilter)[0] if fname: try: write_pickle(self.net, fname) except Exception, e: QMessageBox.critical( self, "Error saving file", str(e), QMessageBox.Ok, QMessageBox.NoButton) else: self.set_title(fname)
class SimulationGui(QMainWindow): def __init__(self, net=None, parent=None, fname=None): QMainWindow.__init__(self) self.ui = Ui_SimulationWindow() self.ui.setupUi(self) if fname: self.set_title(fname) # context menu self.ui.nodeInspector.addAction(self.ui.actionCopyInspectorData) self.ui.nodeInspector.addAction(self.ui.actionShowLocalizedSubclusters) # callbacks self.ui.actionCopyInspectorData.activated\ .connect(self.on_actionCopyInspectorData_triggered) self.ui.actionShowLocalizedSubclusters.activated\ .connect(self.on_actionShowLocalizedSubclusters_triggered) self.dpi = 72 # take size of networDisplayWidget self.fig = Figure((700 / self.dpi, 731 / self.dpi), self.dpi, facecolor='0.9') self.canvas = FigureCanvas(self.fig) self.canvas.setParent(self.ui.networkDisplayWidget) self.nav = NavigationToolbar(self.canvas, self.ui.networkDisplayWidget, coordinates=True) self.nav.setGeometry(QRect(0, 0, 651, 36)) self.nav.setIconSize(QSize(24, 24)) self.axes = self.fig.add_subplot(111) # matplotlib.org/api/figure_api.html#matplotlib.figure.SubplotParams self.fig.subplots_adjust(left=0.03, right=0.99, top=0.92) if net: self.init_sim(net) self.connect(self.ui.showNodes, SIGNAL('stateChanged(int)'), self.refresh_visibility) self.connect(self.ui.showEdges, SIGNAL('stateChanged(int)'), self.refresh_visibility) self.connect(self.ui.showMessages, SIGNAL('stateChanged(int)'), self.refresh_visibility) self.connect(self.ui.showLabels, SIGNAL('stateChanged(int)'), self.refresh_visibility) self.connect(self.ui.redrawNetworkButton, SIGNAL('clicked(bool)'), self.redraw) self.connect(self.ui.treeGroupBox, SIGNAL('toggled(bool)'), self.refresh_visibility) self.connect(self.ui.treeKey, SIGNAL('textEdited(QString)'), self.redraw) self.connect(self.ui.propagationError, SIGNAL('toggled(bool)'), self.refresh_visibility) self.connect(self.ui.locKey, SIGNAL('textEdited(QString)'), self.redraw) # callbacks self.ui.actionOpenNetwork.activated\ .connect(self.on_actionOpenNetwork_triggered) self.ui.actionSaveNetwork.activated\ .connect(self.on_actionSaveNetwork_triggered) self.ui.actionRun.activated.connect(self.on_actionRun_triggered) self.ui.actionStep.activated.connect(self.on_actionStep_triggered) self.ui.actionReset.activated.connect(self.on_actionReset_triggered) self.canvas.mpl_connect('pick_event', self.on_pick) def handleInspectorMenu(self, pos): menu = QMenu() menu.addAction('Add') menu.addAction('Delete') menu.exec_(QCursor.pos()) def init_sim(self, net): self.net = net self.sim = Simulation(net) self.connect(self.sim, SIGNAL("redraw()"), self.redraw) self.connect(self.sim, SIGNAL("updateLog(QString)"), self.update_log) self.redraw() def update_log(self, text): """ Add item to list widget """ print "Add: " + text self.ui.logListWidget.insertItem(0, text) # self.ui.logListWidget.sortItems() def redraw(self): self.refresh_network_inspector() self.draw_network() self.reset_zoom() self.refresh_visibility() def draw_network(self, net=None, clear=True, subclusters=None, drawMessages=True): if not net: net = self.net currentAlgorithm = self.net.get_current_algorithm() if clear: self.axes.clear() self.axes.imshow(net.environment.im, vmin=0, cmap='binary_r', origin='lower') self.draw_tree(str(self.ui.treeKey.text()), net) self.draw_edges(net) self.draw_propagation_errors(str(self.ui.locKey.text()), net) if subclusters: node_colors = self.get_node_colors(net, subclusters=subclusters) else: node_colors = self.get_node_colors(net, algorithm=currentAlgorithm) self.node_collection = self.draw_nodes(net, node_colors) if drawMessages: self.draw_messages(net) self.draw_labels(net) self.drawnNet = net step_text = ' (step %d)' % self.net.algorithmState['step'] \ if isinstance(currentAlgorithm, NodeAlgorithm) else '' self.axes.set_title( (currentAlgorithm.name if currentAlgorithm else '') + step_text) self.refresh_visibility() # To save multiple figs of the simulation uncomment next two lines: #self.fig.savefig('network-alg-%d-step-%d.png' % # (self.net.algorithmState['index'], self.net.algorithmState['step'])) def draw_nodes(self, net=None, node_colors={}, node_radius={}): if not net: net = self.net if type(node_colors) == str: node_colors = {node: node_colors for node in net.nodes()} nodeCircles = [] for n in net.nodes(): c = NodeCircle(tuple(net.pos[n]), node_radius.get(n, 8.0), color=node_colors.get(n, 'r'), ec='k', lw=1.0, ls='solid', picker=3) nodeCircles.append(c) node_collection = PatchCollection(nodeCircles, match_original=True) node_collection.set_picker(3) self.axes.add_collection(node_collection) return node_collection def get_node_colors(self, net, algorithm=None, subclusters=None, drawLegend=True): COLORS = 'rgbcmyw' * 100 node_colors = {} if algorithm: color_map = {} if isinstance(algorithm, NodeAlgorithm): for ind, status in enumerate(algorithm.STATUS.keys()): if status == 'IDLE': color_map.update({status: 'k'}) else: color_map.update({status: COLORS[ind]}) if drawLegend: proxy = [] labels = [] for status, color in color_map.items(): proxy.append( Circle((0, 0), radius=8.0, color=color, ec='k', lw=1.0, ls='solid')) labels.append(status) self.fig.legends = [] self.fig.legend(proxy, labels, loc=8, prop={'size': '10.0'}, ncol=len(proxy), title='Statuses for %s:' % algorithm.name) for n in net.nodes(): if n.status == '' or not n.status in color_map.keys(): node_colors[n] = 'r' else: node_colors[n] = color_map[n.status] elif subclusters: for i, sc in enumerate(subclusters): for n in sc: if n in node_colors: node_colors[n] = 'k' else: node_colors[n] = COLORS[i] return node_colors def draw_edges(self, net=None): if not net: net = self.net self.edge_collection = nx.draw_networkx_edges(net, net.pos, alpha=0.6, edgelist=None, ax=self.axes) def draw_messages(self, net=None): if not net: net = self.net self.messages = [] msgCircles = [] for node in net.nodes(): for msg in node.outbox: # broadcast if msg.destination is None: for neighbor in net.adj[node].keys(): nbr_msg = msg.copy() nbr_msg.destination = neighbor c = MessageCircle(nbr_msg, net, 'out', 3.0, lw=0, picker=3, zorder=3, color='b') self.messages.append(nbr_msg) msgCircles.append(c) else: c = MessageCircle(msg, net, 'out', 3.0, lw=0, picker=3, zorder=3, color='b') self.messages.append(msg) msgCircles.append(c) for msg in node.inbox: c = MessageCircle(msg, net, 'in', 3.0, lw=0, picker=3, zorder=3, color='g') self.messages.append(msg) msgCircles.append(c) if self.messages: self.message_collection = PatchCollection(msgCircles, match_original=True) self.message_collection.set_picker(3) self.axes.add_collection(self.message_collection) def draw_labels(self, net=None): if not net: net = self.net label_pos = {} for n in net.nodes(): label_pos[n] = net.pos[n].copy() + 10 self.label_collection = nx.draw_networkx_labels(net, label_pos, labels=net.labels, ax=self.axes) def refresh_visibility(self): try: self.node_collection.set_visible(self.ui.showNodes.isChecked()) self.edge_collection.set_visible(self.ui.showEdges.isChecked()) for label in self.label_collection.values(): label.set_visible(self.ui.showLabels.isChecked()) self.tree_collection.set_visible(self.ui.treeGroupBox.isChecked()) self.ini_error_collection.set_visible(self.ui.propagationError\ .isChecked()) self.propagation_error_collection.set_visible(self.ui\ .propagationError\ .isChecked()) # sould be last, sometimes there are no messages self.message_collection.set_visible(self.ui.showMessages\ .isChecked()) except AttributeError: print 'Refresh visibility warning' self.canvas.draw() def draw_tree(self, treeKey, net=None): """ Show tree representation of network. Attributes: treeKey (str): key in nodes memory (dictionary) where tree data is stored storage format can be a list off tree neighbors or a dict: {'parent': parent_node, 'children': [child_node1, child_node2 ...]} """ if not net: net = self.net treeNet = net.get_tree_net(treeKey) if treeNet: self.tree_collection = draw_networkx_edges(treeNet, treeNet.pos, treeNet.edges(), width=1.8, alpha=0.6, ax=self.axes) def draw_propagation_errors(self, locKey, net): SCALE_FACTOR = 0.6 if not net: net = self.net if any([not locKey in node.memory for node in net.nodes()]): self.propagation_error_collection = [] self.ini_error_collection = [] return rms = {'iniRms': {}, 'stitchRms': {}} for node in net.nodes(): rms['iniRms'][node] = get_rms(self.net.pos, (node.memory['iniLocs']), True) * \ SCALE_FACTOR rms['stitchRms'][node] = get_rms(self.net.pos, node.memory[locKey], True) * SCALE_FACTOR self.propagation_error_collection = \ self.draw_nodes(net=net, node_colors='g', node_radius=rms['stitchRms']) self.ini_error_collection = self.draw_nodes(net=net, node_colors='b', node_radius=rms['iniRms']) def reset_zoom(self): self.axes.set_xlim((0, self.net.environment.im.shape[1])) self.axes.set_ylim((0, self.net.environment.im.shape[0])) def set_title(self, fname): new = ' - '.join([str(self.windowTitle()).split(' - ')[0], str(fname)]) self.setWindowTitle(new) def refresh_network_inspector(self): niModel = DictionaryTreeModel(dic=self.net.get_dic()) self.ui.networkInspector.setModel(niModel) self.ui.networkInspector.expandToDepth(0) """ Callbacks """ def on_actionRun_triggered(self): self.ui.logListWidget.clear() print 'running ...', self.sim.stepping = True self.sim.run_all() def on_actionStep_triggered(self): print 'next step ...', self.sim.run(self.ui.stepSize.value()) def on_actionReset_triggered(self): print 'reset ...', self.sim.reset() self.redraw() def on_actionCopyInspectorData_triggered(self): string = 'Node inspector data\n-------------------' # raise() for qModelIndex in self.ui.nodeInspector.selectedIndexes(): string += '\n' + qModelIndex.internalPointer().toString(' ') clipboard = app.clipboard() clipboard.setText(string) event = QEvent(QEvent.Clipboard) app.sendEvent(clipboard, event) def on_actionShowLocalizedSubclusters_triggered(self): if len(self.ui.nodeInspector.selectedIndexes()) == 1: qModelIndex = self.ui.nodeInspector.selectedIndexes()[0] treeItem = qModelIndex.internalPointer() assert (isinstance(treeItem.itemDataValue, Positions)) estimated = deepcopy(treeItem.itemDataValue) estimatedsub = estimated.subclusters[0] # rotate, translate and optionally scale # w.r.t. original positions (pos) align_clusters(Positions.create(self.net.pos), estimated, True) net = self.net.subnetwork(estimatedsub.keys(), pos=estimatedsub) self.draw_network(net=net, drawMessages=False) edge_pos = numpy.asarray([ (self.net.pos[node], estimatedsub[node][:2]) for node in net ]) error_collection = LineCollection(edge_pos, colors='r') self.axes.add_collection(error_collection) rms = get_rms(self.net.pos, estimated, scale=False) self.update_log('rms = %.3f' % rms) self.update_log('localized = %.2f%% (%d/%d)' % (len(estimatedsub) * 1. / len(self.net.pos) * 100, len(estimatedsub), len(self.net.pos))) def on_actionSaveNetwork_triggered(self, *args): default_filetype = 'gz' start = datetime.now().strftime('%Y%m%d') + default_filetype filters = ['Network pickle (*.gz)', 'All files (*)'] selectedFilter = 'Network pickle (gz)' filters = ';;'.join(filters) fname = QFileDialog.getSaveFileName(self, "Choose a filename", start, filters, selectedFilter)[0] if fname: try: write_pickle(self.net, fname) except Exception, e: QMessageBox.critical(self, "Error saving file", str(e), QMessageBox.Ok, QMessageBox.NoButton) else: self.set_title(fname)
class CameraDisplay: """Camera Display using matplotlib. Parameters ---------- geometry : `~ctapipe.io.CameraGeometry` Definition of the Camera/Image axis : `matplotlib.axes.Axes` A matplotlib axes object to plot on, or None to create a new one title : str Title to put on camera plot allow_pick : bool (default False) if True, allow user to click and select a pixel autoupdate : bool (default True) redraw automatically (otherwise need to call plt.draw()) antialiased : bool (default True) whether to draw in antialiased mode or not. Notes ----- Speed: CameraDisplay is not intended to be very fast (matplotlib is not a very speed performant graphics library, it is intended for nice output plots). However, most of the slowness of CameraDisplay is in the constructor. Once one is displayed, changing the image that is displayed is relatively fast and efficient. Therefore it is best to initialize an instance, and change the data, rather than generating new CameraDisplays. Pixel Implementation: Pixels are rendered as a `matplotlib.collections.PatchCollection` of Polygons (either 6 or 4 sided). You can access the PatchCollection directly (to e.g. change low-level style parameters) via `CameraDisplay.pixels` Output: Since CameraDisplay uses matplotlib, any display can be saved to any output file supported via plt.savefig(filename). This includes `.pdf` and `.png`. """ def __init__(self, geometry, axes=None, title="Camera", allow_pick=False, autoupdate=True, antialiased=True): self.axes = axes if axes is not None else plt.gca() self.geom = geometry self.pixels = None self.cmap = plt.cm.jet self.autoupdate = autoupdate self._active_pixel = None self._active_pixel_label = None # initialize the plot and generate the pixels as a # RegularPolyCollection patches = [] for xx, yy, aa in zip(u.Quantity(self.geom.pix_x).value, u.Quantity(self.geom.pix_y).value, u.Quantity(np.array(self.geom.pix_area))): if self.geom.pix_type.startswith("hex"): rr = sqrt(aa * 2 / 3 / sqrt(3)) poly = RegularPolygon((xx, yy), 6, radius=rr, orientation=np.radians(0), fill=True) else: rr = sqrt(aa) * sqrt(2) poly = Rectangle((xx, yy), width=rr, height=rr, angle=np.radians(0), fill=True) patches.append(poly) self.pixels = PatchCollection(patches, cmap=self.cmap, linewidth=0) self.axes.add_collection(self.pixels) # Set up some nice plot defaults self.axes.set_aspect('equal', 'datalim') self.axes.set_title(title) self.axes.set_xlabel("X position ({})".format(self.geom.pix_x.unit)) self.axes.set_ylabel("Y position ({})".format(self.geom.pix_y.unit)) self.axes.autoscale_view() # set up a patch to display when a pixel is clicked (and # pixel_picker is enabled): self._active_pixel = copy.copy(patches[0]) self._active_pixel.set_facecolor('r') self._active_pixel.set_alpha(0.5) self._active_pixel.set_linewidth(2.0) self._active_pixel.set_visible(False) self.axes.add_patch(self._active_pixel) self._active_pixel_label = plt.text(self._active_pixel.xy[0], self._active_pixel.xy[1], "0", horizontalalignment='center', verticalalignment='center') self._active_pixel_label.set_visible(False) # enable ability to click on pixel and do something (can be # enabled on-the-fly later as well: if allow_pick: self.enable_pixel_picker() def enable_pixel_picker(self): """ enable ability to click on pixels """ self.pixels.set_picker(True) # enable click self.pixels.set_pickradius(sqrt(u.Quantity(self.geom.pix_area[0]) .value) / np.pi) self.pixels.set_snap(True) # snap cursor to pixel center self.axes.figure.canvas.mpl_connect('pick_event', self._on_pick) def set_cmap(self, cmap): """ Change the color map Parameters ---------- self: type description cmap: `matplotlib.colors.ColorMap` a color map, e.g. from `matplotlib.pyplot.cm.*` """ self.pixels.set_cmap(cmap) def set_image(self, image): """ Change the image displayed on the Camera. Parameters ---------- image: array_like array of values corresponding to the pixels in the CameraGeometry. """ image = np.asanyarray(image) if image.shape != self.geom.pix_x.shape: raise ValueError("Image has a different shape {} than the" "given CameraGeometry {}" .format(image.shape, self.geom.pix_x.shape)) self.pixels.set_array(image) self.update() def update(self): """ signal a redraw if necessary """ if self.autoupdate: plt.draw() def add_colorbar(self): """ add a colobar to the camera plot """ self.axes.figure.colorbar(self.pixels) def add_ellipse(self, centroid, length, width, angle, asymmetry=0.0, **kwargs): """ plot an ellipse on top of the camera Parameters ---------- centroid: (float,float) position of centroid length: float major axis width: float minor axis angle: float rotation angle wrt "up" about the centroid, clockwise, in radians asymmetry: float 3rd-order moment for directionality if known kwargs: any MatPlotLib style arguments to pass to the Ellipse patch """ ellipse = Ellipse(xy=centroid, width=width, height=length, angle=np.degrees(angle), fill=False, **kwargs) self.axes.add_patch(ellipse) self.update() return ellipse def overlay_moments(self, momparams, **kwargs): """helper to overlay ellipse from a `reco.MomentParameters` structure Parameters ---------- momparams: `reco.MomentParameters` structuring containing Hillas-style parameterization kwargs: key=value any style keywords to pass to matplotlib (e.g. color='red' or linewidth=6) """ el = self.add_ellipse(centroid=(momparams.cen_x, momparams.cen_y), length=momparams.length, width=momparams.width, angle=momparams.psi, **kwargs) self.axes.text(momparams.cen_x, momparams.cen_y, ("({:.02f},{:.02f})\n" "[w={:.02f},l={:.02f}]") .format(momparams.cen_x, momparams.cen_y, momparams.width, momparams.length), color=el.get_edgecolor()) def _on_pick(self, event): """ handler for when a pixel is clicked """ pix_id = event.ind.pop() xx, yy = u.Quantity(self.geom.pix_x[pix_id]).value,\ u.Quantity(self.geom.pix_y[pix_id]).value self._active_pixel.xy = (xx, yy) self._active_pixel.set_visible(True) self._active_pixel_label.set_x(xx) self._active_pixel_label.set_y(yy) self._active_pixel_label.set_text("{:003d}".format(pix_id)) self._active_pixel_label.set_visible(True) self.update() self.on_pixel_clicked(pix_id) # call user-function def on_pixel_clicked(self, pix_id): """virtual function to overide in sub-classes to do something special when a pixel is clicked """ print("Clicked pixel_id {}".format(pix_id))
class CameraDisplay: """Camera Display using matplotlib. Parameters ---------- geometry : `~ctapipe.io.CameraGeometry` Definition of the Camera/Image image: array_like array of values corresponding to the pixels in the CameraGeometry. ax : `matplotlib.axes.Axes` A matplotlib axes object to plot on, or None to create a new one title : str Title to put on camera plot allow_pick : bool (default False) if True, allow user to click and select a pixel autoupdate : bool (default True) redraw automatically (otherwise need to call plt.draw()) antialiased : bool (default True) whether to draw in antialiased mode or not. Notes ----- Speed: CameraDisplay is not intended to be very fast (matplotlib is not a very speed performant graphics library, it is intended for nice output plots). However, most of the slowness of CameraDisplay is in the constructor. Once one is displayed, changing the image that is displayed is relatively fast and efficient. Therefore it is best to initialize an instance, and change the data, rather than generating new CameraDisplays. Pixel Implementation: Pixels are rendered as a `matplotlib.collections.PatchCollection` of Polygons (either 6 or 4 sided). You can access the PatchCollection directly (to e.g. change low-level style parameters) via `CameraDisplay.pixels` Output: Since CameraDisplay uses matplotlib, any display can be saved to any output file supported via plt.savefig(filename). This includes `.pdf` and `.png`. """ def __init__(self, geometry, image=None, ax=None, title="Camera", allow_pick=False, autoupdate=True, antialiased=True): self.axes = ax if ax is not None else plt.gca() self.geom = geometry self.pixels = None self.autoupdate = autoupdate self._active_pixel = None self._active_pixel_label = None # initialize the plot and generate the pixels as a # RegularPolyCollection patches = [] for xx, yy, aa in zip(u.Quantity(self.geom.pix_x).value, u.Quantity(self.geom.pix_y).value, u.Quantity(np.array(self.geom.pix_area))): if self.geom.pix_type.startswith("hex"): rr = sqrt(aa * 2 / 3 / sqrt(3)) poly = RegularPolygon((xx, yy), 6, radius=rr, orientation=np.radians(0), fill=True) else: rr = sqrt(aa) poly = Rectangle((xx, yy), width=rr, height=rr, angle=np.radians(0), fill=True) patches.append(poly) self.pixels = PatchCollection(patches, cmap='hot', linewidth=0) self.axes.add_collection(self.pixels) # Set up some nice plot defaults self.axes.set_aspect('equal', 'datalim') self.axes.set_title(title) self.axes.set_xlabel("X position ({})".format(self.geom.pix_x.unit)) self.axes.set_ylabel("Y position ({})".format(self.geom.pix_y.unit)) self.axes.autoscale_view() # set up a patch to display when a pixel is clicked (and # pixel_picker is enabled): self._active_pixel = copy.copy(patches[0]) self._active_pixel.set_facecolor('r') self._active_pixel.set_alpha(0.5) self._active_pixel.set_linewidth(2.0) self._active_pixel.set_visible(False) self.axes.add_patch(self._active_pixel) self._active_pixel_label = plt.text(self._active_pixel.xy[0], self._active_pixel.xy[1], "0", horizontalalignment='center', verticalalignment='center') self._active_pixel_label.set_visible(False) # enable ability to click on pixel and do something (can be # enabled on-the-fly later as well: if allow_pick: self.enable_pixel_picker() if image is not None: self.image = image else: self.image = np.zeros_like(self.geom.pix_id, dtype=np.float) def enable_pixel_picker(self): """ enable ability to click on pixels """ self.pixels.set_picker(True) # enable click self.pixels.set_pickradius(sqrt(u.Quantity(self.geom.pix_area[0]) .value) / np.pi) self.pixels.set_snap(True) # snap cursor to pixel center self.axes.figure.canvas.mpl_connect('pick_event', self._on_pick) def set_limits_minmax(self, zmin, zmax): """ set the color scale limits from min to max """ self.pixels.set_clim(zmin, zmax) self.update() def set_limits_percent(self, percent=95): """ auto-scale the color range to percent of maximum """ zmin = self.pixels.get_array().min() zmax = self.pixels.get_array().max() dz = zmax - zmin frac = percent / 100.0 self.set_limits_minmax(zmin, zmax - (1.0 - frac) * dz) @property def cmap(self): return self.pixels.get_cmap() @cmap.setter def cmap(self, cmap): """ Change the color map Parameters ---------- self: type description cmap: `matplotlib.colors.ColorMap` a color map, e.g. from `matplotlib.pyplot.cm.*` """ self.pixels.set_cmap(cmap) self.update() @property def image(self): return self.pixels.get_array() @image.setter def image(self, image): """ Change the image displayed on the Camera. Parameters ---------- image: array_like array of values corresponding to the pixels in the CameraGeometry. """ image = np.asanyarray(image) if image.shape != self.geom.pix_x.shape: raise ValueError( "Image has a different shape {} than the" "given CameraGeometry {}" .format(image.shape, self.geom.pix_x.shape) ) self.pixels.set_array(image) self.axes._sci(self.pixels) self.update() def set_image(self, image): logger.warn("set_image(x) is deprecated:" " use CameraDisplay.image = x instead") self.image = image def update(self): """ signal a redraw if necessary """ if self.autoupdate: plt.draw() def add_colorbar(self): """ add a colobar to the camera plot """ self.axes.figure.colorbar(self.pixels) def add_ellipse(self, centroid, length, width, angle, asymmetry=0.0, **kwargs): """ plot an ellipse on top of the camera Parameters ---------- centroid: (float,float) position of centroid length: float major axis width: float minor axis angle: float rotation angle wrt "up" about the centroid, clockwise, in radians asymmetry: float 3rd-order moment for directionality if known kwargs: any MatPlotLib style arguments to pass to the Ellipse patch """ ellipse = Ellipse(xy=centroid, width=width, height=length, angle=np.degrees(angle), fill=False, **kwargs) self.axes.add_patch(ellipse) self.update() return ellipse def overlay_moments(self, momparams, **kwargs): """helper to overlay ellipse from a `reco.MomentParameters` structure Parameters ---------- momparams: `reco.MomentParameters` structuring containing Hillas-style parameterization kwargs: key=value any style keywords to pass to matplotlib (e.g. color='red' or linewidth=6) """ el = self.add_ellipse(centroid=(momparams.cen_x, momparams.cen_y), length=momparams.length, width=momparams.width, angle=momparams.psi, **kwargs) self.axes.text(momparams.cen_x, momparams.cen_y, ("({:.02f},{:.02f})\n" "[w={:.02f},l={:.02f}]") .format(momparams.cen_x, momparams.cen_y, momparams.width, momparams.length), color=el.get_edgecolor()) def _on_pick(self, event): """ handler for when a pixel is clicked """ pix_id = event.ind.pop() xx, yy = u.Quantity(self.geom.pix_x[pix_id]).value,\ u.Quantity(self.geom.pix_y[pix_id]).value self._active_pixel.xy = (xx, yy) self._active_pixel.set_visible(True) self._active_pixel_label.set_x(xx) self._active_pixel_label.set_y(yy) self._active_pixel_label.set_text("{:003d}".format(pix_id)) self._active_pixel_label.set_visible(True) self.update() self.on_pixel_clicked(pix_id) # call user-function def on_pixel_clicked(self, pix_id): """virtual function to overide in sub-classes to do something special when a pixel is clicked """ print("Clicked pixel_id {}".format(pix_id))