#!/usr/bin/python
########################################################################################################################
#
# Copyright (c) 2020, Nifty Chips Laboratory, Hanyang University
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
# following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
#   disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
#    following disclaimer in the documentation and/or other materials provided with the distribution.
#
# 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.
#
########################################################################################################################

"""
**laygo2.object.physical** module provides classes for physical objects 
used in IC layout design. The supported physical objects include:

:obj:`PhysicalObject` - Base class for all physical layout objects.

:obj:`IterablePhysicalObject` - Base class for physical objects that can be iterated (eg. arrayed instances).

:obj:`PhysicalObjectGroup` - Defines a group of physical objects (currently not implemented).

:obj:`Rect` - Represents a rectangular shape.

:obj:`Path` - Defines a path.

:obj:`Pin` - Defines a pin.

:obj:`Text` - Defines a text label.

:obj:`Instance` - Represents an instance of a design element.

:obj:`VirtualInstance` - Represents a virtual instance composed of multiple physical objects, acting as a single instance.

Examples of the physical objects supported by this module are shown in the followng figure.

.. image:: ../assets/img/user_guide_physical.png



Check the following links for further details.
"""

__author__ = "Jaeduk Han"
__maintainer__ = "Jaeduk Han"
__status__ = "Prototype"

import numpy as np

# from copy import deepcopy
import laygo2.util.transform as tf


class PhysicalObject:
    """
    Base class for physical layout objects with physical coordinate info.

    """

    def _get_xy(self):
        """numpy.ndarray(dtype=numpy.int): Retrive x,y coordinate values of
        the object."""
        return self._xy

    def _set_xy(self, value):
        """numpy.ndarray(dtype=numpy.int): Update x,y coordinates of
        the object."""
        self._xy = np.asarray(value, dtype=int)
        self._update_pointers()

    name = None
    """str: Object name.

    Example
    -------
    >>> import laygo2
    >>> obj = laygo2.object.physical.PhysicalObject(xy = [[0, 0], [200, 200]], 
            name="test", params={'maxI': 0.005})
    >>> obj.name 
    “test”

    .. image:: ../assets/img/object_physical_PhysicalObject_name.png
      :height: 250

    """

    _xy = np.zeros(2, dtype=int)
    """numpy.ndarray(dtype=numpy.int): The x and y coordinate values 
    stored within."""

    xy = property(_get_xy, _set_xy)
    """numpy.ndarray: Physical coordinate values of the object 
    in the form of [bottom_left, top_right].

    Example
    -------
    >>> import laygo2
    >>> obj = laygo2.object.physical.PhysicalObject(xy = [[0, 0], [200, 200]], 
            name="test", params={'maxI': 0.005})
    >>> obj.xy 
    array([[  0,   0], 
           [200, 200]])
    
    .. image:: ../assets/img/object_physical_PhysicalObject_xy.png
      :height: 250

    """

    master = None
    """numpy.ndarray: Master ojbect for current object (for arrays and pins).

    Example
    -------
    >>> import laygo2
    >>> obj1 = laygo2.object.physical.PhysicalObject(xy = [[0, 0], [200, 200]], 
            name="test1", params=None) 
    >>> obj2 = laygo2.object.physical.Pin(xy = [[0, 0], [100, 100]], 
            layer = ["M1", "drawing"], master=obj1) 
    >>> obj2.master
    <laygo2.object.physical.PhysicalObject object at 0x00000204AAF3C7C0>

    """

    params = None
    """dict: Dictionary storing the parameters associated with the object

    Example
    -------
    >>> import laygo2
    >>> obj = laygo2.object.physical.PhysicalObject(xy = [[0, 0], [200, 200]], 
            name="test", params={'maxI': 0.005})
    >>> obj.params 
    {‘maxI’: 0.005 }

    """

    pointers = None
    """dict: The dictionary containing the key-value pairs of the major physical 
    coordinates of the object, such as 'left', 'right', 'top', 'bottom', 
    'bottom_left', 'center', etc.

    Example
    -------
    >>> import laygo2
    >>> obj = laygo2.object.physical.PhysicalObject(xy = [[0, 0], [200, 200]])
    >>> obj.pointers 
    {'left': array([0, 100]), 'right': array([200, 100]),
     'bottom': array([100, 0]), 'top': array([100, 200]),
     'bottom_left': array([0, 0]), 'bottom_right': array([200, 0]),
     'top_left': array([0, 200]), 'top_right': array([200, 200]),
     ‘center’: array( [100, 100] ) 
    }

    .. image:: ../assets/img/object_physical_PhysicalObject_pointers.png
      :height: 250

    """

    # Frequently used pointers
    left = None
    """numpy.ndarray: The left-center coordinate of the object.

    Example
    -------
    >>> import laygo2
    >>> obj = laygo2.object.physical.PhysicalObject(xy = [[0, 0], [200, 200]])
    >>> obj.left
    array([  0, 100])
    
    .. image:: ../assets/img/object_physical_PhysicalObject_left.png
      :height: 250
    """

    right = None
    """numpy.ndarray: The right-center coordinate of the object.

    Example
    -------
    >>> import laygo2
    >>> obj = laygo2.object.physical.PhysicalObject(xy = [[0, 0], [200, 200]])
    >>> obj.right
    array([200, 100])
    
    .. image:: ../assets/img/object_physical_PhysicalObject_right.png
      :height: 250
    """

    top = None
    """numpy.ndarray: The top-center coordinate of the object.

    Example
    -------
    >>> import laygo2
    >>> obj = laygo2.object.physical.PhysicalObject(xy = [[0, 0], [200, 200]])
    >>> obj.top
    array([100, 200])
    
    .. image:: ../assets/img/object_physical_PhysicalObject_top.png
      :height: 250
    """

    bottom = None
    """numpy.ndarray: The bottom-center coordinate of the object.

    Example
    -------
    >>> import laygo2
    >>> obj = laygo2.object.physical.PhysicalObject(xy = [[0, 0], [200, 200]])
    >>> obj.top
    array([100,   0])
    
    .. image:: ../assets/img/object_physical_PhysicalObject_bottom.png
      :height: 250
    """

    center = None
    """numpy.ndarray: The center-center coordinate of the object.

    Example
    -------
    >>> import laygo2
    >>> obj = laygo2.object.physical.PhysicalObject(xy = [[0, 0], [200, 200]])
    >>> obj.center
    array([100, 100])
    
    .. image:: ../assets/img/object_physical_PhysicalObject_center.png
      :height: 250
    """

    bottom_left = None
    """numpy.ndarray: The bottom-left coordinate of the object.

    Example
    -------
    >>> import laygo2
    >>> obj = laygo2.object.physical.PhysicalObject(xy = [[0, 0], [200, 200]])
    >>> obj.bottom_left
    array([  0,   0])
    
    .. image:: ../assets/img/object_physical_PhysicalObject_bottom_left.png
      :height: 250
    """

    bottom_right = None
    """numpy.ndarray: The bottom-right coordinate of the object.

    Example
    -------
    >>> import laygo2
    >>> obj = laygo2.object.physical.PhysicalObject(xy = [[0, 0], [200, 200]])
    >>> obj.bottom_right
    array([200,   0])
    
    .. image:: ../assets/img/object_physical_PhysicalObject_bottom_right.png 
      :height: 250
    """

    top_left = None
    """numpy.ndarray: The top-left coordinate of the object.

    Example
    -------
    >>> import laygo2
    >>> obj = laygo2.object.physical.PhysicalObject(xy = [[0, 0], [200, 200]])
    >>> obj.top_left
    array([  0, 200])
    
    .. image:: ../assets/img/object_physical_PhysicalObject_top_left.png
      :height: 250
    """

    top_right = None
    """numpy.ndarray: The top-right coordinate of the object.

    Example
    -------
    >>> import laygo2
    >>> obj = laygo2.object.physical.PhysicalObject(xy = [[0, 0], [200, 200]])
    >>> obj.top_right
    array([200, 200])
    
    .. image:: ../assets/img/object_physical_PhysicalObject_top_right.png
      :height: 250
    """

    @property
    def bbox(self):
        """numpy.ndarray: The physical bounding box of the object.

        Example
        -------
        >>> import laygo2
        >>> obj = laygo2.object.physical.PhysicalObject(xy = [[0, 0], [200, 200]])
        >>> obj.bbox
        array([[  0,   0],
               [200, 200]])

        .. image:: ../assets/img/object_physical_PhysicalObject_bbox.png
          :height: 250

        """
        # if self.xy.ndim == 2:
        if isinstance(self.xy[0], np.ndarray):
            return np.sort(np.array([self.xy[0, :], self.xy[1, :]]), axis=0)
        else:  # single-point object.
            return np.array([self.xy, self.xy])

    def __init__(self, xy, name=None, params=None):
        """
        The constructor function.

        Parameters
        ----------
        xy : numpy.ndarray
            Physical coordinate values of the object
            in the form of [bottom_left, top_right].
        name : str
            Object name.
        params : dict
            Dictionary storing the parameters associated with the object.

        Returns
        -------
        PhysicalObject

        Example
        -------
        >>> import laygo2
        >>> obj = laygo2.object.physical.PhysicalObject(xy = [[0, 0], [200, 200]],
                name="test", params={'maxI': 0.005})
        >>> print(obj)
        <laygo2.object.physical.PhysicalObject object at 0x000001ECF0022948>
         name: test,
         class: PhysicalObject,
         xy: [[0, 0], [200, 200]],
         params: {'maxI': 0.005},
        """

        self.name = name
        # Initialize pointers.
        self.pointers = dict()
        self.pointers["left"] = np.array([0, 0], dtype=int)
        self.pointers["right"] = np.array([0, 0], dtype=int)
        self.pointers["bottom"] = np.array([0, 0], dtype=int)
        self.pointers["top"] = np.array([0, 0], dtype=int)
        self.pointers["bottom_left"] = np.array([0, 0], dtype=int)
        self.pointers["bottom_right"] = np.array([0, 0], dtype=int)
        self.pointers["top_left"] = np.array([0, 0], dtype=int)
        self.pointers["top_right"] = np.array([0, 0], dtype=int)
        self.left = self.pointers["left"]
        self.right = self.pointers["right"]
        self.bottom = self.pointers["bottom"]
        self.top = self.pointers["top"]
        self.bottom_left = self.pointers["bottom_left"]
        self.bottom_right = self.pointers["bottom_right"]
        self.top_left = self.pointers["top_left"]
        self.top_right = self.pointers["top_right"]

        self.params = params  # deepcopy(params)  # deepcopy increases the memory usage.
        self.xy = xy

    def __str__(self):
        """Return the summary of the object information."""
        return self.summarize()

    def summarize(self):
        """Return the summary of the object information."""
        name = "None" if self.name is None else self.name
        return (
            self.__repr__() + " \n"
            "    name: "
            + name
            + ", \n"
            + "    class: "
            + self.__class__.__name__
            + ", \n"
            + "    xy: "
            + str(self.xy.tolist())
            + ", \n"
            + "    params: "
            + str(self.params)
            + ", \n"
        )

    def _update_pointers(self):
        """The internal function that updates the object's pointers
        after a change in its physical coordinates."""
        xy_left = np.diag(np.dot(np.array([[1, 0], [0.5, 0.5]]), self.bbox))
        xy_right = np.diag(np.dot(np.array([[0, 1], [0.5, 0.5]]), self.bbox))
        xy_bottom = np.diag(np.dot(np.array([[0.5, 0.5], [1, 0]]), self.bbox))
        xy_top = np.diag(np.dot(np.array([[0.5, 0.5], [0, 1]]), self.bbox))
        xy_bottom_left = np.diag(np.dot(np.array([[1, 0], [1, 0]]), self.bbox))
        xy_bottom_right = np.diag(np.dot(np.array([[0, 1], [1, 0]]), self.bbox))
        xy_top_left = np.diag(np.dot(np.array([[1, 0], [0, 1]]), self.bbox))
        xy_top_right = np.diag(np.dot(np.array([[0, 1], [0, 1]]), self.bbox))
        xy_center = np.diag(np.dot(np.array([[0.5, 0.5], [0.5, 0.5]]), self.bbox))
        self.pointers["left"] = xy_left.astype(int)
        self.pointers["right"] = xy_right.astype(int)
        self.pointers["bottom"] = xy_bottom.astype(int)
        self.pointers["top"] = xy_top.astype(int)
        self.pointers["bottom_left"] = xy_bottom_left.astype(int)
        self.pointers["bottom_right"] = xy_bottom_right.astype(int)
        self.pointers["top_left"] = xy_top_left.astype(int)
        self.pointers["top_right"] = xy_top_right.astype(int)
        self.pointers["center"] = xy_center.astype(int)
        self.left = self.pointers["left"]
        self.right = self.pointers["right"]
        self.bottom = self.pointers["bottom"]
        self.top = self.pointers["top"]
        self.bottom_left = self.pointers["bottom_left"]
        self.bottom_right = self.pointers["bottom_right"]
        self.top_left = self.pointers["top_left"]
        self.top_right = self.pointers["top_right"]
        self.center = self.pointers["center"]


class IterablePhysicalObject(PhysicalObject):
    """
    The base class of entities capable of iterable operations among elements.

    """

    elements = None
    """
    dict: Numpy array containing its element objects.

    Example
    -------
    >>> import laygo2
    >>> phy0 = laygo2.object.physical.PhysicalObject(xy=[[0, 0], [100, 100]]) 
    >>> phy1 = laygo2.object.physical.PhysicalObject(xy=[[0, 0], [200, 200]]) 
    >>> phy2 = laygo2.object.physical.PhysicalObject(xy=[[0, 0], [300, 300]]) 
    >>> element = [phy0, phy1, phy2] 
    >>> iphy0 = laygo2.object.physical.IterablePhysicalObject(
            xy=[[0, 0], [300, 300]], elements = elements)
    >>> iphy0.elements 
    array([<laygo2.object.physical.PhysicalObject object at 0x000002049A77FDF0>,
           <laygo2.object.physical.PhysicalObject object at 0x000002049A77F3D0>,
           <laygo2.object.physical.PhysicalObject object at 0x000002049A77FF40>],
          dtype=object)

    """

    def _get_xy(self):
        """numpy.ndarray(dtype=numpy.int): Retrive x,y coordinate values of
        the object."""
        return self._xy

    def _set_xy(self, value):
        """numpy.ndarray(dtype=numpy.int): Update x,y coordinates of
        the object."""
        # Update the coordinate value of its elements.
        self._update_elements(xy_ofst=value - self.xy)
        # Update the coordinate value of the object itself.
        PhysicalObject._set_xy(self, value=value)

    xy = property(_get_xy, _set_xy)

    @property
    def shape(self):
        """Array size of the object.

        Example
        -------
        >>> import laygo2
        >>> phy0 = laygo2.object.physical.PhysicalObject(xy=[[0, 0], [100, 100]])
        >>> phy1 = laygo2.object.physical.PhysicalObject(xy=[[0, 0], [200, 200]])
        >>> phy2 = laygo2.object.physical.PhysicalObject(xy=[[0, 0], [300, 300]])
        >>> element = [phy0, phy1, phy2]
        >>> iphy0 = laygo2.object.physical.IterablePhysicalObject(
            xy=[[0, 0], [300, 300]], elements = elements)
        >>> iphy0.shape
        array([3])

        """
        if self.elements is None:
            return None
        else:
            return np.array(self.elements.shape, dtype=int)

    def __init__(self, xy, name=None, params=None, elements=None):
        """
        The constructor function.

        Parameters
        ----------
        xy : numpy.ndarray
            Physical coordinate values of the object
            in the form of [bottom_left, top_right].
        name : str
            Object name.
        params : dict
            Dictionary storing the parameters associated with the object.
        elements : list
            List containing its element objects.

        Returns
        -------
        IterablePhysicalObject

        Example
        -------
        >>> import laygo2
        >>> phy0 = laygo2.object.physical.PhysicalObject(xy=[[0, 0], [100, 100]])
        >>> phy1 = laygo2.object.physical.PhysicalObject(xy=[[0, 0], [200, 200]])
        >>> phy2 = laygo2.object.physical.PhysicalObject(xy=[[0, 0], [300, 300]])
        >>> element = [phy0, phy1, phy2]
        >>> iphy0 = laygo2.object.physical.IterablePhysicalObject(
            xy=[[0, 0], [300, 300]], elements = elements)
        >>> print(iphy0)
        <laygo2.object.physical.IterablePhysicalObject object at 0x000002049A77E380>
        name: None,
        class: IterablePhysicalObject,
        xy: [[0, 0], [300, 300]],
        params: None,

        """
        PhysicalObject.__init__(self, xy=xy, name=name, params=params)
        if elements is None:
            self.elements = None
        else:
            self.elements = np.asarray(elements)

    def __getitem__(self, pos):
        """Return the sub-elements of the object, based on the pos parameter."""
        return self.elements[pos]

    def __setitem__(self, pos, item):
        """Update the sub-elements of the object, based on the pos and item
        parameters."""
        self.elements[pos] = item

    def __iter__(self):
        """Iterator that maps directly to the elements attribute of
        this object."""
        return self.elements.__iter__()

    def __next__(self):
        """Iterator that maps directly to the iterator function of
        the elements attribute of this object."""
        return self.elements.__next__()

    def ndenumerate(self):
        """Enumerate over the element array. Calls np.ndenumerate() of
        the elements of this object."""
        return np.ndenumerate(self.elements)

    def _update_elements(self, xy_ofst):
        """Update the xy coordinates of the elements of this object,
        used internally by the _set_xy() function."""
        if np.all(self.elements is not None):
            # Update the x and y coordinate values of elements.
            for n, e in self.ndenumerate():
                if e is not None:
                    e.xy = e.xy + xy_ofst


class PhysicalObjectGroup(IterablePhysicalObject):
    """
    A class for grouped physical objects, designed to be generated as a group
    in Cadence Virtuoso (currently not implemented).
    """

    # TODO: implement this.

    def summarize(self):
        """Get object information summary."""
        return IterablePhysicalObject.summarize(self) + "    elements: " + str(self.elements) + ", \n"

    def __init__(self, xy, name=None, params=None, elements=None):
        """
        The constructor function.

        Parameters
        ----------
        xy : numpy.ndarray(dtype=int)
            The coordinate of this object represented as a Numpy array [x, y].
        name : str, optional
            The name of the object.
        params : dict or None
            The dictionary that contains the parameters of this object, with
            parameter names as keys.
        elements : numpy.ndarray(dtype=LayoutObject) or None
            The iterable elements of the object.
        """
        IterablePhysicalObject.__init__(self, xy=xy, name=name, params=params, elements=elements)


'''
# Deprecated as PhysicalObjectGroup can be used instead in most cases.
# But the code is preserved for reference.
class PhysicalObjectArray(np.ndarray):
    """LayoutObject array class for containing multiple layout objects. Subclassing ndarray to utilize advance slicing
     functions."""
    name = None
    """str: the name of the object."""

    params = None
    """dict or None: parameters of the object. """

    _xy = None  # np.zeros(2, dtype=int)
    """numpy.ndarray(dtype=numpy.int): the internal variable of xy."""

    def get_xy(self):
        """numpy.ndarray(dtype=numpy.int): gets the x and y coordinate values of this object."""
        return self._xy

    def set_xy(self, value):
        """numpy.ndarray(dtype=numpy.int): sets the x and y coordinate values of this object."""
        if value is None:
            self._xy = value
        else:
            self._xy = np.asarray(value, dtype=int)

    xy = property(get_xy, set_xy)
    """numpy.ndarray(detype=numpy.int): the x and y coordinate values of the object."""

    def moveby(self, delta):
        """move the array and child objects by delta."""
        self.xy = self.xy + delta
        for i in self:
            i.xy = i.xy + delta

    def __new__(cls, input_array, name=None, xy=None, params=None):
        """
        Constructor for ndarray subclasses - check the NumPy manual for details.

        Parameters
        ----------
        input_array : np.ndarray
            An array of LayoutObject objects.
        name : str
            The name of the array.
        xy : numpy.ndarray(dtype=int)
            The xy-coordinate of the object. The format is [x0, y0].
        params : dict
            Additional parameters of the array.
        """
        # Input array is an already formed ndarray instance
        # We first cast to be our class type
        obj = np.asarray(input_array).view(cls)
        # add the new attribute to the created instance
        obj.name = name
        obj.xy = None if xy is None else np.asarray(xy, dtype=int)
        obj.params = params
        # Finally, we must return the newly created object:
        return obj

    def __array_finalize__(self, obj):
        """
        Array finalizing function for subclassing ndarray - check the NumPy manual for details
        """
        if obj is None: return
        # Transfer parameters
        self.name = getattr(obj, 'name', None)
        self.xy = getattr(obj, 'xy', None)
        self.params = getattr(obj, 'params', None)

    def __str__(self):
        """Return the summary of the object information."""
        return self.summarize()

    def summarize(self):
        """Return the summary of the object information."""
        return "  " + \
               "name:" + self.name + ", " + \
               "class:" + self.__class__.__name__ + ", " + \
               "shape:" + str(self.shape) + ", " + \
               "xy:" + str(self.xy) + ", " + \
               "params:" + str(self.params) + "\n" + \
               "  elements:" + str(np.ndarray.__str__(self)) + "\n"
'''


class Rect(PhysicalObject):
    """
    Rectangle object class.

    Example
    -------
    >>> from laygo2.object.physical import Rect
    >>> rect0 = Rect(xy=[[0, 0], [100, 100]], layer=['M1', 'drawing'])
    >>> print(rect0)
    <laygo2.object.physical.Rect object at 0x000002049A77F3A0>
    name: None,
    class: Rect,
    xy: [[0, 0], [100, 100]],
    params: None, , layer: ['M1', 'drawing']

    """

    layer = None
    """
    numpy.ndarray: The physical layer information of the object, 
    represented as a list with two elements: [name, purpose].

    Example
    -------
    >>> import laygo2
    >>> rect0 = laygo2.object.physical.Rect(xy=[[0, 0], [100, 100]], 
            layer=['M1', 'drawing'], netname='net0', hextension=20, vextension=20)
    >>> rect0.layer 
    ['M1', 'drawing']

    """

    netname = None
    """
    str: The net name associated with the object.

    Example
    -------
    >>> import laygo2
    >>> rect0 = laygo2.object.physical.Rect(xy=[[0, 0], [100, 100]], 
            layer=['M1', 'drawing'], netname='net0', hextension=20, vextension=20)
    >>> rect0.netname 
    “net0”
    
    """

    hextension = 0
    """
    int: The horizontal extension of the rectangle object above its bounding box.

    Example
    -------
    >>> import laygo2
    >>> rect0 = laygo2.object.physical.Rect(xy=[[0, 0], [100, 100]], 
            layer=['M1', 'drawing'], netname='net0', hextension=20, vextension=20)
    >>> rect0.hextension 
    20

    .. image:: ../assets/img/object_physical_rect_hextension.png
      :height: 250

    """

    vextension = 0
    """
    int: The vertical extension of the rectangle object above its bounding box.

    Example
    -------
    >>> import laygo2
    >>> rect0 = laygo2.object.physical.Rect(xy=[[0, 0], [100, 100]], 
            layer=['M1', 'drawing'], netname='net0', hextension=20, vextension=20)
    >>> rect0.vextension 
    20

    .. image:: ../assets/img/object_physical_rect_vextension.png
      :height: 250

    """

    color = None
    """
    int or None or "not_MPT": The color (multi-patterning identifier) parameter of 
    the object.

    Example
    -------
    >>> import laygo2
    >>> rect0 = laygo2.object.physical.Rect(xy=[[0, 0], [100, 100]], 
            layer=['M1', 'drawing'], netname='net0', color=1)
    >>> rect0.color 
    1

    """

    @property
    def height(self):
        """
        int: The height of the object.

        Example
        -------
        >>> import laygo2
        >>> rect0 = laygo2.object.physical.Rect(xy=[[0, 0], [100, 100]],
                layer=['M1', 'drawing'])
        >>> rect0.height
        100

        .. image:: ../assets/img/object_physical_rect_height.png
          :height: 250

        """
        return abs(self.xy[0, 1] - self.xy[1, 1])

    @property
    def width(self):
        """
        int: The width of the object.

        Example
        -------
        >>> import laygo2
        >>> rect0 = laygo2.object.physical.Rect(xy=[[0, 0], [100, 100]],
                layer=['M1', 'drawing'])
        >>> rect0.width
        100

        .. image:: ../assets/img/object_physical_rect_width.png
          :height: 250

        """
        return abs(self.xy[0, 0] - self.xy[1, 0])

    @property
    def height_vec(self):
        """numpy.ndarray(dtype=int): The height direction vector [0, self.height]
        of the object."""
        return np.array([0, self.height])

    @property
    def width_vec(self):
        """numpy.ndarray(dtype=int): The width direction vector [self.width, 0]
        of the object."""
        return np.array([self.width, 0])

    @property
    def size(self):
        """
        numpy.ndarray: The size of the object ([self.width, self.height]).

        Example
        -------
        >>> import laygo2
        >>> rect0 = laygo2.object.physical.Rect(xy=[[0, 0], [100, 100]],
                layer=['M1', 'drawing'])
        >>> rect0.size
        array([100, 100])

        .. image:: ../assets/img/object_physical_rect_size.png
          :height: 250

        """
        return np.array([self.width, self.height])

    def __init__(
        self,
        xy,
        layer,
        color=None,
        hextension=0,
        vextension=0,
        name=None,
        netname=None,
        params=None,
    ):
        """
        The constructor function.

        Parameters
        ----------
        xy : numpy.ndarray
            Physical coordinate values of the object
            in the form of [bottom_left, top_right].
        layer : list
            The physical layer information of the object,
            represented as a list with two elements: [name, purpose].
        hextension : int
            The horizontal extension value of the object.
        vextension : int
            The vertical extension value of the object.
        name : str
            Object name.
        netname : str
            The net name associated with the object.
        params : dict
            Dictionary storing the parameters associated with the object.
        color : str, optional.
            The color (multi-patterning identifier) parameter of the object.

        Returns
        -------
        Rect

        See Also
        --------
        PhysicalObject

        Example
        -------
        >>> import laygo2
        >>> rect0 = laygo2.object.physical.Rect(xy=[[0, 0], [100, 100]],
                layer=['M1', 'drawing'], netname='net0', color=1)
        >>> print(rect0)
        <laygo2.object.physical.Rect object at 0x000002049A77F3A0>
        name: None,
        class: Rect,
        xy: [[0, 0], [100, 100]],
        params: None, , layer: ['M1', 'drawing'], netname: net0

        .. image:: ../assets/img/object_physical_rect_init.png
          :height: 250

        """
        self.layer = layer
        if netname is None:
            self.netname = name
        else:
            self.netname = netname
        self.hextension = hextension
        self.vextension = vextension
        self.color = color
        PhysicalObject.__init__(self, xy=xy, name=name, params=params)

    def align(self, rect2):
        """
        Match the length of the self and rect2 objects, if either object has a width or height of 0.

        Parameters
        ----------
        rect2 : Rect
            The rect object to be aligned with `self`.
        """
        index = 0
        r0 = self
        r1 = rect2
        if r0.xy[0][0] == r0.xy[1][0]:  # width is zero
            index = 1

        pnt = np.zeros([2, 2], dtype=int)
        pnt[0][1] = r0.bbox[1][index]  # tr
        pnt[1][1] = r1.bbox[1][index]  # tr
        pnt[0][0] = r0.bbox[0][index]  # bl
        pnt[1][0] = r1.bbox[0][index]  # bl

        if pnt[1][1] > pnt[0][1]:  # p1-top is upper then p0-top
            _xy = r0.bbox  # r0 correction
            _xy[1][index] = pnt[1][1]
            r0.xy = _xy
        elif pnt[1][1] < pnt[0][1]:  # p1-top is lower then p0-top
            _xy = r1.bbox  # r1 correction
            _xy[1][index] = pnt[0][1]
            r1.xy = _xy

        if pnt[1][0] < pnt[0][0]:  # p1-bottom is lower then p0-bottom
            _xy = r0.bbox  # r0 correction
            _xy[0][index] = pnt[1][0]
            r0.xy = _xy
        elif pnt[1][0] > pnt[0][0]:
            _xy = r1.bbox  # r1 correction
            _xy[0][index] = pnt[0][0]
            r1.xy = _xy

    def summarize(self):
        """Get object information summary."""
        return (
            PhysicalObject.summarize(self)
            + "    layer: "
            + str(self.layer)
            + ", \n"
            + "    netname: "
            + str(self.netname)
            + ", \n"
        )


class Path(PhysicalObject):
    """
    Path object class.

    Example
    -------
    >>> from laygo2.object.physical.Path import Path
    >>> path0 = Path(xy=[[0, 0], [0, 100]], width=10,
                     extension=5, layer=['M1', 'drawing'])
    >>> print(path0)
    <laygo2.object.physical.Path object at 0x00000280D1F3CE88>
    name: None,
    class: Path,
    xy: [[0, 0], [0, 100]],
    params: None,
    width: 10,
    extension: 5,
    layer: ['M1', 'drawing'],

    """

    # TODO: implement pointers.

    layer = None
    """numpy.ndarray: The physical layer information of the object, 
    represented as a list with two elements: [name, purpose].
    
    """

    netname = None
    """
    str: The net name associated with the object.

    Example
    -------
    >>> import laygo2
    >>> path0 = laygo2.object.physical.Path(xy=[[0, 0], [0, 100]], width=10, 
            extension=5, layer=['M1', 'drawing'], netname='net0’)
    >>> path0.netname 
    “net0”
    
    .. image:: ../assets/img/object_physical_path_netname.png 
      :height: 250

    """

    width = None
    """
    int: the width of the object.

    Example
    -------
    >>> import laygo2
    >>> path0 = laygo2.object.physical.Path(xy=[[0, 0], [0, 100]], width=10, 
            extension=5, layer=['M1', 'drawing'], netname='net0’)
    >>> path0.width 
    10

    .. image:: ../assets/img/object_physical_path_width.png
      :height: 250

    """

    extension = 0
    """
    int: The path extension from its endpoints.

    Example
    -------
    >>> import laygo2
    >>> path0 = laygo2.object.physical.Path(xy=[[0, 0], [0, 100]], width=10, 
            extension=5, layer=['M1', 'drawing'], netname='net0’)
    >>> path0.extension 
    5
    
    .. image:: ../assets/img/object_physical_path_extension.png
      :height: 250

    """

    @property
    def bbox(self):
        """The physical bounding box of the object."""
        return np.sort(np.array([self.xy[0], self.xy[-1]]), axis=0)

    def _update_pointers(self):
        """The internal function that updates the object's pointers
        after a change in its physical coordinates."""
        pass

    def __init__(self, xy, layer, width, extension=0, name=None, netname=None, params=None):
        """
        The constructor function.

        Parameters
        ----------
        xy : numpy.ndarray
            Physical coordinate values of the object
            in the form of [bottom_left, top_right].
        layer : list
            The physical layer information of the object,
            represented as a list with two elements: [name, purpose].
        width : int
            The width of the object.
        extension : int
            The path extension from its endpoints.
        name : str
            Object name.
        netname : str
            The net name associated with the object.
        params : dict
            Dictionary storing the parameters associated with the object.

        Returns
        -------
        Path

        Example
        -------
        >>> import laygo2
        >>> path0 = laygo2.object.physical.Path(xy=[[0, 0], [0, 100]], width=10,
                extension=5, layer=['M1', 'drawing'], netname='net0’)
        >>> print(path0)
        <laygo2.object.physical.Path object at 0x00000280D1F3CE88>
        name: None,
        class: Path,
        xy: [[0, 0], [0, 100]],
        params: None,
        width: 10,
        extension: 5,
        layer: ['M1', 'drawing'],
        netname: net0

        .. image:: ../assets/img/object_physical_path_init.png
          :height: 250

        """
        self.layer = layer
        self.width = width
        self.extension = extension
        self.netname = netname
        PhysicalObject.__init__(self, xy=xy, name=name, params=params)
        self.pointers = dict()  # Pointers are invalid for Path objects.

    def summarize(self):
        """Get object information summary."""
        return (
            PhysicalObject.summarize(self)
            + "    width: "
            + str(self.width)
            + ", \n"
            + "    extension: "
            + str(self.extension)
            + ", \n"
            + "    layer: "
            + str(self.layer)
            + ", \n"
            + "    netname: "
            + str(self.netname)
            + ", \n"
        )


class Pin(IterablePhysicalObject):
    """
    Pin object class.

    Example
    -------
    >>> from laygo2.object.physical import Pin
    >>> pin0 = Pin(xy=[[0, 0], [100, 100]], layer=['M1', 'drawing'],
                   netname='net0', params={'direction': 'input'})
    >>> print(pin0)
    <laygo2.object.physical.Pin object at 0x000002049A77FF70>
        name: None,
        class: Pin,
        xy: [[0, 0], [100, 100]],
        params: {'direction': 'input'}, , layer: ['M1' 'drawing'],
        netname: net0, shape: None, master: None

    """

    layer = None
    """
    numpy.ndarray: The physical layer information of the object, 
    represented as a list with two elements: [name, purpose].

    Example
    -------
    >>> import laygo2
    >>> pin0 = laygo2.object.physical.Pin(xy=[[0, 0], [100, 100]], 
            layer=['M1', 'drawing'], netname='net0', params={'direction': 'input'})
    >>> pin0.layer 
    ['M1', 'drawing']
    
    numpy.ndarray: 객체의 layer 정보 [name, purpose].
    """

    netname = None
    """
    str: The net name associated with the object.

    Example
    -------
    >>> import laygo2
    >>> pin0 = laygo2.object.physical.Pin(xy=[[0, 0], [100, 100]], 
            layer=['M1', 'drawing'], netname='net0', params={'direction': 'input'})
    >>> pin0.netname 
    “net0”
    
    str: 객체의 노드 명.
    """

    master = None
    """
    Instance: The instance that the pin belongs to. Used for pins of instances only.
    """

    @property
    def height(self):
        """
        int: The height of the object.

        Example
        -------
        >>> import laygo2
        >>> pin0 = laygo2.object.physical.Pin(xy=[[0, 0], [100, 100]],
            layer=['M1', 'drawing'], netname='net0', params={'direction': 'input'})
        >>> pin0.height
        100

        """
        return abs(self.xy[0, 1] - self.xy[1, 1])

    @property
    def width(self):
        """
        int: The width of the object.

        Example
        -------
        >>> import laygo2
        >>> pin0 = laygo2.object.physical.Pin(xy=[[0, 0], [100, 100]],
            layer=['M1', 'drawing'], netname='net0', params={'direction': 'input'})
        >>> pin0.width
        100

        """
        return abs(self.xy[0, 0] - self.xy[1, 0])

    @property
    def size(self):
        """
        numpy.ndarray: The size of the object.

        Example
        -------
        >>> import laygo2
        >>> pin0 = laygo2.object.physical.Pin(xy=[[0, 0], [100, 100]],
            layer=['M1', 'drawing'], netname='net0', params={'direction': 'input'})
        >>> pin0.size
        [100, 100]

        """
        return np.array([self.width, self.height])

    @property
    def height_vec(self):
        """numpy.ndarray(dtype=int): The height direction vector [0, self.height]
        of the object."""
        return np.array([0, self.height])

    @property
    def width_vec(self):
        """numpy.ndarray(dtype=int): The width direction vector [self.width, 0]
        of the object."""
        return np.array([self.width, 0])

    def __init__(
        self,
        xy,
        layer,
        name=None,
        netname=None,
        params=None,
        master=None,
        elements=None,
    ):
        """
        The constructor function.

        Parameters
        ----------
        xy : numpy.ndarray
            Physical coordinate values of the object
            in the form of [bottom_left, top_right].
        layer : list
            The physical layer information of the object,
            represented as a list with two elements: [name, purpose].
        name : str
            Object name.
        netname : str
            The net name associated with the object.
        params : dict
            Dictionary storing the parameters associated with the object.

        Returns
        -------
        Pin

        Example
        -------
        >>> import laygo2
        >>> pin0 = laygo2.object.physical.Pin(xy=[[0, 0], [100, 100]],
            layer=['M1', 'drawing'], netname='net0', params={'direction': 'input'})
        >>> print(pin0)
        <laygo2.object.physical.Pin object at 0x000002049A77FF70>
            name: None,
            class: Pin,
            xy: [[0, 0], [100, 100]],
            params: {'direction': 'input'}, , layer: ['M1' 'drawing'],
            netname: net0, shape: None, master: None

        """
        self.layer = np.asarray(layer)
        if netname is None:
            netname = name
        self.netname = netname
        self.master = master
        IterablePhysicalObject.__init__(self, xy=xy, name=name, params=params, elements=elements)

    def summarize(self):
        """Get object information summary."""
        return (
            IterablePhysicalObject.summarize(self)
            + "    layer: "
            + str(self.layer)
            + ", \n"
            + "    netname: "
            + str(self.netname)
            + ", \n"
            + "    shape: "
            + str(self.shape)
            + ", \n"
            + "    master: "
            + str(self.master)
            + ", \n"
        )

    def export_to_dict(self):
        db = dict()
        db["xy"] = self.xy.tolist()
        db["layer"] = self.layer.tolist()
        db["name"] = self.name
        db["netname"] = self.netname
        return db


class Text(PhysicalObject):
    """
    Text object class.

    Example
    -------
    >>> import laygo2
    >>> text0 = laygo2.object.physical.Text(xy=[0, 0],
        layer=['text', 'drawing'], text='test', params=None)
    >>> print(text0)
    <laygo2.object.physical.Text object at 0x000002049A77FD90>
        name: None,
        class: Text,
        xy: [0, 0],
        params: None,
        layer: ['text', 'drawing'],
        text: test

    """

    layer = None
    """
    list: The physical layer information of the object,
          represented as a list with two elements: [name, purpose].

    Example
    -------
    >>> import laygo2
    >>> text0 = laygo2.object.physical.Text(xy=[0, 0], 
            layer=['text', 'drawing'], text='test', params=None)
    >>> text0.layer 
    ['text', 'drawing']

    """

    text = None
    """
    str: Text content of the object.

    Example
    -------
    >>> import laygo2
    >>> text0 = laygo2.object.physical.Text(xy=[0, 0], 
            layer=['text', 'drawing'], text='test', params=None)
    >>> text0.text 
    'test'
    
    """

    def __init__(self, xy, layer, text, name=None, params=None):
        """
        The constructor function.

        Parameters
        ----------
        xy : numpy.ndarray
            Physical coordinate values of the object
            in the form of [bottom_left, top_right].
        layer : list
            The physical layer information of the object,
            represented as a list with two elements: [name, purpose].
        text : str
            The text content.
        name : str
            Object name.
        params : dict
            Dictionary storing the parameters associated with the object.

        Returns
        -------
        Text

        See Also
        --------
        PhysicalObject : base class.

        Example
        -------
        >>> import laygo2
        >>> text0 = laygo2.object.physical.Text(xy=[0, 0],
            layer=['text', 'drawing'], text='test', params=None)
        >>> print(text0)
        <laygo2.object.physical.Text object at 0x000002049A77FD90>
            name: None,
            class: Text,
            xy: [0, 0],
            params: None,
            layer: ['text', 'drawing'],
            text: test

        """
        self.layer = layer
        self.text = text

        PhysicalObject.__init__(self, xy=xy, name=name, params=params)

    def summarize(self):
        """Get object information summary."""
        return (
            PhysicalObject.summarize(self)
            + "    layer: "
            + str(self.layer)
            + ", \n"
            + "    text: "
            + str(self.text)
            + ", \n"
        )


class Instance(IterablePhysicalObject):
    """
    Instance object class.

    Example
    -------
    >>> import laygo2
    >>> inst0_pins = dict()
    >>> inst0_pins['in'] = laygo2.object.physical.Pin(xy=[[0, 0], [10,10]],
            layer = ['M1', 'drawing'], netname = 'in')
    >>> inst0_pins['out']= laygo2.object.physical.Pin(xy=[[90, 90], [100, 100]],
            layer=['M1', 'drawing'], netname='out')
    >>> inst0 = laygo2.object.physical.Instance(name="I0", xy=[100,100],
            libname="mylib", cellname="mycell", shape=[3, 2], pitch=[200,200],
            unit_size=[100, 100], pins=inst0_pins, transform='R0')
    >>> print(inst0)
    <laygo2.object.physical.Instance object at 0x000001AF458AF8E0>
        name: I0,
        class: Instance,
        xy: [100, 100],
        params: None,
        size: [500, 300],
        shape: [3, 2],
        pitch: [200, 200],
        transform: R0,
        pins: {'in': <laygo2.object.physical.Pin object at 0x000001AF560D6170>,
               'out': <laygo2.object.physical.Pin object at 0x000001AF560D5F30>},
    >>> print( inst0[1,0].xy0 )
    array([300, 100])

    """

    # TODO: update (maybe) xy and sub-elements after transform property is updated.

    libname = None
    """
    str: The library name of the object.

    Example
    -------
    >>> import laygo2
    >>> inst0_pins = dict() 
    >>> inst0_pins['in'] = laygo2.object.physical.Pin(xy=[[0, 0], [10,10]], 
            layer = ['M1', 'drawing'], netname = 'in') 
    >>> inst0_pins['out']= laygo2.object.physical.Pin(xy=[[90, 90], [100, 100]], 
            layer=['M1', 'drawing'], netname='out')
    >>> inst0 = laygo2.object.physical.Instance(name="I0", xy=[100,100], 
            libname="mylib", cellname="mycell", shape=[3, 2], pitch=[200,200], 
            unit_size=[100, 100], pins=inst0_pins, transform='R0')
    >>> inst0.libname 
    'mylib'
    
    .. image:: ../assets/img/object_physical_instance_libname.png
      :height: 250

    """

    cellname = None
    """
    str: The cellname of the instance object.

    Example
    -------
    >>> import laygo2
    >>> inst0_pins = dict() 
    >>> inst0_pins['in'] = laygo2.object.physical.Pin(xy=[[0, 0], [10,10]], 
            layer = ['M1', 'drawing'], netname = 'in') 
    >>> inst0_pins['out']= laygo2.object.physical.Pin(xy=[[90, 90], [100, 100]], 
            layer=['M1', 'drawing'], netname='out')
    >>> inst0 = laygo2.object.physical.Instance(name="I0", xy=[100,100], 
            libname="mylib", cellname="mycell", shape=[3, 2], pitch=[200,200], 
            unit_size=[100, 100], pins=inst0_pins, transform='R0')
    >>> inst0.cellname
    'mycell'
    
    .. image:: ../assets/img/object_physical_instance_cellname.png
      :height: 250

    """

    viewname = None
    """str: The view name of the instance."""

    shape = None
    """np.array([int, int]) or None: The mosaic shape of the instance. 
    None if the instance is non-mosaic."""

    _pitch = None
    """np.array([int, int]) or None: The internal variable for self.pitch."""

    unit_size = None
    """
    numpy.ndarray: The array unit size for the object.

    Example
    -------
    >>> import laygo2
    >>> inst0_pins = dict() 
    >>> inst0_pins['in'] = laygo2.object.physical.Pin(xy=[[0, 0], [10,10]], 
            layer = ['M1', 'drawing'], netname = 'in') 
    >>> inst0_pins['out']= laygo2.object.physical.Pin(xy=[[90, 90], [100, 100]], 
            layer=['M1', 'drawing'], netname='out')
    >>> inst0 = laygo2.object.physical.Instance(name="I0", xy=[100,100], 
            libname="mylib", cellname="mycell", shape=[3, 2], pitch=[200,200], 
            unit_size=[100, 100], pins=inst0_pins, transform='R0')
    >>> inst0.unit_size 
    array([100, 100])
    
    .. image:: ../assets/img/object_physical_instance_unit_size.png
      :height: 250

    """

    transform = "R0"
    """
    str: The attribute of the object that defines its transformation 
    (rotation and mirroring).

    Example
    -------
    >>> import laygo2
    >>> inst0_pins = dict() 
    >>> inst0_pins['in'] = laygo2.object.physical.Pin(xy=[[0, 0], [10,10]], 
            layer = ['M1', 'drawing'], netname = 'in') 
    >>> inst0_pins['out']= laygo2.object.physical.Pin(xy=[[90, 90], [100, 100]], 
            layer=['M1', 'drawing'], netname='out')
    >>> inst0 = laygo2.object.physical.Instance(name="I0", xy=[100,100], 
            libname="mylib", cellname="mycell", shape=[3, 2], pitch=[200,200], 
            unit_size=[100, 100], pins=inst0_pins, transform='R0')
    >>> inst0.transform 
    "R0"
    
    .. image:: ../assets/img/object_physical_instance_transform.png
      :height: 250

    """

    pins = None
    """
    dict: Dictionary of pins belonging to the object.

    Example
    -------
    >>> import laygo2
    >>> inst0_pins = dict() 
    >>> inst0_pins['in'] = laygo2.object.physical.Pin(xy=[[0, 0], [10,10]], 
            layer = ['M1', 'drawing'], netname = 'in') 
    >>> inst0_pins['out']= laygo2.object.physical.Pin(xy=[[90, 90], [100, 100]], 
            layer=['M1', 'drawing'], netname='out')
    >>> inst0 = laygo2.object.physical.Instance(name="I0", xy=[100,100], 
            libname="mylib", cellname="mycell", shape=[3, 2], pitch=[200,200], 
            unit_size=[100, 100], pins=inst0_pins, transform='R0')
    >>> inst0.pins 
    {'in': <laygo2.object.physical.Pin object at 0x000001CA76EE1348>, 
    'out': <laygo2.object.physical.Pin object at 0x000001CA7709BD48>} 
    >>> inst0.pins["in"].shape 
    array([3, 2])
    >>> inst0.pins["out"].shape 
    array([3, 2] )
    >>> inst0.pins["in"][1, 1].xy 
    array([[300, 300], [310, 310]])
    
    .. image:: ../assets/img/object_physical_instance_pins.png
      :height: 250

    """
    
    def get_p(self):
        return self.pins

    def set_p(self, val):
        self.pins = val

    p = property(get_p, set_p)
    """str: Alias of pins."""
    
    def _update_pins(self, xy_ofst):
        """
        Internal function to update x,y coordinates of the object's pins. 
        Used as part of _set_xy().
        """
        if self.pins is not None:
            for pn, p in self.pins.items():
                if np.all(p is not None):
                    # Update the x and y coordinate values of elements.
                    for n, e in np.ndenumerate(p):
                        if e is not None:
                            e.xy = e.xy + xy_ofst

    def _get_xy(self):
        """numpy.ndarray(dtype=numpy.int): Retrive x,y coordinate values of
        the object."""
        return self._xy

    def _set_xy(self, value):
        """numpy.ndarray(dtype=numpy.int): Update x,y coordinates of
        the object."""
        # Update the coordinate value of its pins.
        self._update_pins(xy_ofst=value - self.xy)
        IterablePhysicalObject._set_xy(self, value=value)

    xy = property(_get_xy, _set_xy)

    @property
    def xy0(self):
        """
        numpy.ndarray: The x,y coordinates of the primary corner of the object.

        Example
        -------
        >>> import laygo2
        >>> inst0_pins = dict()
        >>> inst0_pins['in'] = laygo2.object.physical.Pin(xy=[[0, 0], [10,10]],
                layer = ['M1', 'drawing'], netname = 'in')
        >>> inst0_pins['out']= laygo2.object.physical.Pin(xy=[[90, 90], [100, 100]],
                layer=['M1', 'drawing'], netname='out')
        >>> inst0 = laygo2.object.physical.Instance(name="I0", xy=[100,100],
                libname="mylib", cellname="mycell", shape=[3, 2], pitch=[200,200],
                unit_size=[100, 100], pins=inst0_pins, transform='R0')
        >>> inst0.xy0
        array([100, 100])

        .. image:: ../assets/img/object_physical_instance_xy0.png
          :height: 250

        """
        return self.xy

    @property
    def xy1(self):
        """
        numpy.ndarray: The x,y coordinates of the secondary corner of the object.

        Example
        -------
        >>> import laygo2
        >>> inst0_pins = dict()
        >>> inst0_pins['in'] = laygo2.object.physical.Pin(xy=[[0, 0], [10,10]],
                layer = ['M1', 'drawing'], netname = 'in')
        >>> inst0_pins['out']= laygo2.object.physical.Pin(xy=[[90, 90], [100, 100]],
                layer=['M1', 'drawing'], netname='out')
        >>> inst0 = laygo2.object.physical.Instance(name="I0", xy=[100,100],
                libname="mylib", cellname="mycell", shape=[3, 2], pitch=[200,200],
                unit_size=[100, 100], pins=inst0_pins, transform='R0')
        >>> inst0.xy1
        array([600, 400])

        .. image:: ../assets/img/object_physical_instance_xy1.png
          :height: 250

        """
        if self.size is None:
            return self.xy
        else:
            return self.xy + np.dot(self.size, tf.Mt(self.transform).T)

    @property
    def size(self):
        """
        numpy.ndarray: The size of the object ([self.width, self.height]).

        Example
        -------
        >>> import laygo2
        >>> inst0_pins = dict()
        >>> inst0_pins['in'] = laygo2.object.physical.Pin(xy=[[0, 0], [10,10]],
                layer = ['M1', 'drawing'], netname = 'in')
        >>> inst0_pins['out']= laygo2.object.physical.Pin(xy=[[90, 90], [100, 100]],
                layer=['M1', 'drawing'], netname='out')
        >>> inst0 = laygo2.object.physical.Instance(name="I0", xy=[100,100],
                libname="mylib", cellname="mycell", shape=[3, 2], pitch=[200,200],
                unit_size=[100, 100], pins=inst0_pins, transform='R0')
        >>> inst0.size
        array([500, 300])

        .. image:: ../assets/img/object_physical_instance_size.png
          :height: 250

        """
        if self.shape is None:
            return self.unit_size
        else:
            return (self.shape - np.array([1, 1])) * self.pitch + self.unit_size

    def get_pitch(self):
        """numpy.ndarray(dtype=int): Retrive the pitch of the instance."""
        if self._pitch is None:
            return self.unit_size
        else:
            return self._pitch

    def set_pitch(self, value):
        """numpy.ndarray(dtype=int): Update the pitch of the instance."""
        self._pitch = value

    pitch = property(get_pitch, set_pitch)
    """
    numpy.ndarray: The pitch between unit objects in the array.

    Example
    -------
    >>> import laygo2
    >>> inst0_pins = dict() 
    >>> inst0_pins['in'] = laygo2.object.physical.Pin(xy=[[0, 0], [10,10]], 
            layer = ['M1', 'drawing'], netname = 'in') 
    >>> inst0_pins['out']= laygo2.object.physical.Pin(xy=[[90, 90], [100, 100]], 
            layer=['M1', 'drawing'], netname='out')
    >>> inst0 = laygo2.object.physical.Instance(name="I0", xy=[100,100], 
            libname="mylib", cellname="mycell", shape=[3, 2], pitch=[200,200], 
            unit_size=[100, 100], pins=inst0_pins, transform='R0')
    >>> inst0.pitch 
    array([200, 200])
    
    .. image:: ../assets/img/object_physical_instance_pitch.png
      :height: 250

    See Also
    --------
    Instance.spacing

    """

    def get_spacing(self):
        return self.pitch

    def set_spacing(self, value):
        self.pitch = value

    spacing = property(get_spacing, set_spacing)
    """
    numpy.ndarray: Spacing between unit object of the object in array.

    Example
    -------
    >>> import laygo2
    >>> inst0_pins = dict() 
    >>> inst0_pins['in'] = laygo2.object.physical.Pin(xy=[[0, 0], [10,10]], 
            layer = ['M1', 'drawing'], netname = 'in') 
    >>> inst0_pins['out']= laygo2.object.physical.Pin(xy=[[90, 90], [100, 100]], 
            layer=['M1', 'drawing'], netname='out')
    >>> inst0 = laygo2.object.physical.Instance(name="I0", xy=[100,100], 
            libname="mylib", cellname="mycell", shape=[3, 2], pitch=[200,200], 
            unit_size=[100, 100], pins=inst0_pins, transform='R0')
    >>> inst0.spacing 
    array([200, 200])
    
    .. image:: ../assets/img/object_physical_instance_spacing.png
      :height: 250

    See Also
    --------
    Instance.pitch

    """

    @property
    def bbox(self):
        """The physical bounding box of the object."""
        bbox = np.array([self.xy0, self.xy1])
        # return bbox
        # return self.xy + np.dot(self.size, tf.Mt(self.transform).T)
        return np.sort(bbox, axis=0)

    @property
    def height(self):
        """
        int: The height of the object.

        Example
        -------
        >>> import laygo2
        >>> inst0_pins = dict()
        >>> inst0_pins['in'] = laygo2.object.physical.Pin(xy=[[0, 0], [10,10]],
                layer = ['M1', 'drawing'], netname = 'in')
        >>> inst0_pins['out']= laygo2.object.physical.Pin(xy=[[90, 90], [100, 100]],
                layer=['M1', 'drawing'], netname='out')
        >>> inst0 = laygo2.object.physical.Instance(name="I0", xy=[100,100],
                libname="mylib", cellname="mycell", shape=[3, 2], pitch=[200,200],
                unit_size=[100, 100], pins=inst0_pins, transform='R0')
        >>> inst0.height
        300

        .. image:: ../assets/img/object_physical_instance_height.png
          :height: 250

        """
        return abs(self.bbox[1][1] - self.bbox[0][1])

    @property
    def width(self):
        """
        int: The width of the object.

        Example
        -------
        >>> import laygo2
        >>> inst0_pins = dict()
        >>> inst0_pins['in'] = laygo2.object.physical.Pin(xy=[[0, 0], [10,10]],
                layer = ['M1', 'drawing'], netname = 'in')
        >>> inst0_pins['out']= laygo2.object.physical.Pin(xy=[[90, 90], [100, 100]],
                layer=['M1', 'drawing'], netname='out')
        >>> inst0 = laygo2.object.physical.Instance(name="I0", xy=[100,100],
                libname="mylib", cellname="mycell", shape=[3, 2], pitch=[200,200],
                unit_size=[100, 100], pins=inst0_pins, transform='R0')
        >>> inst0.width
        500

        .. image:: ../assets/img/object_physical_instance_width.png
          :height: 250

        """
        return abs(self.bbox[1][0] - self.bbox[0][0])

    @property
    def height_vec(self):
        """numpy.ndarray(dtype=int): The height direction vector [0, self.height]
        of the object."""
        return np.array([0, self.height])

    @property
    def width_vec(self):
        """numpy.ndarray(dtype=int): The width direction vector [self.width, 0]
        of the object."""
        return np.array([self.width, 0])

    def __init__(
        self,
        xy,
        libname,
        cellname,
        viewname="layout",
        shape=None,
        pitch=None,
        transform="R0",
        unit_size=np.array([0, 0]),
        pins=None,
        name=None,
        params=None,
    ):
        """
        The constructor function.

        Parameters
        ----------
        xy : numpy.ndarray
            The primary coordinate ([x0, y0]) of the object.
        libname : str
            The library name of the object.
        cellname : str
            The cell name of the object.
        shape : numpy.ndarray
            The shape [col, row] of the elements.
        pitch : numpy.ndarray
            The pitch between unit objects in the array.
        transform : str
            The attribute of the object that defines its transformation 
            (rotation and mirroring).
        unit_size : list
            The array unit size for the object.
        pins : dict
            Dictionary of pins belonging to the object.
        name : str
            Object name.
        params : dict
            (optional) Dictionary storing the parameters associated with 
            the object.

        Returns
        -------
        Instance : The constructed Instance object.

        See Also
        --------
        IterablePhysicalObject

        Example
        -------
        >>> import laygo2
        >>> inst0_pins = dict()
        >>> inst0_pins['in'] = laygo2.object.physical.Pin(xy=[[0, 0], [10,10]],
                layer = ['M1', 'drawing'], netname = 'in')
        >>> inst0_pins['out']= laygo2.object.physical.Pin(xy=[[90, 90], [100, 100]],
                layer=['M1', 'drawing'], netname='out')
        >>> inst0 = laygo2.object.physical.Instance(name="I0", xy=[100,100],
                libname="mylib", cellname="mycell", shape=[3, 2], pitch=[200,200],
                unit_size=[100, 100], pins=inst0_pins, transform='R0')
        >>> print(inst0)
        <laygo2.object.physical.Instance object at 0x000001AF458AF8E0>
            name: I0,
            class: Instance,
            xy: [100, 100],
            params: None,
            size: [500, 300],
            shape: [3, 2],
            pitch: [200, 200],
            transform: R0,
            pins: {'in': <laygo2.object.physical.Pin object at 0x000001AF560D6170>,
                   'out': <laygo2.object.physical.Pin object at 0x000001AF560D5F30>},
        >>> print( inst0[1,0].xy0 )
        array([300, 100])

        .. image:: ../assets/img/object_physical_instance_init.png
          :height: 250

        """
        # Assign parameters.
        xy = np.asarray(xy)
        self.libname = libname
        self.cellname = cellname
        self.viewname = viewname
        if shape is not None:
            _shape = np.asarray(shape)
            if _shape.shape != (2,):
                raise ValueError("Instance shape should be a (2, ) numpy array or None.")
            self.shape = _shape
        if pitch is not None:
            self.pitch = np.asarray(pitch)
        self.transform = transform
        self.unit_size = np.asarray(unit_size)

        # Construct an array for elements.
        if shape is None:
            # elements = self  # self-referencing causes recursion errors.
            # (Deprecated).
            elements = None
        else:
            _shape = tuple(shape)
            elements = np.zeros(_shape, dtype=object)
            # elements = LayoutObjectArray(np.zeros(_shape, dtype=np.object))
            _it = np.nditer(elements, flags=["multi_index", "refs_ok"])
            while not _it.finished:
                _idx = _it.multi_index
                _xy = xy + np.dot(self.pitch * np.array(_idx), tf.Mt(self.transform).T)
                inst = Instance(
                    xy=_xy,
                    libname=libname,
                    cellname=cellname,
                    shape=None,
                    pitch=pitch,
                    transform=self.transform,
                    unit_size=self.unit_size,
                    pins=pins,
                    name=name,
                    params=params,
                )
                elements[_idx] = inst
                _it.iternext()

        IterablePhysicalObject.__init__(self, xy=xy, name=name, params=params, elements=elements)
        # Create the pin dictionary. Can we do the same thing without generating
        # these many Pin objects?
        self.pins = dict()
        if pins is not None:
            if not isinstance(pins, dict):
                raise ValueError("The pins parameter for Instance objects should be a dictionary.")
            for pn, p in pins.items():
                _xy0 = xy + np.dot(p.xy, tf.Mt(transform).T)
                if shape is not None:
                    elements = []
                    for i in range(shape[0]):
                        elements.append([])
                        for j in range(shape[1]):
                            _xy = _xy0 + np.dot(self.pitch * np.array([i, j]), tf.Mt(transform).T)
                            # If p has elements, they need to be copied and
                            # transferred to the new pin.
                            _pelem = None
                            if p.elements is not None:
                                _pelem = np.empty(p.elements.shape, dtype=object)
                                for _idx, _pe in np.ndenumerate(p.elements):
                                    _pexy0 = (
                                        xy
                                        + np.dot(_pe.xy, tf.Mt(transform).T)
                                        + np.dot(
                                            self.pitch * np.array([i, j]),
                                            tf.Mt(transform).T,
                                        )
                                    )
                                    _pelem[_idx] = Pin(
                                        xy=_pexy0,
                                        netname=_pe.netname,
                                        layer=_pe.layer,
                                        name=_pe.name,
                                        master=self,
                                    )
                            pin = Pin(
                                xy=_xy,
                                netname=p.netname,
                                layer=p.layer,
                                name=p.name,
                                master=self,
                                elements=_pelem,
                            )  # master uses self instead of self.elements[i, j].
                            elements[i].append(pin)
                    elements = np.array(elements)
                else:
                    # If p has elements, they need to be copied and transferred
                    # to the new pin.
                    _pelem = None
                    if p.elements is not None:
                        _pelem = np.empty(p.elements.shape, dtype=object)
                        for _idx, _pe in np.ndenumerate(p.elements):
                            _pexy0 = xy + np.dot(_pe.xy, tf.Mt(transform).T)
                            _pelem[_idx] = Pin(
                                xy=_pexy0,
                                netname=_pe.netname,
                                layer=_pe.layer,
                                name=_pe.name,
                                master=self,
                            )
                    elements = _pelem
                self.pins[pn] = Pin(
                    xy=_xy0,
                    netname=p.netname,
                    layer=p.layer,
                    name=p.name,
                    master=self,
                    elements=elements,
                )

    def summarize(self):
        """Return the summary of the object information."""
        _shape = str(None if self.shape is None else self.shape.tolist())
        return (
            PhysicalObject.summarize(self)
            + "    size: "
            + str(self.size.tolist())
            + ", \n"
            + "    shape: "
            + _shape
            + ", \n"
            + "    pitch: "
            + str(self.pitch.tolist())
            + ", \n"
            + "    transform: "
            + str(self.transform)
            + ", \n"
            + "    pins: "
            + str(self.pins)
            + ", \n"
        )

    def update_netname(self, netmap: dict):
        """
        Update the netname information for all pins belonging 
        to this object.
        """
        for pn, p in self.pins.items():
            if p.netname in netmap:
                p.netname = netmap[p.netname]  # update netname information


class VirtualInstance(Instance):  # IterablePhysicalObject):
    """
    The VirtualInstance class implements functions for a group of objects 
    to be treated as a single instance with dedicated dimensional, 
    port, and related parameters.

    Example
    -------
    >>> import laygo2
    >>> vinst0_pins = dict()
    >>> # Pin information
    >>> vinst0_pins['in'] = laygo2.object.physical.Pin(xy=[[0, 0], [10, 10]],
            layer=['M1', 'drawing'], netname='in')
    >>> vinst0_pins['out'] = laygo2.object.physical.Pin(xy=[[90, 90], [100, 100]],
            layer=['M1', 'drawing'], netname='out')
    >>> # Element information
    >>> native_elements = dict()
    >>> native_elements['R0'] = laygo2.object.physical.Rect(xy=[[0, 0], [10, 10]],
            layer=['M1', 'drawing'])
    >>> native_elements['R1'] = laygo2.object.physical.Rect(xy=[[90, 90], [100, 100]],
            layer=['M1', 'drawing'])
    >>> native_elements['R2'] = laygo2.object.physical.Rect(xy=[[0, 0], [100, 100]],
            layer=['prBoundary', 'drawing'])
    >>> vinst0 = laygo2.object.physical.VirtualInstance(name='I0', libname='mylib',
            cellname='myvcell', xy=[500, 500], native_elements=native_elements,
            shape=[3, 2], pitch=[100, 100], unit_size=[100, 100], pins=vinst0_pins,
            transform='R0')
    >>> vinst0.native_elements
    {'R0': <laygo2.object.physical.Rect object at 0x00000204AAFCE170>,
     'R1': <laygo2.object.physical.Rect object at 0x00000204AAFCEA40>,
     'R2': <laygo2.object.physical.Rect object at 0x00000204AAFCE0B0>}

    """

    native_elements = None
    """
    dict: Dictionary that holds the physical element entities 
        of the VirtualInstance object.

    Example
    -------
    >>> import laygo2
    >>> vinst0_pins = dict() 
    >>> # Pin information
    >>> vinst0_pins['in'] = laygo2.object.physical.Pin(xy=[[0, 0], [10, 10]], 
            layer=['M1', 'drawing'], netname='in') 
    >>> vinst0_pins['out'] = laygo2.object.physical.Pin(xy=[[90, 90], [100, 100]], 
            layer=['M1', 'drawing'], netname='out')
    >>> # Element information
    >>> native_elements = dict() 
    >>> native_elements['R0'] = laygo2.object.physical.Rect(xy=[[0, 0], [10, 10]], 
            layer=['M1', 'drawing']) 
    >>> native_elements['R1'] = laygo2.object.physical.Rect(xy=[[90, 90], [100, 100]], 
            layer=['M1', 'drawing']) 
    >>> native_elements['R2'] = laygo2.object.physical.Rect(xy=[[0, 0], [100, 100]], 
            layer=['prBoundary', 'drawing'])
    >>> vinst0 = laygo2.object.physical.VirtualInstance(name='I0', libname='mylib', 
            cellname='myvcell', xy=[500, 500], native_elements=native_elements, 
            shape=[3, 2], pitch=[100, 100], unit_size=[100, 100], pins=vinst0_pins, 
            transform='R0')
    >>> vinst0.native_elements 
    {'R0': <laygo2.object.physical.Rect object at 0x00000204AAFCE170>, 
     'R1': <laygo2.object.physical.Rect object at 0x00000204AAFCEA40>, 
     'R2': <laygo2.object.physical.Rect object at 0x00000204AAFCE0B0>}

    .. image:: ../assets/img/object_physical_VirtualInstance_native_elements.png
      :height: 250

    """
    # Dict[PhysicalObject] the elements that compose the virtual instance. Its keys represent the names of the elements.

    def __init__(
        self,
        xy,
        libname,
        cellname,
        native_elements,
        viewname="layout",
        shape=None,
        pitch=None,
        transform="R0",
        unit_size=np.array([0, 0]),
        pins=None,
        name=None,
        params=None,
    ):
        """
        The constructor function.

        Parameters
        ----------
        xy : numpy.ndarray
            Physical coordinate values of the object
            in the form of [bottom_left, top_right].
        libname : str
            The library name of the object.
        cellname : str
            The cell name of the object.
        native_elements : dict
            Dictionary that holds the physical element entities 
            of the VirtualInstance object.
        shape : numpy.ndarray
            The shape [col, row] of the elements.
        pitch : numpy.ndarray
            The pitch between unit objects in the array.
        transform : str
            The attribute of the object that defines its transformation 
            (rotation and mirroring).
        unit_size : list
            Unit size of object.
        pins : dict
            The array unit size for the object.
        name : str
            Object name.
        params : dict
            (optional) Dictionary storing the parameters associated with 
            the object.

        Returns
        -------
        laygo2.VirtualInstance : The constructed VirtualInstance object.

        See Also
        --------
        Instance

        Example
        -------
        >>> import laygo2
        >>> vinst0_pins = dict()
        >>> # Pin information
        >>> vinst0_pins['in'] = laygo2.object.physical.Pin(xy=[[0, 0], [10, 10]],
                layer=['M1', 'drawing'], netname='in')
        >>> vinst0_pins['out'] = laygo2.object.physical.Pin(xy=[[90, 90], [100, 100]],
                layer=['M1', 'drawing'], netname='out')
        >>> # Element information
        >>> native_elements = dict()
        >>> native_elements['R0'] = laygo2.object.physical.Rect(xy=[[0, 0], [10, 10]],
                layer=['M1', 'drawing'])
        >>> native_elements['R1'] = laygo2.object.physical.Rect(xy=[[90, 90], [100, 100]],
                layer=['M1', 'drawing'])
        >>> native_elements['R2'] = laygo2.object.physical.Rect(xy=[[0, 0], [100, 100]],
                layer=['prBoundary', 'drawing'])
        >>> vinst0 = laygo2.object.physical.VirtualInstance(name='I0', libname='mylib',
                cellname='myvcell', xy=[500, 500], native_elements=native_elements,
                shape=[3, 2], pitch=[100, 100], unit_size=[100, 100], pins=vinst0_pins,
                transform='R0')
        >>> vinst0.native_elements
        {'R0': <laygo2.object.physical.Rect object at 0x00000204AAFCE170>,
         'R1': <laygo2.object.physical.Rect object at 0x00000204AAFCEA40>,
         'R2': <laygo2.object.physical.Rect object at 0x00000204AAFCE0B0>}

        .. image:: ../assets/img/object_physical_VirtualInstance_init.png
          :height: 250

        """
        self.native_elements = native_elements

        Instance.__init__(
            self,
            xy=xy,
            libname=libname,
            cellname=cellname,
            viewname=viewname,
            shape=shape,
            pitch=pitch,
            transform=transform,
            unit_size=unit_size,
            pins=pins,
            name=name,
            params=params,
        )

    def summarize(self):
        """Return the summary of the object information."""
        return Instance.summarize(self) + "    native elements: " + str(self.native_elements) + "\n"

    def get_element_position(self, obj):
        """
        Get x,y coordinates of the element obj (which belongs to the object)
        relative to the origin (0, 0).

        Parameters
        ----------
        obj : element
            The element belongs to the object.
        """
        vinst = self
        tr = vinst.transform
        coners = np.zeros((4, 2))
        v_r = np.zeros(2)  # for rotation
        bbox_raw = obj.bbox
        offset = vinst.xy
        if tr == "R0":
            v_r = v_r + (1, 1)
            coners[0] = offset + v_r * bbox_raw[0]
            coners[2] = offset + v_r * bbox_raw[1]
        elif tr == "MX":
            v_r = v_r + (1, -1)
            coners[1] = offset + v_r * bbox_raw[0]
            coners[3] = offset + v_r * bbox_raw[1]
            coners[0] = coners[0] + (coners[1][0], coners[3][1])
            coners[2] = coners[2] + (coners[3][0], coners[1][1])
        elif tr == "MY":
            v_r = v_r + (-1, 1)
            coners[3] = offset + v_r * bbox_raw[0]
            coners[1] = offset + v_r * bbox_raw[1]
            coners[0] = coners[0] + (coners[1][0], coners[3][1])
            coners[2] = coners[2] + (coners[3][0], coners[1][1])
        elif tr == "R90":
            v_r = v_r + (-1, -1)
            coners[2] = offset + v_r * bbox_raw[0]
            coners[0] = offset + v_r * bbox_raw[1]
        else:
            raise ValueError(" Others transfom not implemented")
        return coners[0], coners[2]

class Via(IterablePhysicalObject):
    """
    Via object class.

    Attributes
    ----------
    netname : str
    color : str
    size : np.array(dtype=np.int)

    Methods
    -------
    """

    netname = None
    size = np.array([0,0])
    unit_size = None
    _pitch = None
    transform = 'R0'
    color = None
    
    def _get_xy(self):
        """numpy.ndarray(dtype=numpy.int): Get the x and y coordinate values of this object."""
        return self._xy

    def _set_xy(self, value):
        """numpy.ndarray(dtype=numpy.int): Set the x and y coordinate values of this object."""
        # Update the coordinate value of its pins.
        IterablePhysicalObject._set_xy(self, value=value)

    xy = property(_get_xy, _set_xy)
    
    @property
    def xy0(self):
        """attribute
        numpy.ndarray: Coordinates of major corner of object.

        

        Examples
        --------
       
        """
        return self.xy

    @property
    def xy1(self):
        """attribute
        numpy.ndarray: Coordinates of minor corner of object.

        Examples
        --------

        """
        if self.size is None:
            return self.xy
        else:
            return self.xy + np.dot(self.size, tf.Mt(self.transform).T)

    @property
    def bbox(self):
        bbox = np.array([self.xy0, self.xy1])
        #return bbox
        #return self.xy + np.dot(self.size, tf.Mt(self.transform).T)
        return np.sort(bbox, axis=0)
     
    def __init__(self, xy, color=None, name=None, netname=None, params=None, transform= 'R0',elements=None):

        xy = np.asarray(xy)
        if netname is None:
            self.netname = name
        else:
            self.netname = netname
        self.color = color
        IterablePhysicalObject.__init__(self, xy=xy, name=name, params=params, elements=elements)


    def summarize(self):
        """Return the summary of the object information."""
        return IterablePhysicalObject.summarize(self) + ", " + \
                                              "netname: " + str(self.netname)


# Test
if __name__ == "__main__":
    test_rect = False
    test_path = False
    test_pin = False
    test_text = False
    test_pointer = False
    test_instance = True
    test_virtual_instance = False

    # You can create various objects by running part of the following commands.
    if test_rect:
        print("Rect test")
        rect0 = Rect(
            xy=[[0, 0], [100, 100]],
            layer=["M1", "drawing"],
            netname="net0",
            params={"maxI": 0.005},
        )
        print(rect0)
    if test_path:
        print("Path test")
        path0 = Path(
            xy=[[0, 0], [0, 100]],
            width=10,
            extension=5,
            layer=["M1", "drawing"],
            netname="net0",
        )
        print(path0)
    if test_pin:
        print("Pin test")
        pin0 = Pin(
            xy=[[0, 0], [100, 100]],
            layer=["M1", "drawing"],
            netname="net0",
            master=rect0,
            params={"direction": "input"},
        )
        print(pin0)
    if test_text:
        print("Text test")
        text0 = Text(xy=[0, 0], layer=["text", "drawing"], text="test", params=None)
        print(text0)
    if test_instance:
        print("Instance test - creating a vanilla instance.")
        inst0_pins = dict()
        inst0_pins["in"] = Pin(xy=[[0, 0], [10, 10]], layer=["M1", "drawing"], netname="in")
        inst0_pins["out"] = Pin(xy=[[90, 90], [100, 100]], layer=["M1", "drawing"], netname="out")
        inst0 = Instance(
            name="I0",
            xy=[100, 100],
            libname="mylib",
            cellname="mycell",
            shape=[3, 2],
            pitch=[100, 100],
            unit_size=[100, 100],
            pins=inst0_pins,
            transform="R0",
        )
        print("  ", inst0)
        print("  ", inst0.pointers)
        print(inst0.elements)
        for idx, it in inst0.ndenumerate():
            print("what?")
            print("  ", idx, it)
            print("  ", idx, it.pins["in"])
        print("Instance test - updating the instance's coordinate values.")
        inst0.xy = [200, 200]
        print("  ", inst0)
        print("  ", inst0.pointers)
        for idx, it in inst0.ndenumerate():
            print("  ", idx, it)
            print("  ", idx, it.pins["in"])
    if test_virtual_instance:
        print("VirtualInstance test - creating a vanilla virtual instance.")
        inst1_pins = dict()
        inst1_pins["in"] = Pin(xy=[[0, 0], [10, 10]], layer=["M1", "drawing"], netname="in")
        inst1_pins["out"] = Pin(xy=[[90, 90], [100, 100]], layer=["M1", "drawing"], netname="out")
        inst1_native_elements = dict()
        inst1_native_elements["R0"] = Rect(xy=[[0, 0], [10, 10]], layer=["M1", "drawing"])
        inst1_native_elements["R1"] = Rect(xy=[[90, 90], [100, 100]], layer=["M1", "drawing"])
        inst1_native_elements["R2"] = Rect(xy=[[0, 0], [100, 100]], layer=["prBoundary", "drawing"])
        inst1 = VirtualInstance(
            name="I0",
            libname="mylib",
            cellname="myvcell",
            xy=[500, 500],
            native_elements=inst1_native_elements,
            shape=[3, 2],
            pitch=[100, 100],
            unit_size=[100, 100],
            pins=inst1_pins,
            transform="R0",
        )
        print("  ", inst1)
        for idx, it in inst1.ndenumerate():
            print("  ", idx, it.pins["in"])
        for idx, it in inst1.pins["in"].ndenumerate():
            print("  ", idx, it)
