# -*- coding: utf-8 -*-
# last edit: 11.04.2018
'''
Library for plotting fully automatic a Smith Chart with various customizable
parameters and well selected default values. It also provides the following 
modifications and features:

    - circle shaped drawing area with labels placed around 
    - :meth:`plot` accepts single real and complex numbers and numpy.ndarray's
    - plotted lines can be interpolated
    - start/end markers of lines can be modified and rotate tangential
    - gridlines are 3-point arcs to improve space efficiency of exported plots
    - 'fancy' option for adaptive grid generation
    - own tick locators for nice axis labels

For making a Smith Chart plot it is sufficient to import :mod:`smithplot` and
create a new subplot with projection set to 'smith'. Parameters can be set 
either with keyword arguments or :meth:`update_Params`.

Example:

    # creating a new plot and modify parameters afterwards
    import smithplot
    from smithplot import SmithAxes
    from matplotlib import pyplot as pp
    ax = pp.subplot('111', projection='smith')
    SmithAxes.update_scParams(ax, reset=True, grid_major_enable=False)
    ## or in short form direct
    #ax = pp.subplot('111', projection='smith', grid_major_enable=False)
    pp.plot([25, 50 + 50j, 100 - 50j], datatype=SmithAxes.Z_PARAMETER)
    pp.show()
    
Note: Supplying parameters to :meth:`subplot` may not always work as
expected, because subplot uses an index for the axes with a key created
of all given parameters. This does not work always, especially if the
parameters are array-like types (e.g. numpy.ndarray).
'''

from collections import Iterable
from numbers import Number
from types import MethodType, FunctionType

import matplotlib as mp
import numpy as np
from matplotlib.axes import Axes
from matplotlib.axis import XAxis
from matplotlib.cbook import simple_linear_interpolation as linear_interpolation
from matplotlib.legend_handler import HandlerLine2D
from matplotlib.lines import Line2D
from matplotlib.markers import MarkerStyle
from matplotlib.patches import Circle, Arc
from matplotlib.path import Path
from matplotlib.spines import Spine
from matplotlib.ticker import Formatter, AutoMinorLocator, Locator
from matplotlib.transforms import Affine2D, BboxTransformTo, Transform
from scipy.interpolate import fitpack

from . import smithhelper
from .smithhelper import EPSILON, TWO_PI, ang_to_c, z_to_xy


class SmithAxes(Axes):
    '''
    The :class:`SmithAxes` provides an implementation of :class:`matplotlib.axes.Axes`
    for drawing a full automatic Smith Chart it also provides own implementations for
     
        - :class:`matplotlib.transforms.Transform`
            -> :class:`MoebiusTransform`
            -> :class:`InvertedMoebiusTransform`
            -> :class:`PolarTranslate`
        - :class:`matplotlib.ticker.Locator`
            -> :class:`RealMaxNLocator`
            -> :class:`ImagMaxNLocator`
            -> :class:`SmithAutoMinorLocator`
        - :class:`matplotlib.ticker.Formatter`
            -> :class:`RealFormatter`
            -> :class:`ImagFormatter`
    '''

    name = 'smith'

    # data types
    S_PARAMETER = "S"
    Z_PARAMETER = "Z"
    Y_PARAMETER = "Y"
    _datatypes = [S_PARAMETER, Z_PARAMETER, Y_PARAMETER]

    # constants used for indicating values near infinity, which are all transformed into one point
    _inf = smithhelper.INF
    _near_inf = 0.9 * smithhelper.INF
    _ax_lim_x = 2 * _inf  # prevents missing labels in special cases
    _ax_lim_y = 2 * _inf  # prevents missing labels in special cases

    # default parameter, see update_scParams for description
    scDefaultParams = {"plot.zorder": 4,
                       "plot.marker.hack": True,
                       "plot.marker.rotate": True,
                       "plot.marker.start": "s",
                       "plot.marker.default": "o",
                       "plot.marker.end": "^",
                       "plot.default.interpolation": 5,
                       "plot.default.datatype": S_PARAMETER,
                       "grid.zorder": 1,
                       "grid.locator.precision": 2,
                       "grid.major.enable": True,
                       "grid.major.linestyle": '-',
                       "grid.major.linewidth": 1,
                       "grid.major.color": "0.2",
                       "grid.major.xmaxn": 10,
                       "grid.major.ymaxn": 16,
                       "grid.major.fancy": True,
                       "grid.major.fancy.threshold": (100, 50),
                       "grid.minor.enable": True,
                       "grid.minor.capstyle": "round",
                       "grid.minor.dashes": [0.2, 2],
                       "grid.minor.linewidth": 0.75,
                       "grid.minor.color": "0.4",
                       "grid.minor.xauto": 4,
                       "grid.minor.yauto": 4,
                       "grid.minor.fancy": True,
                       "grid.minor.fancy.dividers": [0, 1, 2, 3, 5, 10, 20],
                       "grid.minor.fancy.threshold": 35,
                       "axes.xlabel.rotation": 90,
                       "axes.xlabel.fancybox": {"boxstyle": "round,pad=0.2,rounding_size=0.2",
                                                "facecolor": 'w',
                                                "edgecolor": "w",
                                                "mutation_aspect": 0.75,
                                                "alpha": 1},
                       "axes.ylabel.correction": (-1, 0, 0),
                       "axes.radius": 0.44,
                       "axes.impedance": 50,
                       "axes.normalize": True,
                       "axes.normalize.label": True,
                       "symbol.infinity": "∞ ",  # BUG: symbol gets cut off without end-space
                       "symbol.infinity.correction": 8,
                       "symbol.ohm": "Ω"}

    @staticmethod
    def update_scParams(sc_dict=None, instance=None, filter_dict=False, reset=True, **kwargs):
        '''
        Method for updating the parameters of a SmithAxes instance. If no instance
        is given, the changes are global, but affect only instances created
        afterwards. Parameter can be passed as dictionary or keyword arguments.
        If passed as keyword, the seperator '.' must be  replaced with '_'.

        Note: Parameter changes are not always immediate (e.g. changes to the
        grid). It is not recommended to modify parameter after adding anything to
        the plot. For a reset call :meth:`cla`.

        Example:
            update_scParams({grid.major: True})
            update_scParams(grid_major=True)

        Valid parameters with default values and description:

            plot.zorder: 5
                Zorder of plotted lines.
                Accepts: integer

            plot.marker.hack: True
                Enables the replacement of start and endmarkers.
                Accepts: boolean
                Note: Uses ugly code injection and may causes unexpected behavior.

            plot.marker.rotate: True
                Rotates the endmarker in the direction of its line.
                Accepts: boolean
                Note: needs plot.marker.hack=True

            plot.marker.start: 's',
                Marker for the first point of a line, if it has more than 1 point.
                Accepts: None or see matplotlib.markers.MarkerStyle()
                Note: needs plot.marker.hack=True

            plot.marker.default: 'o'
                Marker used for linepoints.
                Accepts: None or see matplotlib.markers.MarkerStyle()

            plot.marker.end: '^',
                Marker for the last point of a line, if it has more than 1 point.
                Accepts: None or see matplotlib.markers.MarkerStyle()
                Note: needs plot.marker.hack=True

            plot.default.interpolation: 5
                Default number of interpolated steps between two points of a
                line, if interpolation is used.
                Accepts: integer

            plot.default.datatype: SmithAxes.S_PARAMETER
                Default datatype for plots.
                Accepts: SmithAxes.[S_PARAMETER,Z_PARAMETER,Y_PARAMETER]

            grid.zorder : 1
                Zorder of the gridlines.
                Accepts: integer
                Note: may not work as expected

            grid.locator.precision: 2
                Sets the number of significant decimals per decade for the
                Real and Imag MaxNLocators. Example with precision 2:
                    1.12 -> 1.1, 22.5 -> 22, 135 -> 130, ...
                Accepts: integer
                Note: value is an orientation, several exceptions are implemented

            grid.major.enable: True
                Enables the major grid.
                Accepts: boolean

            grid.major.linestyle: 'solid'
                Major gridline style.
                Accepts: see matplotlib.patches.Patch.set_linestyle()

            grid.major.linewidth: 1
                Major gridline width.
                Accepts: float

            grid.major.color: '0.2'
                Major gridline color.
                Accepts: matplotlib color

            grid.major.xmaxn: 10
                Maximum number of spacing steps for the real axis.
                Accepts: integer

            grid.major.ymaxn: 16
                Maximum number of spacing steps for the imaginary axis.
                Accepts: integer

            grid.major.fancy: True
                Draws a fancy major grid instead of the standard one.
                Accepts: boolean

            grid.major.fancy.threshold: (100, 50)
                Minimum distance times 1000 between two gridlines relative to
                total plot size 2x2. Either tuple for individual real and
                imaginary distances or single value for both.
                Accepts: (float, float) or float

            grid.minor.enable: True
                Enables the minor grid.
                Accepts: boolean

            grid.minor.capstyle: 'round'
                Minor dashes capstyle
                Accepts: 'round', 'butt', 'miter', 'projecting'

            grid.minor.dashes: (0.2, 2)
                Minor gridline dash style.
                Accepts: tuple

            grid.minor.linewidth: 0.75
                Minor gridline width.
                Accepts: float

            grid.minor.color: 0.4
                Minor gridline color.
                Accepts: matplotlib color

            grid.minor.xauto: 4
                Maximum number of spacing steps for the real axis.
                Accepts: integer

            grid.minor.yauto: 4
                Maximum number of spacing steps for the imaginary axis.
                Accepts: integer

            grid.minor.fancy: True
                Draws a fancy minor grid instead the standard one.
                Accepts: boolean

            grid.minor.fancy.dividers: [1, 2, 3, 5, 10, 20]
                Divisions for the fancy minor grid, which are selected by
                comparing the distance of gridlines with the threshold value.
                Accepts: list of integers

            grid.minor.fancy.threshold: 25
                Minimum distance for using the next bigger divider. Value times
                1000 relative to total plot size 2.
                Accepts: float

            axes.xlabel.rotation: 90
               Rotation of the real axis labels in degree.
               Accepts: float

            axes.xlabel.fancybox: {"boxstyle": "round4,pad=0.3,rounding_size=0.2",
                                               "facecolor": 'w',
                                               "edgecolor": "w",
                                               "mutation_aspect": 0.75,
                                               "alpha": 1},
                FancyBboxPatch parameters for the x-label background box.
                Accepts: dictionary with rectprops

            axes.ylabel.correction: (-1, 0, 0)
                Correction in x, y, and radial direction for the labels of the imaginary axis.
                Usually needs to be adapted when fontsize changes 'font.size'.
                Accepts: (float, float, float)

            axes.radius: 0.44
                Radius of the plotting area. Usually needs to be adapted to
                the size of the figure.
                Accepts: float

            axes.impedance: 50
                Defines the reference impedance for normalisation.
                Accepts: float

            axes.normalize: True
                If True, the Smith Chart is normalized to the reference impedance.
                Accepts: boolean

            axes.normalize.label: True
                If 'axes.normalize' and True, a textbox with 'Z_0: ... Ohm' is put in
                the lower left corner.
                Accepts: boolean

            symbol.infinity: "∞ "
                Symbol string for infinity.
                Accepts: string

                Note: Without the trailing space the label might get cut off.

            symbol.infinity.correction: 8
                Correction of size for the infinity symbol, because normal symbol
                seems smaller than other letters.
                Accepts: float

            symbol.ohm "Ω"
                Symbol string for the resistance unit (usually a large Omega).
                Accepts: string

        Note: The keywords are processed after the dictionary and override
        possible double entries.
        '''
        scParams = SmithAxes.scDefaultParams if instance is None else instance.scParams

        if sc_dict is not None:
            for key, value in sc_dict.items():
                if key in scParams:
                    scParams[key] = value
                else:
                    raise KeyError("key '%s' is not in scParams" % key)

        remaining = kwargs.copy()
        for key in kwargs:
            key_dot = key.replace("_", ".")
            if key_dot in scParams:
                scParams[key_dot] = remaining.pop(key)

        if not filter_dict and len(remaining) > 0:
            raise KeyError("Following keys are invalid SmithAxes parameters: '%s'" % ",".join(remaining.keys()))

        if reset and instance is not None:
            instance.cla()

        if filter_dict:
            return remaining

    def __init__(self, *args, **kwargs):
        '''
        Builds a new :class:`SmithAxes` instance. For futher details see:
        
            :meth:`update_scParams`
            :class:`matplotlib.axes.Axes`
        '''
        # define new class attributes
        self._majorarcs = None
        self._minorarcs = None
        self._impedance = None
        self._normalize = None
        self._current_zorder = None
        self.scParams = self.scDefaultParams.copy()

        # seperate Axes parameter
        Axes.__init__(self, *args, **SmithAxes.update_scParams(instance=self, filter_dict=True, reset=False, **kwargs))
        self.set_aspect(1, adjustable='box', anchor='C')

        # remove all ticks
        self.tick_params(axis="both", which="both", bottom=False, top=False, left=False, right=False)

    def _get_key(self, key):
        '''
        Get a key from the local parameter dictionary or from global 
        matplotlib rcParams.
        
        Keyword arguments:
            
            *key*:
                Key to get from scParams or matplotlib.rcParams
                Accepts: string
            
        Returns:
        
            *value*:
                Value got from scParams or rcParams with key
        '''
        if key in self.scParams:
            return self.scParams[key]
        elif key in mp.rcParams:
            return mp.rcParams[key]
        else:
            raise KeyError("%s is not a valid key" % key)

    def _init_axis(self):
        self.xaxis = mp.axis.XAxis(self)
        self.yaxis = mp.axis.YAxis(self)
        self._update_transScale()

    def cla(self):
        self._majorarcs = []
        self._minorarcs = []

        # deactivate grid function when calling base class
        tgrid = self.grid

        def dummy(*args, **kwargs):
            pass

        self.grid = dummy
        # Don't forget to call the base class
        Axes.cla(self)
        self.grid = tgrid

        self._normbox = None
        self._impedance = self._get_key("axes.impedance")
        self._normalize = self._get_key("axes.normalize")
        self._current_zorder = self._get_key("plot.zorder")

        self.xaxis.set_major_locator(self.RealMaxNLocator(self, self._get_key("grid.major.xmaxn")))
        self.yaxis.set_major_locator(self.ImagMaxNLocator(self, self._get_key("grid.major.ymaxn")))

        self.xaxis.set_minor_locator(self.SmithAutoMinorLocator(self._get_key("grid.minor.xauto")))
        self.yaxis.set_minor_locator(self.SmithAutoMinorLocator(self._get_key("grid.minor.yauto")))

        self.xaxis.set_ticks_position('none')
        self.yaxis.set_ticks_position('none')

        Axes.set_xlim(self, 0, self._ax_lim_x)
        Axes.set_ylim(self, -self._ax_lim_y, self._ax_lim_y)

        for label in self.get_xticklabels():
            label.set_verticalalignment("center")
            label.set_horizontalalignment('center')
            label.set_rotation_mode("anchor")
            label.set_rotation(self._get_key("axes.xlabel.rotation"))
            label.set_bbox(self._get_key("axes.xlabel.fancybox"))
            self.add_artist(label)  # if not readded, labels are drawn behind grid

        for tick, loc in zip(self.yaxis.get_major_ticks(),
                             self.yaxis.get_majorticklocs()):
            # workaround for fixing to small infinity symbol
            if abs(loc) > self._near_inf:
                tick.label.set_size(tick.label.get_size() +
                                    self._get_key("symbol.infinity.correction"))

            tick.label.set_verticalalignment('center')

            x = np.real(self._moebius_z(loc * 1j))
            if x < -0.1:
                tick.label.set_horizontalalignment('right')
            elif x > 0.1:
                tick.label.set_horizontalalignment('left')
            else:
                tick.label.set_horizontalalignment('center')

        self.yaxis.set_major_formatter(self.ImagFormatter(self))
        self.xaxis.set_major_formatter(self.RealFormatter(self))

        if self._get_key("axes.normalize") and self._get_key("axes.normalize.label"):
            x, y = z_to_xy(self._moebius_inv_z(-1 - 1j))
            box = self.text(x, y, "Z$_\mathrm{0}$ = %d$\,$%s" % (self._impedance, self._get_key("symbol.ohm")), ha="left", va="bottom")

            px = self._get_key("ytick.major.pad")
            py = px + 0.5 * box.get_size()
            box.set_transform(self._yaxis_correction + Affine2D().translate(-px, -py))

        for grid in ['major', "minor"]:
            self.grid(b=self._get_key("grid.%s.enable" % grid), which=grid)

    def _set_lim_and_transforms(self):
        r = self._get_key("axes.radius")
        self.transProjection = self.MoebiusTransform(self)  # data space  -> moebius space
        self.transAffine = Affine2D().scale(r, r).translate(0.5, 0.5)  # moebius space -> axes space
        self.transDataToAxes = self.transProjection + self.transAffine
        self.transAxes = BboxTransformTo(self.bbox)  # axes space -> drawing space
        self.transMoebius = self.transAffine + self.transAxes
        self.transData = self.transProjection + self.transMoebius

        self._xaxis_pretransform = Affine2D().scale(1, 2 * self._ax_lim_y).translate(0, -self._ax_lim_y)
        self._xaxis_transform = self._xaxis_pretransform + self.transData
        self._xaxis_text1_transform = Affine2D().scale(1.0, 0.0) + self.transData

        self._yaxis_stretch = Affine2D().scale(self._ax_lim_x, 1.0)
        self._yaxis_correction = self.transData + Affine2D().translate(*self._get_key("axes.ylabel.correction")[:2])
        self._yaxis_transform = self._yaxis_stretch + self.transData
        self._yaxis_text1_transform = self._yaxis_stretch + self._yaxis_correction

    def get_xaxis_transform(self, which='grid'):
        assert which in ['tick1', 'tick2', 'grid']
        return self._xaxis_transform

    def get_xaxis_text1_transform(self, pixelPad):
        return self._xaxis_text1_transform, 'center', 'center'

    def get_yaxis_transform(self, which='grid'):
        assert which in ['tick1', 'tick2', 'grid']
        return self._yaxis_transform

    def get_yaxis_text1_transform(self, pixelPad):
        if hasattr(self, 'yaxis') and len(self.yaxis.majorTicks) > 0:
            font_size = self.yaxis.majorTicks[0].label.get_size()
        else:
            font_size = self._get_key("font.size")

        offset = self._get_key("axes.ylabel.correction")[2]
        return self._yaxis_text1_transform + self.PolarTranslate(self, pad=pixelPad + offset, font_size=font_size), 'center', 'center'

    def _gen_axes_patch(self):
        return Circle((0.5, 0.5), self._get_key("axes.radius") + 0.015)

    def _gen_axes_spines(self, locations=None, offset=0.0, units='inches'):
        return {SmithAxes.name: Spine.circular_spine(self, (0.5, 0.5), self._get_key("axes.radius"))}

    def set_xscale(self, *args, **kwargs):
        if args[0] != 'linear':
            raise NotImplementedError()
        Axes.set_xscale(self, *args, **kwargs)

    def set_yscale(self, *args, **kwargs):
        if args[0] != 'linear':
            raise NotImplementedError()
        Axes.set_yscale(self, *args, **kwargs)

    def set_xlim(self, *args, **kwargs):
        '''xlim is immutable and always set to (0, infinity)'''
        Axes.set_xlim(self, 0, self._ax_lim_x)

    def set_ylim(self, *args, **kwargs):
        '''ylim is immutable and always set to (-infinity, infinity)'''
        Axes.set_ylim(self, -self._ax_lim_y, self._ax_lim_y)

    def format_coord(self, re, im):
        sgn = "+" if im > 0 else "-"
        return "%.5f %s %.5fj" % (re, sgn, abs(im)) if re > 0 else ""

    def get_data_ratio(self):
        return 1.0

    # disable panning and zoom in matplotlib figure viewer
    def can_zoom(self):
        return False

    def start_pan(self, x, y, button):
        pass

    def end_pan(self):
        pass

    def drag_pan(self, button, key, x, y):
        pass

    def _moebius_z(self, *args, normalize=None):
        '''
        Basic transformation. 
        
        Arguments:

            *z*: 
                Complex number or numpy.ndarray with dtype=complex

            *x, y*:
                Float numbers or numpy.ndarray's with dtype not complex

            *normalize*:
                If True, the values are normalized to self._impedance.
                If None, self._normalize determines behaviour.
                Accepts: boolean or None
                
        Returns:

            *w*:
                Performs w = (z - k) / (z + k) with k = 'axes.scale' 
                Type: Complex number or numpy.ndarray with dtype=complex
        '''
        normalize = self._normalize if normalize is None else normalize
        norm = 1 if normalize else self._impedance
        return smithhelper.moebius_z(*args, norm=norm)

    def _moebius_inv_z(self, *args, normalize=None):
        '''
        Basic inverse transformation. 
        
        Arguments:

            *z*: 
                Complex number or numpy.ndarray with dtype=complex

            *x, y*:
                Float numbers or numpy.ndarray's with dtype not complex

            *normalize*:
                If True, the values are normalized to self._impedance.
                If None, self._normalize determines behaviour.
                Accepts: boolean or None

        Returns:

            *w*:
                Performs w = k * (1 - z) / (1 + z) with k = 'axes.scale' 
                Type: Complex number or numpy.ndarray with dtype=complex
        '''
        normalize = self._normalize if normalize is None else normalize
        norm = 1 if normalize else self._impedance
        return smithhelper.moebius_inv_z(*args, norm=norm)

    def real_interp1d(self, x, steps):
        '''
        Interpolates the given vector as real numbers in the way, that they 
        are evenly spaced after a transformation with imaginary part 0.
        
        Keyword Arguments
        
            *x*:
                Real values to interpolate.
                Accepts: 1D iterable (e.g. list or numpy.ndarray)
                
            *steps*:
                Number of steps between two points.
                Accepts: integer
        '''
        return self._moebius_inv_z(linear_interpolation(self._moebius_z(np.array(x)), steps))

    def imag_interp1d(self, y, steps):
        '''
        Interpolates the given vector as imaginary numbers in the way, that 
        they are evenly spaced after a transformation with real part 0.
        
        Keyword Arguments
        
            *y*:
                Imaginary values to interpolate.
                Accepts: 1D iterable (e.g. list or numpy.ndarray)

            *steps*:
                Number of steps between two points.
                Accepts: integer
        '''
        angs = np.angle(self._moebius_z(np.array(y) * 1j)) % TWO_PI
        i_angs = linear_interpolation(angs, steps)
        return np.imag(self._moebius_inv_z(ang_to_c(i_angs)))

    def legend(self, *args, **kwargs):
        this_axes = self

        class SmithHandlerLine2D(HandlerLine2D):
            def create_artists(self, legend, orig_handle,
                               xdescent, ydescent, width, height, fontsize,
                               trans):
                legline, legline_marker = HandlerLine2D.create_artists(self, legend, orig_handle, xdescent, ydescent,
                                                                       width, height, fontsize, trans)

                if hasattr(orig_handle, "_markerhacked"):
                    this_axes._hack_linedraw(legline_marker, True)
                return legline, legline_marker

        return Axes.legend(self, *args, handler_map={Line2D: SmithHandlerLine2D()}, **kwargs)

    def plot(self, *args, **kwargs):
        '''
        Plot the given data into the Smith Chart. Behavior similar to basic 
        :meth:`matplotlib.axes.Axes.plot`, but with some extensions:
        
            - Additional support for real and complex data. Complex values must be
            either of type 'complex' or a numpy.ndarray with dtype=complex.
            - If 'zorder' is not provided, the current default value is used.
            - If 'marker' is not providet, the default value is used.
            - Extra keywords are added.
        
        Extra keyword arguments:
        
            *datatype*:
                Specifies the input data format. Must be either 'S', 'Z' or 'Y'.
                Accepts: SmithAxes.[S_PARAMETER,Z_PARAMETER,Y_PARAMETER]
                Default: 'plot.default.datatype'
                
            *markerhack*:
                If set, activates the manipulation of start and end markern 
                of the created line.
                Accepts: boolean
                Default: 'plot.marker.hack'
                
            *rotate_marker*:
                If *markerhack* is active, rotates the endmarker in direction
                of the corresponding path.
                Accepts: boolean
                Default: 'plot.rotatemarker'

            *interpolate*:
                If 'value' >0 the given data is interpolated linearly by 'value'
                steps in SmithAxes cooardinate space. 'markevery', if specified,
                will be modified accordingly. If 'True' the 'plot.default_intperpolation'
                value is used.
                Accepts: boolean or integer
                Default: False

            *equipoints*:
                If 'value' >0 the given data is interpolated linearly by equidistant
                steps in SmithAxes cooardinate space. Cannot be used with 'interpolate'
                enabled.
                Accepts: boolean
                Default: False


                
        See :meth:`matplotlib.axes.Axes.plot` for mor details
        '''
        # split input into real and imaginary part if complex
        new_args = ()
        for arg in args:
            # check if argument is a string or already an ndarray
            # if not, try to convert to an ndarray
            if not (isinstance(arg, str) or isinstance(arg, np.ndarray)):
                try:
                    if isinstance(arg, Iterable):
                        arg = np.array(arg)
                    elif isinstance(arg, Number):
                        arg = np.array([arg])
                except TypeError:
                    pass

            # if (converted) arg is an ndarray of complex type, split it
            if isinstance(arg, np.ndarray) and arg.dtype in [np.complex, np.complex128]:
                new_args += z_to_xy(arg)
            else:
                new_args += (arg,)

        # ensure newer plots are above older ones
        if 'zorder' not in kwargs:
            kwargs['zorder'] = self._current_zorder
            self._current_zorder += 0.001

        # extract or load non-matplotlib keyword arguments from parameters
        kwargs.setdefault("marker", self._get_key("plot.marker.default"))
        interpolate = kwargs.pop("interpolate", False)
        equipoints = kwargs.pop("equipoints", False)
        datatype = kwargs.pop("datatype", self._get_key("plot.default.datatype"))
        markerhack = kwargs.pop("markerhack", self._get_key("plot.marker.hack"))
        rotate_marker = kwargs.pop("rotate_marker", self._get_key("plot.marker.rotate"))

        if datatype not in self._datatypes:
            raise ValueError("'datatype' must be either '%s'" % ",".join(self._datatypes))

        if interpolate is not False:
            if equipoints > 0:
                raise ValueError("Interpolation is not available with equidistant markers")

            if interpolate is True:
                interpolate = self._get_key("plot.default.interpolation")
            elif interpolate < 0:
                raise ValueError("Interpolation is only for positive values possible!")

            if "markevery" in kwargs:
                mark = kwargs["markevery"]
                if isinstance(mark, Iterable):
                    mark = np.asarray(mark) * (interpolate + 1)
                else:
                    mark *= interpolate + 1
                kwargs["markevery"] = mark

        lines = Axes.plot(self, *new_args, **kwargs)
        for line in lines:
            cdata = smithhelper.xy_to_z(line.get_data())

            if datatype == SmithAxes.S_PARAMETER:
                z = self._moebius_inv_z(cdata)
            elif datatype == SmithAxes.Y_PARAMETER:
                z = 1 / cdata
            elif datatype == SmithAxes.Z_PARAMETER:
                z = cdata
            else:
                raise ValueError("'datatype' must be '%s', '%s' or '%s'" % (SmithAxes.S_PARAMETER, SmithAxes.Z_PARAMETER, SmithAxes.Y_PARAMETER))

            if self._normalize and datatype != SmithAxes.S_PARAMETER:
                z /= self._impedance

            line.set_data(z_to_xy(z))

            if interpolate or equipoints:
                z = self._moebius_z(*line.get_data())
                if len(z) > 1:
                    spline, t0 = fitpack.splprep(z_to_xy(z), s=0)
                    ilen = (interpolate + 1) * (len(t0) - 1) + 1
                    if equipoints == 1:
                        t = np.linspace(0, 1, ilen)
                    elif equipoints > 1:
                        t = np.linspace(0, 1, equipoints)
                    else:
                        t = np.zeros(ilen)
                        t[0], t[1:] = t0[0], np.concatenate([np.linspace(i0, i1, interpolate + 2)[1:] for i0, i1 in zip(t0[:-1], t0[1:])])

                    z = self._moebius_inv_z(*fitpack.splev(t, spline))
                    line.set_data(z_to_xy(z))

            if markerhack:
                self._hack_linedraw(line, rotate_marker)

        return lines

    def grid(self,
             b=None,
             which='major',
             fancy=None,
             dividers=None,
             threshold=None,
             **kwargs):
        '''
        Complete rewritten grid function. Gridlines are replaced with Arcs, 
        which reduces the amount of points to store and increases speed. The
        grid consist of a minor and major part, which can be drawn either as 
        standard with lines from axis to axis, or fancy with dynamic spacing
        and length adaption. 
        
        Keyword arguments:
        
            *b*:
                Enables or disables the selected grid.
                Accepts: boolean
                
            *which*:
                The grid to be drawn.
                Accepts: ['major', 'minor', 'both']
                
            *axis*:
                The axis to be drawn. x=real and y=imaginary
                Accepts: ['x', 'y', 'both']
                Note: if fancy is set, only 'both' is valid
                
            *fancy*:
                If set to 'True', draws the grid on the fancy way.
                Accepts: boolean

            *dividers*:
                Adaptive divisions for the minor fancy grid.
                Accepts: array with integers
                Note: has no effect on major and non-fancy grid
                
            *threshold*:
                Threshold for dynamic adaption of spacing and line length. Can
                be specified for both axis together or each seperatly.
                Accepts: float or (float, float)
                
            **kwargs*:
                Keyword arguments passed to the gridline creator.
                Note: Gridlines are :class:`matplotlib.patches.Patch` and does 
                not accept all arguments :class:`matplotlib.lines.Line2D` 
                accepts.
        '''
        assert which in ["both", "major", "minor"]
        assert fancy in [None, False, True]

        def get_kwargs(grid):
            kw = kwargs.copy()
            kw.setdefault('zorder', self._get_key("grid.zorder"))
            kw.setdefault("alpha", self._get_key("grid.alpha"))

            for key in ["linestyle", "linewidth", "color"]:
                if grid == "minor" and key == "linestyle":
                    if "linestyle" not in kw:
                        kw.setdefault("dash_capstyle", self._get_key("grid.minor.capstyle"))
                        kw.setdefault("dashes", self._get_key("grid.minor.dashes"))
                else:
                    kw.setdefault(key, self._get_key("grid.%s.%s" % (grid, key)))

            return kw

        def check_fancy(yticks):
            # checks if the imaginary axis is symetric
            len_y = (len(yticks) - 1) // 2
            if not (len(yticks) % 2 == 1 and (yticks[len_y:] + yticks[len_y::-1] < EPSILON).all()):
                raise ValueError(
                    "fancy minor grid is only supported for zero-symetric imaginary grid - e.g. ImagMaxNLocator")
            return yticks[len_y:]

        def split_threshold(threshold):
            if isinstance(threshold, tuple):
                thr_x, thr_y = threshold
            else:
                thr_x = thr_y = threshold

            assert thr_x > 0 and thr_y > 0

            return thr_x / 1000, thr_y / 1000

        def add_arc(ps, p0, p1, grid, type):
            assert grid in ["major", "minor"]
            assert type in ["real", "imag"]
            assert p0 != p1
            arcs = self._majorarcs if grid == "major" else self._minorarcs
            if grid == "minor":
                param["zorder"] -= 1e-9
            arcs.append((type, (ps, p0, p1), self._add_gridline(ps, p0, p1, type, **param)))

        def draw_nonfancy(grid):
            if grid == "major":
                xticks = self.xaxis.get_majorticklocs()
                yticks = self.yaxis.get_majorticklocs()
            else:
                xticks = self.xaxis.get_minorticklocs()
                yticks = self.yaxis.get_minorticklocs()

            xticks = np.round(xticks, 7)
            yticks = np.round(yticks, 7)

            for xs in xticks:
                if xs < self._near_inf:
                    add_arc(xs, -self._near_inf, self._inf, grid, "real")

            for ys in yticks:
                if abs(ys) < self._near_inf:
                    add_arc(ys, 0, self._inf, grid, "imag")

        # set fancy parameters
        if fancy is None:
            fancy_major = self._get_key("grid.major.fancy")
            fancy_minor = self._get_key("grid.minor.fancy")
        else:
            fancy_major = fancy_minor = fancy

        # check parameters
        if "axis" in kwargs and kwargs["axis"] != "both":
            raise ValueError("Only 'both' is a supported value for 'axis'")

        # plot major grid
        if which in ['both', 'major']:
            for _, _, arc in self._majorarcs:
                arc.remove()
            self._majorarcs = []

            if b:
                param = get_kwargs('major')
                if fancy_major:
                    xticks = np.sort(self.xaxis.get_majorticklocs())
                    yticks = np.sort(self.yaxis.get_majorticklocs())
                    assert len(xticks) > 0 and len(yticks) > 0
                    yticks = check_fancy(yticks)

                    if threshold is None:
                        threshold = self._get_key("grid.major.fancy.threshold")

                    thr_x, thr_y = split_threshold(threshold)

                    # draw the 0 line
                    add_arc(yticks[0], 0, self._inf, "major", "imag")

                    tmp_yticks = yticks.copy()
                    for xs in xticks:
                        k = 1
                        while k < len(tmp_yticks):
                            y0, y1 = tmp_yticks[k - 1:k + 1]
                            if abs(self._moebius_z(xs, y0) - self._moebius_z(xs, y1)) < thr_x:
                                add_arc(y1, 0, xs, "major", "imag")
                                add_arc(-y1, 0, xs, "major", "imag")
                                tmp_yticks = np.delete(tmp_yticks, k)
                            else:
                                k += 1

                    for i in range(1, len(yticks)):
                        y0, y1 = yticks[i - 1:i + 1]
                        k = 1
                        while k < len(xticks):
                            x0, x1 = xticks[k - 1:k + 1]
                            if abs(self._moebius_z(x0, y1) - self._moebius_z(x1, y1)) < thr_y:
                                add_arc(x1, -y0, y0, "major", "real")
                                xticks = np.delete(xticks, k)
                            else:
                                k += 1
                else:
                    draw_nonfancy("major")

        # plot minor grid
        if which in ['both', 'minor']:
            # remove the old grid
            for _, _, arc in self._minorarcs:
                arc.remove()
            self._minorarcs = []

            if b:
                param = get_kwargs("minor")

                if fancy_minor:
                    # 1. Step: get x/y grid data
                    xticks = np.sort(self.xaxis.get_majorticklocs())
                    yticks = np.sort(self.yaxis.get_majorticklocs())
                    assert len(xticks) > 0 and len(yticks) > 0
                    yticks = check_fancy(yticks)

                    if dividers is None:
                        dividers = self._get_key("grid.minor.fancy.dividers")
                    assert len(dividers) > 0
                    dividers = np.sort(dividers)

                    if threshold is None:
                        threshold = self._get_key("grid.minor.fancy.threshold")

                    thr_x, thr_y = split_threshold(threshold)
                    len_x, len_y = len(xticks) - 1, len(yticks) - 1

                    # 2. Step: calculate optimal gridspacing for each quadrant
                    d_mat = np.ones((len_x, len_y, 2))

                    # TODO: optimize spacing algorithm
                    for i in range(len_x):
                        for k in range(len_y):
                            x0, x1 = xticks[i:i + 2]
                            y0, y1 = yticks[k:k + 2]

                            xm = self.real_interp1d([x0, x1], 2)[1]
                            ym = self.imag_interp1d([y0, y1], 2)[1]

                            x_div = y_div = dividers[0]

                            for div in dividers[1:]:
                                if abs(self._moebius_z(x1 - (x1 - x0) / div, ym) - self._moebius_z(x1, ym)) > thr_x:
                                    x_div = div
                                else:
                                    break

                            for div in dividers[1:]:
                                if abs(self._moebius_z(xm, y1) - self._moebius_z(xm, y1 - (y1 - y0) / div)) > thr_y:
                                    y_div = div
                                else:
                                    break

                            d_mat[i, k] = [x_div, y_div]

                    # 3. Steps: optimize spacing
                    # ensure the x-spacing declines towards infinity
                    d_mat[:-1, 0, 0] = list(map(np.max, zip(d_mat[:-1, 0, 0], d_mat[1:, 0, 0])))

                    # find the values which are near (0, 0.5) on the plot
                    idx = np.searchsorted(xticks, self._moebius_inv_z(0)) + 1
                    idy = np.searchsorted(yticks, self._moebius_inv_z(1j).imag)

                    # extend the values around the center towards the border
                    if idx > idy:
                        for d in range(idy):
                            delta = idx - idy + d
                            d_mat[delta, :d + 1] = d_mat[:delta, d] = d_mat[delta, 0]
                    else:
                        for d in range(idx):
                            delta = idy - idx + d
                            d_mat[:d + 1, delta] = d_mat[d, :delta] = d_mat[d, 0]

                    # 4. Step: gather and optimize the lines
                    x_lines, y_lines = [], []

                    for i in range(len_x):
                        x0, x1 = xticks[i:i + 2]

                        for k in range(len_y):
                            y0, y1 = yticks[k:k + 2]

                            x_div, y_div = d_mat[i, k]
                            x_div = round(x_div)	# round because we need an integer otherwise have error. Mod from PMarsh Oct 6, 2020
                            y_div = round(y_div)	# round because we need an integer otherwise have error. Mod from PMarsh Oct 6, 2020

                            for xs in np.linspace(x0, x1, x_div + 1)[1:]:
                                x_lines.append([xs, y0, y1])
                                x_lines.append([xs, -y1, -y0])

                            for ys in np.linspace(y0, y1, y_div + 1)[1:]:
                                y_lines.append([ys, x0, x1])
                                y_lines.append([-ys, x0, x1])

                    # round values to prevent float inaccuarcy
                    x_lines = np.round(np.array(x_lines), 7)
                    y_lines = np.round(np.array(y_lines), 7)

                    # remove lines which overlap with the major grid
                    for tp, lines in [("real", x_lines), ("imag", y_lines)]:
                        for i in range(len(lines)):
                            ps, p0, p1 = lines[i]
                            if p0 > p1:
                                p0, p1 = p1, p0

                            for tq, (qs, q0, q1), _ in self._majorarcs:
                                if tp == tq and abs(ps - qs) < EPSILON and p1 > q0 and p0 < q1:
                                    lines[i, :] = np.nan
                                    break

                        lines = lines[~np.isnan(lines[:, 0])]
                        lines = lines[np.lexsort(lines[:, 1::-1].transpose())]

                        ps, p0, p1 = lines[0]
                        for qs, q0, q1 in lines[1:]:
                            if ps != qs or p1 != q0:
                                add_arc(ps, p0, p1, "minor", tp)
                                ps, p0, p1 = qs, q0, q1
                            else:
                                p1 = q1

                else:
                    draw_nonfancy("minor")

    def _hack_linedraw(self, line, rotate_marker=None):
        '''
        Modifies the draw method of a :class:`matplotlib.lines.Line2D` object
        to draw different stard and end marker.
        
        Keyword arguments:
            
            *line*:
                Line to be modified
                Accepts: Line2D 
                
            *rotate_marker*:
                If set, the end marker will be rotated in direction of their
                corresponding path. 
                Accepts: boolean
        '''
        assert isinstance(line, Line2D)

        def new_draw(self_line, renderer):
            def new_draw_markers(self_renderer, gc, marker_path, marker_trans, path, trans, rgbFace=None):
                # get the drawn path for determining the rotation angle
                line_vertices = self_line._get_transformed_path().get_fully_transformed_path().vertices
                vertices = path.vertices

                if len(vertices) == 1:
                    line_set = [[default_marker, vertices]]
                else:
                    if rotate_marker:
                        dx, dy = np.array(line_vertices[-1]) - np.array(line_vertices[-2])
                        end_rot = MarkerStyle(end.get_marker())
                        end_rot._transform += Affine2D().rotate(np.arctan2(dy, dx) - np.pi / 2)
                    else:
                        end_rot = end

                    if len(vertices) == 2:
                        line_set = [[start, vertices[0:1]], [end_rot, vertices[1:2]]]
                    else:
                        line_set = [[start, vertices[0:1]], [default_marker, vertices[1:-1]], [end_rot, vertices[-1:]]]

                for marker, points in line_set:
                    transform = marker.get_transform() + Affine2D().scale(self_line._markersize)
                    old_draw_markers(gc, marker.get_path(), transform, Path(points), trans, rgbFace)

            old_draw_markers = renderer.draw_markers
            renderer.draw_markers = MethodType(new_draw_markers, renderer)
            old_draw(renderer)
            renderer.draw_markers = old_draw_markers

        default_marker = line._marker
        # check if marker is set and visible
        if default_marker:
            start = MarkerStyle(self._get_key("plot.marker.start"))
            if start.get_marker() is None:
                start = default_marker

            end = MarkerStyle(self._get_key("plot.marker.end"))
            if end.get_marker() is None:
                end = default_marker

            if rotate_marker is None:
                rotate_marker = self._get_key("plot.marker.rotate")

            old_draw = line.draw
            line.draw = MethodType(new_draw, line)
            line._markerhacked = True

    def _add_gridline(self, ps, p0, p1, type, **kwargs):
        '''
        Add a gridline for a real axis circle.

        Keyword arguments:

            *ps*:
                Axis value
                Accepts: float

            *p0*:
                Start point 
                Accepts: float

            *p1*:
                End Point
                Accepts: float

            **kwargs*:
                Keywords passed to the arc creator
        '''
        assert type in ["real", "imag"]

        if type == "real":
            assert ps >= 0

            line = Line2D(2 * [ps], [p0, p1], **kwargs)
            line.get_path()._interpolation_steps = "x_gridline"
        else:
            assert 0 <= p0 < p1

            line = Line2D([p0, p1], 2 * [ps], **kwargs)

            if abs(ps) > EPSILON:
                line.get_path()._interpolation_steps = "y_gridline"

        return self.add_artist(line)

    class MoebiusTransform(Transform):
        '''
        Class for transforming points and paths to Smith Chart data space.
        '''
        input_dims = 2
        output_dims = 2
        is_separable = False

        def __init__(self, axes):
            assert isinstance(axes, SmithAxes)
            Transform.__init__(self)
            self._axes = axes

        def transform_non_affine(self, data):
            def _moebius_xy(_xy):
                return z_to_xy(self._axes._moebius_z(*_xy))

            if isinstance(data[0], Iterable):
                return list(map(_moebius_xy, data))
            else:
                return _moebius_xy(data)

        def transform_path_non_affine(self, path):
            vertices = path.vertices
            codes = path.codes

            linetype = path._interpolation_steps
            if linetype in ["x_gridline", "y_gridline"]:
                assert len(vertices) == 2

                x, y = np.array(list(zip(*vertices)))
                z = self._axes._moebius_z(x, y)

                if linetype == "x_gridline":
                    assert x[0] == x[1]
                    zm = 0.5 * (1 + self._axes._moebius_z(x[0]))
                else:
                    assert y[0] == y[1]
                    scale = 1j * (1 if self._axes._normalize else self._axes._impedance)
                    zm = 1 + scale / y[0]

                d = 2 * abs(zm - 1)
                ang0, ang1 = np.angle(z - zm, deg=True) % 360

                reverse = ang0 > ang1
                if reverse:
                    ang0, ang1 = ang1, ang0

                arc = Arc(z_to_xy(zm), d, d, theta1=ang0, theta2=ang1, transform=self._axes.transMoebius)
                arc._path = Path.arc(ang0, ang1)  # fix for Matplotlib 2.1+
                arc_path = arc.get_patch_transform().transform_path(arc.get_path())

                if reverse:
                    new_vertices = arc_path.vertices[::-1]
                else:
                    new_vertices = arc_path.vertices

                new_codes = arc_path.codes
            elif linetype == 1:
                new_vertices = self.transform_non_affine(vertices)
                new_codes = codes
            else:
                raise NotImplementedError("Value for 'path_interpolation' cannot be interpreted.")

            return Path(new_vertices, new_codes)

        def inverted(self):
            return SmithAxes.InvertedMoebiusTransform(self._axes)

    class InvertedMoebiusTransform(Transform):
        '''
        Inverse transformation for points and paths in Smith Chart data space.
        '''
        input_dims = 2
        output_dims = 2
        is_separable = False

        def __init__(self, axes):
            assert isinstance(axes, SmithAxes)
            Transform.__init__(self)
            self._axes = axes

        def transform_non_affine(self, data):
            def _moebius_inv_xy(_xy):
                return z_to_xy(self._axes._moebius_inv_z(*_xy))

            return list(map(_moebius_inv_xy, data))

        def inverted(self):
            return SmithAxes.MoebiusTransform(self._axes)

    class PolarTranslate(Transform):
        '''
        Transformation for translating points away from the center by a given
        padding. 
        
        Keyword arguments:
            
            *axes*:
                Parent :class:`SmithAxes`
                Accepts: SmithAxes instance
                
            *pad*:
                Distance to translate away from center for x and y values.
                
            *font_size*:
                y values are shiftet 0.5 * font_size further away.
        '''
        input_dims = 2
        output_dims = 2
        is_separable = False

        def __init__(self, axes, pad, font_size):
            Transform.__init__(self, shorthand_name=None)
            self.axes = axes
            self.pad = pad
            self.font_size = font_size

        def transform_non_affine(self, xy):
            def _translate(_xy):
                x, y = _xy
                ang = np.angle(complex(x - x0, y - y0))
                return x + np.cos(ang) * self.pad, y + np.sin(ang) * (self.pad + 0.5 * self.font_size)

            x0, y0 = self.axes.transAxes.transform([0.5, 0.5])
            if isinstance(xy[0], Iterable):
                return list(map(_translate, xy))
            else:
                return _translate(xy)

    class RealMaxNLocator(Locator):
        ''' 
        Locator for the real axis of a SmithAxes. Creates a nicely rounded
        spacing with maximum n values. The transformed center value is
        always included.
        
        Keyword arguments:
            
            *axes*:
                Parent SmithAxes
                Accepts: SmithAxes instance
                
            *n*:
                Maximum number of divisions
                Accepts: integer
                
            *precision*:
                Maximum number of significant decimals
                Accepts: integer
        '''

        def __init__(self, axes, n, precision=None):
            assert isinstance(axes, SmithAxes)
            assert n > 0

            Locator.__init__(self)
            self.steps = n
            if precision is None:
                self.precision = axes._get_key("grid.locator.precision")
            else:
                self.precision = precision
            assert self.precision > 0

            self.ticks = None
            self.axes = axes

        def __call__(self):
            if self.ticks is None:
                self.ticks = self.tick_values(0, self.axes._inf)
            return self.ticks

        def nice_round(self, num, down=True):
            # normalize to 'precision' decimals befor comma
            exp = np.ceil(np.log10(np.abs(num) + EPSILON))
            if exp < 1:  # fix for leading 0
                exp += 1
            norm = 10 ** -(exp - self.precision)

            num_normed = num * norm
            # increase precision by 0.5, if normed value is smaller than 1/3
            # of its decade range
            if num_normed < 3.3:
                norm *= 2
            # decrease precision by 1, if normed value is bigger than 1/2
            elif num_normed > 50:
                norm /= 10

            # select rounding function
            if not 1 < num_normed % 10 < 9:
                # round to nearest value, if last digit is 1 or 9
                if abs(num_normed % 10 - 1) < EPSILON:
                    num -= 0.5 / norm
                f_round = np.round
            else:
                f_round = np.floor if down else np.ceil

            return f_round(np.round(num * norm, 1)) / norm

        def tick_values(self, vmin, vmax):
            tmin, tmax = self.transform(vmin), self.transform(vmax)
            mean = self.transform(self.nice_round(self.invert(0.5 * (tmin + tmax))))

            result = [tmin, tmax, mean]
            d0 = abs(tmin - tmax) / (self.steps + 1)
            # calculate values above and below mean, adapt delta
            for sgn, side, end in [[1, False, tmax], [-1, True, tmin]]:
                d, d0 = d0, None
                last = mean
                while True:
                    new = last + d * sgn
                    if self.out_of_range(new) or abs(end - new) < d / 2:
                        break

                    # round new value to the next nice display value
                    new = self.transform(self.nice_round(self.invert(new), side))
                    d = abs(new - last)
                    if d0 is None:
                        d0 = d

                    last = new
                    result.append(last)

            return np.sort(self.invert(np.array(result)))

        def out_of_range(self, x):
            return abs(x) > 1

        def transform(self, x):
            return self.axes._moebius_z(x)

        def invert(self, x):
            return self.axes._moebius_inv_z(x)

    class ImagMaxNLocator(RealMaxNLocator):
        def __init__(self, axes, n, precision=None):
            SmithAxes.RealMaxNLocator.__init__(self, axes, n // 2, precision)

        def __call__(self):
            if self.ticks is None:
                tmp = self.tick_values(0, self.axes._inf)
                self.ticks = np.concatenate((-tmp[:0:-1], tmp))
            return self.ticks

        def out_of_range(self, x):
            return not 0 <= x <= np.pi

        def transform(self, x):
            return np.pi - np.angle(self.axes._moebius_z(x * 1j))

        def invert(self, x):
            return np.imag(-self.axes._moebius_inv_z(ang_to_c(np.pi + np.array(x))))

    class SmithAutoMinorLocator(AutoMinorLocator):
        '''
        AutoLocator for SmithAxes. Returns linear spaced intermediate ticks 
        depending on the major tickvalues. 
        
        Keyword arguments:
            
            *n*:
                Number of intermediate ticks
                Accepts: positive integer
        '''

        def __init__(self, n=4):
            assert isinstance(n, int) and n > 0
            AutoMinorLocator.__init__(self, n=n)
            self._ticks = None

        def __call__(self):
            if self._ticks is None:
                locs = self.axis.get_majorticklocs()
                self._ticks = np.concatenate(
                    [np.linspace(p0, p1, self.ndivs + 1)[1:-1] for (p0, p1) in zip(locs[:-1], locs[1:])])
            return self._ticks

    class RealFormatter(Formatter):
        ''' 
        Formatter for the real axis of a SmithAxes. Prints the numbers as 
        float and removes trailing zeros and commata. Special returns:
            '' for 0. 

        Keyword arguments:

            *axes*:
                Parent axes 
                Accepts: SmithAxes instance
        '''

        def __init__(self, axes, *args, **kwargs):
            assert isinstance(axes, SmithAxes)
            Formatter.__init__(self, *args, **kwargs)
            self._axes = axes

        def __call__(self, x, pos=None):
            if x < EPSILON or x > self._axes._near_inf:
                return ""
            else:
                return ('%f' % x).rstrip('0').rstrip('.')

    class ImagFormatter(RealFormatter):
        ''' 
        Formatter for the imaginary axis of a SmithAxes. Prints the numbers 
        as  float and removes trailing zeros and commata. Special returns:
            - '' for minus infinity 
            - 'symbol.infinity' from scParams for plus infinity
            - '0' for value near zero (prevents -0)
            
        Keyword arguments:

            *axes*:
                Parent axes 
                Accepts: SmithAxes instance
        '''

        def __call__(self, x, pos=None):
            if x < -self._axes._near_inf:
                return ""
            elif x > self._axes._near_inf:
                return self._axes._get_key("symbol.infinity")  # utf8 infinity symbol
            elif abs(x) < EPSILON:
                return "0"
            else:
                return ("%f" % x).rstrip('0').rstrip('.') + "j"

    # update docstrings for all methode not set
    for key, value in locals().copy().items():
        if isinstance(value, FunctionType):
            if value.__doc__ is None and hasattr(Axes, key):
                value.__doc__ = getattr(Axes, key).__doc__


__author__ = "Paul Staerke"
__copyright__ = "Copyright 2018, Paul Staerke"
__license__ = "BSD"
__version__ = "0.3"
__maintainer__ = "Paul Staerke"
__email__ = "paul.staerke@gmail.com"
__status__ = "Prototype"
