Source code for laygo2.interface.mpl

#!/usr/bin/python
########################################################################################################################
#
# Copyright (C) 2023, Nifty Chips Laboratory at Hanyang University - All Rights Reserved
# 
# Unauthorized copying of this software package, via any medium is strictly prohibited
# Proprietary and confidential
# Written by Jaeduk Han, 07-23-2023
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
########################################################################################################################

"""
This module implements the interface with matplotlib.

"""
from typing import (
    TYPE_CHECKING,
    Union,
    List
)

import logging
from math import log10
from decimal import *

import numpy as np
import laygo2.util.transform as tf

import matplotlib
import matplotlib.pyplot as plt

import laygo2.object

#from laygo2._typing import Path

# Type checking
from typing import TYPE_CHECKING, overload, Generic, Dict
from typing import List, Tuple, Iterable, Type, Union, Any, Optional
if TYPE_CHECKING:
    import laygo2

__author__ = ""
__maintainer__ = ""
__status__ = "Prototype"

# support for hover information.
# https://stackoverflow.com/a/47166787/7128154
# https://matplotlib.org/3.3.3/api/collections_api.html#matplotlib.collections.PathCollection
# https://matplotlib.org/3.3.3/api/path_api.html#matplotlib.path.Path
# https://stackoverflow.com/questions/15876011/add-information-to-matplotlib-navigation-toolbar-status-bar
# https://stackoverflow.com/questions/36730261/matplotlib-path-contains-point
# https://stackoverflow.com/a/36335048/7128154
[docs] class StatusbarHoverManager: """ Manage hover information for mpl.axes.Axes object based on appearing artists. Attributes ---------- ax : mpl.axes.Axes subplot to show status information artists : list of mpl.artist.Artist elements on the subplot, which react to mouse over labels : list (list of strings) or strings each element on the top level corresponds to an artist. if the artist has items (i.e. second return value of contains() has key 'ind'), the element has to be of type list. otherwise the element if of type string cid : to reconnect motion_notify_event """
[docs] def __init__(self, fig, ax): assert isinstance(ax, matplotlib.axes.Axes) def hover(event): if event.inaxes != ax: return info = 'x={:.0f}, y={:.0f}'.format(event.xdata, event.ydata) self.text.set_text(info) # replace with self.text.set_text(info) if you want to show the info in the figure information bar. #ax.format_coord = lambda x, y: info cid = ax.figure.canvas.mpl_connect("motion_notify_event", hover) self.ax = ax self.cid = cid self.artists = [] self.labels = [] text = fig.text(0.0, 0.02, "", va="bottom", ha="left", fontsize=6) self.fig = fig self.text = text
[docs] def add_artist_labels(self, artist, label): if isinstance(artist, list): assert len(artist) == 1 artist = artist[0] self.artists += [artist] self.labels += [label] def hover(event): if event.inaxes != self.ax: return info = 'x={:.0f}, y={:.0f}'.format(event.xdata, event.ydata) for aa, artist in enumerate(self.artists): cont, dct = artist.contains(event) if not cont: continue inds = dct.get('ind') if inds is not None: # artist contains items for ii in inds: lbl = self.labels[aa][ii] info += '\n {:}'.format(lbl) else: lbl = self.labels[aa] info += '\n {:}'.format(lbl) self.text.set_text(info) self.fig.canvas.draw_idle() # replace with self.text.set_text(info) if you want to show the info in the figure information bar. #self.ax.format_coord = lambda x, y: info self.ax.figure.canvas.mpl_disconnect(self.cid) self.cid = self.ax.figure.canvas.mpl_connect("motion_notify_event", hover)
def _translate_obj( objname: str, obj: "laygo2.object.physical.PhysicalObject", colormap: dict, scale=1, master: "laygo2.object.physical.PhysicalObject" = None, offset=np.array([0, 0]) ): """ Convert a layout object to corresponding matplotlib patch object. Parameters ---------- objname: str The name of the object. obj: laygo2.object.PhysicalObject The object to be converted. colormap: dict A dictionary that contains layer-color mapping information. scale: float (optional) The scaling factor between laygo2's integer coordinates and plot coordinates. master: laygo2.object.PhysicalObject (optional) The master object of the translated object. offset: np.array (optional) The offset of the translated object's position. Returns ------- pypobjs: list A list of matplotlib patch objects. Each element is a list of the form [patch, layer, annotation, (on-figure annotation)]. """ if master is None: mxy = np.array([0, 0]) mtf = "R0" else: # if the translated object has a master (e.g. VirtualInstance) mxy = master.xy mtf = master.transform if obj.__class__ == laygo2.object.physical.Rect: # Compute the rectangle size _xy = np.sort(obj.xy, axis=0) # make sure obj.xy is sorted _xy = mxy + np.dot( _xy + np.array([[-obj.hextension, -obj.vextension], [obj.hextension, obj.vextension]]), tf.Mt(mtf).T, ) size = [_xy[1, 0] - _xy[0, 0], _xy[1, 1] - _xy[0, 1]] # Create a rectangle patch if obj.layer[0] in colormap: rect = matplotlib.patches.Rectangle( (_xy[0, 0], _xy[0, 1]), size[0], size[1], facecolor=colormap[obj.layer[0]][1], edgecolor=colormap[obj.layer[0]][0], alpha=colormap[obj.layer[0]][2], lw=2, ) return [[rect, obj.layer[0], "[rect] " + objname + "/" + obj.layer[0] + "/" +str(obj.netname)]] return [] elif obj.__class__ == laygo2.object.physical.Path: # TODO: implement path export function. pass elif obj.__class__ == laygo2.object.physical.Pin: if obj.elements is None: _objelem = [obj] else: _objelem = obj.elements for idx, _obj in np.ndenumerate(_objelem): # Compute the pin rectangle size _xy = mxy + np.dot(_obj.xy, tf.Mt(mtf).T) size = [_xy[1, 0] - _xy[0, 0], _xy[1, 1] - _xy[0, 1]] if obj.layer[0] in colormap: # Create a pin rectangle patch rect = matplotlib.patches.Rectangle( (_xy[0, 0], _xy[0, 1]), size[0], size[1], facecolor=colormap[obj.layer[0]][1], edgecolor=colormap[obj.layer[0]][0], alpha=colormap[obj.layer[0]][2], lw=2, ) pypobj = [rect, obj.layer[0], "[pin] " + objname + "/" + obj.layer[0] + "/" + str(obj.netname)] if master is None: # add on-figure annotatation if the pin is at the top level pypobj.append(obj.layer[0]+"/"+str(obj.netname)) return [pypobj] return [] elif obj.__class__ == laygo2.object.physical.Text: return [["text", obj.layer[0], obj.text, obj.xy]] # TODO: implement text export function. pass elif obj.__class__ == laygo2.object.physical.Instance: # Check if the instance belongs to a virtual instance or array. if master is not None: _master = master elif obj.master is not None: _master = obj.master else: _master = None if obj.shape is None: # single instance _xy = mxy + np.dot(obj.xy, tf.Mt(mtf).T) _xy0 = _xy _xy1 = np.dot(obj.xy1-obj.xy0, tf.Mt(mtf).T) #* np.array([num_rows, num_cols]) rect = matplotlib.patches.Rectangle( (_xy0[0], _xy0[1]), _xy1[0], _xy1[1], facecolor=colormap["__instance__"][1], edgecolor=colormap["__instance__"][0], alpha=colormap["__instance__"][2], lw=2, ) pypobj = [rect, "__instance__", "[inst] " + objname + "/" + obj.cellname] if master is None: # add on-figure annotatation if the instance is at the top level if not obj.cellname.startswith("via"): pypobj.append(objname+"/"+obj.cellname) pypobjs = [pypobj] else: # array instance pypobjs = [] for i, e in np.ndenumerate(obj.elements): pypobjs += _translate_obj(e.name+"_"+str(i), e, colormap, scale=scale, master=_master) # Instance pins for pn, p in obj.pins.items(): _pypobj = _translate_obj(pn, p, colormap, scale=scale, master=_master, offset=offset) _pypobj[0][1] = "__instance_pin__" _pypobj[0][0].set(edgecolor=colormap["__instance_pin__"][0]) _pypobj[0][0].set(facecolor=colormap["__instance_pin__"][1]) _pypobj[0][2] = "[inst] " + objname + " " + _pypobj[0][2] pypobjs += _pypobj return pypobjs elif obj.__class__ == laygo2.object.physical.VirtualInstance: # construct the list that constains patches of elements. pypobjs = [] if obj.shape is None: _xy = mxy + np.dot(obj.xy, tf.Mt(mtf).T) _xy0 = _xy _xy1 = np.dot(obj.xy1-obj.xy0, tf.Mt(mtf).T) rect = matplotlib.patches.Rectangle( (_xy0[0], _xy0[1]), _xy1[0], _xy1[1], facecolor=colormap["__instance__"][1], edgecolor=colormap["__instance__"][0], alpha=colormap["__instance__"][2], lw=2, ) pypobjs = [[rect, "__instance__", "[vinst] " + objname + "/" + obj.cellname]] pypobjs[0].append(objname+"/"+obj.cellname) for elem_name, elem in obj.native_elements.items(): if not elem.__class__ == laygo2.object.physical.Pin: if obj.name == None: objname = "NoName" else: objname = obj.name _pypobj = _translate_obj(objname, elem, colormap, scale=scale, master=obj) for _p in _pypobj: _p[2] = "[vinst] " + objname + " " + _p[2] pypobjs += _pypobj else: # arrayed VirtualInstance for i, j in np.ndindex(tuple(obj.shape.tolist())): # iterate over obj.shape for elem_name, elem in obj.native_elements.items(): if not elem.__class__ == laygo2.object.physical.Pin: _pypobj = _translate_obj( obj.name + "_" + elem_name + str(i) + "_" + str(j), elem, colormap, scale=scale, master=obj[i, j], ) _pypobj[0][2] = "[vinst] " + objname + " " + _pypobj[0][2] pypobjs += _pypobj # Instance pins for pn, p in obj.pins.items(): # master coordinate is already included in the pin object of the virtual instance. _pypobj = _translate_obj(pn, p, colormap, scale=scale, master=None, offset=offset) _pypobj[0][1] = "__instance_pin__" _pypobj[0][0].set(edgecolor=colormap["__instance_pin__"][0]) _pypobj[0][0].set(facecolor=colormap["__instance_pin__"][1]) _pypobj[0][2] = "[vinst] " + objname + " " + _pypobj[0][2] pypobjs += _pypobj return pypobjs else: return [] # return [obj.translate_to_matplotlib()] return []
[docs] def export( db: Union["laygo2.object.database.Library", "laygo2.object.database.Design"], cellname: str = None, scale = 1, colormap: dict = None, order = None, xlim: list = None, ylim: list = None, show: bool = False, filename: str = None, annotate_grid: List["laygo2.object.physical.Grid"] = None, ): """ Export a laygo2.object.database.Library or Design object to a matplotlib plot. Parameters ---------- db: laygo2.database.Library or laygo2.database design The library database or design to exported. cellname: str or List[str] (optional) The name(s) of cell(s) to be exported. scale: float (optional) The scaling factor between laygo2's integer coordinates and plot coordinates. colormap: dict A dictionary that contains layer-color mapping information. order: list A list that contains the order of layers to be displayed (former is plotted first). xlim: list (optional) A list that specifies the range of plot in x-axis. ylim: list (optional) A list that specifies the range of plot in y-axis. filename: str (optional) If specified, export a output file for the plot. annotate_grid: list (optional) A list of grid objects to be annotated. Returns ------- matplotlib.pyplot.figure or list: The generated figure object(s). """ # colormap if colormap is None: colormap = dict() # a list to align layered objects in order if order is None: order = [] # cell name handling. if isinstance(db, laygo2.object.database.Design): cellname = [db.cellname] db = {db.cellname:db} if isinstance(db, laygo2.object.database.Library): cellname = db.keys() if cellname is None else cellname # export all cells if cellname is not given. cellname = [cellname] if isinstance(cellname, str) else cellname # convert to a list for iteration. fig = [] _fig = plt.figure() for cn in cellname: if not show: _fig.set_figwidth(max(6, int(6*db[cn].bbox[1, 0]/1200))) _fig.set_figheight(max(6, int(6*db[cn].bbox[1, 1]/1200))) pypobjs = [] ax = _fig.add_subplot(111) shm = StatusbarHoverManager(_fig, ax) for objname, obj in db[cn].items(): pypobjs += _translate_obj(objname, obj, colormap, scale=scale) for _alignobj in order: for _pypobj in pypobjs: if _pypobj[1] == _alignobj: # layer is matched. if isinstance(_pypobj[0], str): if _pypobj[0] == 'text': # Text color = "black" ax.annotate( _pypobj[2], (_pypobj[3][0], _pypobj[3][1]), color=color, weight="bold", fontsize=6, ha="center", va="center" ) elif _pypobj[0].__class__ == matplotlib.patches.Rectangle: # Rect ax.add_patch(_pypobj[0]) if len(_pypobj) >= 3: # annotation. shm.add_artist_labels(_pypobj[0], _pypobj[2]) #ax.add_artist(_pypobj[0]) if len(_pypobj) == 4: # text annotation. rx, ry = _pypobj[0].get_xy() cx = rx + _pypobj[0].get_width() / 2.0 cy = ry + _pypobj[0].get_height() / 2.0 if _pypobj[1] == "__instance__": color = _pypobj[0].get_edgecolor() elif _pypobj[1] == "__instance_pin__": color = _pypobj[0].get_edgecolor() else: color = "black" ax.annotate( _pypobj[3], (cx, cy), color=color, weight="bold", fontsize=6, ha="center", va="center" ) if annotate_grid is not None: for grid in annotate_grid: laygo2.interface.mpl.export_grid( obj=grid, colormap=colormap, order=order, xlim=db[cn].bbox[:, 0], ylim=db[cn].bbox[:, 1], filename=None, fig=_fig, ax=ax, for_annotation=True, ) # scale ax.set_aspect('equal', adjustable='box') #_fig.tight_layout() plt.autoscale() if not (xlim == None): plt.xlim(xlim) if not (ylim == None): plt.ylim(ylim) fig.append(_fig) if len(fig) == 1: fig = fig[-1] #fig = fig[0] if filename is not None: plt.savefig(filename) if show: plt.show() else: plt.clf() plt.close() return fig
[docs] def export_instance( obj: "laygo2.object.physical.Instance", scale = 1, colormap: dict = None, order: list = None, xlim: list = None, ylim: list = None, filename: str = None, ): """ Export a laygo2.object.physical.Instance object to a matplotlib plot. Parameters ---------- obj: laygo2.object.physical.Instance The instance object to exported. scale: float (optional) The scaling factor between laygo2's integer coordinates and plot coordinates. colormap: dict A dictionary that contains layer-color mapping information. order: list A list that contains the order of layers to be displayed (former is plotted first). xlim: list (optional) A list that specifies the range of plot in x-axis. ylim: list (optional) A list that specifies the range of plot in y-axis. filename: str (optional) If specified, export a output file for the plot. Returns ------- matplotlib.pyplot.figure or list: The generated figure object(s). """ # colormap if colormap is None: colormap = dict() # a list to align layered objects in order if order is None: order = [] ''' # xlim and ylim if xlim is None: xlim = [obj.bbox[0][0] - obj.width, obj.bbox[1][0] + obj.width] if ylim is None: ylim = [obj.bbox[0][1] - obj.height, obj.bbox[1][1] + obj.height] ''' fig = plt.figure() pypobjs = [] ax = fig.add_subplot(111) pypobjs += _translate_obj(obj.name, obj, colormap, scale=scale) for _alignobj in order: for _pypobj in pypobjs: if _pypobj[1] == _alignobj: # layer is matched. if isinstance(_pypobj[0], str): if _pypobj[0] == 'text': # Text # [["text", obj.layer[0], obj.text, obj.xy]] color = "black" ax.annotate( _pypobj[2], (_pypobj[3][0], _pypobj[3][1]), color=color, weight="bold", fontsize=6, ha="center", va="center" ) elif _pypobj[0].__class__ == matplotlib.patches.Rectangle: # Rect ax.add_patch(_pypobj[0]) if len(_pypobj) == 3: # annotation. ax.add_artist(_pypobj[0]) rx, ry = _pypobj[0].get_xy() cx = rx + _pypobj[0].get_width() / 2.0 cy = ry + _pypobj[0].get_height() / 2.0 if _pypobj[1] == "__instance_pin__": color = _pypobj[0].get_edgecolor() else: color = "black" ax.annotate( _pypobj[2], (cx, cy), color=color, weight="bold", fontsize=6, ha="center", va="center" ) # scale plt.autoscale() if not (xlim is None): plt.xlim(xlim) if not (ylim is None): plt.ylim(ylim) if filename is not None: plt.savefig(filename) plt.show() return fig
[docs] def export_grid( obj: "laygo2.object.physical.Grid", colormap: dict = None, order: list = None, xlim: list = None, ylim: list = None, filename: str = None, fig: matplotlib.pyplot.figure = None, ax: matplotlib.pyplot.axis = None, for_annotation: bool = False, ): """ Export a laygo2.object.grid.Grid object to a matplotlib plot. Parameters ---------- obj: laygo2.object.grid.Grid The grid object to exported. scale: float (optional) The scaling factor between laygo2's integer coordinates and plot coordinates. colormap: dict A dictionary that contains layer-color mapping information. order: list A list that contains the order of layers to be displayed (former is plotted first). xlim: list (optional) A list that specifies the range of plot in x-axis. ylim: list (optional) A list that specifies the range of plot in y-axis. filename: str (optional) If specified, export a output file for the plot. fig: matplotlib.pyplot.figure (optional) The figure object to be plotted on. ax: matplotlib.pyplot.axis (optional) The axis object to be plotted on. for_annotation: bool (optional) If True, only draw the dashed grid lines. Returns ------- matplotlib.pyplot.figure or list: The generated figure object(s). """ # colormap if colormap is None: colormap = dict() if for_annotation: lw = 0.5 linestyle = "--" wscl = 0 else: lw = 2.0 linestyle = "-" wscl = 1 # a list to align layered objects in order if order is None: order = [] if fig is None: fig = plt.figure() ax = fig.add_subplot(111) pypobjs = [] # scope _xy = (obj.vgrid.range[0], obj.hgrid.range[0]) _width = obj.vgrid.range[1] - obj.vgrid.range[0] _height = obj.hgrid.range[1] - obj.hgrid.range[0] rect = matplotlib.patches.Rectangle(_xy, _width, _height, facecolor="none", edgecolor="black", linestyle=linestyle, lw=lw) ax.add_patch(rect) if not for_annotation: rx, ry = rect.get_xy() cx = rx + rect.get_width() / 2.0 cy = ry + rect.get_height() / 2.0 ax.annotate( obj.name, (cx, cy), color="black", weight="bold", fontsize=6, ha="center", va="center" ) if obj.__class__ == laygo2.object.grid.RoutingGrid: # Routing grid if for_annotation: rng = range(obj.vgrid<=xlim[0],obj.vgrid>=xlim[1]) height0 = ylim[1] else: rng = range(len(obj.vgrid.elements)) height0 = obj.hgrid.range[1] for i in rng: # vertical routing grid ve = obj.vgrid[i][0] _xy = (ve - obj.vwidth[i]/2*wscl, obj.hgrid.range[0]-obj.vextension[i]) _width = obj.vwidth[i]*wscl _height = height0 - obj.hgrid.range[0] + 2 * obj.vextension[i] facecolor=colormap[obj.vlayer[i][0]][1] edgecolor=colormap[obj.vlayer[i][0]][0] alpha=colormap[obj.vlayer[i][0]][2] rect = matplotlib.patches.Rectangle(_xy, _width, _height, facecolor=facecolor, edgecolor=edgecolor, alpha=alpha, linestyle=linestyle, lw=lw) ax.add_patch(rect) _xy = (ve, obj.hgrid.range[0]-obj.vextension[i]) ax.annotate(_xy[0], _xy, color="black", weight="bold", fontsize=6, ha="center", va="center") if for_annotation: rng = range(obj.hgrid<=ylim[0],obj.hgrid>=ylim[1]) width0 = xlim[1] else: rng = range(len(obj.hgrid.elements)) width0 = obj.vgrid.range[1] for i in rng: # horizontal routing grid he = obj.hgrid[i][0] _xy = (obj.vgrid.range[0] - obj.hextension[i], he - obj.hwidth[i]/2*wscl) _width = width0 - obj.vgrid.range[0] + 2*obj.hextension[i] _height = obj.hwidth[i]*wscl facecolor=colormap[obj.hlayer[i][0]][1] edgecolor=colormap[obj.hlayer[i][0]][0] alpha=colormap[obj.hlayer[i][0]][2] rect = matplotlib.patches.Rectangle(_xy, _width, _height, facecolor=facecolor, edgecolor=edgecolor, alpha=alpha, linestyle=linestyle, lw=lw) ax.add_patch(rect) _xy = (obj.vgrid.range[0] - obj.hextension[i], he) ax.annotate(_xy[1], _xy, color="black", weight="bold", fontsize=6, ha="center", va="center") # viamap if for_annotation is False: for i in range(len(obj.vgrid.elements)): # vertical routing grid for j in range(len(obj.hgrid.elements)): # horizontal routing grid v = obj.viamap[i, j] x = obj.vgrid.elements[i] y = obj.hgrid.elements[j] circ = matplotlib.patches.Circle((x, y), radius=2, facecolor="black", edgecolor="black") #, **kwargs) ax.add_patch(circ) ax.annotate(v.name, (x+2, y), color="black", fontsize=4, ha="left", va="bottom") for _alignobj in order: for _pypobj in pypobjs: if _pypobj[1] == _alignobj: # layer is matched. if isinstance(_pypobj[0], str): if _pypobj[0] == 'text': # Text # [["text", obj.layer[0], obj.text, obj.xy]] color = "black" ax.annotate( _pypobj[2], (_pypobj[3][0], _pypobj[3][1]), color=color, weight="bold", fontsize=6, ha="center", va="center" ) elif _pypobj[0].__class__ == matplotlib.patches.Rectangle: # Rect ax.add_patch(_pypobj[0]) if len(_pypobj) == 3: # annotation. ax.add_artist(_pypobj[0]) rx, ry = _pypobj[0].get_xy() cx = rx + _pypobj[0].get_width() / 2.0 cy = ry + _pypobj[0].get_height() / 2.0 if _pypobj[1] == "__instance_pin__": color = _pypobj[0].get_edgecolor() else: color = "black" ax.annotate( _pypobj[2], (cx, cy), color=color, weight="bold", fontsize=6, ha="center", va="center" ) # scale plt.autoscale() if not for_annotation: if not (xlim == None): plt.xlim(xlim) if not (ylim == None): plt.ylim(ylim) if filename is not None: plt.savefig(filename) if not for_annotation: plt.show() return fig