ps_plotter/pySmithPlot/smithplot/smithaxes.py
Luke 190ca4ded5 Inital comit.
Basic transfer function and tank impedance plotting
2018-07-17 18:33:39 -07:00

1544 lines
60 KiB
Python

# -*- 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]
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"