Source code for pysmithchart.transforms
"""Transform-related functionality for SmithAxes."""
import numpy as np
from matplotlib.cbook import simple_linear_interpolation as linear_interpolation
from matplotlib.transforms import Affine2D, BboxTransformTo
from pysmithchart.moebius_transform import MoebiusTransform
from pysmithchart.constants import SC_TWICE_INFINITY
from pysmithchart.polar_transform import PolarTranslate
from pysmithchart import utils
# Only export the mixin class, not imported symbols
__all__ = ["TransformMixin"]
[docs]
class TransformMixin:
"""Mixin class providing transform-related methods for SmithAxes."""
def _should_transform_coordinates(self, coord_system):
"""
Determine if coordinates should be transformed based on the coordinate system.
This is a unified helper to check whether we should apply Smith chart transformations.
Only 'data' coordinates should be transformed; all other coordinate systems
(axes, figure, etc.) should be left alone.
Args:
coord_system (str or Transform): The coordinate system specification.
Can be 'data', 'axes', 'figure', or a Transform object.
Returns:
bool: True if coordinates should be transformed, False otherwise.
"""
# If it's a string, check if it's 'data'
if isinstance(coord_system, str):
return coord_system == "data"
# If it's a Transform object, check if it's transData
# (or None, which defaults to transData)
if coord_system is None:
return True
# Check if it's the data transform
return coord_system is self.transData
def _transform_coordinates(self, x, y, domain):
"""
Transform coordinates from specified domain to impedance space.
This method delegates to the unified _apply_domain_transform() function
for consistent coordinate transformation across all plotting methods.
Args:
x (float or array): Real part of coordinate(s).
y (float or array): Imaginary part of coordinate(s).
domain (str): Coordinate type (Z_DOMAIN, Y_DOMAIN, R_DOMAIN, NORM_Z_DOMAIN).
Returns:
tuple: (x_impedance, y_impedance) in impedance space.
"""
# Delegate to unified transformation function
# Suppress S-parameter warnings for text/annotate (warn_s_parameter=False)
return self._apply_domain_transform(x, y, domain=domain, warn_s_parameter=False)
def _set_lim_and_transforms(self):
"""
Configure the axis limits and transformation pipelines for the chart.
This method defines and applies a series of transformations to map data
space, Möbius space, axes space, and drawing space.
Transformations:
- `transProjection`: Maps data space to Möbius space using a Möbius transformation.
- `transAffine`: Scales and translates Möbius space to fit axes space.
- `transDataToAxes`: Combines `transProjection` and `transAffine` to map data space to axes space.
- `transAxes`: Maps axes space to drawing space using the bounding box (`bbox`).
- `transMoebius`: Combines `transAffine` and `transAxes` to map Möbius space to drawing space.
- `transData`: Combines `transProjection` and `transMoebius` as data-to-drawing-space transform.
X-axis transformations:
- `_xaxis_pretransform`: Scales and centers the x-axis based on axis limits.
- `_xaxis_transform`: Combines `_xaxis_pretransform` and `transData` for full x-axis mapping.
- `_xaxis_text1_transform`: Adjusts x-axis label positions.
Y-axis transformations:
- `_yaxis_stretch`: Scales the y-axis based on axis limits.
- `_yaxis_correction`: Applies additional translation to the y-axis for label adjustments.
- `_yaxis_transform`: Combines `_yaxis_stretch` and `transData` for full y-axis mapping.
- `_yaxis_text1_transform`: Combines `_yaxis_stretch` and `_yaxis_correction` for y label position
"""
r = self._get_key("axes.radius")
self.transProjection = MoebiusTransform(self)
self.transAffine = Affine2D().scale(r, r).translate(0.5, 0.5)
self.transDataToAxes = self.transProjection + self.transAffine
self.transAxes = BboxTransformTo(self.bbox)
self.transMoebius = self.transAffine + self.transAxes
self.transData = self.transProjection + self.transMoebius
self._xaxis_pretransform = Affine2D().scale(1, 2 * SC_TWICE_INFINITY).translate(0, -SC_TWICE_INFINITY)
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(SC_TWICE_INFINITY, 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
[docs]
def get_xaxis_transform(self, which="grid"):
"""
Get the transformation for the x-axis.
Args:
which (str): Specifies which gridlines the transformation is for.
Defaults to "grid".
Returns:
Transform: The transformation object for the x-axis.
"""
assert which in ["tick1", "tick2", "grid"]
return self._xaxis_transform
[docs]
def get_xaxis_text1_transform(self, pad_points): # pylint: disable=unused-argument
"""
Get the transformation for text on the first x-axis.
Args:
pad_points (float): Padding in points.
Returns:
tuple: A tuple containing the transformation and text alignment information.
"""
return self._xaxis_text1_transform, "center", "center"
[docs]
def get_yaxis_transform(self, which="grid"):
"""
Get the transformation for the y-axis.
Args:
which (str): Specifies which gridlines the transformation is for.
Defaults to "grid".
Returns:
Transform: The transformation object for the y-axis.
"""
assert which in ["tick1", "tick2", "grid"]
return self._yaxis_transform
[docs]
def get_yaxis_text1_transform(self, pad_points):
"""
Get the transformation for text on the first y-axis.
Args:
pad_points (float): Padding in points.
Returns:
tuple: A tuple containing the transformation and text alignment information.
"""
if hasattr(self, "yaxis") and len(self.yaxis.majorTicks) > 0:
font_size = self.yaxis.majorTicks[0].label1.get_size()
else:
font_size = self._get_key("font.size")
offset = self._get_key("axes.ylabel.correction")[2]
return (
self._yaxis_text1_transform + PolarTranslate(self, pad=pad_points + offset, font_size=font_size),
"center",
"center",
)
[docs]
def moebius_z(self, *args, normalize=None):
"""
Apply the Möbius transformation to impedance values.
Converts impedance values (Z-parameters) to reflection coefficients (S-parameters)
using the Möbius transformation. Handles both single values and arrays.
Args:
*args: Either a single complex number/array or separate real and imaginary parts.
normalize (bool, optional): Whether to apply normalization. If None, uses
the axes' normalization setting.
Returns:
complex or ndarray: The transformed value(s) in S-parameter space.
Examples:
>>> z = 50 + 50j # Impedance
>>> s = ax.moebius_z(z) # Convert to S-parameter
"""
if normalize is None:
normalize = True
# Parse arguments to get z
if len(args) == 1:
z = args[0]
elif len(args) == 2:
z = args[0] + 1j * args[1]
else:
raise ValueError("Invalid number of arguments")
# Determine normalization value
z0 = self._get_key("axes.Z0")
norm = 1 if normalize else z0
# Call canonical implementation from utils
return utils.moebius_transform(z, norm=norm)
[docs]
def moebius_inv_z(self, *args, normalize=None):
"""
Apply the inverse Möbius transformation to reflection coefficients.
Converts reflection coefficients (S-parameters) back to impedance values
(Z-parameters) using the inverse Möbius transformation. Handles both single
values and arrays.
Args:
*args: Either a single complex number/array or separate real and imaginary parts.
normalize (bool, optional): Whether to apply normalization. If None, uses
the axes' normalization setting.
Returns:
complex or ndarray: The transformed value(s) in Z-parameter space.
Examples:
>>> s = 0.2 + 0.3j # Reflection coefficient
>>> z = ax.moebius_inv_z(s) # Convert to impedance
"""
if normalize is None:
normalize = True
# Parse arguments to get s
if len(args) == 1:
s = args[0]
elif len(args) == 2:
s = args[0] + 1j * args[1]
else:
raise ValueError("Invalid number of arguments")
# Determine normalization value
z0 = self._get_key("axes.Z0")
norm = 1 if normalize else z0
# Call canonical implementation from utils
return utils.moebius_inverse_transform(s, norm=norm)
[docs]
def real_interp1d(self, x, steps):
"""
Interpolate a vector of real values with evenly spaced points.
This method interpolates the given real values such that, after applying a Möbius
transformation with an imaginary part of 0, the resulting points are evenly spaced.
The result is mapped back to the original space using the inverse Möbius transformation.
Args:
x (iterable): Real values to interpolate.
steps (int): Interpolation steps between two points.
Returns: Interpolated real values.
"""
return self.moebius_inv_z(linear_interpolation(self.moebius_z(np.array(x)), steps))
[docs]
def imag_interp1d(self, y, steps):
"""
Interpolate a vector of imaginary values with evenly spaced points.
This method interpolates the given imaginary values such that, after applying
a Möbius transformation with a real part of 0, the resulting points are evenly spaced.
The result is mapped back to the original space using the inverse Möbius transformation.
Args:
y (iterable): Imaginary values to interpolate.
steps (int): Interpolation steps between two points.
Returns: Interpolated imaginary values.
"""
angs = np.angle(self.moebius_z(np.array(y) * 1j)) % (2 * np.pi)
i_angs = linear_interpolation(angs, steps)
return np.imag(self.moebius_inv_z(utils.ang_to_c(i_angs)))