1544 lines
60 KiB
Python
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"
|